结构化输出的策略自适应:在 Provider 原生与 Tool 模式间智能选择
结构化输出的策略自适应:在 Provider 原生与 Tool 模式间智能选择
学习目标
读完本章后,你将能够:
- 理解结构化输出的两种主流实现策略及其适用场景
- 设计自动策略检测机制,根据模型能力选择最优方案
- 处理模型不支持结构化输出时的回退路径
- 分析多结构化输出冲突的检测与处理
前置知识
- 工具调用协议与执行模型 — OpenAI Function Calling 协议
- Pydantic 数据模型基础
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 原生结构化输出。检测逻辑:
- 查询模型 Profile:通过模型元数据(如
model.profile.get("structured_output"))获取能力标志 - Provider 白名单:对已知支持的结构化输出的模型使用硬编码回退列表
- 工具冲突检测:某些模型(如 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_structured 在Agent 初始化时检测一次,之后固化路径。前者更灵活(provider 升级后自动切换),后者性能更好(少一次检测开销)。对于长寿命的 Agent 实例,编译时检测更合适。
陷阱:运行时降级到 free-text 时,输出格式完全依赖模型的自由生成——没有 schema 约束。这要求 Agent 的 prompt 中包含格式指令(如 “Respond with ‘Buy’, ‘Hold’, or ‘Sell’”),并在使用端做启发式解析(如 parse_rating() 的两阶段扫描)。这是”优雅降级”的代价:质量下降但不阻塞流水线。