1. 为什么我要把 DeepSeek Harness 拆开看DeepSeek Harness 是一个基于 Cordis 插件架构的 AI Agent 运行框架代号 dsh用 MIT 协议开源。它能做什么简单说它把传统 Agent 框架里写死的模型适配器、工具注册表、Agent 驱动循环、文件系统、Shell 执行、甚至 UI 界面全部做成了可插拔的插件。适合谁适合那些不满足于“只能用、不能改”的开发者想自己搭一个 AI 编程工具底座的人。我拿到这个框架的第一反应不是去点 Web UI而是想知道它到底怎么把“一切皆插件”落到代码层面的。因为市面上喊插件化的框架不少但大多数是“核心固化 外围扩展”真正能把 Agent 循环本身都做成插件的我见过的屈指可数。DeepSeek Harness 的源码结构让我确认了一件事它的插件体系不是装饰性的而是从启动加载到运行时事件流都贯穿了 Cordis 的时空可组合性设计。这篇文章我会按实际拆解路径来写先看它的插件加载链路和 Cordis 到底怎么工作再走一遍工具注册的完整流程然后重点解决多模型调用的问题——这里我会用 TaoToken 的统一 Key 和 API 通道来接入因为 Harness 的自定义 Provider 支持任意 OpenAI 兼容接口TaoToken 刚好能在这个位置接进去。最后给出可复制的配置片段、本地运行验证动作以及我踩过的几个报错排查。如果你之前用过 Claude Code 或 Cursor可以把 Harness 理解成“毛坯房”墙、管线、灯具都能重新布置。而 TaoToken 在这里的角色是给这间毛坯房接一条统一的水电通道——你不用为每个模型单独拉线一个 Key 就能切换。2. Cordis 插件加载与工具注册的实测拆解2.1 启动时的插件树是怎么叠出来的dsh 启动后不是直接跑一个主程序而是从配置文件按序组合出一棵插件树。根据源码 docs/architecture.md启动层级叠加顺序是Bundle 列表按序→ Profile 级 patch → 全局 Home 级 patch →--patch命令行覆盖。后一层可以覆盖前一层的任意配置项。具体到加载顺序Bundle 层先加载dsh-base这里面包含模型适配器、工具注册表、会话日志、沙箱和权限策略然后加载dsh-web-app这是 Web UI 层。接着是 Profile 级 patch读取当前 Profile 的cordis.patch.yml再是 Home 级 patch读取全局~/.dsh/下的补丁最后是命令行--patch参数指定的临时覆盖。这个设计的好处是你不需要改 Bundle 里的任何源码只需要在 patch 文件里写覆盖项就能替换掉默认的模型适配器或工具注册表。我实测下来patch 的合并策略是深合并对象类型会递归合并数组类型则整体替换。这意味着如果你要替换某个 Provider 的模型列表直接写新数组就行不会和旧数组合并。2.2 Cordis 的两个核心维度Cordis 框架的核心设计理念是时空可组合性拆开看就两个维度。时间可组合也就是可逆效应。插件注册的任何东西——工具、监听器、适配器——在插件卸载时都会被完全回滚不会残留。我验证的方式是写一个测试插件在apply里注册一个工具然后手动卸载插件再调用ctx.tools.get(my-tool)返回 undefined。这说明工具确实被注销了不是只从列表里移除引用。空间可组合也就是依赖注入。每个插件声明自己需要什么服务比如inject: [tools]框架自动等依赖就绪再加载。所有交互通过类型事件完成插件可以在任意节点监听、拦截、改写。这个机制让整个系统像一个插件插槽矩阵每个位置都可替换替换后自动生效卸载后自动恢复。2.3 工具注册的完整链路一个最小工具插件的结构是这样的导出name、inject和apply函数。在apply里调用ctx.tools.register(defineTool({...}))。defineTool接收 name、description、parameters 和 execute 四个字段。parameters 用 schema 声明框架会自动校验模型传过来的参数不用手写校验逻辑。工具 Schema 会自动注入到系统提示词里模型直接能看到。插件卸载时工具自动注销框架帮你处理加载、卸载、资源释放全流程。我实测注册了一个read_file工具然后在会话里让 Agent 读取一个本地文件。Agent 正确调用了工具返回了文件内容。整个过程不需要改框架代码也不需要重启服务器——如果你用的是热加载模式插件安装后下次请求就生效。2.4 Agent 循环的事件流Harness 对 Agent 交互做了清晰抽象一个 Step 是一次模型请求加上它调用的工具一个 Turn 包含零个或多个 Step。简化流程是turn/start→ 组装提示词和工具列表 →agent/pre-step插件可拦截改写输入→step/start→ 发请求给模型 → 模型返回 → 可能调用工具 →tools/pre-execute→ 执行 →tools/post-execute→step/end→agent/turn-stopping→turn/end。这个流程里每一个箭头都是一个事件插件可以挂在任意事件上做拦截。比如你想在每次工具执行前加权限检查只需要监听tools/pre-execute事件返回{ kind: deny }就能拒绝操作不需要改框架代码。会话日志采用事件溯源模式所有操作记录为 SessionEvent只追加不修改。模型看到的上下文、对话回放、会话恢复、Fork 分支全部从这份日志派生。运行时有断言检查保证模型可见的内容一定在日志里。这个设计在实际排障时很有用——当 AI 执行了你不期望的操作你可以精确追溯到哪一步出了问题。3. 用 TaoToken 统一接入多模型的配置片段Harness 内置了 DeepSeek 的 Provider但如果你想像我一样用统一通道接入多个模型就需要配置自定义 Provider。TaoToken 的 API 地址是https://taotoken.net/api兼容 OpenAI 协议所以可以直接填进 Harness 的自定义 Provider 配置里。3.1 settings.yaml 里的 Provider 配置Harness 的全局设置文件在$DSH_HOME/settings.yaml。你要加一个自定义 Provider就在llm-pi-ai下面写 providers。下面是我实际用的配置片段你可以直接复制后改 Keyllm-pi-ai: providers: taotoken: apiKeyEnv: TAOTOKEN_API_KEY api: openai-completions baseURL: https://taotoken.net/api models: - id: deepseek-chat - id: deepseek-v4-flash - id: claude-sonnet-4-20250514 - id: gpt-4o这里apiKeyEnv指定从环境变量读取 Key你也可以直接写apiKey字段但我不建议把明文 Key 写进配置文件。api字段指定协议类型TaoToken 走 OpenAI 兼容协议所以填openai-completions。baseURL就是 TaoToken 的 API 地址注意不要加多余的路径后缀。3.2 环境变量与 Key 的存放我习惯把 Key 放在 shell 的环境变量里而不是写进配置文件。在~/.zshrc或~/.bashrc里加一行export TAOTOKEN_API_KEYsk-你的实际Key然后source ~/.zshrc让环境变量生效。Harness 启动时会读取这个环境变量填充到 Provider 配置里。Key 本身不会出现在 settings.yaml 中避免误提交到 Git。如果你用的是 Web UI 的 Settings → Models 界面也可以直接在自定义 Provider 表单里填 Base URL 和 Key。Key 保存后只显示脱敏描述符实际存储在$DSH_HOME/.credentials.yaml中。我两种方式都试过环境变量方式更适合多项目共用Web UI 方式更适合快速切换。3.3 模型切换与生效时机配置写好后模型变更不需要重启服务器下次请求生效。你可以在 Web UI 的模型下拉框里直接切换不用改环境变量重启。我实测从deepseek-chat切到claude-sonnet-4-20250514下一次对话请求就用了新模型会话日志里记录的模型 ID 也同步更新了。这里有个细节Harness 的模型适配器是注册到ctx.llm的插件可以替换。如果你要接入的模型不在 OpenAI 兼容协议范围内就需要自己写一个适配器插件注册到ctx.llm。但 TaoToken 统一走 OpenAI 兼容协议所以不需要额外写适配器直接用内置的openai-completions协议就行。3.4 权限预设与沙箱的配合在 Settings → Permissions 里有两个预设workspace-write和danger-full-access。默认的workspace-write适合大多数场景AI 能修改工作区内的文件但执行危险命令或访问工作区外的目录时会弹窗确认。如果你对 AI 足够信任切到danger-full-access则完全放开。我建议在接入新 Provider 的初期保持workspace-write因为不同模型的工具调用行为可能有差异。等确认模型行为符合预期后再根据实际需要调整权限预设。这个预设不是单独的功能开关它把沙箱模式和审批策略两个维度打包成一个选项切换预设时两个维度同时变化。4. 本地运行与请求验证的完整动作4.1 环境准备与启动Harness 要求 Node.js 22.19 或 24pnpm 11.7.0Git 2.26。我用的是 Node 24 和 pnpm 11.7.0通过 Corepack 启用 pnpmcorepack enable corepack prepare pnpm11.7.0 --activate最快启动方式是直接跑 npxnpx deepseek-ai/dsh web启动后访问http://127.0.0.1:3080就能看到 Web 界面。如果要从源码构建git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness pnpm install pnpm run build pnpm dsh web我两种方式都试过npx 方式适合快速体验源码构建方式适合需要改源码或调试插件加载流程的场景。4.2 验证请求是否走通启动后在 Web UI 左侧设置栏选好模型点击 Choose workspace 添加项目目录然后开一个会话。我用的验证 prompt 是帮我读取当前目录下的 package.json列出 dependencies 和 devDependencies。如果 Agent 正确调用了read_file工具并返回了文件内容说明工具注册链路和模型调用链路都通了。如果模型返回的是“我无法读取文件”之类的回复说明工具没有正确注册到ctx.tools或者模型没有收到工具 Schema。另一个验证方式是直接看会话日志。Harness 的会话日志是事件溯源的你可以在日志里看到step/start、tools/pre-execute、tools/post-execute、step/end这些事件。如果日志里只有step/start和step/end没有工具执行事件说明模型没有发起工具调用。4.3 用 curl 直接验证 TaoToken 通道在配置 Harness 之前我建议先用 curl 验证 TaoToken 的 API 通道是否通。这样可以排除是 Harness 配置问题还是 API 通道问题curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: deepseek-chat, messages: [{role: user, content: 回复OK}], max_tokens: 10 }如果返回的 JSON 里有choices数组且message.content是“OK”说明通道正常。如果返回 401说明 Key 不对如果返回 404说明 baseURL 路径不对。这一步能帮你快速定位问题出在哪一层。4.4 插件热加载的验证如果你想验证插件热加载可以写一个最小插件放在cordis.yml里注册然后启动 dsh。修改插件代码后如果框架支持热加载下次请求会使用新代码。我实测下来Harness 的插件加载是启动时完成的热加载需要配合开发模式或手动触发重载。目前开发者预览阶段的热加载体验还不够顺滑建议在开发插件时用重启方式验证。5. 常见报错与排查路径5.1 401 Unauthorized这是最常见的报错。如果你在 Harness 里看到模型请求返回 401先检查三件事Key 是否正确、Key 是否过期、Key 是否有对应模型的权限。我遇到过一次是因为环境变量没生效——在.zshrc里加了export但当前终端没有source导致 Harness 读不到 Key。排查方式在终端里echo $TAOTOKEN_API_KEY确认输出的是你的实际 Key。如果输出为空说明环境变量没生效。另一个排查方式是看$DSH_HOME/.credentials.yaml里是否有对应的 Key 记录。5.2 local proxy failed这个报错通常出现在你配置了本地代理或自定义网络层的情况下。Harness 的模型请求走的是 Node.js 的 fetch如果你在环境里设置了HTTP_PROXY或HTTPS_PROXY请求可能会被路由到本地代理而代理没有正确转发。排查方式检查环境变量里是否有HTTP_PROXY、HTTPS_PROXY、ALL_PROXY如果有尝试临时取消这些变量再启动 Harness。另外检查settings.yaml里是否配置了自定义的baseURL确认地址没有拼错。5.3 reading choices 报错这个报错说明模型返回的 JSON 结构不符合预期框架在读取choices字段时失败了。常见原因是 Provider 的api协议类型填错了。如果你用的是 OpenAI 兼容接口api字段必须填openai-completions。如果填成了anthropic-messages或其他协议框架会按错误的格式解析响应。排查方式先用 curl 直接请求 TaoToken 的 API确认返回的 JSON 里有choices数组。如果 curl 返回正常但 Harness 报错检查settings.yaml里的api字段是否和实际协议匹配。5.4 OAuth 相关报错如果你在配置 Provider 时选了 OAuth 认证方式但 Provider 不支持 OAuth就会报这个错。TaoToken 走的是 API Key 认证不需要 OAuth。在自定义 Provider 配置里确保没有启用 OAuth 相关选项apiKeyEnv或apiKey字段正确填写即可。排查方式检查settings.yaml里是否有oauth相关字段如果有删掉或注释掉。然后确认apiKeyEnv指向的环境变量存在且值正确。5.5 工具注册失败如果你写的插件在注册工具时报错常见原因是inject声明不对。工具注册需要inject: [tools]如果漏了这个声明ctx.tools可能是 undefined。另一个原因是defineTool的 parameters schema 格式不对框架在解析 schema 时会报错。排查方式在插件的apply函数里加一行console.log(ctx.tools)确认ctx.tools存在。然后检查defineTool的 parameters 是否符合框架要求的 schema 格式。我踩过的坑是 parameters 里用了required: true但没在顶层声明required数组导致校验失败。6. 接入后的实际体验与后续动作配置好 TaoToken 通道后我在 Harness 里跑了几个实际任务读取项目结构、分析依赖、生成代码片段、执行简单 Shell 命令。整体体验和 Claude Code 类似但有几个差异点值得说。模型切换确实方便。在 Web UI 下拉框里直接切不用改环境变量重启。我测试了deepseek-chat、claude-sonnet-4-20250514和gpt-4o三个模型切换后下次请求就生效会话日志里记录的模型 ID 也同步更新。这对于需要对比不同模型输出效果的场景很实用。权限审批弹窗在 Web 界面里不如终端里按 Y/n 快。如果你习惯了 Claude Code 的终端交互这里需要适应一下。目前没有完整的 CLI 交互体验Web UI 是主要入口。插件开发门槛不高。一个最小工具插件只需要导出name、inject和apply在apply里调用ctx.tools.register。参数验证自动完成注册即生效卸载即清理。我写了一个简单的list_files工具从写代码到在会话里调用成功大概花了十分钟。如果你要长期用 Harness 做开发建议把 TaoToken 的 Key 放在环境变量里settings.yaml里只写apiKeyEnv引用。这样多项目共用同一个 Key切换项目时不用改配置文件。另外权限预设建议从workspace-write开始等确认模型行为符合预期后再调整。后续我打算试的是写一个自定义的 Agent 循环插件替换掉默认的驱动循环看看能不能实现不同的工具调用策略。Harness 的ctx.agentLoop是可替换的这意味着你可以重写 AI 与工具的整个交互流程。这个扩展点是它和传统框架最大的区别——传统框架的插件是给你加功能的核心你动不了Harness 的插件体系是连核心都可以换。如果你只是想用 AI 写代码Claude Code 或 Cursor 更合适。但如果你想自己搭建一个 AI 编程工具或者对 Agent 的底层实现感兴趣Harness 的源码值得一读。它的 Cordis 插件架构和事件溯源会话日志很可能会影响下一代 AI 编程工具的设计方向。
阅读完成 · 觉得有帮助?