news 2026/10/6 3:02:36

HITL机制在Agent工作流中的实现:从状态机到审批协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HITL机制在Agent工作流中的实现:从状态机到审批协议

拿到Cowork后台权限的第一个晚上,我盯着任务详情页上那个“等待人工审批”的黄色标签看了很久。HITL(Human-in-the-Loop,人在回路)这个机制,在官方文档里只是一句“支持人工介入任务执行流程”,但真到了要把它从理念落到代码和状态流里的时候,牵扯出来的问题远比我最初设想的要多得多。

这篇内容适合两类人看:一类是正在做Agent编排平台、RAG知识库自动化工具的技术负责人,另一类是业务方里负责设计审批流、风控合规流程的产品经理。我会把Cowork里HITL机制的完整实现思路拆开讲——包括为什么需要它、任务状态机怎么设计、审批事件如何在系统间流转、生产环境必须处理的边界问题,以及我在实际部署和压测中踩过的坑。里面不会出现云里雾里的架构图,全部是能直接抄到你自己系统里的设计细节和代码结构。

1. 为什么Agent工作流必须引入人在回路:先想清楚HITL解决什么问题

1.1 一个真实的翻车场景:全自动Agent的不可控时刻

在讲HITL的实现之前,得先把动机说透。很多人一上来就纠结“状态机怎么画”“事件总线怎么接”,但没想过一个问题:你的Agent真的敢在所有节点上都全自动执行吗?

我最早在Cowork上搭过一个营销内容发布工作流:Agent自动生成推文、自动配图、自动调用发布接口。同事看了一眼说,如果生成结果里价格写错了怎么办?我当时觉得Agent每次生成完我们都会做个简单校验,问题不大。结果上线第三天,Agent生成了一条新品推广,里面把“预售原价599元”写成了“限时599元,第二件半价”,而第二件半价是上一季的活动,系统直接发出去了。

那件事之后我意识到,Agent执行链路上有两类操作的性质完全不同:一类是“生成内容”“检索信息”“生成图片”这类可逆、低风险的步骤,错了可以重新生成,成本很低;另一类是“发布内容”“发送邮件”“扣减库存”“执行转账”这类有实际副作用、不可轻易回滚的步骤,错了就是事故。HITL机制解决的核心问题只有一个:在高风险、不可逆、低确定性的节点上,强制插入一个人类裁决点,让最终责任落到人身上,而不是让大模型独自承担。

1.2 HITL的三种介入形态:不只有“审批”一种

做实现之前先把场景定义清楚。HITL不是一个单一功能,在Cowork里它至少包含三种不同的介入形态,虽然共享同一套基础设施,但交互逻辑差别很大:

介入形态触发时机典型场景人工操作
前置审批(Pre-approval)Agent生成方案后,执行副作用前内容发布、邮件群发、退款执行批准/驳回/修改意见
运行中接管(Escalation)Agent执行过程中遇到不确定分支识别到用户情绪激动、法律法规相关边界指定新方向、补充信息
结果反馈(Post-hoc Review)Agent产出一轮结果后代码提交前的MR评审、报告生成打分、修正、标记偏好

前置审批是HITL里最重的一个环节,也是Cowork工作流编排中最常用的。它和人机交互里的“人在回路”一样,本质上是把“全自动执行”拆成“自动执行 → 人工裁决 → 继续执行”的三段式。运行中接管的触发条件更复杂一些,通常由Agent自己判断置信度,低于阈值就主动请求人工介入。这个能力在客服机器人、自动化运维工单里很关键,因为Agent遇到模棱两可的指令时,继续跑只会放大错误。

1.3 不是所有节点都适合HITL:审批节点的选取原则

刚开始设计的时候,我犯过一个方向性错误——把流程里几乎所有节点都加了人工确认,认为这样最安全。结果整个工作流跑一次需要三四个小时,大部分时间都浪费在等人点按钮上,Agent的优势完全没了。

