RAG 最小项目(检索 + 引用)
目标:做出“回答可追溯”的文档问答,不再是只会生成文本的黑盒聊天。
目录
目标与边界
如果把 AI Chat 比作“会说话”,那 RAG 更像“会查资料再说话”。
这一步对小白最关键的认知是:RAG 的重点不是模型本身,而是检索链路是否可靠。
只要检索和引用做得稳,回答可信度会明显提升。
- 必须有:切分、检索、回答带引用
- 可选有:重排、混合检索、权限过滤
- 先不做:大规模多租户索引治理
最小流程
这张流程图的重点是“先检索证据,再生成答案”,它和普通聊天最大的区别在于:
- 回答必须有依据,不允许模型纯猜
- 证据可以回溯到文档片段,用户可验证
- 你可以通过命中率和引用准确率做持续优化
数据结构建议
这段类型定义看似简单,但它决定了后续能否做“引用面板”和“问题排查”。
尤其是 meta 字段,不仅用于展示,还用于权限过滤、统计分析和定位原文。
type Chunk = {
id: string;
text: string;
vector: number[];
meta: { docId: string; docTitle: string; page?: number; section?: string };
};
实现骨架
入库
入库阶段做的事情是把“原始文档”转成“可检索资产”:
- 先切分:避免单条文本过长导致检索和上下文失真
- 再向量化:把文本映射到语义空间
- 最后写库:连同元数据一并保存,确保可追溯
下面代码体现的是最小可运行版本,后续可以再加去噪、标题感知切分、批处理重试等增强能力。
const chunks = split(text, 800, 120);
const vectors = await embed(chunks);
await vectorDB.upsert(
chunks.map((c, i) => ({
id: `${docId}-${i}`,
text: c,
vector: vectors[i],
meta: { docId, docTitle, page: i + 1 },
}))
);
检索与回答
这段代码要完成“检索 -> 组装证据 -> 调模型”三步:
- 检索:把用户问题转向量,找到最相关文本
- 组装:把证据编号成
[S1][S2],为引用做准备 - 回答:把问题和证据一起喂给模型,约束其基于证据输出
注意这里的 topK 和 slice(0, 4) 分别对应“召回范围”和“最终注入上下文”,这两个参数直接影响准确率和成本。
const qVec = (await embed([question]))[0];
const topK = await vectorDB.search(qVec, { topK: 10 });
const evidence = topK.slice(0, 4);
const prompt = buildPrompt({
question,
evidence: evidence.map((e, i) => `[S${i + 1}] ${e.text}`).join("\n"),
});
const answer = await callLLM(prompt);
return { answer, evidence };
预期结果是:同一个问题在不同文档版本下会返回不同证据,回答也会随证据变化;这就是 RAG 的价值。
评测与验收
- TopK 命中率:正确证据在检索结果中
- 引用准确率:引用文本确实支持结论
- 拒答准确率:无证据时不乱答
最小验收:
- 回答中能看到
[S1][S2]类引用 - 前端可展开查看对应证据片段
- 更换问题后,引用来源会变化