← 返回笔记

一个真正会停下来的本地 Agent

为什么聊天里的‘请确认’不等于审批,以及 Jarvis 如何让工具调用在副作用发生前暂停、持久化并恢复。

AI AgentHuman-in-the-loop工程实践

当 Agent 只能回答问题时,错误通常只是一段不够好的文本。可一旦它能够操作文件、运行终端命令、修改日历或调用外部服务,错误就会变成真实的副作用。

最容易想到的解决办法,是在系统提示里写一句:

执行危险操作前,请先征求用户同意。

这不是真正的审批。它只是对模型的一条行为建议。

在开发 Jarvis 的过程中,我逐渐把“Human-in-the-loop”理解为一个执行系统问题:暂停必须发生在工具边界,而不是自然语言层;批准之后还必须能够从原来的执行状态继续。

口头确认为什么不可靠

如果审批只依赖提示词,至少存在三个问题。

第一,模型可能先调用工具,再在文本中解释自己做了什么。对写文件、发送邮件等操作来说,这时已经无法撤销“第一次副作用”。

第二,不同模型对“危险”的理解并不一致。本地小模型尤其可能把“等待审批”理解为输出一句“是否继续?”,却没有形成可恢复的工具状态。

第三,聊天消息没有稳定地绑定具体工具调用。用户回答“可以”,到底批准了哪一个命令、哪一组参数,以及同一轮里的其他工具是否也被批准,都可能含糊不清。

因此,我给 Jarvis 设定了一个更强的约束:

用户批准的是一组具有确定 ID、工具名和参数的待执行请求,而不是 Agent 的一段描述。

审批必须位于副作用之前

Jarvis 在工具真正执行前运行一层审批策略。当前策略主要识别两类操作:

  • 不属于已知只读集合的终端命令;
  • 邮件、日历、文件和办公软件 MCP 中具有修改语义的工具。

只读命令可以直接执行,例如 pwd、ls、cat 和 git diff。无法解析或不在安全集合里的命令则默认需要确认。这里的目标不是让模型自己判断风险,而是由应用层根据工具名与参数给出一致决定。

策略命中后,工具不会执行。Agent 运行返回一个延迟请求,其中包含:

tool_call_id
tool_name
arguments

这三个字段构成审批对象。后端同时保存当前活动上下文和待审批请求,使一次页面刷新或短暂断连不会把审批变成一条无法恢复的聊天记录。

Jarvis 在终端写入发生前展示命令,并等待批准或拒绝

从模型事件到界面上的按钮

暂停发生后,后端将延迟请求转换成 approval_needed 事件,通过 SSE 流发送到界面。前端把事件合并到原来的工具卡片,并显示具体命令、Approve 与 Deny。

这里有一个重要细节:界面不是审批的真值来源。

真正的待审批集合存储在后端。用户提交决定时,后端要求请求必须覆盖全部待审批 ID,并且每个 ID 恰好出现一次。缺少、重复或不存在的 ID 都不会进入恢复流程。

这使得“全部批准”“全部拒绝”以及逐项决定可以共享同一个一致性约束,而不是依赖前端当前显示了什么。

批准以后不是重新提问

批准工具后,Jarvis 不会把“用户同意了”拼成一条新消息,再让模型从头猜测下一步。后端会将决定构造成延迟工具结果,并把它连同原活动上下文交回同一次 Agent 执行语义。

整个链路可以概括为:

模型提出工具调用
  → 审批策略拦截
  → 持久化待审批请求
  → SSE 展示工具和参数
  → 用户逐项批准或拒绝
  → 构造 DeferredToolResults
  → 从原上下文恢复执行

在待审批期间,Jarvis 还会阻止两种可能破坏状态一致性的操作:发送一条新的用户消息,以及压缩当前上下文。用户必须先处理完现有审批,再开启另一条执行分支。

批准后,Jarvis 恢复运行并使用只读命令验证结果

拒绝也应该是一种有效结果

审批系统不能把“拒绝”当作异常。对 Agent 而言,拒绝是一次明确的工具结果:这项操作没有被授权,接下来应解释、调整方案,或寻找无副作用的替代路径。

这和执行失败不同。执行失败表示工具尝试过但没有成功;拒绝则表示工具根本不应该运行。两者如果混在一起,Agent 很容易反复请求同一个操作,或者错误地声称任务已经完成。

这仍然不是安全沙箱

当前实现解决的是应用层审批与恢复,不是完整的主机安全问题。

  • 命令分类依赖规则和参数解析,不能证明命令内部没有副作用;
  • workspace 与工作目录限制可以减少误操作范围,但不等于操作系统隔离;
  • 用户批准一个命令,不代表它产生的所有下游行为都已经被理解;
  • 当前 Stop 更接近中止客户端流,还不是持久化、可恢复的运行取消。

因此,我不会把 Jarvis 描述成“安全地执行任意命令”。更准确的说法是:它已经建立了一个可以观察、阻止和恢复的审批边界,而更强的步骤隔离、运行持久化与 OS 级限制仍是后续工作。

我从这条链路得到的判断

一个可靠的 Agent 审批功能,至少应该回答五个问题:

  1. 副作用是在批准前还是批准后发生?
  2. 用户批准的是否是确定的工具、参数和调用 ID?
  3. 等待期间的状态是否可以持久化?
  4. 批准与拒绝之后是否能够恢复原执行?
  5. 新消息、压缩和断线会不会破坏待审批状态?

如果产品只能展示一个“确认”按钮,却无法回答这些问题,它拥有的可能只是审批界面,而不是审批机制。

下一篇笔记会继续讨论另一个与恢复能力密切相关的问题:为什么完整聊天记录不应该直接等于模型上下文。