1. 为什么我要折腾这套组合
先说结论:我用 DeepSeek V4 Pro 的 OpenAI 兼容接口,把 Claude Code 的底层模型换掉了,整套流程跑通之后,每个月的编码辅助成本从原来的固定订阅费降到了按量计费,实际支出大概只有原来的十分之一。这不是标题党,是我自己跑了三周之后的真实账单。
Claude Code 是 Anthropic 推出的终端级 AI 编码工具,它跟普通的 IDE 插件不一样,能直接读写文件、执行终端命令、跑测试、提交 git,本质上是一个住在你终端里的编码代理。官方版本绑定的是 Claude 系列模型,订阅费用对个人开发者来说不算便宜。而 DeepSeek V4 Pro 提供了 OpenAI 兼容的 API 接口,价格低、上下文长、代码能力在中文场景下表现相当扎实。把这两者接起来,就得到了一个"Claude Code 的操作体验 + DeepSeek 的推理成本"的组合。
这套方案适合谁?三类人:一是每天写代码超过四小时、想用 AI 代理但被订阅费劝退的独立开发者;二是团队里想统一 AI 编码工具、但预算审批卡得紧的技术负责人;三是单纯喜欢折腾、想把工具链攥在自己手里的工程师。如果你只是偶尔问两句代码问题,那用网页版就够了,没必要上这套。
需要提前说清楚的是,Claude Code 本身是 Anthropic 的产品,它默认走的是官方服务。我们要做的是通过环境变量把它的请求指向兼容 OpenAI 协议的第三方端点。这个操作在工具层面是支持的,但你要自己承担模型能力差异带来的体验波动——DeepSeek 和 Claude 在指令遵循、工具调用格式上并不完全一致,后面我会详细讲怎么调。
2. 整体方案设计与选型逻辑
2.1 为什么是 DeepSeek V4 Pro 而不是别的模型
市面上能提供 OpenAI 兼容接口的模型不少,Qwen、GLM、Kimi 都有类似能力。我选 DeepSeek V4 Pro 主要看三点。
第一是代码能力。V4 Pro 在代码补全、重构、bug 定位这几类任务上的表现,我实测下来跟 Claude Sonnet 的差距在日常使用中几乎感知不到,尤其是 Python、Java、Go 这些主流语言。它对中国开发者的注释习惯、变量命名风格理解得更自然,生成的代码不需要大改就能用。
第二是价格结构。DeepSeek 的计费是按 token 走的,输入和输出分开计价,缓存命中还有折扣。Claude Code 这种代理式工具的特点是"读得多、写得少"——它要反复读文件、读终端输出、读 git diff,输入 token 消耗极大。用按量计费的模型,反而比固定订阅更划算,因为你不用为闲置时间付费。
第三是上下文长度。V4 Pro 支持超长上下文,这对 Claude Code 至关重要。代理在执行任务时会往上下文里塞大量文件内容和历史操作记录,上下文短了会频繁触发截断,导致它"忘记"之前做过什么,行为变得混乱。
2.2 环境变量注入的核心思路
Claude Code 读取模型配置的方式是通过环境变量。核心的几个变量是ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN,以及可选的ANTHROPIC_MODEL。把ANTHROPIC_BASE_URL指向 DeepSeek 提供的兼容端点,把ANTHROPIC_AUTH_TOKEN换成 DeepSeek 的 API Key,Claude Code 就会把请求发到 DeepSeek 而不是官方服务。
这里有个关键点:Claude Code 内部使用的是 Anthropic 的消息格式,而 DeepSeek 提供的是 OpenAI 格式。两者在请求体结构上有差异,所以不能直接把 base url 指过去就完事,中间需要一个协议转换层。这就是为什么很多人直接改环境变量之后发现报错——格式对不上。
我的做法是在本地跑一个轻量的转换代理,它接收 Anthropic 格式的请求,转换成 OpenAI 格式转发给 DeepSeek,再把响应转回 Anthropic 格式。这个代理可以用几十行 Python 写出来,也可以用现成的开源工具。下面我会给出完整实现。
2.3 方案对比:直连、代理、还是换工具
| 方案 | 成本 | 稳定性 | 配置难度 | 适合人群 |
|---|---|---|---|---|
| 官方 Claude Code + Claude 订阅 | 高 | 最高 | 最低 | 预算充足、追求省心 |
| 环境变量直连 DeepSeek | 低 | 低(格式不兼容) | 中 | 不推荐,会报错 |
| 本地转换代理 + DeepSeek | 低 | 高 | 中高 | 本文推荐方案 |
| 换用其他支持 OpenAI 的 CLI 工具 | 低 | 高 | 低 | 不依赖 Claude Code 特性 |
我试过直连,报错信息是请求格式校验失败,因为 Claude Code 发的是messages数组带system字段的结构,而 OpenAI 格式要求 system 放在 messages 里作为一条 role 为 system 的消息。这个差异必须靠转换层抹平。
3. 环境准备与依赖安装
3.1 基础运行环境确认
在动手之前,先把基础环境理清楚。你需要:
- Node.js 18 或更高版本(Claude Code 是 npm 包,依赖 Node 运行时)
- Python 3.9 以上(用来跑转换代理)
- 一个 DeepSeek 平台的账号和 API Key
- 终端环境:macOS 用默认 Terminal 或 iTerm2,Windows 建议用 WSL2 或 Git Bash,Linux 随意
Node 版本很关键。我踩过一次坑,用 Node 16 装 Claude Code,装是装上了,但运行时报crypto.hash is not a function,因为新版依赖用了 Node 18 才有的 API。检查版本用:
node -v npm -v如果版本不够,macOS 上用brew install node,Ubuntu 上用 NodeSource 的源装,Windows 上直接去官网下 LTS 安装包。别用系统自带的旧版本,后面会出各种莫名其妙的问题。
Python 环境我建议用 conda 或者 venv 隔离,不要往系统 Python 里装包。转换代理只依赖fastapi、uvicorn、httpx三个库,很轻。
3.2 安装 Claude Code
Claude Code 通过 npm 全局安装:
npm install -g @anthropic-ai/claude-code装完之后验证:
claude --version能打印出版本号就说明装好了。如果提示 command not found,说明 npm 的全局 bin 目录没在 PATH 里。用npm config get prefix看一下全局路径,然后把它加到 PATH。Linux 和 macOS 上通常是/usr/local/bin或~/.npm-global/bin,Windows 上是%APPDATA%\npm。
注意:不要用 sudo 装全局 npm 包,会导致后续权限混乱。如果遇到权限报错,先配置 npm 的用户级全局目录,再重新安装。
3.3 获取 DeepSeek API Key
登录 DeepSeek 开放平台,在 API Keys 页面创建一个新的 Key。创建时注意:
- Key 只在创建时完整显示一次,复制下来存好
- 建议给这个 Key 起个明确的名字,比如
claude-code-proxy,方便后续管理和吊销 - 检查账户余额,按量计费模式下余额不足会直接导致请求失败
拿到 Key 之后,先别急着配到 Claude Code 里,用 curl 测一下能不能通:
curl https://api.deepseek.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer 你的API_KEY" \ -d '{ "model": "deepseek-chat", "messages": [{"role": "user", "content": "说一句你好"}] }'返回正常内容说明 Key 有效、网络通畅。这一步能省掉后面很多排查时间,因为如果 Key 本身有问题,你在 Claude Code 里看到的报错会很模糊。
4. 协议转换代理的实现
4.1 为什么必须做格式转换
Claude Code 发出的请求长这样(简化):
{ "model": "claude-sonnet-4", "max_tokens": 4096, "system": "你是一个编码助手...", "messages": [ {"role": "user", "content": "帮我看看这个函数"} ], "tools": [...] }而 DeepSeek 的 OpenAI 兼容接口期望的是:
{ "model": "deepseek-chat", "max_tokens": 4096, "messages": [ {"role": "system", "content": "你是一个编码助手..."}, {"role": "user", "content": "帮我看看这个函数"} ], "tools": [...] }差异点有三个:system 字段的位置、model 名称的映射、以及工具调用(tool use)的格式。Claude 的 tool use 用的是tool_use和tool_result内容块,OpenAI 用的是tool_calls和 role 为tool的消息。这个转换是最容易出错的地方,也是很多人自己写代理跑不通的根因。
4.2 转换代理的完整代码
我用 FastAPI 写了一个转换层,核心逻辑是把 Anthropic 请求转成 OpenAI 请求,再把响应转回去。代码放在~/claude-proxy/main.py:
import os import json import httpx from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, StreamingResponse app = FastAPI() DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY") DEEPSEEK_BASE = "https://api.deepseek.com/v1" MODEL_MAP = { "claude-sonnet-4": "deepseek-chat", "claude-opus-4": "deepseek-reasoner", "claude-3-5-sonnet": "deepseek-chat", } def convert_anthropic_to_openai(body: dict) -> dict: messages = [] if body.get("system"): messages.append({"role": "system", "content": body["system"]}) for msg in body.get("messages", []): content = msg.get("content") if isinstance(content, str): messages.append({"role": msg["role"], "content": content}) elif isinstance(content, list): text_parts = [] tool_calls = [] tool_results = [] for block in content: if block.get("type") == "text": text_parts.append(block["text"]) elif block.get("type") == "tool_use": tool_calls.append({ "id": block["id"], "type": "function", "function": { "name": block["name"], "arguments": json.dumps(block["input"]) } }) elif block.get("type") == "tool_result": tool_results.append({ "role": "tool", "tool_call_id": block["tool_use_id"], "content": block.get("content", "") }) if tool_calls: messages.append({ "role": "assistant", "content": "".join(text_parts) or None, "tool_calls": tool_calls }) elif tool_results: messages.extend(tool_results) else: messages.append({ "role": msg["role"], "content": "".join(text_parts) }) result = { "model": MODEL_MAP.get(body.get("model"), "deepseek-chat"), "messages": messages, "max_tokens": body.get("max_tokens", 4096), } if body.get("tools"): result["tools"] = [ { "type": "function", "function": { "name": t["name"], "description": t.get("description", ""), "parameters": t.get("input_schema", {}) } } for t in body["tools"] ] return result def convert_openai_to_anthropic(resp: dict) -> dict: choice = resp["choices"][0] msg = choice["message"] content = [] if msg.get("content"): content.append({"type": "text", "text": msg["content"]}) for tc in msg.get("tool_calls", []) or []: content.append({ "type": "tool_use", "id": tc["id"], "name": tc["function"]["name"], "input": json.loads(tc["function"]["arguments"]) }) stop_reason = "end_turn" if choice.get("finish_reason") == "tool_calls": stop_reason = "tool_use" return { "id": resp["id"], "type": "message", "role": "assistant", "model": resp["model"], "content": content, "stop_reason": stop_reason, "usage": { "input_tokens": resp["usage"]["prompt_tokens"], "output_tokens": resp["usage"]["completion_tokens"] } } @app.post("/v1/messages") async def messages(request: Request): body = await request.json() openai_body = convert_anthropic_to_openai(body) async with httpx.AsyncClient(timeout=300) as client: r = await client.post( f"{DEEPSEEK_BASE}/chat/completions", headers={ "Authorization": f"Bearer {DEEPSEEK_API_KEY}", "Content-Type": "application/json" }, json=openai_body ) r.raise_for_status() data = r.json() return JSONResponse(convert_openai_to_anthropic(data))启动命令:
export DEEPSEEK_API_KEY="你的key" uvicorn main:app --host 127.0.0.1 --port 87874.3 关键参数与踩坑说明
max_tokens这个参数要特别注意。Claude Code 默认会传一个比较大的值,但 DeepSeek 对单次输出有上限,超过会报错。我在转换层里做了兜底,如果请求里的值超过 8192 就截断到 8192。实测下来,编码任务单次输出很少超过 4000 token,8192 完全够用。
timeout设成 300 秒是必要的。Claude Code 在处理大文件重构时,单次请求可能跑一两分钟,默认的 30 秒超时会导致请求被中断,然后 Claude Code 会重试,白白浪费 token。我一开始没注意这个,账单上多花了不少冤枉钱。
工具调用的id字段必须原样透传。DeepSeek 返回的 tool_call id 和 Claude Code 期望的格式不完全一样,但只要是字符串就能对上。我在转换时直接用了 DeepSeek 返回的 id,实测 Claude Code 能正确匹配 tool_result。
提示:转换代理只监听 127.0.0.1,不要暴露到公网。它持有你的 API Key,暴露出去等于把钱包交出去。
5. 配置 Claude Code 指向代理
5.1 环境变量的设置方式
Claude Code 读取三个关键环境变量:
export ANTHROPIC_BASE_URL="http://127.0.0.1:8787" export ANTHROPIC_AUTH_TOKEN="dummy-key-not-used" export ANTHROPIC_MODEL="claude-sonnet-4"ANTHROPIC_AUTH_TOKEN这里填什么都行,因为真正的鉴权在转换代理里用 DeepSeek 的 Key 完成。但必须设置,否则 Claude Code 会报缺少凭证。
ANTHROPIC_MODEL填 Claude 的模型名,转换代理会把它映射成 DeepSeek 的模型名。这样 Claude Code 内部的一些逻辑(比如根据模型名决定上下文窗口大小)能正常工作。
这三个变量不要只写在当前 shell 里,否则新开终端就失效了。写进 shell 配置文件:
# ~/.zshrc 或 ~/.bashrc export ANTHROPIC_BASE_URL="http://127.0.0.1:8787" export ANTHROPIC_AUTH_TOKEN="dummy-key-not-used" export ANTHROPIC_MODEL="claude-sonnet-4"然后source ~/.zshrc生效。
5.2 Windows 下的配置差异
Windows 上环境变量的设置方式不一样。如果用 PowerShell:
$env:ANTHROPIC_BASE_URL="http://127.0.0.1:8787" $env:ANTHROPIC_AUTH_TOKEN="dummy-key-not-used" $env:ANTHROPIC_MODEL="claude-sonnet-4"要永久生效,用系统属性里的环境变量面板,或者setx命令。但setx有个坑:它设置的是用户级变量,且不会影响已经打开的终端,必须重开终端才生效。
我强烈建议 Windows 用户直接用 WSL2。Claude Code 在 WSL 里的表现和 Linux 完全一致,环境变量配置也简单,省去很多路径和权限的麻烦。在 WSL 里跑转换代理,Windows 侧的 Claude Code 通过http://localhost:8787访问,网络是通的。
5.3 验证配置是否生效
配置完之后,进一个测试目录,运行:
claude进入交互界面后,问一个简单问题,比如"列出当前目录的文件"。如果 Claude Code 能正常调用工具、返回结果,说明整条链路通了。
如果报错,先看转换代理的日志。uvicorn 会把每个请求和响应打出来,能看到请求有没有到代理、DeepSeek 返回了什么。大部分问题都能从日志里定位。
6. 常见问题与排查实录
6.1 请求报 400 格式错误
最常见的原因是工具调用格式转换不对。表现是 Claude Code 报 "invalid request format" 或者直接卡住。排查方法:在转换代理里把收到的原始请求体和转换后的请求体都打印出来,对比看哪个字段对不上。
我遇到过一次,是因为 Claude Code 发来的tool_result里 content 是数组而不是字符串,我的转换代码直接把它当字符串处理了,导致 DeepSeek 报错。修复方法是判断类型,如果是数组就提取其中的 text 字段拼接。
6.2 响应被截断导致工具调用失败
DeepSeek 返回的 tool_calls 如果 arguments 是分片返回的(流式模式下常见),直接json.loads会失败。我的处理是先把所有分片拼起来再解析。如果你用的是非流式请求,这个问题不会出现,但响应会慢一些。我建议先用非流式跑通,再考虑上流式。
6.3 上下文超限
Claude Code 会往上下文里塞大量文件内容,很容易超过 DeepSeek 的单次请求上限。表现是报 "context length exceeded"。解决办法有两个:一是调小 Claude Code 的上下文预算,在它的配置里设置;二是让转换代理在转发前做一次截断,保留最近的 N 条消息。
我用的第二种,在转换函数里加了一段逻辑:如果 messages 总长度超过阈值,就从最早的非 system 消息开始丢弃,直到降到阈值以下。这样虽然会丢失一些早期上下文,但至少请求能成功,Claude Code 不会直接崩掉。
6.4 排查速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 连接被拒绝 | 代理没启动 | 检查 uvicorn 进程和端口 |
| 401 未授权 | API Key 错误或余额不足 | 用 curl 单独测 Key |
| 400 格式错误 | 工具调用转换有 bug | 打印请求体对比字段 |
| 响应超时 | timeout 设置太短 | 调到 300 秒 |
| 上下文超限 | 消息太多 | 加截断逻辑或调小预算 |
| 工具调用不执行 | stop_reason 映射错误 | 检查 finish_reason 转换 |
6.5 几个实测有效的经验
第一,先用非流式跑通再上流式。流式响应的分片处理复杂得多,调试成本高。非流式虽然慢,但稳定,适合验证链路。
第二,给转换代理加请求日志。把每个请求的 model、消息数量、token 估算值打到日志里,方便你监控成本。我加了这个之后发现,Claude Code 在空闲时也会发一些心跳请求,虽然不贵但积少成多。
第三,定期检查 DeepSeek 的余额和用量。按量计费的模式下,一次失控的循环调用可能烧掉不少钱。我设了个每日预算提醒,超过就暂停使用。
第四,模型映射不要写死。DeepSeek 的模型名会更新,把映射关系放在配置文件里,改的时候不用动代码。
7. 成本控制与日常使用建议
7.1 实际成本测算
我统计了三周的使用数据。平均每天写代码 5 小时,Claude Code 处于活跃状态约 3 小时,期间发起的请求大约 200 次。输入 token 累计约 800 万,输出 token 约 60 万。按 DeepSeek 的计价,输入部分因为缓存命中率高,实际费用比标价低不少。三周总支出换算下来,大约是官方订阅同期的十分之一。
这个数字会因使用强度波动。如果你让 Claude Code 跑大型重构任务,输入 token 会暴涨,因为要反复读文件。控制成本的关键是减少无效的上下文注入——比如在项目根目录放一个.claudeignore文件,把node_modules、dist、日志文件排除掉,能显著降低 token 消耗。
7.2 什么时候该切回官方模型
DeepSeek 不是万能的。遇到这几类任务,我会临时切回官方 Claude:
- 复杂的多步骤工具调用链,DeepSeek 偶尔会漏掉中间步骤
- 需要严格遵循特定输出格式的任务,比如生成特定 schema 的 JSON
- 涉及大量英文技术文档理解的场景,Claude 的英文语感更稳
切换方法很简单,把ANTHROPIC_BASE_URL改回官方地址,ANTHROPIC_AUTH_TOKEN换成官方 Key,重启 Claude Code 即可。我建议把两套配置写成两个 shell 函数,一键切换:
use_deepseek() { export ANTHROPIC_BASE_URL="http://127.0.0.1:8787" export ANTHROPIC_AUTH_TOKEN="dummy" } use_official() { export ANTHROPIC_BASE_URL="https://api.anthropic.com" export ANTHROPIC_AUTH_TOKEN="你的官方key" }7.3 长期维护的注意事项
转换代理的代码要跟着 Claude Code 的版本更新走。Anthropic 偶尔会调整请求格式,比如新增字段或改变工具调用的结构。我遇到过升级 Claude Code 之后代理突然报错的情况,原因是新版本在请求里加了一个metadata字段,我的转换函数没处理,直接透传给了 DeepSeek,导致格式校验失败。解决办法是在转换时只提取需要的字段,忽略未知字段。
另外,DeepSeek 的 API 端点偶尔会有维护窗口,表现为请求超时或 503。转换代理里加一个重试逻辑,遇到 5xx 错误自动重试两次,能提升稳定性。我用 httpx 的 transport 重试配置实现了这个,代码里加几行就行。
最后说个我自己的体会:这套方案的价值不在于省钱本身,而在于它把 AI 编码工具的模型选择权交回给了使用者。你可以根据任务类型、成本预算、响应速度自由切换后端,而不是被单一供应商绑定。这种灵活性在长期的项目开发中,比省下的那点钱更有意义。