LangChain 二:输出结果定制与历史管理能力大揭秘 ✨
在 AI 应用开发中,我们常常面临两个核心问题:如何让大模型输出结构化的内容?如何让模型记住上下文实现多轮对话?今天就来深入探讨 LangChain 如何轻松解决这两个问题,结合实战代码带你掌握输出结果定制与历史管理的核心技能!
第一部分:JsonOutputParser 结果定制能力 🧩
当我们调用大模型时,经常会遇到这样的尴尬:模型返回的内容是一段自由文本,而我们需要的是结构化的 JSON 数据(比如提取用户信息、解析商品参数等)。这时候,JsonOutputParser 就成了我们的得力助手!
1. 安装相关依赖 📦
要使用输出定制能力,首先需要安装必要的依赖包:
bash
bash
pnpm i langchain @langchain/deepseek @langchain/core zod
💡 命令解析:
langchain:LangChain 核心框架,提供基础能力@langchain/deepseek:DeepSeek 模型的 LangChain 集成包@langchain/core:包含核心组件(如解析器、提示模板等)zod:用于定义和校验数据结构的工具库,是实现结构化输出的关键
2. 导入输出结果定制能力的关键模块 🔧
javascript
javascript
// 导入 JSON 输出解析器:负责将模型输出转换为 JSON 并校验格式
import { JsonOutputParser } from '@langchain/core/output_parsers';
// 导入 zod:用于定义数据结构规则(Schema),确保输出符合预期格式
import { z } from 'zod';
📝 模块作用详解:
JsonOutputParser:接收模型的原始输出文本,将其解析为 JSON 对象,同时会根据预定义的规则校验格式是否正确,若不符合则抛出错误zod:一个模式验证库,我们用它来定义「模型应该输出什么样的数据结构」(比如必须包含哪些字段、字段类型是什么)
3. Zod、Schema 规则与 Prompt 提示词的关系 🤝
要实现结构化输出,需要三个核心要素协同工作:
- Zod :工具本身,提供了定义数据结构的语法(比如
z.object()定义对象、z.string()定义字符串类型) - Schema :基于 Zod 定义的具体规则。例如:
const UserSchema = z.object({ name: z.string(), age: z.number() })就定义了一个用户信息的结构规则(必须有字符串类型的 name 和数字类型的 age) - Prompt 提示词:将 Schema 规则「翻译」成模型能理解的指令,告诉模型「请按照这个结构输出 JSON 数据」
三者形成完美闭环:用 Zod 定义 Schema 规则 → 在 Prompt 中告知模型规则 → 模型按规则输出 → JsonOutputParser 用 Zod 校验并解析为 JSON
4. 代码实战:实现结构化输出📝
javascript
javascript
// 导入所需模块
import { ChatDeepSeek } from '@langchain/deepseek'
import { ChatPromptTemplate } from '@langchain/core/prompts'
// 将 llm 生成的内容解析为 json 格式
import { JsonOutputParser } from '@langchain/core/output_parsers'
import { z } from 'zod'
import 'dotenv/config' // 你的key
// 1. 定义输出数据的结构规则(基于 Zod)
// 这里要求模型输出用户信息,包含姓名(字符串)和爱好(字符串数组)
const UserInfoSchema = z.object({
name: z.string().describe("用户的姓名"), // describe 用于在提示词中说明字段含义
hobbies: z.array(z.string()).describe("用户的爱好列表,每个爱好是字符串")
});
// 2. 创建 JSON 输出解析器,传入定义好的 Schema
const parser = new JsonOutputParser({ schema: UserInfoSchema });
// 3. 构建提示模板:告诉模型需要输出符合 Schema 的 JSON
// {format_instructions} 会被自动替换为 parser 生成的格式说明
const prompt = ChatPromptTemplate.fromMessages([
["system", "请解析用户输入的信息,按照以下格式输出 JSON:\n{format_instructions}"],
["human", "{input}"]
]);
// 4. 生成格式说明(由 parser 自动生成,告诉模型具体要怎么输出)
const formatInstructions = parser.getFormatInstructions();
// 5. 构建完整的调用链:提示模板 → 模型 → 解析器
const chain = prompt.pipe(model).pipe(parser);
// 6. 执行调用:解析用户输入的信息
const result = await chain.invoke({
input: "我叫李四,平时喜欢打篮球、听音乐,偶尔也会爬山",
format_instructions: formatInstructions
});
// 7. 输出结果(此时 result 已经是符合 Schema 的 JSON 对象)
console.log("解析结果:", result);
💡 代码关键步骤解析:
- 步骤 1:用
z.object()定义输出必须包含name(字符串)和hobbies(字符串数组),describe让模型更清楚每个字段的含义 - 步骤 2:
JsonOutputParser绑定 Schema 后,会自动校验模型输出是否符合规则 - 步骤 3:提示模板中必须包含
{format_instructions},否则模型不知道要输出 JSON - 步骤 6:调用时传入用户输入和格式说明,链会自动完成「提示生成 → 模型调用 → 解析校验」全流程
5. 效果展示 ✨
当我们输入「我叫李四,平时喜欢打篮球、听音乐,偶尔也会爬山」后,模型会输出标准的 JSON 结构:
json
json
{
"name": "李四",
"hobbies": ["打篮球", "听音乐", "爬山"]
}

