
字节笔记本
2026年10月6日 · 约 9 分钟读完
DeepSeek Harness Web 访问层的设计取舍
DeepSeek Harness(dsh)是 DeepSeek 开源的 agent harness 框架,MIT 协议,目前处于开发者预览阶段,架构上主打一切皆插件,底层由 Cordis 插件框架驱动。它把模型需要的联网能力收敛成一个独立的 Web 访问能力接缝(seam):在同一个 ctx.web 服务上横跨 search 与 fetch 两项操作,再拆分到多个包。服务定义在 dsh-web,提供 ctx.web 与提供方注册表;提供方包括搜索侧的 dsh-web-search-exa、dsh-web-search-perplexity、dsh-web-search-deepseek,以及抓取侧的 dsh-web-fetch-http;唯一的消费方是 dsh-tool-web,也就是模型看到的 web_search 与 web_fetch 两个工具的 schema 所在地。
Web 是一项可选能力,不在 agent loop 主干上,因此它的类型与词汇定义在 dsh-web 里,而不是核心子系统。类型入口在 packages/web/web/src/types.ts。项目文档对这套设计的取舍写得很清楚,下面逐条拆解。

一项能力,两种操作
搜索与抓取既不共享请求 schema,也不共享业务逻辑,却被有意放进同一个 ctx.web 中间层。收益有三点:它是提供方选择策略的唯一所有者;它提供一套统一的中止与错误词汇;它给产品留出一个本框架如何访问 Web 的配置界面。代价是服务上并存 search 与 fetch 两条平行方法对,文档明确说明这是有意为之,不是漏掉了可抽取的共性。
分工上,提供方注册的是能力(WebSearchProvider 或 WebFetchProvider),而不是工具;面向模型的名称、schema、提示词引导与结果展示,全部集中在唯一的消费方 dsh-tool-web。由此换来一条稳定承诺:更换搜索提供方,不会改变模型提交查询的方式;更换抓取提供方,也不会改变模型请求 URL 的方式。
搜索:模型只给一个 query
面向模型的工具参数只有一个 query。maxResults 是消费方自己的上限(dsh-tool-web 的 searchMaxResults 配置,默认 8),经接缝透传,并在返回时强制执行:提供方若返回超量,接缝会截断 sources 数组并把 truncated 置为 true。
interface WebSearchResult {
/** 提供方生成的回答或摘要,可选。 */
readonly content?: string
/** 可引用来源,已被截断到 maxResults。 */
readonly sources: readonly WebSearchSource[]
/** 接缝为满足 maxResults 丢弃超量来源时为 true。 */
readonly truncated: boolean
}content 是可选字段:Exa 与 DeepSeek 不返回生成式答案,Perplexity 会返回。来源条目里只有 url 是必有字段,title、snippet、publishedAt 一律可选,理由写得很直白:并非每个提供方都提供这些信息,强迫适配器编造会让接缝撒谎,Perplexity 的引用有时只有 URL。展示层的兜底规则是 title 缺省时渲染主机名。
抓取:非 2xx 是结果,不是错误
抓取请求被刻意压缩到只剩一个 url,超时、格式、提示词、抽取控制一概不进请求;取消作为执行参数单独传入(AbortSignal),展示与更高层的语言模型关注点被挡在安全检索之外。
HTTP 状态码被视为被抓取资源状态的一部分,不自动当作失败:一次网络层面的成功抓取,即使拿到 404 或 500,也会产出包含状态码与长度受限解码正文的 WebFetchResult。结果里的 url 是经过允许的重定向之后的最终 URL。WebError 只保留给无法安全获取或表示资源的情况。
interface WebFetchResult {
readonly url: string
readonly statusCode: number
readonly body: WebFetchBody
readonly truncated: boolean
}
type WebFetchBody =
| { readonly kind: 'html'; readonly content: string }
| { readonly kind: 'text'; readonly content: string }body 是一个封闭的判别联合,归 dsh-web 所有:提供方负责解码出 kind,工具层负责渲染。新增一种 kind 属于跨已知包的协同变更,不是插件可以自行扩展的点;所有消费方用 switch 加 assertNever 消费它,新增 kind 而未处理就会编译报错。每个分支保持独立对象字面量,即使字段暂时重合,为的是某个分支将来能长出自己的字段。
提供方选择:与注册顺序无关
提供方的 available() 是一次廉价的本地检查:凭证是否存在、配置是否可解析,禁止发起网络调用。它是执行时选择提供方的输入,不是健康检查系统。
选择规则从不依赖注册顺序、配置顺序或 HMR 热更新顺序:配置里给了显式 id(searchProvider 或 fetchProvider,或填充同一字段的对应环境变量)就按 id 解析;没给 id 且恰好只有一个可用提供方,就自动选它;存在多个可用提供方却没配 id,直接抛 WEB_PROVIDER_AMBIGUOUS,绝不悄悄选用最先注册的那个。注册阶段遇到重复 id 会抛 WEB_DUPLICATE_PROVIDER,与模型适配器运行时处理重复适配器的思路一致。

错误码是开放式字符串
WebError 继承自核心的 HarnessError,code 字段是开放式 string,与 LlmError、SubagentError 的做法一致:提供方可以在不修改 dsh-web 的前提下抛出自己的错误码,消费方必须容忍未知代码。
错误码按所有者划分。共享运行时抛出的有:WEB_PROVIDER_UNAVAILABLE、WEB_PROVIDER_CONFIGURED_MISSING、WEB_PROVIDER_CONFIGURED_UNAVAILABLE、WEB_PROVIDER_AMBIGUOUS、WEB_DUPLICATE_PROVIDER、WEB_ABORTED,以及兜底的 WEB_PROVIDER_ERROR,DNS 失败、连接被拒、TLS 握手失败等网络与传输故障都从这里出去。抓取传输层的错误码归 dsh-web-fetch-http 所有,其他抓取后端无需抛出:WEB_INVALID_URL、WEB_BLOCKED_URL、WEB_REDIRECT_BLOCKED、WEB_FETCH_TOO_LARGE、WEB_FETCH_TIMEOUT、WEB_UNSUPPORTED_CONTENT_TYPE。
安全边界值得记住
本地抓取后端只接受 HTTP 与 HTTPS、拒绝携带凭证、限制重定向次数、字节数、字符数与总时长,并对每一次同源重定向跳转重新做安全校验,最后才解码正文,展示交给工具层。文档特别提醒:本地后端不会拦截私有网络目标,在能够触及敏感内部目标的环境里,不要启用 web_fetch。
小结
这份子系统文档最值得借鉴的是它对边界的克制:该合并的合并,一个服务承载两种操作;该拆开的拆开,能力归提供方,展示归工具层;该封闭的封闭,body 判别联合加编译期断言;该开放的开放,错误码是不设限的字符串。仓库 deepseek-ai/deepseek-harness 以 MIT 协议开源,npm 包名为 @deepseek-ai/dsh,处于开发者预览阶段,接口仍可能变化,想跟源码的读者从 packages/web 目录入手最直接。



