LangChain 二:输出结果定制与历史管理能力详解

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 会自动做三件事:

    1. messageHistory 中加载该 sessionId 对应的历史记录
    2. 将历史记录填充到 MessagesPlaceholder("history") 位置,与当前输入拼接成完整提示
    3. 调用模型后,自动将「用户输入」和「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 的进阶技巧,快去动手实践吧!如果有任何问题,欢迎在评论区交流哦~🚀

相关推荐
明月_清风2 小时前
不止是代码堆放:带你全面掌握 Monorepo 核心技术与选型
前端
Aliex_git2 小时前
Vue2 - Watch 侦听器源码理解
前端·javascript·vue.js·笔记·学习
你疯了抱抱我2 小时前
【QQ】空间说说批量删除脚本(不用任何额外插件,打开F12控制台即可使用)
开发语言·前端·javascript
进击的野人2 小时前
Vuex 详解:现代 Vue.js 应用的状态管理方案
前端·vue.js·前端框架
未知原色2 小时前
前端工程师转型AI的优势与挑战
前端·人工智能
鹏北海2 小时前
Single-SPA 学习总结
前端·javascript·微服务
想学后端的前端工程师2 小时前
【CSS高级技巧与动画实战指南:打造炫酷的用户体验】
前端·css·ux
_码力全开_2 小时前
第一章 html5 第一节 HTML5入门基础
前端·javascript·css·html·css3·html5
KLW752 小时前
vue 绑定动态样式
前端·javascript·vue.js