如何在委派代理中正确处理人工审批避免循环?

👁 66 次阅读

如何在委派代理中正确处理人工审批避免循环?

来源链接: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)是否在委派点被持久化。
  • 判断是否存在重复副作用:若历史重放会重新触发外部调用,则循环风险更高。
urbantech 提出的方案中,审批与原始委派调用的绑定关系通过一个以 (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)。
  • 协调者在该绑定处挂起。
  • 审批通过后,按该回执恢复对应调用,而不是把审批当作新的规划输入。
cheedli 在 2026-09-02 补充了副作用层面的做法:为每个外部变更型工具提供从调用推导出的稳定键。原文的表述是”giving every externally-mutating tool a stable key derived from the call”。 关于审批前端交互,dav04 在 2025-10-30 提问是否可以集成一次 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,维护者给出了恢复模式的方向性说明,但官方尚未合入完整方案,社区讨论中的模式可作为应用层的参考实现。

订阅 AI 百事知

每周精选 AI 工具评测、行业资讯与使用教程,第一时间送达。

🔗 相关阅读(你可能还想看)

📄 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
🤖 想直接和 AI 对话?
免费体验 DeepSeek V4 等顶级模型,无需注册,随问随答
🎯 立即体验 AI 工具

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

🤖 嘟嘟客服
滚动至顶部
🤖 嘟嘟客服
🤖 嘟嘟客服
';i.value='';m.innerHTML+='
...
';fetch('/dify-chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({query:q,user:'w'+Date.now()})}).then(function(r){return r.json()}).then(function(d){var a=d.answer||'暂时无法回复';m.removeChild(m.lastChild);m.innerHTML+='
'+a+'
'}).catch(function(){m.removeChild(m.lastChild);m.innerHTML+='
网络异常
'})">发送