ai agent ---output汇总

41 阅读5分钟

一.输出方式介绍

大模型的输出有2种处理方式,1.output parser 2.tool

Output Parser(输出解析器)

Tool(工具/函数调用)

它们是两个不同层级的东西,但经常一起出现在 Agent 的流程中

简单来说:

  • Output Parser​ = 「模型吐出文本 → 我把它解析成对象」
  • Tool​ = 「模型说要用某个工具 → 我去执行那个工具 → 把结果喂回模型」

image.png


核心区别: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

方式底层机制格式约束能力模型要求什么时候选它
JsonOutputParserPrompt 引导 + 后解析⭐⭐ 弱(靠模型自觉)任何模型简单 JSON,模型能力强,快速原型
StructuredOutputParserPrompt 引导 + 后解析⭐⭐⭐ 中(带格式说明)任何模型需要自定义字段描述、复杂 schema
XMLOutputParserPrompt 引导 + 后解析⭐⭐ 弱(XML 嵌套易错)任何模型几乎不推荐,除非模型对 XML 特别友好
with_structured_output()原生 Tool/Function Calling⭐⭐⭐⭐⭐ 强(模型原生保证)支持 function calling 的模型首选,生产环境、复杂 schema

三、总结:

能走 with_structured_output() 就走它(它底层就是 Tool Calling,最稳)。

走不了才用 JsonOutputParser 兜底

StructuredOutputParserXMLOutputParser 基本是历史遗留,新项目不用特意学。

二.案例

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 }])
输入单个 AIMessageGenerationResult[](包裹一层)
遇到残缺 JSON❌ 抛异常✅ 返回 [],不炸
内部有缓存❌ 无✅ 有(跨调用累积同一 tool_call)
设计目标解析已完成的消息解析流式进行中的消息
流结束后调用✅ 完全没问题✅ 完全没问题
流过程中调用❌ 会疯狂报错✅ 正确用法

image.png

总结,当你选择了流式输出,就一定得用parserResult。

一句话收尾parseparseResult 的最终输出一样,但 parseResult 是一个"流式友好版",它多了一层残缺容错 + 内部缓存,专门为"数据还在路上"的场景设计的。在你的代码里因为流已经结束再解析,所以两者效果一样——但用 parseResult 是更规范、更面向未来的写法。