一文搞懂 LLM 结构化输出:从"抠 JSON"到"吐 JSON"的进化之路

LLM 返回自由文本,但你的业务需要 JSON。本文从 Output Parser 讲到 Tool Calling,带你理解结构化输出的完整进化路径,附完整代码和面试高频问。


前言

用 LLM 做业务时,你一定遇到过这个问题:大模型返回的是一段话,但你要的是一个 JSON 对象。

比如让它"介绍一下爱因斯坦",它返回的是:

erlang 复制代码
爱因斯坦是德裔物理学家,出生于1879年......

但你的代码需要的是:

json 复制代码
{"name": "爱因斯坦", "birth_year": 1879, "nationality": "德国"}

怎么让 LLM 按格式输出?输出了又怎么解析?本文带你搞懂从 Output Parser 到 Tool Calling 的完整进化路径

你将会收获:

  • 理解 Output Parser 的三种方案及其局限
  • 理解 Tool Calling 为什么是结构化输出的终极方案
  • 掌握 model.withStructuredOutput() 的用法和原理
  • 面试中遇到"LLM 结构化输出"不再慌

技术栈: Node.js + LangChain + OpenAI API + Zod


一、Output Parser 三件套:从文本中"抠"出 JSON

1.1 问题的根源

LLM 的输出本质是自由文本,不是结构化数据。即使你让它"以 JSON 格式返回",它也可能返回:

json 复制代码
```json
{"name": "爱因斯坦", "birth_year": 1879}
ruby 复制代码
前面多了 `` ```json ``,后面多了 `` ``` ``,直接 `JSON.parse()` 会报错。

### 1.2 方案一:正则提取(最原始)

```js
const jsonMatch = response.content.match(/```json\s*([\s\S]*?)\s*```/);
const jsonStr = jsonMatch ? jsonMatch[1] : response.content;
const result = JSON.parse(jsonStr);

问题:每次手写正则,LLM 格式一变就挂。

1.3 方案二:JsonOutputParser(LangChain 封装)

js 复制代码
import { JsonOutputParser } from '@langchain/core/output_parsers';

const parser = new JsonOutputParser();
const prompt = `介绍一下爱因斯坦的信息。
${parser.getFormatInstructions()}`;  // 追加格式说明

const response = await model.invoke(prompt);
const result = await parser.parse(response.content);  // 自动提取 + JSON.parse

两个关键方法

  • getFormatInstructions() → 在 prompt 末尾追加格式约束
  • parser.parse() → 正则提取 markdown + JSON.parse

1.4 方案三:StructuredOutputParser(约束字段结构)

JsonOutputParser 只管"返回 JSON",不管"JSON 里面有什么字段"。升级方案:

js 复制代码
import { StructuredOutputParser } from '@langchain/core/output_parsers';
import { z } from 'zod';

const schema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

const parser = StructuredOutputParser.fromZodSchema(schema);
const prompt = `介绍一下爱因斯坦。${parser.getFormatInstructions()}`;

const response = await model.invoke(prompt);
const result = await parser.parse(response.content);

1.5 Output Parser 的本质

scss 复制代码
prompt 约束(源头)+ 正则提取 + JSON.parse(兜底)
     ↑                    ↑
  getFormatInstructions()  parser.parse()

💡 一句话记住 :Output Parser = 从文本中"抠"出 JSON,是事后解析


二、Tool Calling:结构化输出的终极方案

2.1 换个思路

Output Parser 的问题是:先让模型自由发挥,再从文本里提取结构。这就像从一篇作文里找答案,总有遗漏的风险。

但 LLM 有一个原生能力:Tool Calling(工具调用)。工具调用的参数本身就是结构化的 JSON。

javascript 复制代码
Output Parser:  LLM → 自由文本 → 解析文本 → 提取 JSON(有损耗)
Tool Calling:   LLM → 直接返回结构化参数 → 天然就是 JSON(零损耗)

2.2 关键洞察:工具不需要真的执行

js 复制代码
const modelWithTool = model.bindTools([{
  name: 'extract_scientist_info',
  description: '提取和结构化科学家的详细信息',
  schema: scientistSchema,
}]);

const response = await modelWithTool.invoke('介绍一下爱因斯坦');
console.log(response.tool_calls[0].args);
// 直接拿到结构化数据,工具根本没有执行!

我们不是要调用工具,而是借用 Tool Calling 的结构化能力。 这是 Tool Calling 最巧妙的用法。

2.3 为什么比 Output Parser 更严格?

