Claude Code 每次发包给 DeepSeek,到底经历了什么?
一份对
api.deepseek.com/anthropic兼容层逐字段拆解的技术笔记,基于本地.claude.json与settings.json的真实配置还原。
一、起点:Claude Code 认为自己在对谁说话?
先看 ~/.claude/settings.json 里的三个关键环境变量:
"ANTHROPIC_BASE_URL": "https://api.deepseek.com/anthropic", "ANTHROPIC_AUTH_TOKEN": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "ANTHROPIC_MODEL": "DeepSeek-V4-pro[1M]"
这三行完成了一件事:Claude Code 以为自己连的是 Anthropic 官方服务器,实际上所有请求都被路由到了 DeepSeek 的 Anthropic 兼容端点。
从 Claude Code 的视角看,它只是在用标准的 Anthropic Messages API 跟一个叫 DeepSeek-V4-pro[1M] 的"Anthropic 模型"对话。它完全不知道自己被骗了——这正是兼容层设计的目标:让客户端零修改就能换个后端。
请求的物理路径是:
POST https://api.deepseek.com/anthropic/v1/messages Authorization: Bearer sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
二、请求被发出前:Claude Code 本地组装了什么
在用户按下回车、到真正发出 HTTP 请求之间,Claude Code 在本地把以下内容拼成了一个完整的 Anthropic Messages API 请求体:
| 组装内容 | 来源 |
|---|---|
| System Prompt(角色定义、工具列表、行为规则) | Claude Code 内置模板 + .claude/ 下所有 skills/agents/plugins 的指令合并 |
| 工具定义(Read / Write / Bash / Grep / Agent / Workflow …) | Claude Code 内置 |
| MCP 工具定义(mingpan / word / ppt / figma) | .mcp.json + .claude.json project 级 MCP 配置 |
| 记忆文件 | MEMORY.md 索引的 .claude/projects/.../memory/*.md |
| 对话历史 | 当前 session 完整 transcript |
| 当前用户输入 | 刚刚输入的文字 |
这个请求体完全遵循 Anthropic Messages API 规范,典型结构如下:
{
"model": "DeepSeek-V4-pro[1M]",
"system": [
{ "type": "text", "text": "You are an interactive agent...", "cache_control": { "type": "ephemeral" } },
{ "type": "text", "text": "…大量工具定义…" }
],
"messages": [
{ "role": "user", "content": [{ "type": "text", "text": "帮我改一下 main.py" }] },
{
"role": "assistant",
"content": [
{ "type": "text", "text": "让我先读取文件。" },
{ "type": "tool_use", "id": "toolu_001", "name": "Read", "input": { "file_path": "/path/to/main.py" } }
]
},
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_001", "content": "print('hello world')" }
]
}
],
"tools": [
{ "name": "Read", "description": "Read a file", "input_schema": { "type": "object", "properties": {…}, "required": ["file_path"] } }
],
"max_tokens": 16000,
"stop_sequences": ["\n\nHuman:", "\n\nAssistant:"],
"thinking": { "type": "enabled", "budget_tokens": 4000 }
}
请求发出去了。接下来就是 api.deepseek.com/anthropic 要做的事。
三、请求侧:Anthropic → DeepSeek 原生格式,逐字段转换
DeepSeek 自己的模型不是吃 Anthropic 协议的——它走的是内部原生协议,结构上更接近业界主流的 Chat API 风格(跟 OpenAI 的格式同属一个流派)。所以 api.deepseek.com/anthropic 的第一件事,就是把这套 Anthropic 请求翻译成 DeepSeek 内部协议能懂的东西。
关于下文中的"DeepSeek 协议":DeepSeek
/anthropic端点的内部实现没有开源——它可能直接适配到模型推理管线,也可能中间经过一层 OpenAI 兼容网关。下面用"内部协议 / 内部格式"来指代 Anthropic 请求被翻译到的目标格式,不假设中间是否绕了 OpenAI 格式这一跳。好在这种 Anthropic → 主流 Chat API 的结构性差异是固定的,不管目标格式具体是哪一套,转换的方向和逻辑都一致。
下面把每个字段的转换拆开看。
3.1 System Prompt:从顶层字段变成 messages[0]
Anthropic 的 System Prompt 是一个顶层数组,独立于对话消息:
"system": [
{ "type": "text", "text": "You are an interactive agent…", "cache_control": { "type": "ephemeral" } },
{ "type": "text", "text": "…几百行的工具定义…" }
]
兼容层最可能的处理(类比业内主流 Chat API 的结构推断):
system 数组里的所有 text 块拼接成一个字符串
│
├── 有 cache_control 标记的块 → cache_control 标记大概率被丢弃或内部映射(DeepSeek 没有原样实现此协议)
├── 普通文本块 → 拼入字符串
│
▼
插到 messages 数组的最前面:
{ "role": "system", "content": "You are an interactive agent…\n\n…几百行的工具定义…" }
关于 cache_control 的存疑:Anthropic 的 ephemeral cache 机制允许客户端标记"这段内容短期内不会变,帮我缓存"。DeepSeek 没有公开同样的接口,兼容层大概率会丢掉这些标记。但这不代表完全没有缓存——DeepSeek V4 可能内部有自己的 context caching 实现,兼容层也许会在内部把标记了 cache_control 的块用自己的方式处理。这些都不开源,外部无法验证。
.claude.json 里 lastTotalCacheReadInputTokens: ~550k 这个数字,大概率是兼容层在响应里回填的一个值——它需要让 Claude Code 认为 cache 在正常工作,避免 Claude Code 的缓存逻辑出现异常。但也不排除 DeepSeek 确实有内部缓存命中统计、通过兼容层如实上报的可能。
3.2 消息结构:从嵌套数组平摊成扁平的 role/content
这是最复杂的转换。Anthropic 的消息结构对 DeepSeek 内部协议来说是"反常规"的。以下转换方式基于 Anthropic 与主流 Chat API 的结构性差异做的合理推断——兼容层的实际实现可能有所不同,但结构转换的大方向是一致的。
普通文本消息
// Anthropic — content 永远是一个数组
{ "role": "user", "content": [{ "type": "text", "text": "Hello" }] }
// ↓ 兼容层转换 ↓
// DeepSeek — 单 block 直接降维成字符串
{ "role": "user", "content": "Hello" }
如果 content 里有多段文本(比如 Claude Code 在 user 消息里同时附带了上下文说明),就拼接:
// Anthropic
{ "role": "user", "content": [
{ "type": "text", "text": "下面是一段代码:" },
{ "type": "text", "text": "def foo(): pass" }
]}
// ↓↓
// DeepSeek
{ "role": "user", "content": "下面是一段代码:\ndef foo(): pass" }
工具调用(Assistant 说"我要用工具")
这是结构差异最大的地方。基于业内主流 Chat API 的格式推断,转换大致如下:
// Anthropic — tool_use 嵌在 content 数组里,和文本是兄弟关系
{
"role": "assistant",
"content": [
{ "type": "text", "text": "让我读一下这个文件" },
{ "type": "tool_use", "id": "toolu_001", "name": "Read", "input": { "file_path": "/tmp/test.py" } }
]
}
// ↓ 兼容层转换(推断) ↓
// DeepSeek 内部协议 — tool_calls 大概率是消息的独立字段
{
"role": "assistant",
"content": "让我读一下这个文件",
"tool_calls": [
{
"id": "toolu_001",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"file_path\": \"/tmp/test.py\"}" // ← JSON 对象 → JSON 字符串!
}
}
]
}
注意这里的一个关键转换:Anthropic 的 input 字段是一个 JSON 对象,而内部协议中的 arguments 是一个 JSON 字符串。兼容层必须在这里做 JSON.stringify(input)。反过来,在响应侧做 JSON.parse(arguments)。
这个转换虽然看起来简单,但有隐性风险:
-
如果
input里包含特殊字符(换行符、Unicode、emoji),stringify/parse 可能出问题 -
如果
input的值本身是一个复杂的嵌套对象,转成字符串再转回来,有可能引入微妙的格式差异(比如 key 的顺序、数字精度)
工具结果(User 说"这是工具返回的结果")
接下来这个角色映射是推断出来的——类比主流 Chat API 的格式,tool_result 最自然的对应就是独立的 tool 角色:
// Anthropic — tool_result 是一种特殊的 user 消息
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_001", "content": "print('hello')" }
]
}
// ↓ 兼容层转换(推断) ↓
// DeepSeek — 很可能映射到独立的 tool 角色(不是 user!)
{
"role": "tool",
"tool_call_id": "toolu_001",
"content": "print('hello')"
}
这个转换要求兼容层在遍历消息时跟踪语义变化:当看到 role: "user" + type: "tool_result",它不能只是简单映射字段名,而是要把整条消息的角色从 user 改成 tool。如果有多条连续的 tool_result 消息(Claude Code 并行调用多个工具时就会这样),每条都要独立映射,并且保持 tool_call_id 的对应关系不变。
有一个细微但重要的差异
Anthropic 协议允许一条 assistant 消息里同时包含文本和多个 tool_use:
{
"role": "assistant",
"content": [
{ "type": "text", "text": "我需要同时读取两个文件" },
{ "type": "tool_use", "id": "t1", "name": "Read", "input": {…} },
{ "type": "tool_use", "id": "t2", "name": "Read", "input": {…} }
]
}
这转换后就是一条带有多个 tool_calls 的 assistant 消息。反过来,也可能出现 Claude Code 分两条 assistant 消息各发一个 tool_use 的情况。兼容层需要保留这种"一条消息里几个 tool call"的对应关系,不能错误地把它们拆散或者错误合并。
图片/多模态内容
// Anthropic
{ "type": "image", "source": { "type": "base64", "media_type": "image/png", "data": "iVBORw0KGgo…" } }
// ↓ 兼容层转换(推断) ↓
// DeepSeek 内部协议 — 大概率拼成 data URL
{ "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo…" } }
3.3 工具定义:从 input_schema 到 parameters
// Anthropic
{
"name": "Read",
"description": "Reads a file from the local filesystem.",
"input_schema": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "The absolute path to the file" }
},
"required": ["file_path"]
}
}
// ↓ 兼容层转换 ↓
// DeepSeek 内部协议
{
"type": "function",
"function": {
"name": "Read",
"description": "Reads a file from the local filesystem.",
"parameters": {
"type": "object",
"properties": {
"file_path": { "type": "string", "description": "The absolute path to the file" }
},
"required": ["file_path"]
}
}
}
三个变化(基于主流 Chat API 格式推断):
-
外加一层
{ "type": "function", "function": {...} }包装 -
input_schema→parameters(仅仅改名) -
如果 Anthropic 的 tool 定义里有
cache_control,大概率被丢弃或内部映射
Claude Code 每次请求发的工具数量可能非常大——特别是有多个 MCP server 的时候。像上面截图的配置里挂了 mingpan、word、powerpoint、figma,工具总数轻松过百。这些定义每次都原封不动地塞在请求里,兼容层要对每条逐个做这种结构包装。不过兼容层也可能采用更聪明的策略——比如对工具定义做一次转换后缓存,只在工具列表变化时重新处理。具体实现方式取决于兼容层的设计。
3.4 Thinking / Extended Thinking:黑盒映射
Anthropic 的 extended thinking 是一个独立参数:
"thinking": { "type": "enabled", "budget_tokens": 4000 }
DeepSeek V4 Pro 确实有原生的推理/思维链能力,但它的 API 参数格式没有公开发布。兼容层在这里是厂商特定映射——外部无从得知这个 budget_tokens: 4000 具体被转化成了 DeepSeek 的什么内部参数。
在 settings.json 里配了 ANTHROPIC_REASONING_MODEL: "DeepSeek-V4-pro[1M]",这是告诉 Claude Code"这个模型支持 thinking"。如果这里配成一个不支持 thinking 的模型名,Claude Code 根本不会发 thinking 参数,兼容层也不用处理。
3.5 模型名解析
// Claude Code 发出 "model": "DeepSeek-V4-pro[1M]" // 兼容层解析 // "DeepSeek-V4-pro" → 模型本体 // "[1M]" → 开关标记:启用 1M 上下文窗口
[1M] 后辍是 DeepSeek 兼容层自己约定的标记语法,不是 Anthropic 协议的内容。兼容层把它解析成一个 flag,在调 DeepSeek 原生 API 时设置对应的上下文长度参数。
完整的模型配置:
"ANTHROPIC_DEFAULT_FABLE_MODEL": "DeepSeek-V4-pro[1M]", // ← 1M 上下文 "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME": "DeepSeek-V4-pro", "ANTHROPIC_DEFAULT_OPUS_MODEL": "DeepSeek-V4-pro[1M]", // ← 1M 上下文 "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "DeepSeek-V4-pro", "ANTHROPIC_DEFAULT_SONNET_MODEL": "DeepSeek-V4-pro[1M]", // ← 1M 上下文 "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME":"DeepSeek-V4-pro", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "DeepSeek-V4-pro", // ← 默认上下文 "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "DeepSeek-V4-pro",
Claude Code 内部有"模型路由"机制——它根据任务复杂度自动选模型:
-
简单的一次性问答 → 走 Haiku(上面配置:DeepSeek V4 Pro,无 1M)
-
中等复杂度任务 → 走 Sonnet(上面配置:DeepSeek V4 Pro,有 1M)
-
复杂多步骤任务 → 走 Opus 或 Fable(上面配置:DeepSeek V4 Pro,有 1M)
但是,到了兼容层,这些不同的模型名最重要也最可确认的区别是 [1M] 后缀带来的上下文窗口大小差异。至于 [1M] 版和无后缀版本是否指向同一模型的不同参数配置(如上下文长度、推理算力配额),还是完全同一套推理管线,外部无从确认。Claude Code 以为自己调了四个不同能力的模型,实际后端行为取决于 DeepSeek 的模型路由策略。
3.6 其他参数的映射(推断)
| Anthropic | DeepSeek | 说明(基于类比推断) |
|---|---|---|
max_tokens |
max_tokens |
大概率原值传递 |
temperature |
temperature |
大概率原值传递(两边的合法范围不同,兼容层可能负责 clamp) |
top_p |
top_p |
大概率原值传递 |
top_k |
— | 大概率丢弃或内部映射。DeepSeek 公开 API 无 top_k 对应字段 |
stop_sequences |
stop |
很可能仅改名,值不变 |
metadata.user_id |
可能映射到 user |
大概率丢弃;也可能被兼容层留作日志 |
top_k 的映射问题在绝大多数场景下没有实际影响——Anthropic 官方也建议不要同时使用 top_k 和 top_p,Claude Code 通常不设这个参数。但如果通过自定义 hook 注入了 top_k,它很可能在兼容层被丢弃。
四、聊天模板(Chat Template):从 JSON 到纯文本
第三节聊完了兼容层如何把 Anthropic JSON 转成内部协议 JSON。但内部协议 JSON 仍然不是模型最终消费的东西。在真正喂给推理引擎之前,还有一道关键工序——这一步往往被忽略,但它决定了模型到底"看"到了什么:
内部协议 JSON → Chat Template 渲染 → 纯文本字符串 → Tokenizer → token ID 序列 → 模型
4.1 模型收到的不是 JSON,是一串 token
不管兼容层产出的内部 JSON 长什么样,模型最终拿到的不是结构化数据,而是一个长整型数组:[1, 2675, 389, 337, ...]。这个数组里的每个数字是一个 token ID,指向模型的词表(vocabulary)中的某个元素。模型在这个数组上做自注意力运算,通过 token 之间的位置关系和 special token 的边界标记来"理解"上下文——它对 JSON 的结构、字段名、类型标记完全无感知。
4.2 什么是 Chat Template
Chat Template 是挂在 tokenizer 上的一个字符串模板(通常用 Jinja2 语法),它定义了:
-
system prompt 用什么标记包裹、放在什么位置
-
user / assistant / tool 消息分别用什么标记分隔
-
工具定义和工具调用怎么嵌入
-
是否需要在对话末尾追加一个"生成提示"(generation prompt)来触发 assistant 回复
不同模型的 Chat Template 完全不同。DeepSeek V4 的模板没有官方文档,但我们可以参考 DeepSeek V3 的开源 Chat Template 来推测方向。以下是一个推演示例(标记语法只是示意,V4 的实际标记一定不同):
假设兼容层产出的内部 JSON 是:
system: "You are an interactive agent with tools...\n\n## Tools\n..."
messages:
[0] { role: "system", content: "<拼好的 system>" }
[1] { role: "user", content: "帮我读取 main.py" }
[2] { role: "assistant", content: "让我来读取。", tool_calls: [{...Read...}] }
[3] { role: "tool", content: "print('hello world')", tool_call_id: "t1" }
Chat Template 把它渲染成一段纯文本:
<|begin▁of▁sentence|>
You are an interactive agent with tools...
## Tools
- Read: Reads a file from the local filesystem.
Parameters: {"file_path": {"type": "string", ...}}
- Bash: Runs a shell command.
Parameters: {"command": {"type": "string", ...}}
<tools_available>
以上工具可选调用,每次可调用 0~N 个。
</tools_available>
<solver>帮我读取 main.py</solver>
<assistant>让我来读取。</assistant>
<tool_call>
{"name": "Read", "arguments": {"file_path": "/tmp/main.py"}}
</tool_call>
<tool_response>
print('hello world')
</tool_response>
<assistant>
注意:上面用到的
<|begin▁of▁sentence|>、<solver>、<tool_call>、<tool_response>等标记,来自 DeepSeek V3 的开源 Chat Template,仅作示意。DeepSeek V4 Pro 是新一代架构,它的 special token 设计很可能完全不同于 V3——这里展示的是渲染的逻辑而非精确的标记语法。
4.3 Tokenizer:从文本到数字
渲染出的纯文本再经过 tokenizer 分词:
"<|begin▁of▁sentence|>" → [1]
"You" → [2675]
" are" → [389]
" an" → [337]
" interactive" → [12345, 678]
" agent" → [23456]
...
"Read" → [34567]
...
"<solver>" → [某 special token ID]
"帮我读取" → [45678, 12345]
" main" → [9876]
".py" → [8765]
...
"<assistant>" → [某 special token ID]
"让我来读取。" → [56789, 23456, 7890]
...
"<tool_call>" → [某 special token ID]
"{" → [123]
"\"name\"" → [456, 789]
": " → [234, 345]
"\"Read\"" → [567, 890]
...
"{" → [123]
"\"file_path\"" → [456, 789, 234]
": " → [234, 345]
"\"/tmp/main.py\"" → [567, 890, 123, 456]
...
"</tool_response>" → [某 special token ID]
"<assistant>" → [某 special token ID]
最终得到一个长整型序列:
[1, 2675, 389, 337, 12345, 678, 23456, ..., 34567, ..., 45678, 12345, 9876, 8765, ..., 56789, 23456, 7890, ..., 123, 456, 789, ..., 567, 890, 123, 456, ...]
这才是模型推理引擎真正接收的输入。 没有 role 字段,没有 content block 类型,没有 JSON 的花括号结构——只有一个 token ID 向量。
4.4 这对兼容层的转换质量意味着什么
这个流程揭示了一个重要的现实:在 Anthropic 协议层精心构造的结构,到了模型那里已经几乎不剩什么了。
-
Anthropic 的 content block 类型(
text、tool_use、tool_result、image)到了模型的 token 序列里,全部坍缩成了被 special token 包裹的文本段。模型区分它们只靠 special token 的 embedding 和上下文位置。 -
如果兼容层的 Chat Template 处理工具调用的 special token 和 Anthropic 原生模型的训练模板不同,模型对"工具调用""工具结果"这些概念的理解可能会有微妙偏差。
-
如果 tokenizer 对 JSON 特殊字符(
{}[]":,)的分词策略不同,同一个 tool use 的 arguments 在两个模型那里会产生完全不同的 token 序列。这直接影响模型对 JSON 结构的"直觉"——因为模型在训练时学到的 JSON 模式是和特定分词方式绑定的。
五、DeepSeek V4 Pro 推理
token ID 序列进入 Transformer 推理引擎。模型逐 token 生成输出——预测下一个最可能的 token,把它追加到序列末尾,然后基于新的序列再预测下一个,如此往复直到生成结束标记。
接着,推理引擎的输出经过反向流程:
token ID 序列 → Detokenizer → 纯文本 → Chat Template 反向解析 → 内部协议 JSON
然后才交给兼容层做 Anthropic 格式的反向翻译。
六、响应侧:DeepSeek 原生 → Anthropic 格式,逐字段反转
6.1 普通文本响应
// DeepSeek 返回(内部格式,结构接近业界主流 Chat API)
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "文件内容是 print('hello world')"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 5000,
"completion_tokens": 100,
"total_tokens": 5100
}
}
// ↓ 兼容层转换 ↓
// Anthropic 格式
{
"id": "msg_abc123",
"type": "message",
"role": "assistant",
"model": "DeepSeek-V4-pro[1M]", // ← 回填用户请求的模型名
"content": [
{ "type": "text", "text": "文件内容是 print('hello world')" }
],
"stop_reason": "end_turn", // ← stop → end_turn
"stop_sequence": null,
"usage": {
"input_tokens": 5000,
"output_tokens": 100
}
}
几个转换要点:
-
content必须是数组:哪怕只有一段纯文本,也要包在[{ "type": "text", "text": "..." }]里。这是 Anthropic 协议的硬性要求——text 必须作为 content block 存在。 -
model回填原始请求的模型名:不能用 DeepSeek 内部模型名(比如deepseek-v4-pro-internal),必须用 Claude Code 请求时传的那个名字DeepSeek-V4-pro[1M]。否则 Claude Code 的模型统计和模型路由会乱。 -
finish_reason映射:
| DeepSeek | Anthropic | 含义 |
|---|---|---|
"stop" |
"end_turn" |
模型自然终止 |
"tool_calls" |
"tool_use" |
模型要求调用工具 |
"length" |
"max_tokens" |
达到 max_tokens 上限被截断 |
6.2 Tool Call 响应
// DeepSeek 返回
{
"choices": [{
"message": {
"role": "assistant",
"content": "让我读取这个文件",
"tool_calls": [{
"id": "call_xyz",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"file_path\": \"/tmp/test.py\"}" // ← JSON 字符串
}
}]
},
"finish_reason": "tool_calls"
}]
}
// ↓ 兼容层转换 ↓
// Anthropic 格式
{
"role": "assistant",
"content": [
{ "type": "text", "text": "让我读取这个文件" },
{
"type": "tool_use",
"id": "call_xyz",
"name": "Read",
"input": { "file_path": "/tmp/test.py" } // ← JSON.parse(arguments)
}
],
"stop_reason": "tool_use"
}
这里完成了请求侧的反向操作:arguments(JSON 字符串)→ JSON.parse() → input(JSON 对象)。
一个容易被忽略的坑:DeepSeek 有时会在 arguments 里返回不符合 JSON 语法的内容。比如参数值里有未转义的换行符、或者模型输出了不完整的 JSON(因为 max_tokens 截断)。兼容层必须处理这些异常:
-
截断的 JSON → 尝试补全,补不全就丢弃这个 tool_call,或者交给客户端自己兜底
-
非法 JSON → 同样需要容错逻辑
-
空 arguments →
{}
6.3 Thinking / 推理内容
DeepSeek V4 Pro 在推理过程中会产生内部思考:
// DeepSeek 可能返回
{
"choices": [{
"message": {
"reasoning_content": "用户想改 main.py,我应该先用 Read 看一下文件内容,然后再判断改哪里…",
"content": "让我先读取文件。"
}
}]
}
// ↓ 兼容层转换 ↓
// Anthropic thinking 格式
{
"content": [
{ "type": "thinking", "thinking": "用户想改 main.py,我应该先用 Read 看一下文件内容…" },
{ "type": "text", "text": "让我先读取文件。" }
]
}
这个转换对 Claude Code 的用户体验有直接影响:Anthropic 协议的 thinking 块在 Claude Code 界面上是折叠或灰度展示的,相当于模型的"内心独白"。如果兼容层没有正确映射这个字段,推理内容就会暴露在正文里,或者直接丢失。
从配置里 ANTHROPIC_REASONING_MODEL 的设置来看,这种用法是期望使用 thinking 功能的。兼容层在这里的处理质量直接影响输出效果。
6.4 Token Usage 统计
// DeepSeek 原样返回
"usage": {
"prompt_tokens": 5000,
"completion_tokens": 100,
"total_tokens": 5100
}
// ↓ 兼容层转换 ↓
// Anthropic 格式(多了 cache 相关字段)
"usage": {
"input_tokens": 5000,
"output_tokens": 100,
"cache_read_input_tokens": ~550000, // ← 这个数字从哪来?
"cache_creation_input_tokens": 0
}
.claude.json 记录的上次请求 cache_read_input_tokens: ~550k 非常醒目。几个可能的解释(按可能性排序):
-
兼容层在 Anthropic 响应里回填了一个估算值或固定值,让 Claude Code 认为 cache 在正常工作——维持接口兼容性
-
DeepSeek V4 内部有 context caching 实现,兼容层维护了一个计数器追踪命中情况,如实上报
-
这个数字是其他内部指标(如 system prompt 的 token 数)映射过来的,并非真正的 cache 命中统计
没有开源代码,哪一种成立无法确认。
6.5 Streaming 响应(SSE 事件映射)
如果 Claude Code 用了 streaming(stream: true),兼容层还要做 Streaming SSE 事件的实时翻译:
DeepSeek SSE Event(内部格式) Anthropic SSE Event
─────────────────────────────────────────────────────────────────────
(首个 chunk) ──────────→ event: message_start
data: {"type":"message_start","message":{"id":"...","model":"...",...}}
data: {"choices":[{"delta": ──────────→ event: content_block_start
{"content":"Hello"}}]} data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
data: {"choices":[{"delta": ──────────→ event: content_block_start
{"tool_calls":[{"index":0, data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"...","name":"Read","input":{}}}
"id":"call_x",
"function":{"name":"Read"}}]}}]}
data: {"choices":[{"delta": ──────────→ event: content_block_delta
{"tool_calls":[{"index":0, data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file"}}
"function":{"arguments":"{\"file"}}]}}]}
data: {"choices":[{"delta":{}, ──────────→ event: content_block_delta
"finish_reason":"stop"}}] data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"_path\":\"/tmp/test.py\"}"}}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null}}
[DONE] ──────────→ event: message_stop
data: {"type":"message_stop"}
Streaming 转换的难点在于:
-
Tool call 参数是增量 JSON 片段。兼容层必须维护一个 StringBuilder,把零散的
partial_json拼成完整片段后逐块输出 -
事件顺序必须正确:必须先发
content_block_start再发content_block_delta,不能乱序 -
心跳:Anthropic 的 SSE 实现有 ping 心跳机制(具体间隔由服务端控制)。兼容层如果要完整模拟 Anthropic 服务器的行为,也需要模拟这个机制,否则 Claude Code 可能误判连接断开
七、特殊配置:全模型路由到一个后端
值得拿出来单说的一点是模型路由配置。Claude Code 内置了"模型智能路由"——它根据任务复杂度自动在 Haiku / Sonnet / Opus / Fable 之间选择。但 settings.json 把四个层级全部指到了 DeepSeek:
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "DeepSeek-V4-pro", // 无 1M "ANTHROPIC_DEFAULT_SONNET_MODEL": "DeepSeek-V4-pro[1M]", // 有 1M "ANTHROPIC_DEFAULT_OPUS_MODEL": "DeepSeek-V4-pro[1M]", // 有 1M "ANTHROPIC_DEFAULT_FABLE_MODEL": "DeepSeek-V4-pro[1M]", // 有 1M
这让 Claude Code 在决定"这个任务很复杂,我得用 Opus"时,实际发的还是同一个 DeepSeek V4 Pro。这是一个合理的做法——如果只有一个 API 后端,没必要让 Claude Code 因为模型不匹配而拒绝某些类型的请求。
唯一有意义的区分是 Haiku 走了无 [1M] 版本。这也许意味着"简单任务不需要 1M 上下文,省一点"——但从 DeepSeek 的计费方式看,这个区分可能更多是为了让 Claude Code 内部的路由逻辑不报错。
八、完整链路时序图
用户输入文字并回车
│
▼
┌─ Claude Code 客户端 ──────────────────────────────────────────┐
│ │
│ 1. 读取 MEMORY.md + 关联记忆文件 │
│ 2. 合并 system prompt (内置 + skills + CLAUDE.md) │
│ 3. 收集 tools 定义 (内置 + MCP servers) │
│ 4. 组装完整的 Anthropic Messages API 请求体 │
│ │
└──┬─────────────────────────────────────────────────────────────┘
│ HTTPS POST to https://api.deepseek.com/anthropic/v1/messages
│ Authorization: Bearer sk-... (DeepSeek API Key)
▼
┌─ api.deepseek.com/anthropic ───────────────────────────────────┐
│ │
│ ★ 请求侧:Anthropic → 内部协议 ★ │
│ │
│ □ system 数组 → 拼接字符串, cache_control 大概率丢弃或内部映射, 插到 messages 最前 │
│ □ content[{text}] → content: "string" (去数组化,推断) │
│ □ content[{tool_use}] → tool_calls[], input(对象)→arguments(JSON字符串,推断) │
│ □ content[{tool_result}] → role 从 user 改为 tool(推断) │
│ □ content[{image}] → type:image_url, base64 → data URL(推断) │
│ □ tools[].input_schema → tools[].function.parameters(推断) │
│ □ thinking → DeepSeek 内部 reasoning 参数(黑盒) │
│ □ model="xxx[1M]" → 解析 [1M] flag, 映射到内部模型名 │
│ □ stop_sequences → stop(推断), top_k → 大概率丢弃或内部映射 │
│ □ metadata.user_id → 大概率丢弃或内部映射 │
│ │
│ 输出:内部协议 JSON │
│ │
└──┬─────────────────────────────────────────────────────────────┘
│
▼
┌─ DeepSeek 推理管线 ───────────────────────────────────────────┐
│ │
│ ★ Chat Template 渲染(JSON → 纯文本)★ │
│ □ 按模型模板将 role/tool_calls/arguments 用 special token 包裹│
│ □ system prompt 和工具定义嵌入指定位置 │
│ □ 末尾追加 generation prompt 标记触发 assistant 回复 │
│ │
│ 输出:纯文本字符串 │
│ "<|begin▁of▁sentence|>You are...<solver>帮我读取...<tool_call>..."│
│ │ │
│ ▼ │
│ ★ Tokenizer(纯文本 → token ID 序列)★ │
│ □ 每个词/标记映射到词表中的 token ID │
│ □ special token 有独立 ID(如 <solver> → ID 某值) │
│ │
│ 输出:长整型数组 │
│ [1, 2675, 389, 337, 12345, ...] │
│ │ │
│ ▼ │
│ ★ Transformer 推理引擎 ★ │
│ □ 自注意力在 token 序列上运算 │
│ □ 逐 token 预测下一个最可能的 token │
│ □ 直到生成结束标记(<eos> 或 stop_reason 触发) │
│ │
│ 输出:token ID 序列 │
│ │ │
│ ▼ │
│ ★ Detokenizer + 反向解析 ★ │
│ □ token ID → 文本 → 内部协议 JSON(Chat Template 反向) │
│ │
│ 输出:内部协议 JSON │
│ │
└──┬─────────────────────────────────────────────────────────────┘
│
▼
┌─ api.deepseek.com/anthropic ───────────────────────────────────┐
│ │
│ ★ 响应侧:内部协议 → Anthropic ★ │
│ │
│ □ choices[].message.content → content[{type:"text", text:...}](推断) │
│ □ choices[].message.tool_calls[] → content[{type:"tool_use", ...}](推断) │
│ □ tool_calls[].function.arguments → JSON.parse() → input(推断) │
│ □ finish_reason: stop→end_turn, tool_calls→tool_use, length→max_tokens(推断)│
│ □ reasoning_content → content[{type:"thinking", thinking:...}](黑盒) │
│ □ model → 回填请求时的 "DeepSeek-V4-pro[1M]" │
│ □ usage → input_tokens/output_tokens + 回填或估算 cache_read_input_tokens │
│ □ 如果是 stream: DeepSeek SSE → Anthropic SSE 事件映射(推断) │
│ │
└──┬─────────────────────────────────────────────────────────────┘
│ HTTPS Response(标准 Anthropic Messages API 格式)
▼
┌─ Claude Code 客户端 ──────────────────────────────────────────┐
│ │
│ 1. 解析 Anthropic 格式响应 │
│ 2. type:"text" → 显示给用户 │
│ 3. type:"tool_use" → 执行对应工具 │
│ 4. type:"thinking" → 在界面折叠展示 │
│ 5. usage → 写入 .claude.json 的 lastModelUsage 统计 │
│ 6. 如果执行了工具,结果返回,进入下一轮对话 │
│ │
└────────────────────────────────────────────────────────────────┘
│
▼
用户看到回复
(如果回复是 tool_use,则本轮结束后 Claude Code 自动进入下一轮循环)
九、一些值得关注的点
9.1 Token 统计的准确性
从 .claude.json 的数据示例:
"lastTotalInputTokens": ~68000, "lastTotalOutputTokens": ~16000, "lastTotalCacheReadInputTokens": ~550000
cache_read_input_tokens 高达 550k,远超实际 input tokens 的 68k。这说明缓存统计的口径和实际输入很可能是不同维度的——缓存数字可能包含了历史上被缓存的 system prompt 部分,也可能是兼容层基于某个固定公式估算的结果,而非真实的当次请求缓存命中。
9.2 转换层的性能开销
lastAPIDuration: ~217000ms(约 3.6 分钟),这个耗时包含:
-
兼容层的格式转换耗时(通常 <100ms,可忽略)
-
网络传输(国内到 DeepSeek 服务器)
-
DeepSeek V4 Pro 的推理耗时(占绝对大头)
在 1M 上下文窗口 + 大量工具定义 + thinking 开启的情况下,3.6 分钟的推理时间并不离谱。
9.3 有哪些东西在转换过程中"丢了"
| 在转换中可能丢失的 | 影响(推测) |
|---|---|
cache_control 标记 |
缓存控制精度大概率不如 Anthropic 原生,实际效果取决于 DeepSeek 内部实现 |
top_k |
如果通过 hook 设置了这个参数,它大概率不会生效 |
metadata.user_id |
大概率不影响功能,但 DeepSeek 侧的用户维度统计可能会丢失 |
| 精确的 cache token 统计 | 观察到的 cache 数字不一定反映真实命中情况 |
9.4 有哪些转换是"容易出 bug 的"
按风险从高到低排:
-
Tool call arguments 的 JSON 序列化/反序列化:嵌套对象、特殊字符、截断的 JSON,是兼容层最脆弱的环节
-
Chat Template 的 special token 对齐:DeepSeek V4 的 Chat Template 如果把 tool_call / tool_response / thinking 用不同于 Anthropic 训练数据的 special token 包裹,模型对这些结构的"理解"会偏离预期
-
Thinkinking ↔ reasoning_content 映射:两个厂商的推理格式都是半公开的,对齐程度直接影响 Claude Code 的 thinking 折叠功能;此外 tokenizer 对推理内容的分词方式是否与 Anthropic 一致也是未知
-
Streaming 事件的顺序和完整性:如果 DeepSeek 的 SSE 事件格式有版本变化,兼容层需要跟进
-
多 tool_use 并行时的消息映射:多个 tool 的 id 对应关系一旦错乱,Claude Code 会匹配不上工具结果
十、一句话总结
api.deepseek.com/anthropic 本质上是一个双向协议翻译器:它在 DeepSeek V4 Pro 上面模拟了一套完整的 Anthropic Messages API。Claude Code 全程以为自己在跟 Anthropic 服务器对话——它发出的每一个字段都被翻译成 DeepSeek 能懂的格式,返回的每一个字符又被翻译回 Anthropic 格式。settings.json 里用一个 DeepSeek API Key 和五个 ANTHROPIC_DEFAULT_*_MODEL 配置,让这套翻译器在每次对话中默默地工作了几百次(numStartups: ~380 记录了)。
对于使用者来说,最重要的是理解几件事:
-
观察到的
cache_read_input_tokens统计,数据来源不透明——它可能是真实的缓存命中、兼容层的估算值、或者只是维持接口兼容性的占位数字。不要像用 Anthropic 原生 API 一样把这些数字当精确指标。 -
thinking功能在 DeepSeek V4 上有真实的推理能力支撑,但格式映射是兼容层做的,对齐程度可能不如 Anthropic 原生。 -
所有模型名映射到同一族 DeepSeek V4 Pro,意味着 Claude Code 的"模型路由"实际上不再影响模型能力——唯一有意义的参数是
[1M]后缀带来的上下文窗口大小差异。
更多推荐




所有评论(0)