跳到主要内容

pydantic-ai — 本课题摘录

读了哪几篇: 01-agent-graph(三节点状态图)、03-output-and-end-strategy(结构化输出与收尾策略)。 其余两篇(工具与工具集、中间件与模型抽象)本轮没读。

这一家在本课题里的位置:它把"裸 while 会越加越乱"这件事说得最直白,并给了一个不用上图引擎的替代解。

它对本课题回答了什么

决定四:为什么不用裸 while —— 把"下一步"变成返回值

它自己给的理由:循环本质就是个 while,但有一堆中途出口——token 超了、要人审批、工具要延迟执行、流式要能中断恢复——写成裸 while 会越加越乱。

它的解法不是上一个图引擎那么重,而是:把循环拆成节点,每个节点跑完返回"下一个节点"或"结束"。 (依据:前沿库 · Pydantic AI · 三节点状态图 —— 把循环拆成节点,节点 run() 返回「下一个节点」或 End,于是「下一步是什么」是显式的返回值,可被外部一步步驱动)

连带好处:因为"下一步"是返回值,外部可以逐节点驱动、逐节点观察。 调试时能一步步看图怎么流转。

三个节点:

节点干什么
拼输入把"一句话 / 一段历史+新问题 / 带工具结果来续跑"统一成一条待发的请求
调模型把历史 + 工具定义 + 输出格式打包发出去
处理工具调用整张图的分岔点:结束,还是再问一轮

节点之间不传一大包参数,而是改同一份状态。 状态里装"这次运行会变的东西"(历史、用量、第几轮、重试已用),依赖里装"运行期固定的配置"(模型、工具管理器、输出格式、收尾策略)。 (依据:前沿库 · Pydantic AI · 三节点状态图 —— 所有节点共享一份 GraphAgentState(会变:历史/用量/run_step/重试计数)与一份 GraphAgentDeps(固定:模型/工具管理器/输出 schema/end_strategy))

决定一:系统提示只在历史为空时才拼

一个很小但很实的细节:续跑已有对话时不重复塞系统提示。 (依据:前沿库 · Pydantic AI · 三节点状态图 —— 系统提示只在 message_history 为空时才拼,续跑已有对话不重复塞系统提示)

决定四:分岔点的优先级写得最清楚

有工具调用? → 执行工具,通常回到「调模型」
否则 有可用文本输出? → 当作最终文本输出 → 结束
否则 有图片且允许? → 当作图片输出 → 结束
否则 → 拼一条重试提示,回到模型让它重来

(依据:前沿库 · Pydantic AI · 三节点状态图 —— CallToolsNode 的走向优先级是「有 tool_calls 就先执行工具」,其次文本输出,再次图片输出,都没有就拼 RetryPromptPart 回模型)

"只要有工具调用就优先执行工具,即使模型同时回了文本"——理由跟 openai-agents-js 一模一样:模型常"先说一句我去查一下再调工具",那句文本不是最终答案。两家独立收敛到同一条,可以直接抄。

空响应也被当成一类要处理的情况,而不是崩掉:

情况处理
被 token 上限截断抛错,不重试
被内容过滤抛专门的错
输出类型允许"没有"空响应也算合法结果
其它消耗一次输出重试预算,回去再问

(依据:前沿库 · Pydantic AI · 三节点状态图 —— 空响应按 finish_reason 分情况——length 截断抛错不重试、content_filter 抛专门错、输出类型允许 None 则算合法、其余消耗一次输出重试回到模型)

决定三:一轮里多个工具调用,谁说了算 —— 这是最细的一份

这是别家都没讲透的一问。 模型一次响应可能同时发两个普通工具 + 一个"给最终答案"的工具,于是就有歧义:输出工具命中了普通工具还跑不跑?多个输出工具听谁的?普通工具要求重试但输出工具成功了,算结束还是重试?

它把这套规则单独命名,给了三种策略:

策略行为普通工具会跑吗
早停输出工具按顺序跑,第一个成功就结束仅当所有输出工具都失败时才跑
温和(默认)按顺序跑;输出工具之前的普通工具先跑完,第一个成功的输出工具赢跑,且每段内并行
穷尽所有工具并行跑完,按顺序第一个有效输出当最终结果全跑

(依据:前沿库 · Pydantic AI · 结构化输出与 end_strategy —— end_strategy 三种取值 early/graceful/exhaustive 分别对应「第一个成功输出即结束」「输出工具前的函数工具先跑完」「全部并行跑完」,默认已从 early 改为 graceful)

最容易踩的一条语义:重试优先。 如果任何普通工具产出了"要重试"的结果,最终输出会被压制,模型下一轮先去处理重试。 (依据:前沿库 · Pydantic AI · 结构化输出与 end_strategy —— graceful/exhaustive 下任何函数工具产出 RetryPromptPart 都会压制最终输出,模型下一轮先处理重试)

它给的直觉很好:模型同时"调了个会失败的工具"和"给了最终答案",那答案多半不靠谱——先让它把失败处理了再说。

决定三补充:栅栏工具

默认同段工具并行跑。但有副作用、不能和别人并发的工具可以标成栅栏:它前面发的工具先跑完,它自己单独跑,它后面的等它完才开始。还有一个运行级开关能一刀切把每个工具都变成栅栏。 (依据:前沿库 · Pydantic AI · 结构化输出与 end_strategy —— 标 sequential=True 的工具是 barrier,前面的先跑完、它单独跑、后面的等它;另有运行级 parallel_execution_mode('sequential') 一刀切)

这比 openai-agents-js 的"函数工具并行、改文件系统的串行"更通用——它把"谁能并发"变成了工具自己的一个声明。

它的做法(可以抄的部分)

部分结果也不丢。 工具执行中途被打断(异常或取消)时,已经跑完的工具返回会被打包成一条标了"被中断"的请求塞进历史,这样外面能看到"断点前做到哪",支撑可恢复运行。 (依据:前沿库 · Pydantic AI · 三节点状态图 —— 工具执行中途被打断时,已跑完的工具返回被打包成 state='interrupted' 的 ModelRequest 塞进历史,支撑可恢复运行)

模型调用被一层中间件链包着,于是可观测、缓存、"跳过真实调用直接给个响应"这些功能能统一插进来。

输出重试和每个工具自己的重试是分开计预算的。

它没回答什么

  • 历史怎么压——这两篇没讲。
  • 工具清单怎么每轮变——工具在每轮开始时解析一次(这一步会报工具重名冲突),但没有"按情况增删"的机制。
  • 不用原生工具调用怎么办——它假设模型支持结构化输出。

坑与代价

  • 默认策略变过。 默认从"早停"改成了"温和"。照旧文档/旧经验推断行为会错。
  • 拆成节点的代价是概念变多。 状态、依赖、节点、结束标记、图运行——为了不写乱一个 while,先要理解五个概念。

    判断(无锚): 我们的最小原型不需要这一层,一个 while 加一个"下一步"枚举就够(参考 openai-agents-js 的四个标签)。 如果错,会错在: 如果第一版就要"逐步观察 + 中途插手 + 断点续跑"三件一起要,那节点化的收益会立刻超过它的概念成本。

  • 有两种驱动方式,行为不一样。 一种会触发节点钩子,另一种不会。旧版还有"裸迭代不会清空挂起消息"的坑,新版修了但保留了旧异常类型做兼容。