这次抓到一个很有意思、也很有启发性的 Agent 前端问题:同一个 Codex Desktop,只要打开某个历史 session,CPU 就会狂飙;换到别的 session 又正常。最后定位下来,它不是模型推理慢,也不是后台 app-server 在忙,而是前端/桌面壳在加载一份被视觉工具日志撑爆的历史记录。
现象
本机打开 session 019e1263-2ff1-79c0-a274-f25b42b8f015 时,进程表现大概是:
Codex.app/Contents/MacOS/Codex主进程约199%~219% CPUWindowServer被带到50%+ CPUcodex app-server基本空闲- CC Switch、Claude Code 都不是主要负载来源
这说明问题不在模型端,也不在工具真正执行,而在桌面客户端打开历史时的本地解析、内存搬运和渲染。
关键证据
这个 session 的本地 JSONL 文件大小是 184MB,但只有 2626 行。也就是说它不是普通意义上的“很长对话”,而是少量记录里塞了巨大的 payload。
按类型粗分:
response_item/function_call_output:91.1MBevent_msg/mcp_tool_call_end:88.5MBfunction_call_output里有155条带data:image的记录- 全文件命中
data:image/png;base64/input_image/Computer Use state相关内容约304次 - 最大单行接近
2.9MB
进一步看来源,主要是 Computer Use 相关:
computer-use/get_app_state:31.8MBcomputer-use/click:24.3MBcomputer-use/press_key:18.0MBcomputer-use/set_value:11.8MB
这些记录里包含截图、accessibility tree、工具调用结果,而且很多截图被以 data:image/png;base64 内联进 JSONL。更糟的是,同一类信息同时出现在 response_item/function_call_output 和 event_msg/mcp_tool_call_end 两套日志里,天然放大了一倍左右。
CPU sample 也对得上:热路径集中在 Electron/Node/V8 侧的 Buffer / ArrayBuffer 分配、memmove、bzero,并出现 PNG 解码路径。换句话说,打开这个 session 相当于让前端一次性读入一个 184MB 的 JSONL,再解析大字符串、解码图片、恢复 UI 状态、交给渲染层。
相关 issue:
- 我提交的具体问题:https://github.com/openai/codex/issues/22053
- 相近的 Codex Desktop 高 CPU 报告:https://github.com/openai/codex/issues/18467
- 相近的 WindowServer / renderer 持续高负载报告:https://github.com/openai/codex/issues/18567
对 Agent 前端开发的启发
这件事很像一个“AI Agent 前端会踩的新坑”:传统聊天应用的历史消息主要是文本,最多加少量图片;但 Agent 应用的历史记录会混入工具调用、终端输出、截图、浏览器状态、无障碍树、DOM 快照、MCP 结果、调试日志。它们如果都被当作普通聊天消息无脑持久化和渲染,很快会把桌面端拖死。
我觉得至少有几条设计原则值得单独拿出来:
1. 不要把截图当聊天文本存
截图、录屏、DOM snapshot、accessibility tree 都应该是 artifact,不应该作为大段 base64 内联进 transcript。
更合理的做法是:
- 二进制资源单独落盘或对象存储
- 用 content hash 去重
- transcript 里只存引用、尺寸、mime、摘要和缩略图信息
- UI 默认显示缩略图或折叠卡片,用户点开再加载原图
尤其是 Computer Use 类工具,每一次 click / key / get_app_state 都可能带截图。如果每步都内联全尺寸 PNG,几十步就能堆出一个巨型 session。
2. transcript、event log、debug trace 要分层
Agent 前端至少有三种历史:
- 用户真正想回看的对话 transcript
- Agent 运行所需的结构化状态
- 调试/审计用的低层 event trace
这三者不应该在 UI 打开历史时被同等加载。用户点开一个旧 session,第一屏需要的是可读对话,而不是完整工具事件流和所有截图原图。
可以考虑:
- 默认只 hydrate transcript summary
- event trace 延迟加载
- 大工具输出默认折叠
- 只有进入“调试模式”才加载完整 MCP / tool trace
- 对重复事件做引用,而不是复制 payload
这次的问题里,response_item 和 event_msg 同时保存大图,就是一个典型的放大器。
3. 前端必须有“历史大小预算”
Agent 历史不是无限小的文本流。建议每个 session 至少有几类预算:
- 单条消息最大可直接渲染大小
- 单个工具输出最大内联大小
- 单个 session 首屏最大 hydrate 字节数
- 单个 session 的图片/附件总量提示
- 超限后自动外置 artifact 或折叠
比如超过 256KB 的工具输出就不再直接塞进消息树;超过 1MB 的图片只保留缩略图;超过 20MB 的 session 打开时先进入“瘦身视图”。
4. UI 列表必须虚拟化,图片必须懒解码
即便底层存储没有优化,前端也不应该一次性把整个 session 的所有 rich content 渲染出来。
Agent session UI 应该默认:
- virtual list,只渲染可见消息
- 图片 lazy decode
- 折叠长工具输出
- 大 JSON / 大日志用 preview + download/view raw
- 滚动到某个位置才取对应附件
- 对历史消息的 Markdown / syntax highlight 做按需渲染
否则一个 session 里几百张截图,就足够让 Electron / WebView / WindowServer 一起忙起来。
5. 工具输出要有“人类摘要”和“机器原文”两套视图
对用户来说,get_app_state 最有价值的是:当前窗口、关键控件、动作结果。完整 accessibility tree 和截图原图通常只是调试材料。
更好的 UI 可以这样分层:
- 默认显示:工具名、动作、成功/失败、关键摘要
- 展开一级:可读的控件树摘要
- 展开二级:截图缩略图
- 调试入口:完整 JSON、原图、base64、调用耗时
这样既保留可审计性,也不让普通历史回放承担全部成本。
6. 对 Agent 来说,“可回放”不等于“全量重渲染”
很多 Agent 产品会想做 replay:重放浏览器、重放终端、重放每一步工具调用。但历史回放应该是时间轴式的、分段加载的,而不是把所有原始 payload 当聊天消息一次性恢复。
理想状态是:
- transcript 负责叙事
- timeline 负责步骤
- artifact store 负责大对象
- trace viewer 负责深度调试
- renderer 只处理当前视口附近的内容
这次 Codex 的问题,本质上就是 trace 和 transcript 混在一起,且大对象以 base64 直接进入了主历史文件。
一个实用判断标准
如果一个 Agent 前端打开旧 session 时 CPU 飙升,可以先问几个问题:
- session 文件是不是异常大?
- 大小是不是集中在少数几行?
- 有没有大量
data:image/*;base64? - 工具输出是不是同时记录在 message 和 event 两套结构里?
- app-server / backend 是否空闲,而 Electron / renderer / WindowServer 忙?
- sample 里是否是 Buffer、memmove、image decode、syntax highlight、Markdown render?
如果答案是 yes,这大概率不是“模型慢”,而是 Agent 前端的历史载入架构问题。
结论
Agent 应用不能再照搬传统聊天产品的历史记录模型。只要引入 Computer Use、浏览器自动化、截图、多模态工具,历史数据就会从“文本聊天”变成“文本 + trace + binary artifact + UI state”的混合体。
前端如果没有 artifact 外置、懒加载、虚拟列表、折叠工具输出、去重和大小预算,迟早会出现这种奇怪现象:模型没在跑,后台没在忙,只是打开一个历史 session,就能把本地 CPU 拉满。
这对做 Agent 前端的人挺有价值:真正难的不是把工具调用显示出来,而是知道哪些东西不该默认显示、哪些东西不该内联、哪些东西不该在首屏加载。