AIGC标识 DeepSeek Harness 为什么能热换模型:插件依赖、事件日志与回滚机制

一次 web_search 背后,模型、工具、会话日志与 Agent Loop 如何被动态装配?本文沿完整调用链拆解 DeepSeek Harness 的插件依赖、事件留痕与可逆副作用。

让 Agent 去搜“今日 AI 热点新闻”,表面看只是一次工具调用。真正值得拆的不是搜索本身,而是这次调用之前系统已经完成了什么:模型接缝、工具注册表、系统提示词、会话日志、Web provider 和 Agent Loop 都已经被装进同一个运行时,彼此通过服务名找到对方,又能在替换或卸载时把影响撤干净。

DeepSeek Harness,命令行里叫 dsh,做的就是这层 ​Harness​。它不是模型,也不是单纯的聊天 UI,而是把模型、工具、上下文、权限、会话记录和执行循环连成一套可运行系统的底座。它目前处在 developer preview 阶段,版本仍可能出现破坏性变更,但它的架构取向已经很明确:把 Agent 的每一块都做成插件。

这篇文章不从产品体验聊起,只盯住一个工程问题:

当一套 Agent 系统需要随时换模型、换工具、换运行模式时,怎样保证它不是一堆写死的调用链,而是一棵可以被重新装配、局部替换、失败可回滚的插件树?
inline-01.png

图:运行时只替换受影响的插件与依赖链

先别把 dsh 看成一个固定应用

dsh 的启动过程更像一次 ​运行时编译​。命令行入口接住参数后,并不会直接启动一套写死的业务流程,而是先把多层配置合并成一份插件装配清单,再交给 Cordis 运行时把插件逐个挂上去。

这也是 dsh 文档里那句 “Everything is a plugin” 的实际含义。模型适配器是插件,工具注册表是插件,会话日志是插件,Agent Loop 也是插件。插件之间不直接持有彼此的实现,只认运行时上下文里的服务名,比如 ctx.llmctx.toolsctx.sessionsctx.systemPrompt

可以把整个系统竖着切成六层:

层级 负责什么 典型内容
Launcher 接住 dsh 命令,解析 profile、patch 和启动参数 CLI、启动参数、profile 选择
Composition 决定这次装哪些插件,以及每个插件用什么参数 Bundle、Profile、cordis.patch.yml
Runtime 让插件发现服务、声明依赖、注册可撤销副作用 Cordis Context、Service、Event、Effect
Agent Plane 组织提示词、工具和模型调用,推动任务一步步执行 LLM、Tools、System Prompt、Agent Loop
Data Plane 记录会话事件,并从事件里重建模型上下文 Session Event Log、Projection、持久化
Surface 把同一套运行时暴露成不同入口 Web、Client、Headless

插件真正活跃在中间四层。Composition 决定装谁,Runtime 保证装得上和拆得掉,Agent Plane 与 Data Plane 负责执行和留痕。Launcher 和 Surface 更像入口与出口。

配置不是参数表,而是插件树的输入

dsh 的配置采用分层叠加。后加载的层覆盖前面的层,同一个字段谁最后写,谁生效。
mermaid-01.png

图:Bundle、Profile、机器配置与临时 patch 逐层合并

前四层主要回答“装什么”:哪些插件启用,参数怎么填,某个插件是否禁用。环境变量更像最后一道总闸,回答“怎么跑”:家目录放哪、是否上报、全局开关怎么取值。它不属于那摞 YAML 清单,但能压过运行时行为。

这套分层的价值不在形式,而在边界清楚。

Bundle 是产品默认值,升级时可以跟着项目走;Profile 是一套使用方式,比如 Web 模式和 Headless 模式;机器级配置处理本地差异;--patch 只管一次调试。几类改动不混在一起,插件树才能在不同机器、不同场景下稳定复现。

例如 system-prompt 插件的 persona,基础 Bundle 可以留空,Web Bundle 再填入“你是一个 coding agent”这类说明。用户自己的 profile 还可以继续覆盖它。源码不需要知道最终是哪句话,启动时合并出的插件树会给出答案。

Cordis 只做运行时,不抢业务插件的活

dsh 使用的底层运行时是 Cordis,代码随仓库放在 vendor/ 目录中。它不负责搜索网页,不负责写代码,也不决定模型该说什么。Cordis 管的是更底层的三件事:

  1. 插件怎样声明自己需要哪些服务。
  2. 某个服务上线或下线后,哪些插件要跟着启动、暂停或重连。
  3. 插件注册过的工具、事件、定时器、连接,卸载时怎样按原路撤销。

