a↗Agent 工程实战中文课程 / v1.0.0⌕ 搜索课程/
本页目录

M05 工作流、人工审批与长任务

本章产出:显式任务状态机、可持久化审批对象、跨会话交接记录。核心问题不是“如何暂停函数”,而是“暂停时什么事实必须留下”。

1. 当用户两小时以后才点批准

同步函数里等待用户输入的演示很简单。真实用户可能关掉浏览器,Worker可能重启,订单可能在等待期间变化。若你一直保持数据库事务和工作线程,资源会被大量等待任务占住;若只把批准标志存在内存,重启就丢失。

因此等待不是睡眠,而是一个持久状态。任务进入WAITING_APPROVAL后保存提案、到期时间和恢复位置,释放执行资源。新的审批事件到来后再调度一次短执行。

2. 为run建立显式状态机

本课程主要状态为:QUEUED、RUNNING、WAITING_USER、WAITING_APPROVAL、RECONCILING、CANCELLING、SUCCEEDED、FAILED、CANCELLED、EXPIRED。

RECONCILING表示外部副作用结果未知,需要查询操作状态或人工处理;它不是“失败后重试”的别名。终态不可被迟到的旧Worker随意改回运行态。WAITING_USER通常等待补充信息,WAITING_APPROVAL必须绑定一个具体操作。

每次转移包含旧版本、目标状态、触发事件和原因。执行UPDATE ... WHERE version=expected_version,更新行数为0时重新读取,而不是覆盖别人已经完成的状态。

I11 · 互动推演独立打开 ↗

等待批准时,Worker去了哪里?

业务等待应持久化,不占用运行线程空转。

合成教学数据 · 未运行真实模型
静态推演与完整规格

I11 等待批准时,Worker去了哪里?

关联章节:M05 · 交互类型:state-machine · 状态:提供完整状态/验收规格,UI待实现

教学目标

业务等待应持久化,不占用运行线程空转。

初始状态

{
  "status": "RUNNING",
  "lease_held": true,
  "approval": "pending",
  "version": 1
}
操作控件
  • 进入等待

  • 尝试claim

  • 批准并恢复入队

  • 重复批准

  • 取消

  • 切换非法转移

状态变化规则
  • RUNNING→WAITING_APPROVAL保存提案并释放lease。

  • 等待中claim拒绝;有效批准通过事务转换QUEUED并写Outbox。

  • 重复批准不创建第二次唤醒;终态不恢复执行。

黄金案例与反馈
wait

输入:


[
  "wait",
  "claim"
]

期望:


{
  "status": "WAITING_APPROVAL",
  "lease_held": false,
  "claim": "NOT_RUNNABLE"
}

等待时间不是工具执行时间。

approve-twice

输入:


[
  "wait",
  "approve",
  "approve"
]

期望:


{
  "status": "QUEUED",
  "new_dispatch_messages": 1
}

恢复与去重须在同一受版本保护的事务里。

terminal

输入:


[
  "cancel",
  "approve"
]

期望:


{
  "status": "CANCELLED",
  "new_dispatch_messages": 0
}

旧批准不能复活终态任务。

完成条件
  • 演示等待释放资源

  • 两次批准只唤醒一次

反馈层次
  • 提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读

  • 结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯

  • 第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开

  • 重置恢复固定初始状态并保留历史尝试供用户手动清除

无障碍与静态替代
  • 全部操作有原生button/input/select替代,不依赖拖拽

  • 图与动画提供同步文字状态表;支持prefers-reduced-motion

  • 状态改变用aria-live=polite摘要,不逐字朗读长日志

  • 焦点不跳走;错误提示关联输入;颜色不是唯一编码

无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。

持久化与重置

使用版本化键agent-course:1:I11,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。

必须避免的误导

恢复与取消分支为项目规格,不能称现有RunLedger内核已完整实现。

执行边界

纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。

3. 审批到底批准了什么

一个approved=true布尔值没有说明订单、金额、币种、收款目标或政策版本。它可以被错误地复用在不同操作上。

课程提案包含:租户、用户、订单、动作、服务端计算的金额、币种、订单版本、政策版本、operation_id和过期时间。将规范化后的关键字段做摘要,是为了检测批准对象是否发生变化;摘要不是授权本身。

授权还需要服务端持久记录:谁批准、通过什么认证上下文、何时批准、批准哪个proposal、是否已撤销。网页计算出相同哈希,不能自行把审批变成有效。模型生成一段“用户已批准”的文本也不算。

