LangChain 结构化输出三兄弟:ToolCall、OutputParser、withStructuredOutput 彻底讲透

前言

在做 LLM 应用开发时,让大模型稳定返回 JSON 结构化数据 几乎是必做需求。 最早我是用假 ToolCall(工具调用 hack) ,手动写res.tool_calls[0].args去抠结果; 后面学会了JsonOutputParser输出解析器,靠 Prompt 哄模型输出 JSON; 现在 LangChain 推荐使用withStructuredOutput,一行代码绑定 Schema,直接返回 JS 对象。

很多新手会混淆三者:什么时候用 ToolCall?OutputParser 和 withStructuredOutput 到底差在哪?底层原理、适用场景、坑点,本文一次性讲清楚。

本文代码基于 LangChain JS / TS。

一、第一代:传统 ToolCall 「讨巧方案」

原理

本质是欺骗模型调用一个不存在的工具 ,把我们想要的结构化 JSON 塞到tool_call.args参数里面。 这是早期没有原生结构化输出能力时,社区的 hack 做法。

go 复制代码
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ model: "gpt-4o" });

// 定义一个假工具,只是用来承载返回的结构化数据
const tools = [
  {
    type: "function" as const,
    function: {
      name: "get_scientist_info",
      description: "获取科学家基础信息",
      parameters: {
        type: "object",
        properties: {
          name: { type: "string", description: "科学家名字" },
          field: { type: "string", description: "研究领域" },
          birthYear: { type: "number", description: "出生年份" }
        },
        required: ["name", "field", "birthYear"]
      }
    }
  }
];

// 绑定工具调用
const res = await model.bindTools(tools).invoke("介绍爱因斯坦");

// 重点!必须手动取下标0,再JSON.parse解析字符串
const toolCall = res.tool_calls[0];
const scientistInfo = JSON.parse(toolCall.args);

console.log(scientistInfo);
// { name:"爱因斯坦", field:"理论物理", birthYear:1879 }

代码解析

  1. bindTools(tools):给模型绑定工具定义,开启 function calling
  2. 返回结果resAIMessage,里面带有tool_calls数组
  3. 数组第一个元素下标是[0],取出 toolCall
  4. toolCall.argsJSON 字符串 ,必须手动JSON.parse转 JS 对象

缺点

  1. 必须手动处理数组下标[0],写大量解析胶水代码
  2. 模型偶尔输出异常 JSON,要写 try/catch 容错
  3. 语义很别扭:不是真的调用工具,只是借工具调用的壳返回数据
  4. 适合场景:真的要调用外部函数(Agent 执行工具动作,查询数据库、接口)

一句话总结:ToolCall 是用来干活调用外部函数的,单纯拿结构化 JSON 属于 "借壳使用"

二、第二代:OutputParser 输出解析器(JsonOutputParser)

原理

不使用工具调用,完全靠 Prompt 文本约束模型输出 JSON,模型输出纯文本字符串,再在后端代码做解析。 Parser 是一个可插拔的 Runnable 组件,放在链的末尾,负责清洗、解析大模型返回的文本。

javascript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new ChatOpenAI({ model: "gpt-4o" });
const parser = new JsonOutputParser();

// 提示词注入格式要求
const prompt = PromptTemplate.fromTemplate(`
你需要提取科学家信息,严格输出JSON。
{format_instructions}
问题:{question}
`).partial({ format_instructions: parser.getFormatInstructions() });

// 链式:prompt -> model -> parser
const chain = prompt.pipe(model).pipe(parser);
const scientistInfo = await chain.invoke({ question: "介绍爱因斯坦" });

console.log(scientistInfo);

代码解析

  1. parser.getFormatInstructions() 自动生成一大段文本,塞进 Prompt,告诉模型输出 JSON 格式
  2. LLM 返回纯文本字符串
  3. 管道末尾pipe(parser)自动执行 JSON.parse,返回对象

缺点

  • 约束弱:模型不听话,会附带解释文字、markdown 代码块、残缺 JSON,直接抛出解析异常
  • 依赖 Prompt 工程,不同模型效果差异巨大

适用场景

  1. 模型不支持 ToolCall / 原生结构化输出(很多本地开源大模型)
  2. 需要自定义文本清洗逻辑,灵活处理脏输出
  3. 老项目兼容

一句话总结:OutputParser:大模型随便输出文字,后端代码努力把文字抠成 JSON

三、第三代:withStructuredOutput 原生结构化输出(推荐)

原理

