一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

LLM 结构化输出实战:用 LangChain 从 JSON 解析走向 withStructuredOutput

时间:2026-09-13 16:24:01 编辑:袖梨 来源:一聚教程网

在 LLM 应用中,让模型回答问题并不难,难的是让返回结果稳定进入后续程序。即使提示词要求输出 JSON,响应里仍可能出现 Markdown 包裹、额外说明或字段类型偏差。要解决这些问题,需要理解 LangChain 如何在调用前约束格式、在调用后解析数据,以及 withStructuredOutput 为什么更适合生产场景。

导语

做过 LLM 应用开发的同学一定遇到过这样的场景:你让大模型返回 JSON,它偏偏给你套一层 ```json ``` 的 Markdown 代码块;你想拿到一个数组,它却返回了一段自然语言描述。如何让大模型的输出"听话",是所有 AI 应用从 Demo 走向生产的第一道坎。

LangChain 作为当前最主流的 LLM 应用框架,提供了一整套结构化输出解决方案。但很多同学只会用 JSON.parse() 硬解析,踩坑无数;还有人连 getFormatInstructions() 到底干了什么都说不清楚。

今天这篇文章,我带大家从源码设计和实战代码出发,彻底吃透 LangChain 的结构化输出体系。


一、核心概念:结构化输出到底在解决什么问题?

1.1 一个生活化的比喻

把大模型想象成一位能力很强但很随性的翻译官。你告诉他"请用表格形式输出",他可能会:

  • 用 Markdown 表格
  • 用纯文本对齐
  • 用 JSON
  • 甚至用一段散文描述

LangChain 的结构化输出机制就是给这位翻译官配的"格式校对员" ——它负责两件事:

  1. 事前约定:通过 getFormatInstructions() 把格式要求"贴"到 Prompt 里,告诉模型该怎么输出
  2. 事后清洗:通过 parse() 把模型返回的内容(可能带 Markdown 包裹)解析成程序可用的对象

1.2 四种姿势,四个段位

层级方案约束强度底层原理适用场景
青铜手动正则 + JSON.parse纯字符串处理不推荐
白银JsonOutputParserPrompt 约定 + 解析简单 JSON
StructuredOutputParser.fromZodSchemaZod 转 Prompt 指令复杂字段
王者withStructuredOutput最强原生 Function Calling生产首选

二、核心疑点破解:getFormatInstructions() 到底干了什么?

这是 90% 的初学者都会困惑的一个点,也是理解结构化输出的关键。 我们把它单独拿出来讲透。

2.1 一句话说清楚它的职责

getFormatInstructions() 的作用是:将 Zod schema 转换为 LLM 能理解的输出格式指令,告诉大模型必须按照指定的 JSON 结构来输出。

具体来说,当你写下:

javascriptJavaScript

const parser = StructuredOutputParser.fromZodSchema(scientistSchema);

const question = `请介绍一下居里夫人的详细信息,${parser.getFormatInstructions()}`;
console.log(question);  // ? 打印出来你会发现多了一大段"格式化指令"

getFormatInstructions() 会根据你定义的 scientistSchema 自动生成一段提示词指令,类似这样:

text文本

You must format your output as a JSON value that adheres to a given "JSON Schema" instance.

"JSON Schema" is a declarative language that allows you to annotate and validate JSON documents.