由于JSON键顺序、浮点和Unicode可能导致不同序列化结果,本课程金额用整数分、字段集合固定、序列化顺序固定。生产系统需要明确的规范化方案。摘要只包含业务语义字段,不包含随重试变化的attempt_id。

I10 · 互动推演独立打开 ↗

这份批准还能用吗?

批准绑定具体意图与有效期,而不是一句“用户同意”。

合成教学数据 · 未运行真实模型
静态推演与完整规格

I10 这份批准还能用吗?

关联章节:M05 · 交互类型:approval-binding · 状态:提供纯逻辑参考函数,UI待实现

教学目标

批准绑定具体意图与有效期,而不是一句“用户同意”。

初始状态

{
  "approved": {
    "order_id": "A100",
    "amount_cents": 10000,
    "order_version": 1,
    "policy_version": "policy-v2",
    "operation_id": "op-1",
    "expires_at": 130
  },
  "now": 110,
  "same_principal": true,
  "already_committed": false
}
操作控件
  • 改金额

  • 改订单版本

  • 推进时钟

  • 更换当前用户

  • 切换已执行记录

  • 检查是否可新执行/重放

状态变化规则
  • 先检查身份;再检查参数是否绑定同一意图。

  • 尚未提交时now>=expires_at拒绝。

  • 已有同参确认结果时只返回旧结果,不再执行,因此允许过期后的重放读取。

黄金案例与反馈
expired

输入:


{
  "now": 130,
  "already_committed": false,
  "same_digest": true,
  "same_principal": true
}

期望:


{
  "status": "APPROVAL_EXPIRED"
}

有效期边界为严格小于。

replay

输入:


{
  "now": 500,
  "already_committed": true,
  "same_digest": true,
  "same_principal": true
}

期望:


{
  "status": "RETURN_STORED"
}

这不是重新授权一个过期副作用。

changed

输入:


{
  "same_digest": false,
  "already_committed": false,
  "same_principal": true,
  "now": 110
}

期望:


{
  "status": "DIGEST_MISMATCH"
}

新金额需要新的提案和批准。

完成条件
  • 正确区分新执行与旧结果重放

  • 演示金额/身份/时间任一失配拒绝

反馈层次
  • 提交预测后显示结果;允许跳过预测但记录跳过,不阻止阅读

  • 结果同时显示状态变化、原因与被拒绝动作;不是只给红绿灯

  • 第一次错误给边界提示,第二次提供最小反例;参考推导始终可主动打开

  • 重置恢复固定初始状态并保留历史尝试供用户手动清除

无障碍与静态替代
  • 全部操作有原生button/input/select替代,不依赖拖拽

  • 图与动画提供同步文字状态表;支持prefers-reduced-motion

  • 状态改变用aria-live=polite摘要,不逐字朗读长日志

  • 焦点不跳走;错误提示关联输入;颜色不是唯一编码

无JavaScript时直接展示初始输入、规则和黄金案例表,读者仍能完成纸面推演。状态动画仅作解释,不能成为获取答案的唯一途径。

持久化与重置

使用版本化键agent-course:1:I10,仅保存本地学习状态。支持单卡重置和全站清除;存储失败时以内存模式继续,不阻断阅读。

必须避免的误导

页面哈希只能说明绑定,不代替真实认证;本卡是逻辑模拟。

执行边界

纯本地教学状态机;不执行任意代码,不连接真实资金/模型/宿主Shell。

逻辑参考

web/reference/engines.mjs中的approvalDecision;对应测试在web/reference/engines.test.mjs。UI可复用或等价移植,但不得改变黄金结果。

4. 审批前后都有检查

创建提案时,服务端读取订单并计算可退金额。批准时,检查主体有权批准该提案且尚未过期。执行时,再检查订单版本、余额和政策前置条件。

为什么批准后还要检查?用户可能同时在另一设备完成退款;客服可能修改订单;政策可能失效。检查发生在提交副作用的事务或外部服务原子操作里,不能只在几秒前读一次后就盲写。

如果条件变化,返回STALE_PROPOSAL,重新生成需要再次确认的提案。不要静默把用户批准的100元改成120元再执行,也不要无条件把先前批准迁移到新的收款对象。

5. 取消、拒绝、超时是不同事件

拒绝审批表示不允许这个提案执行,任务可以回到解释或结束。审批超时表示授权不再有效,可生成新提案但需要新确认。取消任务表示停止后续执行,不过已经提交的外部操作不一定能够撤销。

UI应区别“已收到取消请求”“执行已停止”“存在待核对操作”。本课程不把取消等同于分布式事务回滚。若外部退款已成功,再执行所谓“反向退款”是一个新的业务操作,需要新的权限和政策,不是自动补偿按钮。

