VSCode 中 Claude Code 接入 DeepSeek:协议转换与配置指南
2026/9/20 4:31:29 网站建设 项目流程

1. 为什么要在 VSCode 里把 Claude Code 接到 DeepSeek 上

Claude Code 这个命令行工具刚出来的时候,很多人第一反应是"这不就是个终端里的 AI 编程助手吗",但真正用起来才发现它的价值在于把整个项目目录当成上下文,能直接读写文件、跑命令、改代码,比在网页里复制粘贴强太多。问题也很现实:官方默认走的是 Anthropic 的接口,用量一大成本就上来了,而且国内网络环境下调用稳定性也让人头疼。

DeepSeek 这两年在代码能力上的进步有目共睹,尤其是它的推理模型在编程任务上的表现,加上 API 价格相比国际主流模型便宜一个数量级,就成了很多人的替代方案。把 Claude Code 的前端交互和 DeepSeek 的后端模型结合起来,本质上是用 Claude Code 的工程化能力 + DeepSeek 的性价比,这个组合对个人开发者和小团队来说非常划算。

这篇内容适合三类人:一是已经在用 Claude Code 但想换模型的;二是想用 DeepSeek 但不想自己写一套 CLI 工具的;三是纯粹想搞清楚ANTHROPIC_BASE_URL这类环境变量到底怎么配的。我会从原理讲到实操,把每一步为什么这么做都说清楚,配置过程中容易踩的坑也会一并列出来。

需要先说明一点:Claude Code 本身是 Anthropic 的客户端,它默认只认 Anthropic 的接口协议。DeepSeek 官方提供的是 OpenAI 兼容格式的 API,两者协议不完全一样。所以中间需要一个协议转换层,这是整个方案的核心,后面会详细讲。

2. 核心原理拆解:协议转换到底在转什么

2.1 Claude Code 的请求长什么样

Claude Code 发出去的请求走的是 Anthropic Messages API 格式,核心字段包括modelmax_tokensmessagessystemtools等。它和 OpenAI 格式最大的区别在于:

  • system提示是顶层字段,不是放在 messages 数组里的第一条
  • 消息角色只有userassistant,工具调用结果用user角色携带tool_result类型的内容块
  • 工具定义用的是input_schema而不是parameters
  • 流式返回的事件类型是content_block_deltamessage_delta这一套

这些差异意味着你不能简单地把ANTHROPIC_BASE_URL指向 DeepSeek 的地址就完事,字段对不上,请求会直接报 400。

2.2 DeepSeek 的接口格式

DeepSeek 的 API 是 OpenAI 兼容的,/chat/completions端点,system放在 messages 里,工具用tools数组加function.parameters,流式返回是choices[].delta。它支持的模型名在热词里也提到了,比如deepseek-flashdeepseek-v4deepseek-v4-pro这些,具体可用名称要以你账号下的模型列表为准,报错信息里通常会直接告诉你支持哪些。

2.3 转换层的三种实现路径

方案原理优点缺点
官方兼容端点部分服务商直接提供 Anthropic 格式入口零配置,最省事不是所有服务商都有
本地代理转换跑一个本地服务做协议翻译完全可控,可加日志需要额外进程
客户端改写改 Claude Code 的请求逻辑无中间层升级会覆盖,维护成本高

我实测下来,本地代理转换是最稳的路子。原因很简单:Claude Code 更新频繁,改客户端源码每次升级都要重来;而本地代理只要协议映射写对了,客户端怎么升级都不影响。而且代理层可以加请求日志,出问题的时候能直接看到原始请求和转换后的请求,排查效率高很多。

提示:转换层要处理的不只是字段名映射,还有流式响应的 SSE 事件格式转换。很多人卡在"非流式能用、流式就断"就是这里没处理好。

3. 环境准备与依赖安装

3.1 基础环境检查

动手之前先把这几样确认好,缺一个后面都会卡:

  • Node.js 18 以上:Claude Code 是 npm 包,低版本会有兼容问题。用node -v确认。
  • npm 或 pnpm:装包用,pnpm 更快但 npm 也行。
  • DeepSeek API Key:去 DeepSeek 开放平台申请,注意保存好,只显示一次。
  • 一个能跑本地服务的运行时:Node 或 Python 都行,我下面用 Node 举例,因为和 Claude Code 同生态,依赖少。

Windows 用户如果遇到failed to connect to the docker api at npipe这类报错,说明你在用 Docker 方案,其实这个场景不需要 Docker,直接本地跑 Node 服务更简单,别被带偏了。

3.2 安装 Claude Code

npm install -g @anthropic-ai/claude-code

装完用claude --version验证。如果提示命令找不到,检查 npm 全局 bin 目录有没有加到 PATH 里。Windows 上常见的是%APPDATA%\npm,macOS/Linux 是/usr/local/bin~/.npm-global/bin