这就是为什么 dsh 可以同时说“没有特权业务内核”,又仍然有一个 ​Cordis 运行时​。Cordis 是骨架,不是大脑。它给插件留出服务槽位,负责依赖解析和副作用回滚,但不替任何业务插件做决策。

一个插件通常会声明 inject。如果它需要 toolssystemPrompt,那这两个服务没有 active 之前,它就不会加载。Cordis 会把插件状态放在 fiber 上,常见状态包括:

状态 含义
PENDING 等待依赖服务上线
LOADING 插件正在初始化
ACTIVE 插件已经提供服务或完成注册
FAILED 配置校验或启动过程失败
UNLOADING 正在执行清理逻辑
DISPOSED 已移除,不能重新启动

最容易误判的是 PENDING。它不是错误,只是依赖没齐。如果插件装了但没有反应,先看 fiber 状态,比直接翻异常日志更快。

一次 web_search 实际上由六类插件配合完成

用户输入“帮我搜一下今日 AI 热点新闻”以后,模型能调用 web_search,不是因为某个巨大的控制器写死了搜索逻辑,而是几类插件提前完成了各自的注册。

角色 提供什么 依赖关系
system-prompt 带顺序的提示词 section 拼装器 无核心依赖
session 只追加的会话事件日志 依赖类型与持久化基础设施
llm 模型消息协议与适配器接缝 适配器另行注册
tools 工具注册、schema 暴露、执行前后把关 依赖 systemPrompt
tool-web 注册 web_search,定义参数、超时、结果格式 依赖 toolswebsystemPrompt
agent-loop 推动一轮轮模型调用和工具调用 依赖 agentssessionsllmtoolssystemPrompt

这张表也是加载顺序的近似答案。零依赖插件先亮,需要服务越多的插件越晚亮。agent-loop 依赖最多,所以通常最后上线。它一亮,系统才真正有能力接住用户输入并推动完整任务。

web_search 本身还分了两层。上层 tool-web 负责让模型看到一个叫 web_search 的工具,包括名字、参数 schema、提示词说明和返回格式。下层 ctx.web 才是真正的 provider 接缝,后面可以接 DeepSeek 官方搜索,也可以换成 Exa 或 Perplexity。换搜索源时,模型侧工具定义不需要改。

这层切分很关键。模型只知道自己调用了 web_search,不需要知道背后是哪家搜索服务。工具插件只知道要调用 ctx.web.search(),不需要绑定某个 provider。provider 只负责把关键词变成搜索结果,不需要理解 Agent Loop。

从输入到答案,日志里会留下连续脚印

一次搜索任务可以被拆成八步:
mermaid-02.png

图:从用户输入到最终答案,每一步都经过服务协作并留下事件记录

每个关键动作都会进入会话事件日志。用户消息是 user/message,模型流式输出是 assistant/chunk,完整回复是 assistant/message,工具请求是 tool/call,工具返回是 tool/result,一轮或一步结束还会有 step/endturn/end

这里要分清两个概念:

概念 含义
Turn 用户发出一句请求,到系统把这件事彻底处理完为止
Step Turn 里的一次“问模型 + 执行模型要求的工具”

一个 Turn 里可以有多个 Step。模型第一次搜到的结果不够新,可以换个关键词再搜一次。③ 到 ⑤ 会循环,直到模型不再发起工具调用。

dsh 的会话日志还有一条强约束:模型能看到的信息,必须能从日志里重建。也就是说,不应该存在“发给模型了但没有记录”的上下文。这样做的收益很直接:任务可以重放,压缩后的上下文可以追溯,UI 和调试系统也能共享同一份事实来源。

默认开搜索,不默认开抓取,这不是随手配置

tool-web 包里不只有 web_search,也有 web_fetch。但发行配置默认只开搜索,fetch: false

原因在风险边界。搜索是模型给关键词,由搜索 provider 返回候选信息;抓取是模型直接指定 URL,让系统去访问目标地址。后者更容易触碰 SSRF 等安全问题,尤其当 provider 把安全防护留给调用方时,就不适合默认开启。

这类配置说明 dsh 的插件化不是“能注册就都开”。工具注册表要承担权限、审批、参数校验和执行前后钩子。能否默认暴露给模型,是 Harness 的安全决策,不只是功能清单。

热替换能局部发生,靠的是服务名和可逆副作用

假设把 DeepSeek 模型适配器换成另一家模型。理想结果不是整套系统重启,而是只影响依赖 llm 的部分。

