任务单:让 AI 协作可审计的最小协议
靠聊天记录管理 AI 团队,一周就会失控。我用一个九状态的简单状态机, 把「谁在做什么、做到哪一步、凭什么算完成」变成可查询的记录。
前面写过我在即时通讯群里跑人机混合团队(另一篇)。 群解决了信息可见性,但很快暴露出一个新问题: 群消息是一条扁平的时间线,而任务是有状态的。
一、聊天记录为什么不够用
当同时在跑十几件事,群里会频繁出现这样的对话:
这个昨天不是说完了吗?
没有,还卡在评审。
哪个评审?我记得已经过了。
……往上翻一下,大概三百条之前。
这类沟通每天要发生好几次,每次消耗几分钟,而且没有任何沉淀。 更麻烦的是三件事你说不清:
- 当前状态:这件事到底进行到哪一步?
- 责任人:现在该谁动?
- 完成依据:凭什么算做完了?
所以我把任务从消息里抽出来,做成独立实体:任务单。 群消息还是随便聊,但任何要推进的事,必须在任务单里有一个位置。
二、九个状态,够用了
状态机不需要复杂,需要的是每个状态都有明确的责任人和动作。 我的九个状态是这样的:
| 状态 | 含义 | 谁来动 |
|---|---|---|
| 草稿 | 已创建但还没派出去 | 发起方 |
| 已派单 | 任务已下发,等待接手 | 执行方 |
| 进行中 | 已接手,正在做 | 执行方 |
| 已提交 | 执行方自认完成,等验收 | 验收方 |
| 已验收 | 验收通过 | — |
| 已关闭 | 归档,不再变动 | — |
| 受阻 | 缺条件,做不下去 | 发起方/协调 |
| 退回 | 验收不通过,打回重做 | 执行方 |
| 已取消 | 不做了 | — |
前六个是主干流程,后三个是异常分支。异常分支不能省—— 没有"受阻",执行方就会硬扛或者假装完成;没有"退回",验收就变成了橡皮图章。
一个容易被忽略的点:"已提交"和"已验收"必须分开。 执行方说做完,和验收方认可做完,是两件事。合在一起,等于让执行方自己给自己发合格证。
三、一条不变量,挡住大部分脏数据
任务单的当前状态不单独维护,而是由事件推出来。 每次状态变化记一条事件,任务单上的状态永远等于最后一条事件的目标状态。
status == events[-1].to_status
这条不变量看着平淡,但它是整个设计的支点:
- 状态和事件不可能不一致,因为状态是算出来的;
- 任何一次状态跳变都有对应的记录,能回答"谁在什么时候改的、为什么";
- 出问题可以回放整条链,而不是靠人回忆。
反过来,如果既存状态又存事件,两者迟早会打架。而状态一旦和事实脱节, 所有基于它做的统计、看板、催办都会跟着错。
四、序号从哪来:别去扫表
事件需要一个递增序号来保证顺序。最直观的写法是插入时取
MAX(seq) + 1,我一开始也这么干,然后踩了坑:
- 并发下两个事务读到同一个 MAX,插出重复序号;
- 改成对任务行加锁再取 MAX,相邻任务的锁顺序不一致时会互相等,进而死锁。
最后的解法很朴素:序号由任务行上的计数器字段分配。 写路径统一先锁任务行、拿计数器自增、再写事件。串行化点只有一个,锁顺序固定, 既不会重复也不会死锁。
并发问题不要靠"多想一步"解决,要靠"减少一个并发点"解决。
五、幂等:重复投递是常态
我的任务单有两个入口:人手工创建,和外部系统自动同步。 自动同步意味着网络重试、消息重投是常态,同一条派单可能到达两次。
所以每个写入口都要求带一个业务唯一键,重复到达时返回"已存在"而不是再建一单:
- 派单:同一个任务编码重复投递 → 返回已存在,不再触发下游动作;
- 回执:同一个回执消息标识重复投递 → 去重,不重复推进状态。
这条看起来是工程细节,实际上是协作可信度的基础: 一旦出现重复单,执行方就会去做两遍,验收方会看到两条互相矛盾的记录, 然后所有人开始不信任这个系统,退回到翻聊天记录。
六、最小实现
如果要从零做,最小可用的集合就这么几张表:
task
id 主键
task_code 业务编码(唯一)
title 一句话
status 当前状态(由事件推导)
owner_id 当前责任人
next_seq 事件序号计数器
created_at / updated_at
task_event
task_id
seq 序号(由 task.next_seq 分配)
from_status
to_status
actor 谁做的(人或 AI 的工号)
reason 为什么
evidence_ref 证据引用(链接、文件哈希、命令输出摘要)
created_at
特别提一下 evidence_ref 这个字段。它让"完成"从一个形容词变成一个可点开的链接。
验收方不用相信任何人的口头承诺,点开看就行。
这也是我整套协作机制里,唯一一个必须提前设计好的字段。 其他的都可以后补,这个后补不了——因为历史任务的证据已经丢了。