
1. 从一次失败的 Agent 任务说起为什么需要可组合运行时你可能遇到过这种场景同一个 DeepSeek 模型在 A 产品里能顺利改完一个跨 5 个文件的 bug换到 B 产品里却连测试都跑不起来。模型没变任务没变变的是模型外面那层东西——工具怎么暴露、上下文怎么裁剪、Shell 权限给到哪、失败之后重试几次。这层东西DeepSeek Harness 把它叫做 Harness并且给出了一个很直接的等式Model Harness Agent。DeepSeek Harness命令行工具叫 dsh是 DeepSeek 开源的一套 Agent 运行时框架核心主张是 Everything is a plugin。它没有把 Agent loop、模型适配器、会话持久化、沙箱这些系统脊柱写死在框架内核里而是拆成 Cordis 插件和 capability seam让使用者可以声明、替换、组合甚至回滚。Cordis 是配套的组件模型来自 DeepSeek 与北大团队关于时空可组合性的研究工作提供了可逆副作用和响应式依赖两套生命周期语义。这篇文章适合谁正在自建 Agent 平台、需要把模型调用和工具执行拆成可替换模块的开发者想在同一套运行时里做多模型 A/B 对比的评测同学以及已经用过 Claude Code、Cursor想搞清楚可组合运行时到底比可换模型深在哪里的工程团队。我会给出可复制的插件注册配置、运行时目录结构、一次最小 Agent 任务的验证步骤以及通过 TaoToken 统一 Key 和 API 通道接入模型调用的具体做法。全程按能跟着敲的标准写踩过的坑也会标出来。需要先明确一点dsh 目前仍是 developer preview版本迭代快配置字段可能变。下面的配置以 0.1.0-rc 系列为基线你实际跑的时候以dsh --version和官方仓库当前文档为准。但目录结构和 seam 的组织方式是这个框架相对稳定的设计意图理解了它版本升级时你改的是参数而不是思路。2. TaoToken 前置把模型调用收敛成一条可替换的通道在讲插件注册之前先把模型调用这条链路理清楚。因为 Harness 的第一个 capability seam 就是 LLM provider——它把模型从哪来抽象成一个可替换节点。如果你在配置里硬编码某个厂商的 endpoint 和 key那可替换就只是口号。更实际的做法是用一个统一的 OpenAI 兼容通道承接模型调用Harness 侧只认 Base URL Key Model ID 三件套换模型时改 Model ID 就行。TaoToken 在这里扮演的就是这个统一通道。它的 API 地址是https://taotoken.net/api兼容 OpenAI 的/v1/chat/completions和/v1/models接口格式所以任何支持自定义 Base URL 的 Agent 框架都能直接接。对 Harness 来说你只需要在 provider 配置里填三个值Base URL 指向 TaoTokenAPI Key 用你在控制台生成的令牌Model ID 填你要用的模型名。先拿 Key。打开https://taotoken.net/console注册后在 API Keys 页面创建一个令牌。建议按用途分 Key一个给本地调试一个给 CI 或评测任务这样出问题时能快速定位是哪条链路在消耗额度。创建完复制出来形如sk-开头的一串字符只显示一次丢了就重新生成。拿到 Key 之后先别急着配 Harness用一条 curl 验证通道本身是通的curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: deepseek-chat, messages: [{role: user, content: 回复 ok 两个字母即可}], max_tokens: 16 }返回里能看到choices[0].message.content就说明通道没问题。这一步很关键因为后面 Harness 报错时你需要能区分是通道不通还是Harness 配置错。把这条 curl 存成一个check.sh每次改配置前先跑一遍能省掉大量排查时间。关于模型选择TaoToken 的模型列表可以通过https://taotoken.net/api/v1/models拉取你也可以在https://taotoken.net/models页面直接看当前可用的模型和对应的 Model ID。做 Harness 实验时建议固定一个模型 ID 作为基线比如deepseek-chat或deepseek-reasoner等基线跑稳了再换模型做对比。这样你的实验变量才是干净的。还有一点Harness 的 provider seam 支持配置多个 provider你可以同时挂 TaoToken 和别的通道然后在不同 preset 里指定用哪个。这正是可替换的价值——不是只能换而是换的时候不用改工具层和 loop 层的代码。3. 可复制配置插件注册、目录结构与 settings 片段这一节是全文最需要动手的部分。我按目录结构 → 插件注册 → provider 配置 → preset 选择的顺序给每一段都能直接复制。3.1 运行时目录结构dsh 的工作目录建议这样组织把配置、插件、会话日志、任务集分开后面做 A/B 和回放时不会乱my-agent-runtime/ ├── dsh.config.toml # 主配置provider、preset、sandbox ├── plugins/ │ ├── llm-taotoken.ts # LLM provider 插件 │ ├── fs-remote.ts # 文件系统 provider可替换为远程 │ └── shell-persistent.ts # Shell provider ├── presets/ │ ├── minimal.toml # 最小基线 preset │ └── standard.toml # 通用 preset ├── sessions/ # append-only SessionEvent 日志落盘 ├── tasks/ # 评测任务集 └── .env # 存放 TAOTOKEN_API_KEY勿提交sessions/目录是重点。Harness 把会话定义成 append-only 的 typed SessionEvent 事件流resume、replay、fork 都从这条流推导。你把它落盘保留后面做同模型不同 Harness的对比时可以直接从某个 turn boundary 分叉不用重跑整个任务。3.2 插件注册配置Cordis 的插件注册核心是定义—实现—消费三段关系。下面是一个 LLM provider 插件的注册片段用 TOML 声明路径和字段名按你的实际仓库调整# dsh.config.toml [plugins.llm] # 声明这是一个 capability seam 的 provider kind provider seam ctx.llm entry ./plugins/llm-taotoken.ts [plugins.llm.options] base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读不写死在配置里 default_model deepseek-chat timeout_ms 120000 max_retries 2 [plugins.fs] kind provider seam ctx.fs entry ./plugins/fs-remote.ts [plugins.shell] kind provider seam ctx.shell entry ./plugins/shell-persistent.ts # 消费方Agent loop 依赖上面三个 seam [plugins.agent_loop] kind consumer entry ./plugins/agent-loop.ts depends_on [ctx.llm, ctx.fs, ctx.shell]这里的关键是seam字段。它把 provider 和消费者解耦agent_loop 只声明我需要 ctx.llm不关心背后是 TaoToken 还是别的通道。你替换plugins.llm的 entry只要新 provider 实现了同一个 seam 接口agent_loop 不用改一行代码。对应的 provider 实现骨架TypeScript大致长这样// plugins/llm-taotoken.ts import type { Context } from cordis export const name llm-taotoken export const inject [ctx.llm] export function apply(ctx: Context, config: Recordstring, any) { const baseUrl config.base_url const apiKey process.env[config.api_key_env] // 注册到 ctx.llm 这个 capability seam ctx.llm.provide({ async chat(messages, options) { const res await fetch(${baseUrl}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey}, }, body: JSON.stringify({ model: options.model ?? config.default_model, messages, temperature: options.temperature ?? 1.0, top_p: options.top_p ?? 0.95, }), }) if (!res.ok) throw new Error(llm http ${res.status}) return res.json() }, }) // 可逆副作用插件卸载时自动撤销注册 ctx.on(dispose, () ctx.llm.revoke(llm-taotoken)) }注意最后那个ctx.on(dispose, ...)。这就是 Cordis 的可逆副作用语义——插件被卸载时它注册的 capability 会被一并撤销不会留下幽灵 provider。对 Agent 来说这点比普通 IDE 插件重要得多因为残留的 provider 会让后续每一步模型请求都受不可见影响实验就没法复现了。3.3 preset 配置preset 决定模型看到哪些工具、上下文怎么管、执行怎么编排。先建一个 minimal 基线# presets/minimal.toml name minimal tools [persistent_bash, str_replace_editor] context_injection false compaction false sandbox_mode workspace-write再建一个 standard# presets/standard.toml name standard tools [fs, shell, search, web, subagent] context_injection true compaction { strategy summarize, trigger_ratio 0.8 } sandbox_mode workspace-writeminimal 的价值不是功能少而是变量少。它给模型的工具恰好是持久 bash 和编辑器关掉上下文注入和压缩适合做控制基线。standard 则用更大的能力面换通用性代价是工具 schema 多、往返次数多、上下文面大。3.4 环境变量# .env TAOTOKEN_API_KEYsk-你的令牌启动前source .env或让 dsh 自己加载。不要把 Key 写进dsh.config.toml提交到仓库用api_key_env引用环境变量这是最低限度的凭据卫生。4. 验证请求跑通一次最小 Agent 任务配置写完用一次最小任务验证整条链路。选一个读文件 改一行 跑测试的任务足够短出问题好定位。4.1 准备任务仓库mkdir -p /tmp/dsh-demo cd /tmp/dsh-demo git init cat calc.py EOF def add(a, b): return a - b # 故意写错 EOF cat test_calc.py EOF from calc import add def test_add(): assert add(2, 3) 5 EOF4.2 启动 dsh 并指定 presetcd my-agent-runtime dsh run \ --preset presets/minimal.toml \ --config dsh.config.toml \ --workspace /tmp/dsh-demo \ --task 修复 calc.py 里的 add 函数让 test_calc.py 通过4.3 观察成功结果跑通后你会看到类似输出[session] created sess_01H... [llm] providerllm-taotoken modeldeepseek-chat [tool] str_replace_editor read calc.py [tool] str_replace_editor replace line 2 [tool] persistent_bash pytest -q [result] 1 passed in 0.12s [session] closed, events14关键看三行providerllm-taotoken说明模型调用走的是 TaoToken 通道str_replace_editor说明工具来自 minimal preset1 passed说明任务闭环完成。events14是这次会话写入sessions/的事件数后面 replay 和 fork 都基于它。4.4 验证会话可回放dsh session replay sessions/sess_01H....jsonl --from-turn 3能重新打印第 3 轮之后的工具调用和模型响应说明 append-only 事件流是完整的。这一步是后面做组件级 A/B 的前提——没有可回放的事件流同模型不同 Harness的对比就只能靠重跑前置随机性会把差异淹没。4.5 换模型做一次对比把dsh.config.toml里的default_model从deepseek-chat改成deepseek-reasoner其他不动重跑同一个任务dsh run --preset presets/minimal.toml --config dsh.config.toml \ --workspace /tmp/dsh-demo \ --task 修复 calc.py 里的 add 函数让 test_calc.py 通过因为 provider seam 没变、工具没变、preset 没变这次差异只来自模型。这就是同 Harness 不同模型的干净对比。反过来固定模型只换 presetminimal → standard就是同模型不同 Harness的对比。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth配置和验证跑下来最容易撞的是下面几类报错。我按真实报错信息给排查路径。5.1 401 Unauthorized[llm] error: llm http 401九成是 Key 的问题。先跑第 2 节那条 curl如果 curl 也 401说明 Key 本身无效或过期去https://taotoken.net/api-keys重新生成。如果 curl 通但 dsh 报 401检查api_key_env指向的环境变量名是否和.env里的一致以及 dsh 进程有没有真正加载到那个变量。常见坑是.env写了但没source或者用了export但启动 dsh 的是另一个 shell。5.2 local proxy failed[llm] error: local proxy failed: connect ECONNREFUSED 127.0.0.1:xxxx这个报错说明你的配置里还残留着指向本地代理的 Base URL。检查dsh.config.toml里base_url是不是被某次调试改成了http://127.0.0.1:...。正确值应该是https://taotoken.net/api。另外检查系统环境变量里有没有HTTP_PROXY/HTTPS_PROXY指向一个已经关掉的本地端口有的话清掉再启动。5.3 reading choices of undefinedTypeError: Cannot read properties of undefined (reading choices)这是 provider 实现里直接取res.choices[0]但响应结构不对导致的。两种可能一是 Base URL 少了/v1或多了/v1导致请求打到了错误路径返回的不是标准 chat completion 结构二是模型 ID 写错服务端返回了错误对象而不是正常响应。先在 provider 里加一行console.log(JSON.stringify(res).slice(0, 200))看实际返回长什么样。TaoToken 的 chat 接口路径是https://taotoken.net/api/v1/chat/completions确认你的拼接逻辑没重复或漏掉/v1。5.4 OAuth 相关报错error: oauth token expired / oauth flow not supported如果你在 provider 配置里误用了需要 OAuth 的鉴权方式会撞这个。TaoToken 走的是 Bearer Token不需要 OAuth 流程。检查plugins/llm-taotoken.ts里是不是混进了别的鉴权分支或者dsh.config.toml里有没有auth_type oauth之类的字段。统一改成Authorization: Bearer $TAOTOKEN_API_KEY即可。5.5 三件套对照表出现上面任何一类报错先按这张表核对三件套能覆盖大部分情况配置项正确值常见错误Base URLhttps://taotoken.net/api写成http://127.0.0.1:...或漏/v1API Keysk-开头从 console 生成用了过期 Key 或环境变量没加载Model IDdeepseek-chat等从 models 页确认拼写错误或用了不存在的模型名排查顺序建议先 curl 验通道再查环境变量再看 provider 实现里的 URL 拼接最后看 preset 有没有覆盖 provider 配置。这个顺序能让你在五分钟内定位到是哪一层的问题。6. 把模型产品拆成可替换模块下一步怎么走跑通最小任务之后你可以开始做真正的可组合实验了。几个方向值得试。第一把文件系统和 Shell 的 provider 一起指向远程执行后端。Harness 的设计强调 fs 和 subprocess 应共享同一个执行世界——如果 Bash 在容器里跑、文件编辑却落在宿主机就会出现状态不一致。你替换plugins.fs和plugins.shell的 entry让它们指向同一组远程 provideragent_loop 不用改。这是 capability seam 相对普通插件系统最实在的价值。第二用 session fork 做组件级 A/B。从同一个 turn boundary 分出两个子 sessionA 保持原策略B 只换 compaction 策略比较后续成功率、token 和耗时。因为前置轨迹完全一致差异归因比重新跑整个任务干净得多。第三把 Code Mode 纳入对比。Code Mode 让模型写编排代码而不是逐次点工具理论上减少模型—工具往返。但它对模型的程序合成能力要求更高弱模型可能反而更不稳。在你的真实任务集上用同一模型分别跑 native tools 和 Code Mode看完成率、总 token、wall-clock 三个指标而不是只看工具调用次数下降了多少。第四成本按 Cost per Resolved Task 算而不是 token 单价。公式是全部任务模型费用 执行基础设施费用÷ 成功任务数。一个单次便宜但经常跑偏、需要人接管的 Agent未必比单价高但稳定完成的更省钱。TaoToken 的用量可以在控制台按 Key 维度看方便你把成本归到具体实验上。需要提醒的是dsh 仍是 developer preview动态 Cordis 插件这类能力接近运行时给 Agent 一个 bash 级权限生产环境开放前必须有插件白名单、来源校验和审批审计。可组合性扩大能力面的同时也在扩大风险面这一点在自建运行时的时候要提前想清楚。如果你还没开始建议就按第 3 节的目录结构和第 4 节的验证步骤走一遍先把 minimal 基线跑稳再逐步加 preset 和 provider。配置和 Key 都在https://taotoken.net/api-keys和https://taotoken.net/doc能找到对应说明接入文档里有各语言的最小示例对着改 provider 实现很快。