Output Parser Tool Calling
数据来源 从文本中提取 模型原生输出
约束方式 prompt 暗示(模型可能不遵守) schema 强制(模型必须遵守)
解析风险 JSON 可能不合法 天然结构化,无需解析
类型校验 依赖 Zod 事后校验 模型参数自带类型
嵌套支持 需要 Zod 定义 原生支持

💡 一句话记住 :Tool Calling 是模型的原生结构化输出能力 ,Output Parser 只是从文本中抢救数据

2.4 代码演示

js 复制代码
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 },
});

const scientistSchema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

// 方式一:手动绑定工具
const modelWithTool = model.bindTools([{
  name: 'extract_scientist_info',
  description: '提取和结构化科学家的详细信息',
  schema: scientistSchema,
}]);

const response = await modelWithTool.invoke('介绍一下爱因斯坦');
console.log(response.tool_calls[0].args);
// { name: "爱因斯坦", birth_year: 1879, nationality: "德国", fields: ["物理学"] }

三、withStructuredOutput:LangChain 的终极封装

3.1 手动 bindTools 的问题

上面的方式虽然好用,但有两个问题:

  1. 需要手动定义工具的 name、description、schema(重复劳动)
  2. 调用后还要从 tool_calls[0].args 里取数据(样板代码)

3.2 一步到位:withStructuredOutput

js 复制代码
const scientistSchema = z.object({
  name: z.string().describe('科学家的姓名'),
  birth_year: z.number().describe('出生年份'),
  nationality: z.string().describe('国籍'),
  fields: z.array(z.string()).describe('研究领域列表'),
});

// 一行搞定:绑定 + 解析 + 校验
const structuredModel = model.withStructuredOutput(scientistSchema);

const result = await structuredModel.invoke('介绍一下爱因斯坦');
console.log(result);
// { name: "爱因斯坦", birth_year: 1879, nationality: "德国", fields: ["物理学"] }

对比一下:

js 复制代码
// 手动 Tool Calling(繁琐)
const modelWithTool = model.bindTools([{ name: '...', description: '...', schema }]);
const response = await modelWithTool.invoke(prompt);
const result = response.tool_calls[0].args;

// withStructuredOutput(简洁)
const structuredModel = model.withStructuredOutput(schema);
const result = await structuredModel.invoke(prompt);

3.3 底层原理

withStructuredOutput 做了什么?

markdown 复制代码
1. 把 Zod schema 转成 tool definition
2. 绑定到模型(等价于 bindTools)
3. 设置 tool_choice 为强制调用
4. 从 tool_calls 中提取 args
5. 用 Zod schema 校验返回结果

本质还是 Tool Calling,只是包装成语义更好的 API。

3.4 兼容性:不支持 Tool Call 怎么办?

有些模型(老模型或某些国产模型)不支持 Tool Calling。withStructuredOutput自动降级

javascript 复制代码
支持 Tool Calling  → 用原生能力(效果最好)
不支持             → 降级为 prompt 引导 + JSON 解析(兜底)

这也是它比手动 bindTools 更健壮的原因------bindTools 遇到不支持的模型直接报错,withStructuredOutput 会降级处理。


四、进化路径总结

javascript 复制代码
JsonOutputParser → StructuredOutputParser → Tool Calling → withStructuredOutput
     ↓                    ↓                      ↓                ↓
  只管 JSON 格式      加字段约束           模型原生能力       终极封装
  从文本提取          从文本提取           零损耗             零损耗 + 自动降级

核心演进逻辑

阶段 方案 约束方式 本质
1.0 JsonOutputParser prompt 暗示 事后解析文本
2.0 StructuredOutputParser + Zod prompt + schema 描述 事后解析文本
3.0 Tool Calling + bindTools 模型原生 schema 约束 原生结构化输出
4.0 withStructuredOutput 模型原生 + 自动封装 原生 + 降级兜底

五、Output Parser 还有必要存在吗?

有,但场景变窄了。

推荐用 withStructuredOutput 的场景

  • 需要 JSON 格式的结构化数据(90% 的场景)
  • 需要严格的类型校验
  • 需要支持不支持 Tool Call 的模型(自动降级)

