Pydantic AI 如何处理输出截断
Pydantic AI 2.68.0 将非流式输出截断纳入 ModelFallback 默认兜底,但旧版 FallbackModel 与已开始输出的流式请求仍需单独配置。
正文
核心变化
Pydantic AI 2.68.0 把“模型返回了 finish_reason='length'”从一个容易被当成正常文本的边界状态,提升成了可以参与模型兜底决策的公开异常。对使用 ModelFallback 的 Agent 来说,默认 fallback_on 现在包含 OutputTokenLimitExceeded,非流式请求被输出上限截断后会尝试下一个模型;对继续使用 FallbackModel 的代码来说,默认值仍然只有 ModelAPIError,不会自动切换。这个差异决定了升级后哪些调用会改变结果,不能只看版本号就认为两种 fallback 都变了。
这项变化解决的是一个很具体的问题:普通文本被截断时,应用以前可能拿到一段看起来完整的结果,既没有异常,也没有自动兜底信号。结构化输出或工具调用则可能先重试,再以泛化的输出错误结束。2.68.0 新增 OutputTokenLimitExceeded,并让 ModelFallback 用它匹配输出已经触及上限的响应。发布说明把这项改动列为兼容性变化,意味着已有应用需要重新检查 max_tokens、fallback_on 和用量限制之间的关系。
如果项目通过 模型与平台目录 维护多个模型入口,升级后应把“截断是否换模型”当成接入契约的一部分;如果已经处理过 Pydantic AI 的结构化结果,还可以对照站内的 引用字段迁移实践,把模型响应元数据和最终展示文本分开保存。
ModelFallback 的默认行为
ModelFallback 是 Agent capability。它先尝试当前步骤选中的模型,再按配置尝试候选模型。2.68.0 的默认 fallback_on 是:
(ModelAPIError, ContentFilterError, OutputTokenLimitExceeded)
其中,ModelAPIError 对应模型请求本身失败,ContentFilterError 代表被判定为拒答的响应,OutputTokenLimitExceeded 代表响应到达输出 token 上限。第三类不仅覆盖空响应,也覆盖普通文本、结构化输出和工具调用参数已经被截断的响应。官方文档明确说,ModelFallback 会把所有 finish_reason='length' 的非流式响应视为需要判断的截断结果,不能根据当前文本“看起来已经完整”就自行放过。
最小复现可以不连接真实供应商,用 FunctionModel 返回两个确定性响应:
from pydantic_ai import Agent, ModelMessage, ModelResponse, TextPart
from pydantic_ai.capabilities import ModelFallback
from pydantic_ai.models.function import AgentInfo, FunctionModel
def cut_off(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
return ModelResponse(
parts=[TextPart('检查结果:配置已加载,')],
finish_reason='length',
)
def answer(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse:
return ModelResponse(parts=[TextPart('检查结果:配置与权限均通过。')])
agent = Agent(
FunctionModel(cut_off, model_name='primary'),
capabilities=[ModelFallback(FunctionModel(answer, model_name='backup'))],
)
result = agent.run_sync('报告配置检查结果。')
assert result.output == '检查结果:配置与权限均通过。'
assert result.response.failed_attempts[0].model_name == 'primary'
这里的重点不是把第二个模型当成“更强模型”,而是验证 fallback 的判定点:第一个响应已经返回,但它的结束原因是 length,所以在非流式路径中被拒绝并转到下一个候选。官方测试还覆盖了空响应、不完整工具调用和已经能够解析的工具调用;只要结束原因是输出上限,默认策略都会先拒绝这次响应。这样做是有意的,因为框架无法可靠判断一段文本是否只是“刚好在上限处结束”,而模型也可能在工具调用已解析后才触达限制。
如果所有候选都被截断,结果不是最后一段半截文本,而是 FallbackExceptionGroup。它会列出每次尝试及其结果。对生产日志来说,这比只记录最终文本更重要:可以区分“首个模型被长度限制拒绝,但备用模型成功”和“所有候选都不够用”。同时,官方测试说明被拒绝响应产生的 token 仍计入本次运行的用量,并在下一个尝试前参与用量限制检查;fallback 不是免费重试,也不会绕过 Agent 的 token 或成本上限。
FallbackModel 不会自动跟随升级
FallbackModel 与 ModelFallback 不是同一个入口。前者是一个模型包装器,适合直接发起模型请求或在 Agent 外部使用;后者是 Agent capability。2.68.0 只改变了 ModelFallback 的默认值,FallbackModel 仍然默认使用:
(ModelAPIError,)
因此下面的代码仍会把截断文本当作结果:
from pydantic_ai import Agent, ModelAPIError
from pydantic_ai.models.fallback import FallbackModel
from pydantic_ai.exceptions import ContentFilterError, OutputTokenLimitExceeded
model = FallbackModel(primary, backup)
agent = Agent(model)
若希望保留 FallbackModel 的调用形态,同时让输出截断触发切换,需要显式加入异常:
model = FallbackModel(
primary,
backup,
fallback_on=(ModelAPIError, OutputTokenLimitExceeded),
)
另一种迁移路径是把 Agent 的首选模型放回 Agent,其余候选放入 ModelFallback:
agent = Agent(
primary,
capabilities=[ModelFallback(backup)],
)
这两种写法不能只按名称替换。ModelFallback 的候选尝试会从当前步骤选中的模型开始,并为每个候选重新准备模型请求;FallbackModel 则继续作为包装模型工作。更重要的是,给 fallback_on 传入一个自定义响应处理器时,会替换默认策略,而不是在默认列表后追加。比如:
def is_blank(response: ModelResponse) -> bool:
return not (response.text or '').strip()
ModelFallback(backup, fallback_on=is_blank)
这样写之后,API 错误、拒答和输出截断都不会再按默认值兜底。若既要保留内置条件,又要增加空文本判断,应传入列表:
ModelFallback(
backup,
fallback_on=[ModelAPIError, ContentFilterError, OutputTokenLimitExceeded, is_blank],
)
迁移时至少要测试三条路径:首选模型返回 length 后备用模型成功;首选和备用都返回 length;以及把 OutputTokenLimitExceeded 从 fallback_on 移除后,应用是否确实需要接受截断文本。对有严格结构化输出的 Agent,还要覆盖不完整 JSON 和不完整工具参数,不能只测普通字符串。
流式请求是另一条边界
非流式响应可以等到结束原因已知后再决定是否拒绝;流式请求则不同。Pydantic AI 文档说明,ModelFallback 会等待候选流的第一个事件。如果请求在任何输出到达前就失败,仍可能切换到下一个模型;一旦应用已经收到输出,后续的失败、拒答或 length 结束原因就不能再把已经发送给调用方的内容收回,也不会自动启动另一个模型。
因此,下面两类情况必须分开处理:
- 连接在首个事件前失败,或者供应商在接受请求后立即返回错误。此时 fallback 还有机会工作。
- 文本、思考片段或工具参数已经流出,最后事件才说明
length。此时流会被接受,应用需要自己处理不完整结果,或者在捕获错误后重新发起一次有明确模型选择的请求。
这也是为什么不能用一次 run_sync() 的测试结果推断 run_stream() 的行为。官方测试对流式路径单独验证:已经开始输出的截断响应不会触发 ModelFallback,而流式响应处理器也不会等到完整响应后再参与判断。若业务必须保证最终 JSON 完整,应该在应用协议层设计“暂存、校验、提交”的步骤,而不是把 fallback 当作流式事务回滚。
对工具调用尤其如此。非流式路径可以在工具参数被截断时拒绝整个响应,避免执行不完整的调用;流式路径若已经把部分参数交给下游,就必须由调用方决定如何缓冲和校验。工具结果、模型重试和用户可见文本最好使用不同状态记录,避免把“已经开始生成”误认为“已经可以执行”。这类状态管理也和 Pydantic AI 流式请求排查中的原则一致:先确认本地流生命周期,再判断是不是上游 API 故障。
如何决定是否接受截断
默认兜底适合“输出必须完整”的任务,例如结构化 JSON、代码补丁、工具参数和需要明确结论的短答案。此时让另一个模型重新生成,便于在进入业务层之前重新校验结果完整性。但并不是所有 max_tokens 都表示“模型应该完成任务”:有些产品会故意用它限制摘要长度、预览长度或日志片段长度。
对于这种有意限制长度的调用,应关闭该类 fallback,或者直接检查响应结束原因:
agent = Agent(
primary,
capabilities=[
ModelFallback(
backup,
fallback_on=(ModelAPIError, ContentFilterError),
)
],
)
result = agent.run_sync('Write a short preview.')
if result.response.finish_reason == 'length':
mark_as_preview(result.output)
这个配置的含义是:API 错误和拒答仍然切换,但到达输出上限的文本被接受。不要用 IncompleteToolCall 代替 OutputTokenLimitExceeded。前者只表示工具调用参数不完整,是后者的子类;它并不代表所有 finish_reason='length' 的响应。若要匹配所有输出截断,必须把 OutputTokenLimitExceeded 本身放进 fallback_on。
还要检查重试层。Pydantic AI 文档将 retry 和 fallback 视为不同恢复范围:同一模型的短暂网络失败适合有限重试,模型输出已经触及上限则可能需要换模型或提高上限。供应商 SDK 自己的重试也可能在异常到达 ModelFallback 之前发生,导致切换延迟。升级后如果发现备用模型迟迟没有请求,不要先改 fallback 顺序,应先查看 SDK 重试次数、Agent 的 max_retries、max_tokens 和运行级 token 限制。
升级验收清单
针对 2.68.0 的最小验收可以按以下顺序完成:
- 用
FunctionModel固定返回finish_reason='length'的普通文本,确认ModelFallback会调用备用模型。 - 让备用模型也返回
length,确认收到FallbackExceptionGroup,并检查失败尝试列表。 - 使用
fallback_on=(ModelAPIError, ContentFilterError),确认截断文本被保留且result.response.finish_reason仍是length。 - 如果代码使用
FallbackModel,单独验证默认值不会切换,再验证显式加入OutputTokenLimitExceeded后会切换。 - 用
run_stream()覆盖首事件前失败和已经输出后截断两条路径。 - 检查被拒绝尝试的 token 是否会进入用量统计,并确认运行级限制没有让备用模型在预期前被阻止。
- 对结构化 JSON、工具调用参数和普通文本分别测试,不要把一种输出类型的结论推广到全部类型。
这次升级的实际影响可以浓缩成一句话:ModelFallback 现在能把非流式的输出上限当成模型选择信号,但 FallbackModel 和已经开始输出的流式请求仍按各自原有边界工作。只要先明确业务是要“完整结果”还是“固定长度片段”,再决定是否保留 OutputTokenLimitExceeded,就不会因为升级而悄悄改变用户看到的文本。
参考来源
- Pydantic AI v2.68.0 发布说明
- Pydantic AI PR #12407:新增 OutputTokenLimitExceeded 与输出截断兜底
- Pydantic AI Model Fallback 官方文档(v2.68.0)
- Pydantic AI ModelFallback 截断行为测试(v2.68.0)
- Pydantic AI 问题 #12399:输出截断行为讨论
以上来源已于 2026 年 11 月 13 日打开并核对原文;版本相关结论以 Pydantic AI 2.68.0 的发布说明、文档和测试为准。