后来我总结出一套判断标准:当一个节点满足“有明确副作用 + 不可轻易回滚 + 判断规则复杂”三个条件中的任意两个时,才适合加HITL。纯信息检索不需要、纯文本生成不需要、中间运算结果校验不需要。在Cowork里配置工作流时,每个节点都可以设置“执行模式”,有的节点是Auto、有的是Manual,Manual节点就是HITL接入点。这需要在平台设计层面刻意区分“Agent内部处理步骤”和“对外部世界产生影响的步骤”,后者才能挂审批。

另一个实际经验是:审批节点放在“副作用即将发生前的最后一步”,而不是放在流程里觉得“该看一下”的位置。越靠近真实操作,容错空间越小,人工介入的价值越大。比如内容发布流里,审批节点应当放在“调用发布API”前一刻,而不是放在草稿生成后——这样审批人看到的稿件就是最终发布的版本,不会有中间步骤导致的偏差。

2. 任务状态机的核心设计:从“运行中”到“等人批”到“继续跑”

2.1 完整状态流转:一张状态表说清楚所有生命周期

确定了哪些节点需要HITL之后,真正实现上最核心的就是任务状态机。Agent任务在执行到审批节点时,它不再是简单的“执行中”,而是进入一个等待人工响应的中间状态。我在Cowork的调度引擎里设计了一套包含九个状态的生命周期模型:

状态含义进入条件离开条件
pending任务已创建,等待资源调度用户或Agent创建任务调度器分配执行器
runningAgent正在正常执行分配执行器后完成 / 遇到审批点 / 失败
awaiting_approval任务在审批节点挂起执行到Manual节点审批回调到达 / 超时
approved审批通过,待恢复执行审批人点击通过调度器重新调度
rejected审批驳回,不再执行审批人点击驳回终止 / 转人工处理
resuming从挂起点恢复上下文调度器拉起待恢复任务恢复成功
completed全部节点执行终了最后一个步骤成功终态
cancelled任务被主动终止用户取消 / 流程失效终态
failed运行中异常退出不可恢复异常终态

这里有个容易混淆的点:approved和resuming看起来很接近,但它们是两个不同的阶段。approved表示审批结果已经写入,而resuming才是真正把Agent上下文恢复、继续执行后续步骤的过程。故意拆开是为了解耦——审批动作是一个轻量事件,而恢复执行是一个可能涉及重载上下文、重新初始化模型会话的重操作,两者不能在一个事务里完成。

2.2 为什么不能靠线程sleep等人:阻塞式方案的三个死穴

我在第一版实现里犯过一个典型错误:Agent程序执行到需要审批的节点时,直接在一个线程里轮询数据库,看审批结果有没有出现。代码长这样:

while status != "approved": time.sleep(5) status = get_approval_status(task_id)

本地demo跑起来没什么问题,但一旦放到生产环境,这个方案有结构性缺陷。第一,线程被白白占住,每个挂起的任务都要吃掉一个常驻线程,Agent执行器本身是要跑模型推理的,资源既没被利用又无法释放,100个挂起任务就能拖垮节点;第二,进程重启就死,执行器的进程一重启,等待线程全部消失,任务直接卡在awaiting_approval状态再也醒不过来,除非有人工补偿脚本;第三,无法水平扩展,所有等待线程都在同一个进程里,想多挂一台执行器做负载均衡都做不到。

这条经验的本质是:在Agent工作流里,“等人”是一个状态问题,不是线程问题。任务应该把自己标记为“等待外部事件”,然后把占用的所有资源主动释放掉,剩下的事情交给事件驱动机制。

2.3 事件驱动实现:把“等人”变成“等消息”

正确的实现方式是用事件总线配合持久化存储。我设计了这样一个闭环:

  1. Agent执行到Manual节点时,调度引擎把任务状态从running改为awaiting_approval,同时把当前执行进度、上下文快照写入存储,并发布一条approval.requested事件到事件总线。
  2. 审批中心订阅这个事件,在人工工作台展示出来,按审批人分配给对应人员。
  3. 审批人在界面上点“通过”,审批中心发布approval.resolved事件,事件体里带上decision: approved、decision_id、approver_id和comment。
  4. 调度引擎收到approval.resolved后,先校验decision_id去重,再把任务状态更新为approved,放入待恢复队列。
  5. 空闲的执行器从待恢复队列拉取任务,从快照点恢复上下文,状态更新为resuming,继续执行后续节点。

