Agent 架构75 21 分钟

Agent 什么时候该交给人?把人工接管设计成正常路径

Agent 卡住时,最危险的不是它停下来,而是它明明缺证据、权限或确认,却还在自信地往下做。把升级人工的条件、交接资料和恢复方式写清楚,人工接管才不是一句‘请联系客服’。

原理 实现 边界 追问

本篇阅读顺序 · 三遍读法

一篇文章,读出三种能力。

先弄懂它为什么这样工作,再把条件换一换,看方案还能不能站住,最后用代码和证据复盘一遍。顺序固定,读完才知道自己是真的会了,还是只记住了名词。

01 / 基础知识先回答“它为什么这样工作”

沿着直觉、公式和边界读正文,看到变量就问输入、状态、复杂度分别是什么。

02 / 高频追问再回答“条件变了怎么办”

把面试官的追问当作小型设计评审:更大流量、更少上下文、外部失败或安全约束出现时,局部如何重算。

03 / 从零实现把状态、约束和结果落到一个能验收的闭环

沿代码、表格和证据卡复盘,最后用文末的 60 秒回答确认自己没有只记住名词。

问题面试官到底在判断什么
机制系统如何工作
证据代码、指标与取舍
表达30 秒回答骨架

客服 Agent 遇到一笔金额不明的退款申请。

它先查订单,没有唯一匹配;又查了两次历史对话,还是对不上;最后为了不让用户等太久,给出一句:“已为你处理退款,请留意到账。”

这句话的问题,不在语气,而在它把“还没确认”说成了“已经完成”。

真正可靠的系统,应该在这里停下来,把线索和缺口交给一个人处理。人工接管不是 Agent 失败后的补丁,而是高风险任务里提前设计好的出口。

先给一个能复述的答案

人工接管要回答四件事:什么情况必须升级、升级时把什么资料交给人、人工处理后怎样把状态交回系统、如果一直没人接又怎么办。

我会把任务状态拆成 needs_humanhuman_processingresolvedcancelled,由程序负责状态转换,模型只负责整理事实和提出建议。进入人工队列前,系统要冻结可能产生副作用的动作,保存用户目标、已做步骤、证据、权限、预算和最后一个确定状态。人工处理后必须写回结构化结果和版本号,Agent 只能从新的状态继续,不能把旧对话重新播放一遍。

如果没有可用的人,就明确告诉用户当前卡在哪里、已经完成了什么、下一步何时再试;不要用“再想一遍”或无限重试来掩盖没有 owner 的事实。

一、先分清:什么叫“需要人”

不是所有异常都值得打断人工。网络抖了一下,可以退避重试;参数少一个,可以向用户问清楚;但下面几类情况,继续自动做通常只会放大风险。

情况为什么不能继续自动做人工要确认什么
副作用即将发生扣款、发货、权限变更不可轻易撤回目标资源、参数、影响范围
事实互相冲突Agent 没有依据替用户选边哪个来源和口径有效
权限或身份不明确可能把数据给错人,或越权执行用户身份、资源范围、授权记录
反复没有新信息重复搜索只消耗预算,不会改变结论缺口在哪里,是否换路径
低置信度但高影响“大概没问题”不能作为上线依据风险是否可接受,是否需要审批
规则无法覆盖的新情况模型只能猜,系统没有可验证分支新分支的处理方式和 owner

这里有一个简单判断:如果错误成本比等待一个人更高,就应优先升级;如果只是缺少低风险参数,先向用户澄清;如果还有安全的只读工作可以做,可以交付部分结果,但要把未完成的动作单独标出来。

二、升级前先把“现场”整理好

人工最怕接到一句:“这个任务有点问题,你帮忙看一下。”

如果接手的人还要重新翻聊天记录、猜 Agent 查过什么、确认哪一步已经写入外部系统,升级本身就成了新的事故源。

最小的升级包至少包含这些字段:

{
  "task_id": "refund_20260912_0187",
  "status": "needs_human",
  "reason": "multiple_orders_match",
  "user_goal": "退掉昨天重复扣款的那一笔",
  "completed": [
    "读取用户身份",
    "查询近 30 天订单",
    "找到 2 个金额和时间都相同的订单"
  ],
  "missing": ["无法确认目标订单"],
  "evidence": [
    {"source": "order_api", "ref": "order_781", "amount": 99},
    {"source": "order_api", "ref": "order_794", "amount": 99}
  ],
  "side_effects": [],
  "deadline_at": "2026-09-12T11:30:00-07:00",
  "owner": null,
  "resume_token": "state_v18"
}

