装配式中文本地 RAG 框架 —— 基于 LangChain / LangGraph 生态,文档加载 → 分块 → 向量化 → 混合检索 → 重排 → 问答,一个配置开箱即用。
- 中文本地:默认本地中文 embedding(bge-base-zh)+ 嵌入式 LanceDB(零服务),问答模型走 OpenAI 兼容端点(deepseek 等均可)
- 评测驱动:内置 golden set + 检索指标(Recall@k / MRR)+ LLM-judge 生成指标(Faithfulness / Answer Relevance / 拒答正确率),每个效果改进都有量化验证
- 装配式:一个
createKnowledgeBase(config)开库,各环节「配置 / 已就绪实例 / 工厂」三选一 - 可嵌入:对外只见生态标准接口(
Document/Embeddings/VectorStore/BaseRetriever),RAG 图是CompiledStateGraph可作子图 - 依赖纯净:只依赖
@langchain/*生态 + 必要原生库,可直接嵌入你自己的 LangChain / LangGraph 项目
pnpm add ragkit
# 或 npm i ragkit@langchain/core 为 peer 依赖(生态惯例,避免版本重复实例);默认实现(LanceDB / 本地 embedding)随包安装,无需额外配置。
# 复制 .env.example 为 .env,填入 OPENAI_API_KEY / OPENAI_BASE_URL(问答模型,可用 deepseek 兼容端点)
# 首次运行会下载 bge 中文 embedding 模型(约 98MB)开一个知识库,全生命周期:
import "dotenv/config";
import { ChatOpenAI } from "@langchain/openai";
import { createKnowledgeBase } from "ragkit";
const model = new ChatOpenAI({
model: "deepseek-v4-flash",
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
});
const kb = await createKnowledgeBase({
source: ["./docs"], // 目录 / glob / loader 实例 / 工厂
model, // 必填:BaseChatModel
mode: "hybrid", // 默认即 hybrid;simple 为低成本档(多库路由见下节)
});
await kb.index(); // 入库(幂等:按内容 hash 判重,重跑不重复)
const { answer, citations } = await kb.ask("二手房交易按什么流程完成?");
// answer → "挂牌 → 看房 → 议价 → 签约 → 贷款申请 → 过户 → 交房 [1]"
// citations → [{ index:1, title, source, updatedAt }]
for await (const ev of kb.stream("中介费怎么收?")) {
// { type:"token", text } | { type:"done", citations }
}
await kb.remove("/abs/path/to/a.md"); // 文档级删除(同步内存索引)对话演示:
pnpm example:kb(灌入examples/data/*.md并问答)。
| mode | 检索管线 | 成本 | 适用 |
|---|---|---|---|
hybrid(默认) |
BM25 + 向量 RRF 融合 → 拒答 → 生成 | 中 | 开箱默认:词法 + 语义互补(默认叠加本地重排+分层,即「hybrid+rerank+hierarchical」) |
simple |
向量检索 → 拒答判断 → 生成 | 低 | 小库 / 快速验证 |
多知识库路由(agentic)是独立的 createAgenticKB 入口(见下节),不走单一 mode。
const kb = await createKnowledgeBase({
source: ["./docs"],
model,
// 默认即 hybrid + 本地重排 + 分层(评测最优);以下按需叠加增强
retriever: { k: 4, multiQuery: true }, // ← 显式叠加 query 改写
});检索准 = 混合检索(词法+语义) → 重排(精排) → 查询改写(召回补全) → 多库路由(归位)
生成好 = 引用溯源(citations) + 知识边界(不足即拒答) + 结构化上下文
| 环节 | 实现 | 默认 |
|---|---|---|
| 分块 | 中文 RecursiveCharacter(chunkSize 400 / overlap 50) |
✅ |
| 混合检索 | 字符二元组 BM25(零依赖,k1=1.5 / b=0.75)+ 向量,RRF 融合;bm25ScoreScaling 用 BM25 原始分缩放 RRF 贡献,破除长文档对词法侧的排名压制 |
✅ rrfK=30 |
| 融合去重 | 跨向量 / BM25 侧按 contentHash 合流(docKey 一致),融合后默认不折叠(同一来源多个 chunk 都保留,保证答案覆盖率) |
✅ |
| 重排 | 内置零依赖本地重排器(词法重叠 + 新鲜度信号:updatedAt 越新越靠前);可注入 bge-reranker 等交叉编码器 |
rerank: true(默认开启) |
| 查询改写 | LLM 生成语义相同、措辞不同的查询变体 → 多路检索 → 去重合并(覆盖口语化 vs 书面化 gap) | multiQuery: false |
| 拒答 | 问句关键词(中文 bigram + 英文词)对检索上下文的分语言组覆盖率 ≥ 0.35 才作答;否则诚实拒答 | ✅ |
100 例多行业非技术集实测(房产 / 销售 / 餐饮,含 LLM-judge 双采样)。hybrid+rerank+hierarchical 已是默认;下表对比展示该默认组合 vs 关闭增强的对照。
| 模式 | Recall@4 | MRR | ContextPrecision | ContextRecall | Faithfulness | Answer Relevance | 拒答正确率 |
|---|---|---|---|---|---|---|---|
| simple(纯向量) | 0.932 | 0.735 | 0.734 | 0.932 | 0.998 | 0.990 | 0.870 |
| hybrid | 0.979 | 0.895 | 0.895 | 0.979 | 0.995 | 0.985 | 0.960 |
| hybrid+rerank+hierarchical(默认) | 1.000 | 0.924 | 0.921 | 1.000 | 0.995 | 0.984 | 0.950 |
| agentic(3 行业子库路由,路由正确率 0.960) | 0.948 | 0.874 | 0.874 | 0.948 | 0.984 | 0.973 | 0.960 |
- hybrid 显著优于 simple(MRR 0.735→0.895、拒答正确率 0.870→0.960):BM25 词法兜住纯向量漏掉的精确命名与跨语言(SKU 编号 SP-1024、菜品英文名、楼盘名等)
- 默认组合让检索指标最优(Recall@4 / ContextRecall 全 1.000):分层折叠父块使相关源排位最前、上下文完整 → 因此默认即 hybrid+rerank+hierarchical
- 生成指标 LLM-judge 双采样取均值,抑制单次采样偶发噪声(详见
eval/metrics.ts);以本地复现为准 - 拒答正确率非满分:各模式 4-5 个稳定误拒为覆盖率阈值边界(问句含较多虚词稀释关键词覆盖率,检索本身命中、Recall@4=1),按需可调
coverageThreshold;agentic 另有 4 处跨行业词汇重叠导致的路由偏移(如"投诉处理时限"在销售库的售后规范中同样出现,实测路由到零售销售)
interface KnowledgeBase {
index(): Promise<void>; // 幂等入库
remove(source: string): Promise<void>; // 文档级删除
retrieve(question: string, filter?: MetadataFilter): Promise<Document[]>;
ask(question: string, filter?: MetadataFilter): Promise<{ answer, sourceDocuments, citations }>;
stream(question: string, filter?): AsyncGenerator<StreamEvent>;
store(): Promise<VectorStore>; // 底层向量库(懒加载)
retriever: BaseRetriever; // 标准检索器
graph: CompiledQAGraph; // 整张 RAG 图(可作子图)
}| 扩展点 | 默认值 | 注入方式 |
|---|---|---|
source loader |
loadDocuments(txt / md / json / glob,多编码 utf-8 / gbk,frontmatter 适配) |
loader 实例 / 工厂 |
splitter |
中文 RecursiveCharacter(400 / 50) | SplitterConfig / TextSplitter 实例 / 工厂 |
hierarchical |
开启(默认) | false 关闭 / HierarchicalSplitterConfig 自定义尺寸(分层检索,见下) |
embedder |
本地 bge-base-zh-v1.5(cls / q8,~98MB,query 侧自动套 BGE 检索指令前缀) |
EmbedderConfig / Embeddings 实例 / 工厂 |
store |
LanceDB 嵌入式(.lancedb/kb) |
{type:"lancedb",uri,tableName} / VectorStore 实例 / 工厂 |
retriever |
k=4 | 参数(vectorK / bm25K / rrfK / multiQuery / rerank / collapseBySource…) |
model |
无(必填) | BaseChatModel 实例 |
小块检索 / 父块生成:索引按 父块(800) → 小块(200) 切分,小块命中后自动折叠还原父块,生成上下文更完整、引用粒度更大。评测验证为检索指标最优(Recall@4 / ContextRecall 全 1.000,MRR / ContextPrecision 亦领先各模式)。
const kb = await createKnowledgeBase({
source: ["./docs"],
model,
// hierarchical 默认开启,无需配置;也可自定义尺寸:
hierarchical: { smallSize: 200, parentSize: 800 },
retriever: { k: 4, rerank: true }, // 可与 hybrid / rerank / multi-query 组合
});
// 想退回单层(400/50):hierarchical: false注意事项
- 旧索引不兼容:旧 400 块无
parentHash,升级后需用新 tableName 或先 dropTable 重建(旧块会透传降级、混库污染结果)。- 传自定义
splitter时自动退出分层(视为自定分块方案);除非再显式hierarchical: true。- 小块 metadata 内嵌父块全文(自包含还原),存储约 4 倍冗余、生成上下文约 2 倍 token 成本——本地嵌入式可接受,大语料需知悉。
- 父块上下文变大 → 覆盖率升高,拒答边界会漂移;必要时按需调
coverageThreshold。
存储解绑:默认 LanceDB 只是装配默认;传 Qdrant / pgvector / 内存库的
VectorStore实例即可,库内不内置多种向量库实现。
const kbHouse = await createKnowledgeBase({ source: ["./房产"], model, mode: "hybrid" });
const kbSales = await createKnowledgeBase({ source: ["./销售"], model, mode: "hybrid" });
const agentic = await createAgenticKB({
model,
knowledgeBases: [
{ name: "房产经纪", description: "二手房交易 / 门店运营 / 客户服务规范", kb: kbHouse },
{ name: "零售销售", description: "商品 SKU / 促销规则 / 售后处理规范", kb: kbSales },
],
});
await agentic.index(); // 委托全部子库(幂等)
const { answer } = await agentic.ask("中介费怎么收?"); // 自动路由到 房产经纪路由用 LLM 排序 + 证据竞争仲裁(不是盲信单次路由):
- LLM 给子库排序(按 name + description 相关度)
- 依次检索候选库并计算其对问句的覆盖率(同拒答信号)
- 覆盖率 ≥ 0.7 直接胜出(强证据早停);否则取覆盖率最高的库
- 全部覆盖率极低 → 回退首候选,拒答交给下游统一判断
好处:模型单次路由偶发选错库(尤其子库间词汇重叠时)不会致命——词汇重叠的错库覆盖率通常远低于真答案库,被仲裁掉。可配 route: { maxCandidates, strongCoverage }。
描述覆盖 = 路由可见性:
description是 LLM 路由的"目录"——库里有的主题没写进描述,路由就不知道去那个库找(评测曾因此把"系统入口"类问题路由错库,补齐描述即修)。给子库描述尽量枚举库内实际主题,是路由质量里性价比最高的一步。另注意:一个答案横跨多个子库的组合题目前超出"单库路由"能力(只保证命中首源目录),这类题请放进同一子库。
// ① 只要标准检索器,塞进自己的检索节点
const retriever = kb.retriever; // BaseRetriever
// ② 整张 RAG 图作子图,嵌进自己的 LangGraph 主图
builder.addNode("rag", kb.graph); // CompiledStateGraph
// ③ 项目已有 Qdrant / pgvector,只借用问答能力
const kb2 = await createKnowledgeBase({ store: myQdrantStore, model });pnpm eval # simple 基线
RAG_EVAL_MODE=hybrid pnpm eval # 混合检索对比
RAG_EVAL_MODE=hybrid+rerank pnpm eval
RAG_EVAL_MODE=hybrid+multi pnpm eval
RAG_EVAL_MODE=hybrid+rerank+multi pnpm eval
RAG_EVAL_MODE=hybrid+rerank+hierarchical pnpm eval # 分层检索(父块上下文)
RAG_EVAL_MODE=agentic pnpm eval # agentic(多库路由)检索指标含 Context Precision / Context Recall(RAGAS 口径,无模型);报告落盘
eval/report-<mode>.json(只增不改,旧报告可比)。
- golden set:
eval/dataset.ts,100 例(question, expectedAnswer, relevantSources),含 4 例拒答边界样本 - 检索指标:Recall@k / MRR(无模型、快)
- 生成指标:Faithfulness(忠实于检索上下文)/ Answer Relevance(切题)—— LLM-judge,双采样取均值
- 拒答正确率:应拒答且实际拒答 / 应作答且实际作答
- 报告落盘
eval/report-<mode>.json
| 命令 | 作用 |
|---|---|
pnpm typecheck / pnpm build |
类型检查 / 打包(tsup JS + tsc d.ts) |
pnpm test |
单元测试(96 例;e2e 默认跳过) |
RAG_E2E=1 pnpm test |
含真实 embedding + 向量库的端到端 |
pnpm eval |
评测(见上表) |
pnpm example:kb |
最小闭环演示(灌入 examples/data 并问答) |
- LanceDB 版本锁定:嵌入式零服务。
⚠️ LanceDB 0.23+ 停止发布 Intel Mac (darwin-x64) 原生二进制,本包锁定0.22.3(最后支持 x64 版本) - Embedding 镜像:本地 embedding 从 HuggingFace 下载,国内需
HF_ENDPOINT=https://hf-mirror.com。换 embedding 模型需重建索引 - 拒答阈值:覆盖率 0.35 可经
config.confidence调整(coverageThreshold/distanceThreshold)