LangChain 初探:OutputParser 与 Memory 模块入门指南
在使用大语言模型(LLM)构建应用时,我们常常会遇到两个基本但关键的问题:
- 如何让模型返回结构化、可编程处理的数据?
- 如何让模型“记住”之前的对话内容,实现多轮交互?
LangChain 提供了两个非常实用的模块来分别解决这两个问题:OutputParser 和 Memory。本文将用通俗易懂的方式,结合代码示例,带你轻松入门这两个核心概念。
一、让模型输出“规规矩矩”的数据 —— OutputParser
在深入 OutputParser 之前,先让我们认识一位重要的“搭档”——Zod
先认识 Zod:给 JavaScript 加上“类型尺子”
Zod 是一个用于 TypeScript/JavaScript 的运行时类型验证库。你可以把它想象成一把“尺子”——不仅能在代码编写时(开发阶段)检查数据是否符合预期结构,还能在程序运行时(比如接收用户输入或 API 响应时)自动校验并抛出清晰的错误。
例如,你希望一段数据必须包含 name(字符串)和 age(数字),用 Zod 可以这样定义:
const UserSchema = z.object({
name: z.string(),
age: z.number().min(0)
});
如果传入 { name: "张三", age: -5 },Zod 会立刻指出:“年龄不能为负数!”——这种能力对于确保程序健壮性至关重要。
在 LangChain 中,Zod 被用来精确描述你期望 LLM 输出的 JSON 结构,从而为后续解析提供“蓝图”。
再看 OutputParser:把“自由发挥”变成“标准答案”
大语言模型天生擅长“自由写作”,但程序需要的是结构化、可预测的数据。比如,你希望模型返回一个包含“城市名、天气、温度”的对象,而不是一段散文。
这就是 OutputParser 的用武之地。它是 LangChain 中用于解析并验证模型输出的工具。其中最常用的是 JsonOutputParser,它能:
- 自动生成格式指令(告诉模型该怎么写);
- 自动解析模型返回的文本为 JSON 对象;
- 结合 Zod 进行类型校验,确保字段完整、类型正确。
当 Zod 遇上 OutputParser,就相当于你不仅给了模型一张填空表,还配了一个监考老师——既要求格式正确,又实时检查有没有乱填。
使用 JsonOutputParser + Zod 定义结构
LangChain 提供了 JsonOutputParser,配合 zod(一个强大的 TypeScript 类型验证库),我们可以精确地定义期望的输出结构。
首先安装依赖:
pnpm i zod
pnpm i @langchain/core
然后定义一个“前端概念卡片”的结构:
const FrontendConceptSchema = z.object({
name: z.string().describe('概念名称'),
core: z.string().describe('核心要点'),
useCase: z.array(z.string()).describe('常见使用场景'),
difficulty: z.enum(['简单', '中等', '复杂']).describe('学习难度')
});
这段代码就像给模型画了一张“填空表”:必须填“名称”、“核心”、“使用场景(数组)”和“难度(三选一)”。
接着,创建解析器并生成格式指令:
const jsonParser = new JsonOutputParser(FrontendConceptSchema);
const formatInstructions = jsonParser.getFormatInstructions();
getFormatInstructions() 会自动生成一段提示词,告诉模型:“你必须只返回符合这个 JSON 结构的内容,不能多也不能少”。
最后,把提示词、模型和解析器串成一条流水线(chain):
const prompt = PromptTemplate.fromTemplate(`
你是一个只会输出 JSON 的 API,不允许输出任何解释性文字。
⚠️ 你必须【只返回】符合以下 Schema 的 JSON:
- 不允许增加字段
- 不允许减少字段
- 字段名必须完全一致
{format_instructions}
前端概念:{topic}
`);
const chain = prompt.pipe(model).pipe(jsonParser);
const response = await chain.invoke({
topic: 'this关键字',
format_instructions: formatInstructions
});
console.log(response);
输出示例:
💡
这就像是你给外卖小哥一张标准订单表,他必须按表格填写菜品、数量、备注,不能自由发挥写诗。这样厨房(你的程序)才能准确处理。
通过这种方式,我们就能稳定地获取结构化数据,避免手动解析混乱的自然语言输出。
二、让模型“记住”你说过的话 —— Memory(对话记忆)
默认情况下,每次调用大模型都是“失忆”的。比如你先说“我叫张三”,再问“我叫什么名字?”,模型很可能回答“我不知道”。
这就像每次见面都假装第一次认识你——虽然礼貌,但没法聊天!
为了直观感受这一点,我们先看一个没有记忆功能的典型调用):
const model = new ChatDeepSeek({
model: 'deepseek-chat',
temperature: 0
});
const chain = prompt.pipe(model);
// 第一次提问
const res = await model.invoke('我叫张三');
console.log(res.content);
console.log('---------');
//第二次提问,模型失忆
const res2 = await model.invoke('我叫什么名字');
console.log(res2.content);
输出结果:
❌ 问题在哪?
虽然我们在同一个程序里连续调用了两次,但模型根本不知道这是“同一场对话”。因为每次invoke都只发送当前这条消息,历史记录被丢弃了。
引入 Memory:给对话装上“记事本”
记忆四件套:让对话真正连贯起来
1️⃣ ChatPromptTemplate.fromMessages:设计带“插槽”的提示词
我们使用 fromMessages 构建一个多段提示模板,并预留一个位置专门放历史记录:
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个有记忆的助手'],
['placeholder', '{history}'], // ← 这就是历史消息的“插槽”
['human', '{input}']
]);
📌
['placeholder', '{history}']不是普通文本,而是一个动态占位符。运行时,LangChain 会自动把真实的历史消息数组填进去。
2️⃣ InMemoryChatMessageHistory:临时存放对话的小本子
这是 LangChain 内置的内存级历史存储,适合开发或单用户场景:
const messageHistory = new InMemoryChatMessageHistory();
它内部维护一个 messages 数组,记录所有 HumanMessage 和 AIMessage。
⚠️ 注意:重启程序后记忆会丢失。生产环境可用 Redis、数据库等替代。
3️⃣ RunnableWithMessageHistory:给普通链“注入记忆能力”
这是核心魔法!它把一个普通的 runnable(比如 prompt.pipe(model))包装成支持自动读写历史的新对象:
const runnable = prompt.pipe(model);
const chainWithMemory = new RunnableWithMessageHistory({
runnable, // 原始链
getMessageHistory: async () => messageHistory, // 如何获取历史存储
inputMessagesKey: 'input', // 用户输入字段名
historyMessagesKey: 'history' // 历史消息在提示词中的变量名(对应 {history})
});
从此,每次调用 chainWithMemory.invoke(),LangChain 都会:
- 自动从
messageHistory读取历史; - 填入提示词的
{history}位置; - 调用模型;
- 把新的人类输入和 AI 回复自动追加到
messageHistory中。
4️⃣ configurable + sessionId:支持多用户并行对话
在真实应用中,你的 AI 助手不可能只和一个人聊天。张三问完问题,李四接着问——他们的对话必须互不干扰。LangChain 通过 configurable 和 sessionId 轻松实现这一点
await chainWithMemory.invoke(
{
input: '你好,我叫张三'
},
{
configurable: {
sessionId: 'makefriend'
}
}
);
const response = await chainWithMemory.invoke(
{
input: '我刚才说我的名字是什么?'
},
{
configurable: {
sessionId: 'makefriend'
}
}
);
console.log(response.content);
注意:sessionId 是关键!它是每个对话的唯一标识符,同一个会话 ID 才会共享同一段记忆。不同用户或不同对话应使用不同 ID,避免记忆混淆。
输出结果:
总结:LangChain 的两大基石
- OutputParser:确保模型输出结构清晰、类型安全的数据,适合对接前端、数据库或 API。
- Memory(如 RunnableWithMessageHistory) :赋予模型上下文记忆能力,支撑多轮对话、个性化交互等场景。
这两个模块看似独立,却是构建智能应用的“左膀右臂”。在此基础上,LangChain 还提供了更多高级模块:
- Retrieval(检索增强) :让模型基于你的私有文档回答问题;
- Agents(智能体) :让模型自主调用工具、做决策;
- Chains(链式编排) :组合多个步骤形成复杂工作流。
掌握 OutputParser 和 Memory,你就已经迈出了用 LangChain 构建可靠 AI 应用的第一步!接下来,可以尝试将两者结合:既有记忆,又能返回结构化结果——那才是真正的智能助手雏形。