DeepSeek Harness 结构化错误分类体系:基于 `HarnessError` 的端到端机器可路由错误设计
2026/9/19 23:41:07 网站建设 项目流程

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的字段与构造函数设计、错误分类树(LlmErrorToolArgsError等)、错误跨工具执行结果与会话事件传播的完整链路,以及插件如何基于error.code分支处理(沙箱、重试、回放)而不必对消息文本做子串匹配。

问题背景:故障跨越 seam 时沦为裸字符串

在引入本方案之前,DeepSeek Harness 中的失败信息在跨模块边界时会被"扁平化":

  • 工具错误被压成文本块:工具执行失败时,namecodestack全部丢失,只留下一段面向模型的文本。这意味着一个未来的沙箱/重试插件无法区分ENOENTEACCES——两类错误需要的处理策略完全不同,但消费方只能看到同一段文字。
  • 非 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 } }

设计要点有三:

  1. 稳定的code与人类可读的message分离code是程序化的、稳定的失败类别标识(如NO_ADAPTERINVALID_ARGSRATE_LIMITUNKNOWN),与面向人的message彻底解耦。注释明确要求"按code路由,永远不要解析message"——因为消息文本可能随供应商措辞变化,而code是分类契约。
  2. 通过标准ErrorOptions支持cause:构造函数透传ErrorOptions,使得new HarnessError(msg, code, { cause: originalError })能保留底层根因,配合errorChain()工具可以渲染完整因果链。
  3. name默认取子类构造器名:通过new.target.name自动设置,子类无需显式声明(当然也可以像LlmErrorToolArgsError那样显式覆盖以固化名称)。

同文件配套的两个关键函数

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,构造时强制校验messagecode非空、status为 100–599 的整数、providerRetryAfterMs为正有限数、requestId非空,并把可序列化的事实(statusproviderRetryAfterMsrequestId)冻结进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? */ }) } }

LlmErrorcode属于共享分类(如AUTHRATE_LIMITNO_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 附近可看到未知工具错误也继承了HarnessErrorcode: '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/resultturn/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类权限错误走不同策略;QUOTAINVALID_CREDENTIAL等 code 依据 error.ts 的规范语义决定是否可重试(INVALID_CREDENTIAL刻意不在默认可重试集合内)。
  • 通用消费方:在任意 seam 用isHarnessError(value)收窄后读取.code,无需知道具体是哪个子类抛出的。
  • 回放/崩溃恢复:持久化的会话日志保留了结构化error字段,重放时依然可以拿到失败类别(见上文 crash-recovery 测试),而不仅仅是一段不可解析的文本。

边界与约束:模型面与代码面分离

Agent Note 在 Consequences 中明确了三个重要边界:

  1. deriveMessages不把error暴露进模型历史——模型始终看到文本块(Error: ...形式),结构化字段服务于代码与回放。这保证模型的输入面保持稳定,同时不牺牲机器可路由性。
  2. 参数校验保留既有 code 与行为ToolArgsErrorINVALID_ARGS与违规列表行为不变,只是继承了共享基类。
  3. 包自有诊断不变式独立携带稳定 code:不变式注册表不导入产品包,各包(如llm-deepseek/src/invariant.tsllm-retry/src/invariant.tstoken-meter/src/invariant.ts)自有的诊断不变式各自携带稳定 code。共享基类只增加跨 seam 的路由元数据,不改变模型面文本。

总结

HarnessError用最小的架构代价(单一公共基类 + 一个code字段)把 DeepSeek Harness 的错误从"跨 seam 即失真的裸字符串"升级为"端到端机器可路由的结构化分类":

  • 统一基类放dsh-llm叶子包,零新增依赖边,任何包一条 import 即可参与分类树;
  • codemessage分离、ErrorOptions承载causename默认子类名,isHarnessError在 seam 收窄;
  • 工具失败经ToolExecutionResult.errortool/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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询