大模型总是不按格式输出?LLM 结构化输出的四种解法

大模型总是不按格式输出?LLM 结构化输出的四种解法

摘要:LLM 默认吐自由文本,但程序需要的是能 JSON.parse 的对象。本文从 Prompt 约束到 Tool Calling,拆解四代结构化输出方案的原理、可靠性差异与流式表现,帮你选对方案、避开格式陷阱。

📑 目录

  • 为什么大模型总是不按格式输出?
  • 四代实现方式:从"求它"到"管它"
  • 第一代:Prompt 约束 + JsonOutputParser
  • 第二代:StructuredOutputParser 的严格校验
  • 第三代:Tool Calling 借力打力
  • 语义化封装:withStructuredOutput
  • 特殊格式:XML 与 YAML 解析器
  • 流式结构化输出:两条路径的本质差异
  • 方案选型:什么时候用哪个?
  • 互动讨论

为什么大模型总是不按格式输出?

LLM 默认输出自由文本,就像一个人跟你聊天,想到哪说到哪。但程序需要的是能解析的对象------字段名对、类型对、能直接 JSON.parse。

结构化输出就是给 LLM 套一个"表格模板",让它按格子填,而不是自由发挥。

但现实是:你让它输出 JSON,它可能给你一段 Markdown 代码块包裹的 JSON;你让它输出数字,它可能给你字符串 "28"。 格式约束这件事,靠模型"自觉"从来都不靠谱。

四代实现方式:从"求它"到"管它"

代际 方式 原理 可靠性
第一代 Prompt 里写"请输出 JSON" 靠模型自觉 低
第二代 JSON Mode 模型保证输出合法 JSON 中,格式对但字段可能乱
第三代 Tool Calling schema 注册成工具,模型按 schema 填参数 高,主流方案
第四代 约束解码 推理时按 JSON Schema 屏蔽非法 token 最高,需模型支持

本文重点拆解前三代的落地方式,以及它们在流式场景下的差异。

第一代:Prompt 约束 + JsonOutputParser

最朴素的方式:在 prompt 里写"请以 JSON 格式返回",然后用 JsonOutputParser 解析。

javascript

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

const parser = new JsonOutputParser();

const prompt = `
请介绍一下爱因斯坦的信息。
包含以下字段: name(姓名)、birth_year(出生年份)、nationality(国籍)、major_achievements(主要成就,数组)、famous_theory(著名理论)
${parser.getFormatInstructions()}
`;

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

JsonOutputParser 只做一件事

文本 → JS 对象。 输入不合法就抛异常,不修复、不校验、不重试。

