一次 web_search 背后,模型、工具、会话日志与 Agent Loop 如何被动态装配?本文沿完整调用链拆解 DeepSeek Harness 的插件依赖、事件留痕与可逆副作用。
让 Agent 去搜"今日 AI 热点新闻",表面看只是一次工具调用。真正值得拆的不是搜索本身,而是这次调用之前系统已经完成了什么:模型接缝、工具注册表、系统提示词、会话日志、Web provider 和 Agent Loop 都已经被装进同一个运行时,彼此通过服务名找到对方,又能在替换或卸载时把影响撤干净。
DeepSeek Harness,命令行里叫 dsh,做的就是这层 Harness。它不是模型,也不是单纯的聊天 UI,而是把模型、工具、上下文、权限、会话记录和执行循环连成一套可运行系统的底座。它目前处在 developer preview 阶段,版本仍可能出现破坏性变更,但它的架构取向已经很明确:把 Agent 的每一块都做成插件。
这篇文章不从产品体验聊起,只盯住一个工程问题:
当一套 Agent 系统需要随时换模型、换工具、换运行模式时,怎样保证它不是一堆写死的调用链,而是一棵可以被重新装配、局部替换、失败可回滚的插件树?
图:运行时只替换受影响的插件与依赖链
先别把 dsh 看成一个固定应用
dsh 的启动过程更像一次 运行时编译。命令行入口接住参数后,并不会直接启动一套写死的业务流程,而是先把多层配置合并成一份插件装配清单,再交给 Cordis 运行时把插件逐个挂上去。
这也是 dsh 文档里那句 "Everything is a plugin" 的实际含义。模型适配器是插件,工具注册表是插件,会话日志是插件,Agent Loop 也是插件。插件之间不直接持有彼此的实现,只认运行时上下文里的服务名,比如 ctx.llm、ctx.tools、ctx.sessions、ctx.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 的配置采用分层叠加。后加载的层覆盖前面的层,同一个字段谁最后写,谁生效。
图:Bundle、Profile、机器配置与临时 patch 逐层合并
前四层主要回答"装什么":哪些插件启用,参数怎么填,某个插件是否禁用。环境变量更像最后一道总闸,回答"怎么跑":家目录放哪、是否上报、全局开关怎么取值。它不属于那摞 YAML 清单,但能压过运行时行为。
这套分层的价值不在形式,而在边界清楚。
Bundle 是产品默认值,升级时可以跟着项目走;Profile 是一套使用方式,比如 Web 模式和 Headless 模式;机器级配置处理本地差异;--patch 只管一次调试。几类改动不混在一起,插件树才能在不同机器、不同场景下稳定复现。
例如 system-prompt 插件的 persona,基础 Bundle 可以留空,Web Bundle 再填入"你是一个 coding agent"这类说明。用户自己的 profile 还可以继续覆盖它。源码不需要知道最终是哪句话,启动时合并出的插件树会给出答案。
Cordis 只做运行时,不抢业务插件的活
dsh 使用的底层运行时是 Cordis,代码随仓库放在 vendor/ 目录中。它不负责搜索网页,不负责写代码,也不决定模型该说什么。Cordis 管的是更底层的三件事:
- 插件怎样声明自己需要哪些服务。
- 某个服务上线或下线后,哪些插件要跟着启动、暂停或重连。
- 插件注册过的工具、事件、定时器、连接,卸载时怎样按原路撤销。
这就是为什么 dsh 可以同时说"没有特权业务内核",又仍然有一个 Cordis 运行时。Cordis 是骨架,不是大脑。它给插件留出服务槽位,负责依赖解析和副作用回滚,但不替任何业务插件做决策。
一个插件通常会声明 inject。如果它需要 tools 和 systemPrompt,那这两个服务没有 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,定义参数、超时、结果格式 |
依赖 tools、web、systemPrompt |
agent-loop |
推动一轮轮模型调用和工具调用 | 依赖 agents、sessions、llm、tools、systemPrompt |
这张表也是加载顺序的近似答案。零依赖插件先亮,需要服务越多的插件越晚亮。agent-loop 依赖最多,所以通常最后上线。它一亮,系统才真正有能力接住用户输入并推动完整任务。
web_search 本身还分了两层。上层 tool-web 负责让模型看到一个叫 web_search 的工具,包括名字、参数 schema、提示词说明和返回格式。下层 ctx.web 才是真正的 provider 接缝,后面可以接 DeepSeek 官方搜索,也可以换成 Exa 或 Perplexity。换搜索源时,模型侧工具定义不需要改。
这层切分很关键。模型只知道自己调用了 web_search,不需要知道背后是哪家搜索服务。工具插件只知道要调用 ctx.web.search(),不需要绑定某个 provider。provider 只负责把关键词变成搜索结果,不需要理解 Agent Loop。
从输入到答案,日志里会留下连续脚印
一次搜索任务可以被拆成八步:
图:从用户输入到最终答案,每一步都经过服务协作并留下事件记录
每个关键动作都会进入会话事件日志。用户消息是 user/message,模型流式输出是 assistant/chunk,完整回复是 assistant/message,工具请求是 tool/call,工具返回是 tool/result,一轮或一步结束还会有 step/end、turn/end。
这里要分清两个概念:
| 概念 | 含义 |
|---|---|
| Turn | 用户发出一句请求,到系统把这件事彻底处理完为止 |
| Step | Turn 里的一次"问模型 + 执行模型要求的工具" |
一个 Turn 里可以有多个 Step。模型第一次搜到的结果不够新,可以换个关键词再搜一次。③ 到 ⑤ 会循环,直到模型不再发起工具调用。
dsh 的会话日志还有一条强约束:模型能看到的信息,必须能从日志里重建。也就是说,不应该存在"发给模型了但没有记录"的上下文。这样做的收益很直接:任务可以重放,压缩后的上下文可以追溯,UI 和调试系统也能共享同一份事实来源。
默认开搜索,不默认开抓取,这不是随手配置
tool-web 包里不只有 web_search,也有 web_fetch。但发行配置默认只开搜索,fetch: false。
原因在风险边界。搜索是模型给关键词,由搜索 provider 返回候选信息;抓取是模型直接指定 URL,让系统去访问目标地址。后者更容易触碰 SSRF 等安全问题,尤其当 provider 把安全防护留给调用方时,就不适合默认开启。
这类配置说明 dsh 的插件化不是"能注册就都开"。工具注册表要承担权限、审批、参数校验和执行前后钩子。能否默认暴露给模型,是 Harness 的安全决策,不只是功能清单。
热替换能局部发生,靠的是服务名和可逆副作用
假设把 DeepSeek 模型适配器换成另一家模型。理想结果不是整套系统重启,而是只影响依赖 llm 的部分。
过程大致是:
- 旧 LLM 适配器进入
UNLOADING。 - 它注册过的 adapter 按倒序撤销。
agent-loop发现自己依赖的llm服务暂时不可用,退回PENDING并清理自身注册。- 新 LLM 适配器上线,提供新的
ctx.llmadapter。 agent-loop依赖重新满足,再次加载。
会话日志、工具注册表、系统提示词和 Web provider 不需要跟着动。它们依赖的是稳定服务契约,不是具体模型实现。
换搜索源也类似。只要 ctx.web 的服务契约不变,tool-web 注册给模型看的 web_search 就不用改。provider 从 DeepSeek 换成 Exa,影响被收在 Web provider 这一层。
这就是插件系统真正有价值的地方:替换的是局部实现,不是整台机器 。
图:模型适配器变化时,只有相关依赖进入重连
自己写插件前,先想清楚装和拆
很多工具并不值得单独做成插件。读文件、写文件、改文件,如果没有长期资源、没有后台任务、没有复杂依赖,可以放进同一个工具插件里注册多条 tool。
适合单独做插件的,通常有几个特征:
| 特征 | 说明 |
|---|---|
| 有初始化成本 | 需要创建客户端、连接池、鉴权状态或缓存 |
| 有清理责任 | 卸载时必须关闭连接、停止 watcher、清掉事件监听 |
| 有独立依赖 | 依赖其他服务,或被其他插件依赖 |
| 有替换需求 | 将来可能整包换掉实现,比如搜索 provider、沙箱、模型适配器 |
写插件时最重要的不是 apply(ctx) 里注册了什么,而是每一次注册是否都有对应的撤销路径。Cordis 自带的 ctx.on、ctx.tools.register、ctx.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 操作电脑时,真正需要的不是截图,而是界面语义