deepseek-harness 学习手册
动手实验室Lab 02 · 零 fork 换掉一个 provider
20 min

零 fork 换掉一个 provider

把默认的 DeepSeek 官方搜索换成 Exa,全程不改仓库一行代码,也不 fork。

最后核对于 v0.1.1-rc.2 · commit b150a551b8 · 2026-08-22

M2 说「换后端只要改配置」。这一课就是去验证这句话——把默认的 DeepSeek 官方搜索换成 Exa,全程不改仓库任何一行代码,也不 fork

做完你手上会多出一份属于自己的 profile 补丁层,以及一个「层序谁赢」的直觉。

开始前请确认

1 先看清现在挂的是谁

terminal
$ dsh --profile web --dump-config
输出(节选)yaml
# == @deepseek-ai/dsh-base
- id: web
  name: '@deepseek-ai/dsh-web'
  config:
    searchProvider: deepseek-official      ← 就是这一行

注意那行 # == @deepseek-ai/dsh-base 注释:它告诉你这一段来自 base 组合包。你等下写的补丁会盖在它上面——同一个 id,后写的赢。

动手之前,先把整段抄下来

patch 是替换整个 config,不是合并。如果 web 那一行还有别的字段(比如 fetchProvider),你没写的都会消失。先用第 1 步的 dump 抄全,再改。

2 把 Exa provider 装进 profile

provider 就是一个普通的 npm 包。dsh plugin 会把它写进 profile 的依赖:

terminal
$ dsh plugin --profile web add @deepseek-ai/dsh-web-search-exa
$ export EXA_API_KEY=...

dsh plugin 做的事很朴素:<args...> 原样转发给 pnpm,工作目录设成 profile 目录——add / remove / why / update 每个 pnpm 动词都照常工作(pnpm 必须在 PATH 上)。

每次成功之后它还会做一次对账:每一个解析出来的依赖,只要它的 manifest 声明了 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },就自动加入层栈;没有这个声明的依赖保持为普通包,并给一次性警告。

为什么需要这一步:本站核对时确认,@deepseek-ai/dsh 这个 CLI 的 62 个生产依赖里没有 web-search-exa——树内有这个包,但它不在默认安装闭包里。所以你确实得装。待核对:不同发行版的闭包可能不同,装之前可以先 dsh plugin --profile web why @deepseek-ai/dsh-web-search-exa 看一眼。

3 写你自己的补丁层

这就是全部的「改动」。两段——一段把 seam 的选路改成 exa,一段把 provider 挂上去:

~/.dsh/profiles/web/cordis.patch.ymlyaml
- id: web
  config:
    searchProvider: exa

- insert:
    - id: web-search-exa
      name: '@deepseek-ai/dsh-web-search-exa'
      config:
        searchType: neural
        apiKey: !!js process.env.EXA_API_KEY

两种条目形态,别搞混:

  • id-targeted(第一段):定位一个已存在的行,替换它整个 config
  • insert(第二段):加一行本来不存在的。Exa provider 不在 base 里,所以必须 insert。

apiKey 留空的话,这个 provider 自己会回落到 $EXA_API_KEY——源码里就是 config.apiKey ?? launchEnvironmentOf(ctx).get('EXA_API_KEY')?.value ?? ''。所以上面那行 !!js 其实可以省掉。写出来是为了让你看见 !!js 长什么样:在插件 config 和条目的 disabled 字段里可以用 !!js两个叹号,不是一个),其它元数据保持字面量。

4 再 dump 一次,确认你这一层赢了

terminal
$ dsh --profile web --dump-config
输出(节选)yaml
# == profile cordis.patch.yml
- id: web
  config:
    searchProvider: exa                    ← 你的这一层赢了

然后启动 dsh --profile web,问一个需要联网的问题,工具轨迹里就是 Exa 了。

仓库代码改动行数:0。cordis.patch.yml 删掉,一切原样回退。

卡住了?

症状先查这个
dump 里根本没有 web 这一行你的 profile 可能没叠 base 组合包。看 package.json 里的 dsh.profile.bundles
改了但没生效层序是:各 bundle → profile 的 patch → home 级 patch--patch overlay。home 级排在 profile 后面,所以它更大。检查是不是被 ~/.dsh/cordis.patch.yml 盖了。
启动时报错说服务重复注册你可能同时挂了两个占同一个 key 的 provider。这是设计内的大声失败——base bundle 的 README 就拿 bash/pwsh 举过例:两个 executor 家族注册同一个 bash 服务,不完整的切换配方会在加载时失败。
patch 指向的 id 找不到不会崩,但会往 stderr 打一行警告,带上是哪一层。看你的 stderr。
provider 静默不可用Exa 的 apiKey 为空时这个 provider 就是不可用的;baseURL 解析不了也一样。先确认环境变量真的传进去了。
对比即教学

同样这件事,在别家要几步?

dsh 2 段 配置。核心零改动,随时删掉补丁层回退。旧实现彻底消失,不会跟新的并存。
OpenCode fork 或走上游 PR。搜索实现在 server 侧,plugin hook 包不住它。
待核对
Codex / Claude Code +1 工具 经 MCP 加一个新工具。内置那个还在,模型要自己选
待核对
这三格不是在分高下:「+1 个工具」在很多场景下是更省事的答案——它不需要 seam、不需要 provider 注册表、不需要一份接口契约。只有当你需要旧实现彻底消失、而不是并存时(比如你不想让模型在两个搜索工具之间乱挑),seam 的价值才真正兑现。
下一课

Lab 3 · 写一个最小的树外插件——一个 prompt section,或者一个工具。45 分钟。

目录

本页

Lab 02 · 零 fork 换掉一个 provider