入门篇:LangGraph 入门实战:从零搭建最简单的 RAG 问答系统

前言

有了向量知识库,下一步就是搭建真正的 RAG 问答系统。很多人一开始会写一堆零散的函数调用,但流程一复杂就难以维护。LangGraph 提供了基于状态机的工作流编排方式,让 RAG 的每一步都清晰可控。

本文是系列第二篇,我们基于上一篇建好的《天龙八部》Milvus 向量库,用 LangGraph 搭建最基础的 RAG 流程:提问 → 向量检索 → 大模型生成答案。虽然简单,但这是所有复杂 RAG 的雏形。

一、基础 RAG 的核心流程

最简单的 RAG 只有两个核心节点,形成一条直线工作流:

sql 复制代码
START → retrieve(检索) → generate(生成) → END
  1. retrieve 节点:接收用户问题,去 Milvus 向量库检索最相似的 Top-K 文本片段
  2. generate 节点:把检索到的片段作为上下文,交给 LLM 结合问题生成答案

二、状态设计

LangGraph 的核心是「状态」,所有节点共享同一份状态数据,节点执行后更新状态,传递给下一个节点。

我们定义的状态包含四个字段:

  • question:用户原始问题
  • k:检索返回的片段数量
  • documents:检索到的文档列表
  • generation:最终生成的答案
arduino 复制代码
const GraphState = Annotation.Root({
  question: Annotation,   // 用户问题
  k: Annotation,          // 检索 Top-K
  documents: Annotation,  // 检索到的文档
  generation: Annotation  // 生成的回答
});

三、完整代码逐段解析

1. 依赖与初始化

导入 LangGraph、Milvus 向量存储、大模型等依赖,初始化客户端。

arduino 复制代码
import "dotenv/config";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { Annotation, END, START, StateGraph } from '@langchain/langgraph';
import { Milvus } from '@langchain/community/vectorstores/milvus';

const COLLECTION_NAME = "ebook_collection";
const TOP_K = 5;

// 初始化大模型
const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  temperature: 0, // 设为 0,保证回答稳定准确
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  apiKey: process.env.OPENAI_API_KEY
});

// 初始化向量模型(与入库时保持一致)
const embeddings = new OpenAIEmbeddings({
  model: "text-embedding-v3",
  dimensions: 1024,
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
  apiKey: process.env.OPENAI_API_KEY
});

let vectorStore;

2. 检索节点

封装 Milvus 相似度搜索,返回带分数、章节信息的文档列表。

javascript 复制代码
async function retrieveRelevantContent(question, k = TOP_K) {
  try {
    const docsWithScores = await vectorStore.similaritySearchWithScore(question, k);
    return docsWithScores.map(([doc, score]) => ({
      score,
      content: doc.pageContent,
      id: doc.metadata?.id ?? "unknown",
      book_id: doc.metadata?.book_id ?? "未知",
      chapter_num: doc.metadata?.chapter_num ?? "未知",
      index: doc.metadata?.index ?? "未知"
    }));
  } catch(error) {
    console.error("检索内容时出错:", error.message);
    return [];
  }
}

const retrieveNode = async (state) => {
  const documents = await retrieveRelevantContent(state.question, state.k);
  return { documents }; // 更新状态中的 documents
}

3. 生成节点

把检索到的文档拼接成上下文,构造 Prompt 交给 LLM 流式生成答案。

javascript 复制代码
const generateNode = async (state) => {
  // 拼接上下文,标注片段序号和章节
  const context = state.documents
    .map((item, i) => `[片段 ${i+1}]
    章节: 第 ${item.chapter_num}章
    内容:${item.content}
    `).join("\n\n----------\n\n");

  const prompt = `
  你是一个专业的《天龙八部》小说助手。基于小说内容回答问题,用准确,详细的语言。
  请根据以下《天龙八部》小说片段内容回答问题:
  ${context}
  用户问题:${state.question}
  回答要求:
  1. 如果片段中有相关信息,请结合小说内容给出详细、准确的回答
  2. 可以综合多个片段的内容,提供完整的答案
  3. 如果片段中没有相关信息,请如实告知用户
  4. 回答要准确,符合小说的情节和人物设定
  5. 可以引用原文内容来支持你的回答
  AI 助手的回答:
  `;

  process.stdout.write("\n[AI回答(流式)]\n");
  let generation = "";
  const stream = await model.stream(prompt);
  
  // 流式输出,逐字打印
  for await (const chunk of stream) {
    const text = typeof chunk.content === "string" ? chunk.content : "";
    if (!text) continue;
    generation += text;
    process.stdout.write(text);
  }
  process.stdout.write("\n");

  return { generation };
}

4. 构建并编译图

把节点加入 StateGraph,定义边的连接关系,最后编译成可执行的图。