这个链路里,Agent执行器在整个等待期间完全闲着——不占线程、不占显存,进程重启也不影响任务状态,因为状态和快照都在持久化存储里。审批系统可以单独部署、单独扩展,甚至审批人在手机上点一下,事件也能正常到达调度引擎。这就是事件驱动和轮询的本质区别:一个在等“消息”,一个在等“时间”。

2.4 快照恢复:不重跑任务的唯一正确姿势

任务恢复执行时有个关键设计:不能从头再跑一遍。Agent流程动辄十几个节点,每次执行都会调用模型推理、检索知识库、生成中间结果,重跑一遍不仅浪费钱,还会导致上下文不一致。我在Cowork里采用的方案是节点级快照:

{ "checkpoint_id": "cp_20250115_001", "task_id": "task_79a3f2", "node_id": "publish_review", "executed_nodes": ["draft_generate", "image_generate", "fact_check"], "pending_nodes": ["publish_api"], "data_artifacts": { "draft_text": "s3://cowork/artifacts/task_79a3f2/draft.md", "image_path": "s3://cowork/artifacts/task_79a3f2/cover.png" }, "llm_context": { "conversation_id": "conv_8a2e", "history_summary": "已生成推文初稿,完成事实核查,等待最终批准" } }

恢复时把执行器指到publish_review之后的下一个待执行节点,从data_artifacts里加载中间产物,继续跑就行。这里有个细节:llm_context里不要存全部对话历史,存一个摘要就够了——因为Agent重新恢复时,前面的工作已经落成了结构化产物,对话历史的边际价值很低,存摘要能显著降低序列化体积和恢复耗时。

3. 审批协议设计:批准、驳回、带修改意见的完整链路

3.1 审批事件的请求与响应结构:一个标准JSON模板

状态机定完之后,要设计审批事件在系统之间的通信协议。HITL机制跨了“任务编排引擎”和“人工审批中心”两个子系统,既然实际部署时它们可能跑在不同服务甚至不同团队维护的代码库里,协议就必须稳定、自解释。

我最终采用的审批操作接口长这样:

{ "event_type": "approval.resolved", "event_id": "evt_01JQ2X", "timestamp": "2025-01-15T14:32:07Z", "task_id": "task_79a3f2", "checkpoint_id": "cp_20250115_001", "decision_id": "dec_55f8c1", "decision": "approved", "approver_id": "user_lisi", "comment": "文案没问题,可以发布", "next_approver_id": null }

几个字段的选型理由说一下。event_id和decision_id都是幂等键,前者防止事件总线重复投递,后者防止审批人重复点击;checkpoint_id把审批事件和具体的快照版本绑定起来,避免任务已经被修改后收到陈旧回调;next_approver_id支持多级审批——第一级通过后自动流转到第二级审批人,这时任务状态应该保持awaiting_approval,而不是进入approved。

驳回时的协议也差不多,只是decision变成rejected,同时comment字段是必填的。我特意要求驳回必须带意见,原因后面第3.3节会说,这直接关系到Agent能不能根据反馈修正。

3.2 审批人看到的不是代码,是决策摘要

这里有个容易被技术团队忽略的体验点:审批人通常不是工程师,他们可能是运营、法务、财务这些完全不看系统日志的人。所以审批中心展示的内容必须经过抽象,我总结出“一张卡片四要素”结构:任务的目标是什么、Agent做了什么、将要触发什么动作、风险提示是什么。

一个内容发布审批卡片长这样:

  • 任务目标:新品发布会预热推文
  • Agent已完成:生成推文初稿、生成配图、完成敏感词校验
  • 将要执行:调用官方微博接口直接发布
  • 风险提示:发布后无法撤回修改,请确认文案中的价格信息无误

