Skip to content

异步双工:让长任务不打断对话,工程上要解决四件事 ​

一句话结论:把"能跑长任务"当成能力已经完成,是绝大多数 Agent 产品在第二个月就会撞上的墙。真正难的不是把任务跑起来,是让它跑的时候对话还能用。

先把这个词说清楚,因为它经常被混用。

异步双工不是全双工。 全双工指对话信道上可以随时打断、抢话;异步双工指对话流与任务流并行推进、互不阻塞,且任务结果能回到原来的会话上下文。前者是通信层的事,后者是调度与状态管理的事。

两种典型错误 ​

错误一:任务孤岛。 对话管对话,任务管任务。用户想问"我那个活跑到哪了",得切到另一个页面;任务跑完也不回流到对话,用户要自己去捞结果。

看起来功能都有,实际上用户被劈成了两个身份:聊天的他,和盯任务的他。

错误二:假并行。 界面上任务在后台跑,但对话被锁住——要么整条对话在等,要么新消息发不进去。用户以为能继续聊,其实在排队。

这两种都不是"实现得不够好",是架构上从第一天就没把两条流当成平等的东西。

要解决的四件事 ​

一、上下文回灌:结果落到哪 ​

任务跑完,结果要回到会话里,但不能污染当前对话的上下文。

这里有个容易被忽略的细节:回灌的内容该带多少上下文?带全量,等于把几万 token 的中间过程塞回会话,下一轮对话就爆窗;只带结论,用户想追问"第二步为什么失败"就无从追起。

实践中比较稳的做法是摘要 + 引用:会话里只落一段结论摘要,完整过程留在任务侧,需要时按引用回查。

二、状态可见性:用户得知道它在动 ​

长任务最伤体验的不是慢,是没有状态。用户看到转圈 40 秒,会开始怀疑是不是卡死了。

所以必须有状态流:不是"进度 37%"这种假精确,而是阶段——"正在检索资料" → "正在生成初稿" → "正在核对引用"。阶段是真实的,百分比是编的。

三、中断语义:能不能撤 ​

长任务要不要支持取消?两个阵营都有道理:

  • 不支持取消:实现简单,但用户发现方向错了只能干等,甚至白白消耗算力点。
  • 支持取消:必须处理部分结果——已经跑了一半的工作怎么算?丢弃、保留成半成品、还是允许从断点续?

我的经验是:支持取消,但明确"取消后保留已产出部分为草稿"。让用户自己决定要不要,比替他决定更安全。

四、失败与续跑:从哪继续 ​

长任务失败是常态,不是异常。关键在检查点:

做法后果
无检查点,失败重头跑又花钱又慢,用户第二次就不敢用了
每步都存检查点存储成本高,但续跑几乎无损
只在阶段边界存平衡点,多数场景够用

同时要保证幂等:重试不能让"发一封邮件"变成发三封。

三种实现路径 ​

路径做法适合代价
任务队列 + 轮询任务入队,前端定时问状态简单、任务少状态延迟、请求量大
事件流推送SSE / WebSocket 把阶段事件推给前端体验最好要维护连接与断线重连
DAG 编排任务拆成有向无环图,节点并行跑复杂任务、可复用实现最重,调试最难

三者不互斥。通常是"队列承接 + 事件流回灌 + 复杂任务走 DAG"。

五个踩过的坑 ​

这几条都是被真实故障教出来的,不是推演出来的:

  1. 模型行为怪异,先查工具返回值。 有一次工具输出按字节硬截断,中文被切在多字节字符中间,模型拿到非法 UTF-8 的尾巴加三成内容缺失,于是反复重读、反复写脚本,最后触发护栏收工。当时第一反应是去改提示词,改了两版无效——根因在工具返回值,不在提示。
  2. "有输出装不下"不等于"不产出"。 判断模型是否空产出时,如果因为长度限制被截断,那是配置问题,不是模型坏了。把这两种混为一谈,会误判健康状态。
  3. 熔断别用"连续同名工具"计数。 模型连续调同一个工具 6 次,可能是卡死,也可能是在批量处理文件。更合理的判据是"换工具就重置"。
  4. 轮次预算不是越高越好。 给长任务放开轮次限制,它不会更聪明,只会更早撞上别的上限。
  5. 状态流要带阶段,不要带百分比。 编出来的进度条,用户第三次就不信了。

一个实现例子 ​

Lyncore 的对话页按这套做的:任务派发后不占用对话流,你继续聊;任务在后台按阶段推进,完成后把摘要回灌到原会话,完整过程留在任务侧可查。

具体交互和边界写在这里:对话与任务执行。

结论 ​

异步双工的难点不在"能不能并行",在四件事有没有想清楚:结果落在哪、状态怎么给、能不能撤、失败从哪续。

这四件里任何一件糊弄,用户第一次用觉得挺酷,第二次就不敢把重要的活交给它了。


相关阅读:五层记忆机制 · 工作流与 DAG