☰
【DeepSeek Harness 研究】用“一切皆插件”范式拆解 LLM Agent Harness 的可复制配置
2026/10/4 20:15:29 网站建设 项目流程

1. 从一次工具调用失败说起:LLM Agent Harness 到底在管什么

如果你正在自建 LLM Agent,大概率遇到过这种场景:模型明明输出了正确的 tool-call,参数也对,但执行结果回注上下文后,下一轮模型却像没看见一样重复调用同一个工具。排查半天发现不是模型的问题,而是 harness 在工具结果规范化那一步把content字段吞掉了。

这就是 LLM Agent Harness 存在的意义。它不是模型,也不是简单的 function calling 封装,而是承载 agent「推理—行动—观察」循环的运行时基础设施。DeepSeek Harness(下称 dsh)是 DeepSeek AI 开源的一套 agent harness 框架,核心范式叫「一切皆插件」(everything is a plugin)。它把模型适配、工具执行、会话持久化、沙箱安全、人机审批、Web 界面全部做成可插拔的插件树,底层跑在 Cordis 框架的「时空可组合性」编程模型上。

适合谁看:想自建 LLM Agent Harness 的开发者、正在评估 agent 框架选型的技术负责人、以及被「工具注册后不生效」「插件热更新残留旧实例」这类问题折磨过的人。这篇不聊虚的架构图,直接给可复制的插件注册配置片段,演示新增一个工具插件后的完整验证动作,把「一切皆插件」落到你能跑起来的代码上。

我试过把一个自定义的天气查询工具接进 dsh,中间踩了插件加载顺序和 seam 替换的坑,下面按步骤拆开讲。

2. TaoToken 前置准备:给 Harness 接一个稳定的模型入口

dsh 的ctx.llm是一个可替换的 seam,默认适配器指向 DeepSeek 官方接口。但在实际开发中,你经常需要切换模型、做 A/B 对比、或者给 agent 配一个独立的调用通道。这时候用 TaoToken 作为模型接入层会比较省事——它提供 OpenAI 兼容的 API 格式,dsh 的 llm 适配器可以直接对接。

先说清楚 TaoToken 是什么:它是一个大模型 API 聚合服务,提供统一的调用入口,支持多种模型。对 dsh 来说,你只需要把它当成一个 OpenAI 兼容的 endpoint 就行。

第一步:拿到 API Key

访问 https://taotoken.net/api-keys 创建密钥。注意这个页面是 deep link,直接进到 key 管理界面,不用在首页找入口。创建后复制那串sk-开头的 key,后面配置要用。

第二步:确认 Base URL

TaoToken 的 API 根地址是https://taotoken.net/api。注意这里不加任何 UTM 参数,就是纯 API 地址。dsh 的 llm 适配器配置里填这个。

第三步:选模型 ID

在模型对话页面可以查看当前可用的模型列表。dsh 默认用deepseek-v4-flash,你也可以换成其他模型做对比测试。记住模型 ID 的准确写法,配置里写错会直接报 404。

第四步:理解 dsh 的 llm seam 结构

dsh 的 llm 能力是一个典型的 seam 三角色结构:

角色职责dsh 中的实现
Service Definition声明接口,拥有ctx.llm键dsh-llm
Service Provider实现接口dsh-llm-deepseek/dsh-llm-pi-ai/dsh-llm-replay
Consumer消费服务dsh-agent-loop

你要做的是替换 Provider,或者在现有 Provider 的 config 里改 baseURL 和 apiKey。这两种方式后面都会给配置片段。

第五步:确认凭据存储方式

dsh 有独立的凭据管理(ctx.credentials),不要把 API Key 硬编码在cordis.patch.yml里。正确做法是通过$DSH_HOME下的凭据文件或者环境变量注入。这一点在事故复盘 0002 里有教训——!!js表达式只在插件 config 内求值,写在条目元数据里会被拒绝。

前置准备到这里就够了。接下来进入正题:插件注册与加载。

3. 可复制配置:插件注册、加载与新增工具插件

这一节是全文的核心。我会给出完整的 JSON/TOML/settings 片段,路径与 dsh 仓库原文一致,你可以直接复制到自己的 profile 里。

3.1 理解 profile 与 bundle 的叠加顺序

