尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DeepSeek Harness 终端 LLM 流失败:LlmRuntime 如何将适配器异常统一为单一 terminal finish 协议

DeepSeek Harness 终端 LLM 流失败:LlmRuntime 如何将适配器异常统一为单一 terminal finish 协议 DeepSeek Harness 终端 LLM 流失败LlmRuntime 如何将适配器异常统一为单一 terminal finish 协议【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本文基于 DeepSeek Harnessdsh仓库中的架构决策笔记 Terminal LLM stream failures完整解读该方案如何消除 LLM 适配器失败的双轨表示抛出异常 流内 finish把归一化职责收敛到LlmRuntime一个边界上。读完后你可以掌握LlmFailure归一化的 catch 边界到底覆盖哪些语句、重试策略为何从“流键控 sidecar”改为由PreparedLlmCall显式携带以及 agent loop 如何仅凭一个终端 finish 完成失败消费——这套设计对任何需要编排流式 LLM 调用的 agent 框架都有直接参考价值。背景适配器失败曾经有两种“公开表示”在该决策落地之前一次 LLM 适配器调用失败会以两种互不相同的形态暴露给上层抛出的异常可能来自最终适配器的选择selection、同步 dispatch、迭代器构造iterator construction或迭代next()流内的终端 finishfinish { kind: error | aborted }这种 in-band 结束协议。为了让 agent loop 能区分“这是适配器失败”还是“中间件/消费者自己的代码出错”当时的做法是LlmRuntime在一个**以流为键stream-keyed的 sidecar旁路映射表**里标记抛出的对象消费方据此识别适配器失败。这个机制的代价在原文档的 Problem 一节说得很直白消费方仍然必须围绕迭代过程包一个 catch而这个 catch 区域里混着自己的信号检查signal checks、chunk 日志、消息组装assembly等本身可能出错的代码正确性依赖于“证明到底是哪条语句抛了异常”并去查询恰好附着在那个返回的 iterable 上的元数据——分类逻辑与 iterable 包装器的对象身份耦合重试策略的所有权同样间接prepareCall()明明已经捕获了 serving registration服务注册策略却只能在 dispatch 之后通过 stream sidecar 发现。于是 wrapper 拥有的路由和 adapter 拥有的路由共享同一个不透明查找 API却拥有不同的权限authority。决策一LlmRuntime 是“单次适配器尝试”的归一化边界新设计的核心断言只有一句LlmRuntime是单次适配器尝试one adapter attempt的归一化边界。它只 catch 四类失败catch 范围说明最终适配器选择final-adapter selection路由/注册查找阶段的同步失败同步 dispatch发起请求时的同步抛出迭代器构造iterator construction构造AsyncIterator时的抛出next()失败迭代过程中 SDK/transport 抛出对落入上述边界的抛出值LlmRuntime将其转换为不可变的LlmFailure并恰好发出一个终端finish。原因reason的选择规则是调用方取消caller cancellation或ABORTED失败 → 选择aborted其余一切适配器失败 → 选择error适配器也可以直接发出两种终端原因之一in-band 路径不受影响。归一化函数在源码中对应 normalizeLlmFailuredeepseek-ai/dsh-llm/adapter-failure模块调用点在 LlmRuntime.stream 的实现 中const failure normalizeLlmFailure(error)。从源码实现看这个函数体现了“归一化”的三个具体要点剥离对象身份保留可序列化事实抛出值如果不是Error会包成带UNKNOWNcode 的HarnessError函数最终返回Object.freeze(...)的纯净负载原始 Error 对象不再跨越 LLM 流接缝。这正对应文档中“恢复机制放弃了精确的抛出对象身份只暴露与 provider 无关的事实detached provider-neutral facts”防御宿主 SDK 的属性陷阱读取error.code、error.failure、error.message时全部走Object.getOwnPropertyDescriptor只读自有数据属性任何 getter 抛出都被吞掉并回退到安全值如消息回退为LLM adapter failed。这是对第三方 SDK 错误对象“可能具有恶意/异常 getter”的防御式处理code 分类法只信任自有体系harnessErrorCode 只认HarnessError实例的 code第三方 SDK 的 code 一律归为UNKNOWN——因为“第三方的 code 不是我们的分类法taxonomy”。LlmFailure负载本身的字段由 bounded LLM request recovery 笔记 定义本文档不取代那部分message、code、可选的status、providerRetryAfterMs、带品牌brand的requestId。这些结构化失败事实、重试策略、持久化尝试记录等仍归前述笔记所有本文档只取代其中“抛出异常的身份识别 call-local sidecar”这一机制。决策二适配器拥有的 catch 在“每个 yield 的 chunk 之前”结束这是边界设计中容易误解的一点。不是“适配器相关的所有异常都归一化”。文档明确来自llm/stream中间件、嵌套调用、适配器清理cleanup、chunk 消费者、日志、信号检查和组装assembly的错误继续按缺陷defect或生命周期失败抛出绝不进入模型请求恢复路径。一个关键推论是流不变量stream invariant的放宽由于 transport 在已经流出部分 delta 之后才失败块block可能来不及闭合因此流不变量只允许终端 error 或 aborted finish 时保留未闭合块而且这种不完整输出永远不会被组装成 assistant 消息或 tool call——重试会从持久化日志重建下一个尝试而不是拼接残缺输出。决策三重试策略挂在 PreparedLlmCall 上替代 sidecar 查找旧机制里消费方要通过 stream sidecar 才能查到服务策略。新设计中PreparedLlmCall显式暴露不可变的重试策略该策略与其 config 和 registration 在准备阶段一起捕获一次性的 prepared call 被复用、或 config 不匹配仍是同步的INVALID_PREPARED_CALL误用错误misuse error完全由llm/stream中间件服务的路由没有 prepared registration因此也没有服务策略serving policy——中间件独占路由在类型层面就是“无策略”的而不是查不到值。这一点在 agent loop 源码中可以直接验证agent loop 组装agent/request-error事件 时retryPolicy直接取自preparedCall?.retryPolicy——策略随已准备调用到达扩展点不再经过任何以流为键的元数据表。策略本身的形态由 retry-policy 模块 定义provider 拥有的、随注册解析的不可变策略分normal有界瞬时重试与always无界重试两种模式。从当前源码可确认normal模式的默认值maxRetries: 5、initialDelayMs: 500、maxDelayMs: 10_000、jitterRatio: 0.1默认可重试 code 集合为EMPTY_RESPONSE、RATE_LIMIT、SERVER、TIMEOUT、TRANSPORTbackoff的延迟上限被 Node 最大定时器延迟MAX_TIMER_DELAY_MS约束。策略的执行者是函数式插件deepseek-ai/dsh-llm-retry监听agent/request-error它不引入新服务或新 loop 分支——这与本文档的归一化边界是互补关系前者管“失败之后要不要重试、等多久”后者管“失败事实从哪里来、以什么形态到达”。决策四agent loop 只消费一个失败表示归一化边界建立后agent loop 的消费路径被简化为三步迭代并记录 chunk不带分类 catch——因为适配器操作失败已经全部变成终端 finishcatch 不再需要“猜这是谁的异常”检查终端 finish——如果带LlmFailure则失败事实已经就绪把失败事实 prepared 策略一起交给agent/request-error。同时旧的三个公开 sidecar API——isLlmAdapterFailure、llmFailureOf、llmRetryPolicyOf——被删除。消费方删除了“识别哪个适配器抛出”的 catch 和流键控元数据这是该重构在消费侧的主要收益。考虑并否决的备选方案原文档记录了四个被否决的替代方案其否决理由本身就是理解设计约束的最好材料保留 call-local 错误标记——保留了抛出对象身份但每个消费方都得 catch 一个包含自身易错工作的区域且分类耦合在 iterable 包装器身份上。“原始错误对象在恢复中没有持久角色归一化事实才是有用的边界值。”要求所有适配器只发失败 chunk、禁止 throw——库迭代器、transport 和 JavaScript dispatch 本来就会抛。要求每个适配器复刻同一套 catch 边界等于复制所有权也保护不了直接使用LlmRuntime的消费者。在 agent loop 里 catch 所有迭代错误——loop 无法可靠区分 provider 失败、中间件失败、session append、取消或组装失败除非重建一个“流对象 → 适配器调用”的 sidecar 映射。结论分类应该发生在发起适配器调用的地方。在流之前返回Result——流前结果无法表示“部分输出之后的 transport 失败”除非引入第二套响应生命周期。既有的终端 chunk 协议已经能表达“早失败”和“晚失败”两种尝试结果不必再造一套。后果与权衡按原文档 Consequences 一节这套取舍的账是所有LlmRuntime.stream()消费者通过同一个类型化终端协议接收适配器操作失败而编程错误和生命周期失败保留普通异常语义——两类故障从此有清晰的分界线恢复机制放弃了精确的抛出对象身份只暴露与 provider 无关的解耦事实诊断原始错误仍留在 cause 链和会话日志中流服务承担了略多的适配器管线代码catch 边界内移换来消费侧删除识别性 catch 和流键控元数据prepared call显式携带策略纯中间件路由在类型上就“可见地无策略”不再依赖不透明查找。边界关系与相邻恢复机制的分工为避免与其他机制混淆值得把三条笔记的分工钉死bounded LLM request recovery 继续拥有LlmFailure结构化事实、有界重试策略瞬时 code 选择、指数退避 抖动、Retry-After处理、llm/retry持久化事件与dsh-llm-retry插件本文档取代其中的抛出错误身份与 stream-sidecar 机制after-call compaction pressure and context-overflow recovery 拥有上下文溢出恢复CONTEXT_WINDOW_EXCEEDED归一化后走同一条agent/request-error路径replaceGeneration变化作为重试凭证本文档独占的只有最终适配器边界的归一化、单一终端 finish、PreparedLlmCall上的策略携带、以及 sidecar API 的删除。对应的验证面在仓库中可查归一化函数的单测 adapter-failure.spec.ts、服务层含PreparedLlmCall误用检查的 service.spec.ts以及 agent loop 侧的 request-error.spec.ts——覆盖“thrown 与 in-band 失败统一到达agent/request-error、携带当前 prepared 策略”等场景。小结这篇架构笔记解决的是一个非常典型的 agent 框架问题流式 LLM 调用的失败事实应该在哪里被定型。答案是把它定型在离适配器最近、离消费语义最远的地方——LlmRuntime的 catch 边界——让上层agent loop、重试插件、溢出恢复插件只面对一种失败表示和一份随调用显式携带的策略。对于正在设计 LLM 编排层、为不同 provider SDK 统一错误语义的开发者这套“归一化边界 终端协议 显式策略携带”的组合比“在每个消费点各自 try/catch 再查元数据”的常见做法更易于推理和测试。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表