如果审批人看到的是这种卡片,平均决策时间可以从几分钟压缩到十几秒。技术上的难点在于:决策摘要本身也要由Agent生成,这意味着在执行到审批节点前,编排引擎要先让Agent用专门的摘要Prompt对当前任务做一次提炼。这一步必须在快照里一起保存,审批中心直接从快照里取,而不是在审批界面动态生成。

3.3 修改意见如何编入提示词:驳回不是终点

相比二进制式的“通过/驳回”,带修改意见的审批才是HITL机制真正发挥价值的地方。Agent被驳回后不应该只是停止任务,而应该能根据人类反馈重新生成一版方案,这让HITL从一个“安全闸门”升级成“质量闭环”。

恢复执行的Prompt模板我做了专门设计。在resuming阶段,调度器会把审批意见作为一条高优先级消息注入到Agent的会话上下文里:

系统通知: 你之前的方案在节点「发布审核」被审批人驳回。 审批人意见:文案里“限时599元,第二件半价”是上季活动,本期只做599元预售,请修正价格描述,语气从促销风改为新品介绍风。 请基于保存的草稿内容重新生成最终版本,不要重复执行已完成的步骤。

值得注意的是,这条注入消息要放在system层而不是user层。因为user层的消息会被Agent当成“新的用户需求”处理,可能引发它重新理解整个任务;而system层的指令是“对当前执行计划的修正”,能更精准地约束Agent只调整被点名的地方。

我被现实教育过一次:一开始把意见放在user层,结果Agent重新生成了整个营销方案,还把配图风格也改了,审批人第二次看到直接打回来说“我只让你改价格”。从那之后我养成了一个习惯:研发同学设计反馈注入逻辑时,一定要明确告诉Agent“改动范围”和“哪些步骤已完成不要重做”。

4. 生产环境的边界问题:超时、取消、重复回调与审计回溯

4.1 审批超时不等于永久等待:分级处理策略

真实业务里,审批人不是随叫随到的。凌晨两点发起的审批,第二天早上九点才有人看到,这期间任务一直挂在awaiting_approval状态,既占着存储,又可能会让后续流程整体延迟。超时策略不能一刀切,我采用的是分级通知加自动降级:

等待时长动作说明
15分钟发送站内提醒轻打扰,不弹窗
60分钟发送IM通知+短信标记为“待处理”
4小时升级到审批人的直属上级同时抄送任务创建者
8小时自动转为“默认拒绝+转人工”不入Agent自动执行,防止僵持

超时自动降级到哪个状态,需要结合业务场景决定。比如内容发布类任务,超时后我建议默认转草稿(相当于拒绝但不删除,人工后续可以手动发);而像“批量发送授权邮件”这种任务,超时后直接取消更合理,因为等太久邮件内容可能已经过期。

4.2 幂等与去重:同一张审批单只能生效一次

审批系统对接的外部环境没那么干净。审批人可能连续点两次“通过”,审批中心的Webhook可能因为网络抖动重试,事件总线的at-least-once投递语义也会导致同一个事件被调度器收到多次。如果不做去重,同一个任务会被恢复执行两次,副作用操作就会重复发生——比如发布两次推文、给用户发两封确认邮件。

我的方案是双键去重:事件到达调度器先查event_id,处理过就直接丢弃;更新任务状态时WHERE条件加上checkpoint_id匹配,update返回影响行数为0就说明状态已被更新过。第一道防重复投递,第二道防并发竞争。这两个加起来基本能保证审批事件生效且只生效一次。

4.3 多级审批与并行会签:两条路径一个协议

多级审批是最容易被低估复杂度的场景。一条采购审批流,可能要经过“部门负责人 → 财务 → 总经理”三级,每一级通过后才进入下一级,最后一级通过才恢复Agent执行。我在协议里用next_approver_id字段解决串行流转:调度器收到approval.resolved后检查该字段,如果还有下一级,任务状态保持awaiting_approval不变,只把审批人换成下一个;没有了才改成approved。

