ByteNoteByteNote
Agent宣布成功时,用户看到的还是白屏:一次验收事故复盘
字

字节笔记本

2026年10月6日 · 约 7 分钟读完

Agent宣布成功时,用户看到的还是白屏:一次验收事故复盘

API中转
¥120

给 AI agent 一个界面,不等于它知道自己在跟哪个页面打交道。DeepSeek 开源的 agent harness 项目 deepseek-harness(MIT 许可)在仓库的 docs/postmortem 目录维护着一份事故复盘合集,专门记录那些「bug 不该出现却出现了,值得追问流程为什么放行」的案例。按该仓库自己定的标准,值得写复盘的 bug 要同时满足三点:机制隐蔽、逃逸原因是系统性缺口而非一次性笔误、重新发现的代价高。其中 0003 号复盘讲的就是一次典型的验收错位:web agent 修改了 GUI 源码,却不知道当前会话对应哪个 URL、由哪个进程承载,一连串单看都合理的动作,最终验收了一个错误的服务器。

事故经过:三个端口,三种「成功」

事故发生在一个运行于端口 3081 的 DeepSeek Harness Web GUI 会话里,用户选择的 Workspace 是一个空的 test/ 目录。模型的请求既没有指明这个 GUI,也没有给出它的源码检出目录、URL、进程和更新模式。仓库在 apps/web 里提供了 Vite 开发脚本,完整的浏览器组合则由 dsh web 命令提供。

接下来每个轮次都「成功」了一次:

轮次 2,agent 修改主题后直接把验收交还给用户,让用户自己运行 pnpm run demo:tui,或打开一个它都没说清楚在哪的 Web 应用,对组装后的 Web 应用没做任何验收。

轮次 3,agent 读取 apps/web/package.json,在端口 5173 启动裸 Vite,看到 HTTP 200 就宣布成功。而浏览器实际抛出 client-modules: window.__DSH_BOOT__ is missing or not an object,页面一片白。这个启动标记只由完整宿主注入,裸 Vite 根本给不了,传输层就绪不等于应用就绪。

轮次 4,agent 找到完整的 dsh web 启动路径,重新构建 shell,在端口 3334 起了一个不受管理的进程,检查了 200 响应和启动 manifest,再次宣布成功。它始终没有探测过端口 3081。

轮次 5,用户指出 3081 其实已经显示新主题,agent 这才检查既有进程并移除冗余服务器。原来用户打开的页面早已加载了重建产物,真正生效的改动靠的是既有服务自己加载的产物,而不是那两个新起的服务器。

这篇复盘的写作方式也值得借鉴:所有结论都来自该会话持久化事件日志的具体序列号,初始请求头在第 6 序列,面向用户的交接、裸 Vite 启动、替代宿主启动、manifest 探测、首次探测 3081 分别落在 30939、31865、34309、34441 和 34681,而不是根据后续报告反推意图。整个调查只读,没有重启或修改那两个试验服务。

一次会话,三个服务器:谁才是验收对象

根因:验收对象从未对齐

按复盘的归纳,问题不在某一个动作,而在系统没有给模型提供身份信息:当前 GUI 是谁、规范 URL 是什么、处于生产还是开发模式,模型一概不知。会话的工作目录正确标识了用户选的 Workspace,模型却把这个空目录当成了应用目录;系统也没有任何持久记录,把 GUI 源码检出目录、构建产物、服务进程、目标 origin 和浏览器验收关联起来。源码修改、成功构建、HTTP 200、注入的启动 manifest 和用户原本打开的页面,就这样被当成了可以互相替代的事实。

几个机制层面的坑叠加在一起。裸 Vite 的 HTTP 200 让错误的启动路径显得合理。更有意思的是,第一版回归测试用另一种方式重复了同样的错误:超时机制杀掉 Vite 之后,非零退出断言照样通过,靠真实复现才暴露了这个误报。另外,agent 用 shell 的 & 绕过了后台进程语义,任务身份、完成通知、结果收集和清理机制全部失效,于是那个 3334 端口的服务器一直活到了下一个轮次,直到用户提出质疑。

修复:让事实可见、可查、可拒绝

防护措施分三层。第一层是暴露事实:web 启动器在 app:web-surface 提示词区段和受管的 $DSH_WEB_URL、$DSH_WEB_MODE 环境变量里,发布规范回环 URL 和实际的生产或开发模式,模型和 shell 都能直接查到。第二层是拒绝错误启动:apps/web 的独立 Vite 服务模式在配置阶段就拒绝启动,配套的子进程测试验证进程能自然退出,还插桩了 Server.listen(),确保短暂绑定端口也逃不过检测。第三层是分层验收:生产模式要求重新构建产物,并在刷新后验证既有 URL;开发模式明确 dsh web --dev 只挂载 HMR 接收端,同一源码检出目录里的 pnpm run dev:web 还必须重建客户端插件 bundle,shell 和普通包的改动仍需刷新页面。

验收标准也随之收紧:PR 证据保留了原始 3081 会话的截图和真实模型驱动的修改前后对比,验收只认外部浏览器、HTTP、进程和会话日志的观测结果。分层的真实路径测试则覆盖了 CLI 请求、精确的生产与开发模式提示词、shell 运行时事实、同端口静态产物替换、源码 watcher 重建、宿主 stat 轮询,以及页面 identity 不变的浏览器 HMR。

修复:把运行时事实交还给模型

四条可以带走的教训

复盘结尾的四条教训,对任何做 agent 产品的人都适用。第一,agent 必须先知道隐藏的运行时前置条件,才能指导用户;启动模式属于应用上下文,不能依赖团队口口相传。第二,HTTP 就绪、构建成功和启动 manifest 是三个不同的事实,验收必须指定确切的 origin,并从外部观察所请求的改动是否真的在那里生效。第三,替代服务无法证明既有页面已经改变;确实收到启动长时进程的请求时,应该使用受管的任务生命周期。第四,回归测试必须能针对所报告的机制失败:进程超时不等于快速失败,进程退出后端口可用也不能证明端口从未被绑定。

这次事故最值得回味的地方在于:agent 的每一步单看都没错,改代码、跑构建、拿 200,但它验收的始终是「我做出来的东西」,而不是「用户看到的东西」。对正在给 agent 接浏览器、接 GUI 的团队来说,这个区别就是全部。

相关文章

分享: