常见用例

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. 把每个文档加上编号 [1] [2] ...,你更容易在答案里做引用。
  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 };
};

为了避免死循环,建议加两层保护:

  1. recursionLimit:给整个 Graph 一个硬上限
  2. 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 };

💡 提示

缓存能显著降低成本与延迟,但要注意缓存失效策略。


💡 练习题

  1. 思考题:在自适应 RAG 中,如果系统陷入了“检索 -> 不相关 -> 重写 -> 检索 -> 不相关”的死循环怎么办?

    点击查看答案

    增加 retry_count 并设置上限,或使用 recursionLimit 强制结束并降级。

  2. 操作题:实现一个简单的 Corrective RAG (CRAG),当检索结果不佳时回退到 Web Search。

    点击查看答案

    在评分节点中检测 validDocs.length === 0 时改为调用 web search 工具,并把结果写回 documents。

  3. 思考题:为什么需要在生成前对文档做去重和过滤?

    点击查看答案

    冗余和噪声会显著增加幻觉概率,过滤能提升答案准确性。

  4. 操作题:为答案添加引用编号,并确保只基于 Context 作答。

    点击查看答案

    在 SystemMessage 中约束输出格式,并在生成前为文档编号。

  5. 思考题:什么时候适合加“答案验证”节点?

    点击查看答案

    在高风险或对准确性要求高的场景(医疗/法律/财务)更适合。


✅ 总结

本章要点

  • LangGraph 让 RAG 可以拥有复杂的控制流(如循环、条件分支)。
  • 自适应 RAG 通过引入反馈环路(Feedback Loop)显著提高了问答质量。

下一步:我们将探讨如何利用 LangGraph 进行数据分析

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page