Semantic Kernel 错误处理改进实战指南:基于 ADR-0004 的 .NET 异常体系设计与落地
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
导读
本文以官方架构决策记录 docs/decisions/0004-error-handling.md 为骨架,系统梳理 Semantic Kernel(SK)在 .NET 侧错误处理的设计演进:从"异常存入 SKContext"到"异常向上抛出"、从"自定义 SK 异常泛滥"到"优先使用 .NET 标准异常"、再到HttpOperationException的统一 HTTP 错误抽象。读完本文,你将理解 SK 异常体系的取舍逻辑,掌握KernelException、HttpOperationException、KernelFunctionCanceledException的正确用法与源码级实现细节,并能在自己的 SK 应用中以标准 .NET 方式处理异常。
适用范围说明:本 ADR 与下文引用的源码均位于
dotnet/目录,面向 .NET 版本的 Semantic Kernel SDK。文档明确声明:本文不涉及日志(logging)、弹性(resiliency)与可观测性(observability)三个领域。
背景:SK 错误处理存在的五个问题
ADR-0004 发布于 2023-06-23(状态:accepted),记录于 0004-error-handling.md。它指出当时 SK 的错误处理在五个方面偏离了 .NET 惯例:
- 异常传播方式特殊:
Kernel.RunAsync、SKFunction.InvokeAsync等公开方法不抛异常,而是捕获后存入SKContext。这违反了 .NET "契约满足则成功执行、契约违反则抛出异常" 的标准约定,客户端开发者必须分析SKContext的特定属性才能判断调用是否成功,体验糟糕。 - 异常使用不当:部分组件用自定义 SK 异常表达"参数非法""配置错误"等场景,而这些场景本应使用
ArgumentNullException、ArgumentOutOfRangeException等 .NET 标准异常。 - 异常层级不统一:一半自定义异常派生自
SKException,另一半直接派生自Exception,异常模型缺乏一致性。 - 存在多余且冗长的异常:Kernel、Planner 各自的
KernelException、PlanningException以及每个 Memory 连接器专属的PineconeMemoryException、QdrantMemoryException等,除了名字不同、成员签名完全相同,不携带额外信息。这让客户端无法用单一 catch 块统一处理,新增或移除一个组件实现就要改动一次客户端代码。 - 丢失原始异常细节:某些 SK 异常不保留原始失败原因,也不通过属性暴露,客户端无从理解问题根源、无法正确处理。
决策驱动因素:五条设计原则
该 ADR 在 Decision Drivers 中确立了五条指导原则,后续所有方案都围绕它们展开:
- 异常应传播给 SK 客户端代码,而非存储在
SKContext中,使 SK 错误处理回归 .NET 惯例; - 异常层级遵循"少即是多"(less is more):新增异常容易,删除困难,因此初始设计应尽量精简;
- 优先使用 .NET 标准异常而非 SK 自定义异常:它们易识别、零维护成本、覆盖常见错误场景、提供标准化错误消息;
- 除非有助于 SK 或客户端构建可执行的应对逻辑,否则不应将异常包装进 SK 异常再抛给调用方;
- (隐含约束)保留原始异常为 InnerException,缺失的必须补齐。
决策方案:七项改进措施
方案一:精简自定义异常层级
移除除SKException及其有实际用途的派生类型之外的所有自定义异常类型;需要传达更多细节时才创建新的派生异常。
这一方案在当前仓库中的落地结果是:原先的SKException已被统一为 KernelException.cs(Microsoft.SemanticKernel命名空间下public class KernelException : Exception),它成为"所有 Semantic Kernel 异常派生的基类",提供标准的三个构造函数(无参、仅消息、消息 + 内层异常),并在Exception.Data中可选携带符合 OpenTelemetry 标准的遥测信息。
从源码结构看,SemanticKernel.Abstractions项目内仅保留了两个公开异常类型:KernelException.cs 与 HttpOperationException.cs,外加一个特殊用途的 KernelFunctionCanceledException.cs(见下文"方案的落地形态")。这种"核心异常数量极少、按需派生"的形态正是"less is more"原则的直接体现。
方案二:用 .NET 标准异常替代自定义异常
当类参数值缺失或非法时,抛ArgumentOutOfRangeException、ArgumentNullException等标准异常,而非自定义 SK 异常;并全面审查异常使用点,找出其他可替换为标准异常的地方。
这一方案在 KernelFunctionFromMethod.cs 中有典型实现:当参数值类型不匹配且转换失败时,代码抛ArgumentOutOfRangeException(name, value, e.Message);同时注意捕获条件catch (Exception e) when (!e.IsCriticalException())——非关键异常才被转换为参数范围异常,关键异常直接向上传播。
而在"参数缺失"这类无法用标准异常覆盖的场景,实现则以KernelException包装标准异常:例如 KernelFunctionFromMethod.cs 抛KernelException("Missing service for function parameter '{parameter.Name}'", new ArgumentException(...)),L713-L714 抛KernelException("Missing argument for function parameter '{name}'", new ArgumentException(...))。这种"外层 KernelException + 内层标准异常"的组合,既保留了 SK 的统一入口,又通过 InnerException 暴露了标准化的错误语义。
方案三:移除仅为包装而包装的异常
删除"仅仅为了包装"而把未处理异常包进AIException或其他 SK 异常的逻辑——这类包装除了给出"Something went wrong"这类无信息量的通用消息外毫无用处。
结合方案二可见,当前仓库中KernelException的用法已经从"纯包装"收敛为"携带可行动信息"(如缺失参数名、缺失服务、非法函数名等场景),例如:
- MemoryBuilder.cs:依赖未注入时抛出带明确指引的
KernelException("UseWithMemoryStoremethod"); - FunctionIdBlock.cs:非法函数名抛出带规则说明的
KernelException; - NamedArgBlock.cs:命名参数格式错误时抛出带分隔符说明的
KernelException; - KernelPromptTemplate.cs 与 CodeBlock.cs:模板解析错误抛出携带错误详情的
KernelException。
方案四:保留原始异常为 InnerException
排查所有"重新抛出 SK 异常但未保留原始异常"的场景并逐一修复。这条原则贯穿了上述所有KernelException(message, innerException)的调用点,也体现在下文的HttpOperationException构造中。
方案五:引入 HttpOperationException 统一 HTTP 错误
创建带StatusCode属性的HttpOperationException,并实现从HttpStatusCode、HttpRequestException、Azure.RequestFailedException到该异常的映射逻辑;所有与 HTTP 栈交互的 SK 代码在请求失败时抛出HttpOperationException,并将原始异常设为 InnerException。
当前实现位于 HttpOperationException.cs,其关键设计:
- 标准构造器:
HttpOperationException()、HttpOperationException(string?)、HttpOperationException(string?, Exception?); - 增强构造器:
HttpOperationException(HttpStatusCode? statusCode, string? responseContent, string? message, Exception? innerException); - 核心属性:
StatusCode(HTTP 状态码,为 null 表示未收到响应)、ResponseContent(HTTP 响应正文); - 遗留属性:
RequestMethod、RequestUri、RequestPayload已标记[Obsolete],建议改用Exception.Data['Name']、Exception.Data['Url']、Exception.Data['Data']获取; - 遥测兼容:同
KernelException一样,可通过Exception.Data携带 OpenTelemetry 标准的键值信息。
映射实现分为两处:
- Azure/OpenAI 侧:
Azure.RequestFailedException通过 RequestFailedExceptionExtensions.cs 转换为HttpOperationException——当exception.Status == 0(NoResponseReceived)时StatusCode为 null,读取响应正文失败时静默吞掉(保证一定抛出HttpOperationException而非其他异常); - OpenAI SDK 侧:
System.ClientModel的ClientResultException通过 ClientResultExceptionExtensions.cs 以相同模式转换(Status == 0→ null 状态码,并尽力提取ResponseContent)。
调用链佐证:OpenAI 连接器的 ClientCore.cs 中,RunRequestAsync<T>与RunRequest<T>两个方法统一try { ... } catch (ClientResultException e) { throw e.ToHttpOperationException(); },即所有 OpenAI 请求失败都收敛为HttpOperationException。单元测试 ClientResultExceptionExtensionsTests.cs 验证了三条关键行为:无响应时StatusCode为 null 且保留原始异常为 InnerException;有响应时正确回填StatusCode与ResponseContent;消息与原始异常保持一致。
方案六:所有组件改为重新抛出异常
将所有原本"把异常存入 SK Context"的组件改为重新抛出(rethrow)。这与方案一配合,是 SK 错误处理向标准 .NET 模型靠拢的核心一步。落地后,Kernel.InvokeAsync系列 API 的文档契约明确标注了KernelFunctionCanceledException(见 Kernel.cs 的<exception cref>声明),调用方可以用标准 try/catch 捕获异常,而非检查上下文状态。
方案七:精简关键异常判定逻辑
将IsCriticalException扩展方法精简为排除StackOverflowException与OutOfMemoryException:前者根本不会被抛出(调用代码不会执行),后者不必然阻止恢复代码执行。
当前实现位于 ExceptionExtensions.cs,IsCriticalException只对以下类型返回 true:
ex is ThreadAbortException or AccessViolationException or AppDomainUnloadedException or BadImageFormatException or CannotUnloadAppDomainException or InvalidProgramException;该扩展方法被 Kernel 核心执行路径广泛使用,例如 KernelFunctionFromMethod.cs 的参数转换捕获,以及 L1114 的文化回退逻辑catch (Exception e) when (!e.IsCriticalException() && cultureInfo != CultureInfo.InvariantCulture)——后者在特定文化解析失败时回退到 InvariantCulture 重试,但关键异常不会被吞掉,而是直接向上传播。
方案的落地形态:当前仓库中的异常全景
将 ADR 的七项方案映射到当前仓库,可以得到一张清晰的异常使用地图:
| 异常类型 | 定义位置 | 用途 | 与 ADR 方案的关系 |
|---|---|---|---|
KernelException | SemanticKernel.Abstractions/KernelException.cs | 所有 SK 异常的公共基类;携带消息与 InnerException | 方案一(精简层级)、方案四(保留 InnerException) |
HttpOperationException | SemanticKernel.Abstractions/Http/HttpOperationException.cs | 统一 HTTP 请求失败错误,暴露StatusCode与ResponseContent | 方案五 |
KernelFunctionCanceledException | SemanticKernel.Abstractions/Functions/KernelFunctionCanceledException.cs | 派生自OperationCanceledException;当函数过滤器请求取消时由KernelFunction调用抛出,附带Kernel、Function、Arguments、FunctionResult上下文 | 方案六落地后的新增可行动异常 |
ArgumentNullException/ArgumentOutOfRangeException等 | .NET BCL | 参数缺失、类型不合法等 | 方案二 |
关键异常(ThreadAbortException等六类) | .NET BCL | 不捕获、直接传播 | 方案七 |
值得注意KernelFunctionCanceledException的设计:KernelFunctionCanceledException.cs 将FunctionResult也纳入构造参数——当函数在成功完成后才被请求取消时,调用方仍能从异常中取回函数结果。这正是 ADR "除非有助于构建可行动逻辑,否则不包装"原则的正面例证:这个异常不是简单的包装,而是携带了完整上下文、可被客户端直接消费的"可行动"类型。
对 SK 客户端开发者的实践指引
综合 ADR 决策与当前实现,SK .NET 客户端代码应遵循以下异常处理范式:
1. 用标准 try/catch 处理调用结果。Kernel.InvokeAsync/KernelFunction.InvokeAsync的失败一律以异常形式呈现,不要再检查上下文属性:
try { FunctionResult result = await kernel.InvokeAsync(pluginName, functionName, arguments); Console.WriteLine(result); } catch (KernelException ex) when (ex.InnerException is ArgumentException) { // 参数缺失/非法:根据 InnerException 的 ParamName 定位问题参数 } catch (HttpOperationException ex) when (ex.StatusCode is HttpStatusCode.TooManyRequests) { // 触发限流:读取 ex.ResponseContent 获取服务端返回详情 } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // 用户取消或函数过滤器请求取消 }2. 用IsCriticalException语义保护自己的恢复逻辑。仿照 KernelFunctionFromMethod.cs,任何"尝试失败后回退/重试"的 catch 都应加when (!e.IsCriticalException())过滤,避免吞掉进程级关键异常。
3. 只捕获可行动的异常。对HttpOperationException优先按StatusCode分支处理;对KernelException优先读取InnerException与消息中携带的参数名/指引(如 MemoryBuilder 的 "UseWithMemoryStoremethod" 提示);对无法恢复的错误直接放行,交给上层或全局异常处理器。
4. 识别遗留异常属性的迁移信号。若在旧代码中见到HttpOperationException.RequestUri/RequestMethod/RequestPayload,应改用Exception.Data['Url']/['Name']/['Data'](见 HttpOperationException.cs 的[Obsolete]标注)。
验证与测试:错误处理契约的守护者
ADR 的决策并非停留在设计文档层面,仓库中的测试用例持续守护着这些契约:
- ClientResultExceptionExtensionsTests.cs 验证
ClientResultException → HttpOperationException转换的三种场景(无响应、有响应正文、无正文),确保StatusCode/ResponseContent/InnerException的映射准确; - ChatHistorySummarizationReducerTests.cs 验证消息归约器在 HTTP 失败时确实抛出
HttpOperationException,证明异常向上传播的契约生效; - 集成测试侧,Agents 与 Azure/OpenAI 连接器的多个测试(如
AzureOpenAIChatClientTests、OpenAIAssistantAgentTests)在断言中引用了HttpOperationException,印证其在真实服务交互路径上被一致抛出。
延伸阅读
- 完整决策记录:docs/decisions/0004-error-handling.md
- 异常类型实现:KernelException.cs、HttpOperationException.cs、KernelFunctionCanceledException.cs
- 异常转换工具:RequestFailedExceptionExtensions.cs、ClientResultExceptionExtensions.cs
- 关键异常过滤实现:ExceptionExtensions.cs
- 调用链示例:ClientCore.cs、KernelFunctionFromMethod.cs
- 单元测试:ClientResultExceptionExtensionsTests.cs
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考