A) 起因
这两年 Code Agent 多了很多。它们大多可以读代码、执行命令、修改文件,看起来已经足够成熟。在这种情况下再写一个类似的项目,好像没有太大必要。
我最初也没有准备做一套完整系统,只是想把自己常用的模型接到本地项目中。界面负责对话,后端转发模型请求,再提供几个文件和终端工具,理论上很快就能完成。
真正开始使用以后,问题很快从“模型能不能回答”变成了另外一些事情:切换页面后任务是否继续运行,终端进程由谁管理,工具执行了一半断线怎么办,长对话如何压缩,多台设备怎样看到相同状态,以及多个 Agent 同时修改工作区时如何避免互相覆盖。
这些问题单独看都不复杂,放在一起以后就不再是聊天界面了。于是项目逐渐变成了 OhMyCode,一个桌面优先的 Code Agent 工作空间。
项目目前处于 0.1.0 阶段,代码已经开源在 GitHub。桌面端、移动端、API 和生产部署都可以运行,不过距离我理解中的“放心交给它工作”还有不少事情要做。
B) 先把一次对话说清楚
最早的实现和普通聊天应用差不多:用户发送消息,服务端请求模型,再把文字流式返回页面。加入工具调用后,服务端需要等待本地执行结果,然后带着结果继续请求模型。
问题是,一次 Agent 任务可能包含很多阶段。模型先思考,再调用终端,接着读取文件,修改代码,最后才返回答案。只使用“用户消息”和“助手消息”已经很难准确表达中间状态。
OhMyCode 最后统一成了三个概念:
- Thread 表示一段可以持续恢复的会话。
- Turn 表示从一次用户请求到完成、失败或停止的完整执行。
- Item 表示 Turn 中的一项活动,例如思考、消息、命令或文件操作。
每个 Item 都经过 started、delta 和 completed,每条事件还有递增的 sequence。界面只需要按照事件顺序渲染,不再分别猜测“模型是不是还在思考”“命令有没有结束”。
上图是现在的工作区。左边管理项目和会话,中间展示完整执行时间线,右边保留持久终端。终端并不是一个装饰性的代码块,而是真实 PTY 进程,Agent 与用户可以围绕同一个项目继续工作。
C) 为什么 Runtime 放在 Electron Main
桌面端使用 Electron 和 React。最省事的方式,是直接在 Renderer 中管理请求和任务状态,但页面刷新、路由切换或者窗口关闭都会影响任务。一个持续几分钟的 Agent 不应该因为用户点进设置页就停止。
因此真正的 Runtime 放在 Electron Main 中常驻。React Renderer 只负责展示和交互,通过受限的 preload IPC 调用 Runtime。文件系统、终端和密钥不会直接暴露给页面。
Runtime 为每次 Turn 分配 ID,管理模型流、工具循环、终端所有权和停止流程。页面离开时只会取消订阅,不会取消任务;再次进入会话时,先订阅实时事件,再读取快照,并按照 sequence 去重和补齐离开期间的内容。
事件还会先写入本地 SQLite Journal,再发布给界面。如果 Electron 异常退出,重新启动后至少能够明确知道哪些 Turn 没有结束,并把它们恢复成中断状态,而不是一直显示一个不会结束的加载动画。
用户主动停止任务也比调用一次 abort() 麻烦。Runtime 需要同时取消模型流、关闭这次 Turn 创建的终端、通知服务端保存已有输出,并避免“停止”和“自然完成”同时写入两个最终状态。目前这些流程已经可以工作,但断线、重启和重复工具结果的端到端测试仍然是后续重点。
D) 工具不能只是几个函数
最初的工具直接写在执行逻辑里。文件读取、命令执行、任务计划各有一段判断,工具数量不多时没有问题,加入桌面端、移动端、Skills 和 MCP 后很快开始混乱。
现在工具被拆成独立插件,并统一注册到 Tool Registry。每个插件自己声明模型看到的 Schema、参数处理、执行方式和资源清理。桌面端可以注册终端、文件和图片工具,移动端只注册计划任务、同步 Skill、HTTP MCP 等安全能力,不会因为复用代码而意外获得本地文件权限。
当前 Run 使用的工具定义还会生成快照并保存。这样恢复一轮任务时,系统知道模型当时究竟看到了哪些能力,不需要根据历史消息反推 MCP 是否加载过。
大工具结果也不能简单截断。命令输出或者文件内容可能非常长,全部塞回上下文会迅速耗尽 Token,只保留开头和结尾又可能丢掉关键错误。OhMyCode 会保存完整结果,模型上下文中只放有限预览和 resultRef,需要时再通过读取和搜索工具取得精确片段。
这套设计比直接注册几个函数麻烦得多,但工具一旦涉及重试、恢复和多个运行环境,明确的边界比少写几行代码重要。
E) 长对话如何继续
Code Agent 的上下文增长很快。一次命令可能输出几千行,连续修改几个文件后,早期讨论很容易被挤出去。如果每轮都发送完整历史,成本和响应时间也会不断增加。
OhMyCode 会估算上下文长度,达到用户设置的阈值后生成 Context Checkpoint。最近两轮保留完整事件,更早的已完成 Turn 可以使用摘要。摘要带有明确的消息、Run 和事件游标,因此系统知道它覆盖到了哪里,不会把同一段内容重复加入上下文。
原始事件仍然保留,摘要只是给模型使用的压缩视图。超过一定体积的历史 Turn 会在后台交给 Celery 处理,而不是阻塞当前对话。
这里最容易犯的错误,是把摘要当成新的事实来源。如果摘要生成失败或者遗漏细节,原始记录必须仍然能够查询。另一个问题是工具结果,模型有时需要的恰好是几轮之前的一行错误,因此才有了前面提到的 resultRef 和结果检索工具。
F) Multi-Agent 不等于同时开几个聊天框
加入 Multi-Agent 时,我最先想到的是并行执行。后来发现,只要多个 Agent 能修改同一个工作区,并行就会立刻带来冲突:一个人在重构文件,另一个人基于旧内容继续修改,最后谁覆盖谁很难说清楚。
当前版本使用主持人调度的群聊模型。用户先创建一套可复用配置,为主持人和成员分别设置职责、模型和提示词。
任务开始后,主持人负责选择下一位成员、整理阶段结果并决定什么时候结束。每次只有一个 Agent 获得执行权,所有成员仍然复用同一套 Thread、Turn、工具和上下文机制。
串行调度看起来没有同时运行热闹,但更容易维护因果顺序,也能通过工作区写锁避免两个成员同时落盘。每次 Agent Run 都可以查看耗时、Token 用量、思考过程和工具活动。
Multi-Agent 最难的部分并不是多请求几个模型,而是明确每个成员看到了什么、谁可以写文件、用户插话后从哪里恢复,以及协作结束时如何把结果带回主会话。目前实现已经能完成可复用团队配置、主持人调度、用户回复后恢复和运行详情追踪,不过真正的并行隔离仍然需要更完整的工作树和合并策略。
G) 设置不只是保存一个 API Key
为了兼容不同模型,OhMyCode 使用 OpenAI-compatible Chat Completions 协议。用户可以配置模型地址、密钥、上下文长度和压缩阈值,也可以管理 MCP、Skills、后台任务和应用更新。
模型兼容并没有协议名字看起来那么统一。有些服务不接受 stream_options,有些把 reasoning 放在不同字段中,工具调用的结束方式也可能不一样。因此 Provider 层需要把网络错误、鉴权错误、流式结束和可重试失败区分开,而不是把所有异常都显示成“请求失败”。
密钥保存在 Electron Main 或服务端,Renderer 不会拿到明文。桌面端更新也会检查 GitHub Release 和校验文件,不过 Windows 代码签名、macOS Developer ID 与公证仍然没有完成。这也是 0.1.0 目前不适合被称为正式版的原因之一。
H) 项目结构
项目使用 pnpm workspace 管理多个客户端和共享包。
桌面端是 Electron、React、TypeScript 和 Vite。Electron Main 负责 Runtime、本地 SQLite、PTY 终端和文件工具,Renderer 通过 IPC 订阅事件。
服务端使用 Python 3.12、Flask 和 SQLAlchemy。PostgreSQL 保存用户、项目、会话、消息和运行记录;Redis 提供 Celery broker、结果传输和分布式锁;MinIO 保存头像与同步 Skill 等对象;pgvector 用于能力检索。
共享的 agent-runtime 包负责事件 Journal、模型流解析、Tool Loop 和 Turn 执行,tool-contracts 保存平台无关的工具协议,protocol 负责 Thread、Turn、Item 事件类型。移动端使用 Expo,复用协议和 Runtime 合约,但拥有独立且更严格的工具注册表。
生产环境通过 Docker Compose 启动 API、Worker、Beat、PostgreSQL、Redis、MinIO 和 Nginx。桌面应用不把这些服务偷偷打包成一个黑盒,开发和部署时可以分别检查每一层的问题。
I) 目前还缺什么
写到现在,我越来越觉得 Code Agent 最难的部分不是让模型调用工具,而是处理工具调用前后那些不理想的情况。
高风险文件修改目前还缺少完整的 Diff 审批流程;长时间运行的模型任务仍需要更可靠的租约、幂等恢复和取消所有权;数据库、对象存储和本地 Journal 也需要正式的备份与回滚方案。发布环节还要补上代码签名、依赖扫描和迁移测试。
测试方面,现在有服务层测试和部分 Runtime、文件工具的 smoke test,但从登录、流式回复、工具执行到重启恢复的完整链路还不够。系统正常运行时看起来都差不多,真正能区分它们的,往往是网络断开、应用退出或者模型返回一半格式错误的时候。
这些内容不适合藏在“后续优化”四个字里,所以仓库单独维护了一份 issues 清单。完成一项就移走一项,至少能够明确当前版本可以信任到什么程度。
J) 最后
做 OhMyCode 之前,我以为主要工作会是设计 Prompt 和聊天界面。实际写下来,时间更多花在事件顺序、进程边界、工具生命周期、上下文预算和异常恢复上。模型当然重要,但它只负责系统中最不确定的一段。
一个演示可以假设网络稳定、命令成功、用户不会切换页面。真正每天使用的工具不能一直依赖这些假设。它需要知道任务属于谁,失败以后从哪里继续,停止时应该清理什么,以及重新打开应用后哪些状态仍然可信。
OhMyCode 还远没有完成。0.1.0 更像是把骨架搭起来:桌面端能够使用本地工具持续执行任务,移动端可以安全接入部分能力,Multi-Agent 共用同一套 Runtime,长结果和长对话也有了继续扩展的基础。
接下来比增加更多按钮更重要的,是继续补齐审批、恢复、测试和发布安全。毕竟 Code Agent 的价值不在于它偶尔能写出一段漂亮代码,而在于把一个真实项目交给它以后,过程仍然可见,结果仍然可追踪,出了问题也知道该从哪里找。