这里的 completed 只写已经被工具回执证明的动作,不能把模型计划当成已完成;side_effects 要列出已经发生或可能发生的外部变化;resume_token 用来指向持久化状态,不要把一长段聊天记录当作恢复依据。

如果证据里含有身份证号、联系方式或内部链接,给人工看的页面可以做脱敏,但审计记录仍要保留可追溯的引用或哈希。脱敏不是删除事实,更不是让接手的人只能靠猜。

三、状态机要把人工接管当成一条路

一个实用的状态流可以这样写:

received
  ├─ 信息不足 ───────────────> needs_clarification
  ├─ 低风险、可自动完成 ────> running
  ├─ 高风险或事实冲突 ───────> needs_human
  └─ 不允许执行 ─────────────> rejected

needs_human
  ├─ 有人接单 ───────────────> human_processing
  ├─ 超过等待时间 ───────────> expired
  └─ 用户取消 ───────────────> cancelled

human_processing
  ├─ 补齐证据并批准 ─────────> running
  ├─ 人工直接完成 ───────────> resolved
  └─ 无法处理 ───────────────> rejected

状态转换要由服务端控制,并记录 fromtoactorreasonversion 和时间。模型可以建议“需要人工确认”,但不能自己把状态改成 resolved;人工页面上的“完成”按钮,也要重新检查任务版本,防止有人在等待期间已经处理过同一资源。

一个容易漏掉的细节是租约。人工接单后,任务不能永远挂在某个人名下;可以设置短租约和续租动作,超时就回到队列或升级给值班负责人。这样“有人接了但没有处理”不会变成系统眼里的成功。

四、交接不是转发聊天记录

交给人的资料要按决策顺序排列,而不是按消息时间堆在页面上。

我通常把接手页面分成四块:

  1. 用户要什么:一句话目标、关键约束和截止时间;
  2. 已经确定的事实:每条事实旁边放来源和时间;
  3. 还差什么:缺参数、冲突来源、权限或审批;
  4. 人可以做什么:确认、补查、拒绝、转交,分别对应明确的按钮和结果。

“继续处理”不是一个足够好的按钮。它没有告诉人要处理哪一笔、会不会产生副作用、完成后状态怎样变化。按钮应该写成“确认订单 781 并提交退款”“要求用户补充订单号”或“拒绝并说明原因”,让动作本身就带着边界。

人工做出的决定也要结构化回写:

{
  "task_id": "refund_20260912_0187",
  "decision": "ask_user",
  "message": "请确认要退款的订单号:781 还是 794?",
  "actor_id": "support_42",
  "based_on_version": "state_v18",
  "next_state": "needs_clarification"
}

如果人工决定直接执行写操作,提交层仍应再次检查权限、资源版本和幂等键。人工不是绕过系统规则的超级用户;只是把当前无法自动判断的那一小段决策交给了有责任的人。

五、用户看到的应该是事实,不是内部术语

内部状态可以叫 needs_human,用户不需要看到这个枚举。用户真正关心的是:

  • 现在做到哪一步了;
  • 为什么不能马上完成;
  • 还需要他提供什么;
  • 谁会继续处理,大概什么时候有结果。

比如不要说:

任务进入人工升级队列,等待 workflow owner 续租。

可以说:

我找到了两笔金额相同的订单,但还不能确定你要退哪一笔。现在先不提交退款,请回复订单号 781 或 794;如果你不确定,我也可以把两笔订单的时间和商户发给你核对。

这段话没有承诺不存在的进展,也没有把责任推给一个看不见的“系统”。它把缺口说清楚,还给了用户下一步。

六、没人接手时,系统怎么收尾

人工队列不是无限容量。要提前定义等待时间和降级动作:

等待状态系统动作用户看到什么
0~5 分钟保留任务和证据,通知值班队列已收到,正在确认
超过目标时限提醒 owner,禁止新增副作用仍在核对,给出预计时间
队列满或无人值班转备用队列或交付部分结果哪些已完成,哪些需要稍后继续
超过保留期限关闭任务,保留审计记录任务已关闭,可重新发起

