Docling Agent Skill 深读:Pydantic AI 高级工具机制(审批、重试、校验器、ToolReturn 与按需加载)
本文基于 Docling 仓库内置的 Agent 技能参考文档 TOOLS-ADVANCED.md,系统讲解 Pydantic AI 框架中的高级工具行为:Human-in-the-Loop 审批(DeferredToolRequests)、工具重试(ModelRetry + retries=)、执行前业务校验(args_validator=)、富返回值 ToolReturn、超时与串行执行控制,以及大规模工具目录下的按需加载(defer_loading)。读完后,你可以在为 Docling 生态搭建 AI Agent(如文档问答、格式转换工作流编排)时,正确处理“审批暂停—恢复”、“模型犯错—自动重试”、“参数越界—提前拦截”、“工具过多—延迟发现”这四类生产级问题。
文档定位:它是 Docling 仓库里的一份 Agent 技能参考
这份文档并不是 Docling 自身转换管线的源码,而是随仓库分发的 开发技能(development skill)。仓库根目录的 AGENTS.md 明确了这一分层:
开发技能(供在 Docling 上工作的贡献者使用)位于仓库根目录
.agents/skills/,例如dignified-python和building-pydantic-ai-agents。
也就是说,当 AI 编码 Agent 在 Docling 仓库中执行任务、且任务涉及“用 Pydantic AI 构建 Agent、添加工具、测试 Agent 行为”时,会加载 SKILL.md 中的 Task Routing Table,其中一行直接把“使用审批、重试、ToolReturn、校验器、超时或工具搜索等高级工具特性”路由到本文档:
I want to... Reference Use advanced tool features such as approval, retries, ToolReturn, validators, timeouts, or tool searchTools Advanced
技能的元数据同时声明了适用前提:Python 3.10+,模型字符串格式为 "provider:model-name"(如 "openai:gpt-5.2")。这与 Docling 自身的环境约束一致——pyproject.toml 中 requires-python = '>=3.10,<4.0',所以技能文档中的示例代码与本仓库的最低 Python 版本天然兼容。
理解了它的定位,就能抓住使用方式:这是“按任务加载”的参考手册,通常与同目录下的 TOOLS-CORE.md(基础工具注册、Toolset、MCP)和 ARCHITECTURE.md(抽象选择决策树)配合使用。
一、工具审批:让 Agent 运行暂停等待人工确认(Human in the Loop)
对于删除文件、发钱、发函这类高危操作,正确的做法不是把工具藏起来,而是让运行暂停、由外部系统(人或策略引擎)批准后再恢复。文档给出的完整模式依赖三个 API:DeferredToolRequests、DeferredToolResults、ToolDenied。
from pydantic_ai import (
Agent,
DeferredToolRequests,
DeferredToolResults,
ToolDenied,
)
agent = Agent('openai:gpt-5.2', output_type=[str, DeferredToolRequests])
@agent.tool_plain(requires_approval=True)
def delete_file(path: str) -> str:
return f'File {path!r} deleted'
result = agent.run_sync('Delete __init__.py')
messages = result.all_messages()
assert isinstance(result.output, DeferredToolRequests)
results = DeferredToolResults()
for call in result.output.approvals:
results.approvals[call.tool_call_id] = ToolDenied('Deleting files is not allowed')
result = agent.run_sync('Continue', message_history=messages, deferred_tool_results=results)
print(result.output)
这段代码的执行链值得逐行拆解:
output_type=[str, DeferredToolRequests]——把DeferredToolRequests加入输出类型是第一条硬性规则。当某次运行触发了待审批工具调用时,Agent 不继续执行,而是把“审批请求”作为本次运行的输出交还给你。注意str也在联合类型中:模型仍可以选择不调工具、直接用文本结束运行。requires_approval=True标记在@agent.tool_plain上,表示该工具每次调用前都必须审批。- 第一次
run_sync('Delete __init__.py')返回后,result.output是DeferredToolRequests,其.approvals字段列出待审批的tool_call_id。此时务必保存result.all_messages()——它是恢复运行时的消息历史。 - 构造
DeferredToolResults,按tool_call_id逐项给出裁决:这里用ToolDenied('Deleting files is not allowed')表示拒绝(拒绝理由会作为反馈回到模型,模型据此向用户解释)。若批准,则填入工具实际执行结果即可。 - 第二次
run_sync('Continue', message_history=messages, deferred_tool_results=results)携带历史与裁决恢复同一次 Agent 运行,拿到最终输出。
文档特别强调了两条关键规则,这也是审批模式最容易踩的两个坑:
DeferredToolRequests必须出现在output_type中,否则框架无法把“暂停”表达为合法输出;- 如果只需要条件性审批(例如只审批超过某个阈值的操作),应在工具内部抛出
ApprovalRequired(...),而不是把整个工具标记为requires_approval=True——后者会让该工具的所有调用无一例外地暂停,包括那些本可自动通过的低风险调用。
这套“暂停—裁决—恢复”三段式,本质上把审批决策从模型循环中剥离出来,交给具备权限的应用代码或前端交互,适合审批策略、审计日志都在模型之外的企业场景。
二、重试:用 ModelRetry 让模型自我纠错
工具重试与审批解决的是不同问题:审批解决“该不该做”,重试解决“做错了怎么办”。当错误是可恢复的模型失误(比如模型编造了一个不存在的用户名),正确做法是在工具内部抛出 ModelRetry,把一条可读的错误信息塞回模型上下文,让它带着反馈再试一次:
from pydantic_ai import Agent, ModelRetry, RunContext
agent = Agent('openai:gpt-5.2', deps_type=dict[str, int])
@agent.tool(retries=2)
def get_user_by_name(ctx: RunContext[dict[str, int]], name: str) -> int:
user_id = ctx.deps.get(name)
if user_id is None:
raise ModelRetry(f'No user found with name {name!r}')
return user_id
要点:
retries=2直接写在@agent.tool装饰器上,限定该工具最多允许两次模型重试;超过后运行才会失败,而不是无限循环。ModelRetry的消息(f'No user found with name {name!r}')会作为工具结果反馈给模型,因此信息应当可操作——告诉模型哪里错了,而不只是“出错了”。- 此例同时示范了
deps_type=dict[str, int]依赖注入:RunContext的第一个泛型参数就是deps的类型,工具通过ctx.deps访问运行时注入的数据(这里是用户名到用户 ID 的映射)。注意 SKILL.md 在 Common Gotchas 中反复提醒:@agent.tool要求RunContext作为第一个参数,@agent.tool_plain则不能有,混用会导致运行时错误。 - 文档给出的使用边界同样重要:重试用于可恢复的模型错误,而不是应用崩溃。数据库连接断开、磁盘写满这类故障用
ModelRetry只会浪费 token 并掩盖真实错误。
三、执行前校验:args_validator 拦截“结构合法但业务非法”的参数
Pydantic 类型注解已经保证参数“结构上合法”(类型、必填),但业务规则往往在类型之外:金额上限、配额、白名单。文档指出这类场景应该用 args_validator=,在校验失败时用 ModelRetry 把约束反馈给模型:
from pydantic_ai import Agent, DeferredToolRequests, ModelRetry, RunContext
agent = Agent('openai:gpt-5.2', deps_type=int, output_type=[str, DeferredToolRequests])
def validate_sum_limit(ctx: RunContext[int], x: int, y: int) -> None:
if x + y > ctx.deps:
raise ModelRetry(f'Sum of x and y must not exceed {ctx.deps}')
@agent.tool(requires_approval=True, args_validator=validate_sum_limit)
def add_numbers(ctx: RunContext[int], x: int, y: int) -> int:
return x + y
从这个例子可以读出三层设计意图:
- 校验器签名与工具同构:
validate_sum_limit同样接收RunContext和与工具一致的位置参数(x: int, y: int),返回None;不通过则抛异常。 - 校验先于执行(也先于审批):文档原话是“当参数结构合法但仍需在执行或审批前做业务规则校验时使用”。也就是说,越界参数不会触发审批暂停,而是先转成
ModelRetry让模型改参——避免人工审批者浪费时间审核注定失败的操作。 - 与审批组合:
requires_approval=True与args_validator=可以叠加,形成“先自动拦截非法参数、再人工批准合法参数”的双重防线。本例中deps_type=int注入的上限就是审批/校验策略的配置项。
四、进阶特性一览:ToolReturn、prepare=、timeout=、sequential=True
文档列出了“当你需要的不只是简单函数工具”时的四个进阶能力,并给出了 ToolReturn 的完整示例:
| 特性 | 解决的问题 |
|---|---|
ToolReturn |
富返回值:一个结构化返回值 + 独立的 content/metadata |
prepare= |
动态工具定义(工具定义可以在运行时被修改) |
timeout= |
工具执行时间上限 |
sequential=True |
强制工具调用串行,不允许并发执行 |
ToolReturn 示例:
from pydantic_ai import Agent, BinaryContent, ToolReturn
agent = Agent('openai:gpt-5.2')
@agent.tool_plain
def click_and_capture(x: int, y: int) -> ToolReturn:
return ToolReturn(
return_value=f'Successfully clicked at ({x}, {y})',
content=['After:', BinaryContent(data=b'png-data', media_type='image/png')],
metadata={'coordinates': {'x': x, 'y': y}},
)
这个例子来自“点击屏幕并截图”的浏览器/桌面自动化场景,ToolReturn 的三个字段各司其职:
return_value:字符串,是模型在下一轮推理中直接读到的“工具结果”——应当简明、面向决策;content:可附加多模态内容。示例中的BinaryContent(data=..., media_type='image/png')把点击后的截图作为图片回传给支持多模态的模型,让模型“看到”操作后果,而不是仅凭文本猜测;metadata:结构化元数据(这里是坐标字典),供应用侧日志、审计、回放使用,不干扰模型推理。
把“给模型看的文本”、“给模型看的多模态证据”、“给系统看的数据”三者分离,正是复杂工具(视觉 Agent、支付、报表生成)与普通函数式工具的分水岭。而 prepare= / timeout= / sequential=True 分别对应动态工具目录、外部依赖可能卡死、以及共享可变资源需要互斥这三类运行时控制需求——例如并发点击同一页面时设置 sequential=True 可避免竞态。
五、网络错误与限流的边界:ModelRetry 不是万能重试
文档单独用一节澄清了一个常见误解:
工具调用层面的重试,用
ModelRetry和工具的retries=...。 HTTP 传输层的请求重试,请使用库自带的 retry 配置,并单独处理。不要假设仅靠ModelRetry就能解决供应商侧的传输故障。
两层重试的分工可以这样理解:
ModelRetry(应用/模型层):工具业务逻辑判定“模型这次做错了”,反馈给模型重做——消耗的是模型推理轮次;- 传输层 retry(HTTP 层):连接抖动、5xx、限流(429)这类与模型输出内容无关的故障,应由 HTTP 客户端的重试配置(退避、上限)自动吸收——消耗的是网络请求。
把 429 限流当成“模型错误”交给 ModelRetry,不仅浪费推理轮次,还会在模型什么都没做错的情况下污染其上下文。在 Docling 这类会同时调用 OCR、布局检测、VLM 多个重型后端的项目里,分层重试的边界划分尤其值得借鉴。
六、工具搜索与延迟加载:大工具目录的上下文治理
当 Agent 挂了大量工具(比如一个包含上百个工具的 MCP 服务器),把所有工具 schema 一次性塞进系统提示会显著挤占上下文。文档给出的方案是延迟加载 + 按需发现:
from pydantic_ai import Agent
agent = Agent('openai:gpt-5.2')
@agent.tool_plain(defer_loading=True)
def lookup_internal_policy(policy_name: str) -> str:
return f'policy details for {policy_name}'
defer_loading=True 让该工具不随初始工具列表一起下发;模型需要时通过框架提供的 search_tools 机制自行搜索发现,再发起调用。文档列出的适用场景:
- 大型 MCP 服务器(工具数多,schema 总量大);
- 庞大的工具目录;
- 任何“加载全部工具 schema 会撑爆上下文”的情形。
结合同系列 TOOLS-CORE.md 的内容可以推断,延迟加载属于“跨切面行为”(cross-cutting behavior)之一:除直接标在 @agent.tool_plain 上外,也可以由包装工具集(wrapper toolset)对一组工具统一施加——这与 ARCHITECTURE.md 决策树中“多组相关工具 → FunctionToolset”、“按步过滤/修改工具定义 → PrepareTools()”的分支相互印证。
七、与 Docling 仓库实践的衔接
这份技能文档虽然讲的是 Pydantic AI,但它在 Docling 仓库中不是孤立的:
- 技能入口与路由:SKILL.md 声明了触发条件(“用户提到 Pydantic AI、导入
pydantic_ai、要求构建 Agent/添加工具/流式输出/测试行为”),并维护任务路由表;本文档是其中“高级工具特性”的落点。 - 环境一致性:技能要求 Python 3.10+,与 pyproject.toml 的
requires-python = '>=3.10,<4.0'一致;Docling 核心依赖pydantic>=2.0.0,<3.0.0,Pydantic AI 同样构建在 Pydantic v2 之上,两套体系在本仓库内没有版本冲突。 - 配套参考:审批/重试之外的基础能力(
@agent.tool与@agent.tool_plain的选择、RunContext可用字段ctx.deps/ctx.usage/ctx.messages/ctx.retry、MCP 服务器接入)在 TOOLS-CORE.md;抽象层对比与决策树在 ARCHITECTURE.md;确定性测试(TestModel+agent.override)在 SKILL.md 的 Quick-Start Patterns 与 TESTING-AND-DEBUGGING 参考中。
小结:一张选择速查表
| 场景 | 机制 | 关键 API |
|---|---|---|
| 高危操作需人工批准 | 审批暂停—恢复 | requires_approval=True + DeferredToolRequests/DeferredToolResults/ToolDenied;条件审批改用 ApprovalRequired(...) |
| 模型参数/事实错误,可自纠 | 工具重试 | @agent.tool(retries=N) + 工具内 raise ModelRetry(msg) |
| 参数结构合法但业务非法 | 执行前校验 | args_validator=,失败抛 ModelRetry 携带约束信息 |
| 富返回(文本 + 多模态 + 元数据) | 结构化工具返回 | ToolReturn(return_value=..., content=[BinaryContent(...)], metadata=...) |
| 工具执行限流/互斥 | 运行时控制 | timeout=、sequential=True、prepare= |
| 限流、网络抖动 | 传输层重试 | 库自带的 HTTP retry 配置,勿混用 ModelRetry |
| 工具目录过大、schema 撑爆上下文 | 延迟加载 + 按需发现 | defer_loading=True + search_tools;适配大型 MCP 服务器 |
需要记住的三条铁律:DeferredToolRequests 必须进 output_type;ModelRetry 只用于可恢复的模型错误;审批是“条件性”的就用 ApprovalRequired(...) 而非全局 requires_approval=True。掌握这几点,文档中的每一段示例代码都可以直接复制到你的 Agent 应用中作为审批、重试与工具治理的起点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00