AI

DeepSeek Harness 深度解析:一切皆插件的 Agent 运行时与自定义开发指南

从 Agent 主循环到能力缝架构,从前端面板定制到接入私有模型,一篇文章讲清 DeepSeek Harness 的设计与扩展方式

2026-08-16AI 辅助写的0 次浏览

一、这是什么:一切皆插件的 Agent 运行时

DeepSeek Harness 是一个基于 Cordis 插件框架构建的 Agent 运行时(monorepo,npm scope 为 @deepseek-ai/dsh-*)。它最核心的理念是:没有特权核心。模型适配器、工具注册表、会话日志、甚至 Agent 主循环本身,全部都是插件,挂载到共享的 Context 上——任何部分都可以从配置层替换,而不需要改主循环代码。

这种设计带来一个直接的好处:作为使用者,你几乎所有的定制需求——换模型、加工具、改 UI、定义子智能体、定制每会话的 Agent 组合——都不需要 fork 代码,只需要往对应的扩展点注册实现。

架构分层一览:

二、两个基石设计模式

2.1 能力缝(Capability Seam)

每类可替换能力都是一个「三角色组合」,缺一不可:

  • Service Definition:拥有 ctx.<key> 的抽象 Service 类(如 LlmRuntimeShellExecutor),定义接口词汇;
  • Service Provider:一个或多个实现该接口的插件;
  • Consumer:注入服务并使用它的插件(通常是面向模型的工具)。

典范是 shell 家族:dsh-shell(Definition,ctx.shell)→ dsh-bash-local / dsh-pwsh-local(Provider)→ dsh-tool-bash(Consumer)。扩展插件只依赖 Definition,绝不依赖具体 Provider——这正是可替换性的来源。

2.2 Cordis 事件四模式

插件之间通过类型化事件协作,所有事件用 TypeScript declaration merging 声明:

模式语义典型事件
emit通知,不等待agent/createdsession/event
waterfall中间件链,可改写结果;listener 必须调 next() 否则短路agent/requesttools/pre-execute
parallel并行等待所有 listenersession/flush
serial按序等待agent/turn-stopping

所有注册(工具、监听器、适配器)都通过 ctx.effect() / ctx.on() 安装,插件卸载时自动回收——注册是可逆副作用

三、Agent 是怎么写的

Agent 分两层:packages/core/agent 提供抽象(Agent 接口、AgentRegistry),packages/core/agent-loop 提供实现(ReactLoopAgent 主循环驱动器)。

3.1 主循环:事件驱动的命令式状态机

入口是 Inboxfollowup() / steer() / inject() 把消息排入队列,每次变更都持久化为 agent/inbox/spliced 事件。wakeDriver() 唤醒处于 idle 相位的驱动器,进入 turn()

  1. 组装提示:每个 step 先调 ctx.systemPrompt.assemble()(sections/contexts/variables 分层合并,带 system-prompt/assemble waterfall),从 event-sourced 的 session surface 派生历史消息;
  2. pre-step 裁决agent/pre-step waterfall——插件可拒绝或替换进入 step 的消息;
  3. 请求配置agent/request waterfall——插件可改写本次请求的 LLM 配置;
  4. 流式请求:每个 chunk 落 assistant/chunk 日志,支持 token 级重放;
  5. 错误接管:失败走 agent/request-error waterfall,listener 返回 {kind:'retry'} 即接管重试;
  6. 工具调度executeToolCalls()exclusive 工具构成 barrier,parallel 工具走有界并发池;派发可重叠但结果严格按模型顺序提交;每个调用经过 tools/pre-execute(审批门)→ tools/execute(超时/重试包装)→ tools/post-execute(结果改写)三段 waterfall;
  7. turn 收尾agent/turn-stopping serial(listener 可 steer() 追加工作),然后 turn/end

3.2 持久化:事件溯源