“超时就自动完成”通常不是降级,是把未知结果伪装成成功。对于扣款、删除、发消息等动作,超过时限应保留 unknownneeds_human,等回执和资源状态核对清楚后再决定。

七、怎么评测人工接管做得好不好

不要只统计“转人工率”。转得太少,可能是 Agent 在硬猜;转得太多,人工队列会先被拖垮。至少看下面几组指标:

指标说明
升级准确率真正需要人工的任务有没有被挡住,低风险任务有没有被过度升级
接手完整率人工首次打开页面后,能否直接知道目标、证据和缺口
首次处理时间进入队列到有人开始处理的时间
恢复成功率人工决定写回后,Agent 能否从正确状态继续
重复副作用率接管、重试或恢复过程中是否重复扣款、发货、发消息
用户补充轮次用户需要来回补充几次才能完成任务
超时关闭率没人接、资料不全或队列拥堵造成的关闭比例

评测集要刻意加入边界样本:两个资源都匹配、审批刚过期、工具已经提交但响应超时、用户中途取消、人工处理后版本发生变化。只测“正常订单成功退款”,测不出接管系统是否可靠。

八、一个够小的升级判断函数

判断逻辑可以先写在普通代码里,再决定哪些部分值得交给模型:

def next_route(state, result):
    if result.has_side_effect and not result.confirmed:
        return "needs_human", "副作用尚未得到确认"

    if result.permission == "unknown":
        return "needs_human", "无法确认资源权限"

    if result.conflict_count > 0:
        return "needs_human", "证据存在冲突"

    if state.no_progress_steps >= 2:
        return "needs_human", "连续两步没有获得新信息"

    if state.remaining_ms < state.minimum_commit_budget_ms:
        return "needs_human", "剩余时间不足以安全提交"

    if result.missing_fields:
        return "needs_clarification", "缺少用户参数"

    return "running", "继续自动处理"

这里的顺序很重要:先挡副作用、权限和冲突,再判断是否还有预算。不能因为“还剩很多 token”就绕过安全条件,也不能把用户需要确认的事情当成模型可以自行补齐的字段。

模型适合做的是把 reason 翻译成用户听得懂的话、整理证据摘要、提出少量澄清问题;真正的状态跳转、队列租约、幂等提交和审计记录应该由程序控制。

九、面试官继续追问时怎么答

问:什么时候应该转人工,而不是让 Agent 再试一次?

答:我会先看风险和信息增益。副作用、权限不明、证据冲突、连续无新信息或剩余预算不足时,继续尝试的收益很低而风险在上升,就进入 needs_human。如果只是缺少用户参数,进入澄清;如果是可恢复的网络错误,才按策略重试。

问:人工接管后怎样避免重复执行?

答:升级前冻结可写动作并保存任务版本、资源版本和幂等键。人工提交时重新比较版本,成功后写回结构化结果;Agent 只从新状态继续,不能重新播放旧聊天记录。外部动作已经发出但结果未知时,先对账,不直接重试。

问:人工队列也超时怎么办?

答:把队列租约、目标处理时间和关闭策略写成状态机。超时后提醒、转备用队列或交付部分结果,但不把未知状态改成成功。用户要看到已经完成的部分、未完成的原因和下一步,而不是一句模糊的“系统繁忙”。

最后检查一遍

  • 哪些风险、冲突和权限情况必须升级,已经写成可测试条件
  • 升级包里有目标、已完成步骤、证据、缺口、预算和版本
  • needs_humanhuman_processingresolved 等状态由程序控制
  • 人工接单有租约,超时能回队列或交给备用 owner
  • 人工决定会重新检查权限、资源版本和幂等键
  • 用户看到的是当前事实和下一步,不是内部枚举和日志堆栈
  • 评测集包含冲突、未知结果、取消和版本变化
  • 没有人接手时,系统不会把未知结果包装成成功

人工接管做得好,用户未必会注意到它;但当 Agent 真正遇到模糊、高风险或没见过的情况时,系统不会靠猜测硬撑,而是知道该停在哪里、把什么交给谁、怎样安全地回来。这才是“自动化”可以长期运行的前提。