[Bug]: 在EKKO里使用claude+DeepSeek会导致缓存命中极低。
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" 消息:
{"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_agent、agent=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):
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];
}DHn 由 KZe 调用(return {model, messages: DHn(r), ...}),也就是 /api/claude-code-proxy/:key/v1/messages 流式路径(AQn)和非流式路径(CQn)构建 chat_completions 请求体的地方。上面的函数名与位置取自打包产物,对应的源码应该在 Coding 家族的 server 模块里。
问题出在两件事叠加:
- Claude Code 每轮在尾部追加一条会变化的
system消息(<total_tokens>提醒,另外还有一个# Environmentsystem 块)。尾部追加本来对前缀缓存完全无害。 - 代理把这些 system 消息提到头部,于是那条每轮变化的提醒跑到整段对话前面。对前缀缓存后端来说,头部任何一处字节变化,后面的内容全部失效。
对 Anthropic 官方 API 来说顺序无关紧要(Claude Code 用的是显式 cache_control 断点,最后一个断点之后的内容本来就不缓存),只有自动前缀缓存的 provider 会被打中——恰好就是 nw() 已经单独识别的那几家:
function nw(t){let e=`${t.provider} ${t.model} ${t.baseUrl}`.toLowerCase();
return ["deepseek","moonshot","kimi","mimo","xiaomimimo"].some(n=>e.includes(n));}验证方式
Claude Code 的会话记录(
~/.claude/projects/.../*.jsonl,版本 2.1.272)里,该会话有 123 条total_tokens_reminder附件,即每轮一条,数值递减(15,000,000 → 14,996,421 → …)。我把 Claude Code 指向本地一个只记录请求体的 mock 端点,抓到了真实的出站
/v1/messages请求体(参数与 Studio 启动时保持一致):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"消息。用代理自己的
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=true、CLAUDE_CODE_AUTO_COMPACT_WINDOW=1024000、CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=50) - Target:
provider=deepseek、apiMode=chat_completions、baseUrl=https://api.deepseek.com、modeldeepseek-flash - 路由:
POST /api/claude-code-proxy/:key/v1/messages - 数据来源:
~/.hermes-web-ui/hermes-web-ui.db→session_usage,每次调用的prompt_cache_hit_tokens/prompt_cache_miss_tokens来自上游 usage 事件
Steps to Reproduce
只需要使用Claude Code + DeepSeek
Expected Behavior
缓存命中正常
Actual Behavior
缓存命中不正常
Logs / Error Messages
Environment
Windows
Node Version
No response
Additional Context
No response
Source: EKKOLearnAI/hermes-studio