ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DeepSeek Harness 源码深度解读:官方开源 Agent 框架的架构拆解与插件化实践

DeepSeek Harness 源码深度解读:官方开源 Agent 框架的架构拆解与插件化实践 1. 从一次源码走读说起DeepSeek Harness 到底解决了什么问题DeepSeek Harness仓库名deepseek-ai/deepseek-harness命令行简称dsh是 DeepSeek 官方开源的一个插件化 Agent 框架MIT 协议口号只有一句Everything is a Plugin。它能做什么简单说它把「模型适配器、工具注册表、会话日志、甚至 Agent Loop 本身」全部做成了可插拔的插件你可以在不改动核心的前提下替换任意一层。适合谁适合已经用过 LangChain、AutoGen 这类框架、但被「核心不可动、扩展只能挂回调」折磨过的工程师也适合想认真读一份 20 万行 TypeScript 工程、理解现代 Agent 运行时怎么设计的二次开发者。我第一次克隆这个仓库时最直观的感受是「包太多了」——packages/下 54 个包全 ESMpnpm workspaces 管理每个包都叫deepseek-ai/dsh-*。但真正让我坐下来读源码的是文档里那句「不存在需要打补丁的特权内核」。大多数号称可扩展的框架核心是一个巨大的Agent类扩展点就是几个onToolCall回调而 dsh 把 Agent Loop 本身都做成了插件core/agent-loop这意味着你可以整条替换掉默认的 ReAct 驱动器。这篇不走「部署 demo」路线而是带你做一次可复现的源码走读先把本地运行环境配好再顺着插件注册链路读一遍最后自己写一个最小插件挂上去验证。全程命令可复制报错有对照。读完之后你应该能独立判断一个扩展点该挂在哪个事件域、该用 waterfall 还是 serial。需要说明的是dsh 当前版本是0.1.0-rc.5官方明确标注 Developer Preview破坏性变更随时可能发生SQLite schema 用单调版本号、会话格式无兼容承诺。所以本文的定位是「读懂架构 本地验证」不是「上生产」。2. 前置准备本地跑通 dsh 与 TaoToken 接入配置在动源码之前先把运行环境跑起来否则后面读调用链会缺少「真实日志」这个参照物。dsh 的运行依赖 Node.js建议 20 LTS 以上和 pnpm。克隆与安装git clone https://github.com/deepseek-ai/deepseek-harness.git cd deepseek-harness corepack enable pnpm install pnpm buildpnpm build会编译全部 54 个包第一次跑大概几分钟。构建完成后最省事的启动方式是直接用发布到 npm 的包npx deepseek-ai/dsh web # 默认监听 http://127.0.0.1:3080Web UI 起来之后你需要给它配一个模型提供方。dsh 的模型适配器在packages/llm/下默认走 DeepSeek 系deepseek/pi-ai。如果你希望用统一的 API 网关来管理 Key、方便在多个模型之间切换可以把 Base URL 指向 TaoToken 的兼容端点。TaoToken 官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 这个地址不加 UTM 参数。先去控制台创建一个 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面拿到密钥https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。dsh 的配置走的是「Profile Bundle Patch」三层叠放模型配置文件放在 Harness home 目录下。默认 home 是~/.dsh/你可以用环境变量覆盖export DSH_HOME$HOME/.dsh mkdir -p $DSH_HOME模型凭据的注入方式最直接的是环境变量。dsh 的llm插件会读取标准的 provider 环境变量你可以这样设置export DEEPSEEK_API_KEYsk-你的TaoToken密钥 export DEEPSEEK_BASE_URLhttps://taotoken.net/api如果你更希望把配置写进文件而不是环境变量可以在$DSH_HOME/cordis.patch.yml里做覆盖。这个文件是 home 级 patch叠放顺序在 profile 的 patch 之后、--patchoverlay 之前也就是「越晚叠的层权力越大」。一个最小 patch 长这样# $DSH_HOME/cordis.patch.yml - id: llm-deepseek config: baseURL: https://taotoken.net/api apiKey: !!js process.env.DEEPSEEK_API_KEY model: deepseek-chat这里id对应插件树里的条目 idconfig会整体替换该条目的配置。注意!!js是 Cordis 配置里执行 JS 表达式的写法用来避免把密钥明文写进文件。如果你不确定某个插件的 id 是什么可以用dsh的 inspect 能力打印当前插件树后面第 4 节会讲怎么验证。配好之后先用 headless 模式跑一次最小任务确认模型链路是通的npx deepseek-ai/dsh headless 用一句话说明什么是插件化框架如果这一步能正常返回文本说明 Base URL、Key、Model ID 三件套都对上了。如果报 401先别急着怀疑代码八成是 Key 没读到或者 Base URL 少了/api后缀——这两个是最高频的坑第 5 节会专门列。3. 可复制配置插件注册与 Profile 组装实操这一节是全文的技术核心。dsh 的「一切皆插件」不是口号它落到代码上就是一套非常具体的注册机制理解这套机制你才能自己写插件。先看底层。dsh 直接 vendor 了 Cordis 源码到vendor/cordis/并 rescope 成私有包。Cordis 的核心抽象是插件向共享上下文Context贡献服务Service、类型化事件Event和可逆副作用Effect插件之间通过ctx.xxx互相引用注册即副作用卸载自动撤销。这句话翻译成人话就是——你ctx.plugin(MyPlugin)挂上去它注册的东西就生效你卸载它它注册的东西自动清理不需要你手写dispose。一个最小插件长这样// packages/my-plugin/src/index.ts import { Context, Service } from deepseek-ai/dsh-cordis export const name my-plugin export const inject [tools] // 声明依赖Cordis 会保证 tools 先就绪 export function apply(ctx: Context) { // 注册一个工具模型就能看到它 ctx.tools.register({ name: hello_dsh, description: 返回一句问候用于验证插件注册链路, parameters: { type: object, properties: { who: { type: string, description: 问候对象 }, }, required: [who], }, async execute(args) { return { content: hello, ${args.who} } }, }) // 监听一个 waterfall 事件必须调用 next() 才能委托给下游 ctx.on(agent/pre-step, async (payload, next) { ctx.logger.info(pre-step 被触发当前输入条数:, payload.inbox.length) return next() }) }这里有两个关键点。第一inject声明依赖Cordis 会做拓扑排序保证tools服务在你apply之前就绪这解决了插件加载顺序问题。第二agent/pre-step是 waterfall 事件监听器必须调用next()才能把控制权交给下游——这正是 dsh 实现「拦截/改写」的机制。如果你不调next()就等于把这一步吞掉了后面的监听器和默认逻辑都不会执行。再看 Profile 与组合包。运行中的 dsh 是一棵由多层配置叠加出来的插件树叠放顺序是空条目列表 → profile 列出的各组合包 → profile 的cordis.patch.yml→ home 级 patch →--patchoverlay。dsh-base是每个 profile 的第一层包含模型、工具、持久化、沙箱、审批、设置、凭据、遥测dsh-web-app加浏览器应用dsh-headless提供无服务器的「跑一次就退」模式。一个自定义 profile 的配置示例# $DSH_HOME/profiles/my-profile/cordis.yml - id: dsh-base - id: dsh-headless - id: my-plugin config: greeting: 来自自定义 profile然后在启动时指定 profilenpx deepseek-ai/dsh headless --profile my-profile 调用 hello_dsh 工具问候 dsh如果你想把插件挂到已有发行树旁边而不改发行版用 patch 更合适。patch 的机制是「按 id 定位条目、替换整个 config 或插入新条目」# $DSH_HOME/cordis.patch.yml - id: dsh-base config: telemetry: enabled: false - insert: id: my-plugin after: core/toolsinsert会在core/tools之后插入你的插件这样它就能拿到已经就绪的tools服务。这套「配置即组装」的设计让「加一个能力」和「改一行配置」变成了同一件事不需要重新编译核心。最后提醒一个工程细节dsh 要求每个源文件 100% 行覆盖率CI 门禁并且每个包都必须自带一个 invariant运行时不变量断言模块。你写自己的插件时如果打算提 PR这两条是硬门槛如果只是本地玩可以忽略但建议至少给关键路径写个 invariant因为 dsh 的很多设计假设是靠 invariant 在运行时兜底的。4. 验证请求顺着调用链确认插件真的生效配置写完不算数得验证。这一节给你三条可复制的验证路径从粗到细。第一条验证插件树。dsh 提供了 inspect 能力可以打印当前运行时的 Cordis 插件树。启动 Web UI 后在对话里让 Agent 调用cordis_inspect_*系列工具这组工具默认不进任何发行树是刻意 opt-in 的需要你在 profile 里显式挂载web-cordis示例对应的插件。更简单的方式是直接用 headless 加日志级别DSH_LOG_LEVELdebug npx deepseek-ai/dsh headless --profile my-profile 列出当前已注册的工具名debug 日志里会打印插件加载顺序和每个插件注册的服务。你应该能看到my-plugin出现在core/tools之后并且hello_dsh出现在工具列表里。第二条验证工具调用链。让模型实际调用一次你注册的工具npx deepseek-ai/dsh headless --profile my-profile 请调用 hello_dsh 工具参数 who 设为 world预期结果是模型返回类似「hello, world」的内容。如果模型说「我没有这个工具」说明插件没挂上如果模型调用了但报错说明execute里有问题。这一步能同时验证「工具注册」和「模型可见」两件事。第三条验证事件链路。这是最能体现 dsh 设计的地方。前面我们在agent/pre-step里加了日志现在跑一个多步任务观察日志输出DSH_LOG_LEVELdebug npx deepseek-ai/dsh headless --profile my-profile \ 先读取 package.json再统计 dependencies 数量你会看到pre-step 被触发打印多次——因为一个 Turn 里可能有多个 Step每个 Step 前都会触发agent/pre-step。同时日志里应该能看到完整的轮次流程turn/start→agent/pre-step→step/start→agent/request→llm/stream→assistant/chunk*→assistant/message→tool/call*→tools/pre-execute→tools/execute→tools/post-execute→tool/result*→step/end→ 下一个 step →agent/turn-stopping→turn/end。这里有个值得注意的工程细节assistant/message会记录每次成功的提供方调用包括返回空内容或以 max-tokens 结束的调用。空内容不进派生历史但用量保留sourceEventSeqs精确对应 chunk 事件。这就是「模型可见即已记录」这条运行时不变式的落地——抵达模型请求的一切都必须能从会话日志重建。所以你新增一项模型可见输入时必须新增一个会话事件否则 invariant 会在运行时断言失败。如果你想更直观地看事件流可以打开 Web UI在会话面板里查看事件时间线。Web UI 是自研的 Vue 系组件库事件流是可视化的比翻日志舒服得多。启动方式还是npx deepseek-ai/dsh web然后浏览器打开http://127.0.0.1:3080。验证通过后你就完成了一次完整的「源码走读 扩展验证」闭环读懂了插件注册机制写了一个插件挂到了真实运行时并用日志确认了它在调用链里的位置。5. 常见报错排查401、local proxy failed 与 reading choices这一节按真实报错来。dsh 是 Developer Preview报错信息有时候不够友好我把踩过的坑按频率列出来。401 Unauthorized / invalid api key。这是最高频的。先确认三件事Key 是否被正确读取、Base URL 是否带了/api后缀、Model ID 是否拼对。dsh 的llm-deepseek插件默认 Base URL 是官方端点如果你用 TaoToken 网关必须显式覆盖成https://taotoken.net/api。检查方式echo $DEEPSEEK_API_KEY echo $DEEPSEEK_BASE_URL如果环境变量是空的说明你的 shell 没 source 到或者 patch 文件里的!!js process.env.DEEPSEEK_API_KEY没生效。注意!!js表达式是在 Cordis 配置解析时求值的如果进程启动时环境变量还没设置就会拿到 undefined。这种情况改成在启动命令前直接内联DEEPSEEK_API_KEYsk-xxx DEEPSEEK_BASE_URLhttps://taotoken.net/api \ npx deepseek-ai/dsh headless testlocal proxy failed / ECONNREFUSED。这个报错通常出现在你配了本地代理但代理没起来的时候。dsh 本身不内置代理它走 Node 的标准 fetch。如果你在 patch 里配了proxy字段先确认代理进程在监听。排查顺序先curl https://taotoken.net/api看网络是否通再看 patch 里有没有残留的 proxy 配置。如果你压根没配代理却报这个错检查是不是 shell 里设了HTTP_PROXY/HTTPS_PROXY环境变量Node 的 fetch 会读这两个变量。Cannot read properties of undefined (reading choices)。这个报错说明模型返回的响应体结构不符合 OpenAI 兼容格式代码在解析response.choices[0]时炸了。常见原因有两个一是 Base URL 指向了一个不兼容 OpenAI 格式的端点二是网关返回了错误页比如 HTML 的 502 页面但 HTTP 状态码是 200。排查方式是在llm/stream事件上加日志或者直接 curl 一下curl -s https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $DEEPSEEK_API_KEY \ -H Content-Type: application/json \ -d {model:deepseek-chat,messages:[{role:user,content:hi}]} | head -c 500如果返回的是 HTML 或者错误 JSON说明网关侧有问题不是 dsh 的锅。OAuth / token expired。如果你用的是需要 OAuth 的 provider比如某些企业网关dsh 的凭据管理在core/credentials插件里。token 过期后会报 OAuth 相关错误。dsh 的凭据是持久化的存在 Harness home 下清理方式是删掉对应的凭据文件后重新走一次授权流程。注意不要手动改凭据文件格式是内部约定的。插件加载顺序导致的ctx.tools is undefined。这是写插件时最容易犯的错。如果你在apply里直接用ctx.tools但没在inject里声明toolsCordis 不保证tools服务已就绪就会拿到 undefined。修法就是前面示例里的export const inject [tools]。同理依赖ctx.fs、ctx.llm都要声明。CC Switch / Cline MCP / Codex auth.json 场景。如果你是通过这些工具来管理 dsh 的模型配置记住三件套必须齐全Base URL、Key、Model ID。以 Codex 的auth.json为例它需要OPENAI_BASE_URL指向https://taotoken.net/apiOPENAI_API_KEY填 TaoToken 的 KeyModel ID 填deepseek-chat或你实际要用的模型。Cline 的 MCP 配置里baseUrl和apiKey是分开的字段别把 Key 填到 baseUrl 里。CC Switch 切换配置时确认切换后的 profile 里这三项都覆盖到了否则会出现「切了但没生效」的错觉。排障的通用思路是先确认网络层curl 通不通再确认配置层环境变量/patch 有没有生效最后确认代码层事件日志里调用链走到哪一步断了。dsh 的 debug 日志粒度很细善用DSH_LOG_LEVELdebug。6. 继续深入从源码走读到二次开发的路径走到这里你已经完成了本地运行、插件注册、调用链验证和排障四件事。如果还想继续深入我建议按这个顺序推进。先读packages/core/agent-loop/src/agent.ts大约 500 行是默认驱动器ReactLoopAgent的实现。把第 4 节日志里看到的轮次流程和这份源码对照着读你会对「步骤Step 一次模型请求 它调用的工具轮次Turn 零到多个步骤」这两个术语有具象理解。重点看agent/turn-stopping这个 serial 事件——它是唯一没有next()的检查点专门用于「是否该停」的终检这个设计在别的框架里很少见。再读packages/fs/和packages/shell/理解 seam 三件套Service Definition / Service Provider / Consumer怎么落地。文档里举的例子是文件系统与进程 Provider 共享同一个执行世界把ctx.fs指向远程沙箱E2BBash、PTY、LSP 就全部一起搬过去了。这个「一个替换所有能力一起隔离」的效果是 seam 设计最大的红利值得你亲手试一次——把fs-e2b挂上观察bash工具的执行位置变化。然后看packages/subagent/它把「委派」做成了完整的可插拔能力支持 6 种 Provider包括subagent-claude-code通过官方 Claude Agent SDK 启动真实 Claude Code和subagent-codex启动真实 Codex app-server。也就是说dsh 里的 Agent 可以把活儿委派给另一个产品当子 agent多智能体编排在 seam 抽象下变成了「选 Provider」的问题。最后是自指能力cordis_define/cordis_run/cordis_stop/cordis_inspect_*这组工具让 Agent 在运行时定义、启动、停止自己的插件。默认不进任何发行树是刻意 opt-in 的但examples/web-cordis演示了一个能检查并修改内存中 Cordis 插件树的「自指 agent」。这是 dsh 区别于其他框架的标志性能力也是理解「一切皆插件」最极致的入口。如果你在二次开发中需要频繁调用模型来测试插件行为用 TaoToken 的模型对话页面可以快速验证 prompt 和工具 schema 是否匹配https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。如果你打算长期做 Agent 方向的开发、需要稳定的编码额度可以看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的 Base URL 和参数说明。dsh 现在最大的价值是给 Agent 工程社区提供了一个极其干净的架构范本。0.1.0 的版本号意味着生态故事才刚开始dsh-plugintopic 下的第三方插件还很少这既是风险也是机会——你现在读懂的每一层设计都会在生态起来之后变成实打实的先发优势。
返回列表