还有一种是并行会签场景,比如“公告发布”需要市场部和法务同时点头。这种实现上可以拆成多个审批子任务,每个子任务独立产生approval.resolved事件,调度器维护一个“通过计数”,计数达标才恢复执行。并行会签的协议不需要大变,但要注意每收到一个事件都要更新计数并持久化,否则又是并发一致性问题。

4.4 审计回溯:每一个决策点都能重放现场

HITL机制天然是一个高风险动作的密集区,审计能力是必须做的。等到出问题再来追查为什么Agent发布了错误内容,才发现没有任何记录,那才是最被动的。我在Cowork里为每个审批节点保存了三类审计数据:

  • 审批动作记录:审批人、时间、决策、意见原文
  • 上下文快照:Agent执行到审批节点时的全部中间产物、使用的Prompt版本
  • 模型调用日志:快照中每一次LLM调用的输入输出(只存摘要和关键片段,全量保存成本太高)

这三类数据合起来能还原“为什么Agent会生成这条内容”以及“为什么审批人放行了它”。这里给一个成本优化建议:模型调用日志只保存可能引发风险的节点附近数据,不用全流程保存。比如内容生成这一步调用日志要留,而知识库检索这一步留个检索词就够了。

5. 实测中的优化与体验取舍:把审批延迟变成系统竞争力

5.1 挂起状态下的资源释放与成本控制

等待审批期间,Agent执行器完全空闲。第一版我犯过一个隐形浪费,因为Agent实例没有显式销毁,只是不再跑新节点,但实例依然在内存里占着模型会话、向量索引等资源。后来我改成了在进入awaiting_approval时,调度器主动将Agent实例序列化并销毁,恢复时才重新load快照重建实例。这个改动之后,挂起上千个等待审批的任务,只占存储空间,不占任何计算资源。

成本上还有个可量化的收益。模型推理是按token计费的,等待期间不允许Agent做任何自说自话的推理任务(比如自己预演后续步骤),这部分token消耗可以直接降为零。我用一个月的数据对比过:加上HITL机制后,单个任务的平均token消耗只增加了5%(摘要生成和恢复会话的注入),换来的却是关键节点的100%人工可控,这笔账非常划算。

5.2 审批期预执行:哪些活可以提前干

等待审批的这几分钟甚至几小时里,不是所有步骤都得干等。有些下游步骤不依赖审批结果、也没有副作用,可以预先执行掉。还是内容发布的例子:审批人正在看推文文案,此时完全可以并行把图片上传到CDN、生成短链、构建好正文HTML模板,审批一通过了之后,发布就是一次纯API调用,几十毫秒完成。

这套预执行方案需要注意一个原则:可以预执行的是准备类、无副作用的工作,绝不能预执行“发布”“发送”“扣库存”这类动作。我专门给每个节点标注了can_preheat标记,调度引擎在任务进入awaiting_approval的瞬间,就把所有可预热节点的任务投递到一个专用预执行队列。如果审批被驳回,这些预热产物清删掉即可,浪费的仅仅是算力,不会产生业务副作用。

5.3 恢复速度是体验的生命线:从几秒压到几百毫秒

审批点通过之后,Agent多久能恢复运行,直接决定用户对系统的感知。我第一版恢复流程走的是“从持久化存储load完整历史 → 重新初始化LLM会话 → 重新构建提示词”,整个流程要4到6秒。后来优化成两层:

预热层:审批卡片在页面渲染时,前端就上报“正在查看”,后台立刻把对应快照加载到本地缓存。恢复层:调度器收到approval.resolved时,直接从缓存拿快照,省掉存储IO,还有一步关键的优化——上一步Prompt模板内嵌在快照里,不需要重新拼装。优化后恢复平均耗时降到了600毫秒左右,用户几乎感觉不到任务曾经被暂停过。

6. 从HITL到数据飞轮:每一次人工审批都是标注数据

当HITL跑通之后,有人会问:这个机制是不是只是给Agent上了个安全锁?我的体会是远不止如此。每一次人工审批的“通过/驳回/修改意见”,本质上都是一条高质量的标注数据——它包含了“什么场景下,Agent的哪个决策是错的、正确的应该是什么”。这类数据比任何benchmark都值钱,因为它是真实业务里的真实反馈。

