DeepSeek Harness 结构化错误分类体系:基于 HarnessError 的跨 Seam 错误路由与分类实践
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
本文深入解析 DeepSeek Harness 中一项已落地(implemented)的架构决策——结构化错误分类体系(Structured Error Taxonomy)。该方案以@deepseek-ai/dsh-llm叶子包中的HarnessError基类为枢纽,为工具执行、Agent 循环与会话事件提供端到端可机器路由的稳定code,让插件无需对消息做子串匹配即可分支处理 ENOENT 与 EACCES 这类差异。阅读本文后,你将掌握:HarnessError的设计动机与源码实现、LlmError/ToolArgsError的继承方式、结构化错误如何穿过工具注册表与tool/result会话事件保留在日志中,以及不规范throw如何被包装为携带UNKNOWNcode 的可路由错误。该设计记录源自仓库 Agent Note 2026-06-11-structured-error-taxonomy.zh.md。
背景:故障跨越 Seam 时的信息丢失问题
在引入分类体系之前,Harness 中的故障在跨越系统边界(seam)时退化为裸字符串。具体表现为三类问题:
- 工具错误被扁平化:工具抛出的错误被压缩成一个文本块,
name、code和stack全部丢失。这让未来的沙箱/重试插件无法区分ENOENT(文件不存在)与EACCES(权限拒绝),模型也得不到本可提供的更具可操作性的反馈。 - 非 Error 的 throw 退化更严重:Agent 循环(agent loop)将不规范抛出值包装为
new Error(String(x)),丢弃了其中携带的全部 code。 - 缺少共享基类:
LlmError是系统中唯一的类型化错误,没有公共基类,消费方无法对其做通用的instanceof判断,也就无法编写跨模块的通用错误处理逻辑。
这一问题的本质是:错误信息在传输过程中丢失了结构,机器无法路由,只能靠人类读文本。
核心决策:在 dsh-llm 叶子包中引入 HarnessError 基类
决策的关键约束是不引入新的依赖边。方案选择在dsh-llm(一个所有其他包都已依赖的叶子包)中定义HarnessError extends Error基类,使全仓库包只需一条 import 语句即可共享错误分类,代价几乎为零。
HarnessError 的源码实现
基类位于 packages/llm/llm/src/error.ts:
export class HarnessError extends Error { /** 稳定的、可机器路由的失败类别(例如 RATE_LIMIT);永远基于此路由,绝不解析 message */ readonly code: string constructor(message: string, code: string, options?: ErrorOptions) { super(message, options) this.code = code this.name = new.target.name } }基类承载三个核心语义:
- 稳定的
code:与人类可读的message分离,作为程序化路由的稳定标识。源码注释明确要求"route on this, never by parsingmessage"。 cause链:通过标准ErrorOptions支持错误原因链式连接,便于保留底层失败上下文。name默认取子类名:通过new.target.name自动获得,无需子类手动设置。
类型收窄函数 isHarnessError
同文件还导出了类型守卫 isHarnessError:
export function isHarnessError(value: unknown): value is HarnessError { return value instanceof HarnessError }它只在 seam(运行时边界)处做instanceof收窄,且注释明确:只有真实实例才收窄,duck-typing 或跨 realm 的错误不会通过,避免误判。
配套的规范错误码常量
同文件定义了一组提供商中立(provider-neutral)的规范 code 常量,用于统一不同提供商在同类失败上的差异:
| 常量 | code 值 | 语义 |
|---|---|---|
CONTEXT_WINDOW_EXCEEDED_CODE | CONTEXT_WINDOW_EXCEEDED | 请求超出模型上下文窗口 |
QUOTA_EXCEEDED_CODE | QUOTA | 账户配额或余额耗尽(终态,区别于瞬时限流) |
EMPTY_RESPONSE_CODE | EMPTY_RESPONSE | 响应正常结束但无任何内容块,适配器将其归类为失败而非空消息 |
INVALID_CREDENTIAL_CODE | INVALID_CREDENTIAL | 凭据存在但格式错误/不可用(区别于缺失),修复方式是更正存储值而非补充 |
这些常量定义于 packages/llm/llm/src/error.ts。与之配套的isContextWindowExceededError/isQuotaExceededError分类器(同文件第 80、94 行)通过正则识别 OpenAI 兼容提供商与库适配器的措辞,将文本分类到上述稳定 code——这正是"文本分类只在边界做一次、之后全部路由 code"的设计体现。
子类实践:LlmError 与 ToolArgsError
LlmError:携带可序列化失败事实的 LLM 专属错误
LlmError定义于 packages/llm/llm/src/index.ts,继承HarnessError并保留了既有的 code(如AUTH、RATE_LIMIT、NO_ADAPTER)。它在基类之上增加了可序列化的事实字段failure,并通过LlmErrorOptions接受三个经过严格校验的提供商边界事实:
status:提供商边界观测到的合法 HTTP 状态码(必须是 100~599 的整数,否则构造即抛错);providerRetryAfterMs:提供商要求的重试延迟毫秒数(必须是正有限数);requestId:非空的提供商请求 ID。
构造时若message或code为空字符串同样直接抛错。failure对象通过Object.freeze冻结,确保错误事实在跨 seam 传递中不可变。
ToolArgsError:工具参数校验错误
ToolArgsError定义于 packages/core/tools/src/schema.ts,继承HarnessError并携带固定 codeINVALID_ARGS:
export class ToolArgsError extends HarnessError { readonly violations: string[] constructor(violations: string[]) { super(`invalid arguments: ${violations.join('; ')}`, 'INVALID_ARGS') this.name = 'ToolArgsError' this.violations = violations } }它保留了既有 code(INVALID_ARGS)和行为,同时将逐条 schema 违规明细保存在violations数组中,供代码消费。
结构化错误如何穿过工具注册表
ToolExecutionResult 新增可选 error 字段
工具执行的结果类型ToolExecutionResult是判别联合类型,定义于 packages/core/tools/src/index.ts。失败分支ToolExecutionFailure固定携带error: ToolFailure,而成功分支则通过error?: never保证二者互斥:
export interface ToolExecutionFailure { readonly isError: true readonly error: ToolFailure readonly value?: never readonly content: ContentBlock[] // ... }注册表 catch 中的填充逻辑
当工具抛出值为HarnessError时,注册表的 catch 分支会将其结构化为error字段。核心实现是 toolErrorResult:
function toolErrorResult(error: unknown): ToolExecutionResult { const info = errorInfo(error) const message = errorMessage(error) return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true, error: { message, ...info ? { info } : {} }, } }这里体现了本设计的双轨原则:面向模型的content文本块保持不变(模型仍然看到文本),而结构化信息则进入error.info,供代码与回放消费。
会话事件 tool/result 保留结构化失败
Agent 循环将ToolExecutionResult转发到tool/result会话事件。该事件新增了同一可选字段,其类型定义于 packages/core/session/src/types.ts:
error?: { name: string; code: string }正是这一字段使结构化失败信息得以保留进日志,供重试/沙箱插件和回放使用。从测试断言可看到真实场景中的形态,例如 packages/core/tools/tests/tools.spec.ts 中的error: { info: { name: 'HarnessError', code: 'TOOL_FAILURE' } },以及WRAPPER_FAILURE、POST_FAILURE、BOOM、DENIED等 code,均以{ name, code }结构存在于结果中;会话/轨迹层测试如 packages/core/session/tests/invariant.spec.ts 也验证了error: { name: 'ToolNotStartedError', code: TOOL_NOT_STARTED }这类形态在事件中的保留。
toError:把不规范 throw 变成可路由错误
Agent 循环中的toError转换逻辑将非 Error 的 throw 包装为HarnessError,而非裸Error:
- 包装后 code 固定为
'UNKNOWN'; - 原始值作为
cause链接保留,便于errorChain渲染时回溯根因; - 这样即使是不规范的 throw,也能携带可路由的 code 进入会话的
error事件(该事件此前已暴露code字段)。
配套的渲染工具是 errorChain:它递归渲染完整 cause 链与AggregateError成员,使 undici 的TypeError: fetch failed这类传输包装层暴露底层真实失败;同时以Set追踪递归路径防止循环 cause,并对恶意访问器做了防御性兜底(返回<unrenderable value>)。注释明确其定位——仅用于诊断面(消息、通知、日志)渲染,绝不用于解析路由,路由只能基于HarnessError.code。
设计后果与取舍
端到端可机器路由
错误从抛出处到日志全程携带稳定code:插件可以基于error.code分支,而无需对消息做子串匹配。例如沙箱插件可以区分ENOENT与EACCES,重试插件可以依据RATE_LIMIT/QUOTA的差异决定重试策略——QUOTA是终态,重试无意义,而EMPTY_RESPONSE被明确标记为"可以安全重试"。
零新增依赖边
一个基类被广泛导入,但它位于所有包已经依赖的包中,代价仅是一条 import 语句,而非新的依赖边。这保证了架构的依赖方向不被破坏。
面向模型的文本不变
deriveMessages不会将error暴露到模型历史中——模型仍然看到文本块;结构化字段服务于代码和回放。这一双轨设计确保了模型提示的稳定性不受影响。
参数校验与不变式保持独立
- 参数校验保留其既有的 code 和行为(如
INVALID_ARGS); - 包自有的诊断不变式独立携带稳定 code,使不变式注册表无需导入产品包;
- 共享基类只增加跨 seam 的路由元数据,不改变面向模型的文本。
总结
DeepSeek Harness 的结构化错误分类体系用一个位于叶子包、被全仓库依赖的HarnessError基类,解决了"错误跨 seam 退化为裸字符串"的架构问题。其核心价值在于:在错误抛出处分类一次(通过显式 code 或边界文本分类器),之后全程以结构化{ name, code }路由,模型看文本、代码看 code、日志保真完整 cause 链。这一设计为沙箱/重试插件、回放和会话诊断提供了稳定的机器可读基础,是"Everything is a Plugin"理念在错误处理维度的落地体现。
如需深入,可继续阅读:
- 基类与分类器实现:packages/llm/llm/src/error.ts
- 子类 LlmError:packages/llm/llm/src/index.ts
- 子类 ToolArgsError:packages/core/tools/src/schema.ts
- 结果类型与填充逻辑:packages/core/tools/src/index.ts
- 会话事件 error 字段:packages/core/session/src/types.ts
- 工具子系统全景:docs/subsystems/tools.md
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考