7大开源Agent源码对比解读——性能设计

本文是「7大开源Agent源码对比解读」系列第 8 篇。评估对象: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安全与权限控制沙箱两种语义、审批默认姿态、企业管控、敏感数据保护

性能设计的核心工程问题是:让长会话的每一轮都命中前缀缓存。缓存读价通常是标准价的 10%,长会话里这笔钱很可观。七个项目在这个问题上分成三档。

三档分化

第一档:把缓存断点放置当一等工程问题。 opencode 在协议无关层写统一的缓存策略,omp 把「稳前缀」做进数据结构,专门设计了 append-only 上下文和 fork 缓存身份继承。

第二档:吃 provider 原生缓存但做会话亲和。 codex、kimi、qwen 用 prompt_cache_keymetadata.user_id 把同一会话的请求钉到同一缓存槽。

第三档:只读不写。 gemini-cli 依赖 Gemini 隐式缓存,只读响应里的 cachedContentTokenCount;dsh 连断点都不碰,只把 provider 返回的 cache 字段透传进账本。

缓存机制对比

项目缓存机制
codexprompt_cache_key 会话亲和 + WS 增量请求 + 粘性路由
gemini-cliGemini 隐式缓存,只读计量,不主动建缓存资源
qwen-codeOpenAI 前缀缓存标记 + Anthropic cache_control 自动放置
opencodeCachePolicy 默认 3 断点,仅 2 种协议注入 wire 标记
kimi-codeprompt_cache_key 三协议全注入 + fork 缓存探针
dsh不放断点,透传 provider 的 cache 用量字段
ompappend-only 稳前缀 + 1h TTL + fork 继承缓存身份

逐家看点

codex:增量请求与粘性路由

codex 优先走 Responses-over-WebSocket。同一 turn 内连续发多次请求时,客户端缓存上一轮的完整请求与响应,判断当前请求是否是上一请求的增量扩展:只有当 model、instructions、tools 等非 input 字段全部不变,且 input 是上一 input 的前缀扩展时,才只发新增的条目加 previous_response_id,不重发整段历史(client.rs:1232-1273)。直接降低出站字节量,也利于 provider 侧前缀命中。

粘性路由是客户端与服务端的契约:turn 启动时服务端在响应头返回 x-codex-turn-state token,此后该 turn 的所有请求(重试、增量、续写)原样回放这个 token,不同 turn 之间不能混用(client.rs:278-288)。效果是同一 turn 粘到同一后端节点,保连接复用和缓存局部性。

启动优化做成了可观测的独立句柄:会话启动时异步发一个 generate=false 的预热请求,提前建好 WebSocket、跑完认证、拿到 previous_response_id,首条用户消息到达时直接复用(session_startup_prewarm.rs:26-247)。超时、取消都有专门遥测指标。这是七家中唯一把连接预热做成一等公民的。

opencode:协议无关的缓存策略

CachePolicy 定义在协议无关层:auto 模式在最后一个工具定义、最后一个 system part、最新一条 user message 三处放断点(cache-policy.ts:18-97)。关键门控是只有 anthropic-messagesbedrock-converse 两种协议会真正注入 wire 标记;OpenAI 系、Gemini、Azure 等协议走 provider 原生隐式前缀缓存,整个策略 pass 直接跳过,注释的理由是「发标记无害但没意义」。断点策略只填补调用方留空的缺口,手动标记会被保留。

出站请求(auto 模式放三个断点)

最后一个工具定义

最后一个 system part

历史消息

最新一条 user 消息

断点 ①

断点 ②

断点 ③

仅 anthropic-messages / bedrock-converse 注入 wire 标记,
其余协议走隐式缓存、整个策略 pass 直接跳过

成本核算也下了功夫:模型定价从 models.dev 目录拉取,支持按上下文长度分档计价,超 200k token 有独立档价(含 cache_read/cache_write 分别计价),避免用单一单价外推(models-dev.ts:25-50)。

启动优化:Bun 单二进制,为每个平台额外构建不带 AVX2 的 baseline 变体兼容旧 CPU。

omp:稳前缀是数据结构问题

omp 的思路最彻底:缓存友好不靠请求时打标记,靠数据结构本身(append-only-context.ts:1-15)。

两个机制:StablePrefix 把 system prompt 加工具规格算一次后冻结,后续 turn 复用完全相同的字节序列,除非显式失效或指纹变化;AppendOnlyLog 里消息只增不改,历史 turn 永不重序列化。两者叠加,每轮缓存未命中的只有用户新消息那一段增量。

StablePrefix 冻结段
system prompt + 工具规格
字节序列完全相同

AppendOnlyLog 历史轮次
只追加,永不重序列化

本轮新消息
唯一未命中缓存的增量

syncMessages:需要归一化改写消息时,
只 trim 到最长稳定前缀,
避免整段缓存失效

syncMessages() 处理了一个隐蔽的坑:归一化消息进 log 时,找到最长字节稳定前缀,原地改写只 trim 到该前缀(:188-214)。这修复的是「单条消息被改写导致整段缓存失效」的问题。

fork 继承缓存身份:/fork 等路径把父会话的 providerPromptCacheKey 传给子会话,子会话首请求能命中父会话已建立的缓存前缀(session-entries.ts:45-52)。注意只有 exact-route full fork 才继承,分支型 fork 会冷未命中。

Anthropic 族支持 cache_control 的 5 分钟和 1 小时两档,按模型能力选择,同一 block 多个声明取最强保留(anthropic-messages-server.ts:281-295)。

