一.输出方式介绍
大模型的输出有2种处理方式,1.output parser 2.tool
Output Parser(输出解析器)
Tool(工具/函数调用) 。
它们是两个不同层级的东西,但经常一起出现在 Agent 的流程中
简单来说:
- Output Parser = 「模型吐出文本 → 我把它解析成对象」
- Tool = 「模型说要用某个工具 → 我去执行那个工具 → 把结果喂回模型」
核心区别:Parser vs Tool
| 维度 | Output Parser(输出解析器) | Tool / Function Calling(工具调用) |
|---|---|---|
| 本质 | 把 LLM 返回的原始文本解析成程序可用的结构化数据(dict / Pydantic / 等) | LLM 决定调用哪个外部函数,由你的代码执行该函数 |
| 谁在执行 | 你的应用代码对模型输出字符串做解析 | 你的应用代码执行被调用的函数/API |
| 典型用途 | 从自然语言中提取结构化信息(如抽取用户画像 JSON) | 让 Agent 查天气、搜数据库、调 API、算数等 |
| 模型参与方式 | 模型只负责生成文本,Parser 是后处理 | 模型生成工具名+参数(tool_call),框架解析并执行 |
| LangChain 对应 | JsonOutputParser / PydanticOutputParser | @tool 装饰器 / with_structured_output() / Tool Calling |
简单来说:
- Output Parser = 「模型吐出文本 → 我把它解析成对象」
- Tool = 「模型说要用某个工具 → 我去执行那个工具 → 把结果喂回模型」
二、什么时候选择用JsonOutputParser,StructuredOutputParser,XMLOutputParser什么时候用withStructuredOutput
| 方式 | 底层机制 | 格式约束能力 | 模型要求 | 什么时候选它 |
|---|---|---|---|---|
JsonOutputParser | Prompt 引导 + 后解析 | ⭐⭐ 弱(靠模型自觉) | 任何模型 | 简单 JSON,模型能力强,快速原型 |
StructuredOutputParser | Prompt 引导 + 后解析 | ⭐⭐⭐ 中(带格式说明) | 任何模型 | 需要自定义字段描述、复杂 schema |
XMLOutputParser | Prompt 引导 + 后解析 | ⭐⭐ 弱(XML 嵌套易错) | 任何模型 | 几乎不推荐,除非模型对 XML 特别友好 |
with_structured_output() | 原生 Tool/Function Calling | ⭐⭐⭐⭐⭐ 强(模型原生保证) | 支持 function calling 的模型 | 首选,生产环境、复杂 schema |
三、总结:
能走 with_structured_output() 就走它(它底层就是 Tool Calling,最稳)。
走不了才用 JsonOutputParser 兜底。
StructuredOutputParser 和 XMLOutputParser 基本是历史遗留,新项目不用特意学。
二.案例
1.使用JsonOutputParser
import "dotenv/config";
import {ChatOpenAI} from "@langchain/openai";
import {JsonOutputParser} from "@langchain/core/output_parsers";
import {HumanMessage} from '@langchain/core/messages';
const model= new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const parser = new JsonOutputParser();
const question = `请你介绍一下爱因斯坦的信息,以json的格式返回,具体包含的字段如下:
name(姓名),birth(出生日期),death(逝世日期),nationality(国籍),education(教育背景)famous_theory(著名理论)
${parser.getFormatInstructions()}
`;
//response.content是一个markdown格式的字符串,包含了爱因斯坦的信息。我们希望将这些信息转换成json格式,以便于后续处理。为此,我们可以使用parser.parse()方法进行转换。
const response = await model.invoke([new HumanMessage(question)]);
console.log("格式化前"+response.content);
//使用parser的目的是为了将response.content转换成json格式,这样就可以直接使用result变量来访问解析后的数据了。
const result = await parser.parse(response.content);
console.log("格式化后"+JSON.stringify(result, null, 2));
JsonOutputParser把带有md格式的字符串,转化成了我们想要的json形式。这个json自动使用prompt提示语里面的字段。
2.使用StructuredOutputParser
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {StructuredOutputParser} from '@langchain/core/output_parsers';
import {HumanMessage} from '@langchain/core/messages';
const model= new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const parser = StructuredOutputParser.fromNamesAndDescriptions({
name: '姓名',
birth: '出生日期',
death: '逝世日期',
nationality: '国籍',
main_work: '主要成就',
});
const question = `请你介绍一下爱因斯坦的信息, ${parser.getFormatInstructions()}`;
const response = await model.invoke([new HumanMessage(question)]);
console.log("格式化前"+response.content);
const output = await parser.parse(response.content);
console.log("格式化后"+JSON.stringify(output, null, 2));
fromNamesAndDescriptions 指定生成格式
3.StructuredOutputParser+zod
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { StructuredOutputParser } from '@langchain/core/output_parsers';
import { HumanMessage } from '@langchain/core/messages';
import { z } from 'zod';
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const schema = z.object({
name: z.string().describe('姓名'),
birth: z
.string()
.describe('出生日期,格式为 YYYY-MM-DD'),
death: z
.string()
.optional()
.describe('逝世日期,格式为 YYYY-MM-DD,如果仍在世则不要输出此字段'),
nationality: z.string().describe('国籍'),
main_work: z
.array(z.string())
.describe('主要成就列表,必须是字符串数组,如 ["相对论", "光电效应"]'),
});
const parser = StructuredOutputParser.fromZodSchema(schema);
const question = `请你介绍一下爱因斯坦的信息。\n\n${parser.getFormatInstructions()}`;
const response = await model.invoke([new HumanMessage(question)]);
console.log('格式化前:', response.content);
const output = await parser.parse(response.content);
console.log('格式化后:', JSON.stringify(output, null, 2));
4.model用绑定json格式的tool形式处理结果
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
import { z } from 'zod';
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const schema = z.object({
name: z.string().describe('姓名'),
birth: z
.string()
.describe('出生日期,格式为 YYYY-MM-DD'),
death: z
.string()
.optional()
.describe('逝世日期,格式为 YYYY-MM-DD,如果仍在世则不要输出此字段'),
nationality: z.string().describe('国籍'),
main_work: z
.array(z.string())
.describe('主要成就列表,必须是字符串数组,如 ["相对论", "光电效应"]'),
});
const modelWithTool = model.bindTools([ {
name: "extract_info",
description: "从输入的文本中提取信息,并返回一个 JSON 对象。",
schema: schema,
}]
);
const question = `请你介绍一下爱因斯坦的信息。`;
const response = await modelWithTool.invoke([new HumanMessage(question)]);
console.log('格式化前:', response.tool_calls[0].args);
console.log("格式化以后",JSON.stringify(response.tool_calls[0].args, null, 2))
5.withStructuredOutput
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
// 初始化模型,增加 timeout 配置 (例如设置为 60秒/60000毫秒)
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
timeout: 60000, // ✅ 关键修复:将超时时间延长至 60 秒
maxRetries: 3, // ✅ 可选:开启内部自动重试次数
});
// 修复 Zod Schema
const schema = z.object({
name: z.string().describe('人物的中文名称,必须返回"爱因斯坦"'),
birth: z
.string()
.describe('出生日期,格式为 YYYY-MM-DD'),
death: z
.string()
.nullable()
.describe('逝世日期,格式为 YYYY-MM-DD,如果仍在世则输出 null'),
nationality: z.string().describe('国籍'),
main_work: z
.array(z.string())
.nullable()
.describe('主要成就列表,必须是字符串数组,如 ["相对论", "光电效应"]'),
});
const structuredOutputParser = model.withStructuredOutput(schema);
const question = `请你介绍一下爱因斯坦的信息,请用中文回复。`;
try {
const response = await structuredOutputParser.invoke(question);
console.log('格式化以后', JSON.stringify(response, null, 2));
} catch (error) {
console.error('发生未知错误:', error);
}
6.stream --流式输出
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
timeout: 60000,
maxRetries: 3,
});
const question = `请你介绍一下爱因斯坦的信息`;
const stream = await model.stream(question); // ✅ 注意变量名 steam(避免和 chunk 混)
let full = '';
for await (const chunk of stream) {
const content = chunk.content; // ✅ LangChain chunk 用 .content,不是 .text
if (typeof content === 'string') {
full += content;
process.stdout.write(content);
}
}
console.log('\n===== 完整内容 =====');
console.log(full);
7.stream 流式输出+withStructuredOutput格式化流式输出
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
timeout: 60000,
maxRetries: 3,
});
// 1. 定义 Schema(建议字段全用 string,避免数字解析失败)
const schema = z.object({
name: z.string().describe('姓名'),
birth_year: z.string().describe('出生年份'),
death_year: z.string().nullable().describe('去世年份,在世则输出 null'),
nationality: z.string().describe('国籍'),
famous_works: z.array(z.string()).describe('著名作品或成就列表'),
});
// 2. 普通模型用于流式输出(不强制 JSON 模式,体验流畅不报错)
const baseModel = model;
// 3. 结构化模型用于最终提取(只走一次性调用 invoke)
const structuredModel = model.withStructuredOutput(schema);
const prompt = `请你详细介绍爱因斯坦的信息。`;
async function runStableStreamAndExtract() {
// ========== 第一阶段:流式输出给用户看 ==========
const stream = await baseModel.stream(prompt);
let fullText = '';
console.log('🎬 开始流式接收...\n');
for await (const chunk of stream) {
const content = chunk.content;
if (typeof content === 'string') {
process.stdout.write(content);//实时输出
fullText += content;
}
}
console.log('\n\n✅ 流式接收完毕,开始结构化提取...\n');
// ========== 第二阶段:对完整文本进行结构化提取 ==========
try {
// 将刚才的完整文本作为上下文,让模型提取结构化数据
const extractPrompt = `请根据以下文本,提取出结构化的 JSON 信息:\n\n${fullText}。要求:完全按照文件回答问题,不要胡编乱造`;
const result = await structuredModel.invoke(extractPrompt);
console.log('🎯 最终结构化对象:');
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error('❌ 结构化提取失败:', e.message);
}
}
runStableStreamAndExtract();
process.stdout.write(content);//实时输出
await baseModel.stream(prompt);//流式输出
model.withStructuredOutput(schema);--json格式化
const result = await structuredModel.invoke(extractPrompt);---结构化
此时的流式输出里面你可以使用jsonoutputparser,structuredOutputParser,也可以用tool,都可以达到格式化数据的效果。
8.使用zodToJsonSchema做json格式化处理
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
// ========== 1. 定义 Zod Schema(describe 全部中文) ==========
const schema = z.object({
name: z.string().describe('人物的姓名'),
birth: z.string().describe('出生日期,格式为 YYYY-MM-DD'),
death: z
.string()
.nullable()
.describe('逝世日期,格式为 YYYY-MM-DD,如果仍在世则输出 null'),
nationality: z.string().describe('国籍'),
main_work: z
.array(z.string())
.nullable()
.describe('主要成就或作品的列表,必须是字符串数组,如 ["相对论", "光电效应"]'),
});
// ========== 2. Zod → JSON Schema ==========
const jsonSchema = zodToJsonSchema(schema, {
name: 'scientist_info',
$refStrategy: 'root',
});
// ========== 3. 初始化模型 ==========
const model = new ChatOpenAI({
modelName: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
timeout: 60000,
maxRetries: 3,
modelKwargs: {
response_format: {
type: 'json_schema',
json_schema: {
name: 'scientist_info',
strict: true,
schema: jsonSchema,
},
},
},
});
// ========== 4. 调用 ==========
async function testJsonSchema() {
try {
const res = await model.invoke([
new SystemMessage(
'你是一个专业的信息提取助手。请严格按照 JSON Schema 返回结果,不要输出任何 JSON 以外的内容。\n' +
'【重要指令】:所有字段的文本内容(如姓名、国籍、作品等)都必须使用简体中文输出,不要使用英文。专有名词请使用中文标准译名(如"相对论"而不是"Theory of Relativity")。'
),
new HumanMessage('请你介绍一下爱因斯坦的信息'),
]);
console.log('原始 content:', res.content);
const parsed = JSON.parse(res.content);
console.log('格式化后:', JSON.stringify(parsed, null, 2));
} catch (error) {
console.error('❌ 报错:', error.message);
}
}
testJsonSchema();
9.stream + JsonOutputParser解析
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import {
HumanMessage,
SystemMessage,
ToolMessage,
AIMessage,
} from '@langchain/core/messages';
import {
readFileTool,
writeTool,
executeCommandTool,
listDirectoryTool,
} from './all-tools.mjs';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { JsonOutputToolsParser } from '@langchain/core/output_parsers/openai_tools';
const model = new ChatOpenAI({
modelName: 'qwen-plus',
apiKey: process.env.OPENAI_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
const tools = [
readFileTool,
writeTool,
executeCommandTool,
listDirectoryTool,
];
const toolMap = Object.fromEntries(tools.map((t) => [t.name, t]));
const modelWithTool = model.bindTools(tools);
const toolParser = new JsonOutputToolsParser();
async function runAgentWithTools(query, max = 30) {
const history = new InMemoryChatMessageHistory();
await history.addMessages([
new SystemMessage(`
你是一个项目管理助手,使用工具完成任务,当前的工作项目的目录是:${process.cwd()}
工具:
1. read_file: 读取文件内容
2. write_file: 写入文件内容
3. execute_command: 执行终端命令
4. list_directory: 列出目录内容
重要规则 - execute_command:
- workingDirectory 参数会自动切换到指定目录
- 当使用 workingDirectory 时,绝对不要在 command 中使用 cd
- 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" } ❌
- 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" } ✅
回复要简洁,只说做了什么就好。
`.trim()),
new HumanMessage(query),
]);
for (let step = 0; step < max; step++) {
console.log(`\n🔄 [Step ${step + 1}] 模型思考中...`);
const msgs = await history.getMessages();
const rawStream = await modelWithTool.stream(msgs);
let fullAIMessage = null;
let parsedTools = []; // ✅ 在循环外先声明,用来承接最终解析结果
for await (const chunk of rawStream) {
fullAIMessage = fullAIMessage ? fullAIMessage.concat(chunk) : chunk;
// 即使 tool_calls 的 JSON 参数还没吐完,它也不会崩,会返回 [] 或累积中的结果
try {
//parser.Result是为了数据还在路上的流式输出准备的,使用stream时,需要用到这个特性来避免流还没吐完就解析导致的错误。
const partialTools = await toolParser.parseResult([{ message: fullAIMessage }]);
if (partialTools && partialTools.length > 0) {
parsedTools = partialTools; // 拿到就更新,流结束时就是最终结果
}
} catch {
// 参数残缺时静默忽略,等下一个 chunk 补全后再试
}
}
// 防御:stream 异常导致为空
if (!fullAIMessage) {
console.error('❌ 未收到模型回复');
break;
}
// 存入历史
await history.addMessages([fullAIMessage]);
if (fullAIMessage.content) {
console.log(`🤖 模型回复: ${fullAIMessage.content}`);
}
console.log(`🔧 解析到 ${parsedTools.length} 个工具调用`);
// 没有 tool_call → 任务完成
if (parsedTools.length === 0) {
console.log('✅ 模型判断任务完成。');
break;
}
// ===== 执行每个 tool_call =====
for (const toolCall of parsedTools) {
const { name, args, id } = toolCall;
const tool = toolMap[name];
if (!tool) {
await history.addMessages([
new ToolMessage({
content: `错误:未知工具 "${name}"`,
tool_call_id: id,
}),
]);
continue;
}
try {
const result = await tool.invoke(args);
console.log(`✅ 工具 ${name} 执行成功`);
await history.addMessages([
new ToolMessage({
content:
typeof result === 'string' ? result : JSON.stringify(result, null, 2),
tool_call_id: id,
}),
]);
} catch (error) {
console.error(`❌ 工具 ${name} 执行失败:`, error.message);
await history.addMessages([
new ToolMessage({
content: `工具执行失败: ${error.message}`,
tool_call_id: id,
}),
]);
}
}
}
const allMsgs = await history.getMessages();
return allMsgs[allMsgs.length - 1].content;
}
// ========== 测试 ==========
const casel = `创建一个功能丰富的 React TodoList应用:
1. 创建项目: echo n && echo n | pnpm create vite react-todo-app1 --template react-ts
2.修改 src/App.tsx,实现完整功能的TodoList:
添加、删除、编辑、标记完成
分类筛选(全部/进行中/已完成)
统计信息显示
localStorage 数据持久化
3.添加复杂样式:
渐变背景(蓝到紫)
卡片阴影、圆角
悬停效果
添加/删除时的过渡动画
使用 CSS transitions
5.列出目录确认
注意:使用 pnpm,功能要完整,样式要美观,要有动画效果
之后在 react-todo-app 项目中:
1.使用 pnpm install 安装依赖
2.使用 pnpm run dev 启动服务器`;
const finalAnswer = await runAgentWithTools(casel);
console.log('\n🏁 最终回复:', finalAnswer);
三.问题总结
1.在使用JsonOutputParser的时候,什么时候用parser,什么时候用parserResult?
| 维度 | parse(msg) | parseResult([{ message: msg }]) |
|---|---|---|
| 输入 | 单个 AIMessage | GenerationResult[](包裹一层) |
| 遇到残缺 JSON | ❌ 抛异常 | ✅ 返回 [],不炸 |
| 内部有缓存 | ❌ 无 | ✅ 有(跨调用累积同一 tool_call) |
| 设计目标 | 解析已完成的消息 | 解析流式进行中的消息 |
| 流结束后调用 | ✅ 完全没问题 | ✅ 完全没问题 |
| 流过程中调用 | ❌ 会疯狂报错 | ✅ 正确用法 |
总结,当你选择了流式输出,就一定得用parserResult。
一句话收尾:parse 和 parseResult 的最终输出一样,但 parseResult 是一个"流式友好版",它多了一层残缺容错 + 内部缓存,专门为"数据还在路上"的场景设计的。在你的代码里因为流已经结束再解析,所以两者效果一样——但用 parseResult 是更规范、更面向未来的写法。