做桌面端或 Web 端的 Agent 客户端时,经常会遇到一个很烦人的体验问题:
模型在屏幕上已经把最后一个字输出完了,但输入框依然置灰,显示“任务执行中…”,光标点不进去。等上几秒甚至十几秒它才慢慢解锁;如果中途切到别的窗口,有时候直接永久卡死在 loading 状态。
这个现象看起来是前端状态没绑好,但顺着调用链往下排查,本质是后端的异步持久化和事件分发顺序没理顺。
flowchart TD
subgraph Backend[Agent 主进程 / 运行时]
M[收到最后文本片段] --> MC[发送 MessageComplete]
MC --> P[重型异步持久化: SQLite 落盘与文件归档]
P --> AE[发送 AgentEnd 终态事件]
end
subgraph Legacy[传统串行模式]
L1[等待持久化完成] --> L2[通过长队列 IPC 通知前端]
L2 --> L3[清空 isStreaming / 用户感知严重卡顿]
end
subgraph Optimized[解耦与对账优化]
MC -->|乐观判定: 无后续工具| UI1[立即解禁前端输入框]
AE -->|Bypass 终态快轨| UI1
T[10s 周期性 Stale 对账心跳] -.->|极端丢包或失焦兜底| UI1
end
1. 为什么输入框会卡住? #
消息从大模型产出到写进磁盘,通常要过三层:
- 采样运行时:收模型 SSE 流,吐出 token delta 和
MessageComplete; - 宿主主进程(Node.js / IPC 层):负责跨进程通信、写 SQLite 数据库、截屏图片落盘、写 JSONL;
- 渲染进程(前端 UI):React/Vue 管理
isStreaming和输入框禁用。
排查日志后,卡顿主要来自三个地方:
原因一:终态事件排在重型 IO 的 await 后面 #
以前主进程的写法很直白:等当前轮次的所有持久化搞定之后,才发 agent_end 给前端。
// 以前的做法:重型 IO 阻塞了终态通知
await persistTurnToSqlite(turnData); // 50~200ms
await materializeImagesToDisk(images); // 500ms~2s
await appendSessionJsonl(largePayload); // 100~500ms
// 全部写完才通知前端解禁
emitToRenderer('agent_end', session);模型输出完毕时,主进程还在后台忙着写磁盘。在长会话或机械硬盘上,这一步动辄卡几秒,输入框就跟着一直锁着。
原因二:IPC 队列堵塞 #
为了保序,前端通常有个顺序事件队列(Emit Chain)。如果模型收尾时顺手吐了一堆诊断日志,真正的 agent_end 会被排在这些大包后面,产生排队延迟。
原因三:窗口失焦丢包 #
用户在生成中途切到别的软件,Chromium 会降低后台定时器频率;若此时跨进程事件处理出现小抖动,前端把 agent_end 丢了,输入框就会一直卡死在“执行中”。
2. 怎么解决? #
我们在 IPC 通信和前端状态管理上改了三点:
1. 乐观解禁(Optimistic Unlocking) #
输入框能不能用,不应该等磁盘写完。只要前端收到 MessageComplete 且满足三个条件,立刻解禁输入框:
- 回复是 Assistant 的终点(
EndTurn); - 没有未完成的后台工具在跑;
- 用户没有点过停止。
这样用户感知到的输入框解锁延迟直接变成了零。
2. Bypass 终态快轨 #
生命周期事件(agent_end、error、cancelled)跟普通文本增量不一样,优先级应该最高。我们在 IPC 层加了 Bypass 通道:
- 终态事件直接绕过普通的顺序队列,第一时间推给渲染进程;
- 哪怕后台的数据库写入和日志归档还在排队,前端的状态机也能立刻就位。
3. 10 秒低频对账心跳 #
为了防范窗口失焦或极端丢包导致的孤儿状态,前端加了个 10 秒周期的对账循环:
useEffect(() => {
const timer = setInterval(() => {
// 检查是否有超过 10 秒无新数据却依然挂着 streaming 标志的会话
reconcileStaleStreamingSessions();
}, 10_000);
return () => clearInterval(timer);
}, []);只要发现有挂死的 session,主动校准并清理标记。
3. 总结 #
改完之后,任务一结束输入框就能立即打字,切窗口导致卡死的问题也没再出现过。
做客户端应用,前台交互要尽可能乐观敏捷,耗时的持久化放后台异步跑,中间靠定期对账兜底状态一致性。