跳到主要内容

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用一个函数处理模型的纯文本输出输出本就是文本/自定义解析

OutputModeoutput.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_retriesUnexpectedModelBehavior_agent_graph.py:192)。注意它和每个工具自己的重试预算是分开计的。

3.8 巧妙之处

  • 输出和工具统一管线:输出就是一种特殊工具,不用为「结构化输出」单开一套代码路径——复用工具执行 + 校验。
  • retry-wins 防幻觉答案:模型「边失败边给答案」时优先纠错,避免把不可靠输出当最终结果。
  • 模式可由 profile 自动选:同一份业务代码跨 provider 复用,落地方式交给 ModelProfile

3.9 代码地图

主题文件关键符号
输出 markerpydantic_ai_slim/pydantic_ai/output.pyToolOutput, NativeOutput, PromptedOutput, TextOutput, OutputMode
输出内部pydantic_ai_slim/pydantic_ai/_output.pyBaseOutputProcessor
end_strategy 定义pydantic_ai_slim/pydantic_ai/_agent_graph.pyEndStrategy
工具执行裁决pydantic_ai_slim/pydantic_ai/_tool_execution.pyprocess_tool_calls, _EarlyProcessor, _GracefulProcessor, _ExhaustiveProcessor, _segment_by_barriers
输出重试预算pydantic_ai_slim/pydantic_ai/_agent_graph.pyGraphAgentState.consume_output_retry