Output Parser 仍然有用的场景

  • 需要 XML 格式输出(XmlOutputParser
  • 需要 YAML 格式输出
  • 模型不支持 Tool Call 且你不想依赖 LangChain 的降级逻辑
js 复制代码
// XML 格式 → 用 XmlOutputParser
import { XmlOutputParser } from 'langchain/output_parsers';
const parser = new XmlOutputParser();
const result = await parser.parse(response.content);

// JSON 格式 → 用 withStructuredOutput(推荐)
const structuredModel = model.withStructuredOutput(schema);
const result = await structuredModel.invoke(prompt);

💡 一句话记住 :JSON 结构化用 withStructuredOutput,非 JSON 格式(XML/YAML)用 Output Parser。


六、面试高频问

Q1:LLM 结构化输出有哪些方案?

四种,按进化顺序:

  1. 正则提取:prompt 约束 + 正则 + JSON.parse(最弱)
  2. JsonOutputParser:LangChain 封装,自动处理 markdown 包裹
  3. StructuredOutputParser + Zod:约束字段名、类型、嵌套结构
  4. Tool Calling / withStructuredOutput:模型原生结构化输出(最强)

推荐用 withStructuredOutput,它是 Tool Calling 的高级封装,兼容性最好。
Q2:Tool Calling 为什么比 OutputParser 更靠谱?

OutputParser 是从 LLM 返回的文本中提取 JSON ,有截断、格式错误等风险。Tool Calling 是 LLM 的原生结构化输出能力,返回的工具调用参数本身就是合法 JSON,不依赖文本解析。

类比:OutputParser 是"从作文里找答案",Tool Calling 是"直接填表格"。
Q3:withStructuredOutput 和 bindTools 的区别?

withStructuredOutputbindTools 的高级封装:

  • bindTools:手动定义工具 → 调用 → 手动取 tool_calls[0].args
  • withStructuredOutput:传入 Zod schema → 直接返回结构化对象

底层都是 Tool Calling,withStructuredOutput 省去了样板代码,还支持自动降级(模型不支持 Tool Call 时用 prompt 引导)。
Q4:如果模型不支持 Tool Calling 怎么办?

withStructuredOutput 会自动降级为 prompt 引导 + JSON 解析。而手动 bindTools 会直接报错。这也是推荐用 withStructuredOutput 的原因之一。
Q5:getFormatInstructions() 做了什么?

在 prompt 末尾追加一段格式说明,告诉 LLM "请按以下 JSON schema 返回"。这是 Output Parser 的核心------通过在 prompt 中嵌入格式要求,引导 LLM 按指定结构输出。

但这种方式只是"暗示",模型可能不遵守。Tool Calling 的 schema 约束才是"强制"。
Q6:Output Parser 还有必要学吗?

有必要,但不是重点。了解其原理(prompt 约束 + 事后解析)有助于理解结构化输出的演进。实际项目中,JSON 结构化优先用 withStructuredOutput,只有处理 XML/YAML 等非 JSON 格式时才用 Output Parser。


总结

核心概念速查表

概念 一句话
Output Parser 从 LLM 文本输出中提取结构化数据(事后解析)
Tool Calling LLM 原生结构化输出能力,参数天然就是 JSON
withStructuredOutput LangChain 对 Tool Calling 的高级封装,一步到位
getFormatInstructions() 在 prompt 里追加格式约束说明(Output Parser 专用)
bindTools 手动绑定工具到模型(底层 API)

一句话总结

javascript 复制代码
结构化输出的进化:从文本中"抠"JSON → 让模型直接"吐"JSON。
推荐用 withStructuredOutput,底层是 Tool Calling,兼容性最好。

核心代码骨架

js 复制代码
// 推荐方案:withStructuredOutput(Tool Calling 的高级封装)
const schema = z.object({
  name: z.string().describe('姓名'),
  age: z.number().describe('年龄'),
});
const structuredModel = model.withStructuredOutput(schema);
const result = await structuredModel.invoke('介绍一下xxx');
// result 直接就是结构化对象,不用解析

希望这篇文章对你有帮助!有问题欢迎在评论区交流 🔥

相关推荐
L小航呀3 小时前
代码随想录刷题 Day16
leetcode·面试
苦瓜小生5 小时前
【前端】【力扣与手撕】十天带你刷完前端算法与手撕,全是最简单好记的最优解法!day4
前端·数据结构·算法·leetcode·面试
AIsoft_86885 小时前
最好用的会议纪要APP怎么选?功能横评
面试·职场和发展·iphone
小白羊丨7 小时前
问数 Agent 整体架构、意图识别、历史上下文与 NL2SQL
java·面试·架构
ocean210320 小时前
2025-2026年AI部署与MLOps大厂面试高频问题
人工智能·面试·大模型推理·ai部署
洋不写bug1 天前
链表面试笔试经典题目详细解析,题目多解法,复杂度分析
数据结构·链表·面试
黄敬峰1 天前
一文搞懂结构化大模型输出:从 SSE 到 Tool Calling 的四种姿势
面试
星星落进兜里1 天前
Redis 内存缓存,面试补充
数据库·redis·面试
蒸蒸yyyyzwd1 天前
cpp 选手秋招学习笔记 day27
服务器·c++·面试·八股