6. 框架的中断并不是任意Python续跑

LangGraph的Checkpointer与Store分别面向线程内图状态和跨线程数据,内存实现不代表进程重启后仍保留。S24

更容易考错的是interrupt恢复语义:官方文档说明,恢复可以从触发中断的节点开头重新执行,节点中interrupt之前的代码可能再次运行。因此不要在前面放未经保护的发邮件、扣款或生成新业务意图键的操作。S25

正确的比较方式不是“框架A能暂停,框架B不能”,而是检查保存边界、重执行范围、外部副作用处理、代码版本变更和迁移能力。课程先实现显式状态机,再通过对照表学习框架的语义。

7. 长任务需要交接协议,不只是摘要

Coding Agent跨多个上下文窗口时,需要知道目标、已经做了什么、哪些测试还没过,以及工作区处于哪个版本。一个散文式摘要可能遗漏“测试已修改”或“依赖只装了一半”。

课程交接对象包括:目标与不可变约束、当前代码或数据版本、已完成的可验收里程碑、失败测试、待验证假设、下一步建议、重要artifact引用。后继执行器应重新验证环境,而不是把交接文字当可信事实。

Anthropic关于长时间运行Agent的文章展示了初始化和逐步推进、保留交接材料的做法。我们提取的是增量验收的原则,不把其中某一模型的行为视为永远成立。S06

8. 什么时候使用固定工作流

身份校验、审批、扣减预算、审计和提交写操作适合确定性工作流。开放式检索、判断是否需要补充材料、组织解释可能交给模型。混合设计能够让模型有灵活性,同时不让它决定哪些授权条件可以跳过。

不要为了“看起来更Agent”把所有业务规则改成Prompt;也不要为了可预测性把所有自然语言变化硬编码成数百个分支。先找清楚变化来自哪里:数据、表达方式,还是业务政策本身。

9. 本章实验

L05的第一部分实现审批对象与状态机,并测试:重复批准、其他用户批准、过期、金额改变、订单版本改变、等待时重启。第二部分在M06把它服务化。

过关标准是:没有一个模型输出字段能够直接授权写操作;任何批准都能追溯到具体提案;等待不占用长事务;恢复路径可被确定性测试覆盖。

K06 · 长任务交接靠显式工件,不靠模型“记住”

K06 长任务交接靠显式工件,不靠模型“记住”

类型:长任务Harness · 关联章节:M05 · 阅读方式:先做下面的推演,再看参考讨论。

原文在说什么

原文讨论长时间运行Agent跨上下文/会话推进任务的困难,以及通过初始化、进度工件与验证组织工作。

对本课程的具体启发(课程推论)

在本课程中,功能列表、操作ID、测试结果和待办状态是持久工件。重开会话时读取经过验证的事实,不要求模型从一段随意摘要恢复所有执行语义。

两分钟推演

设计一次Coding任务交接包,最多六个字段。分别标记哪个字段是权威状态、哪个只是可疑的模型自述。

参考讨论

包含任务目标、基础commit、补丁/工件引用、可信测试结果、未完成项、环境/预算状态。模型写的“已全部完成”不是可信测试结果。

不能从这里推出什么

进度文件本身也可被错误修改。重要状态要可核验,长期任务还需处理外部环境变化。

原始来源

本卡是原创中文概括与教学延伸,不是全文翻译,也不表示原作者认可本课程。不要把原文中的结果替换成本项目实测结果。

独立阅读 →
K17 · Interrupt恢复不是任意Python指令续跑

K17 Interrupt恢复不是任意Python指令续跑

类型:框架语义 · 关联章节:M05 · 阅读方式:先做下面的推演,再看参考讨论。

原文在说什么

LangGraph文档描述持久化与interrupt,并指出恢复时节点会重新执行到中断处,节点内早于interrupt的代码可能再次运行。

对本课程的具体启发(课程推论)

把有副作用动作放在会重复执行的区域时,要有幂等边界。不要把“暂停图执行”误解为保存任意Python栈并从下一字节继续。

两分钟推演

节点中先发送邮件再interrupt等批准,恢复时会发生什么?给出两种修复。

参考讨论

邮件可能再次发送。将发送移到审批后的独立幂等节点,或使用稳定业务键去重;不要仅用进程内布尔变量标记已发。

不能从这里推出什么

不同框架和版本语义可能不同,必须看原文和测试;不能把本卡推广为所有工作流引擎。

原始来源

本卡是原创中文概括与教学延伸,不是全文翻译,也不表示原作者认可本课程。不要把原文中的结果替换成本项目实测结果。

独立阅读 →
做当章练习 →