Schema 直接传递给模型底层 API,不是写在 Prompt 里面,由大模型原生能力强制保证输出符合 Schema 。 底层内部可以自动选择function_calling或者json_schema两种策略,LangChain 把取下标、JSON.parse 全部封装隐藏。

go 复制代码
import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({ model: "gpt-4o" });

// 定义Schema,描述期望返回的数据结构
const scientistSchema = {
  type: "object",
  properties: {
    name: { type: "string", description: "科学家名字" },
    field: { type: "string", description: "研究领域" },
    birthYear: { type: "number", description: "出生年份" }
  },
  required: ["name", "field", "birthYear"]
};

// 绑定Schema,生成包装后的模型
const structuredModel = model.withStructuredOutput(scientistSchema);

// 直接调用,返回已经解析完成的JS对象!
const scientistInfo = await structuredModel.invoke("介绍爱因斯坦");
console.log(scientistInfo);

代码解析

  1. model.withStructuredOutput(schema):包装模型实例,绑定数据契约
  2. 调用 invoke,直接拿到结构化对象,不需要 tool_calls 0,不需要 JSON.parse
  3. LangChain 内部自动选择底层策略(ToolStrategy / Provider 原生 JSON Schema)

优点

✅ 稳定性极强,模型 API 层面强制约束,极少出现 JSON 格式崩坏 ✅ 不需要修改 Prompt,不用注入一大段格式说明文字 ✅ 代码极简,上层看不到 tool_calls 解析逻辑

适用场景

只想要 AI 返回固定结构 JSON,不需要调用外部工具。 信息提取、数据结构化、实体抽取,优先选这个。
一句话总结:withStructuredOutput:提前告诉模型接口输出规范,模型原生保证输出合规 JSON

四、三者横向对比表

表格

方案 底层原理 是否依赖 tool_call 解析位置 稳定性 最佳场景
传统 ToolCall 假工具调用 hack ✅是 业务代码手动解析tool_calls[0] 中等 Agent,真的调用外部函数
JsonOutputParser Prompt 文本约束 + 后端文本解析 ❌否 输出解析器,模型输出文本后解析 低,容易脏输出 开源模型、需要自定义清洗逻辑
withStructuredOutput 模型原生结构化输出,内部封装 toolcall 底层可选 LangChain 内部自动解析 ⭐⭐⭐⭐⭐高 提取数据、结构化返回,无外部函数调用

五、怎么选型?3 条判断规则

  1. 需要调用外部工具(查询接口、数据库) → 用 ToolCall /bindTools
  2. 只是提取信息,输出固定 JSON,模型支持原生结构化 → 优先 withStructuredOutput
  3. 本地开源模型,不支持原生结构化输出 → 选用 OutputParser

六、常见误区澄清

误区 1:withStructuredOutput 完全脱离 tool call?

不是。底层策略function_calling模式下,内部依然走 tool call。但是 LangChain 把tool_calls[0]、JSON.parse 全部封装,上层业务代码看不见。 底层在用 tool call,但是上层不用写 tool call 解析代码。

误区 2:OutputParser 被淘汰了?

没有。当模型不支持结构化输出时,OutputParser 依然是唯一方案,灵活性最高,可以自定义正则清洗、修复脏文本。

结尾

做 AI Agent 开发,分清这三者的定位,能避开大量 "模型输出 JSON 格式错乱" 的坑。 简单一句话记忆:

  • 要干活调用外部工具:ToolCall
  • 哄模型按文字要求输出 JSON:OutputParser
  • 单纯拿结构化数据,追求稳定:withStructuredOutput
相关推荐
知兀1 小时前
【前端】受控和非受控组件
前端·javascript·react
晴天161 小时前
JavaScript 与 Java 垃圾回收机制对比
java·开发语言·javascript
huali1 小时前
vue-split-screen:让 Vue Router 的导航轨迹变成两个页面
前端·javascript·vue.js
bug总结2 小时前
uniapp总结
开发语言·前端·javascript
走到天涯海角2 小时前
pinia和Vuex的区别以及使用pinia
开发语言·前端·javascript
晴天162 小时前
Web 页面内存溢出与 CPU 溢出:区别、诊断与最佳实践
前端·javascript
Highcharts.js3 小时前
常见报错排雷指南3:兼容性问题的官方解法
前端·javascript·react.js·可视化·highcharts·报错解法
言乐63 小时前
Python加速器2视频网页加速器
前端·javascript·css·python·音视频
志尊宝3 小时前
Vue3 零基础每日笔记(017):事件处理进阶——$event、多事件与事件修饰符全家桶
javascript·vue.js·笔记