03 · 结构化输出与 end_strategy
本章讲两个常被忽略但很关键的机制:① 「让模型返回一个对象而不是文本」有四种实现方式;② 一轮里出现多个工具调用时,谁先跑、谁当最终答案的裁决规则。
3.1 为什么需要「结构化输出」
LLM 默认吐自由文本,但应用层往往要一个结构化对象(比如 {name: str, color: str})。Pydantic AI 让你直接把一个 Pydantic 模型/dataclass 当 output_type,框架负责让模型产出符合它的数据并校验。
# 示意,非源码:把 Pydantic 模型当输出类型
from pydantic import BaseModel
from pydantic_ai import Agent
class Fruit(BaseModel):
name: str
color: str
agent = Agent('openai:gpt-5.2', output_type=Fruit)
result = agent.run_sync('A ripe banana')
print(result.output) # Fruit(name='banana', color='yellow'),已校验
3.2 四种输出落地方式
「让模型给结构化数据」在不同模型上有不同实现,Pydantic AI 把它们做成可选的 output marker(output.py):
| 方式 | marker | 怎么实现 | 何时用 |
|---|---|---|---|
| 工具 | ToolOutput | 把输出 schema 注册成一个特殊「输出工具」,模型「调用」它就等于交答案 | 默认、最通用 |
| 原生 | NativeOutput | 用 provider 原生的「结构化输出 / JSON 模式」 | 模型支持原生约束时 |
| 提示 | PromptedOutput | 把 schema 塞进 prompt,要求模型按格式回 JSON | 模型啥都不支持时的兜底 |
| 文本 | TextOutput | 用一个函数处理模型的纯文本输出 | 输出本就是文本/自定义解析 |
OutputMode(output.py:42)枚举了这些模式,auto 表示「按模型 profile 的 default_structured_output_mode 自动挑」。也就是说同一段 output_type=Fruit 代码,在不同模型上可能走不同的落地方式,由 profile 决定,你不用管。
关键直觉: 「输出工具」和「普通工具」在执行管线里是同类——都是工具调用。区别只在:输出工具命中 = 运行结束(走 End),普通工具命中 = 再问一轮。这就引出了下面的裁决问题。
3.3 问题:一轮里多个工具调用,谁说了算?
模型一次响应可能同时发:两个普通工具 + 一个输出工具。这就有歧义:
- 输出工具命中了,普通工具还跑不跑?
- 多个输出工具都命中,听谁的?
- 普通工具跑出来要求
ModelRetry,但输出工具也成功了,到底算结束还是重试?
这套规则就是 end_strategy,定义在 _agent_graph.py:70,由 process_tool_calls(_tool_execution.py:100)执行。
3.4 三种 end_strategy
EndStrategy = Literal['early', 'graceful', 'exhaustive'](_agent_graph.py:70)。三个处理器分别是 _EarlyProcessor / _GracefulProcessor / _ExhaustiveProcessor(_tool_execution.py:849,877,906),在 process_tool_calls 里按策略选用(_tool_execution.py:145-152)。
| 策略 | 行为 | 函数工具会跑吗 |
|---|---|---|
early | 输出工具按发出顺序跑,第一个成功就结束 | 仅当所有输出工具都失败时才跑(给模型纠错机会) |
graceful(默认) | 按发出顺序跑;输出工具前的函数工具先跑完,第一个成功的输出工具赢,后续输出工具跳过 | 跑,且每段内并行 |
exhaustive | 所有工具并行跑完;按发出顺序第一个有效输出当最终结果 | 全跑 |
默认从 v1 的 early 改成了 graceful(_agent_graph.py:87)。想保留旧行为(输出工具一成功立刻结束、函数工具不跑)就设 end_strategy='early'。
3.5 关键规则:retry-wins(重试优先)
这是最容易踩的语义。graceful / exhaustive 下:如果任何函数工具产出了 RetryPromptPart,最终输出会被压制,模型下一轮先去处理重试(_tool_execution.py:131-136)。
直觉:模型同时「调了个会失败的工具」和「给了最终答案」,那答案多半不靠谱——先让它把工具失败处理了再说。
几个例外(retry 不压制 output):
- 输出工具自己的重试不触发(「第一个有效输出就赢」)。
early策略不触发(该模式下函数工 具根本不和成功的输出并跑)。Agent.run_stream已经把流式输出提交了,也不压制。
3.6 sequential:把工具变成「栅栏」
默认同段工具并行跑。但有些工具有副作用、不能和别人并发。标 sequential=True(函数工具,或 ToolOutput(sequential=True))的工具是栅栏(barrier):它前面发的工具先跑完,它单独跑,它后面发的工具等它完才开始(_agent_graph.py:82、_tool_execution.py:127)。
还有运行级开关 parallel_execution_mode('sequential'),一刀切把每个工具都变成自己的栅栏(_tool_execution.py:129)。_segment_by_barriers(_tool_execution.py:78)就是按栅栏把工具调用切成一段段的逻辑。
3.7 输出重试预算
输出侧也有重试上限。GraphAgentState.consume_output_retry(_agent_graph.py:178)每次输出校验失败/空响应重试就 +1,超过 max_output_retries 抛 UnexpectedModelBehavior(_agent_graph.py:192)。注意它和每个工具自己的重试预算是分开计的。