再也不用手动处理字符串切割或正则匹配了!如果模型输出不符合格式(比如少了 hobbies 字段),JsonOutputParser 会直接抛出错误,方便我们快速排查问题。
第二部分:LLM 历史管理能力 📜
大模型的 API 调用和 HTTP 请求一样,本质是「无状态」的 ------ 每次调用都是独立的,模型不会记住上一次的对话内容。这就导致如果不做特殊处理,模型无法进行多轮对话(比如你先告诉它「我叫张三」,再问「我叫什么」,它会回答「不知道」)。
为什么需要历史管理?举个例子 🌰
如果直接调用模型,没有历史管理的话,会是这样的效果:
javascript
arduino
// 无历史管理的单轮调用
const model = new ChatDeepSeek({ model: 'deepseek-chat' });
// 第一轮:告诉模型姓名
const res1 = await model.invoke([["human", "我叫张三"]]);
console.log(res1.content); // 输出:你好,张三!很高兴认识你~
// 第二轮:询问姓名(模型已忘记)
const res2 = await model.invoke([["human", "我叫什么?"]]);
console.log(res2.content); // 输出:抱歉,我不知道你叫什么名字,可以告诉我吗?
输出结果:

这显然不符合我们对「对话」的预期。解决办法很简单:维护一份对话历史,每次调用模型时都把历史记录一起传给它。
1. 导入历史管理能力的关键模块 🔧
LangChain 提供了专门的组件来简化历史管理,核心模块如下:
javascript
javascript
// 提示模板工具:用于构建包含历史记录的对话提示
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
// 带历史的可运行对象:核心组件,自动管理对话历史与模型调用的结合
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
// 内存型历史存储:临时保存对话历史(进程结束后丢失)
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
📝 模块作用详解:
ChatPromptTemplate:构建对话模板,支持系统消息、用户消息、AI 消息等多角色定义MessagesPlaceholder:提示模板中的「历史消息占位符」,运行时会自动替换为真实的对话历史RunnableWithMessageHistory:将基础对话链与历史存储关联,自动完成「加载历史 → 拼接提示 → 调用模型 → 保存历史」的全流程InMemoryChatMessageHistory:内存中的历史存储容器,简单易用但不适合生产环境(数据不持久化)
2. 代码实战:实现多轮对话记忆📝
下面是基于 DeepSeek 模型的完整多轮对话代码:
javascript
javascript
// 第一步:初始化 DeepSeek 模型
const model = new ChatDeepSeek({
model: 'deepseek-chat', // 指定模型版本
temperature: 0, // 0 表示输出更稳定、确定(适合需要准确记忆的场景)
});
// 第二步:构建包含历史占位符的提示模板
const prompt = ChatPromptTemplate.fromMessages([
["system", "你是一个有记忆的助手,会记住之前的对话内容"], // 系统角色定义
new MessagesPlaceholder("history"), // 历史消息占位符(关键!会自动填充历史)
["human", "{input}"] // 用户当前输入的占位符
]);
// 第三步:构建基础对话链(提示模板 → 模型)
const runnable = prompt.pipe(model);
// 第四步:初始化内存型历史存储(保存对话记录)
const messageHistory = new InMemoryChatMessageHistory();
// 第五步:用 RunnableWithMessageHistory 包装对话链,实现历史管理
const chain = new RunnableWithMessageHistory({
runnable: runnable, // 基础对话链
getMessageHistory: async () => messageHistory, // 提供历史存储实例
inputMessagesKey: 'input', // 绑定用户输入的参数名(对应 prompt 中的 {input})
historyMessagesKey: 'history' // 绑定历史占位符的名称(对应 MessagesPlaceholder 的 "history")
});
// 第六步:执行多轮对话
// 第一轮:告知姓名和喜好
const res1 = await chain.invoke(
{ input: '我叫张三,喜欢喝白兰地' }, // 用户输入
{ configurable: { sessionId: 'makefriend' } } // 会话 ID(用于区分不同对话)
);
console.log('AI 回复 1:', res1.content); // 输出:你好,张三!看来你喜欢喝白兰地呢~
// 第二轮:验证是否记住姓名
const res2 = await chain.invoke(
{ input: '我叫什么?' }, // 基于历史的提问
{ configurable: { sessionId: 'makefriend' } } // 同一会话 ID,复用历史
);
console.log('AI 回复 2:', res2.content); // 输出:你叫张三呀,刚才你说喜欢喝白兰地呢~
💡 核心逻辑解析:
-
当调用
chain.invoke()时,RunnableWithMessageHistory会自动做三件事:- 从
messageHistory中加载该sessionId对应的历史记录 - 将历史记录填充到
MessagesPlaceholder("history")位置,与当前输入拼接成完整提示 - 调用模型后,自动将「用户输入」和「AI 回复」保存到
messageHistory中
- 从
-
sessionId的作用:如果有多个用户同时对话,用sessionId区分不同会话的历史记录(比如用户 A 和用户 B 分别有自己的sessionId)
3. 效果展示 ✨
运行上述代码后,我们会得到这样的输出:
AI 回复 1:你好,张三!看来你喜欢喝白兰地呢~
AI 回复 2:你叫张三呀,刚才你说喜欢喝白兰地呢~

