尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【DeepSeek Harness 研究】用“一切皆插件”范式拆解 LLM Agent Harness 的可复制配置

【DeepSeek Harness 研究】用“一切皆插件”范式拆解 LLM Agent Harness 的可复制配置 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 URLTaoToken 的 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-llmService Provider实现接口dsh-llm-deepseek/dsh-llm-pi-ai/dsh-llm-replayConsumer消费服务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 运行时的插件树不是硬编码的而是按以下顺序叠加后应用的层按行胜出profile 的dsh.profile.bundles列表先dsh-base再各组合包按加入顺序profile 自己的cordis.patch.ymlhome 级$DSH_HOME/cordis.patch.yml每个--patch pathoverlay按 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_KEYsk-你的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.tsimport { 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 Schemadsh 会把它转成模型能理解的 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: celsiusinsert添加新条目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 UnauthorizedError: llm request failed: 401 Unauthorized根因API Key 无效或未正确注入。排查步骤确认TAOTOKEN_API_KEY环境变量已导出echo $TAOTOKEN_API_KEY检查cordis.patch.yml里apiKey的插值写法是否正确确认 Base URL 是https://taotoken.net/api没有多余路径去 https://taotoken.net/api-keys 确认 key 状态正常注意dsh 的凭据管理有独立的ctx.credentials如果你把 key 写在凭据文件里patch 里的apiKey可能被覆盖。检查加载顺序。5.2 local proxy failedError: local proxy failed: connect ECONNREFUSED 127.0.0.1:xxxx根因dsh 的某些 Provider 实现会走本地代理端口如果代理进程没起来或者端口被占就会报这个错。排查步骤确认没有其他进程占用该端口lsof -i :xxxx检查 Provider 配置里是否有proxy字段如果有确认代理服务在运行如果你不需要代理把proxy字段删掉或设为null这个报错在切换 Provider 时容易出现——旧 Provider 的代理配置残留在 patch 里新 Provider 不认。5.3 reading choicesTypeError: Cannot read properties of undefined (reading choices)根因LLM 返回的响应结构不符合预期。dsh 的 llm 适配器期望 OpenAI 格式的choices数组但实际返回可能是错误对象或空响应。排查步骤确认模型 ID 拼写正确。写错模型 ID 时某些服务返回{ error: ... }而不是标准响应检查 Base URL 是否指向了正确的 API 路径。TaoToken 的根地址是https://taotoken.net/api适配器会自动拼/v1/chat/completions打开 debug 日志看原始响应在 patch 里给 llm 条目加debug: true如果用的是自定义 Provider检查它是否正确处理了流式响应的 chunk 拼接这个报错在事故复盘里没直接出现但属于 llm seam 替换时的高频问题。5.4 OAuth 相关报错Error: OAuth token expired or invalid根因某些 Provider如 Claude Code 委派走 OAuth 认证token 过期后没刷新。排查步骤确认你用的是 API Key 模式还是 OAuth 模式。TaoToken 走 API Key不涉及 OAuth如果确实需要 OAuth检查ctx.credentials里的 token 刷新逻辑对于 subagent 委派场景subagent-claude-codeOAuth 由被委派产品管理dsh 只负责传递5.5 工具插件不生效[app-boot] entry tool-weather activated但模型就是调不到这个工具。排查步骤确认inject: [tools]已声明。没有 injectctx.tools可能是 undefined确认插件导出的是命名导出apply不是export default。事故复盘 0001 的教训检查defineTool的name字段是否与模型看到的工具名一致用--dump-config确认工具条目在合成后的配置树里检查agent/pre-step阶段有没有监听器过滤掉了工具 schema5.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 就无法被后续监听器撤销这个语义在写策略时很关键。
返回列表