创见博客
Agent 结构化输出的两种策略:toolStrategy 与 providerStrategy
七崽爱吃小饼干2026/10/15阅读 0

前面讲了 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:借工具调用产出结构化结果

js
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 会记录它是怎么产出的:

js
res.messages.map((m) => m.type); // ["human", "ai", "tool", "ai"]

那条 ai 消息的 tool_calls 里,藏着模型调用的「隐藏工具」:

js
res.messages.find((m) => m.tool_calls?.length).tool_calls[0];
// { name: "extract-1", args: { name: "张三", age: 30, city: "上海" }, id: "call_xxx", ... }

toolStrategy 内部做了四件事:

  1. 把 ContactSchema 转成 JSON Schema,包装成一个临时函数工具(名字形如 extract-1);
  2. 把它绑给模型,模型「调用」它,参数就是抽取结果;
  3. 框架解析工具参数 → 用 schema 校验 → 写入 structuredResponse;
  4. 同时在消息历史里补一条 tool 消息和一条 ai 消息,保证对话合法。

三、toolStrategy 的选项

js
toolStrategy(schema, {
  toolMessageContent: "已收到结构化结果",
  handleError: false,
});

3.1 toolMessageContent

自定义「产出结构化结果后」那条 AI 消息 的文本。默认是 Returning structured response: {...},设置后变成你给的内容:

js
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 故意让模型同时调用两个工具,观察报错:

js
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,模型根据输入内容自己决定调用哪一个——相当于给结构化输出加了一层「联合类型」。

js
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 产出,约束最强。它接受两种形式的入参:

js
import { providerStrategy } from "langchain";

// 形式一:直接给 schema
providerStrategy(z.object({ ... }));

// 形式二:显式指定 strict(默认 true)
providerStrategy({ schema: z.object({ ... }), strict: true });

用法:

js
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 自动选择:

js
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 汇总成结构化结果。

js
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。

python
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)

参数名的对应关系:

能力JSPython
自定义消息toolMessageContenttool_message_content
错误处理handleErrorhandle_errors
严格模式providerStrategy({ strict })ProviderStrategy(schema, strict=...)
结果字段result.structuredResponseresult["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 让框架自动挑。

评论
0/100