让代理停下来等人回答:把澄清建模为持久状态
一个帮忙写验收条件的代理,遇到「逾期提交怎么处理」这种需求里没写的问题,会先提问。可它提完问题后接着就写出了草案,草案里的规则看起来很合理,只是没人回答过那个问题:答案是它自己补上的。另一个版本改成了真的等用户回答,结果服务一重启,正在等的问题就丢了,用户第二天的回答没有任何地方接收。
让模型先问清楚再动手,是减少「看起来对、实际上猜的」产出的好办法。但「提问」只是第一步,真正的工程问题在后面:提问之后,系统处于什么状态。如果没有把「等待人工回答」建模为一个持久的状态,代理要么在同一个运行里自问自答,要么在进程里阻塞等待,进程一重启状态就没了。
本文用一个确定性的假模型和 SQLite 实现一个最小的澄清状态机,验证重启恢复、错题与重复投递、过期、取消和提问预算。实验见文末。
一、自问自答:问题被提出,却没有被回答
最省事的实现是一个循环:模型输出问题,再让模型「根据常识」补一个答案,继续生成。
模型自问自答:直接产出 2 条验收条件,其中 2 条的依据是模型自己编的答案(q-late, q-group)两条验收条件都有依据,只是依据是模型编的。这比不提问更隐蔽:草案里看不出哪些规则是人确认过的,哪些是推测的。提问的价值在于让领域专家补上缺失的知识;同一个模型既提问又回答,等于把缺失的知识换成了模型的先验。
二、状态机:提问之后停下来
把一次澄清任务的生命周期写成状态:
| 状态 | 含义 | 可以转到 |
|---|---|---|
| drafting | 根据已确认的答案判断还缺什么 | awaiting_input、done、needs_human_review |
| awaiting_input | 有一个问题在等人回答,记录问题 ID 与截止时间 | drafting(收到回答)、expired、cancelled |
| done | 草案完成,依据全部来自已确认的答案 | — |
| expired / cancelled | 超时或被取消 | — |
| needs_human_review | 提问预算用完仍有缺口 | — |
关键在于 awaiting_input 是写进数据库的状态,而不是代码里的一次阻塞调用。代理提问之后结束本次执行;回答到达时,由回答触发下一次执行。
新任务:awaiting_input(q-late)
重启后读取持久状态:awaiting_input,仍在等 q-late,没有重新提问(已提问 1 次)重启之后从数据库读出状态,知道自己在等哪个问题,不会重新提问,也不会忘记已经问过几次。
三、收到回答时要检查的四件事
回答一个没有在问的问题:拒绝:在等 q-late,收到的是 q-group
回答 q-late:接受,awaiting_input(q-group)
同一条回答因网络重试再投递一次:重复投递,忽略
回答 q-group:接受,done
任务完成后又收到一条回答:拒绝:当前状态是 done- 问题 ID 要对得上。回答属于某个具体的问题,问题的 ID 要稳定,不能随措辞变化;回答一个不在等的问题应当拒绝,而不是塞进上下文让模型自己理解。
- 按回答 ID 去重。消息渠道会重试,同一条回答可能到达多次。用回答 ID 做唯一键,写入回答和推进状态放在同一个事务里,重复投递就不会推进两次。
- 只在 awaiting_input 状态接受回答。任务已经完成、取消或过期之后到达的回答,不能改变已经产出的结果。
- 检查截止时间:
限时 10 秒的任务:awaiting_input(q-late)
第 11 秒才收到回答:拒绝:已过期,任务转为 expired
等待中取消:已取消;再投递回答:拒绝:当前状态是 cancelled这四件事和处理任何外部回调是一样的:支付回调、审批回调、Webhook 都需要关联 ID、幂等、状态检查和过期。代理场景并没有让这些要求消失,只是让人更容易忘记它们,以为「模型会理解上下文」。
四、提问也需要预算
只允许问 1 个问题:回答之后 needs_human_review,不再继续追问模型可能一直觉得还有不清楚的地方。给提问次数设上限,达到上限仍有缺口时转人工审阅,而不是无限追问或者强行完成。上限的取值是产品决策;重要的是有一个明确的出口状态,并且这个状态对用户可见。
五、草案里的依据要可追溯
草案依据的都是人给出的答案:q-group: 组内任一成员提交,以最后一次为准;q-late: 超过 24 小时记为迟交,不影响继续提交已确认的答案单独存放(谁、在什么时候、回答了哪个问题),草案引用它们,而不是把整段对话塞给模型重新理解。这样后来有人问「为什么迟交只记录不拒绝」,可以追到具体的回答;需求变了,也知道要更新哪一条。
模型提出的问题、人给出的答案、模型推断的内容,是三种可信度不同的东西,在数据上就应该分开。验收条件怎样从这些确认过的规则推出可执行的场景,见从行为到场景到断言;状态机本身的设计方法见把状态分支变成可验证模型。
六、检查清单
- 提问之后结束本次执行,把「等待」写进持久状态;不要在进程里阻塞等人。
- 问题有稳定 ID;回答有唯一 ID,写入与状态推进在同一事务。
- 只在等待状态接受回答,检查问题 ID 与截止时间。
- 提问次数有上限,超出后转人工。
- 已确认的答案与模型推断分开存放,草案引用答案而不是对话。
- 不让同一个模型回答自己提出的问题。
配套实验
- codesphere-labs/ai/clarification-agent-state:Python 3.14.8、SQLite:自问自答的反例,持久状态、重启恢复、错题与重复投递、过期、取消、提问预算(验证记录)
参考资料