我花了3个月,把一个终端 AI Agent 搬进了浏览器——踩坑全记录
一、为什么要把终端 AI Agent 搬进浏览器
2025年初,我在终端里跑通了一个基于 LangChain 的 AI Agent,能自动执行代码、调用工具、处理文件。功能很强大,但每次演示都要打开终端、激活虚拟环境、敲命令——同事看了直摇头:「这玩意儿能不能在浏览器里点一下就跑?」
于是,我决定花3个月时间,把这个终端 AI Agent 完整地搬到浏览器里。这篇文章记录了我踩过的所有坑,希望能帮你少走弯路。
二、技术选型:从 WebSocket 到 Web Worker
第一个问题就是:终端里的 Agent 是 Python 写的,浏览器里跑不了 Python。怎么办?
方案一:后端代理 + WebSocket
最直接的想法:后端起一个 Python 进程,浏览器通过 WebSocket 发送指令,后端执行后把结果推回来。这个方案我用了两周,踩了三个坑:
- 连接管理:每个用户一个进程,服务器内存直接爆炸
- 状态同步:Agent 的中间状态(思考链、工具调用)很难实时推给前端
- 断连恢复:浏览器刷新一下,Agent 状态全丢了
方案二:WebAssembly + Python 运行时
用 Pyodide 在浏览器里直接跑 Python?试了之后发现:Pyodide 不支持 subprocess,Agent 调不了系统命令,等于废了一半。
方案三:前端重写 + Web Worker
最终方案:用 TypeScript 重写 Agent 核心逻辑,在 Web Worker 里运行。Worker 里跑 Agent 循环,主线程只负责渲染 UI。这样既解决了进程管理问题,又天然支持断连恢复——Worker 在页面关闭前一直活着。
三、踩坑实录:那些让我熬夜的 Bug
坑1:Web Worker 里的异步陷阱
Agent 的核心是循环调用 LLM,每次调用都是异步的。在 Worker 里写异步循环时,我犯了一个经典错误:
// 错误写法:循环里直接 await,导致事件循环阻塞
while (true) {
const result = await callLLM(messages);
messages.push(result);
}
// 正确写法:用递归 + setTimeout 让出事件循环
function agentLoop() {
callLLM(messages).then(result => {
messages.push(result);
if (!isDone) setTimeout(agentLoop, 0);
});
}
这个 Bug 让我排查了两天,最后用 Chrome DevTools 的 Performance 面板才发现事件循环被阻塞了。
坑2:流式输出的渲染性能
Agent 的思考过程是流式输出的,每生成一个 token 就要更新一次 UI。一开始我用 React 的 setState 直接更新,结果页面卡成 PPT。
解决方案:用 requestAnimationFrame 做节流,每 16ms 批量更新一次 DOM。同时把长文本用 contenteditable 的选区操作来增量插入,避免全量替换。
坑3:工具调用的沙箱隔离
Agent 需要执行代码、读写文件,但在浏览器里不能直接操作文件系统。我用了 OPFS(Origin Private File System)来做沙箱文件系统,用 iframe sandbox 来执行用户代码。
但 OPFS 的 API 是异步的,Agent 的工具调用却是同步的(至少从 Agent 的角度看)。最后我封装了一个 ToolBridge,把异步操作包装成 Promise,Agent 通过 await bridge.call('writeFile', ...) 来调用。
四、架构演进:从单体到微前端
项目做到第二个月,代码量已经超过 2 万行。Agent 核心、工具系统、UI 组件、状态管理全部耦合在一起,改一个地方崩一片。
我做了三件事来解耦:
- Agent 核心独立成包:用
@agent/core管理 LLM 调用、思考循环、记忆管理 - 工具系统插件化:每个工具是一个独立的
ToolPlugin,通过接口注册到 Agent - UI 层用微前端:Agent 面板、文件浏览器、终端模拟器各自独立部署,通过
iframe postMessage通信
五、性能优化:从 5 秒到 500 毫秒
搬进浏览器后,最直观的问题是慢。终端里秒回的 Agent,在浏览器里要等 5 秒才有反应。
优化路径:
- LLM 调用缓存:相同的 prompt 组合直接走缓存,减少重复请求
- Web Worker 预热:页面加载时提前初始化 Worker,用户点击「开始」时直接跑
- 虚拟滚动:Agent 的思考日志可能很长,用虚拟滚动只渲染可视区域
- 增量序列化:Agent 状态用
structuredClone做快照,但只序列化变化的部分
六、总结与建议
3 个月下来,我最大的感受是:把终端工具搬进浏览器,不是简单的「换个 UI」,而是整个架构的重构。
给想尝试的同学几点建议:
- 先想清楚边界:哪些逻辑必须在浏览器里跑,哪些可以放后端
- Web Worker 是你的朋友:别在主线程里跑 Agent 循环
- 做好状态持久化:浏览器刷新是常态,用 IndexedDB 存 Agent 状态
- 性能监控要早做:别等用户反馈卡顿才去排查
最终成果是一个可以在浏览器里直接运行的 AI Agent 演示平台,支持代码执行、文件操作、API 调用等 20+ 工具。虽然过程痛苦,但看到用户在浏览器里点一下就能跑通 Agent,一切都值了。
更多推荐
所有评论(0)