For example, the example "JSON Schema" instance
{"properties": {"foo": {"description": "a list of test words", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
would match an object with one required property, "foo".

The "type" property specifies the type... 

The object MUST have the following properties: "name", "birth_year", "nationality", "fields", "awards", "major_achievement", "famous_theory", "biography".

... (包含所有字段的 JSON Schema 定义) ...

Please output the extracted information in JSON format according to this schema.

2.2 为什么必须拼在提示词里?

因为 LLM 默认会自由发挥输出格式。 这是最本质的原因。

image.png

  • 不加这段指令:模型可能返回一段纯文本的居里夫人介绍,字段全凭它心情
  • 加上这段指令:模型会严格按你定义的 schemanamebirth_yearnationalityfieldsawards 等)返回结构化 JSON

这样后面的 parser.parse(response.content) 才能成功解析为 scientistSchema 对应的对象。两段代码是配合使用的

javascript

// 事前:注入格式指令
const question = `请介绍一下居里夫人的详细信息,${parser.getFormatInstructions()}`;

// 事中:调用 LLM(此时模型已经被格式指令"洗脑")
const response = await model.invoke(question);

// 事后:解析(因为前面注入了指令,这里才大概率能解析成功)
const result = await parser.parse(response.content);

2.3 关键澄清:为什么 JsonOutputParser 打印为空?

注意getFormatInstructions() 的具体行为取决于解析器的实现,这也是最容易混淆的地方。

解析器是否传 schemagetFormatInstructions() 返回
JsonOutputParser❌ 无 schema空字符串 ""
JsonOutputParser✅ 带 schema详细的 JSON Schema 指令
StructuredOutputParser.fromNamesAndDescriptions字段级描述指令
StructuredOutputParser.fromZodSchema完整 JSON Schema 指令(较长)

所以:

javascript

// 场景 A:打印为空
const parser = new JsonOutputParser();
console.log(parser.getFormatInstructions());  // ""  ? 空!

// 场景 B:打印一大堆指令
const parser2 = new JsonOutputParser({ schema: scientistSchema });
console.log(parser2.getFormatInstructions());  // "Please output a JSON object..."

// 场景 C:也是详细指令
const parser3 = StructuredOutputParser.fromZodSchema(scientistSchema);
console.log(parser3.getFormatInstructions());  // "You must format your output as..."

为什么 JsonOutputParser 无 schema 时返回空? 设计者认为 JSON 是 LLM 训练语料的"常识格式" ,简单场景直接说"返回 JSON"模型就能理解,无需啰嗦。这属于 "约定优于配置" 的经典设计——零成本覆盖 80% 的简单需求

2.4 用生活比喻理解这个机制

想象你去餐厅点餐:

  • 不写格式指令getFormatInstructions 为空)→ 告诉服务员"随便给我来点吃的",厨师(LLM)自由发挥,可能给你端个炒饭,也可能端个拉面
  • 写了格式指令getFormatInstructions 返回详细 Schema)→ 告诉服务员"我要一份{主料:牛肉,配菜:土豆+胡萝卜,口味:微辣}",厨师必须按你的单子做

格式指令就是把"随便来点"变成"精确订单"的关键一步。


三、痛点与场景:为什么我们需要它?

3.1 一个真实的"翻车"现场

javascript

// ❌ 错误示范:直接 JSON.parse 模型输出
const response = await model.invoke("请返回爱因斯坦信息的JSON")
const jsonResult = JSON.parse(response.content)  // ? 大概率报错

为什么会失败? 因为 LLM 的输出往往长这样:

text

好的,这是爱因斯坦的信息:

```json
{
  "name": "阿尔伯特·爱因斯坦",
  "birth_year": 1879
}
```

希望对你有帮助!

直接 JSON.parse 必然抛出 Unexpected token 错误。这就是核心痛点:LLM 输出常被 Markdown 格式包裹,这是它展示信息的天性。

3.2 手动处理的"原始方案"

第一版解决方案非常典型:

javascript

// 使用正则提取 markdown ```json ... ``` 中的 JSON 内容
const match = response.content.match(/```jsons*([sS]*?)s*```/)
const jsonStr = match ? match[1] : response.content
const jsonResult = JSON.parse(jsonStr)

这段代码能跑,但有三个问题:

  1. 正则脆弱:模型可能用 ```JSON(大写)、```(无语言标记)等变体
  2. 重复造轮子:每个 AI 调用点都要写一遍
  3. 无语义约束:无法校验字段类型、必填项

LangChain 的封装就是把这段"业务脏活"标准化了。


四、重难点剖析(核心)

重难点一:StructuredOutputParser 的字段级约束升级

设计者为什么这么写?

StructuredOutputParser.fromNamesAndDescriptions 通过字段描述生成格式指令:

javascript

const parser = StructuredOutputParser.fromNamesAndDescriptions({
    name: '姓名',
    birth_year: '出生年份',
    nationality: '国籍',
    major_achievement: '主要成就,数组',
    famous_theory: '著名的理论',
})

