【LangChain.js学习】 RAG(检索增强生成)完整实现解析

一、核心说明

你提供的代码是 LangChain.js 实现 RAG(Retrieval-Augmented Generation,检索增强生成) 的标准范例,核心是将「向量数据库的相似性检索」与「大模型生成」结合,确保回答严格基于知识库文本(避免模型幻觉)。整个流程实现了「用户提问→检索相关文本→拼接上下文→大模型回答」的端到端知识库问答能力。

二、代码核心逻辑拆解

整体执行流程

flowchart TD A[加载文档并分割] --> B[文本块存入内存向量库] C[用户提问] --> D[向量库检索Top1相关文本] D --> E[格式化检索结果为上下文] E --> F[拼接上下文+问题生成提示词] F --> G[打印最终提示词] G --> H[大模型基于上下文生成回答] H --> I[输出回答结果]

完整带注释代码(可直接运行)

typescript 复制代码
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/classic/text_splitter";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import { OpenAIEmbeddings } from "@langchain/openai";
import { ChatOpenAI } from "@langchain/openai";
import type { Document } from "@langchain/core/documents";
import type { ChatPromptValueInterface } from "@langchain/core/prompt_values";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableLambda, RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";

// ===================== 1. 定义提示词模板(约束回答范围) =====================
// 核心:强制大模型只能基于{context}(检索到的知识库文本)回答{input}(用户问题)
const promptTemplate = ChatPromptTemplate.fromMessages([
    ["system", "你是一个专业的问答机器人,你的回答必须基于上下文,不能编造信息"],
    ["human", "知识:{context},问题:{input}"],
]);

// ===================== 2. 初始化大模型(通义千问) =====================
// 通义千问旗舰版模型
const chatModel = new ChatOpenAI({
    model: "qwen-max",
    // 设为0减少随机性,保证回答精准
    temperature: 0,
    configuration: {
        // 阿里百炼OpenAI兼容接口
        baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
        // 替换为个人有效API Key
        apiKey: "[你的阿里百炼API Key]",
    },
});

// ===================== 3. 初始化嵌入模型+内存向量库 =====================
// 嵌入模型:将文本转为数值向量,用于相似性检索
const embeddingsModel = new OpenAIEmbeddings({
    // 通义千问文本嵌入模型(768维向量)
    model: "text-embedding-v2",
    configuration: {
        // 阿里百炼OpenAI兼容接口
        baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
        // 替换为个人有效API Key
        apiKey: "[你的阿里百炼API Key]",
    },
});

// 内存向量库:存储文本向量,支持相似性检索(程序重启后数据丢失)
const vectorStore = new MemoryVectorStore(embeddingsModel);

// ===================== 4. 加载并处理文档(存入向量库) =====================
// 加载TXT格式的知识库文档
const loader = new TextLoader("./data/data.txt");
const rawDocuments = await loader.load();

// 文本分割器:将长文本拆分为小文本块(适配嵌入模型上下文,提升检索精度)
const splitter = new RecursiveCharacterTextSplitter({
    // 每个文本块最大字符数
    chunkSize: 25,
    // 块间重叠字符(保证语义连贯)
    chunkOverlap: 5,
    // 优先按中文标点分割,避免语义断裂
    separators: [",", "。"],
});

// 分割文档并将文本块存入向量库(自动生成向量)
const splitDocuments = await splitter.splitDocuments(rawDocuments);
await vectorStore.addDocuments(splitDocuments);

// ===================== 5. 自定义Runnable组件(格式化/调试) =====================
// 5.1 格式化检索结果:将Document数组转为纯文本字符串(方便拼接提示词)
const formatDocuments = RunnableLambda.from((documents: Document[]): string => {
    return documents.map(doc => doc.pageContent).join("\n"); // 多个文本块换行分隔
});

// 5.2 打印提示词:调试用,输出最终传给大模型的完整提示词
const printPrompt = RunnableLambda.from((input: ChatPromptValueInterface): ChatPromptValueInterface => {
    console.log("【最终传给大模型的提示词】:\n", input.toString(), "\n");
    return input; // 透传提示词,不修改内容
});

// ===================== 6. 构建检索器(向量库检索入口) =====================
// asRetriever(1):检索Top1最相似的文本块(可调整数字扩大检索范围)
const retriever = vectorStore.asRetriever(1);

// ===================== 7. 构建RAG链式调用 =====================
// 定义用户问题
const question: string = "李娟的出生于哪里?";

// 链式调用流程:
// 1. 并行处理:context=检索结果格式化,input=用户问题透传
// 2. 拼接提示词模板
// 3. 打印提示词(调试)
// 4. 大模型生成回答
const chain = RunnableSequence.from([
    {
        // 检索→格式化
        context: retriever.pipe(formatDocuments),
        // 透传用户问题
        input: new RunnablePassthrough(),
    },
    // 拼接提示词
    promptTemplate,
    // 打印提示词
    printPrompt,
    // 大模型生成回答
    chatModel,
]);

