[Bug]: 在EKKO里使用claude+DeepSeek会导致缓存命中极低。

Author: DameyshellCreated Sep 15, 2026Updated Sep 15, 2026
Labelsbug

Ekko Studio Version

v0.7.21

Agent Runtime and Version (if applicable)

Claude v2.1.272

Bug Description

[bug] claude-code-proxy 把 system 消息提到 prompt 开头,导致 DeepSeek 前缀缓存基本失效(命中率 ~8%)

内容由Hermes自我排查后生成。

摘要

Coding Agent 用 Claude Code + DeepSeek(OpenAI 兼容 chat_completions 时,claude-code-proxy 在做 Anthropic→OpenAI 转换的过程中,会把 messages 里所有 role: "system" 的消息全部提到 prompt 最前面、拼成一条(DHn[{role:"system", content: <所有 system 消息拼接>}, ...其余消息])。

而 Claude Code 2.1.x 每一轮都会在对话尾部追加一条 role: "system" 消息:

json
{"type": "text", "text": "<total_tokens>14996421 tokens left</total_tokens>"}

两者叠加后,每次请求的开头就变成了「系统提示 + 环境块 + 历次 <total_tokens> 提醒」,而且每一轮都在变。DeepSeek 的上下文缓存是自动的、严格按前缀逐字节匹配的,所以只能复用变化点之前的那一段(系统提示 + 环境块,约 5K token),它后面整段对话在每次请求里都被当作未命中重新计费。命中率因此只有 8%,而同一模型、同一 key 走原生 agent 路径时是 95%+。

影响

一个小时的 Claude Code 会话(18:00–19:00,142 次请求,全部 source=coding_agentagent=claude_code):

项目 tokens
输入 · 未命中缓存 10,837,780
输入 · 命中缓存 959,488
输出 162,310
命中率 8.1%

对照(同一模型、同一 provider/key,几分钟后的原生 agent 路径):

时段 agent 请求数 命中率
16:00 hermes(原生) 30 96.7%
22:00 hermes(原生) 117 99.6%
18:00 claude_code 142 8.1%

逐次看受影响的那个会话(123 次请求,18:11–18:29):prompt 从约 13.6K token 一路涨到约 146K,而 prompt_cache_hit_tokens 始终钉在 5,120–6,656 —— 典型的「只有固定头部能复用」特征。

实际代价:那一小时约 10.3M token 按全价输入计费,本来大部分应该走便宜约 10 倍的缓存命中价。

根因

resources/webui/dist/server/index.js(Studio 0.7.21):

javascript
function DHn(t){let e=[],n=[];
  for(let a of t) a?.role==="system" ? e.push(a) : n.push(a);
  if(e.length===0) return t;
  if(e.length===1){ let a=e[0]; return t[0]===a ? t : [a,...n]; }
  return [{role:"system", content:e.map(a=>typeof a.content==="string"?a.content:Pp(a.content)).filter(Boolean).join("\r\n\r\n")}, ...n];
}

DHnKZe 调用(return {model, messages: DHn(r), ...}),也就是 /api/claude-code-proxy/:key/v1/messages 流式路径(AQn)和非流式路径(CQn)构建 chat_completions 请求体的地方。上面的函数名与位置取自打包产物,对应的源码应该在 Coding 家族的 server 模块里。

问题出在两件事叠加:

  1. Claude Code 每轮在尾部追加一条会变化的 system 消息(<total_tokens> 提醒,另外还有一个 # Environment system 块)。尾部追加本来对前缀缓存完全无害。
  2. 代理把这些 system 消息提到头部,于是那条每轮变化的提醒跑到整段对话前面。对前缀缓存后端来说,头部任何一处字节变化,后面的内容全部失效。

对 Anthropic 官方 API 来说顺序无关紧要(Claude Code 用的是显式 cache_control 断点,最后一个断点之后的内容本来就不缓存),只有自动前缀缓存的 provider 会被打中——恰好就是 nw() 已经单独识别的那几家:

javascript
function nw(t){let e=`${t.provider} ${t.model} ${t.baseUrl}`.toLowerCase();
  return ["deepseek","moonshot","kimi","mimo","xiaomimimo"].some(n=>e.includes(n));}

验证方式

  1. Claude Code 的会话记录(~/.claude/projects/.../*.jsonl,版本 2.1.272)里,该会话有 123 条 total_tokens_reminder 附件,即每轮一条,数值递减(15,000,000 → 14,996,421 → …)。

  2. 我把 Claude Code 指向本地一个只记录请求体的 mock 端点,抓到了真实的出站 /v1/messages 请求体(参数与 Studio 启动时保持一致):

    bash
    ANTHROPIC_BASE_URL=http://127.0.0.1:8899 ANTHROPIC_API_KEY=test \
    ANTHROPIC_MODEL=deepseek-flash \
    claude -p "..." --settings <与 Studio 一致的 settings> \
      --setting-sources local --append-system-prompt-file <hermes-rules.md> \
      --dangerously-skip-permissions

    抓包结果确认了请求形态:system 数组逐字节稳定、tools 完全一致、messages 只做追加;唯一的变化就是每轮多出一条含 <total_tokens>… tokens left</total_tokens>role:"system" 消息。

  3. 用代理自己的 KZe/DHn 逻辑渲染两个相邻请求再做最长公共前缀对比:

    req2 → req3: hoisted system block 19016 → 19069 chars
       common prefix inside the hoisted block: 19016 chars
       stable tail of prefix:  ...<total_tokens>15000000 tokens left</total_tokens>
       first differing text:     "\r\n\r\n<total_tokens>14999888 tokens left</total_tokens>"

    整个 prompt 的最长公共前缀正好断在新增的那条提醒处;那段内容完全相同的对话(以及本轮追加的内容)全都落在断点之后。稳定头部约 19K 字符 ≈ 5K token,与线上观测到的 5,120 token 命中平台值吻合。

    说明:CLAUDE_CODE_MAX_CONTEXT_TOKENS、去掉 CLAUDE_CODE_AUTO_COMPACT_WINDOW / CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 覆盖我都试过,这条提醒照样会发,客户端侧没有现成开关,所以建议在代理侧修。

修复建议

针对自动前缀缓存的 provider(nw(t) === true),让 prompt 头部保持「只追加」:

  • 首选:不要再做上提。把 Claude Code 放在尾部的 system 消息改写成一条 user 角色的消息、放到消息列表末尾(等价于 Claude Code 本来想渲染的 <system-reminder>);如果 provider 允许在对话中间出现 system 消息,直接保持原位置也可以。
  • 另外(或同时):转发前丢掉 <total_tokens>…</total_tokens> 这条提醒——它只是 Anthropic 侧上下文管理的记账信息,对 DeepSeek/Kimi/Moonshot 没有意义。
  • nw()(或 apiMode === "chat_completions" + provider 前缀缓存标记)把行为限定住,让 Anthropic 形态的 target 保持现有顺序。

两个改动任选其一都能恢复缓存:去掉提醒后头部就是逐字节稳定的;不做上提时,那条变化的提醒落在尾部,只有最后几个 token 不命中。

临时规避

修复前,DeepSeek 目标建议改用 dsh coding agent 或原生 agent 路径(实测命中率:dsh 83.2%、原生 Hermes agent 95–99.6%、Claude Code 8.3%)。

环境

  • Ekko Studio / Hermes Web UI 0.7.21(Windows 11)
  • Claude Code 2.1.272(entrypoint sdk-cli;Studio 启动时带 ENABLE_TOOL_SEARCH=trueCLAUDE_CODE_AUTO_COMPACT_WINDOW=1024000CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50
  • Target:provider=deepseekapiMode=chat_completionsbaseUrl=https://api.deepseek.com、model deepseek-flash
  • 路由:POST /api/claude-code-proxy/:key/v1/messages
  • 数据来源:~/.hermes-web-ui/hermes-web-ui.dbsession_usage,每次调用的 prompt_cache_hit_tokens / prompt_cache_miss_tokens 来自上游 usage 事件

Steps to Reproduce

只需要使用Claude Code + DeepSeek

Expected Behavior

缓存命中正常

Actual Behavior

缓存命中不正常

Logs / Error Messages

bash

Environment

Windows

Node Version

No response

Additional Context

No response

Source: EKKOLearnAI/hermes-studio