但它有致命弱点——只描述字段名和语义,不约束类型。升级到 Zod:

javascript

const scientistSchema = z.object({
    name: z.string().describe('科学家的姓名'),
    birth_year: z.number().int().describe('出生年份,纯数字整数'),
    death_year: z.number().optional().describe('死亡年份,在世则缺省'),
    fields: z.array(z.string()).describe('科学家的领域,字符串数组'),
    awards: z.array(
        z.object({
            name: z.string(),
            year: z.number(),
            reason: z.string(),
        })
    ).describe('科学家获得的奖励,对象数组'),
});

const parser = StructuredOutputParser.fromZodSchema(scientistSchema);

Zod Schema 的威力

  • 类型强制(number vs string
  • 可选字段(.optional()
  • 嵌套对象和数组
  • 正则、枚举、范围校验

fromZodSchema 会把整个 Zod 结构转成一段 JSON Schema 描述,作为 getFormatInstructions() 的返回值,注入到 Prompt 里。


重难点二(重点):withStructuredOutput 才是终极答案

bindToolswithStructuredOutput 的演进

先看一个"偏门但可靠"的写法——手动 bindTools

javascript

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

const response = await modelWithToolCall.invoke('介绍一下爱因斯坦')
console.log(response.tool_calls[0].args)  // 需要手动取 tool_calls[0].args

核心洞察:这走的是模型原生 Function Calling 通道,比 Prompt 注入格式指令再手动解析可靠得多

但每次都手动 bindTools + 手动取 tool_calls[0].args 太啰嗦了。LangChain 为此提供了一步到位的封装

javascript

// ✨ 终极推荐写法
const structuredModel = model.withStructuredOutput(scientistSchema, {
    name: 'extract_scientist_info',
});

const result = await structuredModel.invoke('介绍一下爱因斯坦');
console.log(result.name);         // "阿尔伯特·爱因斯坦"
console.log(result.birth_year);   // 1879

withStructuredOutput 内部做的事

image.png

优势总结

维度手动 bindToolswithStructuredOutput
5~10 行1~2 行
返回内容tool_calls[0].args直接是对象
类型安全手动断言自动推导
兼容性处理自己写内部自动降级到 JSON 模式
推荐度⭐⭐⭐⭐⭐⭐⭐

三种方案的关键差异对比

image.png

核心差异

  • StructuredOutputParser"软约束" ——通过 Prompt 里注入格式指令,让模型"尽量"听话
  • withStructuredOutput"硬约束" ——通过模型原生 Function Calling 通道,让模型"必须"按规范输出

关键问题:OutputParser 还有存在必要吗?

答案是:有,但场景在收窄。

  • 需要 OutputParser:模型不支持 Function Calling(如某些开源小模型)、需要流式增量解析、简单 JSON 提取、教学演示
  • 不需要 OutputParser:生产环境 + 模型支持 tools → 无脑上 withStructuredOutput

标准决策流程

image.png

五、避坑指南/最佳实践

坑 1:搞不清 getFormatInstructions() 何时返回空

javascript

// ❌ 误区:以为所有 Parser 的 getFormatInstructions 都返回指令
const parser = new JsonOutputParser();
console.log(parser.getFormatInstructions());  // ""  ? 空的!

// ✅ 想要非空输出,给它传 schema
const parser2 = new JsonOutputParser({ schema: scientistSchema });
// 或者直接用 StructuredOutputParser.fromZodSchema

排查口诀看 Parser 类型,看是否传 schema。空字符串是设计行为,不是 bug。

坑 2:忘记把 getFormatInstructions() 拼到 Prompt 里

javascript

// ❌ 白调了指令,没拼进去
const question = `请介绍一下居里夫人`;
const response = await model.invoke(question);
const result = await parser.parse(response.content);  // ? 大概率失败

// ✅ 必须拼进去
const question = `请介绍一下居里夫人,${parser.getFormatInstructions()}`;

注意getFormatInstructions()不自动生效的,它只是返回一段字符串,你得自己拼到 Prompt 里。这是新手最常见的失误。

坑 3:正则匹配不严谨

javascript

// ❌ 只匹配小写 json
response.content.match(/```jsons*([sS]*?)s*```/)

// ✅ 使用解析器,内部已处理各种变体
const result = await parser.parse(response.content)

坑 4:字段描述写得太敷衍

javascript

// ❌ 描述含糊,模型容易搞错
z.object({ birth_year: z.number().describe('年份') })

// ✅ 明确语义 + 附加约束 + 示例
z.object({
    birth_year: z.number().int()
        .describe('出生年份,纯数字整数,例如 1879'),
    famous_theory: z.array(z.string())
        .describe('著名理论名称数组,例如 ["相对论", "光电效应"]'),
})

坑 5:无脑堆 OutputParser,忽视 withStructuredOutput

javascript

// ❌ 落后写法(模型支持 tools 的情况下)
const parser = StructuredOutputParser.fromZodSchema(schema)
const response = await model.invoke(`${question}n${parser.getFormatInstructions()}`)
const result = await parser.parse(response.content)

// ✅ 现代写法
const result = await model
    .withStructuredOutput(schema, { name: 'extract' })
    .invoke(question)

坑 6:不处理异常

javascript

async function safeInvoke(model, schema, prompt, maxRetry = 3) {
    for (let i = 0; i < maxRetry; i++) {
        try {
            const structuredModel = model.withStructuredOutput(schema)
            return await structuredModel.invoke(prompt)
        } catch (error) {
            console.warn(`第 ${i + 1} 次解析失败:`, error.message)
            if (i === maxRetry - 1) return null  // 降级
        }
    }
}

六、面试高频考点

考点 1:getFormatInstructions() 的作用是什么?为什么有的 Parser 返回空字符串?

回答要点

  1. 作用:把 Zod schema(或字段描述)转换成 LLM 能理解的 JSON Schema 指令,注入 Prompt 中,让模型输出从"自由文本"变成"结构化 JSON"
  2. 返回空的场景JsonOutputParser 无 schema 时返回 ""——设计者认为 JSON 是 LLM 的"常识格式",简单场景无需啰嗦,属于"约定优于配置"
  3. 必须手动拼到 Prompt 里getFormatInstructions() 只返回字符串,不会自动生效,新手最容易在这里翻车
  4. 配合关系getFormatInstructions() 负责"事前约定",parser.parse() 负责"事后清洗",两者缺一不可

考点 2:withStructuredOutputOutputParser 有什么本质区别?为什么不无脑用它?

回答要点

  1. 底层机制不同

    • OutputParser = Prompt 注入格式 + 手动正则清洗 + JSON.parse(软约束)
    • withStructuredOutput = 模型原生 Function Calling(tools,硬约束)
  2. 可靠性差异巨大:原生 function calling 走的是模型训练过的通道,不存在"忘记加 JSON 标记"的问题

  3. 不无脑用的原因

    • 模型兼容性:部分开源小模型不支持 tools
    • Token 开销:tools 定义会消耗额外 token
    • 流式场景:JsonOutputParser 支持增量解析,withStructuredOutput 通常返回完整对象
  4. 最佳实践:生产 + 支持 tools 的模型 → 首选 withStructuredOutput;其余场景按需选 Parser


考点 3:fromZodSchema 相比 fromNamesAndDescriptions 的优势是什么?

回答要点

  1. 类型约束:Zod 支持 .int().min().max().regex() 等校验,能生成更严格的格式指令
  2. 嵌套支持:支持对象数组、可选字段等复杂结构,fromNamesAndDescriptions 只能描述扁平字段
  3. 解析后校验:即使模型输出格式对了,还能用 Zod 二次校验类型,防止"格式对但语义错"
  4. 可复用:Zod Schema 可以同时用于 fromZodSchemawithStructuredOutputbindTools,一份定义多场景使用

七、总结

一句话记忆

  • getFormatInstructions()"把 Zod 翻译成 Prompt 指令" ——事前约定
  • parser.parse()"把 Markdown 洗成对象" ——事后清洗
  • JsonOutputParser 是"能不说话就不说话"的极简派
  • StructuredOutputParser 是"字字珠玑"的描述派
  • withStructuredOutput 是"直接走模型原生通道"的终极派

能用 withStructuredOutput 就别用 Parser。

热门栏目