https://bailian.console.aliyun.com/ 百炼为新用户提供北京地域专属的新人免费额度,用于体验模型调用。 未认证用户免费额度用完后无法继续使用,需要完成认证并充值后方能继续按量付费。
对于熟悉 TypeScript 的开发者来说,LangChain.js 是一个很实用的选择,它让你可以直接在 Node.js 环境下构建 AI 应用,而不必切换到 Python。
它的核心思想是提供一套标准化的"组件",让你像搭积木一样把它们组合起来。理解下面这几个核心概念,就能快速上手。
🧩 核心概念:一切皆 Runnable
LangChain.js 里几乎所有东西,比如模型、提示模板、解析器,都实现了一个统一的 Runnable 接口。这意味着它们都有统一的调用方式,可以互相连接:
invoke(input):单次调用,返回完整结果。stream(input):流式调用,逐块返回结果。batch(inputs):批量调用。
正是因为接口统一,你才能用管道(pipe)把它们串起来。
🔗 LCEL:用 .pipe() 连接组件
LCEL (LangChain Expression Language) 是组合组件的官方方式。它的核心是把上一个组件的输出,作为下一个组件的输入。
⚠️ 一个关键的坑 :Python 版本可以用 | 运算符来连接组件,但 JavaScript 不行 ,因为 JS 不支持运算符重载。在 TS 里必须使用 .pipe() 方法。
typescript
// 这是一个典型的链式结构:提示模板 → 模型 → 输出解析器
const chain = prompt.pipe(model).pipe(outputParser);
📝 快速上手代码示例
下面是一个最小可运行的例子,它把提示模板、模型和输出解析器串成了一条链。
typescript
// 1. 导入所需组件
import "dotenv/config"; // 加载 .env 文件中的 API Key
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
// 2. 定义提示模板
// 使用 fromMessages 可以灵活定义系统角色和用户输入
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个简洁的技术助手,回答控制在三句话以内。"],
["human", "{question}"], // {question} 是占位符
]);
// 3. 初始化模型
const model = new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
});
// 4. 定义输出解析器
// 它的作用是从模型的 AIMessage 对象里提取纯文本 content
const outputParser = new StringOutputParser();
// 5. 用 .pipe() 组合成链
const chain = prompt.pipe(model).pipe(outputParser);
// 6. 调用链并传入变量
const answer = await chain.invoke({
question: "用一句话解释什么是 Runnable?"
});
console.log(answer); // 输出的是纯字符串
🚀 下一步可以探索什么
打好基础后,你可以按这个顺序继续深入,这也是官方推荐的学习路径:
- 流式输出 (Streaming) :把最后的
.invoke换成.stream,就能实现类似 ChatGPT 的打字机效果。 - 函数调用与工具 (Tools):让模型能够调用你写的函数,比如查询天气、执行计算。
- 智能体 (Agents):组合模型和工具,让 AI 能够自己决定"下一步该做什么",比如先查资料再回答。
- 检索增强生成 (RAG):把你的文档(PDF、网页等)变成模型可以查询的知识库,让它基于你的私有数据回答。
LangChain.js 的灵活性很高,你可以从最简单的链开始,逐步增加复杂度。
调用工具
接下来我们学习函数调用与工具(Tools)。这是让大模型从"聊天"走向"干活"的关键一步。
核心思想:模型负责"决策",你的代码负责"执行"
首先要澄清一个关键概念:模型本身并不会真的去调用你的函数。
整个流程分为三步:
- 决策(模型) :模型看到你提供的工具描述后,判断是否需要调用工具。如果需要,它会生成一个结构化的
tool_calls对象,包含工具名称 和参数。 - 执行(你的代码):你编写的代码接收到这个对象,用参数去执行真正的函数。
- 响应(模型) :你把函数执行的结果(
ToolMessage)传回模型,模型据此生成最终的自然语言回答。
🛠️ 第一步:定义一个工具
使用 @langchain/core/tools 的 tool() 函数来定义工具。它需要三个关键信息:执行函数 、名称 、描述 ,以及用 Zod 定义的参数结构。
为什么必须用 Zod? Zod 不仅让 LangChain 能自动把参数结构转换成模型能理解的 JSON Schema,还能在调用时自动验证参数类型,省去手动写校验逻辑的麻烦。
typescript
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const calculatorTool = tool(
// 1. 执行函数:接收经过 Zod 验证的参数,返回字符串
async ({ operation, number1, number2 }) => {
if (operation === "add") return `${number1 + number2}`;
if (operation === "subtract") return `${number1 - number2}`;
if (operation === "multiply") return `${number1 * number2}`;
if (operation === "divide") return `${number1 / number2}`;
throw new Error("Invalid operation");
},
// 2. 工具配置
{
name: "calculator",
// 描述至关重要:模型完全依赖它来决定何时调用这个工具
description: "Can perform mathematical operations.",
schema: z.object({
operation: z
.enum(["add", "subtract", "multiply", "divide"])
.describe("The type of operation to execute."),
number1: z.number().describe("The first number to operate on."),
number2: z.number().describe("The second number to operate on."),
}),
}
);
description 和每个参数的 .describe() 都直接传递给模型,是模型判断"该不该用"和"怎么用"的唯一依据。
🔗 第二步:绑定工具到模型
定义好工具后,用 bindTools() 告诉模型有哪些工具可用。你可以绑定多个工具,模型会自动根据用户问题选择最合适的一个。
typescript
import { ChatOpenAI } from "@langchain/openai";
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const modelWithTools = model.bindTools([calculatorTool]);
⚙️ 第三步:处理工具调用(核心模式)
绑定工具后,模型不会直接返回文本,而是可能返回 tool_calls。你需要手动执行工具,把结果送回模型。
一个常见的坑 :bindTools 本身不会执行 工具,它只让模型"知道"工具的存在。tool_calls 的解析和执行需要你自己写逻辑。
typescript
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import "dotenv/config"; // 加载 .env 文件中的 API Key
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
const calculatorTool = tool(
// 1. 执行函数:接收经过 Zod 验证的参数,返回字符串
async ({ operation, number1, number2 }) => {
if (operation === "add") return `${number1 + number2}`;
if (operation === "subtract") return `${number1 - number2}`;
if (operation === "multiply") return `${number1 * number2}`;
if (operation === "divide") return `${number1 / number2}`;
throw new Error("Invalid operation");
},
// 2. 工具配置
{
name: "calculator",
// 描述至关重要:模型完全依赖它来决定何时调用这个工具[citation:1]
description: "Can perform mathematical operations.",
schema: z.object({
operation: z
.enum(["add", "subtract", "multiply", "divide"])
.describe("The type of operation to execute."),
number1: z.number().describe("The first number to operate on."),
number2: z.number().describe("The second number to operate on."),
}),
}
);
const model = new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
});
const modelWithTools = model.bindTools([calculatorTool]);
// 1. 用户提问,模型决策
const response = await modelWithTools.invoke([
new HumanMessage("What is 3 multiplied by 12?")
]);
// 2. 检查模型是否决定调用工具
if (response.tool_calls && response.tool_calls.length > 0) {
const toolCall = response.tool_calls[0];
console.log("模型选择的工具:", toolCall.name); // "calculator"
console.log("模型生成的参数:", toolCall.args); // { operation: "multiply", number1: 3, number2: 12 }
// 3. 你的代码执行工具
const toolResult = await calculatorTool.invoke(toolCall);
// 4. calculatorTool.invoke 已经返回了带 tool_call_id 的 ToolMessage
const finalResponse = await modelWithTools.invoke([
new HumanMessage("What is 3 multiplied by 12?"),
response, // 模型自己的 tool_calls 响应
toolResult,
]);
console.log(finalResponse.content); // "36"
}
🎛️ 进阶控制:tool_choice 参数
默认情况下模型自行决定是否调用工具("auto")。你可以通过 tool_choice 强制干预:
| 选项 | 含义 |
|---|---|
"auto" |
默认,模型自行决定 |
"none" |
禁止调用任何工具 |
"any" |
必须调用至少一个工具 |
| 指定工具名 | 强制调用某个特定工具 |
typescript
// 强制使用 calculator 工具
const forcedResponse = await modelWithTools.invoke(
"What is 3 × 12?",
{ tool_choice: "calculator" }
);
📦 补充:结构化输出 withStructuredOutput
如果你想让模型直接返回 符合特定结构的 JSON 对象(而不是先去调工具再处理),可以用 withStructuredOutput。它底层也是利用 function calling 机制来强制模型按 schema 输出。
typescript
const sentimentSchema = z.object({
sentiment: z.enum(["正面", "负面", "中性"]),
score: z.number().min(0).max(1),
});
const structuredModel = model.withStructuredOutput(sentimentSchema, {
method: "functionCalling", // 对中转网关兼容性更好
});
const result = await structuredModel.invoke("分析这条评论:这个产品太棒了!");
// result 已经是解析好的 JS 对象,带 TS 类型
console.log(result.sentiment); // "正面"
一个实用经验 :如果你使用中转网关,withStructuredOutput 最好显式加上 method: 'functionCalling'。默认的 json_schema 模式对中转支持参差不齐,可能导致 JSON.parse 报错。
🚀 下一步
掌握了工具调用后,你就可以进入 Agent(智能体) 了。Agent 本质上就是一个循环:让模型不断"决策 → 调用工具 → 观察结果 → 再决策",直到完成任务。你可以用 createAgent 快速搭建一个 Agent,把定义好的工具传进去,它就会自动处理多轮工具调用的循环。
搭建一个 Agent
接下来我们用 createAgent 搭建你的第一个 Agent。它本质上就是把之前学的"模型决策 → 工具执行"循环自动化了,而且 LangChain v1 的 API 非常简洁。
认识 createAgent
createAgent 是 LangChain v1 中构建 Agent 的标准方式,取代了旧版的 createReactAgent。你只需要告诉它三件事:用什么模型、有哪些工具、扮演什么角色,它就会自动处理"模型决定调用工具 → 执行工具 → 把结果送回模型 → 生成最终回答"的完整循环。
第一步:最小可运行的 Agent
我们直接复用上一课定义的 calculatorTool,只需几行代码就能让 Agent 自主完成计算任务。
typescript
import "dotenv/config";
import { createAgent, tool } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
// 1. 定义工具(和上一课一样)
const calculatorTool = tool(
async ({ operation, number1, number2 }) => {
if (operation === "add") return `${number1 + number2}`;
if (operation === "multiply") return `${number1 * number2}`;
// ... 其他操作
},
{
name: "calculator",
description: "执行数学运算:加法、乘法等。",
schema: z.object({
operation: z.enum(["add", "multiply"]),
number1: z.number(),
number2: z.number(),
}),
}
);
// 2. 创建 Agent
const agent = createAgent({
model: new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
}),
tools: [calculatorTool],
systemPrompt: "你是一个数学助手,使用工具完成计算。",
});
// 3. 调用 Agent
const result = await agent.invoke({
messages: [{ role: "user", content: "帮我算一下 12 乘以 8 等于多少?" }],
});
// 4. 获取最终回答
console.log(result.messages.at(-1)?.content);
// 输出:"12 乘以 8 等于 96。"
关键区别 :在上一课中,你需要手动检查 tool_calls、执行工具、构造 ToolMessage、再次调用模型。createAgent 把这些全部封装了。你只需要调用一次 agent.invoke,它内部会自动完成所有轮次,直到模型给出最终文本回答。
第二步:让 Agent 处理多工具任务
Agent 真正的价值在于自主规划。给它多个工具,它能自己决定先用哪个、再用哪个。
假设你有一个"搜索工具"和一个"计算器工具",用户可以问:"帮我搜一下当前比特币价格,然后乘以 100 算算需要多少钱。"
typescript
import { createAgent, tool } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import "dotenv/config";
// 搜索工具(模拟)
const searchTool = tool(
async ({ query }) => {
// 实际项目中这里会调用搜索 API
if (query.includes("比特币")) {
return "当前比特币价格:$67,432";
}
return `搜索 "${query}" 的结果...`;
},
{
name: "web_search",
description: "搜索互联网获取实时信息,如价格、新闻等。",
schema: z.object({
query: z.string().describe("搜索关键词"),
}),
}
);
// 计算器工具
const calculatorTool = tool(
async ({ expression }) => {
// 注意:生产环境不要直接用 eval
return eval(expression).toString();
},
{
name: "calculator",
description: "计算数学表达式,支持 +、-、*、/。",
schema: z.object({
expression: z.string().describe("要计算的数学表达式,如 '67432 * 100'"),
}),
}
);
const agent = createAgent({
model: new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
}),
tools: [searchTool, calculatorTool],
systemPrompt: "你是一个助手,可以搜索信息并进行计算。",
});
// 一个需要多步操作的任务
const result = await agent.invoke({
messages: [{
role: "user",
content: "搜索比特币当前价格,然后告诉我 100 个比特币值多少钱。",
}],
});
console.log(result.messages.at(-1)?.content);
// Agent 会先调用 web_search,再调用 calculator,最后给出答案。
第三步:用中间件增加控制
createAgent 最强大的特性是中间件(Middleware)。中间件让你能在 Agent 循环的特定节点插入自定义逻辑,比如限制工具调用次数、拦截错误、动态切换模型等。
一个实用的例子是限制最大工具调用次数,防止 Agent 陷入无限循环:
typescript
import { createAgent, toolCallLimitMiddleware } from "langchain";
const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-4o-mini" }),
tools: [searchTool, calculatorTool],
middleware: [
toolCallLimitMiddleware({ runLimit: 5 }), // 最多调用 5 次工具
],
});
另一个常见场景是错误处理中间件。当工具执行失败时,返回友好的错误信息而不是让整个 Agent 崩溃:
typescript
import { createAgent, createMiddleware, ToolMessage } from "langchain";
const handleToolErrors = createMiddleware({
name: "HandleToolErrors",
wrapToolCall: async (request, handler) => {
try {
return await handler(request);
} catch (error) {
return new ToolMessage({
content: `工具执行出错:${error.message}。请尝试其他方法。`,
tool_call_id: request.toolCall.id,
});
}
},
});
const agent = createAgent({
model: new ChatOpenAI({ model: "gpt-4o-mini" }),
tools: [searchTool, calculatorTool],
middleware: [handleToolErrors],
});
调试建议
Agent 的"思考过程"对调试很重要。你可以用 streamEvents 观察 Agent 每一步在做什么:
typescript
const stream = await agent.streamEvents(
{ messages: [{ role: "user", content: "搜索比特币价格并乘以 100" }] },
{ version: "v3" }
);
for await (const snapshot of stream.values) {
const latest = snapshot.messages.at(-1);
if (latest?.tool_calls?.length) {
console.log(`Agent 正在调用: ${latest.tool_calls.map(tc => tc.name).join(", ")}`);
}
}
关键要点
| 概念 | 作用 |
|---|---|
createAgent |
一站式创建 Agent,自动处理工具调用循环 |
tools |
传给 Agent 的工具列表,它自主决定何时用哪个 |
systemPrompt |
定义 Agent 的角色和行为准则 |
middleware |
在 Agent 循环中插入自定义逻辑(限制、错误处理、日志等) |
Agent 的核心思想是委托 :你把工具和角色定义清楚,剩下的"什么时候用哪个工具、用几次"交给模型自己决定。这也是为什么工具的描述质量至关重要------模型完全依赖 description 来判断该不该调用某个工具。
检索增强生成(RAG)
检索增强生成(RAG)的核心思路很直接:让模型在回答前,先去你的文档库里"查资料",然后基于查到的内容来生成答案。
之前 Agent 让模型学会了"用工具",RAG 则是给模型装上了一个专属知识库,让它能回答私有数据的问题。
为什么需要 RAG
直接问模型关于你公司内部文档的问题,它要么答不上来,要么会"编造"一个看似合理的错误答案。因为模型训练时并没有见过你的私有文档。
RAG 解决了三个关键问题:
- 私有知识:让模型能基于你的内部文档、手册、邮件来回答。
- 实时性:知识库更新后,答案也能同步更新,不受模型训练时间限制。
- 可溯源:可以要求模型回答时附上引用的来源,方便核实。
RAG 的四个核心步骤
一个基础的 RAG 流程分为四个阶段:
- 加载与分割:把长文档(PDF、Word)读进来,切成小块(Chunk)。
- 嵌入与存储:把每个小块通过 Embedding 模型转成向量,存进向量数据库。
- 检索:用户提问时,把问题也转成向量,去数据库里找出最相似的几个文档块。
- 生成:把"检索到的文档块"和"用户问题"一起交给模型,让它生成答案。
🛠️ 第一步:加载与分割文档
RAG 的第一步是把文档处理成适合检索的"小块"。
为什么必须分割? 因为模型上下文窗口有限,且整篇文档作为一个向量,语义太杂,检索精度低。
LangChain 提供了多种分割器,推荐从 RecursiveCharacterTextSplitter 开始,它会优先保持段落完整,再逐级向下切分。
your-document.txt
员工健康保险覆盖范围:
1. 眼科检查:每年一次,全额报销。
2. 牙科:每年 2000 元额度。
3. 心理咨询:每年 10 次免费。
请假制度:
员工每年享有 15 天带薪年假,需提前 3 天申请。
typescript
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { readFileSync } from "fs";
// 1. 读取文档
const text = readFileSync("./your-document.txt", "utf8");
// 2. 创建分割器
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500, // 每个块约 500 字符
chunkOverlap: 50, // 相邻块重叠 50 字符,避免语义断裂
});
// 3. 执行分割
const chunks = await splitter.splitText(text);
console.log(`文档被切成了 ${chunks.length} 个块`);
chunkOverlap 很重要:它让相邻块之间有重叠内容,避免关键信息刚好被切在边界上而丢失。
📦 第二步:嵌入与向量存储
分割后,需要把文本块转成向量并存储,以便后续做语义检索。
typescript
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
// 1. 创建嵌入模型(把文本转为向量)
const embeddings = new OpenAIEmbeddings({
modelName: "text-embedding-3-small",
});
// 2. 创建内存向量库(仅用于演示/测试)
const vectorStore = new MemoryVectorStore(embeddings);
// 3. 把文本块加入向量库(自动生成向量)
await vectorStore.addDocuments(
chunks.map(chunk => ({ pageContent: chunk }))
);
存储选择 :MemoryVectorStore 适合本地测试。生产环境可以考虑 Azure AI Search 、Pinecone 或 Chroma。切换向量库时,LangChain 的接口基本一致,不用重写检索逻辑。
🔍 第三步:检索与生成(组合成链)
这是 RAG 的核心:把检索到的文档和用户问题一起交给模型。
typescript
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnablePassthrough } from "@langchain/core/runnables";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { readFileSync } from "fs";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
import "dotenv/config";
// 🛠️ 第一步:加载与分割文档
// 1. 读取文档
const text = readFileSync("./your-document.txt", "utf8");
// 2. 创建分割器
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500, // 每个块约 500 字符
chunkOverlap: 50, // 相邻块重叠 50 字符,避免语义断裂
});
// 3. 执行分割
const chunks = await splitter.splitText(text);
console.log(`文档被切成了 ${chunks.length} 个块`);
// 📦 第二步:嵌入与向量存储
// 1. 创建嵌入模型(把文本转为向量)
const embeddings = new OpenAIEmbeddings({
modelName: "text-embedding-v3",
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
});
// 2. 创建内存向量库(仅用于演示/测试)
const vectorStore = new MemoryVectorStore(embeddings);
// 3. 把文本块加入向量库(自动生成向量)
await vectorStore.addDocuments(
chunks.map(chunk => ({ pageContent: chunk }))
);
// 🔍 第三步:检索与生成(组合成链)
const model = new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
})
// 1. 创建检索器
const retriever = vectorStore.asRetriever({ k: 3 }); // 返回最相似的 3 个块
// 2. 定义提示模板(必须把检索到的上下文传给模型)
const prompt = ChatPromptTemplate.fromMessages([
["system", "根据以下上下文回答问题。如果上下文中没有答案,就说不知道。\n\n上下文:\n{context}"],
["human", "{question}"],
]);
// 3. 用 LCEL 组合成 RAG 链
const ragChain = RunnablePassthrough.assign({
// 这一步:用问题去检索,把结果格式化成字符串
context: async ({ question }) => {
const docs = await retriever.invoke(question);
return docs.map(d => d.pageContent).join("\n\n");
},
}).pipe(prompt).pipe(model).pipe(new StringOutputParser());
// 4. 调用
const answer = await ragChain.invoke({
question: "员工健康保险覆盖眼科检查吗?",
});
console.log(answer);
关键点 :RunnablePassthrough.assign 让原始问题既能用于检索,又能保留下来传给提示模板。链的执行顺序是:问题 → 检索 → 得到上下文 → 提示模板(问题 + 上下文)→ 模型 → 答案。
⚠️ 常见问题与调试
RAG 的"坑"通常出在检索环节,而不是生成环节。如果答案不理想,按这个顺序排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索到的文档不相关 | Embedding 模型不适合你的语言/领域 | 尝试换嵌入模型 |
| 关键信息不在检索结果里 | 分割策略不好,关键信息被切碎 | 调整 chunkSize 或换分割器 |
| 答案不准确 | 模型没用好上下文,或提示词太弱 | 在提示词里强调"只根据上下文回答" |
| 检索结果太少/太多 | k 值不合适 |
调整 asRetriever({ k: 3 }) 里的 k |
一个实用的调试技巧:先单独测试检索 。用几个你知道答案的问题,看 retriever.invoke(question) 返回的文档块里有没有正确的信息。如果检索这一步就错了,后面无论模型多强都救不回来。
与 Agent 的关系
RAG 可以和之前学的 Agent 结合使用。你可以把"文档检索"封装成一个工具 ,让 Agent 自己决定什么时候去查资料。这就是 Agentic RAG 的思路:Agent 可以判断问题是否需要查文档,查完后再决定是否需要进一步处理。
不过,建议先用最基础的 RAG 链跑通全流程,等理解了数据流转,再升级到 Agentic RAG。
记忆
在 LangChain 中,Agent 的记忆处理已经演进为更清晰的两层架构:短期记忆 (Checkpointer)和长期记忆 (Store)。旧的 ConversationBufferMemory 等 API 已被视为遗留方案,新项目不建议再使用。
🧠 短期记忆:用 Checkpointer 维持会话连续性
短期记忆的核心是线程(Thread)隔离 。每个独立的对话(比如一个用户的一次会话)对应一个 thread_id,Checkpointer 会自动保存和恢复该线程内的完整对话历史(包括消息、工具调用等状态)。
为什么需要它? 如果没有它,你的 Agent 在两次 invoke 之间会"失忆"。有了 Checkpointer,你只需在调用时传入相同的 thread_id,Agent 就能自动"想起"之前的对话内容。
基础用法:
js
import { MemorySaver } from "@langchain/langgraph";
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";
import "dotenv/config";
import { InMemoryStore } from "@langchain/langgraph";
// 1. 创建 Checkpointer(内存版,重启后数据丢失)
const checkpointer = new MemorySaver();
// 1. 创建 Store
const store = new InMemoryStore();
// 2. 创建 Agent 时传入
const agent = createAgent({
model: new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
}),
checkpointer, // 关键:指定检查点保存器
store, // 长期记忆存储
});
// 3. 调用时传入 thread_id,同一线程的对话会自动关联
const config = { configurable: { thread_id: "user-123-session-1" } };
await agent.invoke(
{ messages: [{ role: "user", content: "我叫 Bob" }] },
config
);
// 第二次调用,Agent 能记住名字
const result = await agent.invoke(
{ messages: [{ role: "user", content: "我叫什么?" }] },
config // 相同的 thread_id
);
// result 会包含 "你叫 Bob"
console.log(result.messages.at(-1)?.content);
生产环境注意 :MemorySaver 将数据存在内存中,进程重启后所有对话历史都会丢失 。生产环境务必换成持久化后端,如 PostgresSaver、MongoDBSaver 或 SqliteSaver。
🌐 长期记忆:用 Store 跨会话记住用户偏好
长期记忆不局限于单个线程 ,它用于存储那些应该"永远记住"的信息,比如用户偏好、个人资料、累积的知识等。Store 将数据存储为 JSON 文档,通过命名空间(namespace)和键(key) 来组织。
基础用法:
js
import { createAgent, tool, } from "langchain";
import { InMemoryStore } from "@langchain/langgraph";
import * as z from "zod";
import { ChatOpenAI } from "@langchain/openai";
import "dotenv/config";
// 1. 初始化 store 并预存一条记忆
const store = new InMemoryStore();
const namespaceForMemory = ["user_123", "memories"];
await store.put(namespaceForMemory, "food_pref", {
food_preference: "I like pizza",
});
// 2. 定义一个读取记忆的工具
const contextSchema = z.object({ userId: z.string() });
const getFoodPreference = tool(
async (_, runtime) => {
const userId = runtime.context.userId;
const namespace = [userId, "memories"];
// 使用 search 获取该用户的所有记忆
const memories = await runtime.store.search(namespace);
if (!memories || memories.length === 0) {
return "未找到用户偏好";
}
// 3. 关键一步:从 memories 中提取 value 并组合成可读的文本
// 这里取最新的一条,提取它的 value 对象中的 food_preference 字段
const latest = memories[memories.length - 1];
const pref = latest.value.food_preference;
return `用户喜欢的食物是:${pref}`;
},
{
name: "get_food_preference",
description: "查找用户喜欢的食物偏好",
schema: z.object({}),
}
);
const agent = createAgent({
model: new ChatOpenAI({
modelName: "qwen-max",
temperature: 0.7,
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
},
}),
tools: [getFoodPreference],
contextSchema,
store, // 传入 store,工具内才能通过 runtime.store 访问
});
const result = await agent.invoke(
{ messages: [{ role: "user", content: "我喜欢的食物?" }] },
{ context: { userId: "user_123" } },
);
console.log(result.messages.at(-1)?.content);