跳到主要内容

RAG 最小项目(检索 + 引用)

目标:做出“回答可追溯”的文档问答,不再是只会生成文本的黑盒聊天。

目录

目标与边界

如果把 AI Chat 比作“会说话”,那 RAG 更像“会查资料再说话”。
这一步对小白最关键的认知是:RAG 的重点不是模型本身,而是检索链路是否可靠。
只要检索和引用做得稳,回答可信度会明显提升。

  • 必须有:切分、检索、回答带引用
  • 可选有:重排、混合检索、权限过滤
  • 先不做:大规模多租户索引治理

最小流程

这张流程图的重点是“先检索证据,再生成答案”,它和普通聊天最大的区别在于:

  • 回答必须有依据,不允许模型纯猜
  • 证据可以回溯到文档片段,用户可验证
  • 你可以通过命中率和引用准确率做持续优化

数据结构建议

这段类型定义看似简单,但它决定了后续能否做“引用面板”和“问题排查”。 尤其是 meta 字段,不仅用于展示,还用于权限过滤、统计分析和定位原文。

type Chunk = {
id: string;
text: string;
vector: number[];
meta: { docId: string; docTitle: string; page?: number; section?: string };
};

实现骨架

入库

入库阶段做的事情是把“原始文档”转成“可检索资产”:

  1. 先切分:避免单条文本过长导致检索和上下文失真
  2. 再向量化:把文本映射到语义空间
  3. 最后写库:连同元数据一并保存,确保可追溯

下面代码体现的是最小可运行版本,后续可以再加去噪、标题感知切分、批处理重试等增强能力。

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],为引用做准备
  • 回答:把问题和证据一起喂给模型,约束其基于证据输出

注意这里的 topKslice(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] 类引用
  • 前端可展开查看对应证据片段
  • 更换问题后,引用来源会变化