Session 是 append-only 事件日志(turn/*step/*user/messageassistant/*tool/* 等),核心不变量是**「模型可见 ⟺ 可从日志重建」**。新增模型可见输入必须通过 declaration merging 扩展 SessionEventMap。真正的落盘由订阅 session/event / session/flush 的持久化插件完成(JSONL/SQLite provider),主循环本身不关心存储。

3.3 组合:preset 机制

一个 preset = 一个含 agent.cordis.yml 的目录(目录名即 id)。每个 preset 在进程内只挂载一次(standing mount),Agent 创建时通过 scope 父子绑定加入:工具、prompt section 向下继承,事件向上传播。子 Agent 通过 composeFrom() 继承父 Agent 的同一代组合。apps/cli/config/agent-presets/ 下的 standard / code / minimal 就是现成例子——minimal 甚至演示了用 isolate: { fs: true } 里的本地 fs provider 遮蔽宿主沙箱 provider 的写法。

四、自定义前端面板:client 插件 + slot 系统

内置 Web GUI(apps/web + packages/client/*,React 18)本身就是插件化组合的,扩展点是 slot 系统ctx.slots.register)。想在某处加一个 tab,标准做法如下

  1. 新建 client 插件包package.json 声明 dsh.clientplatform: 'web'),经 exports["./client"] 导出构建好的 bundle,在部署的 cordis.yml 中加一行组合它;
  2. 向 slot 注册组件:在 apply() 里调用 ctx.slots.register({ name, ... }, Component)

常用 slot 入口:

Slot效果
'conversation.view'会话主区的视图标签页环——Chat、Trajectory 都是其中一页,注册 { id, order, label } 即投影为新 tab
'settings.section' / 'settings.plugins.tab'设置页加分区 / 加标签页
'conversation.session.header.actions'会话标题栏按钮
'conversation.input.dock'输入区 dock(TodoDock 在此)
'conversation.composer'chain 式接管输入区(审批面板的实现方式)
'tool.call.toolview'按工具名定制工具卡片

约束:一个 UI 功能一个包;跨包不直接 import 符号,只走 slot 或 ctx 服务。工具侧无需碰前端代码——presentCall / presentResult 返回纯函数的 render intent(generic / terminal / diff / search / web + locations 文件跳转),各 UI 自行映射渲染。

如果不想进入这套 React/slot 体系,也可以走 packages/sdk(stdio JSON-RPC,有 TS 和 Python client)自写前端,但拿不到工具卡片、富审批 UI 等能力;ACP 则明确只面向自动化场景。

五、接入自己的模型提供商

LLM 是标准能力缝:LlmRuntimectx.llm)是 Definition,LlmAdapter 是要实现的抽象类——唯一必需的方法是 stream(options)(产出 StreamChunk,尊重 signal),可选覆写 resolveModel()listModels()providerRetryPolicy()

路线 A:零代码(内部网关是 OpenAI 兼容协议时)

直接用现有 adapter:

  • llm-deepseekbaseURL 指向内部网关;
  • 或在 llm-pi-ai 的 settings 里手写声明路由:baseURL + api: openai-completions + apiKeyEnv + 模型清单。

API key 通过 credentials 能力管理:配置里只放环境变量名引用,值由 ctx.credentials 每次请求时解析,改 key 免重启

路线 B:自写 provider 插件

仿照 packages/llm/llm-deepseek,两个核心文件:

  1. src/adapter.tsclass MyAdapter extends LlmAdapter,实现 stream(),请求必须带 attributionHeaders()
  2. src/index.ts:cordis 插件,inject = ['llm']apply() 里:
    • ctx.llm.registerAdapter(['my-route'], adapter)
    • ctx.llm.registerConfigurableProviders([...])(让设置页能发现)
    • 可选 installSettingsSection() 支持热更新;
  3. 宿主 cordis.yml 加一行 { id: llm-mine, name: '<你的包>', config: {...} } 即完成装配。

另有 llm/stream waterfall 可在不碰 adapter 的情况下做全局重试、路由、replay。

六、子智能体:原生支持且可插件化扩展

packages/subagent 是完整能力缝:

  • DefinitionSubagentRuntimectx.subagents),命名 provider 注册表 + start(name, request) 一次性委派 + continuable 系列(followup / interrupt / reportFrom);
  • Provider:内置 spawn(全新子 Agent)、fork(以父会话前缀为 seed),以及进程外的 acp / codex / claude-code / dsh-sdk 后端。自定义后端 = 实现 SubagentProvider 接口 + ctx.subagents.registerProvider(),一行配置即可被所有 preset 按名选用;
  • Consumerdsh-tool-subagent 注册 subagent 工具,config 指定 providertoolNamebackgroundModeagentOptions(子 Agent 的模型路由)、persona(scoped 覆盖)、toolFiltermaxDepth(默认 3)。

preset 里可挂多个实例各指不同 provider——standard preset 就同时挂了 spawn / fork / codex / claude-code 四个(后两个默认 disabled)。子 Agent 自动继承父 Agent 的 preset 组合。

七、开发速查表

需求做法
加模型 provider实现 LlmAdapter 注册到 ctx.llm(或零代码配 llm-pi-ai 路由)
加模型工具ctx.tools.register(defineTool({...})),声明 render intent
加 UI 面板 / 标签页client 插件 + ctx.slots.register'conversation.view' 等)
加持久状态declaration merging 扩展 SessionEventMap
拦截请求 / 工具 / 轮次监听 agent/requesttools/pre-executeagent/turn-stopping 等 waterfall
加子智能体后端实现 SubagentProvider 注册到 ctx.subagents
定制每会话 Agent$DSH_HOME/.agent-presets/<id>/agent.cordis.yml 写 preset
限定单 Agent 的注册agent.ctx(scope 机制,同名注册遮蔽全局)

配套文档:docs/architecture.md(架构总纲)、docs/cordis-primer.md(框架机制)、docs/cookbook/(加包 / 工具 / LLM adapter / Chat 节点的分步指南)、packages/client/AGENTS.md(UI 插件约束)。

结语

这个 harness 的一切扩展——换模型、加工具、改 UI、定义子智能体、定制每会话组合——都不需要改主循环。要做的只是:往对应的能力缝注册 Provider、往事件 waterfall 挂监听器、往 slot 注册组件、或往 preset 写一份 agent.cordis.yml。开发时先确定你的需求属于哪条缝,再按三角色(Definition / Provider / Consumer)的既有范例动手即可。

Last updated on

On this page