dsh 运行时的插件树不是硬编码的,而是按以下顺序叠加(后应用的层按行胜出):

  1. profile 的dsh.profile.bundles列表(先dsh-base,再各组合包按加入顺序)
  2. profile 自己的cordis.patch.yml
  3. home 级$DSH_HOME/cordis.patch.yml
  4. 每个--patch <path>overlay(按 argv 顺序)

关键语义:一条 patch 按id定位条目并替换其整个 config,而不是深度合并。这意味着你必须重述需要保留的每个字段。这个设计避免了隐式继承的歧义,但初次配置容易踩坑。

3.2 配置 LLM 适配器指向 TaoToken

在$DSH_HOME/profiles/web/cordis.patch.yml里添加或修改 llm 条目:

# $DSH_HOME/profiles/web/cordis.patch.yml - id: llm-deepseek config: baseURL: "https://taotoken.net/api" apiKey: "${TAOTOKEN_API_KEY}" model: "deepseek-v4-flash" timeout: 60000 maxRetries: 2

注意apiKey用了环境变量插值。dsh 的!!js表达式只在插件 config 内求值,${VAR}这种写法是安全的。启动前确保TAOTOKEN_API_KEY已导出:

export TAOTOKEN_API_KEY="sk-你的key"

如果你用的是 headless profile,路径换成$DSH_HOME/profiles/headless/cordis.patch.yml,配置内容一样。

3.3 注册一个工具插件

dsh 的工具注册走ctx.tools注册表。新增一个工具插件需要三样东西:工具定义(JSON Schema)、执行体、以及注册副作用。

先看工具插件的package.json,关键是dsh字段声明:

