DeepSeek Harness 结构化错误分类体系:基于HarnessError的端到端机器可路由错误设计
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
导读
本篇文章围绕 DeepSeek Harness 仓库中的架构 Agent Note《Structured error taxonomy》展开,讲解项目如何用一个统一的HarnessError基类取代跨越各能力 seam 的裸字符串错误,让工具错误、LLM 调用失败、非 Error throw 等各类故障都携带稳定、可机器路由的code。读完你将掌握HarnessError的字段与构造函数设计、错误分类树(LlmError、ToolArgsError等)、错误跨工具执行结果与会话事件传播的完整链路,以及插件如何基于error.code分支处理(沙箱、重试、回放)而不必对消息文本做子串匹配。
问题背景:故障跨越 seam 时沦为裸字符串
在引入本方案之前,DeepSeek Harness 中的失败信息在跨模块边界时会被"扁平化":
- 工具错误被压成文本块:工具执行失败时,
name、code和stack全部丢失,只留下一段面向模型的文本。这意味着一个未来的沙箱/重试插件无法区分ENOENT和EACCES——两类错误需要的处理策略完全不同,但消费方只能看到同一段文字。 - 非 Error 的 throw 退化更严重:当某处
throw一个非Error值(例如throw { reason: 'denied' })时,agent loop(智能体循环)会用new Error(String(x))包装它,code信息被彻底丢弃,连可读的cause链也丢了。 - 缺少共享基类:当时
LlmError是系统中唯一的类型化错误,没有公共基类,消费方无法对任意错误做通用的instanceof判断,也就无法在统一出口(seam)做类型收窄。
于是决策落地为:在dsh-llm包中引入一个HarnessError extends Error基类,作为全系统错误的公共祖先。
核心设计:HarnessError基类
基类实现位于 packages/llm/llm/src/error.ts,核心代码如下:
export class HarnessError extends Error { /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */ readonly code: string constructor(message: string, code: string, options?: ErrorOptions) { super(message, options) this.code = code this.name = new.target.name } }设计要点有三:
- 稳定的
code与人类可读的message分离:code是程序化的、稳定的失败类别标识(如NO_ADAPTER、INVALID_ARGS、RATE_LIMIT、UNKNOWN),与面向人的message彻底解耦。注释明确要求"按code路由,永远不要解析message"——因为消息文本可能随供应商措辞变化,而code是分类契约。 - 通过标准
ErrorOptions支持cause链:构造函数透传ErrorOptions,使得new HarnessError(msg, code, { cause: originalError })能保留底层根因,配合errorChain()工具可以渲染完整因果链。 name默认取子类构造器名:通过new.target.name自动设置,子类无需显式声明(当然也可以像LlmError、ToolArgsError那样显式覆盖以固化名称)。
同文件配套的两个关键函数
error.ts还导出了与错误体系配套的两个函数:
isHarnessError(value)(error.ts#L161-L163):类型守卫,value instanceof HarnessError。注释特别强调只收窄真实实例,duck-typed 或跨 realm 的错误不会误判,用于各 seam 处的运行时边界收窄。errorChain(value)(error.ts#L114-L154):把任意抛出值渲染为带完整cause链的文本,并处理AggregateError成员与循环引用(渲染<circular cause>)。它专门用于诊断面(消息、通知、日志)的渲染——注释明确"只渲染,绝不解析;路由请走HarnessError.code"。
为什么放在dsh-llm(叶子包)
决策的约束是"不引入新的依赖边":dsh-llm是所有其他包都已经依赖的叶子包,把基类放这里,任何包想继承或instanceof它都只需一条import语句,而不是新增一个包依赖。正如 Agent Note 所述:"一个基类被广泛导入,但它位于所有包已经依赖的包中,代价仅是一条 import,而非新的依赖边。"这一点可从 packages/llm/llm/src/index.ts#L36-L39 看到:export * from './error.ts'使基类从@deepseek-ai/dsh-llm公开导出。
供应商中立的规范 code
error.ts中还定义了一批供应商中立的规范 code 常量与分类器,进一步夯实"稳定 code"的语义:
CONTEXT_WINDOW_EXCEEDED_CODE = 'CONTEXT_WINDOW_EXCEEDED':请求超出模型上下文窗口。QUOTA_EXCEEDED_CODE = 'QUOTA':账户配额/余额耗尽(区别于瞬时限流)。EMPTY_RESPONSE_CODE = 'EMPTY_RESPONSE':响应正常结束但没有任何内容块(视为可安全重试)。INVALID_CREDENTIAL_CODE = 'INVALID_CREDENTIAL':凭据存在但不可用(格式错误),修复方式是修正存储值而非补充凭据,且刻意不放入默认可重试集合——格式错误的凭据每次尝试都会同样失败。
配套的isContextWindowExceededError(detail)(error.ts#L80-L86)和isQuotaExceededError(detail)(error.ts#L94-L100)用正则识别各 OpenAI 兼容供应商的措辞,把"某个供应商到底算不算上下文超限/配额耗尽"统一收敛为规范 code。这些分类器与 adapter-failure.ts 的normalizeLlmFailure协同——后者只信任 Harness 自有的 code(error instanceof HarnessError ? error.code : 'UNKNOWN'),第三方 SDK 的 code 不属于本分类体系。
错误分类树:现有子类
HarnessError作为公共基类,被两类既有错误继承,且都保留了各自既有的 code:
LlmError(LLM 相关失败)
位于 packages/llm/llm/src/index.ts#L86-L120,构造时强制校验message与code非空、status为 100–599 的整数、providerRetryAfterMs为正有限数、requestId非空,并把可序列化的事实(status、providerRetryAfterMs、requestId)冻结进readonly failure: LlmFailure字段:
export class LlmError extends HarnessError { readonly failure: LlmFailure constructor(message: string, code: string, options?: LlmErrorOptions) { // ...参数校验... super(message, code, options) this.name = 'LlmError' this.failure = Object.freeze({ message, code, /* status? providerRetryAfterMs? requestId? */ }) } }LlmError的code属于共享分类(如AUTH、RATE_LIMIT、NO_ADAPTER),携带的failure事实则供持久化与策略层使用。
ToolArgsError(工具参数校验失败)
位于 packages/core/tools/src/schema.ts#L461-L470,是dsh-tools中的参数校验错误,固定使用INVALID_ARGScode,并保留逐条违规列表:
export class ToolArgsError extends HarnessError { readonly violations: string[] constructor(violations: string[]) { super(`invalid arguments: ${violations.join('; ')}`, 'INVALID_ARGS') this.name = 'ToolArgsError' this.violations = violations } }除此之外,从 packages/core/tools/src/index.ts#L489-L491 附近可看到未知工具错误也继承了HarnessError(code: 'UNKNOWN_TOOL'),说明该分类树会随各包继续生长。
跨 seam 的传播链路
Agent Note 强调"错误端到端可机器路由",其关键在于三处 seam 的打通:
1. 工具执行结果:ToolExecutionResult.error
dsh-tools定义了结构化失败元数据 ToolErrorInfo:
export interface ToolErrorInfo { name: string code: string } export interface ToolFailure { message: string // 人类可读消息(无 Native `Error: ` 前缀) info?: ToolErrorInfo // 供策略与持久化诊断使用的内部错误类别 }失败结果 ToolExecutionFailure 通过error: ToolFailure携带该信息,与成功结果的value互斥(readonly error?: never)。注册表 catch 处(toolErrorResult)在抛出值为HarnessError时填充结构化信息:
function toolErrorResult(error: unknown): ToolExecutionResult { const info = errorInfo(error) // error instanceof HarnessError ? { name, code } : undefined const message = errorMessage(error) return { content: [{ type: 'text', text: `Error: ${message}` }], // 模型面文本块不变 isError: true, error: { message, ...info ? { info } : {} }, } }注意errorInfo(index.ts#L642-L647)只对HarnessError实例产出结构化字段,并用 try/catch 兜底——即使抛出值"充满敌意"(instanceof被陷阱化),也不会让错误归一化边界本身崩溃。
2. 会话事件:tool/result与turn/end
agent loop 将结构化失败转发到tool/result会话事件(该事件同样新增可选error字段),使结构化失败信息进入持久化日志,供重试/沙箱插件与回放使用。这一点有端到端测试直接印证:crash-recovery.e2e.ts 验证崩溃恢复后重放的tool/result事件携带{ name: 'ToolOutcomeUnknownError', code: TOOL_OUTCOME_UNKNOWN },且模型面文本仍含Do not retry blindly.——证明"结构化字段与模型文本并存"的设计。
3. agent loop 的toError归一化:非 Error throw →UNKNOWN
Agent Note 提到的toError归一化逻辑,在 packages/core/agent-loop/src/agent.ts#L309-L322 的 turn catch 中可以看到对应实现:
// Every failure is structured: an `LlmError` keeps its facts, anything // else flattens to `errorChain` text under the `UNKNOWN` code. turnEnds = { kind: 'error', error: error instanceof LlmError ? error.failure : { message: errorChain(error), code: 'UNKNOWN' }, }即:LlmError保留其结构化failure事实;其他任意抛出值(包括非 Error)统一通过errorChain渲染为因果链文本,并打上UNKNOWN兜底 code。会话的error事件此前已暴露code,因此即使是糟糕的 throw,也能携带可路由 code 进入日志,而不是被new Error(String(x))抹掉全部类别信息。
消费端实践:插件如何基于error.code路由
整个设计的收益在消费端兑现:
- 沙箱/重试插件:从
tool/result事件读出error.info.code,直接分支处理。例如ENOENT类文件系统错误与EACCES类权限错误走不同策略;QUOTA、INVALID_CREDENTIAL等 code 依据 error.ts 的规范语义决定是否可重试(INVALID_CREDENTIAL刻意不在默认可重试集合内)。 - 通用消费方:在任意 seam 用
isHarnessError(value)收窄后读取.code,无需知道具体是哪个子类抛出的。 - 回放/崩溃恢复:持久化的会话日志保留了结构化
error字段,重放时依然可以拿到失败类别(见上文 crash-recovery 测试),而不仅仅是一段不可解析的文本。
边界与约束:模型面与代码面分离
Agent Note 在 Consequences 中明确了三个重要边界:
deriveMessages不把error暴露进模型历史——模型始终看到文本块(Error: ...形式),结构化字段服务于代码与回放。这保证模型的输入面保持稳定,同时不牺牲机器可路由性。- 参数校验保留既有 code 与行为:
ToolArgsError的INVALID_ARGS与违规列表行为不变,只是继承了共享基类。 - 包自有诊断不变式独立携带稳定 code:不变式注册表不导入产品包,各包(如
llm-deepseek/src/invariant.ts、llm-retry/src/invariant.ts、token-meter/src/invariant.ts)自有的诊断不变式各自携带稳定 code。共享基类只增加跨 seam 的路由元数据,不改变模型面文本。
总结
HarnessError用最小的架构代价(单一公共基类 + 一个code字段)把 DeepSeek Harness 的错误从"跨 seam 即失真的裸字符串"升级为"端到端机器可路由的结构化分类":
- 统一基类放
dsh-llm叶子包,零新增依赖边,任何包一条 import 即可参与分类树; code与message分离、ErrorOptions承载cause、name默认子类名,isHarnessError在 seam 收窄;- 工具失败经
ToolExecutionResult.error与tool/result事件进入日志,非 Error throw 归一化为UNKNOWNcode; - 模型看到的文本块保持不变,结构化字段只服务代码、策略与回放。
对于希望扩展 Harness 生态(沙箱策略、重试策略、回放分析)的开发者,这条链路给出了清晰的接入点:读会话事件中的结构化error.info.code,按 code 分支,而不是解析消息文本。
相关实现与验证可继续查阅:packages/llm/llm/src/error.ts、packages/llm/llm/src/index.ts、packages/core/tools/src/schema.ts、packages/core/tools/src/index.ts、packages/core/agent-loop/src/agent.ts、packages/session/session-checkpoint-policy/tests/crash-recovery.e2e.ts。
【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考