👁 67 次阅读
如何在委派代理中正确处理人工审批避免循环?
来源链接:https://github.com/pydantic/pydantic-ai/issues/3274现象描述
在使用多代理系统时,协调者代理(coordinator)将任务委派给专职代理(如文件管理专职代理),当专职代理需要执行删除文件等敏感操作时,开发者希望先获得人工审批再继续。issue #3274 的提问者描述了自己尝试的流程:- 专职代理返回一个
DeferredToolRequest用于删除操作 - 协调者通过抛出
ApprovalRequired异常来处理 - 收到人工批准后,创建
DeferredToolResults并回传给协调者
DeferredToolResults 提供给协调者时,协调者似乎只是再次调用专职代理,形成循环,而非完成已批准的操作。提问者的核心诉求是:在委派工具调用中,人工审批的正确模式是什么,如何组织流程才能让审批通过后原始工具请求被正确执行。
该 issue 当前仍为 open 状态,标签为 feature,最后一条评论日期为 2026-09-02,共有 42 条评论(剔除 2 条机器人或无效留言后,正文依据 40 条实质讨论)。
背景原因
维护者 DouweM 在 2025-10-28 的回复中给出了关键判断:协调者与专职代理各自都需要用自己专属的消息历史加DeferredToolResults 来恢复运行。原文指出:
> 当在协调者的 run_specialist 工具中 result = specialist_agent.run() 输出 DeferredToolRequest 时,应将 result.all_messages() 与该 DeferredToolRequest(或至少工具调用 ID)保存在某处(可通过 deps),然后再……
这说明循环的根因在于:审批结果被当成给协调者的新输入,而不是对某个特定委派调用的恢复。讨论中多位参与者围绕此点展开,其中 urbantech 在 2026-06-20 的评论给出了高度概括的判断:
> 通常能打破循环的模式,是把审批视为对某个特定委派调用的恢复,而不是协调者的新输入。
jinngimk-lang 在 2026-08-17 从 HITL 失败模式的角度描述为:
> 人类批准了一个延迟操作,但恢复/委派重新进入规划器,而不是消费那个确切的批准,于是工作流循环,授权变得模糊。
cheedli 在 2026-09-02 进一步指出,嵌套场景真正困难的地方在于副作用而非消息历史本身:如果专职代理在协调者审批落定之前已经调用过 API,重放它的历史会再次执行该副作用。
排查与量化方法
根据讨论中给出的识别信号,可以从以下角度判断是否落入审批循环:- 观察协调者收到
DeferredToolResults后是否重新进入规划器,而不是恢复指定调用。 - 检查专职代理的
all_messages()与DeferredToolRequest(或工具调用 ID)是否在委派点被持久化。 - 判断是否存在重复副作用:若历史重放会重新触发外部调用,则循环风险更高。
(run_id, delegation_id, tool_call_id, approval_epoch) 为键的待处理操作回执(pending-operation receipt)来记录。这一键结构可用于量化和追踪某个审批到底绑定了哪个具体调用。
| 维度 | 失败模式 | 讨论中提出的缓解方向 |
| 审批归属 | 审批被当作协调者的新输入 | 将其视为特定委派调用的恢复 |
| 恢复对象 | 重新进入规划器 | 绑定 tool_call_id 精确恢复 |
| 状态保存 | 未保存专职代理消息历史 | 通过 deps 保存 all_messages() 与工具调用 ID |
| 副作用 | 历史重放重复触发外部调用 | 为外部变更型工具提供稳定键(据 issue 讨论) |
解决方案
维护者给出的基础流程是把协调者和专职代理分别恢复。据 DouweM 在 2025-10-28 的评论,在协调者的run_specialist 工具中,当 result = specialist_agent.run() 输出 DeferredToolRequest 时,需要先保存 result.all_messages() 与该 DeferredToolRequest(或工具调用 ID):
result = specialist_agent.run()
# result 输出 DeferredToolRequest
# 保存 result.all_messages() 和 DeferredToolRequest(或至少其 tool call IDs)
保存位置可以是通过 deps。随后协调者与专职代理各自以各自的消息历史加 DeferredToolResults 恢复运行。
urbantech 在 2026-06-20 的评论中给出了更完整的生产模式:
- 当专职代理发出
DeferredToolRequest,协调者记录一个小的待处理操作回执,键为(run_id, delegation_id, tool_call_id, approval_epoch)。 - 协调者在该绑定处挂起。
- 审批通过后,按该回执恢复对应调用,而不是把审批当作新的规划输入。
agent.run 去要求用户确认,还是可以用直接方法,并说明自己是在 FastAPI 的 StreamingResponse 聊天中使用代理。DouweM 同日回复,询问是否使用了 https://ai.pydantic.dev/ui/overview/ 中的某个前端协议或自定义方案,并说明:
> 通常你可以在代理返回 DeferredToolRequests 时将其视为完成,然后向前端发送某个事件请求用户批准,一旦前端收到批准,它会发送一个新请求,你可用来继续代理运行,传入消息历史……
renezander030 在 2026-07-23 的评论中,对”是否使用协议更好”给出了倾向:
> 建议使用聊天应用前端框架,如 CopilotKit(AG-UI)或 Vercel AI Elements(Vercel AI Data Stream Protocol)。
常见错误排查
- 审批后仍循环:协调者收到
DeferredToolResults后重新调用专职代理而非恢复指定调用。应确认是否按tool_call_id精准恢复。 - 专职代理消息历史丢失:未保存
result.all_messages(),导致无法用正确历史恢复。应通过deps等位置保存。 - 只保存了工具调用 ID 却未保存请求对象:据维护者原话,至少要保存工具调用 ID;保存完整
DeferredToolRequest更为稳妥。 - 审批被视为新输入:这正是 issue 描述的错误模式,应改为对特定委派调用的恢复。
- 重放历史导致副作用重复:若专职代理在审批落定前已调用外部 API,重放会再次执行;据 issue 讨论,应为外部变更型工具提供稳定键。
实践建议
讨论中的多位参与者把该问题定位为授权边界问题,而非单纯的流程控制问题。rpelevin 在 2026-06-13 指出,不应把私人事故披露附到 GitHub issue 上,但这类模式并非纯理论: > 在这些讨论串中反复看到的模式是近乎失误与被阻断操作驱动的:人们关闭高风险连接器的自动模式,把特定工具置于人工审查之后,拆分凭据,加入沙箱,并保留审计…… arian-gogani 在 2026-06-23 进一步区分了手工审批日志与可验证审批记录: > 手工审批日志从操作者视角回答”这被批准了吗”。可验证审批记录回答”这是否被批准给这个确切的……” cheedli 的经验则强调把成本关注点从消息历史转移到副作用管理。 综合来看,要点集中在:- 审批必须绑定到具体调用,键结构可参考
(run_id, delegation_id, tool_call_id, approval_epoch)(据 issue 讨论)。 - 协调者与专职代理各自恢复,不能混用一方历史。
- 外部变更型工具应具备稳定键以避免重放副作用。
- 前端协议选择影响审批交互的组织方式,据 issue 讨论可考虑 AG-UI 或 Vercel AI Data Stream Protocol 相关框架。
FAQ
Q:审批通过后代理人仍然循环怎么办? A:检查是否把审批当作协调者的新输入。正确做法是恢复绑定到特定tool_call_id 的委派调用,而不是重新进入规划器。
Q:协调者和专职代理的消息历史如何处理?
A:据维护者评论,两者都需要用各自的消息历史加 DeferredToolResults 恢复。在专职代理返回 DeferredToolRequest 时应保存 result.all_messages()。
Q:必须保存整个 DeferredToolRequest 吗?
A:维护者原文说”或至少工具调用 ID”,因此保存工具调用 ID 是底线;保存完整请求对象更稳妥。
Q:前端如何收集人工审批?
A:据 DouweM 评论,可在代理返回 DeferredToolRequests 时视为完成,向前端发送事件请求批准,前端收到批准后发新请求继续运行并传入消息历史。框架选择上,据 renezander030 评论可考虑 CopilotKit(AG-UI)或 Vercel AI Elements。
Q:嵌套委派下如何避免重复副作用?
A:据 cheedli 评论,为每个外部变更型工具提供从调用推导出的稳定键,避免历史重放再次触发外部调用。
Q:该 issue 是否已有官方修复?
A:截至最后一条评论日期 2026-09-02,issue 状态仍为 open,维护者给出了恢复模式的方向性说明,但官方尚未合入完整方案,社区讨论中的模式可作为应用层的参考实现。
🔗 相关阅读(你可能还想看)
📄 Outlook 节点创建草稿添加附件为何报 UnableToDeserializePostBody?
📄 Flowise 点击市场或聊天页为何整页白屏?
📄 如何解决 Azure AI Search 向量存储的 $filter 递归深度超限错误?
📄 如何解决 shadcn/ui Sidebar 与 Header 重叠的问题?
📄 Next.js 开发服务器为何会 JavaScript heap out of memory?
📄 Nuxt 偶发 Cannot read properties of undefined 报错是怎么回事
📄 如何解决「Visual Studio Code for ipad」相关的问题?
📄 如何在AutoGen中实现企业级代理治理的加密行动收据(AAR)?
📄 记忆内容未清洗直接注入系统提示词,如何防范间接提示注入?
crewAI LiteAgent 将记忆内容直接拼接到系统提示词,存在间接提示注入风险。社区提出分层防御方案,但 iss