可以看到,模型成功记住了第一轮对话中提到的姓名,实现了带记忆的多轮对话!
面试官可能会问这些问题 🤔
- 1.如何让 LLM 输出固定格式的 JSON 数据?
答:使用 LangChain 的 JsonOutputParser 结合 Zod 库。先用 Zod 定义数据结构(Schema),再通过提示模板告诉模型按 Schema 输出,最后用 JsonOutputParser 解析并校验结果,形成「定义规则→传达规则→校验规则」的闭环。
- 2.InMemoryChatMessageHistory 有什么局限性?生产环境中如何替代?
答:局限性是数据保存在内存中,进程重启后丢失,且不支持分布式部署。生产环境可使用持久化存储,比如 RedisChatMessageHistory(基于 Redis)、MongoDBChatMessageHistory(基于 MongoDB)等,这些组件会将历史记录存储到数据库中。
- 3.RunnableWithMessageHistory 的核心作用是什么?
答:它是连接「对话链」和「历史存储」的桥梁,自动完成历史记录的加载、提示拼接、模型调用和历史保存,简化了多轮对话的实现流程,无需手动管理历史记录的传递与存储。
- 4.多轮对话中,对话历史是如何传递给模型的?
答:通过 MessagesPlaceholder 在提示模板中预留位置,RunnableWithMessageHistory 会在调用时将历史记录填充到该位置,与系统消息、当前输入拼接成完整提示,再传递给模型,让模型「看到」所有历史对话。
- 5.如果对话历史很长,会有什么问题?如何解决?
答:问题是可能超过模型的上下文窗口长度(Token 限制),导致模型无法处理。解决办法:① 用 ConversationSummaryMemory 对历史进行总结压缩;② 按时间或重要性截断历史记录;③ 选择支持更长上下文窗口的模型。
结语 🌟
通过 LangChain 的 JsonOutputParser 和历史管理组件,我们可以轻松实现「结构化输出」和「多轮对话记忆」这两个核心功能。结构化输出让 AI 结果更易被程序处理,历史管理让对话更自然流畅 ------ 这两者结合起来,能大幅提升 AI 应用的实用性和用户体验。
希望这篇文章能帮你掌握 LangChain 的进阶技巧,快去动手实践吧!如果有任何问题,欢迎在评论区交流哦~🚀