AI 工程基础体系 · 第 98/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
Agent 人工介入:确认点、升级、接管、恢复和责任边界
Agent 并不是“模型调用工具”的另一种说法。一个能够读取信息、规划步骤、调用工具并根据结果继续行动的 Agent,实际上是在代表某个主体改变外部世界:修改数据库、发送邮件、发布内容、创建工单、执行付款,或者改变另一个系统的权限。
因此,人工介入(Human-in-the-loop,HITL)不是在模型输出旁边加一个“是否同意”的按钮,而是要回答一组生产系统问题:
- 哪些动作必须由人确认?
- 什么时候应该升级,而不是继续等待?
- 人工接管后,Agent 是否还能继续执行?
- 人工拒绝、超时、系统崩溃后,如何恢复?
- 出错时由模型、编排器、工具调用者、批准人还是业务主体负责?
- 如何证明某次动作在当时得到了什么授权?
这篇文章把模型、数据、评测、权限和成本放在同一个生产系统中讨论。人工介入的目标不是让人审阅所有模型输出,而是让系统在风险、不可逆性和不确定性达到阈值时,把决策权交给合适的人,并且保留可验证的责任链。
一、先区分四种不同的人工介入
“人工介入”常被当成一个概念使用,实际上至少包含四种机制。
1. 确认点:执行前由人批准
**确认点(approval point)**是一个在具体动作执行前暂停流程的状态。系统向人展示待执行动作、参数、依据、风险和权限范围;人批准后,系统才允许执行。
例如:
Agent 生成退款请求
↓
系统校验退款金额、订单和权限
↓
等待财务人员批准
↓
调用退款 API
关键是“批准什么”必须是明确的动作,而不是泛泛地批准一段自然语言。
不可靠的确认:
Agent:我已经检查过订单,是否继续?
更可靠的确认:
动作:POST /refunds
订单:ORD-20250301-17
金额:¥8,000
原因:重复扣款
目标账户:原支付账户
影响:将产生不可逆的资金变动
依据:订单状态为 paid,支付记录存在两笔相同授权
批准有效期:10 分钟
确认应绑定到结构化动作或其不可变摘要。否则,Agent 在获得批准后改变了金额、目标对象或工具参数,原批准就不应继续有效。
2. 升级:当前执行者无法安全决策
**升级(escalation)**不是简单地把任务转发给另一个人,而是把“需要更高权限、更高专业性或更高责任等级的决策”转移给指定队列或角色。
典型触发条件包括:
- 置信度低,且候选答案之间无法区分;
- 任务超出 Agent 的权限范围;
- 规则冲突,例如客户政策允许退款,但反欺诈规则禁止退款;
- 金额、影响范围或敏感等级超过阈值;
- 多次工具调用失败,继续重试可能造成副作用;
- 检索资料互相矛盾,系统无法形成可审计依据;
- 请求涉及法律、医疗、财务或安全责任;
- 人工确认在规定时间内没有完成。
升级的核心是责任级别变化。如果只是排队等待同一个审批者,不一定叫升级;如果从一线客服转给财务主管、从普通工程师转给安全值班人员,则是升级。
3. 接管:人工成为当前执行者
**接管(takeover)**表示人工不只是批准某一步,而是暂时成为任务的主要执行者。人工可以查看上下文、修改计划、调用工具、暂停任务或终止任务。
接管适用于:
- Agent 已经进入不稳定状态,需要人工重新规划;
- 任务是开放式操作,无法预先枚举每个确认点;
- 发生安全事件,需要先停止自动动作;
- 复杂例外需要领域专家直接处理;
- 需要与外部对象沟通,而不是执行固定 API。
接管后必须定义一个明确的控制权状态。如果人和 Agent 同时认为自己拥有执行权,就会出现“双重执行”:
Agent 认为“客户已批准退款”,准备调用 API
人工也在后台手动退款
两个动作最终都成功
这不是模型质量问题,而是并发控制和幂等性设计错误。
4. 恢复:从暂停、失败或接管状态安全回到流程
**恢复(recovery)**是系统在暂停、拒绝、超时、进程崩溃、工具部分成功或人工接管后,决定如何继续、补偿、回滚或终止。
恢复不是“让 Agent 再试一次”。必须先确定上一个动作的外部结果:
- 没有发送出去,可以安全重试;
- 请求已发送但响应丢失,必须查询状态,不能盲目重试;
- 部分步骤已成功,需要继续未完成步骤或执行补偿;
- 动作不可逆,只能记录事实并进入人工处理;
- 人工拒绝,必须保存拒绝原因,不能在同一条件下自动绕过;
- 确认超时,原批准不能自动延长,除非策略明确允许。
二、Agent 的执行对象不是“回答”,而是带权限的动作
普通聊天系统通常关注输出文本是否正确。Agent 系统还必须关注动作(action)。
可以把一次执行建模为:
其中:
- :任务主体,例如用户、业务部门或服务账户;
- :上下文,包括输入、检索结果、历史状态和模型生成内容;
- :权限与策略集合;
- :准备执行的动作;
- :动作结果;
- :时间和版本信息。
动作 不只是工具名称,还应包含:
例如 send_email 的动作至少应区分:
{
"tool": "send_email",
"arguments": {
"to": ["customer@example.com"],
"subject": "退款处理结果",
"body": "..."
},
"target": "customer@example.com",
"side_effect": "external_communication",
"scope": "one_customer"
}
同一个 Agent 生成“退款建议”和“执行退款请求”,风险完全不同。前者可能只改变内部草稿,后者会改变外部资产。
因此,人工确认的对象应是动作的权限化表示,而不是自然语言结论。
三、风险不是只由模型置信度决定
模型概率不能直接等价于业务安全性。一个模型对错误答案可能有很高的 token 概率,而一个低置信度的答案也可能只是无害的格式选择。
对动作 ,可以定义一个简化的风险函数:
其中:
- :在上下文 下动作造成损害的概率;
- :损害影响,例如金额、用户数、服务中断范围;
- :不可逆成本;
- :泄露或越权处理敏感数据的成本。
这不是要求系统精确计算真实概率,而是说明确认策略必须同时考虑:
- 发生错误的可能性;
- 错误发生后的损失;
- 是否还能补救;
- 是否涉及法律、隐私或安全边界。
例如:
| 动作 | 可能的错误 | 可逆性 | 通常的控制 |
|---|---|---|---|
| 给内部文档生成摘要 | 遗漏信息 | 高 | 自动执行,抽样评测 |
| 创建草稿邮件 | 语气或事实错误 | 高 | 自动生成,发送前确认 |
| 发送外部邮件 | 收件人或内容错误 | 中 | 发送前确认或规则放行 |
| 修改生产配置 | 服务中断 | 中/低 | 双人确认、变更窗口、回滚 |
| 转移资金 | 金额或账户错误 | 低 | 强身份认证、二次审批、幂等键 |
| 删除数据 | 误删 | 低 | 禁止 Agent 直接执行,或强制人工接管 |
模型、数据、评测、权限和成本应一起看。降低模型成本而提高自动执行范围,可能增加人工复核和事故成本;增加检索数据量可能提高回答质量,也可能扩大敏感数据暴露范围;更换模型版本可能改变工具选择分布,从而改变人工审批量。
四、确认点应该放在哪里
确认点不是越多越安全。每增加一个确认点,就增加等待时间、人工成本和“无脑点击批准”的可能性。
可以把 Agent 的计划表示为一串动作:
对每个动作定义:
- :副作用等级;
- :失败后的可恢复性;
- :数据敏感等级;
- :所需权限等级;
- :人工确认成本。
当动作满足下式时,通常应设置确认点:
直觉是:副作用大、难以恢复、处理敏感数据或超过 Agent 权限的动作,都不能仅依靠模型继续执行。
确认点的三个位置
1. 工具调用前
这是最常见的方式:
模型决定调用工具
→ 策略引擎检查
→ 生成人工确认请求
→ 批准后调用工具
它能阻止高风险动作,但要求系统能准确描述最终参数。
2. 计划提交前
当一个计划包含多个相关动作时,可以先让人批准计划:
1. 查询订单
2. 生成退款草稿
3. 发送客户通知
4. 执行退款
这种方式减少确认次数,但有一个边界:批准计划不一定等于批准计划中未来所有具体参数。如果订单、金额或收件人会变化,执行前仍需重新校验。
3. 结果提交前
某些动作已经在沙箱中执行,人工只批准将结果写入生产系统:
Agent 在临时环境生成数据库迁移
→ 自动测试
→ 人工确认
→ 应用到生产库
这适合可预览、可验证的操作,但不适合已经造成外部副作用的动作。邮件一旦发送,不能靠“结果提交前确认”挽回。
五、一个完整的状态机
把人工介入实现为状态机,比在代码中散落 if approved 更可靠。
一个任务可以使用以下状态:
CREATED
↓
RUNNING
├── WAITING_APPROVAL
├── ESCALATED
├── HUMAN_TAKEOVER
├── WAITING_RETRY
├── SUCCEEDED
├── REJECTED
├── CANCELLED
└── FAILED
状态含义必须互斥。例如:
WAITING_APPROVAL:Agent 仍是控制者,但被策略阻塞;ESCALATED:任务等待更高权限或更专业的处理者;HUMAN_TAKEOVER:人工持有控制权,Agent 不得自动调用外部工具;WAITING_RETRY:系统已确定可以重试,但尚未执行重试;FAILED:当前流程无法继续,可能需要补偿或人工介入;CANCELLED:任务被明确终止,不应因为队列重放而重新执行。
可以将合法转换写成:
例如:
RUNNING + high_risk_action → WAITING_APPROVAL
WAITING_APPROVAL + approve → RUNNING
WAITING_APPROVAL + reject → REJECTED
WAITING_APPROVAL + timeout → ESCALATED
RUNNING + repeated_tool_failure → ESCALATED
RUNNING + takeover → HUMAN_TAKEOVER
HUMAN_TAKEOVER + release → RUNNING
RUNNING + completed → SUCCEEDED
不合法的转换必须被系统拒绝:
REJECTED + retry
SUCCEEDED + execute_again
HUMAN_TAKEOVER + agent_tool_call
CANCELLED + queue_redelivery
其中最后一个尤其重要。消息队列至少一次投递时,旧消息可能在任务已取消后重新到达。消费者必须再次检查任务状态,而不能只相信消息内容。
stateDiagram-v2
[*] --> CREATED
CREATED --> RUNNING
RUNNING --> WAITING_APPROVAL: 高风险动作
WAITING_APPROVAL --> RUNNING: 批准且参数仍有效
WAITING_APPROVAL --> REJECTED: 拒绝
WAITING_APPROVAL --> ESCALATED: 超时或审批者无权限
RUNNING --> HUMAN_TAKEOVER: 人工接管
HUMAN_TAKEOVER --> RUNNING: 人工释放控制权
RUNNING --> WAITING_RETRY: 可重试失败
WAITING_RETRY --> RUNNING: 退避后重试
RUNNING --> SUCCEEDED: 完成
RUNNING --> ESCALATED: 不确定、越权或反复失败
HUMAN_TAKEOVER --> CANCELLED: 人工终止
ESCALATED --> HUMAN_TAKEOVER: 专家接管
核心路径不是“模型—人工—模型”,而是:
任务状态存储
↑ ↓
编排器 ← 策略引擎
↓
模型 ←→ 工具适配器 ←→ 外部系统
↑
人工界面 / 审批队列
所有关键决策都应通过状态存储和策略引擎落盘。人工界面不能直接调用业务工具绕过编排器,否则审计记录、权限检查和幂等控制会被破坏。
六、确认请求必须是不可混淆的授权对象
一个确认请求至少应包含以下字段:
{
"approval_id": "apr_01J...",
"task_id": "task_01J...",
"action_hash": "sha256:...",
"tool": "refund_payment",
"arguments": {
"order_id": "ORD-17",
"amount": 8000,
"currency": "CNY"
},
"reason": "检测到重复扣款",
"evidence_refs": [
"payment:pay-1",
"payment:pay-2"
],
"risk": {
"side_effect": "financial",
"reversibility": "low"
},
"requested_role": "finance_approver",
"expires_at": "2025-03-01T10:10:00Z",
"policy_version": "refund-policy-12",
"model_version": "agent-model-2025-02"
}
其中 action_hash 是对规范化动作进行哈希后的摘要。批准时系统重新计算当前动作的哈希:
只有当:
并且确认未过期、策略版本仍有效、批准人拥有对应角色时,才允许执行。
为什么不能只存一个布尔值
下面这种数据结构不足以审计:
{
"approved": true
}
它无法回答:
- 谁批准的?
- 批准的是什么金额?
- 批准时看到的证据是什么?
- 模型或策略版本是什么?
- 批准是否已经过期?
- 批准之后参数是否被改过?
- 批准人是否有权批准这个动作?
审批是授权事实,不是 UI 状态。
七、一个可运行的最小确认与幂等示例
下面的 Python 示例只使用标准库,演示确认点、动作哈希、过期、状态检查和幂等执行。它不是生产级支付系统,但可以直接运行来观察核心行为。
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from hashlib import sha256
import json
import uuid
def now() -> datetime:
return datetime.now(timezone.utc)
def canonical_json(value: dict) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def action_hash(action: dict) -> str:
return sha256(canonical_json(action).encode("utf-8")).hexdigest()
@dataclass
class Approval:
approval_id: str
task_id: str
action: dict
action_hash: str
approver_role: str
expires_at: datetime
approved_by: str | None = None
status: str = "PENDING"
@dataclass
class Task:
task_id: str
state: str = "WAITING_APPROVAL"
executed_keys: set[str] = field(default_factory=set)
class ApprovalError(Exception):
pass
def approve(approval: Approval, actor: str, actor_roles: set[str]) -> None:
if approval.status != "PENDING":
raise ApprovalError(f"approval is {approval.status}")
if approval.approver_role not in actor_roles:
raise ApprovalError("actor lacks required approval role")
if now() >= approval.expires_at:
approval.status = "EXPIRED"
raise ApprovalError("approval expired")
approval.approved_by = actor
approval.status = "APPROVED"
def execute(task: Task, approval: Approval, current_action: dict) -> str:
if task.state != "WAITING_APPROVAL":
raise ApprovalError(f"task is not waiting for approval: {task.state}")
if approval.status != "APPROVED":
raise ApprovalError(f"approval is {approval.status}")
if now() >= approval.expires_at:
approval.status = "EXPIRED"
raise ApprovalError("approval expired")
# 防止批准后参数被替换
if action_hash(current_action) != approval.action_hash:
raise ApprovalError("approved action does not match current action")
# 一个真实系统应把这个键持久化到数据库唯一索引中
idempotency_key = f"{task.task_id}:{approval.approval_id}"
if idempotency_key in task.executed_keys:
return "ALREADY_EXECUTED"
# 这里代表调用外部副作用 API。
# 生产环境中应先在外部系统使用相同幂等键,再标记本地完成。
task.executed_keys.add(idempotency_key)
task.state = "SUCCEEDED"
approval.status = "CONSUMED"
return "EXECUTED"
if __name__ == "__main__":
task = Task(task_id="task-" + uuid.uuid4().hex[:8])
action = {
"tool": "refund_payment",
"arguments": {
"order_id": "ORD-17",
"amount": 8000,
"currency": "CNY",
},
}
approval = Approval(
approval_id="apr-" + uuid.uuid4().hex[:8],
task_id=task.task_id,
action=action,
action_hash=action_hash(action),
approver_role="finance_approver",
expires_at=now() + timedelta(minutes=10),
)
approve(approval, actor="alice", actor_roles={"finance_approver"})
print(execute(task, approval, current_action=action))
print(execute(task, approval, current_action=action))
print(task.state)
预期输出类似:
EXECUTED
ALREADY_EXECUTED
SUCCEEDED
这个结果体现了三个约束:
alice必须具备finance_approver角色;- 执行时动作参数必须与批准时的哈希一致;
- 同一个任务和批准请求重复到达时,不得产生第二次副作用。
示例中的 executed_keys 只是内存集合,进程重启后会丢失。生产环境必须将它放入持久化存储,并用唯一约束或原子插入防止并发执行。更进一步,外部支付系统也必须支持幂等键;仅在本地防重不能阻止网络超时后重复支付。
八、并发、超时和“结果未知”是人工介入最容易出错的地方
1. 双重审批
两个审批者同时点击批准,不应导致两次执行。审批状态转换必须是条件更新:
UPDATE approvals
SET status = 'APPROVED',
approved_by = :actor,
approved_at = CURRENT_TIMESTAMP
WHERE approval_id = :id
AND status = 'PENDING'
AND expires_at > CURRENT_TIMESTAMP;
受影响行数为:
1:本次成功取得批准;0:已被其他人处理、已过期或不存在。
不能先 SELECT status,再单独 UPDATE,因为两个请求可能同时读到 PENDING。
2. Agent 与人工同时操作
接管时应使用租约或版本号:
RUNNING(version=7)
→ HUMAN_TAKEOVER(owner=alice, version=8)
Agent 每次调用工具前检查:
当前状态仍为 RUNNING?
控制者仍为 agent?
版本号仍为 8?
如果其中一个条件不满足,就拒绝调用。仅仅在 UI 上显示“人工已接管”是不够的,工具网关也必须强制执行这个约束。
3. 审批超时不是批准
确认请求超过有效期后应进入 EXPIRED 或 ESCALATED。不能因为队列延迟、值班人员未登录或系统时钟不同步,就把沉默解释为同意。
有些低风险系统可以配置“超时自动拒绝”或“超时转人工队列”;高风险动作不应配置“超时自动批准”,除非业务责任人明确接受这种策略,并且有独立的法律和安全评估。
4. 工具超时不等于工具未执行
假设 Agent 调用支付服务:
客户端发送请求
→ 支付服务成功扣款
→ 响应在网络中丢失
→ Agent 收到 timeout
此时状态不是“失败且可以重试”,而是:
OUTCOME_UNKNOWN
恢复步骤应是:
- 使用原幂等键查询支付服务;
- 若查到成功,记录本地为成功;
- 若查到未执行,才允许重试;
- 若支付服务无法查询,升级给人工或进入对账流程;
- 在结果未确定前,禁止生成第二个支付请求。
这是分布式系统的事实,不是 Agent 特有的问题;自然语言规划反而会让错误重试更隐蔽。
九、升级策略:按原因、权限和时限路由
升级不能只设置一个“转人工”按钮。一个可用的升级事件至少需要记录:
{
"task_id": "task-17",
"reason_code": "TOOL_OUTCOME_UNKNOWN",
"severity": "HIGH",
"required_role": "payment_operations",
"deadline": "2025-03-01T10:20:00Z",
"attempts": 2,
"last_error": "timeout after request accepted",
"context_snapshot": "ctx-...",
"suggested_action": "query payment provider by idempotency key"
}
按原因路由
POLICY_CONFLICT:交给业务规则负责人;PERMISSION_DENIED:交给权限管理员或业务审批人;SENSITIVE_DATA:交给隐私或安全人员;TOOL_OUTCOME_UNKNOWN:交给外部系统运维或对账人员;LOW_CONFIDENCE:交给领域专家;PROMPT_INJECTION_SUSPECTED:交给安全响应人员;BUDGET_EXCEEDED:交给成本负责人,而不是让 Agent 自动切换到无约束模式。
按时限路由
升级需要服务级别目标(SLA),但 SLA 不是“超过时限就继续执行”。例如:
0–5 分钟:分配给一线审批队列
5–15 分钟:通知值班主管
15 分钟后:冻结相关自动动作并创建高优先级事件
每一层都应明确允许的动作范围。主管可以批准更高金额,不代表可以修改安全策略或绕过身份验证。
十、接管不是把聊天窗口交给人
接管界面应展示可操作的状态,而不是只展示完整对话记录。至少需要:
- 当前任务状态和控制者;
- 已执行动作及结果;
- 尚未执行的计划;
- 工具参数和权限;
- 证据来源与时间;
- 模型、提示模板、策略和工具版本;
- 失败、重试、超时和外部请求 ID;
- 可能的补偿动作;
- 人工修改前后的差异。
人工接管时应产生一个事件:
{
"event": "CONTROL_TRANSFERRED",
"from": "agent",
"to": "alice",
"reason": "policy_conflict",
"at": "2025-03-01T10:05:00Z",
"task_version": 8
}
人工修改 Agent 生成的计划也应记录为新版本,而不是覆盖原计划:
plan-v1:退款 8,000 元并通知客户
plan-v2:先冻结退款,核对支付渠道,再决定
这样才能在事故调查中区分:
- 模型原本建议了什么;
- 策略引擎阻止了什么;
- 人工改变了什么;
- 外部系统实际执行了什么。
十一、恢复策略必须按副作用分类
可以把失败动作分成三类。
A. 确定未执行
例如本地参数校验失败,工具网关在发出请求前拒绝。
处理方式:
修正参数或升级
→ 生成新的动作版本
→ 重新请求批准
不能沿用旧批准,因为动作可能已改变。
B. 确定已执行
例如外部系统返回成功,但本地保存结果时进程崩溃。
处理方式:
- 根据外部请求 ID 或幂等键查询;
- 将本地状态补记为成功;
- 不再次调用副作用工具;
- 继续后续步骤时重新检查依赖。
C. 结果未知
例如请求超时、连接断开或响应解析失败。
处理方式:
禁止盲目重试
→ 查询外部状态
→ 根据查询结果进入成功、未执行或人工对账
补偿不等于回滚
如果系统已经发送邮件,删除本地发送记录不能撤回邮件;如果已经扣款,写一条“退款成功”记录也不能证明钱已退回。
补偿动作是一个新的业务动作:
原动作:扣款
补偿动作:退款
补偿也可能失败,也需要幂等性、权限和确认点。
十二、MCP 在人工介入中的边界
Model Context Protocol(MCP)定义了模型应用与外部上下文、工具和资源之间的协议结构。其典型关系是:
Host(模型应用)
├── MCP Client
│ └── MCP Server
│ ├── Tools
│ ├── Resources
│ └── Prompts
MCP 的作用是标准化发现和调用能力,例如服务器可以暴露工具,客户端可以获取资源或提示模板。它并不自动替业务系统决定:
- 哪些工具必须人工批准;
- 谁拥有批准权限;
- 批准是否满足双人规则;
- 工具调用失败后如何补偿;
- 某个动作由谁承担业务责任。
换句话说,协议层的工具可发现性不等于业务层的执行授权。
MCP 规范包含能力协商和交互机制;某些版本的规范还定义了服务器请求客户端向用户发起 elicitation(信息采集或确认)的方式。但即使使用这种协议能力,最终的批准仍应由 Host 或业务编排层根据本地身份、权限、策略和审计要求决定。不能因为 MCP Server 请求了用户输入,就把它当成已经获得了可执行的业务授权。
一个安全的调用路径应类似:
Agent 选择 MCP tool
→ Host 根据工具元数据分类风险
→ 本地策略检查用户、租户、权限和预算
→ 必要时创建审批请求
→ 批准结果绑定工具名和参数摘要
→ Host 调用 MCP tool
→ 记录协议请求、响应和业务结果
MCP 工具描述可以帮助系统识别工具用途和参数,但工具描述本身是不可信的业务授权来源。生产系统应对工具实施允许列表、参数校验、超时、网络边界、速率限制和审计。
还需要区分两种身份:
- 调用身份:MCP Client 或服务账户使用什么凭据访问服务器;
- 责任主体:谁授权了这次动作,动作代表谁的业务意图。
服务账户成功调用工具,并不意味着某个人已经批准该业务动作。
十三、Agent 框架中的人工介入位置
Agent 框架通常会提供以下能力的组合:
- 指令和上下文管理;
- 工具调用;
- 多 Agent handoff;
- guardrails;
- tracing;
- 任务状态或运行上下文。
OpenAI 的 Agents Guide 将 Agent 系统描述为模型、工具、指令以及运行时控制机制的组合,并讨论了 guardrails、handoffs 和 tracing 等概念。这里需要区分:
- guardrail:自动检查输入、输出或工具调用是否满足规则;
- handoff:把任务转给另一个 Agent 或专门处理节点;
- 人工审批:由具备身份和权限的人批准一个明确动作;
- 人工接管:人取得任务控制权;
- 人工复核:事后抽样检查,不能替代执行前授权。
一个 guardrail 可以阻止“向外部发送身份证号”,但它不能自动证明某个人批准了发送;一个 handoff 可以把客服 Agent 转给退款 Agent,但不等于财务审批;一个 trace 可以记录调用链,但不等于不可篡改的业务审计日志。
因此,在框架生命周期中,人工介入通常位于工具调用边界:
plan = agent.plan(context)
for action in plan.actions:
policy_result = policy.check(
principal=context.principal,
action=action,
budget=context.budget,
)
if policy_result.requires_human:
approval = approval_store.create(action, policy_result)
return RunPaused(
reason="WAITING_APPROVAL",
approval_id=approval.id,
)
result = tool_gateway.execute(action)
context = context.apply(result)
return RunSucceeded(context)
恢复时不要重新从原始用户请求开始完整运行,因为这样可能重新生成不同计划并重复执行已有动作。应从持久化检查点恢复:
读取 task state
→ 读取已完成动作
→ 查询不确定动作的外部状态
→ 生成剩余计划
→ 对新动作重新做策略判断
如果框架自动重试模型调用,也必须保证模型重试不会绕过工具审批。模型可以重新生成解释或计划,但实际工具网关仍应以任务状态、动作哈希和权限检查为准。
由于 Agent 框架和 API 版本会变化,具体的 approval、handoff 或 guardrail 参数不能脱离所用 SDK 版本直接照抄。稳定的设计原则是:把人工审批作为持久化业务状态和工具网关策略,而不是只依赖某个 SDK 的内存回调。
十四、人工确认也会失败
1. 人看到的信息不足
只显示:
是否允许 Agent 继续?
会导致形式化批准。审批人无法判断金额、对象、证据和影响范围,点击批准只是批准了一个未知动作。
诊断方法是检查审批事件中是否能重建当时的动作;如果只能重建 Agent 的自然语言描述,审计信息不足。
2. 审批疲劳
如果低风险动作也频繁弹窗,人工会形成“全部批准”习惯,确认点的安全价值下降。
可以通过风险分层减少数量:
- 低风险:自动执行并抽样评测;
- 中风险:批量确认,但绑定明确范围;
- 高风险:单动作确认;
- 极高风险:双人审批或人工接管。
批量审批必须明确集合边界,例如“批准以下 20 封内部通知”,而不是“批准 Agent 接下来所有动作”。
3. 批准后上下文变化
库存、价格、账户状态或权限可能在审批后改变。动作哈希只能保证参数没有变化,不能保证外部事实没有变化。因此执行前仍应做时效性检查:
批准时余额:¥10,000
执行时余额:¥6,000
如果策略要求余额必须满足阈值,执行前重新校验失败时应拒绝动作并生成新的审批请求。
4. 人工输入被当成可信指令
人工界面中的备注、外部邮件或上传文档可能包含提示注入内容。人工审批不是绕过输入安全检查的通道。审批人填写的结构化字段应经过权限和格式校验;自由文本不能直接拼接成高权限工具参数。
5. 人工选择本身出错
人也会误读金额、选错账户或被社会工程攻击。因此高风险流程仍需要:
- 强认证;
- 清晰展示目标和金额;
- 二次确认;
- 双人规则;
- 高风险操作的独立渠道验证;
- 操作后对账。
HITL 降低的是自动化风险,不会把人为风险降为零。
十五、权限边界:谁能决定,谁能执行,谁承担后果
至少要区分三个角色:
1. 提议者
Agent 或模型生成动作建议。提议者可以拥有读取数据和生成计划的权限,但不一定拥有执行权限。
2. 批准者
人工确认动作符合业务规则。批准者的权限应绑定到动作类型、金额、租户和时间范围。
3. 执行者
工具网关或服务账户真正调用外部系统。执行者应验证批准结果,而不能仅凭 Agent 传来的“已批准”字段执行。
更严格的系统还需要第四个角色:
4. 责任主体
代表哪个用户、部门或法人承担业务后果。责任主体不一定是登录系统的操作员,也不一定是模型开发者。
可以用一个授权关系表示:
其中 是执行时间。缺少任一项,都不应视为完整授权。
责任不能由“模型说了什么”决定
以下责任划分更接近生产现实:
| 环节 | 主要责任 |
|---|---|
| 业务规则定义 | 业务负责人 |
| 模型和提示配置 | Agent/模型工程团队 |
| 数据质量和访问范围 | 数据与系统负责人 |
| 工具副作用和接口语义 | 工具服务负责人 |
| 审批人是否有权限 | 业务审批体系 |
| 是否执行已批准动作 | 编排器和工具网关 |
| 外部系统最终状态 | 外部系统负责人及业务对账方 |
| 事故响应和补偿 | 事先指定的运营与安全团队 |
这不是说模型团队不对结果负责,而是不能把所有系统性问题都归因于“模型幻觉”。如果工具网关没有校验权限、数据库没有幂等约束、审批日志无法证明动作内容,那么事故根因在系统设计,而不只是模型输出。
十六、评测人工介入,而不只是评测模型回答
人工介入系统需要独立的评测指标。
安全相关指标
- 高风险动作拦截率;
- 越权调用阻断率;
- 批准后参数变更检测率;
- 重复副作用发生率;
- 结果未知时的盲目重试率;
- 过期批准被执行的次数;
- 人工接管期间 Agent 误调用工具的次数。
人效相关指标
- 平均审批等待时间;
- 升级到正确队列的比例;
- 单个任务人工操作时长;
- 审批拒绝率及拒绝原因;
- 审批后撤销或补偿比例;
- 人工接管后成功恢复比例。
质量和成本的联合指标
可以把单个任务的期望成本表示为:
模型调用更便宜,不代表总成本更低。如果模型频繁产生需要人工处理的错误计划, 会上升;如果降低人工审批范围导致事故概率上升,最后一项可能远大于模型成本。
评测集也应包含:
- 正常低风险任务;
- 高风险但合法的任务;
- 权限不足任务;
- 证据冲突任务;
- 工具超时和重复投递;
- 审批过期;
- 人工拒绝;
- 接管与释放;
- 恶意提示注入;
- 外部状态已改变的恢复场景。
十七、一个完整算例:退款 Agent 如何运行
假设客服 Agent 可以读取订单和支付记录,但不能直接退款。退款金额超过 1,000 元需要财务人员批准,超过 10,000 元需要两名财务人员批准。
第一步:读取数据
Agent 调用只读工具:
get_order(order_id)
get_payments(order_id)
这两个动作没有外部副作用,但仍要检查租户和客户数据权限。
返回:
订单金额:8,000 元
订单状态:已支付
支付记录:同一订单成功扣款两次
第二步:生成动作
Agent 生成:
refund_payment(
order_id="ORD-17",
amount=8000,
currency="CNY",
reason="duplicate_charge"
)
第三步:策略判断
因为金额大于 1,000 且退款具有资金副作用,策略引擎返回:
requires_approval = true
required_role = finance_approver
required_approvals = 1
任务进入:
WAITING_APPROVAL
Agent 此时可以生成给客户的内部草稿,但不能调用退款 API,也不能向客户承诺“退款已完成”。
第四步:人工查看
审批界面显示订单、两笔支付证据、金额、目标账户、政策版本和动作哈希。财务人员批准。
第五步:执行前重校验
工具网关检查:
- 任务仍为
WAITING_APPROVAL; - 批准状态为
APPROVED; - 当前动作哈希未改变;
- 批准未过期;
- 财务人员角色有效;
- 订单仍满足退款规则;
- 外部支付调用使用唯一幂等键。
第六步:处理超时
如果支付 API 超时,任务进入:
OUTCOME_UNKNOWN
系统查询:
GET /refunds/by-idempotency-key/{key}
如果返回退款成功,任务进入 SUCCEEDED;如果返回不存在,才可以重试;如果支付平台不可查询,则升级给支付运营人员。
反例:为什么不能让模型直接发送“退款成功”
模型可能在工具调用失败后根据计划继续生成:
已为您完成退款,预计原路返回。
这会把“计划完成”误写成“业务事实完成”。正确做法是把外部系统的确认结果作为事实来源:
退款状态 = succeeded
只有在工具或对账系统确认后,Agent 才能使用“已完成”这种事实性表述。
十八、实践中的边界判断
“所有工具调用都让人确认”并不安全
这会造成审批疲劳,也会把低风险操作的处理成本推高。更合理的是根据副作用、敏感性、可逆性和权限分层。
“只让人确认最终回答”通常太晚
如果 Agent 已经发送邮件、修改数据或调用支付接口,最终回答前的确认无法撤回副作用。确认点应位于副作用发生前。
“模型置信度高就不需要确认”是错误推论
置信度反映模型内部预测,不等于业务授权,也不等于事实正确。高置信度地调用错误账户仍然是高风险动作。
“人工接管后可以关闭所有自动检查”是错误设计
接管改变控制者,不应取消权限、数据访问、审计和工具安全检查。人工操作也应经过相同的工具网关,必要时使用更高但明确的权限。
“重试失败工具调用”不是通用恢复方案
必须先判断失败发生在请求发送前、发送后还是结果写入前。对于有副作用的操作,查询状态优先于重试。
“日志里记录了对话”不等于可审计
审计需要记录动作参数、身份、授权、策略版本、工具请求 ID、外部结果和状态转换。自然语言对话只能作为上下文,不能替代结构化事件。
十九、上线前应验证的最小闭环
一个真正可用的人工介入系统,至少应能通过以下测试:
- 高风险工具调用是否一定进入确认或接管状态?
- 确认请求展示的参数是否与最终执行参数完全一致?
- 批准者没有对应角色时,是否无法批准?
- 批准过期后,旧按钮和旧消息是否都不能执行?
- 两个并发批准是否只产生一次有效状态转换?
- 消息重复投递时,是否不会产生第二次副作用?
- 工具响应超时时,系统是否区分未执行和结果未知?
- Agent 在人工接管期间是否无法调用工具?
- 人工拒绝后,模型是否会因重试而绕过拒绝?
- 模型、策略、工具和审批事件是否能够按任务 ID 重建?
- 外部系统已成功、本地状态丢失时,是否可以对账恢复?
- 成本预算耗尽时,系统是停止、升级还是进入人工接管,是否有明确策略?
这些测试验证的不是某个模型版本的回答质量,而是控制权、授权和外部副作用是否被系统正确约束。
人工介入的核心不是“让人参与”,而是让系统明确记录并执行四件事:什么时候自动行动,什么时候暂停,谁有权接手,以及如何证明动作合法且只执行了一次。
确认点解决执行前授权,升级解决能力和责任等级不足,接管解决开放式或异常任务的控制权转移,恢复解决失败和不确定结果,责任边界则把模型建议、系统执行、人工批准和业务后果分开。只有这几层同时成立,Agent 才不是一个会偶尔调用 API 的聊天程序,而是一个具备可控权限、可恢复状态和可审计责任链的生产系统。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:Agent 工具沙箱:文件、命令、网络、凭证、审批与审计
- 下一篇:浏览器 Agent:页面理解、定位、动作、等待、验证和抗变化
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论