背景

跨境电商团队在运营过程中会积累大量碎片知识:VPS 配置、支付接入文档、物流方案、产品规格说明。这些内容分散在微信聊天、笔记软件、Confluence 页面里,每次排查问题都要翻半天。

AstroNexus 在 2026-04-21 完成了多语言站点重构后,顺手把 kb-api 启动了起来——这原本是一套”备用方案”,结果跑着跑着就成了日常离不开的工具。

核心需求

需求说明
业务问答俄罗斯用什么快递?YooKassa 怎么接?最硬的床垫是哪款?
本地部署不能依赖外部付费 API(小团队,经费有限)
快速响应Agent 在对话中调用,延迟不能高
可维护新文档能随时追加,不需要重建整个系统

架构设计

用户问题(如"俄罗斯VPS用什么配置?")

  kb_search(query, top_k)

┌─────────────────────────────────┐
│     kb_api.py (FastAPI)         │
│  Port 8000 /root/kb/            │
│                                 │
│  ① BM25 关键词初筛              │
│  ② LanceDB 向量索引             │
│  ③ 结果合并返回 JSON            │
└─────────────────────────────────┘

  OpenClaw Agent 获取检索片段

  综合回答(如:
  "根据知识库记录,俄罗斯主要用
   Selectel,莫斯科3-5天到货…")

存储层:LanceDB

LanceDB 是一个嵌向量数据库,直接以文件系统为存储后端,不需要启动独立服务。表结构:

kb 表
├── id: string        # 文档唯一标识
├── text: string      # 分块后的文本内容
└── metadata: string  # JSON 序列化的来源/分类信息

当前索引了 17 篇文档,总大小 128KB,查询延迟约 100ms。

检索层:BM25 + 向量双轨

kb-api 没有接入外部 Embedding API(保持零依赖),而是采用:

  • BM25 — 经典的倒排索引算法,关键词精确命中优先
  • fallback — 当前以 BM25 为唯一检索通道,向量维度在下一版本补入

这样设计的好处是:完全不依赖网络,无需 API Key,裸机即可运行

Embedding 脚本(备用)

embedding.py 使用 HuggingFace Inference API,模型为 sentence-transformers/all-MiniLM-L6-v2(384 维,免费额度充足)。如果不满足于纯 BM25,可以随时切换到向量模式。

知识库内容

/root/kb-docs/
├── infrastructure/     ← VPS、网络、CDN 等基础设施文档
├── business/           ← 产品说明、支付、物流等业务文档
└── knowledge/          ← 常见问题、联系人等知识

目前已录入:

  • duopudun-products.md — 产品列表与规格
  • mattress-product-faq.md — 床垫 FAQ(硬度等级/材质/尺寸/物流/支付)

接入 Agent 工作流

AstroNexus 通过 kb-rag skill 接入:

# OpenClaw skill 调用
kb_search(query="俄罗斯VPS配置", top_k=3)

# → 返回
{
  "query": "俄罗斯VPS配置",
  "count": 3,
  "results": [
    {
      "id": "mattress-product-faq_1",
      "text": "### 俄罗斯主要城市\n- **莫斯科**:3-5 个工作日\n- **CDEK**:覆盖 300+ 城市\n...",
      "score": 8.37
    },
    ...
  ]
}

Agent 拿到检索片段后,结合自己的知识综合回答,全程无需人工查阅文档。

关键代码:BM25 检索实现

def bm25_search(query: str, top_k: int = 5):
    bm25, ids = load_bm25_index()
    all_docs = get_all_docs()
    doc_map = {d["id"]: d for d in all_docs}

    query_tokens = tokenize(query)  # jieba 分词
    scores = bm25.get_scores(query_tokens)
    top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]

    results = []
    for idx in top_indices:
        doc_id = ids[idx]
        doc = doc_map.get(doc_id, {})
        results.append({
            "id": doc_id,
            "text": doc.get("text", ""),
            "metadata": json.loads(doc.get("metadata", "{}")),
            "score": round(float(scores[idx]), 4)
        })
    return results

BM25 索引通过 rank_bm25 库构建,索引文件持久化为 bm25_index.json,服务重启后无需重建。

启动与部署

服务通过 systemd 管理:

[Unit]
Description=KB-RAG API Service

[Service]
Type=simple
User=root
WorkingDirectory=/root/kb
Environment="MINIMAX_API_KEY=***"
ExecStart=/usr/bin/python3 /root/kb/kb_api.py
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

启动后自动监听 0.0.0.0:8000,通过 /kb/search?q=关键词 提供检索。

经验总结

  1. 本地优先 — 小团队不要上来就堆 LangChain + Pinecone,先用现有工具(FastAPI + SQLite/LanceDB + BM25)跑起来
  2. 零 API 依赖 — 不需要任何外部 key 服务就能跑通检索,对基础设施受限的环境非常友好
  3. chunk 策略重要 — 产品 FAQ 和长文档的 chunk 策略不同,按段落分割效果最好,避免一刀切
  4. 冷启动快 — 17 篇文档的索引在 1 秒内加载完毕,完全满足实时对话场景

后续计划

  • 给 LanceDB 表加上向量列,接入 MiniMax embedding 实现语义检索
  • 将 Duopudun 735MB PDF 手册导入知识库
  • 支持俄语(ru)关键词的 BM25 分词
  • 给 kb-api 增加 /kb/ingest 接口,支持热更新文档

项目代码位于 /root/kb/,服务状态:运行中(端口 8000)