异步双工:让长任务不打断对话,工程上要解决四件事
一句话结论:把"能跑长任务"当成能力已经完成,是绝大多数 Agent 产品在第二个月就会撞上的墙。真正难的不是把任务跑起来,是让它跑的时候对话还能用。
先把这个词说清楚,因为它经常被混用。
异步双工不是全双工。 全双工指对话信道上可以随时打断、抢话;异步双工指对话流与任务流并行推进、互不阻塞,且任务结果能回到原来的会话上下文。前者是通信层的事,后者是调度与状态管理的事。
两种典型错误
错误一:任务孤岛。 对话管对话,任务管任务。用户想问"我那个活跑到哪了",得切到另一个页面;任务跑完也不回流到对话,用户要自己去捞结果。
看起来功能都有,实际上用户被劈成了两个身份:聊天的他,和盯任务的他。
错误二:假并行。 界面上任务在后台跑,但对话被锁住——要么整条对话在等,要么新消息发不进去。用户以为能继续聊,其实在排队。
这两种都不是"实现得不够好",是架构上从第一天就没把两条流当成平等的东西。
要解决的四件事
一、上下文回灌:结果落到哪
任务跑完,结果要回到会话里,但不能污染当前对话的上下文。
这里有个容易被忽略的细节:回灌的内容该带多少上下文?带全量,等于把几万 token 的中间过程塞回会话,下一轮对话就爆窗;只带结论,用户想追问"第二步为什么失败"就无从追起。
实践中比较稳的做法是摘要 + 引用:会话里只落一段结论摘要,完整过程留在任务侧,需要时按引用回查。
二、状态可见性:用户得知道它在动
长任务最伤体验的不是慢,是没有状态。用户看到转圈 40 秒,会开始怀疑是不是卡死了。
所以必须有状态流:不是"进度 37%"这种假精确,而是阶段——"正在检索资料" → "正在生成初稿" → "正在核对引用"。阶段是真实的,百分比是编的。
三、中断语义:能不能撤
长任务要不要支持取消?两个阵营都有道理:
- 不支持取消:实现简单,但用户发现方向错了只能干等,甚至白白消耗算力点。
- 支持取消:必须处理部分结果——已经跑了一半的工作怎么算?丢弃、保留成半成品、还是允许从断点续?
我的经验是:支持取消,但明确"取消后保留已产出部分为草稿"。让用户自己决定要不要,比替他决定更安全。
四、失败与续跑:从哪继续
长任务失败是常态,不是异常。关键在检查点:
| 做法 | 后果 |
|---|---|
| 无检查点,失败重头跑 | 又花钱又慢,用户第二次就不敢用了 |
| 每步都存检查点 | 存储成本高,但续跑几乎无损 |
| 只在阶段边界存 | 平衡点,多数场景够用 |
同时要保证幂等:重试不能让"发一封邮件"变成发三封。
三种实现路径
| 路径 | 做法 | 适合 | 代价 |
|---|---|---|---|
| 任务队列 + 轮询 | 任务入队,前端定时问状态 | 简单、任务少 | 状态延迟、请求量大 |
| 事件流推送 | SSE / WebSocket 把阶段事件推给前端 | 体验最好 | 要维护连接与断线重连 |
| DAG 编排 | 任务拆成有向无环图,节点并行跑 | 复杂任务、可复用 | 实现最重,调试最难 |
三者不互斥。通常是"队列承接 + 事件流回灌 + 复杂任务走 DAG"。
五个踩过的坑
这几条都是被真实故障教出来的,不是推演出来的:
- 模型行为怪异,先查工具返回值。 有一次工具输出按字节硬截断,中文被切在多字节字符中间,模型拿到非法 UTF-8 的尾巴加三成内容缺失,于是反复重读、反复写脚本,最后触发护栏收工。当时第一反应是去改提示词,改了两版无效——根因在工具返回值,不在提示。
- "有输出装不下"不等于"不产出"。 判断模型是否空产出时,如果因为长度限制被截断,那是配置问题,不是模型坏了。把这两种混为一谈,会误判健康状态。
- 熔断别用"连续同名工具"计数。 模型连续调同一个工具 6 次,可能是卡死,也可能是在批量处理文件。更合理的判据是"换工具就重置"。
- 轮次预算不是越高越好。 给长任务放开轮次限制,它不会更聪明,只会更早撞上别的上限。
- 状态流要带阶段,不要带百分比。 编出来的进度条,用户第三次就不信了。
一个实现例子
Lyncore 的对话页按这套做的:任务派发后不占用对话流,你继续聊;任务在后台按阶段推进,完成后把摘要回灌到原会话,完整过程留在任务侧可查。
具体交互和边界写在这里:对话与任务执行。
结论
异步双工的难点不在"能不能并行",在四件事有没有想清楚:结果落在哪、状态怎么给、能不能撤、失败从哪续。
这四件里任何一件糊弄,用户第一次用觉得挺酷,第二次就不敢把重要的活交给它了。