首页
/ Docling Agent Skill 深读:Pydantic AI 高级工具机制(审批、重试、校验器、ToolReturn 与按需加载)

Docling Agent Skill 深读:Pydantic AI 高级工具机制(审批、重试、校验器、ToolReturn 与按需加载)

2026-09-05 18:17:47作者:尤辰城Agatha

本文基于 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-pythonbuilding-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 search Tools Advanced

技能的元数据同时声明了适用前提:Python 3.10+,模型字符串格式为 "provider:model-name"(如 "openai:gpt-5.2")。这与 Docling 自身的环境约束一致——pyproject.tomlrequires-python = '>=3.10,<4.0',所以技能文档中的示例代码与本仓库的最低 Python 版本天然兼容。

理解了它的定位,就能抓住使用方式:这是“按任务加载”的参考手册,通常与同目录下的 TOOLS-CORE.md(基础工具注册、Toolset、MCP)和 ARCHITECTURE.md(抽象选择决策树)配合使用。

一、工具审批:让 Agent 运行暂停等待人工确认(Human in the Loop)

对于删除文件、发钱、发函这类高危操作,正确的做法不是把工具藏起来,而是让运行暂停、由外部系统(人或策略引擎)批准后再恢复。文档给出的完整模式依赖三个 API:DeferredToolRequestsDeferredToolResultsToolDenied

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)

这段代码的执行链值得逐行拆解:

  1. output_type=[str, DeferredToolRequests]——把 DeferredToolRequests 加入输出类型是第一条硬性规则。当某次运行触发了待审批工具调用时,Agent 不继续执行,而是把“审批请求”作为本次运行的输出交还给你。注意 str 也在联合类型中:模型仍可以选择不调工具、直接用文本结束运行。
  2. requires_approval=True 标记在 @agent.tool_plain 上,表示该工具每次调用前都必须审批。
  3. 第一次 run_sync('Delete __init__.py') 返回后,result.outputDeferredToolRequests,其 .approvals 字段列出待审批的 tool_call_id。此时务必保存 result.all_messages()——它是恢复运行时的消息历史。
  4. 构造 DeferredToolResults,按 tool_call_id 逐项给出裁决:这里用 ToolDenied('Deleting files is not allowed') 表示拒绝(拒绝理由会作为反馈回到模型,模型据此向用户解释)。若批准,则填入工具实际执行结果即可。
  5. 第二次 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=Trueargs_validator= 可以叠加,形成“先自动拦截非法参数、再人工批准合法参数”的双重防线。本例中 deps_type=int 注入的上限就是审批/校验策略的配置项。

四、进阶特性一览:ToolReturnprepare=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.tomlrequires-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=Trueprepare=
限流、网络抖动 传输层重试 库自带的 HTTP retry 配置,勿混用 ModelRetry
工具目录过大、schema 撑爆上下文 延迟加载 + 按需发现 defer_loading=True + search_tools;适配大型 MCP 服务器

需要记住的三条铁律:DeferredToolRequests 必须进 output_typeModelRetry 只用于可恢复的模型错误;审批是“条件性”的就用 ApprovalRequired(...) 而非全局 requires_approval=True。掌握这几点,文档中的每一段示例代码都可以直接复制到你的 Agent 应用中作为审批、重试与工具治理的起点。

登录后查看全文
热门项目推荐
相关项目推荐