// ===================== 8. 执行链式调用并输出结果 =====================
const result = await chain.invoke(question);
console.log("【最终回答】:", result.content);

三、核心组件详解

1. 关键Runnable组件

组件 作用 核心说明
RunnablePassthrough 透传数据 不修改输入,直接传递到下一个环节(此处用于保留用户问题)
RunnableLambda 自定义逻辑 封装格式化、打印等自定义函数,融入链式调用
RunnableSequence 链式执行 按顺序执行多个Runnable,前一个输出作为后一个输入
retriever.pipe(formatDocuments) 管道组合 检索结果直接传入格式化函数,简化嵌套调用

2. 核心代码段解析

(1)检索器构建

typescript 复制代码
// asRetriever(k):k表示返回最相似的k个文本块,此处设为1表示只取最相关的1条
// 检索器本质是封装了similaritySearch方法,更适配LangChain的Runnable体系
const retriever = vectorStore.asRetriever(1);

(2)链式调用数据流转

typescript 复制代码
{
    // 执行时:用户问题→检索器→Top1文本块→格式化字符串
    context: retriever.pipe(formatDocuments),
    // 执行时:直接等于用户问题(李娟的出生于哪里?)
    input: new RunnablePassthrough(),
}
  • 执行时会并行生成两个字段,作为参数传入promptTemplate,替换{context}{input}占位符。

(3)提示词最终效果

假设检索到的文本块是 李娟,1979年7月出生于新疆生产建设兵团,则最终提示词为:

makefile 复制代码
System: 你是一个专业的问答机器人,你的回答必须基于上下文,不能编造信息
Human: 知识:李娟,1979年7月出生于新疆生产建设兵团,问题:李娟的出生于哪里?

四、运行效果示例

输入输出示例

yaml 复制代码
【最终传给大模型的提示词】:
 System: 你是一个专业的问答机器人,你的回答必须基于上下文,不能编造信息
Human: 知识:李娟,1979年7月出生于新疆生产建设兵团,问题:李娟的出生于哪里? 

【最终回答】: 李娟于1979年7月出生于新疆生产建设兵团。

五、关键优化与注意事项

1. RAG核心优化点

  • 检索参数调整asRetriever(1)k 值可根据需求调整(如设为3,取Top3相关文本),平衡检索全面性和回答精准度;
  • 提示词模板优化:强化「仅基于给定上下文回答」的约束,例如添加「如果上下文没有相关信息,直接回复'未查询到相关内容',不要编造」;
  • 文本分割优化 :根据知识库文档类型调整 chunkSize(如长文档设为50-100字符),保证检索到的文本块语义完整;

2. RAG常见问题排查

  • 检索不到结果:检查文本分割是否过细、嵌入模型与向量库是否兼容、用户问题表述是否与文档语义匹配;
  • 模型回答偏离上下文 :优化提示词模板的约束性,或增大检索k值补充更多相关上下文;
  • 提示词拼接异常 :确保格式化函数正确处理空检索结果(如返回「无相关知识」),避免提示词缺失context字段。

六、总结

  1. 该代码是 LangChain.js 实现 RAG 的标准范式,核心是「检索+生成」结合,解决大模型幻觉问题;
  2. RunnableSequence 是链式调用的核心,通过 pipe/passthrough 实现灵活的数据流控制;
  3. 文本分割的「中文标点分隔」和「块重叠」是保证中文语义完整性,提升RAG检索精度的关键;
  4. 检索器的 k 值、提示词模板的约束性,直接决定RAG最终回答的精准度和可靠性。
相关推荐
兔子零10241 小时前
Star-Office-UI-Node 实战:从 0 到 1 接入 OpenClaw 的多 Agent 看板
前端·ai编程
helloweilei1 小时前
一文搞懂Nextjs中的Proxy
前端·next.js
wuhen_n2 小时前
Pinia状态管理原理:从响应式核心到源码实现
前端·javascript·vue.js
陆枫Larry2 小时前
小程序 scroll-view 设置 padding 右侧不生效?用一层包裹解决
前端
晴殇i2 小时前
CommonJS 与 ES6 模块引入的区别详解
前端·javascript·面试
Selicens2 小时前
git批量删除本地多余分支
前端·git·后端
wuhen_n2 小时前
KeepAlive:组件缓存实现深度解析
前端·javascript·vue.js
前端付豪3 小时前
Nest 项目小实践之图书展示和搜索
前端·node.js·nestjs
wuhen_n3 小时前
Vue Router与响应式系统的集成
前端·javascript·vue.js