内部流程:去空白 → 剥离 ```````json```` 代码块标记 → JSON.parse() → 返回对象。

关键认知 :LLM 返回的是纯文本 ,可能混有 Markdown 代码块标记,parser 负责剥离。getFormatInstructions() 不传 schema 时,只返回"请输出合法 JSON"这类通用说明,不含字段名、类型、嵌套结构。

它不校验字段和类型

javascript

arduino 复制代码
// 模型返回 {"age": "28"}(字符串)
// JsonOutputParser 照样解析成功
// 后续代码拿 age 做数学运算时才会崩

JsonOutputParser 只保证"能解析成 JS 对象",不保证字段名对、类型对。

会抛错的典型情况:

  • JSON 缺括号、多逗号
  • 不是合法 JSON(name: 张三)
  • 前面有废话(好的,以下是结果:{...})
  • 空字符串

OutputParser 的完整能力

除了 parse,解析器作为 Runnable 体系一员,还有其他方法:

方法 作用
parse(text) 同步解析原始文本
parseResult(result) 处理多个候选生成结果
invoke(input) Runnable 标准入口,链式调用用
getFormatInstructions() 返回格式说明文本,注入 prompt
aparse / aparseResult 异步版本
batch(inputs) 批量处理
withRetry(config) 解析失败自动重试
withFallbacks([...]) 失败切换备选方案
pipe(next) 链式组合

第二代:StructuredOutputParser 的严格校验

JsonOutputParser 相当于只说了一句"要以 JSON 格式输出"。StructuredOutputParser 则是把每个 key 的格式说明也帮我们做了,更加严谨。

它有两个静态方法:

  • fromNamesAndDescriptions:最基础,所有字段都是字符串,无类型校验
  • fromZodSchema:传入 Zod schema,解析后做 Zod 校验

fromNamesAndDescriptions

javascript

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

const parser = StructuredOutputParser.fromNamesAndDescriptions({
    name: "姓名",
    birth_year: "出生年份",
    nationality: "国籍",
    major_achievements: "主要成就,用逗号分隔的字符串",
    famous_theory: "著名理论"
});

const question = `
请介绍一下爱因斯坦的信息。
${parser.getFormatInstructions()}
`;

getFormatInstructions() 会自动生成一段详细的 JSON Schema 说明,包括字段名、类型、描述、必填项、不允许额外属性等。这比 JsonOutputParser 的"请输出 JSON"具体得多。

fromZodSchema

javascript

css 复制代码
import { z } from "zod";

const scientistSchema = z.object({
    name: z.string().describe('科学家的姓名'),
    birth_year: z.number().describe('出生年份'),
    death_year: z.number().optional().describe('去世年份,如果还在世则不填'),
    nationality: z.string().describe('科学家的国籍'),
    fields: z.array(z.string()).describe('研究领域列表'),
    awards: z.array(
        z.object({
            name: z.string().describe('获奖名称'),
            year: z.number().describe('获奖年份'),
            reason: z.string().describe('获奖原因')
        })
    ).describe('获得的重要奖项列表'),
    biography: z.string().describe('简短传记,100字内')
});

const parser = StructuredOutputParser.fromZodSchema(scientistSchema);

Zod schema 允许定义嵌套对象、数组、可选字段、数字类型等。解析后会做 Zod 校验,类型不对会抛异常。

注意:JS 版对复杂 schema 支持不完整------嵌套对象、数组、联合类型、可选字段的边界行为可能不符合预期。简单扁平对象没问题,复杂结构容易出问题。

它仍然走 Prompt 约束

StructuredOutputParser 仍走"prompt 约束 + 文本解析",不走 Tool Calling。它的价值场景是:模型不支持 Tool Calling,但你又需要 schema 校验。

第三代:Tool Calling 借力打力

这是目前最可靠的方案。核心思路:把 schema 注册成一个工具,模型在调用工具时会按 schema 填写参数------这个参数就是我们需要的结构化输出。

bindTools:底层机制

javascript

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

const modelWithTools = model.bindTools([
    {
        name: "extract_scientist_info",
        description: "提取和结构化科学家的详细信息",
        schema: scientistSchema
    }
]);

const response = await modelWithTools.invoke("介绍一下伽利略");
console.log(response.tool_calls[0].args);

这个工具没有函数体 ------它的目的不是被调用,而是通过"告知模型这里有一个工具"的方式,把 schema 巧妙地交给 LLM。模型调用这个工具时,就会按照 schema 生成 tool_calls,而这个 tool_calls[0].args 正好就是我们需要的最终输出格式。

易错点 :bindTools 只负责"告诉模型有哪些工具",不自动提取结果、不校验、不强制调用。不设 tool_choice,模型可能返回普通文本,根本不调工具。

手动提取 + 校验:

javascript

ini 复制代码
const args = response.tool_calls?.[0]?.args;
const parsed = scientistSchema.parse(args);

Tool Calling 与 XML 的关系

原生 Tool Calling 协议本身只认 JSON。 OpenAI 的 function calling,parameters 是 JSON Schema 对象,模型返回的 arguments 是 JSON 格式字符串。Anthropic 的 tool use 也一样。

不能通过 tool schema 实现 XML 结构化输出。XML 只能走"prompt 约束 + OutputParser"的模拟路线,可靠性低于原生 Tool Calling。

语义化封装:withStructuredOutput

bindTools 需要手动绑定工具、手动提取 args、手动 Zod 校验,代码繁琐。withStructuredOutput 把这些封装好了:

javascript

ini 复制代码
const structuredModel = model.withStructuredOutput(scientistSchema);
const result = await structuredModel.invoke("介绍一下爱因斯坦");
// result 直接是 { name: "爱因斯坦", birth_year: "1879", ... },不是 AIMessage

底层流程

  1. Zod schema 转成 JSON Schema
  2. JSON Schema 注册成一个工具定义(name、description、parameters)
  3. 这个工具没有实际实现函数 ,只存在于请求的 tools 参数里
  4. 模型在 tool_calls 里返回 { name: "extract_user", arguments: "{"name":"张三","age":28}" }
  5. LangChain 从 tool_calls[0].args 取出参数,Zod 校验,返回对象

关键点 :模型并没有真的"调用"一个函数,它只是输出了一个符合工具 schema 的 JSON 结构。API 层保证这个 JSON 符合 parameters 定义。

参数控制

javascript

php 复制代码
model.withStructuredOutput(schema, {
    name: "extract_user",
    method: "functionCalling",   // 默认;可选 "jsonMode" / "jsonSchema"
    strict: true,                // 仅 OpenAI jsonSchema 模式有效
    includeRaw: true,            // 返回 { parsed, raw }
});

它不是绝对可靠

如果模型不调用工具、调用错工具、参数不符合 schema,都会抛异常。它只是"比 JsonOutputParser 可靠",生产环境仍需 try/catch 和重试。

也不是所有模型都支持 :functionCalling 依赖模型的工具调用能力。小模型、部分开源模型不支持,会报错或退化。

自动降级 :在模型不支持 tool-call 时,withStructuredOutput 会自动降级成"提示词 + JSON 格式输出"模式。双重保障。

特殊格式:XML 与 YAML 解析器

OutputParser 模块并没有被淘汰------结构化输出不只是 JSON,还有 XML、YAML 等格式。

解析器 输出类型 适用场景
XMLOutputParser dict 模型更擅长生成 XML 时
CommaSeparatedListOutputParser List[str] 简单列表
YamlOutputParser 对象 比 JSON 更易读的配置类输出
EnumOutputParser 枚举值 固定选项选择

XMLOutputParser 示例

javascript

ini 复制代码
import { XMLOutputParser } from "@langchain/core/output_parsers";

const parser = new XMLOutputParser();
const question = `
请提取以下文本中的人物信息: 爱因斯坦出生于1879年,是一位伟大的物理学家
${parser.getFormatInstructions()}
`;

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

XML 是比 JSON 更老的数据交换标准(XMLHttpRequest 时代),现在基本都改用 JSON 了。但某些模型对 XML 的生成能力更强,这时可以用 XMLOutputParser。

流程控制型解析器

解析器 作用
OutputFixingParser 解析失败时调 LLM 修复输出
RetryWithErrorOutputParser 修复时额外提供原始格式指令

这两个解析器在"模型输出格式错误"时,会自动调 LLM 修复,而不是直接抛异常。代价是多一次 LLM 调用。

流式结构化输出:两条路径的本质差异

这是最容易踩坑的地方。两条路径的语义完全不同。

JsonOutputParser 的流式:逐块解析部分 JSON

靠内部 parsePartialResult,模型每吐出一个 chunk,parser 就把当前累积文本拿去尝试解析,能解析出多少字段就先返回多少。

javascript

csharp 复制代码
const parser = new JsonOutputParser();
const chain = prompt.pipe(model).pipe(parser);

const stream = await chain.stream({ input: "张三,28岁" });

for await (const partial of stream) {
    console.log(partial);
    // 可能依次输出:
    // {}
    // { name: "张三" }
    // { name: "张三", age: 28 }
}

底层用 strictParsePartialJson,递归下降解析器,能处理未闭合的字符串、数组、对象。

withStructuredOutput 的流式:底层 token 流,结果最后一次性返回

javascript

javascript 复制代码
const structuredModel = model.withStructuredOutput(schema);
const stream = await structuredModel.stream("张三,28岁");

for await (const chunk of stream) {
    console.log(chunk);
    // 每个 chunk 是底层的 token/消息块,
    // 不是逐步补全的 { name, age } 对象
}
// 流结束后,才得到完整的结构化对象

为什么不能逐块产出结构化结果? 因为 JSON 不完整时无法通过 Zod 校验。LangChain 不会一边收一边解析 Zod 对象,它会等模型完整返回全部 JSON 参数之后,一次性校验、拼装成结构化对象返回。

易错 :以为 withStructuredOutput 也能逐块产出部分结构化结果。它的流式只是底层 token 流的透传,不是结构化结果的渐进。

普通流式输出(无结构化)

javascript

ini 复制代码
const stream = await model.stream("详细介绍莫扎特的生平");

let fullContent = '';
for await (const chunk of stream) {
    const content = chunk.content;
    fullContent += content;
    process.stdout.write(content);
}

stream 返回异步可迭代对象,for await 逐个消费 chunk。chunk 是普通对象(AIMessageChunk),异步发生在获取下一个 chunk 的过程 ------迭代器每次 next() 返回 Promise,for await 自动 await 它。

方案选型:什么时候用哪个?

层级 机制 可靠性 流式结构化
JsonOutputParser prompt 格式说明 + 文本解析,不校验 低 ✅ parsePartialResult 逐块补全
StructuredOutputParser prompt 格式说明(含 schema)+ 文本解析 + Zod 校验 中 有限,不推荐依赖
withStructuredOutput Tool Calling,API 层保证格式 高 ❌ 底层 token 流,结果最后一次性返回
bindTools(底层) 手动绑定工具 + 强制 tool_choice + 提取 args + 校验 高 同 withStructuredOutput

核心区分

路线 原理 流式能力 可靠性
OutputParser 路线 文本 → 解析 强(JsonOutputParser 能逐块补全) 低(依赖模型生成合法文本)
withStructuredOutput 路线 Tool Calling → 参数提取 弱(结果最后一次性返回) 高(API 层保证格式)

选型建议

场景 推荐方案
需要严格 schema 校验、模型支持 Tool Calling withStructuredOutput
需要流式渐进展示结构化结果 JsonOutputParser 的 parsePartialResult
模型不支持 Tool Calling StructuredOutputParser.fromZodSchema
需要 XML/YAML 等特殊格式 对应的 OutputParser
解析失败需要自动修复 OutputFixingParser / RetryWithErrorOutputParser

一句话 :要流式渐进的结构化结果,走 JsonOutputParser 的 parsePartialResult;要严格的、经过 Zod 校验的最终对象,用 withStructuredOutput。

易错 :以为"流式调用"和"流式结构化结果"是一回事。前者是传输方式,后者是数据形态。withStructuredOutput 支持前者,不支持后者。

互动讨论

💬 JsonOutputParser 和 StructuredOutputParser 的核心区别是什么?

JsonOutputParser 只保证"能解析成 JS 对象",不校验字段名和类型。StructuredOutputParser 通过 fromNamesAndDescriptions 或 fromZodSchema 生成详细的格式说明注入 prompt,解析后再做校验,更严格但更可靠。

💬 withStructuredOutput 底层到底做了什么?

它把 Zod schema 转成 JSON Schema,注册成一个没有函数体的工具,模型在 tool_calls 里返回符合 schema 的 JSON 参数,LangChain 从 tool_calls[0].args 提取并做 Zod 校验,最终返回对象。

💬 为什么 withStructuredOutput 不能流式输出结构化结果?

因为 JSON 不完整时无法通过 Zod 校验。LangChain 会等模型完整返回全部 JSON 参数之后,一次性校验、拼装成结构化对象。它的流式只是底层 token 流的透传。

💬 Tool Calling 能实现 XML 结构化输出吗?

不能。原生 Tool Calling 协议只认 JSON,parameters 是 JSON Schema 对象,返回的 arguments 是 JSON 字符串。XML 只能走"prompt 约束 + OutputParser"的模拟路线。

💬 OutputParser 模块还有存在的必要吗?

有。结构化输出不只是 JSON,还有 XML、YAML、CSV 等格式。withStructuredOutput 虽然是最推荐的方式,但特殊格式仍需对应的 OutputParser 来解析。

相关推荐
智能工人1 小时前
Isaac Sim 6.1.0 导入URDF资产踩坑复盘:以Carter.urdf为例
人工智能
鬼手点金1 小时前
消息的四种角色
java·前端·人工智能·git·深度学习
m4Rk_1 小时前
【论文阅读】Agent记忆机制(106):COUNTERMEM——用可执行世界模型验证“如果当时换个动作”的反事实记忆
论文阅读·人工智能·学习·开源·github
人人都是软件工程师1 小时前
📚 Tesla AI 总监:让 LLM 把文档"编译"成持续生长的知识库 llm_wiki
人工智能·rust·开源
武子康1 小时前
灰度完成率从100%变成50%:五条失败记录怎样消失
人工智能·后端·agent
狂潮lister1 小时前
第6篇 掀翻共识:注意力叠权才是一切,异源对撞破同源自噬|【狂潮元理论 2.1‑public】
人工智能·llm
溯朢1 小时前
让AI从制度文档里建本体,怎么保证它不瞎建
人工智能
橙色云-智橙协同研发1 小时前
智搜·智驱·智淀:智橙PLM三大AI能力深度解析
人工智能
我是李工啊1 小时前
Prompt 不是越精确越好:提示词的战术执行与战略探索两种写法
人工智能