1. 这个改造到底在折腾什么
第一次看到“80行 TypeScript Mods 把 Claude Code 改造成 Deepseek Harness”这个说法,我脑子里蹦出来的第一个念头是:这又是一个拿命令行工具开刀的玩法。Claude Code 本身是一个跑在终端里的智能编码助手,它通过读取项目文件、执行命令、调用模型接口来完成代码生成和修改任务。而 Deepseek Harness 这个词,在圈子里通常指的是一套围绕 Deepseek 模型构建的本地化调用框架——说白了,就是把模型能力封装成可编排、可拦截、可扩展的“马具”,让上层工具能像骑马一样驾驭模型输出。
这个项目的核心思路非常直接:不去改 Claude Code 的源码,也不去重新编译整个工具链,而是利用它对外暴露的 Mods 机制,用大约 80 行 TypeScript 写一个中间层,把原本发往某个模型服务的请求,重定向到 Deepseek 的接口上,同时对请求和响应做一层适配。这样做的好处是,你依然保留 Claude Code 的交互体验、文件操作能力和命令执行逻辑,但底层跑的模型换成了 Deepseek。对于想用 Deepseek 做代码辅助、又舍不得 Claude Code 那套顺手交互的人来说,这几乎是最小成本的方案。
适合谁来参考?如果你已经用过 Claude Code,知道怎么在终端里跟它对话、怎么让它改文件,同时对 TypeScript 有一定了解,能看懂接口定义和异步请求,那这个改造对你来说就是一顿饭的功夫。如果你完全没接触过命令行编码助手,也没关系,我会把每一步拆开讲,包括为什么要这么改、参数怎么填、哪里容易翻车。
提示:本文提到的所有工具、模型、接口均为技术讨论,实际使用时请确保你的调用方式符合相关服务的使用条款。
2. 为什么选择 Mods 而不是 Fork 源码
2.1 改源码的代价有多大
很多人第一反应是直接去改 Claude Code 的源码,把里面调用模型的地方替换掉。我试过类似的路子,结论是:不划算。原因有三。第一,Claude Code 的代码库更新频率不低,你改完一版,下次它升级,你的修改要么被覆盖,要么产生冲突,维护成本极高。第二,它的模型调用逻辑往往和上下文管理、工具调用、流式输出等模块耦合在一起,你动一处可能影响一片,调试起来非常痛苦。第三,改源码意味着你要重新构建整个项目,依赖安装、编译环境、版本对齐,每一步都可能卡住。
而 Mods 机制的设计初衷就是让外部开发者能在不侵入核心逻辑的前提下,对行为进行扩展或替换。它通常提供钩子函数、中间件管道或者配置注入点,让你在请求发出前、响应返回后插入自己的处理逻辑。80 行代码能搞定的事情,没必要去动几千行的源码。
2.2 Mods 机制的核心优势
用 Mods 做改造,最大的好处是隔离性。你的 TypeScript 文件独立于主程序存在,升级 Claude Code 时,只要 Mods 的接口签名没变,你的代码就不用动。即使接口有调整,改动的范围也局限在你自己的文件里,不会波及整个工具链。
第二个优势是可测试性。你可以单独写单元测试来验证请求转换逻辑,比如给定一个 Claude Code 发出的请求对象,检查转换后的 Deepseek 请求是否符合预期。这种测试在源码改造模式下很难做,因为你要模拟整个运行环境。
第三个优势是可逆性。哪天你想换回原来的模型,或者想试试别的模型,只需要修改 Mods 里的配置或替换转换逻辑,不用回滚代码库。这种灵活性在快速实验阶段特别重要。
2.3 80 行代码的边界在哪里
80 行 TypeScript 能做的事情有限,它主要覆盖的是请求地址替换、请求体格式转换、响应体格式转换这三件事。它不会去改 Claude Code 的 UI,不会去动文件读写逻辑,也不会去重新实现工具调用协议。换句话说,它只负责“把话递对地方,把回话翻译回来”,其他的都交给原有框架。
这也意味着,如果你的需求超出了这个范围,比如你想让 Deepseek 支持 Claude Code 特有的某种工具调用格式,而 Deepseek 原生不支持,那 80 行可能就不够了,你需要额外的适配层。但在大多数常规编码辅助场景下,请求和响应的结构差异并不大,80 行足够覆盖核心逻辑。
3. 核心细节拆解:请求与响应怎么转
3.1 请求侧:从 Claude Code 格式到 Deepseek 格式
Claude Code 发出的请求通常包含几个关键字段:模型名称、消息列表、温度参数、最大输出长度、是否流式返回等。Deepseek 的接口在这些字段上大同小异,但命名和结构可能有差异。比如,Claude Code 可能用max_tokens,而 Deepseek 用max_output_tokens;消息列表里角色的命名也可能不同,一个用assistant,另一个用ai。
转换逻辑就是做字段映射。你需要写一个函数,接收原始请求对象,返回一个新的对象,字段名和结构按照 Deepseek 的要求来。这里有个细节:如果原始请求里包含 Claude Code 特有的字段,而 Deepseek 不认识,你要么删掉,要么放到一个扩展字段里,避免接口报错。
function transformRequest(original: ClaudeRequest): DeepseekRequest { return { model: "deepseek-chat", messages: original.messages.map(m => ({ role: m.role === "assistant" ? "assistant" : "user", content: m.content })), max_tokens: original.max_tokens || 4096, temperature: original.temperature ?? 0.7, stream: original.stream ?? false }; }上面这段代码就是最核心的转换逻辑。注意??和||的区别:??只在值为null或undefined时用默认值,而||会在值为0、""等假值时也替换。温度参数可能被设为 0,这时候用||就会错误地变成 0.7,所以用??更安全。
3.2 响应侧:从 Deepseek 格式回到 Claude Code 格式
响应转换是反向操作。Deepseek 返回的数据结构里,生成的内容可能在choices[0].message.content,而 Claude Code 期望的可能是completion或者text字段。你需要把内容提取出来,重新组装成 Claude Code 能识别的对象。
如果涉及流式返回,事情会稍微复杂一点。流式响应是一系列数据块,每个块包含一小段生成内容。你需要逐个块转换,确保每个块的结构都符合 Claude Code 的预期。这里容易踩的坑是:Deepseek 的流式块可能包含额外的元数据字段,而 Claude Code 的解析器遇到未知字段可能会报错,所以转换时要过滤掉不需要的字段。
function transformResponse(original: DeepseekResponse): ClaudeResponse { const content = original.choices?.[0]?.message?.content ?? ""; return { completion: content, stop_reason: original.choices?.[0]?.finish_reason ?? "stop", model: original.model }; }这段代码处理的是非流式响应。流式响应的转换需要放在一个循环里,对每个数据块调用类似的转换函数,然后重新序列化发送给 Claude Code 的解析器。
3.3 错误处理与超时配置
请求发出去,不一定能顺利回来。网络抖动、接口限流、模型过载,都可能导致失败。你的 Mods 里需要包含基本的错误处理逻辑:捕获请求异常,判断是超时还是服务端错误,然后决定是重试还是直接返回错误信息给 Claude Code。
超时配置也很关键。Claude Code 默认的超时时间可能不适合 Deepseek 的响应速度,尤其是当你在处理长文本生成时。你可以在 Mods 里覆盖这个超时值,比如从默认的 30 秒调整到 120 秒。但要注意,超时设得太长,用户会以为程序卡死;设得太短,长任务会被频繁中断。根据我的经验,60 到 90 秒是一个比较平衡的区间。
注意:错误处理里不要直接把原始错误堆栈抛给 Claude Code,那样用户看到的是一堆看不懂的报错。最好包装成友好的提示,比如“模型服务暂时不可用,请稍后重试”。
4. 实操过程:从零到跑通的完整步骤
4.1 环境准备与依赖安装
开始之前,你需要确认几件事。第一,Claude Code 已经安装并能正常运行。第二,你的机器上有 Node.js 和 npm,因为 TypeScript 需要编译。第三,你有一个可用的 Deepseek API 密钥,并且知道接口地址。
依赖方面,通常需要安装typescript和ts-node,前者用于编译,后者用于直接运行 TypeScript 文件。如果你打算把 Mods 集成到 Claude Code 的启动流程里,可能还需要一个打包工具,比如esbuild,把 TypeScript 编译成 JavaScript 后放到指定目录。
npm install -g typescript ts-node npm install --save-dev @types/node这两条命令全局安装 TypeScript 和 ts-node,然后在项目里安装 Node.js 的类型定义。类型定义很重要,它让你在写代码时能获得自动补全和类型检查,减少低级错误。
4.2 编写 Mods 文件的关键步骤
创建一个名为deepseek-mod.ts的文件,然后按照以下结构组织代码。第一步,导入必要的类型和工具函数。第二步,定义请求和响应的接口,明确每个字段的类型。第三步,实现转换函数。第四步,注册 Mods 钩子,把转换函数挂到请求发出前和响应返回后的时机上。
注册钩子的方式取决于 Claude Code 的 Mods API。常见的形式是调用一个registerMod函数,传入一个对象,对象里包含onRequest和onResponse两个方法。onRequest在请求发出前执行,你在这里做请求转换;onResponse在响应返回后执行,你在这里做响应转换。
import { registerMod } from "claude-code-mods"; registerMod({ name: "deepseek-harness", onRequest: async (req) => { const transformed = transformRequest(req); transformed.baseURL = "https://api.deepseek.com/v1"; transformed.apiKey = process.env.DEEPSEEK_API_KEY; return transformed; }, onResponse: async (res) => { return transformResponse(res); } });这段代码里,baseURL和apiKey是从环境变量里读取的,这样你就不用把密钥硬编码在文件里。硬编码密钥是大忌,一旦文件被分享出去,密钥就泄露了。用环境变量既安全又灵活,换密钥时不用改代码。
4.3 参数计算与配置调优
Deepseek 的接口有一些参数需要你根据实际情况调整。比如max_tokens,它决定了模型一次最多生成多少内容。设得太小,代码生成到一半就断了;设得太大,响应时间会变长,而且可能浪费额度。对于代码辅助场景,我一般设 4096 到 8192 之间。如果是在做长文件重构,可以临时调到 16384,但要注意接口是否支持这么高的上限。
温度参数temperature控制输出的随机性。写代码时,我希望模型输出稳定、可预测,所以温度设低一点,比如 0.2 到 0.4。如果是在做创意性的代码注释或者文档生成,可以适当调高到 0.7 左右。这个参数没有绝对标准,多试几次就能找到适合你任务的值。
还有一个容易被忽略的参数是top_p,它和温度配合使用,控制候选词的采样范围。通常保持默认值 1.0 就行,除非你发现输出质量不稳定,可以尝试降到 0.9 或 0.95。
| 参数 | 推荐值 | 适用场景 | 注意事项 |
|---|---|---|---|
| max_tokens | 4096-8192 | 常规代码生成 | 超过 8192 需确认接口支持 |
| temperature | 0.2-0.4 | 代码补全、重构 | 过高会导致输出不稳定 |
| top_p | 0.9-1.0 | 通用场景 | 与温度配合调整 |
| timeout | 60000-90000 | 长文本生成 | 单位毫秒,过短会频繁超时 |
4.4 跑通验证与日志排查
配置完成后,启动 Claude Code,随便输入一个编码任务,比如“帮我写一个快速排序函数”。观察终端输出,如果一切正常,你应该能看到 Deepseek 生成的代码。如果报错,先看错误信息里有没有“401”或“403”,这通常是密钥问题;如果有“404”,检查接口地址是否写对;如果有“429”,说明请求太频繁,需要降低调用频率或增加重试间隔。
日志是你最好的朋友。在 Mods 里加几行console.log,把转换前后的请求和响应打印出来,对比一下结构是否一致。很多时候问题就出在某个字段名拼错了,或者某个必填字段漏了。日志不用一直开着,调试阶段打开,跑通后关掉就行,免得刷屏。
5. 常见问题与排查技巧实录
5.1 请求发出后没有响应怎么办
这是最常见的问题。先检查网络连通性,确认你的机器能访问 Deepseek 的接口地址。然后检查密钥是否有效,可以在终端里用curl手动发一个最简单的请求测试。如果手动请求能通,但 Claude Code 里不通,那问题就出在 Mods 的转换逻辑上。重点检查请求体里有没有多余的字段,或者字段类型不对,比如把数字写成了字符串。
另一个可能的原因是流式配置不匹配。Claude Code 可能默认开启流式,而 Deepseek 的流式接口需要额外的头部或者参数。你可以在 Mods 里强制把stream设为false,先跑通非流式,再逐步调试流式。
5.2 响应内容被截断或乱码
内容截断通常和max_tokens有关。如果模型生成到一半停了,检查这个值是不是设得太小。另外,有些接口在流式模式下,最后一个数据块可能不包含完整内容,你需要把所有块拼接起来再返回给 Claude Code。如果拼接逻辑写错了,就会丢内容。
乱码问题多半是编码格式不统一。确保你的请求和响应都使用 UTF-8 编码。在 Node.js 里,默认就是 UTF-8,但如果你手动处理了 Buffer,可能会引入编码错误。检查一下有没有对响应体做了不必要的二进制转换。
5.3 工具调用功能失效
Claude Code 支持工具调用,比如让它执行一个 shell 命令或者读取某个文件。这个功能依赖于模型返回特定格式的工具调用请求。Deepseek 可能不支持完全相同的格式,导致 Claude Code 无法识别。解决办法是在响应转换时,把 Deepseek 的工具调用格式映射成 Claude Code 期望的格式。如果 Deepseek 根本不支持工具调用,那这个功能就只能放弃,或者你在 Mods 里模拟一个工具调用层,把 Deepseek 的普通文本输出解析成工具调用请求。
提示:工具调用的适配是 80 行代码之外的事情,如果你重度依赖这个功能,可能需要额外写几百行解析逻辑。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 无响应 | 网络不通/密钥无效 | curl 手动测试 | 检查网络和密钥 |
| 401 错误 | 密钥错误或过期 | 查看密钥状态 | 更换有效密钥 |
| 404 错误 | 接口地址错误 | 核对 baseURL | 修正接口路径 |
| 429 错误 | 请求频率过高 | 查看调用间隔 | 增加重试延迟 |
| 内容截断 | max_tokens 过小 | 检查参数值 | 调大 max_tokens |
| 乱码 | 编码不一致 | 检查 Buffer 处理 | 统一使用 UTF-8 |
| 工具调用失效 | 格式不兼容 | 对比请求结构 | 增加格式映射层 |
6. 实操心得与避坑经验
6.1 密钥管理别偷懒
我见过太多人把 API 密钥直接写在代码里,然后不小心提交到了公开仓库。一旦泄露,别人就能用你的额度,甚至可能产生费用。正确的做法是用环境变量,或者用一个.env文件,并把.env加到.gitignore里。如果你在团队里共享配置,用密钥管理服务,别用聊天工具发密钥。
6.2 先跑通再优化
很多人一上来就想把流式、工具调用、错误重试全部做完美,结果卡在某个细节上,迟迟跑不通。我的建议是:先用最简单的非流式请求跑通整个链路,确认请求能发出去、响应能回来、Claude Code 能正常显示。然后再逐步加上流式、重试、工具调用适配。每一步只改一个变量,这样出问题时容易定位。
6.3 日志要加时间戳
调试时打印日志,一定要带上时间戳。否则当请求和响应交错出现时,你根本分不清哪个响应对应哪个请求。时间戳加上请求 ID,能让你快速追踪一次完整调用。请求 ID 可以用随机字符串生成,在请求发出时打印一次,在响应返回时再打印一次,两边一对就能串起来。
6.4 版本兼容性要留意
Claude Code 和 Deepseek 的接口都可能更新。你的 Mods 代码里,最好把接口版本号写成配置项,而不是硬编码在 URL 里。这样当接口升级时,你只需要改配置,不用改代码。另外,定期检查 Claude Code 的 Mods API 有没有变化,如果签名变了,你的代码可能编译不过,提前发现比运行时崩溃要好。
6.5 性能监控不能少
跑通之后,记录一下每次请求的耗时。如果发现响应时间越来越长,可能是模型服务端负载高了,也可能是你的网络环境变差了。有了监控数据,你就能判断是暂时波动还是趋势性恶化。简单的做法是在日志里打印请求开始和结束的时间差,复杂一点可以接入一个轻量级的监控工具,把耗时数据可视化。
这个改造方案的核心价值在于用最小的代码量撬动了模型切换的能力。80 行 TypeScript 听起来不多,但每一行都落在关键路径上:请求转换、响应转换、错误处理、配置注入。你把这几块写扎实了,整个链路就稳了。后续如果想支持更多模型,只需要增加转换函数,不用动框架本身。我在实际使用中发现,最耗时间的不是写代码,而是调试接口格式的差异。所以建议你在动手之前,先把两个接口的文档对照看一遍,把字段映射关系列成表格,写代码时直接照着填,能省下不少来回试错的时间。