7大开源Agent源码对比解读------架构对比
本文是「7大开源Agent源码对比解读」系列第 1 篇。评估对象:codex、gemini-cli、qwen-code、opencode、kimi-code、deepseek-harness(下称 dsh)、oh-my-pi(下称 omp)。评估基于各项目源码逐文件取证,关键论断附
文件:行号引证。总体结论与评估方法见系列总述。
| 篇 | 文章 | 内容速览 |
|---|---|---|
| 总述 | 拆了七大开源 Agent 的源码,最高分竟然不是 Codex | 总体结论、评估方法与评分卡 |
| 01 | 架构对比 | 四种流派与分化根源、主循环与事件机制、插件化与耦合风险 |
| 02 | 上下文管理 | 压缩触发阈值、摘要方式、token 计数口径、跨会话记忆 |
| 03 | 会话管理 | 持久化模型三层次、并发控制、崩溃恢复与 resume / fork |
| 04 | 工具调用 | 注册与可见性、并行调度四种语义、审批门控、错误处理 |
| 05 | 重连与容错 | 重试预算、流中断处理、降级链、副作用安全 |
| 06 | 系统提示词与指令遵循 | 四种组装范式、动态注入、注入防御三层与共同敞口 |
| 07 | 思维链与工作流编排 | 思维链接入、plan 模式语义、子代理治理、工作流引擎 |
| 08 | 性能设计 | 前缀缓存三档分化、启动优化、成本核算 |
| 09 | 可扩展性 | MCP 接入、自定义工具、多 provider、SDK 与 API |
| 10 | 安全与权限控制 | 沙箱两种语义、审批默认姿态、企业管控、敏感数据保护 |
七个项目的主循环长得都一样:采样、批量执行工具、结果回灌,循环到模型认为任务结束。但主循环之下的架构已经分化成四个流派。回头看,分化由三个初始约束决定。
四种流派
| 流派 | 项目 | 画像 |
|---|---|---|
| 原生内核派 | codex、omp | 把热路径沉到 Rust:循环、沙箱、shell、tokenizer 编进进程 |
| 事件溯源平台派 | dsh、opencode、kimi-code(v2) | 会话即事件日志,一切状态可重放、可审计、可投影成 UI 和存储 |
| 产品生态派 | qwen-code、kimi-code | 围绕自家模型建全端矩阵:daemon、IM、desktop、CUA、IDE、SDK |
| 标准工程派 | gemini-cli | Google 式完备:策略引擎、OTel、evals、perf 基线,但深度绑定 Gemini |
分化的三个根源
语言与运行时决定能做什么。 codex 和 omp 选了 Rust 原生内核,沙箱、tokenizer、shell 能编进进程,工具执行零 fork;其余五家用 TypeScript,工具执行必经子进程,换来的是更快的迭代和更宽的生态。
「谁是事实源」决定持久化层级。 把模型可见内容做成 append-only 事件日志的项目(dsh、opencode、kimi v2),fork、resume、审计、回放都能从同一份日志派生;快照式(gemini-cli)和转录式(codex、qwen、omp)的 resume 语义就受限。这是从「个人 CLI」走向「可审计平台」的分水岭。
宿主数量决定要不要拆分内核。 只跑一个 TUI 的项目(codex、gemini-cli、omp)可以把循环、状态、UI 揉在一个进程里;要同时服务 TUI、Web、IDE、ACP 的项目(kimi-code、opencode、dsh)就必须把内核与宿主切开,于是出现 DI 容器、作用域生命周期、SSE 事件投影这一整套机制。
主循环与事件机制
| 项目 | 主循环位置 | 事件机制 |
|---|---|---|
| codex | run_turn(session/turn.rs:153) |
ResponseEvent 流 + EventMsg 推送,两套独立 |
| gemini-cli | while 循环(legacy-agent-session.ts:183) | GeminiEventType 事件流 + MessageBus 审批总线 |
| qwen-code | AgentCore(agent-core.ts:895) | ServerGeminiStreamEvent 扩展事件族 |
| opencode | runLoop(session/prompt.ts:1081) |
durable 事件溯源(SQLite 事件表)+ SSE |
| kimi-code | v1 runTurn / v2 AgentLoopService |
wire.jsonl 事件流 + 契约 manifest |
| dsh | ReactLoopAgent(agent-loop/src/agent.ts:64) |
三类事件(会话/活事件/能力)+ waterfall |
| omp | agentLoop(agent-loop.ts,2935 行) |
异步生成器事件流 + 约 60 个宿主钩子 |
插件化与耦合风险
| 项目 | 插件化程度 | 耦合风险点 |
|---|---|---|
| codex | 高(hooks 11 事件、plugins、skills、MCP) | session/mod.rs 4273 行,God Object 苗头 |
| gemini-cli | 中(extensions + MCP) | Config 4199 行服务定位器,新旧双轨并存 |
| qwen-code | 高(extensions、hooks、skills、channels) | scheduler 单文件 6496 行,Gemini 命名漂移 |
| opencode | 高(npm/本地插件 + 15 钩子) | V1/V2 双栈迁移中期 |
| kimi-code | 中(plugins、skills、hooks、MCP) | v1/v2 双引擎双份维护 |
| dsh | 极高(模型可在运行时定义插件) | 概念词汇表庞大(seam/profile/bundle) |
| omp | 高(extensions、skills、marketplace) | coding-agent 单包 84.6k 行,内核与产品同包 |
逐家看点
codex:一个二进制,四种人格
codex 编译出一个可执行文件,靠两层分发切换人格。第一层看 argv[0]:二进制以 apply_patch、codex-linux-sandbox 等名字被调用时直接跑对应子工具入口(arg0/src/lib.rs:60)。第二层是子命令分发:默认进 TUI,exec 走非交互执行,mcp-server 把自己暴露成 MCP 服务端,app-server 给 IDE 和 SDK 嵌入(cli/src/main.rs:1048)。四种人格共享同一份 core 与配置栈。
#mermaid-svg-wjh1IWO13ZAeUkuQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wjh1IWO13ZAeUkuQ .error-icon{fill:#552222;}#mermaid-svg-wjh1IWO13ZAeUkuQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wjh1IWO13ZAeUkuQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .marker.cross{stroke:#333333;}#mermaid-svg-wjh1IWO13ZAeUkuQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wjh1IWO13ZAeUkuQ p{margin:0;}#mermaid-svg-wjh1IWO13ZAeUkuQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster-label text{fill:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster-label span{color:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster-label span p{background-color:transparent;}#mermaid-svg-wjh1IWO13ZAeUkuQ .label text,#mermaid-svg-wjh1IWO13ZAeUkuQ span{fill:#333;color:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .node rect,#mermaid-svg-wjh1IWO13ZAeUkuQ .node circle,#mermaid-svg-wjh1IWO13ZAeUkuQ .node ellipse,#mermaid-svg-wjh1IWO13ZAeUkuQ .node polygon,#mermaid-svg-wjh1IWO13ZAeUkuQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .rough-node .label text,#mermaid-svg-wjh1IWO13ZAeUkuQ .node .label text,#mermaid-svg-wjh1IWO13ZAeUkuQ .image-shape .label,#mermaid-svg-wjh1IWO13ZAeUkuQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-wjh1IWO13ZAeUkuQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .rough-node .label,#mermaid-svg-wjh1IWO13ZAeUkuQ .node .label,#mermaid-svg-wjh1IWO13ZAeUkuQ .image-shape .label,#mermaid-svg-wjh1IWO13ZAeUkuQ .icon-shape .label{text-align:center;}#mermaid-svg-wjh1IWO13ZAeUkuQ .node.clickable{cursor:pointer;}#mermaid-svg-wjh1IWO13ZAeUkuQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .arrowheadPath{fill:#333333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wjh1IWO13ZAeUkuQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wjh1IWO13ZAeUkuQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wjh1IWO13ZAeUkuQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster text{fill:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ .cluster span{color:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wjh1IWO13ZAeUkuQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wjh1IWO13ZAeUkuQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-wjh1IWO13ZAeUkuQ .icon-shape,#mermaid-svg-wjh1IWO13ZAeUkuQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wjh1IWO13ZAeUkuQ .icon-shape p,#mermaid-svg-wjh1IWO13ZAeUkuQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wjh1IWO13ZAeUkuQ .icon-shape .label rect,#mermaid-svg-wjh1IWO13ZAeUkuQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wjh1IWO13ZAeUkuQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wjh1IWO13ZAeUkuQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wjh1IWO13ZAeUkuQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 以 apply_patch / codex-linux-sandbox
等名字被调用
以主程序名调用
默认
exec
mcp-server
app-server
codex 单一可执行文件
第一层分发:看 argv0
(arg0/src/lib.rs:60)
直接跑对应子工具入口
第二层分发:子命令
(cli/src/main.rs:1048)
TUI 交互
非交互执行
暴露为 MCP 服务端
给 IDE 和 SDK 嵌入
共享同一份 core 与配置栈
事件分两层,边界很清楚:ResponseEvent 是模型侧的流式协议事件(「模型在说什么」),EventMsg 是会话侧的语义事件(「会话在发生什么」),各自走独立通道。
风险点在 core/src/session/mod.rs,4273 行,一个 Session 结构体揽下会话状态、并发控制、事件分发、上下文管理、MCP 装配全部职责。改任何一处都碰到同一个巨型对象。
gemini-cli:教科书循环,新旧双轨
core/cli 双层,逻辑不依赖 UI,是七个项目里最干净的内核/外壳划分。主循环是教科书式 ReAct while 循环(legacy-agent-session.ts:183),流事件用 for await 消费,工具调用批量交给 Scheduler。
问题在双轨。旧循环 legacy-agent-session 是当前生效的主循环;新的 agent-session 和一套 8 处理器的 ContextManager 管线已经写好,但没有接线进循环。另外 4199 行的 Config 类充当服务定位器,谁要什么服务都从它取,是测试和维护的双重难点。
qwen-code:fork 演进的活样本
qwen-code 是 gemini-cli 的 fork,目录结构几乎照搬,在骨架上加了三样东西:把单一生成器拆成四套 ContentGenerator(Anthropic/Gemini/OpenAI/日志),加 daemon 支持后台常驻和多渠道接入,扩展了事件族(GoalState、ModelFallback 等 20 个成员,服务 serve 场景)。
fork 演进留下两处痕迹。真正的 ReAct 循环已从 GeminiClient 搬到 agent-core.ts:895,但核心类名仍叫 GeminiClient、GeminiChat,事件叫 ServerGeminiStreamEvent,产品是 qwen-code,代码里到处是 Gemini 前缀。工具调度器 coreToolScheduler.ts 涨到 6496 行(测试文件另有 18901 行)。这两处都是可维护性负债。
opencode:client/server 切得最干净
packages/server 是服务端(HTTP API + SSE),TUI、Web、Desktop 全是消费同一服务端的客户端。这是七者中最明确的 client/server 架构。
事件溯源也是七者中最完整的一例。模型可见的每一次变更先写成 append-only 事件,session_message 表带 seq 字段并在 (session_id, seq) 上建唯一索引;projector.ts 用 30 多个 events.project() 调用把事件 fold 进 message/part/session 行;事件类型用 Event.define 声明了 32 种;服务端再把事件以 SSE 推给客户端。UI、恢复、遥测全部从同一份日志派生。
代价是迁移期的双栈:代码里 SessionV1、MessageV2、EventV2 并存,prompt.ts 同时操作新旧两套模型,core 与 opencode 两个包各有一份 session/prompt.ts。
kimi-code:一核心多宿主
agent 内核做成与宿主无关的核心,再为 TUI、headless、ACP、Web、VSCode 各写适配。支撑多宿主并发的是 v2 引擎自研的 DI + Scope 机制:服务按「会话/宿主」作用域创建与回收,不同宿主拿各自的作用域实例,互不串扰(_base/di/scope.ts、loopService.ts:109)。
事件持久化走 wire.jsonl,每次模型请求都以 JSONL 追加写入,可以从 wire 日志重建任意一次请求。配套的 wire-manifest.d.ts 是自动生成的协议清单,把所有可持久化记录类型固化为机器可读契约,任意一方改动就能知道是否破坏契约。
负担是 v1、v2 两套完整引擎并行,runTurn 与 AgentLoopService 各管一套循环,两份都要维护,还要保证行为一致。
dsh:一切皆插件,包括模型自己
dsh 把整个运行时建在 vendor 进仓库的 Cordis 插件框架上,packages/ 下几乎所有包都是 Cordis 插件。启动拼装走三层组合:profile 声明用哪些 bundle,每个 bundle 贡献一组 patch,最后叠加用户自己的 patch 层(boot/app-boot/src/profile.ts:2-21)。
最激进的一点:模型本身被暴露了一组 cordis 工具,cordis_define 能在运行中动态定义、安装新插件(extensions/tool-cordis/src/index.ts:148)。运行时自修改到这个粒度,七者中独一份。
代价是认知门槛。seam、profile、bundle、patch、能力系列、活事件、会话事件、waterfall,新贡献者要先学这套词汇表才能动代码。
omp:三层结构,内核很厚
omp 分三层:packages/agent 是通用内核(对产品层零依赖),packages/coding-agent 是编码 Agent 产品层,packages/natives + Rust crate 是原生底座,把 ripgrep、brush bash、coreutils、tiktoken 编进进程,TS 经 N-API 调用。
主循环 agent-loop.ts 单文件 2935 行,塞了循环、工具调度、事件分发、媒体降级、上下文投影,每步用协作式 yieldIfDue() 让出执行权。宿主接入面很宽:AgentLoopConfig 加继承链合计约 60 个可配置点。
边界没划干净的地方:大量本应下沉到内核的会话编排逻辑在 coding-agent 包里,agent-session.ts 9659 行。产品包扛着内核级的体量,是后续拆分的主要债务。
给 Agent 开发者的借鉴清单
- 事件溯源值得作为自研架构的默认选择。 dsh 把「模型可见的内容必须可从日志重建」写成运行时不变量,opencode 的 projector 同思路但粒度到 message/part。fork、resume、审计、UI 回放、遥测全部是同一份日志的不同投影。
- 多宿主场景早做内核/宿主分离。 等第二个宿主出现再拆,就要补 DI、作用域、事件投影一整套(kimi-code 的 v2 就是重做一遍)。
- 事件分两层。 模型侧流事件和会话语义事件分开(codex),前端消费语义事件,不直接解析模型流。
- 工具可见性治理要进架构。 工具集一大,全量塞给模型会污染 prompt,deferred 注册、工具搜索这些机制要提前留位置。
- 警惕单文件膨胀。 codex 的 session/mod.rs、gemini-cli 的 Config、qwen 的 scheduler、omp 的 agent-session,四个项目四个 God Object 样本,都是「改一处碰全身」的起点。
- 双栈迁移要有退役时间表。 gemini-cli 的新管线写好但没接线,opencode、kimi 的新旧模型并存,迁移悬而不决本身就是耦合源。
- fork 要还命名债。 qwen-code 的 Gemini 前缀提醒:fork 之后第一批该改的就是核心类名。
文档地图
| 篇 | 文章 | 内容速览 |
|---|---|---|
| 总述 | 拆了七大开源 Agent 的源码,最高分竟然不是 Codex | 总体结论、评估方法与评分卡 |
| 01 | 架构对比 | 四种流派与分化根源、主循环与事件机制、插件化与耦合风险 |
| 02 | 上下文管理 | 压缩触发阈值、摘要方式、token 计数口径、跨会话记忆 |
| 03 | 会话管理 | 持久化模型三层次、并发控制、崩溃恢复与 resume / fork |
| 04 | 工具调用 | 注册与可见性、并行调度四种语义、审批门控、错误处理 |
| 05 | 重连与容错 | 重试预算、流中断处理、降级链、副作用安全 |
| 06 | 系统提示词与指令遵循 | 四种组装范式、动态注入、注入防御三层与共同敞口 |
| 07 | 思维链与工作流编排 | 思维链接入、plan 模式语义、子代理治理、工作流引擎 |
| 08 | 性能设计 | 前缀缓存三档分化、启动优化、成本核算 |
| 09 | 可扩展性 | MCP 接入、自定义工具、多 provider、SDK 与 API |
| 10 | 安全与权限控制 | 沙箱两种语义、审批默认姿态、企业管控、敏感数据保护 |