arduino 复制代码
const graph = new StateGraph(GraphState)
  .addNode("retrieve", retrieveNode)
  .addNode("generate", generateNode)
  .addEdge(START, "retrieve")    // 起点 → 检索
  .addEdge("retrieve", "generate") // 检索 → 生成
  .addEdge("generate", END)      // 生成 → 结束
  .compile();

5. 主函数运行

连接 Milvus 向量库,传入问题调用图,输出检索结果和最终回答。

javascript 复制代码
async function main() {
  const question = "阿朱的结局是什么?";
  const kArg = 5;

  console.log("连接到 Milvus...");
  // 连接已有的向量集合
  vectorStore = await Milvus.fromExistingCollection(embeddings, {
    collectionName: COLLECTION_NAME, 
    url: "localhost:19530",
    textField: "content",
    primaryField: "id",
    vectorField: "vector",
    indexCreateOptions: {
      metric_type: "COSINE",
      index_type: "HNSW",
      param: { M: 16, efConstruction: 200 },
      search_params: { ef: 64 }
    }
  });

  vectorStore.indexSearchParams = {
    metric_type: "COSINE",
    params: JSON.stringify({ef:64})
  }
  console.log('已连接');

  // 加载集合
  try {
    await vectorStore.client.loadCollection({collection_name: COLLECTION_NAME})
    console.log(`集合 ${COLLECTION_NAME} 已加载`);
  } catch(error) {
    if (!error.message.includes("already loaded")) throw error;
    console.log(`集合已处于加载状态`);
  }

  console.log("=".repeat(80));
  console.log(`问题:${question}`);
  console.log("=".repeat(80));

  // 调用图执行
  const result = await graph.invoke({
    question,
    k: Number.isFinite(kArg) ? kArg : TOP_K,
    documents: [],
    generation: ""
  });

  // 打印检索到的片段
  console.log("\n检索相关内容:");
  if (result.documents.length === 0) {
    console.log("未找到相关内容");
  } else {
    result.documents.forEach((item, i) => {
      console.log(`\n【片段 ${i+1} 相似度:】${item.score.toFixed(4)}`);
      console.log(`书籍: ${item.book_id}`);
      console.log(`章节: ${item.chapter_num}章`);
      console.log(`内容: ${item.content.substring(0, 200)}...`);
    })
  }
}

main().catch(err => console.error);

四、运行效果

执行后会先输出检索到的 5 个最相关片段,然后流式输出最终答案:

makefile 复制代码
================================================================================
问题:阿朱的结局是什么?
================================================================================

检索相关内容:

【片段 1 相似度:】0.8921
书籍: 1
章节: 23章
内容: 萧峰一掌击在阿朱胸口,阿朱身子直飞出去...

...(其余片段省略)

[AI回答(流式)]
阿朱最终死于萧峰的掌下。在小镜湖青石桥上,阿朱为了化解父亲段正淳与萧峰的仇怨,易容成段正淳的模样赴约,被悲愤中的萧峰一掌击中,最终死在萧峰怀中...

五、基础 RAG 的局限

这版 RAG 虽然能跑通,但有明显的不足:

  1. 所有问题都走检索:哪怕是「1+1 等于几」这种常识,也要浪费一次向量检索
  2. 单轮检索能力有限:遇到「段誉遇到的神仙姐姐画像是谁的弟子」这种多跳问题,一次检索很难命中答案
  3. 没有流程控制:无法根据中间结果动态调整策略

下一篇,我们就来解决第一个问题:给 RAG 加上智能路由,让常识问题直接回答,复杂问题才走检索。

相关推荐
YIAN2 小时前
前置篇:手把手用 Milvus + LangChain 搭建小说向量知识库,《天龙八部》EPUB 一键入库
javascript·langchain
YIAN2 小时前
进阶篇:RAG 优化第一步:给你的问答系统加上智能路由,实现双分支分流
javascript·langchain
Shockang2 小时前
LangChain 智能体治理实战:预算、权限与故障恢复
langchain
inksci2 小时前
发布Ai生成的html markdown 推荐
前端·javascript·html
是江迪呀2 小时前
向全世界售卖你的产品:做了一款教程引导插件
前端·javascript·后端
码艺-Alimjan4 小时前
维吾尔语 RTL 排版的隐形利器:`\u200F`
前端·javascript
Highcharts.js4 小时前
Highcharts 数据过滤实战指南:从基础实现到性能优化
javascript·性能优化·bi·highcharts·前端可视化·数据过滤·交互图表
一木 之林5 小时前
LangChain 第9章 RAG 与项目实战:文档切分、嵌入、Milvus 与客服知识库
人工智能·计算机视觉·langchain·milvus
何何____5 小时前
TypeScript 函数类型兼容性
前端·javascript·typescript