RAG 系统
基于 LangGraph 构建检索增强生成 (Retrieval-Augmented Generation) 系统
📚 学习目标
学完这篇文章后,你将能够:
- 理解 RAG 的基本工作流程
- 构建包含“检索”、“评分”、“生成”等步骤的 LangGraph
- 实现“自适应 RAG”:根据检索质量决定是否重新检索
前置知识
在开始学习之前,建议先阅读:
你需要了解:
- 向量数据库和 Embedding 的基础概念
1 什么是 RAG?
RAG (Retrieval-Augmented Generation) 通过在生成回答前先检索相关文档,解决了 LLM 知识过时和幻觉的问题。
经典 RAG 流程
扩展版 RAG 流程(带评分与重写)
💡 说明
评分与重写能显著提高检索质量,但需要设置重试上限。
索引准备(高频被忽略的关键步骤)
RAG 的效果高度依赖索引质量,通常包括:
- 切分:将长文档切成合理 chunk
- 向量化:为每个 chunk 生成 embedding
- 存储:写入向量库,并保留 metadata
- 更新:增量更新,避免全量重建
const chunks = splitDocuments(docs, { chunkSize: 500, overlap: 50 });
await vectorStore.addDocuments(chunks);⚠️ 注意
chunk 太大导致召回不准,太小会降低语义完整性。
2 使用 LangGraph 构建 RAG
LangGraph 的优势在于它可以让 RAG 流程更加灵活,不仅仅是线性的“检索->生成”。
状态定义
import { Annotation } from '@langchain/langgraph';
import { Document } from '@langchain/core/documents';
const RAGState = Annotation.Root({
question: Annotation<string>(),
documents: Annotation<Document[]>({
reducer: (_current, update) => update,
default: () => [],
}),
answer: Annotation<string>(),
});📝 提醒
生产级 RAG 的“难点”不在 Graph,而在索引构建:chunking、embedding、向量库、metadata、增量更新。
节点实现
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
const model = new ChatOpenAI({ model: 'gpt-4o-mini', temperature: 0 });
const retrieveNode = async (state: typeof RAGState.State) => {
const docs = await vectorStore.similaritySearch(state.question, 4);
return { documents: docs };
};
const generateNode = async (state: typeof RAGState.State) => {
const context = state.documents
.map((d, i) => `[${i + 1}] ${d.pageContent}`)
.join('\n\n');
const messages = [
new SystemMessage('你是严谨的知识库助手。只基于给定 Context 回答,并引用编号。'),
new HumanMessage(`Context:\n${context}\n\nQuestion: ${state.question}`),
];
const response = await model.invoke(messages);
return { answer: String(response.content) };
};代码解析:
- 把每个文档加上编号
[1] [2] ...,你更容易在答案里做引用。 - System Prompt 明确“只基于 Context”,能显著降低幻觉。
3 进阶:自适应 RAG (Adaptive RAG)
我们可以引入一个“评分节点”来评估检索到的文档是否真的与问题相关。
流程图
代码逻辑
import { Command } from '@langchain/langgraph';
const gradeDocumentsNode = async (state) => {
const { documents, question } = state;
const validDocs = [];
for (const doc of documents) {
const score = await graderModel.invoke({...}); // 让 LLM 打分
if (score.isRelevant) validDocs.push(doc);
}
if (validDocs.length === 0) {
return new Command({ goto: "rewrite_query" });
}
return { documents: validDocs };
};为了避免死循环,建议加两层保护:
recursionLimit:给整个 Graph 一个硬上限retry_count:在 state 里累计重写次数,超过阈值后降级(例如改用 Web Search 工具)
4 查询重写 (Query Rewrite)
当检索结果不相关时,先重写问题再检索往往比直接“重复检索”更有效:
const rewriteNode = async (state: typeof RAGState.State) => {
const rewritePrompt = `请把问题改写为更适合检索的形式:${state.question}`;
const response = await model.invoke([new HumanMessage(rewritePrompt)]);
return { question: String(response.content) };
};💡 提示
重写问题时要保留关键实体和约束条件,否则检索会偏离原意。
5 文档过滤与去重
即使检索到了文档,也需要做过滤:
- 去除重复段落
- 移除过短或无意义的片段
- 保留高相关、高信息密度的内容
const filterDocs = (docs: Document[]) =>
docs.filter((d) => d.pageContent.length > 50);ℹ️ 说明
过滤步骤越干净,生成回答时幻觉越少。
6 答案验证(可选)
对高风险场景(医疗/法律)建议加“答案验证”节点:
const verifyNode = async (state: typeof RAGState.State) => {
const prompt = `请判断回答是否仅基于上下文:\n回答:${state.answer}`;
const verdict = await model.invoke([new HumanMessage(prompt)]);
return { answer: state.answer, isVerified: String(verdict.content).includes('是') };
};⚠️ 注意
验证失败时可以降级为“提示用户未找到可靠答案”。
7 引用与格式化
让模型输出带引用编号的答案更可靠:
const system = new SystemMessage(
'只基于 Context 回答,并在每条结论后标注来源编号,如 [1][2]。'
);8 多路检索与融合
在知识库较大时,可以并行检索多个索引,再合并结果:
const retrieveA = async (state: typeof RAGState.State) => ({
documents: await vectorStoreA.similaritySearch(state.question, 3),
});
const retrieveB = async (state: typeof RAGState.State) => ({
documents: await vectorStoreB.similaritySearch(state.question, 3),
});
const mergeDocs = (state: typeof RAGState.State) => ({
documents: dedupeDocuments(state.documents),
});💡 思路
先并行召回,再做去重与排序,能提高覆盖率与稳定性。
9 召回质量指标
在调参时可以关注:
- 覆盖率:检索到的文档是否包含关键答案
- 噪声比例:无关文档占比
- 响应时延:检索耗时是否可接受
ℹ️ 说明
指标数据可以作为“评分节点”的输入,驱动自适应 RAG。
10 缓存与重复问题
对于高频问题,可以缓存检索结果或最终答案:
const cacheKey = `rag:${state.question}`;
const cached = await cache.get(cacheKey);
if (cached) return { answer: cached };💡 提示
缓存能显著降低成本与延迟,但要注意缓存失效策略。
💡 练习题
-
思考题:在自适应 RAG 中,如果系统陷入了“检索 -> 不相关 -> 重写 -> 检索 -> 不相关”的死循环怎么办?
点击查看答案
增加
retry_count并设置上限,或使用recursionLimit强制结束并降级。 -
操作题:实现一个简单的 Corrective RAG (CRAG),当检索结果不佳时回退到 Web Search。
点击查看答案
在评分节点中检测
validDocs.length === 0时改为调用 web search 工具,并把结果写回 documents。 -
思考题:为什么需要在生成前对文档做去重和过滤?
点击查看答案
冗余和噪声会显著增加幻觉概率,过滤能提升答案准确性。
-
操作题:为答案添加引用编号,并确保只基于 Context 作答。
点击查看答案
在 SystemMessage 中约束输出格式,并在生成前为文档编号。
-
思考题:什么时候适合加“答案验证”节点?
点击查看答案
在高风险或对准确性要求高的场景(医疗/法律/财务)更适合。
✅ 总结
本章要点:
- LangGraph 让 RAG 可以拥有复杂的控制流(如循环、条件分支)。
- 自适应 RAG 通过引入反馈环路(Feedback Loop)显著提高了问答质量。
下一步:我们将探讨如何利用 LangGraph 进行数据分析。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。