前面讲了 Agent(createAgent)和结构化输出。把它们合在一起,就是做一个能对接程序、又能自主调用工具的智能体——它先跑完 ReAct 循环,再按你指定的 schema 交出结构化结果。这个「以什么方式产出结构化结果」的选择,就是 responseFormat 的两种策略:toolStrategy 和 providerStrategy。这篇把两者讲透:原理、选项、兼容性,以及怎么根据模型自动选。
一、为什么有「两种策略」
让 Agent 输出指定结构,底层有两条路:
- 靠工具调用:把一个「隐藏工具」塞给模型,工具的参数就是你想要的字段;模型「调用」它,把结果填进参数里,框架再解析、校验。这条路的代表是
toolStrategy。 - 靠厂商原生能力:把 schema 作为
response_format: json_schema直接交给模型厂商的接口,模型不经工具调用、直接按 schema 产出。这条路的代表是providerStrategy。
两者对使用者的呈现是一样的——结果都放在 result.structuredResponse 里。区别在兼容性和约束强度。
| 策略 | 原理 | 兼容性 | 约束强度 |
|---|---|---|---|
toolStrategy | 借工具调用产出 | 只要模型支持工具调用即可 | 依赖模型遵守工具参数 schema |
providerStrategy | 厂商原生结构化输出 | 需厂商支持(OpenAI/Anthropic/xAI 等) | 最强,由厂商强制 |
结论先行:能用原生就
providerStrategy,求稳就用toolStrategy;拿不定主意,直接把 schema 丢给responseFormat让框架自动选。
二、toolStrategy:借工具调用产出结构化结果
import { z } from "zod";
import { createAgent, toolStrategy, initChatModel } from "langchain";
const model = await initChatModel("deepseek:deepseek-chat", { temperature: 0 });
const ContactSchema = z.object({
name: z.string().describe("人名"),
age: z.number().describe("年龄(整数)"),
city: z.string().describe("所在城市"),
});
const agent = createAgent({
model,
tools: [], // 只做抽取,没有业务工具
responseFormat: toolStrategy(ContactSchema),
});
const res = await agent.invoke({
messages: [{ role: "user", content: "从这句话提取信息:张三,今年 30 岁,住在上海。" }],
});
console.log(res.structuredResponse);
// { name: '张三', age: 30, city: '上海' }
console.log(typeof res.structuredResponse.age); // "number"
结果不在 messages 里,而是单独放在 result.structuredResponse。 但 messages 会记录它是怎么产出的:
res.messages.map((m) => m.type); // ["human", "ai", "tool", "ai"]
那条 ai 消息的 tool_calls 里,藏着模型调用的「隐藏工具」:
res.messages.find((m) => m.tool_calls?.length).tool_calls[0];
// { name: "extract-1", args: { name: "张三", age: 30, city: "上海" }, id: "call_xxx", ... }
toolStrategy 内部做了四件事:
- 把
ContactSchema转成 JSON Schema,包装成一个临时函数工具(名字形如extract-1); - 把它绑给模型,模型「调用」它,参数就是抽取结果;
- 框架解析工具参数 → 用 schema 校验 → 写入
structuredResponse; - 同时在消息历史里补一条
tool消息和一条ai消息,保证对话合法。
三、toolStrategy 的选项
toolStrategy(schema, {
toolMessageContent: "已收到结构化结果",
handleError: false,
});
3.1 toolMessageContent
自定义「产出结构化结果后」那条 AI 消息 的文本。默认是 Returning structured response: {...},设置后变成你给的内容:
const agent = createAgent({
model, tools: [],
responseFormat: toolStrategy(ContactSchema, { toolMessageContent: "已收到结构化结果" }),
});
const res = await agent.invoke({ messages: [{ role: "user", content: "张三 30 岁 上海" }] });
res.messages.at(-1).content; // "已收到结构化结果"
res.structuredResponse; // { name: '张三', age: 30, city: '上海' }(不受影响)
3.2 handleError
模型用工具调用产出结构化结果时可能出错,最典型的是一次调用了多个结构化工具——框架不知道该用哪个,会抛 MultipleStructuredOutputsError。handleError 决定怎么处理:
| 取值 | 行为 |
|---|---|
true(默认) | 自动重试 |
false | 直接抛错,交给你的代码处理 |
string | 用这条文案提示模型重试 |
function | 自定义处理:返回字符串则按该字符串重试,抛错则终止 |
下面用 handleError: false 故意让模型同时调用两个工具,观察报错:
const agent = createAgent({
model, tools: [],
responseFormat: toolStrategy([Person, Company], { handleError: false }),
});
try {
await agent.invoke({
messages: [{ role: "user", content: "请同时调用两个工具……" }],
});
} catch (e) {
console.log(e.constructor.name); // MultipleStructuredOutputsError
console.log(e.message);
// The model has called multiple tools: extract-5, extract-6 ... This is not supported.
}
四、多个 schema:让模型自己选形状
toolStrategy 还接受一组 schema,模型根据输入内容自己决定调用哪一个——相当于给结构化输出加了一层「联合类型」。
const Person = z.object({
type: z.literal("person"),
name: z.string().describe("人名"),
age: z.number().describe("年龄"),
});
const Company = z.object({
type: z.literal("company"),
companyName: z.string().describe("公司名"),
employees: z.number().describe("员工数"),
});
const agent = createAgent({
model, tools: [],
responseFormat: toolStrategy([Person, Company]),
});
const r1 = await agent.invoke({ messages: [{ role: "user", content: "张三,30 岁。" }] });
// { type: 'person', name: '张三', age: 30 }
const r2 = await agent.invoke({ messages: [{ role: "user", content: "ACME 公司有 100 名员工。" }] });
// { type: 'company', companyName: 'ACME', employees: 100 }
两次输出的形状不同,模型自动挑了匹配的那个 schema。代价是它有可能一次调用多个(见上一节的 MultipleStructuredOutputsError)。
另一种多形状写法是
z.discriminatedUnion(...),但注意顶层直接放联合在部分厂商会报错(见 demo-11),实践中用「一组 schema」更省事。
五、providerStrategy:走厂商原生能力
providerStrategy 把 schema 直接交给厂商原生的结构化输出接口,模型不经过工具调用、直接按 schema 产出,约束最强。它接受两种形式的入参:
import { providerStrategy } from "langchain";
// 形式一:直接给 schema
providerStrategy(z.object({ ... }));
// 形式二:显式指定 strict(默认 true)
providerStrategy({ schema: z.object({ ... }), strict: true });
用法:
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
responseFormat: providerStrategy(ContactSchema),
});
const res = await agent.invoke({ messages: [{ role: "user", content: "..." }] });
console.log(res.structuredResponse);
代价是兼容性:厂商不支持原生 json_schema 时会直接报错。比如 DeepSeek:
providerStrategy => 失败:400 This response_format type is unavailable now
原因是 DeepSeek 模型的 profile 里 structuredOutput = false,不支持原生结构化输出。这时必须改用 toolStrategy。
六、不想手选?直接给 schema 自动挑
如果不包 toolStrategy / providerStrategy,直接把 schema 交给 responseFormat,框架会根据模型的 model.profile.structuredOutput 自动选择:
const agent = createAgent({
model,
tools: [],
responseFormat: ContactSchema, // 直接给 schema
});
const res = await agent.invoke({ messages: [{ role: "user", content: "..." }] });
console.log(res.structuredResponse); // { name: '张三', age: 30, city: '上海' }
判断规则:
- 支持原生(
structuredOutput = true) → 自动用providerStrategy; - 不支持 → 自动退化为
toolStrategy(DeepSeek 就是这一类)。
这是最省心的写法:你不必记住哪个模型支持什么,交给框架即可。
七、和业务工具一起用
结构化输出最有价值的场景,是和业务工具配合:Agent 先调用工具拿数据,再按 schema 汇总成结构化结果。
const getWeather = tool(({ city }) => `${city} 今天晴,气温 25℃`, {
name: "get_weather",
description: "查询指定城市今天的天气",
schema: z.object({ city: z.string().describe("城市名") }),
});
const WeatherReport = z.object({
city: z.string().describe("城市"),
summary: z.string().describe("一句话天气总结"),
});
const agent = createAgent({
model,
tools: [getWeather],
responseFormat: toolStrategy(WeatherReport),
});
const res = await agent.invoke({
messages: [{ role: "user", content: "北京今天天气怎么样?给个总结。" }],
});
console.log(res.messages.map((m) => m.type));
// ["human", "ai", "tool", "ai", "tool", "ai"]
// ↑先调业务工具 get_weather ↑再调结构化工具
console.log(res.structuredResponse);
// { city: '北京', summary: '北京今天晴,气温25℃,天气不错。' }
从消息序列可以看出:Agent 先把业务工具跑完(get_weather),最后才产出结构化结果。
八、Python 对应写法
Python 侧能力与 JS 完全对齐:create_agent 用 response_format,结果放在 structured_response,两个策略类定义在 langchain.agents.structured_output。
from pydantic import BaseModel, Field
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy, ProviderStrategy
class Contact(BaseModel):
name: str = Field(description="人名")
age: int = Field(description="年龄")
city: str = Field(description="所在城市")
# toolStrategy(兼容性最好)
agent = create_agent(
model="deepseek:deepseek-chat",
tools=[],
response_format=ToolStrategy(Contact),
)
result = agent.invoke({"messages": [{"role": "user", "content": "张三,30 岁,上海"}]})
print(result["structured_response"]) # Contact(name='张三', age=30, city='上海')
# providerStrategy(厂商原生,需支持)
agent = create_agent(model="gpt-4o", tools=[], response_format=ProviderStrategy(Contact))
# 直接给 schema,自动选择
agent = create_agent(model="gpt-4o", tools=[], response_format=Contact)
参数名的对应关系:
| 能力 | JS | Python |
|---|---|---|
| 自定义消息 | toolMessageContent | tool_message_content |
| 错误处理 | handleError | handle_errors |
| 严格模式 | providerStrategy({ strict }) | ProviderStrategy(schema, strict=...) |
| 结果字段 | result.structuredResponse | result["structured_response"] |
Python 的 handle_errors 还支持传异常类型(ValueError / (ValueError, TypeError)),只对指定异常重试,覆盖面比 JS 更广。
九、小结与选型
两种策略:
toolStrategy(schema[, options]):借工具调用产出,兼容性最好;选项toolMessageContent、handleError;支持传一组 schema 让模型自选形状。providerStrategy(schema | { schema, strict }):厂商原生,约束最强,但需厂商支持。
自动选择:直接把 schema 交给 responseFormat,框架按 model.profile.structuredOutput 自动在两者间选择(不支持原生就退化为 toolStrategy),最省心。
结果位置:无论哪种策略,结构化结果都在 result.structuredResponse;和业务工具配合时,Agent 会先跑完工具,再产出结构化结果。
一句话:能用原生就 providerStrategy,求稳就用 toolStrategy;拿不定主意,直接给 schema 让框架自动挑。