同一订单先退款一百元,过几天又退款五十元。如果把订单号当作所有执行的唯一标识,第二次可能被误认为重复。反过来,每次重试都生成新标识,又会把一次申请执行多次。
运行时首先需要把“同一个什么”说清楚。会话、任务、执行尝试和业务操作的生命周期不同,不能全部放进一个 request ID。
用一组对象描述真实过程
Thread 或 Session 是交互容器,可以包含多个业务任务。Task 是用户要完成的目标;Run 是一次执行尝试;Step 是其中一个工作单元;Event 是发生过的变化。各框架名字可能不同,本文采用这套术语帮助比较,不宣称它们都有同样的数据模型。
会话 C7
退款申请 T17,退 10000 分
执行 R1:核验后等待批准
执行 R2:提交支付,响应丢失
执行 R3:查询原操作 O17 的结果
退款申请 T18,另退 5000 分
执行 R4:使用新的操作 O18
R2 和 R3 必须关联同一个 O17;T18 则需要新操作,同时仍检查订单剩余可退余额。业务去重以申请与操作的语义为基础,而不是碰巧用了相同模型消息。
ID 应在可信入口产生或验证,不能让模型随意冒充其他租户任务。猜到 task ID 也不代表有权读取;查询接口仍要按当前主体检查归属。
消息、运行状态和账本各保存什么
消息保存用户要求和模型观察,运行状态保存当前阶段与下一步,业务账本记录退款操作与金额。模型说“已退款”不是账本,状态写着 executing 也不能证明支付服务尚未处理。
最小任务记录可以包含 task ID、tenant、order ID、amount_minor、currency、revision、status、operation ID 和动作指纹。金额使用整数最小单位并携带币种;True 在 Python 中是 int 子类,因此严格校验不能只用 isinstance(value, int)。
未知字段与未知事实要区别对待。金额没有提供,应进入补充信息;支付结果未知,应进入对账。两个 unknown 对恢复行为的要求完全不同,不宜只用一个空字符串表示。
状态机表达允许发生的变化
退款任务可以经历 queued、waiting_input、waiting_approval、ready、executing、reconciling、completed,也可能进入 rejected、failed 或 cancelled。状态越多并不一定越好,只有会改变后续行为的区别才需要显式表达。
例如审批拒绝应写入 rejected,不能只是走到图的 END,仍保留 waiting_approval。图执行完毕是框架事实,业务任务终态是应用事实,两者需要明确映射。
终态也不能让在途动作消失。取消在支付提交前可以直接终止;提交后如果结果未知,应先保存取消意图,再查询结果,最终记录实际业务事实。业务上需要补偿时,补偿是新动作,不能把原来成功记录抹掉。
两个更新者如何避免覆盖
任务当前 revision 为 4,用户在界面修改金额,Worker 同时读取旧金额准备执行。使用条件更新可以让其中一个发现状态已经变化:
-- 只有当前仍为预期版本和状态时才更新;执行方必须检查受影响行数。
UPDATE tasks
SET status = 'ready', revision = revision + 1
WHERE task_id = ? AND revision = ? AND status = 'waiting_approval';
影响零行表示条件不再成立,需要重新读取并决定冲突处理。它不一定表示数据库故障,更不应该不加判断地重试同一业务动作。
版本比较只保护参与该比较的记录。它不会自动阻止已经发出的支付请求,也不会同步更新另一台服务。后面的 operation ID、fencing 和对账负责其他边界。
Event 与当前状态怎样保持一致
每次合法变化可以追加事件,包含序号、task ID、事件类型、发生时间和必要数据。当前状态便于查询,事件便于解释过程。如果更新状态成功而事件写入失败,用户可能看不到进度;同一数据库内可以用事务一起提交。
有审计事件不等于采用事件溯源。事件溯源要求事件作为权威记录,当前状态可以由事件重建;普通系统也可以把任务表作为权威状态,事件只是派生记录。应该选择一种明确模型,而不是随意宣称“有 Event 就能重放所有东西”。
Snapshot 用于减少从头重建的成本,应带事件游标和 Schema 版本。读取 Snapshot 后还要应用游标之后的事件。若日志被清理,恢复能力会受到保留策略限制,不能把删除全部历史与任意时间点重建同时承诺。
ID 的映射比统一名称更重要
LangGraph 用 thread_id 定位图状态,Temporal 有 Workflow ID 与 Run ID,AgentScope 使用会话上下文寻址。应用可以把它们关联到自己的 task ID,但不要强行认为这些字段一一等价。
例如一次业务任务可能为了版本升级创建新的工作流执行,业务操作标识仍应稳定。若每次引擎 Run ID 改变都创建新支付操作,会破坏去重语义。
配套 SQLite 实现下载将任务、事件与操作意图分别保存。它以 SQLite 写事务串行保护状态更新;上面的版本条件更新是生产设计示意,未在该简化存储中实现。示例重点展示事务边界,接下来用工作流图解释这些状态怎样被推进。