kimi-code:全协议亲和与 fork 探针

会话 ID 作为缓存亲和键注入三家协议的请求体:Anthropic 映射到 metadata.user_id(注释说明这是 Anthropic 侧对应 prompt_cache_key 的等价物),OpenAI 和 Kimi 直接注入 generationKwargs.prompt_cache_key(provider-manager.ts:268-375)。

fork 探针是独有的遥测:fork 出的子代理首轮 usage 记录完成后,发一个 prompt_cache_probe 事件,携带输入、缓存读、缓存创建等字段,专门度量「父会话缓存身份在子会话是否仍命中」(cacheProbeService.ts:23-49)。想优化 fork 场景的缓存继承,先要有这样的度量。

其余三家

gemini-cli 不主动创建缓存资源,依赖 Gemini 隐式前缀缓存,客户端只读 cachedContentTokenCount 进遥测和 UI 度量。启动优化上它是七家中唯一带公开性能回归框架的:perf-tests/ 目录有基线文件和测试,冷启动 927.6ms 是提交进仓、有 CI 守护的回归阈值(baselines.json:5-6)。其余六家没有类似基线,性能数字不可横向引用。

qwen-code 对 OpenAI 族标记可复用的历史前缀,Anthropic 族自动放置 cache_control 断点(system 文本、最后一个工具、尾部 user 消息),支持 1 小时扩展档。另有 V8 字节码编译缓存加速冷启动。/stats 命令展示缓存命中率。

dsh 的策略是「provider 给什么记什么」:不放断点,只把返回的 cacheReadTokenscacheWriteTokens 透传进账本,并明确要求三类输入计数互斥(不含缓存的输入、缓存读、缓存写分开记,把缓存命中折叠进总数的 provider 要减出来,types.ts:127-141)。token-meter 逐节点计价,每条消息、每个内容块单独定价,保证上下文明细和计价服务用同一套启发式。

成本核算的对比

项目成本核算特色
codex缓存读、缓存写分别作为独立 span 属性上报,不合并
gemini-cli12 个 LLM 角色分开记账(主对话、子代理、压缩器、循环检测器等)
qwen-codetelemetry 聚合 + 会话级 token 限额
opencode分档计价 + 超 200k 独立档 + stats 命令
kimi-codeusage 区分 cache read / cache creation
dshtoken-meter 逐节点计价 + 会话统计投影
omp本地看板实时显示 tokens/s、cache rate、成本

gemini-cli 的分角色记账值得展开:LlmRole 枚举 12 个值,主对话、子代理之外,压缩器、摘要器、路由器、循环检测器、下一发言判断、编辑纠正等辅助调用各占一个角色,每个 token 事件带角色字段(llmRole.ts:7-20)。成本分析时能直接回答「钱是主推理花的还是辅助调用花的」。辅助 LLM 调用一多(循环检测、压缩、标题生成),没有分账就是笔糊涂账。

给 Agent 开发者的借鉴清单

  1. 稳前缀优先做进数据结构。 冻结前缀字节、历史只追加、消息改写只 trim 到稳定前缀(omp 三件套),比在请求时找位置打断点更根本。
  2. 会话亲和键全协议注入。 prompt_cache_keymetadata.user_id,让同一会话钉在同一缓存槽(kimi 的三协议映射表可以直接抄)。
  3. fork 要继承缓存身份,并度量它。 继承与否的命中率差异要有遥测(kimi 的 fork 探针)。
  4. 缓存断点策略放协议无关层。 只对支持显式标记的协议注入,其余走隐式缓存并跳过(opencode 的门控集合)。
  5. 缓存读写分开计量。 读是省钱,写是成本,合并了就看不清(codex、kimi、dsh 都分)。
  6. 辅助 LLM 调用分角色记账。 压缩、裁判、路由各算各的(gemini-cli 的 LlmRole)。
  7. 性能基线进仓库。 冷启动、长会话恢复这类指标做成回归测试,数字才可比较(gemini-cli 是唯一示范)。
  8. 连接预热做成可观测句柄。 带超时、取消、遥测,集中管理而非散在启动流程里(codex 的 prewarm handle)。

文档地图

文章内容速览
总述拆了七大开源 Agent 的源码,最高分竟然不是 Codex总体结论、评估方法与评分卡
01架构对比四种流派与分化根源、主循环与事件机制、插件化与耦合风险
02上下文管理压缩触发阈值、摘要方式、token 计数口径、跨会话记忆
03会话管理持久化模型三层次、并发控制、崩溃恢复与 resume / fork
04工具调用注册与可见性、并行调度四种语义、审批门控、错误处理
05重连与容错重试预算、流中断处理、降级链、副作用安全
06系统提示词与指令遵循四种组装范式、动态注入、注入防御三层与共同敞口
07思维链与工作流编排思维链接入、plan 模式语义、子代理治理、工作流引擎
08性能设计前缀缓存三档分化、启动优化、成本核算
09可扩展性MCP 接入、自定义工具、多 provider、SDK 与 API
10安全与权限控制沙箱两种语义、审批默认姿态、企业管控、敏感数据保护
Logo

葡萄城是专业的软件开发技术和低代码平台提供商,聚焦软件开发技术,以“赋能开发者”为使命,致力于通过表格控件、低代码和BI等各类软件开发工具和服务,一站式满足开发者需求,帮助企业提升开发效率并创新开发模式。

更多推荐