我在Cowork里加了两个分析维度:按驳回率排序节点,按驳回理由聚类关键词。跑了一周之后发现,内容生成节点的驳回率高达40%,其中超过一半的驳回理由集中在“语气不符品牌调性”和“数字细节有误”。于是做了两件事:在生成节点前的Prompt里专门加了品牌语料few-shot示例,并接了一条“数字核对”子任务,强制Agent在生成后被一个专门的校验Agent复核价格、日期、型号这类关键字段。第二周,驳回率从40%降到了16%。

这就是HITL和数据飞轮的闭环:人工介入不是为了“打断”Agent,而是为了给Agent的优化方向提供监督信号。改动Prompt后,被驳回的任务会自动进入一个“样本池”,我不定期抽取样本池里的驳回记录做分析,再反哺到Prompt迭代里。

回到最初的问题,HITL机制的实现,真正的难点不在于某个状态怎么设计、某个事件怎么传递,而在于你有没有想清楚:这个系统在什么节点上必须停一下,停下来让谁看、看什么,以及看的结果如何反哺下一次自动化。等这套机制运转顺了,你会明显感觉到,Agent系统终于从“厉害的玩具”变成了“敢真正派活的员工”——它负责干活,而你在关键路口握着方向盘,这比什么都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:02:33

C++数据结构实训:基于链表的作业管理系统实现与避坑指南

简介:一套用于数据结构C实训的作业完成情况管理程序资源,适合正在学习数据结构与C面向对象编程的高校学生,旨在通过实现作业添加、更新与查询功能,帮助学习者掌握数组、链表、栈与队列等数据结构的实际应用。压缩包内共十一个文件…

作者头像 李华
网站建设 2026/10/6 3:02:03

Redis 分布式锁宕机丢失怎么办?从持久化到 RedLock 全解析

面试官问出“Redis 宕机了锁不就丢了吗”这句话时,其实是在考察你有没有真正理解分布式锁的边界条件。很多人的第一反应是“那用 Redisson 啊,有看门狗自动续期”,但这个回答在面试官面前往往只能得个及格分。要真正把这个问题答透&#xff0…

作者头像 李华
网站建设 2026/10/6 3:02:03

微信小程序商城Java源码部署与实战避坑指南

简介:这是一套面向Java后端开发者与小程序初学者的微信在线点餐系统源码,聚焦餐饮行业轻量化SaaS解决方案,覆盖用户点餐、菜品管理、订单处理及微信支付全流程。资源共60个文件,包含10个JS逻辑文件(实现页面交互与API调…

作者头像 李华
网站建设 2026/10/6 3:01:57

Redis缓存淘汰算法详解:LRU与LFU实现原理、配置与调优

线上 Redis 内存被打满、OOM 告警、缓存命中率一夜之间掉一半——这类事故里,有相当一部分的根因不在机器资源,而在淘汰策略。LRU 和 LFU 这两个词,大家在八股文里都背过,都知道一个是“最近最少使用”、一个是“最不经常使用”&a…

作者头像 李华
网站建设 2026/10/6 3:01:46

SSM + 微信小程序 + MySql:校园闲置物品交易平台毕业设计全流程实战

简介:这份资源是一套面向毕业设计场景的大学生闲置物品交易小程序完整项目,基于微信小程序SSMMySQL开发,源码、数据库脚本、毕业论文与演示视频一并打包,适合计算机相关专业学生完成课题设计或快速搭建校园二手交易原型。压缩包共…

作者头像 李华
网站建设 2026/10/6 3:01:08

联机大厅工具开发:房间人数监控与连接保活实践

东方非想天则的老玩家应该都有这种感受:大厅联机是这游戏最灵魂的部分,但也是最让人头疼的部分。房间列表时好时坏,人数刷不出来,自己建的房间没一会儿就消失,对手进不来,或者大厅直接弹错误。我陆陆续续帮…

作者头像 李华