如果只是和用户聊天,大模型返回自然语言就够了。
但程序更希望拿到:
yaml
{
name: 'Albert Einstein',
birth_year: 1879,
nationality: 'German'
}
而不是:
yaml
爱因斯坦出生于 1879 年,是一位德国物理学家......
这就是结构化输出要解决的问题:
让模型输出从"给人阅读的文本",变成"程序可以稳定处理的数据"。
一、最简单的方法:要求模型返回 JSON
可以直接在 Prompt 中写:
ini
const prompt = `
请介绍一下爱因斯坦。
以 JSON 格式返回,包含:
name
birth_year
nationality
major_achievement
`;
模型可能返回:
json
{
"name": "Albert Einstein",
"birth_year": 1879,
"nationality": "German",
"major_achievement": [
"Theory of Relativity"
]
}
然后:
ini
const result =
JSON.parse(response.content);
这样字符串就变成了 JavaScript 对象。
但问题也很明显。
模型可能返回:
go
```json
{
"name": "Albert Einstein"
}
```
也可能夹杂解释文字。
这时:
javascript
JSON.parse()
就可能直接失败。
二、JsonOutputParser
LangChain 提供:
javascript
import {
JsonOutputParser
} from '@langchain/core/output_parsers';
创建解析器:
ini
const parser =
new JsonOutputParser();
然后把格式要求加入 Prompt:
ini
const prompt = `
请介绍一下爱因斯坦。
${parser.getFormatInstructions()}
`;
调用模型:
ini
const response =
await model.invoke(prompt);
最后:
ini
const result =
await parser.parse(response.content);
流程变成:
javascript
Prompt
↓
要求输出 JSON
↓
LLM
↓
JSON 文本
↓
JsonOutputParser
↓
JavaScript Object
但这仍然只能解决:
javascript
是不是合法 JSON
不能保证:
必须有哪些字段
字段是什么类型
三、StructuredOutputParser
于是约束进一步升级。
javascript
import {
StructuredOutputParser
} from '@langchain/core/output_parsers';
可以声明字段:
php
const parser =
StructuredOutputParser
.fromNamesAndDescriptions({
name: '姓名',
birth_year: '出生年份',
nationality: '国籍',
major_achievement:
'主要成就'
});
然后:
ini
const prompt = `
请介绍一下爱因斯坦。
${parser.getFormatInstructions()}
`;
这次模型不仅知道:
javascript
返回 JSON
还知道:
javascript
JSON 应该有哪些字段
每个字段是什么意思
结构约束更明确了。
四、但字段名称正确还不够
假设我们希望:
birth_year
必须是:
typescript
number
结果模型返回:
json
{
"birth_year": "1879"
}
这依然是合法 JSON。
甚至字段名也是对的。
但类型错了。
真实业务通常还会要求:
typescript
name 必须是 string
birth_year 必须是 number
major_achievement 必须是 string[]
某些字段可选
某些字段又是嵌套对象
所以真正需要的是:
Schema
五、Zod:给数据定义 Schema
Zod 是 JavaScript / TypeScript 中常见的 Schema 校验库。
例如:
css
import { z } from 'zod';
const schema = z.object({
name: z.string(),
birth_year: z.number(),
nationality: z.string(),
major_achievement:
z.array(z.string()),
famous_theory:
z.string().optional()
});
它表达的是:
typescript
整个结果必须是 object
name
→ string
birth_year
→ number
nationality
→ string
major_achievement
→ string[]
famous_theory
→ 可选 string
Zod 的核心作用一句话就够:
定义数据应该长什么样,并在运行时验证数据。
例如:
php
schema.parse({
name: 'Einstein',
birth_year: '1879'
});
这里:
birth_year
本来要求:
typescript
number
却传入:
c
string
Zod 会直接发现类型不符合 Schema。
六、在 LangChain 中使用 Zod
可以把 Zod Schema 交给:
StructuredOutputParser
例如:
css
const schema = z.object({
name: z.string()
.describe('姓名'),
birth_year: z.number()
.describe('出生年份'),
nationality: z.string()
.describe('国籍'),
major_achievement:
z.array(z.string())
.describe('主要成就')
});
const parser =
StructuredOutputParser
.fromZodSchema(schema);
然后:
ini
const prompt = `
请介绍一下爱因斯坦。
${parser.getFormatInstructions()}
`;
调用:
ini
const response =
await model.invoke(prompt);
const result =
await parser.parse(response.content);
现在约束已经从:
javascript
JSON
升级到了:
diff
具体字段
+
具体类型
+
数据结构
七、Tool Calling 又做了什么
前面的 Parser 思路,本质是:
javascript
让模型生成一段文本
↓
文本里包含 JSON
↓
再解析 JSON
另一种思路是:
不让模型"写 JSON 文本",而是让模型生成符合工具参数定义的结构化参数。
例如定义:
css
const weatherTool = {
name: 'get_weather',
description: '获取天气',
schema: z.object({
city: z.string()
})
};
模型需要调用工具时,可能产生:
css
{
name: 'get_weather',
args: {
city: '杭州'
}
}
这里:
args
已经是结构化数据。
因此 Tool Calling 也经常被用于结构化输出。
八、现在更直接的写法:withStructuredOutput()
如果目的不是调用真实工具,只是:
我希望模型按照 Schema 返回数据。
那么现在可以直接:
css
const schema = z.object({
name: z.string(),
birth_year: z.number(),
nationality: z.string(),
major_achievement:
z.array(z.string())
});
然后:
ini
const structuredModel =
model.withStructuredOutput(schema);
调用:
arduino
const result =
await structuredModel.invoke(
'介绍一下爱因斯坦'
);
拿到的结果直接可以按照 Schema 使用:
arduino
console.log(result.name);
console.log(result.birth_year);
代码已经从:
css
LLM
↓
字符串
↓
Parser
↓
Object
逐渐演进成:
css
Schema
↓
LLM
↓
Structured Object
九、整个结构化输出主线其实很简单
从最开始一路看下来:
scss
自然语言
↓
要求 JSON
↓
JSON.parse()
↓
JsonOutputParser
↓
StructuredOutputParser
↓
Zod Schema
↓
Tool Calling
↓
withStructuredOutput()
每一步都只是在解决同一个问题:
如何让大模型输出越来越稳定、越来越适合程序直接使用。
可以把几个方案简单理解成:
scss
JSON
→ 有格式
StructuredOutputParser
→ 有字段
Zod
→ 有字段 + 有类型
withStructuredOutput()
→ 直接把结构要求交给模型调用
真正开发新项目时,如果当前模型支持结构化输出,通常优先考虑:
ini
model.withStructuredOutput(schema);
而了解前面的 Parser 演进,是为了真正理解:
javascript
Schema 为什么存在
Parser 在解决什么
结构化输出究竟比普通 JSON 强在哪里
最终目的不是"学会更多 API",而是让模型输出的数据能够真正进入业务代码。