过程大致是:

  1. 旧 LLM 适配器进入 UNLOADING
  2. 它注册过的 adapter 按倒序撤销。
  3. agent-loop 发现自己依赖的 llm 服务暂时不可用,退回 PENDING 并清理自身注册。
  4. 新 LLM 适配器上线,提供新的 ctx.llm adapter。
  5. agent-loop 依赖重新满足,再次加载。

会话日志、工具注册表、系统提示词和 Web provider 不需要跟着动。它们依赖的是稳定服务契约,不是具体模型实现。

换搜索源也类似。只要 ctx.web 的服务契约不变,tool-web 注册给模型看的 web_search 就不用改。provider 从 DeepSeek 换成 Exa,影响被收在 Web provider 这一层。

这就是插件系统真正有价值的地方:​替换的是局部实现,不是整台机器​。
inline-02.png

图:模型适配器变化时,只有相关依赖进入重连

自己写插件前,先想清楚装和拆

很多工具并不值得单独做成插件。读文件、写文件、改文件,如果没有长期资源、没有后台任务、没有复杂依赖,可以放进同一个工具插件里注册多条 tool。

适合单独做插件的,通常有几个特征:

特征 说明
有初始化成本 需要创建客户端、连接池、鉴权状态或缓存
有清理责任 卸载时必须关闭连接、停止 watcher、清掉事件监听
有独立依赖 依赖其他服务,或被其他插件依赖
有替换需求 将来可能整包换掉实现,比如搜索 provider、沙箱、模型适配器

写插件时最重要的不是 apply(ctx) 里注册了什么,而是每一次注册是否都有对应的撤销路径。Cordis 自带的 ctx.onctx.tools.registerctx.systemPrompt.section 都会返回可撤销效果。定时器、文件监听、数据库连接这类运行时不知道的资源,需要自己包进 ctx.effect()

一个典型的工具注册逻辑会长这样:

ctx.tools.register(defineTool({
  name: 'web_search',
  description: 'Search the web for current information.',
  parameters: {
    query: {
      type: 'string',
      required: true,
      description: 'The search query.',
    },
  },
  timeoutMs,
  isConcurrencySafe: () => true,
  async execute(args, exec) {
    const input = parseSearchArgs(args)
    const result = await ctx.web.search(
      { query: input.query, maxResults },
      exec.signal,
    )
    return {
      ...result,
      sources: result.sources.map(projectSource),
    }
  },
}))

这段代码真正依赖的不是某家搜索服务,而是 ctx.web 这个接缝。只要接缝不变,provider 就可以换。

这套架构的收益和代价

dsh 的插件系统换来的是替换成本下降。模型、搜索源、会话存储、工具包、Agent Loop 都可以被配置或插件替换,而调用方只依赖服务契约。

代价也很明确。

第一,排查问题的入口变了。以前主要看调用栈,现在要先看插件状态。PENDING 不一定报错,依赖缺失也可能只是静默等待。

第二,写插件要同时考虑上线和下线。只写注册不写撤销,平时可能没事,热重载或局部替换时就会留下幽灵工具、旧监听器或失效连接。

第三,依赖关系本身变成设计工作。inject 写错,插件可能永远等不到依赖;两个插件互相依赖,还可能一起卡在 PENDING

这些都不是模型能力问题,而是 Harness 工程问题。

结语

DeepSeek Harness 最值得看的地方,不是它眼下作为 Coding Agent 是否已经足够顺手,而是它把 Agent 系统拆成了可以装配、可以替换、可以撤销影响的一组运行时组件。

一次 web_search 看上去只是“模型调用了搜索工具”。拆开以后能看到更底层的机制:配置层编译插件树,Cordis 管依赖和副作用,工具注册表暴露能力,会话日志保留事实,Agent Loop 把模型与工具推成一个 Turn。

未来的 Agent 不会只靠更长 Prompt 和更多工具变强。它们需要一套能承受长期运行、局部升级和失败恢复的 Harness。dsh 现在还早,但它已经把这个问题摆得很具体:Agent 的能力不是一团粘在一起的代码,而应该是一棵随时能重组、又不丢失连续性的插件树。

推荐阅读

DeepSeek Harness:Agent 自我改进之前,先让 Harness 可观察、可组合、可回滚

别只给 AI 产品接模型:Agent 能做成事,靠的是策略和 Harness

AX Tree:Agent 操作电脑时,真正需要的不是截图,而是界面语义

拆解 Opus 5 提示词:顶级 Agent 是被设计成可靠的

MCP 这次协议升级,真正改的是 Agent 基础设施的伸缩方式

aaa_compressed_under_1M.png

posted @ 2026-08-18 10:41  AI小老六  阅读(186)  评论(0)    收藏  举报