从零构建私有知识库:RAG 检索系统在跨境业务中的落地实践
背景
跨境电商团队在运营过程中会积累大量碎片知识: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=关键词 提供检索。
经验总结
- 本地优先 — 小团队不要上来就堆 LangChain + Pinecone,先用现有工具(FastAPI + SQLite/LanceDB + BM25)跑起来
- 零 API 依赖 — 不需要任何外部 key 服务就能跑通检索,对基础设施受限的环境非常友好
- chunk 策略重要 — 产品 FAQ 和长文档的 chunk 策略不同,按段落分割效果最好,避免一刀切
- 冷启动快 — 17 篇文档的索引在 1 秒内加载完毕,完全满足实时对话场景
后续计划
- 给 LanceDB 表加上向量列,接入 MiniMax embedding 实现语义检索
- 将 Duopudun 735MB PDF 手册导入知识库
- 支持俄语(ru)关键词的 BM25 分词
- 给 kb-api 增加
/kb/ingest接口,支持热更新文档
项目代码位于 /root/kb/,服务状态:运行中(端口 8000)