跳转到内容

结构化输出的策略自适应:在 Provider 原生与 Tool 模式间智能选择

结构化输出的策略自适应:在 Provider 原生与 Tool 模式间智能选择

学习目标

读完本章后,你将能够:

  • 理解结构化输出的两种主流实现策略及其适用场景
  • 设计自动策略检测机制,根据模型能力选择最优方案
  • 处理模型不支持结构化输出时的回退路径
  • 分析多结构化输出冲突的检测与处理

前置知识


1. 概念定义

结构化输出(Structured Output)是指让 LLM 返回符合预定义 Schema 的 JSON 数据,而非自由文本。这在需要程序化处理模型输出的场景中至关重要(如数据提取、分类、API 参数生成)。

实现结构化输出有两种主流策略:

策略实现方式适用模型
ProviderStrategy使用模型 Provider 的原生 Structured Output API(如 OpenAI response_format={"type": "json_schema"}支持原生结构化输出的模型
ToolStrategy将输出 Schema 包装为工具定义,引导模型通过 tool_call 返回结构化数据所有支持 Function Calling 的模型

策略自适应是指框架在运行时自动检测模型能力,选择最优策略的过程。

2. 三种策略模式

2.1 AutoStrategy(自动检测)

最上层的抽象。用户提供一个 Pydantic Schema,框架在模型调用时自动检测:

AutoStrategy 使得用户只需关注 Schema 定义,无需关心底层实现细节。

2.2 ProviderStrategy(原生结构化输出)

使用模型 Provider 提供的原生 API 来强制输出符合 Schema 的 JSON。

优势

  • 不消耗 tool_call 配额(某些模型对 tool_call 数量有限制)
  • 输出更纯粹(没有工具调用的额外元数据)
  • 性能更好(直接 JSON 解析,无需工具路由)

限制

  • 仅部分模型支持(如 OpenAI GPT-4o 及以上版本)
  • 与 tool_call 可能不兼容(某些模型不支持同时使用结构化输出和工具调用)

2.3 ToolStrategy(工具调用模式)

将输出 Schema 包装为一个虚拟工具,引导模型通过 tool_call 返回结构化数据。

优势

  • 兼容所有支持 Function Calling 的模型
  • 可以与真实工具调用共存
  • 天然支持结构化输出的”重试”机制(模型可以多次调用工具来修正输出)

限制

  • 消耗 tool_call 配额
  • 输出中包含工具调用的元数据(tool_call_id 等)

3. 模型能力检测

策略自适应的核心是判断模型是否支持 Provider 原生结构化输出。检测逻辑:

  1. 查询模型 Profile:通过模型元数据(如 model.profile.get("structured_output"))获取能力标志
  2. Provider 白名单:对已知支持的结构化输出的模型使用硬编码回退列表
  3. 工具冲突检测:某些模型(如 Gemini < 3 系列)在同时使用工具调用和结构化输出时有冲突,需特殊处理
# 伪代码:能力检测逻辑
def supports_provider_strategy(model, tools=None):
# 1. 通过模型 profile 查询
if model.profile and model.profile.get("structured_output"):
# 排除不支持同时使用工具和结构化输出的模型
if tools and "gemini" in model.name and "gemini-3" not in model.name:
return False
return True
# 2. 回退到已知模型列表
return any(prefix in model.name for prefix in ["gpt-4o", "gpt-5", "grok"])

4. 多结构化输出处理

当模型在一次响应中调用了多个结构化输出工具时,需要检测并处理:

4.1 多个结构化输出冲突

处理策略

  • 配置 handle_errors=False:直接抛出 MultipleStructuredOutputsError
  • 配置 handle_errors=True:注入错误 ToolMessage,引导模型重试
  • 配置 handle_errors=ExceptionType:仅对特定异常类型重试
  • 配置 handle_errors=callable:使用自定义函数处理

4.2 结构化输出验证失败

模型返回的结构化数据可能不符合 Schema:

# 伪代码:验证失败处理
try:
structured_response = output_tool.parse(model_output)
except ValidationError as exc:
if should_retry:
# 注入错误消息,引导模型修正
error_msg = f"Error: {exc}. Please fix your mistakes."
return {"messages": [output, ToolMessage(content=error_msg)]}
else:
raise StructuredOutputValidationError(...)

自动重试机制:验证失败时,框架注入错误 ToolMessage,模型在下一次调用中看到错误并修正输出。重试次数由 Agent 的递归限制控制。

5. 问题与规避

5.1 工具与结构化输出的兼容性

问题:某些模型不支持同时使用工具调用和结构化输出(如 Gemini < 3 系列)。

对策:在能力检测中增加工具冲突检查,当同时使用工具和结构化输出时,自动降级为 ToolStrategy。

5.2 结构化输出的 Token 开销

问题:ToolStrategy 需要将完整 Schema 作为工具定义发送给模型,增加系统 Token 消耗。

对策

  • 对大型 Schema 使用字段精简(去除冗余 description)
  • 使用 ProviderStrategy 时不消耗额外 Token(原生 API 支持)
  • 缓存编译后的工具定义,避免重复序列化

5.3 策略切换时的工具名称一致性

问题:AutoStrategy 在检测到 ProviderStrategy 后,原本为 ToolStrategy 预注册的工具名称可能不一致。

对策:ToolStrategy 在 Agent 创建时就预注册所有结构化输出工具,AutoStrategy 切换策略时复用相同的工具名称和 Schema。


参考来源


补充:编译时检测 + 运行时双降级容错

来源:TradingAgents(TauricResearch/TradingAgents)tradingagents/agents/utils/structured.py,commit 61522e1

在 Agent 管道中,结构化输出的容错不仅需要在运行时处理失败,还需要在编译时检测 provider 是否支持。TradingAgents 实现了一个两阶段降级模式:

第一阶段:编译时检测(Agent 初始化)

# 伪代码:TradingAgents 模式
def bind_structured(llm, schema, agent_name):
try:
return llm.with_structured_output(schema) # 尝试绑定
except (NotImplementedError, AttributeError):
# 编译时就知道该 provider 不支持,后续直接走 free-text
return None

这个绑定结果缓存在 Agent 实例中。后续每次调用都基于这个预检查结果决定路径,而不是每次调用都重新检测。

第二阶段:运行时双降级

两级降级的区别

降级层级触发时机原因
编译时降级Agent 初始化provider 不支持 with_structured_output
运行时降级单次调用结构化调用失败(JSON 格式错误、临时故障)

为什么需要双降级?

  • 编译时降级处理能力差异:Ollama 本地模型、旧版 API 不支持结构化输出
  • 运行时降级处理瞬时故障:即使 provider 支持,模型仍可能返回 malformed JSON 或触发 transient error

与 AutoStrategy 的对比

本文前面介绍的 AutoStrategy 在每次调用时检测能力。TradingAgents 的 bind_structuredAgent 初始化时检测一次,之后固化路径。前者更灵活(provider 升级后自动切换),后者性能更好(少一次检测开销)。对于长寿命的 Agent 实例,编译时检测更合适。

陷阱:运行时降级到 free-text 时,输出格式完全依赖模型的自由生成——没有 schema 约束。这要求 Agent 的 prompt 中包含格式指令(如 “Respond with ‘Buy’, ‘Hold’, or ‘Sell’”),并在使用端做启发式解析(如 parse_rating() 的两阶段扫描)。这是”优雅降级”的代价:质量下降但不阻塞流水线。