{ "name": "dsh-tool-weather", "version": "0.1.0", "type": "module", "main": "lib/index.js", "dsh": { "bundle": { "patch": "cordis.patch.yml" } }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*" } }

然后是插件入口src/index.ts:

import { defineTool } from '@deepseek-ai/dsh-tools' import type { Context } from '@deepseek-ai/cordis' export const name = 'dsh-tool-weather' export const inject = ['tools'] export function apply(ctx: Context) { ctx.tools.register( defineTool({ name: 'get_weather', description: '查询指定城市的当前天气', parameters: { type: 'object', properties: { city: { type: 'string', description: '城市名称,如 北京、上海' }, unit: { type: 'string', enum: ['celsius', 'fahrenheit'], default: 'celsius' } }, required: ['city'] }, async execute(args, exec) { exec.signal.throwIfAborted() const resp = await fetch( `https://api.example.com/weather?city=${encodeURIComponent(args.city)}&unit=${args.unit}`, { signal: exec.signal } ) if (!resp.ok) { return { isError: true, content: `天气服务返回 ${resp.status}` } } const data = await resp.json() return { content: `${args.city} 当前 ${data.temp}°${args.unit === 'celsius' ? 'C' : 'F'},${data.desc}` } } }) ) }

几个关键点:

  • inject: ['tools']声明依赖,Cordis 会保证ctx.tools在apply执行前已就绪。
  • defineTool的parameters是标准 JSON Schema,dsh 会把它转成模型能理解的 tool schema。
  • execute的第二个参数exec携带signal,用于超时和取消。必须在耗时操作前调用throwIfAborted(),否则工具流水线的 timeout 包装层无法正确中断。
  • 返回值content是字符串,dsh 会在finalizeContent阶段做内容不变式检查。

3.4 把工具插件挂进插件树

工具插件写好了,怎么让它出现在运行中的 dsh 里?两种方式。

方式一:作为 bundle 加入 profile

在你的 profilepackage.json里声明依赖,然后在dsh.profile.bundles列表里加上:

{ "name": "my-dsh-profile", "dsh": { "profile": { "bundles": [ "dsh-base", "dsh-web-app", "dsh-tool-weather" ] } }, "dependencies": { "dsh-tool-weather": "workspace:*" } }

方式二:用 patch 直接 insert

如果你不想改 profile 的 bundles 列表,可以在cordis.patch.yml里 insert:

# $DSH_HOME/profiles/web/cordis.patch.yml - insert: - id: tool-weather plugin: dsh-tool-weather config: defaultUnit: celsius

insert添加新条目,id是这条目的唯一标识,plugin指向包名。注意 patch 的config会整体替换,不是合并。

3.5 验证配置合成结果

配置写完后,别急着启动。先用--dump-config看合成后的完整配置树:

dsh --profile web --dump-config

输出里会带# == 层名注释,标出每个条目来自哪一层。检查你的tool-weather条目是否出现、config 是否正确、有没有被后面的层覆盖。

如果看到assertEntriesLoaded或assertEntriesActivated报错,说明有条目「已启用但未加载/未激活」。这通常是inject声明缺失或者插件导出形态不对——事故复盘 0001 就是export default丢了inject导致的。

4. 验证请求:新增工具插件后的完整验证动作

配置合成通过后,进入验证阶段。这一节给出从启动到看到工具调用结果的完整动作,以及每一步的预期输出。

4.1 启动 dsh 并确认插件加载

dsh --profile web

启动日志里应该能看到类似输出:

[app-boot] mounting include tree... [app-boot] entry tool-weather loaded [app-boot] entry tool-weather activated [app-boot] all entries loaded and activated

如果tool-weather没出现在 loaded 列表里,回到 3.5 检查--dump-config输出。

4.2 用 headless 模式跑一次工具调用

Web UI 适合交互调试,但验证工具插件用 headless 更快:

dsh --profile headless "查一下北京现在的天气"

预期输出会包含工具调用和结果:

[turn/start] [step/start] [agent/request] -> llm/stream [assistant/message] tool_call: get_weather({ city: "北京", unit: "celsius" }) [tool/call] get_weather [tools/pre-execute] allow [tools/execute] executing... [tools/post-execute] accept [tool/result] 北京 当前 12°C,晴 [assistant/message] 北京现在 12 摄氏度,天气晴。 [turn/end]

看到[tool/result]后面跟着你的工具返回内容,说明整条流水线跑通了。

4.3 检查会话日志确认事件溯源

dsh 的会话日志是唯一真源。找到会话文件:

ls $DSH_HOME/sessions/

用jq看事件序列:

cat $DSH_HOME/sessions/<session-id>.jsonl | jq -c '{type, seq}'

你应该能看到tool/call和tool/result成对出现,seq单调递增无缺口。如果seq有缺口,load会拒绝重建——这是持久化后端的硬性不变量。

4.4 验证工具结果回注模型

关键验证点:模型是否真的「看到」了工具结果。检查assistant/message事件里是否包含工具返回的内容。如果模型在下一轮重复调用同一个工具,说明结果没回注成功。

排查方向:看tools/post-execute阶段有没有监听器把结果replace或block掉了。dsh 的 post-execute 是 waterfall,监听器可以改写结果。

4.5 测试插件热替换

dsh 支持 HMR。修改工具插件的description字段,保存后观察日志:

[hmr] reloading dsh-tool-weather [hmr] disposing old instance [hmr] entry tool-weather activated

然后重新跑一次 headless 请求,确认新 description 生效。如果旧实例的注册残留了,说明插件没有正确使用ctx.effect()注册副作用——Cordis 的可逆副作用机制要求所有注册随插件卸载自动撤销。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

这一节对照真实报错,给出排查路径。每个报错都标注了根因和修复动作。

5.1 401 Unauthorized

Error: llm request failed: 401 Unauthorized

根因:API Key 无效或未正确注入。

排查步骤:

  1. 确认TAOTOKEN_API_KEY环境变量已导出:echo $TAOTOKEN_API_KEY
  2. 检查cordis.patch.yml里apiKey的插值写法是否正确
  3. 确认 Base URL 是https://taotoken.net/api,没有多余路径
  4. 去 https://taotoken.net/api-keys 确认 key 状态正常

注意:dsh 的凭据管理有独立的ctx.credentials,如果你把 key 写在凭据文件里,patch 里的apiKey可能被覆盖。检查加载顺序。

5.2 local proxy failed

Error: local proxy failed: connect ECONNREFUSED 127.0.0.1:xxxx

根因:dsh 的某些 Provider 实现会走本地代理端口,如果代理进程没起来或者端口被占,就会报这个错。

排查步骤:

  1. 确认没有其他进程占用该端口:lsof -i :xxxx
  2. 检查 Provider 配置里是否有proxy字段,如果有,确认代理服务在运行
  3. 如果你不需要代理,把proxy字段删掉或设为null

这个报错在切换 Provider 时容易出现——旧 Provider 的代理配置残留在 patch 里,新 Provider 不认。

5.3 reading choices

TypeError: Cannot read properties of undefined (reading 'choices')

根因:LLM 返回的响应结构不符合预期。dsh 的 llm 适配器期望 OpenAI 格式的choices数组,但实际返回可能是错误对象或空响应。

排查步骤:

  1. 确认模型 ID 拼写正确。写错模型 ID 时,某些服务返回{ error: ... }而不是标准响应
  2. 检查 Base URL 是否指向了正确的 API 路径。TaoToken 的根地址是https://taotoken.net/api,适配器会自动拼/v1/chat/completions
  3. 打开 debug 日志看原始响应:在 patch 里给 llm 条目加debug: true
  4. 如果用的是自定义 Provider,检查它是否正确处理了流式响应的 chunk 拼接

这个报错在事故复盘里没直接出现,但属于 llm seam 替换时的高频问题。

5.4 OAuth 相关报错

Error: OAuth token expired or invalid

根因:某些 Provider(如 Claude Code 委派)走 OAuth 认证,token 过期后没刷新。

排查步骤:

  1. 确认你用的是 API Key 模式还是 OAuth 模式。TaoToken 走 API Key,不涉及 OAuth
  2. 如果确实需要 OAuth,检查ctx.credentials里的 token 刷新逻辑
  3. 对于 subagent 委派场景(subagent-claude-code),OAuth 由被委派产品管理,dsh 只负责传递

5.5 工具插件不生效

[app-boot] entry tool-weather activated

但模型就是调不到这个工具。

排查步骤:

  1. 确认inject: ['tools']已声明。没有 inject,ctx.tools可能是 undefined
  2. 确认插件导出的是命名导出apply,不是export default。事故复盘 0001 的教训
  3. 检查defineTool的name字段是否与模型看到的工具名一致
  4. 用--dump-config确认工具条目在合成后的配置树里
  5. 检查agent/pre-step阶段有没有监听器过滤掉了工具 schema

5.6 配置片段速查

如果你在排查过程中需要重新对齐配置,这里给一份完整的 settings 片段,包含 Base URL、Key、Model ID 三件套:

{ "llm": { "provider": "deepseek", "baseURL": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "model": "deepseek-v4-flash", "timeout": 60000, "maxRetries": 2 }, "tools": { "weather": { "enabled": true, "defaultUnit": "celsius" } } }

这份配置对应$DSH_HOME/profiles/web/cordis.patch.yml的 llm 和 tools 条目。如果你用的是 Codex 的auth.json格式,字段名会不同,但 Base URL、Key、Model ID 这三个核心字段的语义是一样的。

6. 把 Harness 跑起来之后:接入文档与后续路径

配置跑通、工具插件验证通过之后,下一步通常是把它接到真实工作流里。这里给几条路径,按你的场景选。

如果你在排障或做接入:TaoToken 的接入文档在 https://taotoken.net/doc,里面有各语言的调用示例和错误码说明。API Keys 管理在 https://taotoken.net/api-keys。这两个页面是排障时最常回的。

如果你想先验证模型效果再决定接哪个:模型对话页面 https://taotoken.net/chat 可以直接试。不用写代码,输入 prompt 看返回,确认模型 ID 和响应格式符合预期后再往 dsh 里接。

如果你要做长期编码或 Agent 开发:Coding Plan 页面 https://taotoken.net/coding-plan 有面向持续开发场景的套餐说明。dsh 这类 agent harness 的调用量通常比普通对话大——一次任务可能触发几十次模型请求,选套餐时把工具调用的轮次算进去。

如果你要深入 dsh 的插件开发:控制台 https://taotoken.net/console 可以看调用日志和用量统计。调试工具插件时,对照控制台的请求记录和 dsh 的会话日志,能快速定位是 harness 层的问题还是模型层的问题。

最后说一个实际经验:dsh 的插件树在启动时按序叠加,patch 的「整体替换」语义意味着你每次改配置都要重述完整字段。我一开始图省事只写了要改的字段,结果其他字段被清空,排查了半天。后来养成习惯,改任何条目之前先--dump-config看当前完整 config,改完再 dump 一次对比。这个习惯能省掉大部分「配置写了但不生效」的问题。

工具插件跑通之后,你可以试着把tools/pre-execute的守卫加上,做一个「危险命令拦截」策略插件。这是理解 dsh 安全模型最好的练习——守卫是单调的,一旦 deny 就无法被后续监听器撤销,这个语义在写策略时很关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询