3.3 准备转换代理项目

新建一个目录,初始化:

mkdir claude-deepseek-proxy && cd claude-deepseek-proxy npm init -y npm install express axios

这里选 Express 是因为它足够轻,写个转发接口几十行就够。axios 用来转发请求到 DeepSeek。如果你更喜欢原生 fetch,Node 18 自带,可以省掉 axios 依赖。

3.4 配置环境变量

在项目根目录建一个.env文件:

DEEPSEEK_API_KEY=sk-你的key DEEPSEEK_BASE_URL=https://api.deepseek.com DEEPSEEK_MODEL=deepseek-v4 PROXY_PORT=8787

DEEPSEEK_MODEL这里填你账号下实际可用的模型名。如果填错了,DeepSeek 会返回 400 并告诉你支持的模型列表,照着改就行。热词里出现的the supported api model names are deepseek-flash, deepseek-v4-pro就是这类报错,属于配置问题不是代码问题。

4. 协议转换代理的完整实现

4.1 请求转换:Anthropic 到 OpenAI

核心逻辑是把 Anthropic 的请求体翻译成 OpenAI 格式。关键映射关系:

function anthropicToOpenAI(body) { const messages = []; // system 顶层字段搬到 messages 第一条 if (body.system) { messages.push({ role: 'system', content: body.system }); } // 逐条转换消息 for (const msg of body.messages) { if (typeof msg.content === 'string') { messages.push({ role: msg.role, content: msg.content }); } else if (Array.isArray(msg.content)) { // 处理内容块:text / tool_use / tool_result const textParts = []; const toolCalls = []; const toolResults = []; for (const block of msg.content) { if (block.type === 'text') { textParts.push(block.text); } else if (block.type === 'tool_use') { toolCalls.push({ id: block.id, type: 'function', function: { name: block.name, arguments: JSON.stringify(block.input) } }); } else if (block.type === 'tool_result') { toolResults.push({ role: 'tool', tool_call_id: block.tool_use_id, content: typeof block.content === 'string' ? block.content : JSON.stringify(block.content) }); } } if (toolResults.length > 0) { messages.push(...toolResults); } else if (toolCalls.length > 0) { messages.push({ role: 'assistant', content: textParts.join('\n') || null, tool_calls: toolCalls }); } else { messages.push({ role: msg.role, content: textParts.join('\n') }); } } } // 工具定义转换 const tools = body.tools?.map(t => ({ type: 'function', function: { name: t.name, description: t.description, parameters: t.input_schema } })); return { model: process.env.DEEPSEEK_MODEL, messages, tools, max_tokens: body.max_tokens, stream: body.stream }; }

这段代码有几个容易写错的地方。第一,tool_result在 Anthropic 里是放在user消息的内容块里的,转换后要变成独立的role: 'tool'消息,顺序不能乱。第二,tool_useinput是对象,OpenAI 要的是 JSON 字符串。第三,当一条 assistant 消息同时有文本和工具调用时,contenttool_calls要同时存在。

4.2 响应转换:OpenAI 到 Anthropic

非流式响应相对简单,把choices[0].message拆成 Anthropic 的 content 块数组:

function openAIToAnthropic(resp, model) { const choice = resp.choices[0]; const content = []; if (choice.message.content) { content.push({ type: 'text', text: choice.message.content }); } if (choice.message.tool_calls) { for (const tc of choice.message.tool_calls) { content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input: JSON.parse(tc.function.arguments) }); } } return { id: resp.id, type: 'message', role: 'assistant', model, content, stop_reason: choice.finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn', usage: { input_tokens: resp.usage?.prompt_tokens || 0, output_tokens: resp.usage?.completion_tokens || 0 } }; }

stop_reason的映射很关键。Claude Code 靠这个字段判断要不要继续执行工具,如果tool_calls存在但stop_reason给成了end_turn,工具就不会被执行,表现为"模型说要调工具但没动静"。

4.3 流式响应的事件转换

流式是最麻烦的部分。Anthropic 的 SSE 事件序列大致是:

message_start content_block_start (text 或 tool_use) content_block_delta (text_delta 或 input_json_delta) content_block_stop message_delta (带 stop_reason) message_stop

而 OpenAI 的流式是每个 chunk 带choices[0].delta,工具调用的参数是分片拼接的。转换时要维护状态机:

async function* streamConvert(openaiStream, model) { const messageId = 'msg_' + Date.now(); let contentIndex = 0; let currentToolCall = null; let toolArgsBuffer = ''; yield sse('message_start', { type: 'message_start', message: { id: messageId, type: 'message', role: 'assistant', model, content: [], usage: { input_tokens: 0, output_tokens: 0 } } }); for await (const chunk of openaiStream) { const delta = chunk.choices?.[0]?.delta; if (!delta) continue; // 文本增量 if (delta.content) { if (contentIndex === 0) { yield sse('content_block_start', { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }); } yield sse('content_block_delta', { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: delta.content } }); } // 工具调用增量 if (delta.tool_calls) { for (const tc of delta.tool_calls) { if (tc.function?.name) { // 新工具开始 if (currentToolCall) { yield sse('content_block_stop', { type: 'content_block_stop', index: contentIndex }); contentIndex++; } currentToolCall = tc; toolArgsBuffer = ''; yield sse('content_block_start', { type: 'content_block_start', index: contentIndex, content_block: { type: 'tool_use', id: tc.id, name: tc.function.name, input: {} } }); } if (tc.function?.arguments) { toolArgsBuffer += tc.function.arguments; yield sse('content_block_delta', { type: 'content_block_delta', index: contentIndex, delta: { type: 'input_json_delta', partial_json: tc.function.arguments } }); } } } // 结束原因 if (chunk.choices[0].finish_reason) { if (currentToolCall) { yield sse('content_block_stop', { type: 'content_block_stop', index: contentIndex }); } else if (contentIndex === 0) { yield sse('content_block_stop', { type: 'content_block_stop', index: 0 }); } yield sse('message_delta', { type: 'message_delta', delta: { stop_reason: chunk.choices[0].finish_reason === 'tool_calls' ? 'tool_use' : 'end_turn' }, usage: { output_tokens: 0 } }); } } yield sse('message_stop', { type: 'message_stop' }); }

sse是个辅助函数,把事件名和数据拼成event: xxx\ndata: {...}\n\n的格式。这里的状态机要特别注意工具调用的 index 管理,多个工具连续调用时 index 要递增,否则 Claude Code 解析会乱。

4.4 主服务入口

import express from 'express'; import axios from 'axios'; const app = express(); app.use(express.json({ limit: '50mb' })); app.post('/v1/messages', async (req, res) => { const openaiBody = anthropicToOpenAI(req.body); try { if (req.body.stream) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const response = await axios.post( `${process.env.DEEPSEEK_BASE_URL}/chat/completions`, { ...openaiBody, stream: true }, { headers: { 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, 'Content-Type': 'application/json' }, responseType: 'stream' } ); // 把 axios stream 转成 async iterable for await (const event of streamConvert(parseSSE(response.data), openaiBody.model)) { res.write(event); } res.end(); } else { const response = await axios.post( `${process.env.DEEPSEEK_BASE_URL}/chat/completions`, openaiBody, { headers: { 'Authorization': `Bearer ${process.env.DEEPSEEK_API_KEY}`, 'Content-Type': 'application/json' } } ); res.json(openAIToAnthropic(response.data, openaiBody.model)); } } catch (err) { console.error('Proxy error:', err.response?.data || err.message); res.status(err.response?.status || 500).json({ type: 'error', error: { type: 'api_error', message: err.response?.data?.error?.message || err.message } }); } }); app.listen(process.env.PROXY_PORT, () => { console.log(`Proxy running on http://localhost:${process.env.PROXY_PORT}`); });

parseSSE是个把 SSE 流解析成对象的异步生成器,处理data:前缀和[DONE]结束标记。这部分代码不复杂但容易漏掉边界情况,比如跨 chunk 的半行数据要缓存起来。

5. 把 Claude Code 指向本地代理

5.1 环境变量配置

代理跑起来之后,让 Claude Code 走本地:

export ANTHROPIC_BASE_URL=http://localhost:8787 export ANTHROPIC_API_KEY=any-string-works

ANTHROPIC_API_KEY这里填什么都行,因为真正的鉴权在代理层用 DeepSeek 的 key 完成。但 Claude Code 启动时会检查这个变量存不存在,不填会直接报错退出。

Windows PowerShell 用$env:ANTHROPIC_BASE_URL="http://localhost:8787",想持久化就写进系统环境变量。macOS/Linux 想持久化就加到~/.zshrc~/.bashrc

5.2 验证连通性

先单独测代理:

curl http://localhost:8787/v1/messages \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "max_tokens": 100, "messages": [{"role": "user", "content": "说一句话"}] }'

能返回 Anthropic 格式的 JSON 就说明代理通了。然后再跑claude进交互模式,随便问一句,看有没有正常回复。

5.3 模型名映射的处理

Claude Code 内部会传它认为的模型名,比如claude-3-5-sonnet-20241022。代理层要忽略这个字段,统一替换成DEEPSEEK_MODEL。我在anthropicToOpenAI里就是这么做的,直接写死用环境变量。如果你想支持多模型切换,可以在代理里加个映射表,根据请求的 model 字段路由到不同的 DeepSeek 模型。

注意:不要试图让 Claude Code 直接传 DeepSeek 的模型名,它的模型列表是硬编码的,传不认识的名字会在客户端就报错,根本到不了代理层。

6. 常见报错与排查速查

6.1 400 类错误

报错信息原因解决
the supported api model names are...模型名填错改成报错里列出的名字
maximum context length is 1048576 tokens上下文超限清理对话历史或换长上下文模型
invalid_request_error: messages消息格式转换有误检查 tool_result 是否转成了独立 tool 消息
tools[0].function.parameters工具 schema 不合法确认 input_schema 是合法 JSON Schema

6.2 连接类错误

failed to connect to the docker api at npipe这种是 Docker Desktop 没启动或者管道配置问题。但前面说了,这个方案不需要 Docker,如果你看到这个报错,说明你参考的教程用了容器方案,直接换成本地 Node 跑就行。

login failed. check api token一般是ANTHROPIC_API_KEY没设,或者代理没起来。先确认代理进程在跑,再确认环境变量在当前 shell 生效。

6.3 流式中断问题

最常见的表现是回复到一半卡住,或者工具调用参数不完整。排查思路:

  1. 看代理日志里 DeepSeek 返回的原始 chunk,确认数据是完整的
  2. 检查content_block_stop有没有在正确时机发出
  3. 确认message_delta里的stop_reason和实际 finish_reason 一致

我踩过的一个坑是:DeepSeek 在某些情况下会返回空的 delta chunk(只有 role 没有 content),如果代码里没做空值判断,会往content_block_delta里塞空文本,Claude Code 解析时可能直接断流。

6.4 工具调用不执行

如果模型明确说要调用工具但 Claude Code 没动作,九成是stop_reason映射错了。Anthropic 的tool_use对应 OpenAI 的tool_calls,别映射成end_turn。另外工具调用的id要保证唯一,重复的 id 会让客户端状态混乱。

7. 实操心得与性能调优

7.1 代理层的日志策略

强烈建议在代理里加请求日志,但要注意别把 API Key 打出来。我一般记录这些字段:请求的 model、消息条数、是否有工具、响应耗时、token 用量。出问题的时候这些信息足够定位,又不会泄露敏感数据。

日志写到文件里,用pino或简单的fs.appendFile都行。跑一段时间后你会发现,大部分问题都能从日志里直接看出来,比盲目调试快得多。

7.2 超时和重试

DeepSeek 的响应速度整体不错,但高峰期偶尔会慢。代理层要设合理的超时,我一般设 120 秒,流式的话用responseType: 'stream'配合 axios 的timeout只作用于建立连接阶段,不会中途掐断。

重试要谨慎。非流式请求可以重试,流式请求不要自动重试,因为流已经吐出去一部分了,重试会导致内容重复。真要重试就让用户手动重发。

7.3 上下文长度管理

Claude Code 会把项目文件内容塞进上下文,很容易撑爆。DeepSeek 不同模型的上下文窗口不一样,用之前确认一下。如果经常超限,可以在 Claude Code 里用/compact压缩历史,或者调整它的文件读取策略,别让它一次读太多文件。

7.4 成本控制

DeepSeek 便宜是相对的,工具调用密集的场景 token 消耗还是很快。我的做法是在代理层统计每天的 token 用量,超过阈值就告警。另外 Claude Code 有些操作会触发大量文件读取,可以在它的配置里限制单次读取的文件数量。

7.5 稳定性观察

跑了大概两周,整体稳定性可以。遇到过一次 DeepSeek 侧返回 503,代理直接把错误透传给 Claude Code,客户端显示"API 错误"但没崩,重发就好了。这种上游抖动没法完全避免,代理层做好错误透传,让用户知道是上游问题而不是本地配置问题,体验会好很多。

8. 后续可以扩展的方向

代理层跑通之后,其实可以做不少有意思的事。比如加一个请求缓存,相同的 prompt 直接返回缓存结果,省 token 又提速。再比如加多模型路由,简单任务走便宜的 flash 模型,复杂推理走 pro 模型,根据请求内容自动判断。

还有一个方向是本地模型兜底。如果 DeepSeek 接口临时不可用,代理可以自动切到本地跑的小模型,虽然效果差一些但至少不断线。这个用 Ollama 之类的本地推理服务配合就能实现,代理层做个 fallback 逻辑即可。

工具调用这块也可以优化。Claude Code 的工具定义比较固定,代理层可以针对 DeepSeek 的特点做 prompt 层面的适配,比如在 system 里加一些引导,让模型更规范地输出工具调用格式,减少解析失败的概率。

我个人在实际操作中的体会是,这套方案的价值不在于省了多少钱,而在于把选择权拿回自己手里。模型可以换、参数可以调、日志可以看,出了问题知道去哪找原因,这种掌控感是直接用官方服务给不了的。配置过程确实有点折腾,但一次配好之后就很省心,后面换模型也就是改个环境变量的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询