码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • .NET 异常处理的"暗门":代码里写满 catch,你依然能抓住它——从一个 AI Agent 运行时的源码说起


    一个健壮的系统,必然到处都是有意的 catch;异常被消化不等于问题不存在。 观测与韧性,是一个硬币的两面——降级逻辑保证系统不崩,FirstChance 保证你能看见它为什么降级。
    合集 - AI开源项目(61)
    1.为 openclaw.net 集成 ElBruno.MempalaceNet 记忆系统05-032.Graphify-DotNet:AI 驱动的 .NET 代码知识图谱构建工具05-023.SonnetDB:.NET 生态下的高性能嵌入式时序数据库04-284.C# 原生编码智能体运行时 SharpClawCode04-275.SharpIDE: 基于 .NET 与 Godot 引擎的跨平台开源 IDE04-266.OpenClaw.NET .NET 原生插件开发完全指南:以 Mempalace 插件为范例05-057..NET生态系统中的A2A(Agent-to-Agent)协议支持与跨平台多智能体协同05-068.Memgraph 与 Neo4j 图数据库对比及 .NET 生态适配分析05-089.OpenClaw.NET 外部 CLI 连接器 (External CLI Connectors) 详细技术总结05-1010.OpenClaw.NET:给智能以形态05-1211.架构融合:Activity Host 作为确定性编排与认知智能代理的桥梁05-1812.OpenClaw.NET 兼容性目录指南(Compatibility Catalog)05-2113.当 AgentGateway 遇上 OpenClaw.NET:企业级智能体基础设施的深度协同实战06-0614.OpenClaw.NET 上线 MetaSkills :软件工程第一性原理的工业级实践06-1715.OpenClaw.NET MetaSKILLs 系统深度解析:AI Agent 正在学会「自己给自己写技能」06-1816.OpenClaw.NET 重大更新:Goal 机制登场,让 AI Agent 不再"半途而废"06-1817.OpenClaw.NET 祭出 TokenJuice:Agent 时代的"Token 瘦身引擎",让 LLM 上下文不再膨胀06-1918.从 OpenClaw.NET 的 /loop 实现,看 Loop Engineering 如何从概念走向工程实践06-2019.将 Rust 绑定到 .NET 10:Oxigraph 的 FFI 桥接实践06-2820.Token IO 架构的设计游戏:大模型产品形态四年演进的本质06-2921.当 AI 不再只是"说话":OpenClaw.NET 率先原生支持 MCP Apps06-3022.你的 AI Agent 会在服务器上"修仙"——OpenClaw.NET 长持久会话技术解读07-0423.从 Bun 的 Rust 重写,看 C# 如何重建 AI 基础设施层07-1024.当 AI 对话里直接弹出业务界面:MCP Apps 正在改写 SaaS 的集成逻辑07-1225.MCP + A2A 融合:协议层已就绪,信任层才是硬仗07-2126.C# MCP SDK 2.0 即将发布:去会话化、去握手、去运维噩梦07-2427.数字员工的成本账:OpenClaw.NET 如何用工程化实现"成功任务的单位经济学"(下)07-2328.25家巨头联名的那封公开信,为什么数字员工架构师应该逐字读一遍07-2829.从 Harness 引擎到 MetaSkill DAG 的确定性架构07-3030.从DDD到Ontology:当数字员工不再认"限界上下文"这堵墙07-3131.把 284B 的 DeepSeek V4 Flash 装进纯 .NET:TensorSharp 用一天改写了 .NET 推理栈的位置08-0132.MCP 第五版 × OpenClaw.NET:从协议升级到生态编排08-0233.纯 C# 追平 llama.cpp?.NET 本地推理三国杀08-0334."从"前锋"到"中场":为什么 Agent 基础设施的建设者必须是系统思考者08-0435.修 Bug 的手艺与架构的艺术:从熵增到熵减08-0636.改变世界的17个方程,如何重塑 GoodCrew 的架构哲学08-0637.Agent 系统的不确定性治理:当软件工程遭遇"梯度下降"08-1138.WorkBuddy 专家团的下一步:用 MetaSkill 把「提示词约定」升级为「运行时硬约束」08-1039.JVMGuard 来了,.NET 开发者别慌:你的 dotnet-monitor 早就准备好了08-1440.两种 Harness 哲学:从 DeepSeek Harness 的"过度抽象"争议,看 OpenClaw.NET 的另一种答案08-1541.当"入门第一课"的作者开始 Vibe Coding:廖雪峰 2026 上半年 AI 编程实践深度探索08-1642.TensorSharp 最新进展研究报告:从 DeepSeek V4 Flash 到 GLM-5.2,以及等待中的 GLM-5.308-2043.AI 编程时代,.NET 的机会在哪里?08-2544.3 张卡到底能不能跑大模型推理?从 vLLM、llama.cpp 到 TensorSharp 的多卡真相08-2845.纯 .NET 手写 CUDA kernel,GLM-5.3-Flash decode 跑出 llama.cpp 的 2 倍08-2946.TensorSharp 3.3.0.0 发布,视频生成、DFlash2 投机解码、安全加固一起来了08-31
    47..NET 异常处理的"暗门":代码里写满 catch,你依然能抓住它——从一个 AI Agent 运行时的源码说起09-11
    48.MetaSkill 落地 .NET:当 Agent 从「调用工具」进化到「组织工具」09-0349.写给 Java 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Spring 思维,跑起一个生产级 AI Agent09-0650.写给 Golang 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Go 思维,跑起一个生产级 AI Agent09-0451.写给 Python 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Python 思维,跑起一个生产级 AI Agent09-0552.写给 TypeScript 工程师的 OpenClaw.NET 上手指南:用你熟悉的 TS 思维,跑起一个生产级 AI Agent09-0553.写给 Rust 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Rust 思维,跑起一个生产级 AI Agent09-0854.写给 C++ 工程师的 OpenClaw.NET 上手指南:用你熟悉的 C++ 思维,跑起一个生产级 AI Agent09-1055.写给 PHP 工程师的 OpenClaw.NET 上手指南:用你熟悉的 PHP 思维,跑起一个生产级 AI Agent09-0956.RedNb.Nacos 2.0.0 正式发布:.NET 接入 Nacos 3.2.4,AI Registry 全能力落地09-1357.把 LLM 密钥从环境变量里解放出来:OpenClaw.NET 迎来 Vault/OpenBao 密钥后端09-1658.事件风暴遇上 MetaSkill:把企业「世界模型」变成可执行的 DAG09-2159.TensorSharp 支持 Jev 模式了:一次去噪,直接读出决策09-2360.定时任务还想上 Hangfire?这个被 AI Agent 项目看上的 TickerQ,把反射全干掉了09-2461.不让模型写作文,直接从它脑子里读答案:Jev 决策在 .NET 的两条路线09-25
    收起

    你的系统里是不是也有这样的代码?

    try { DoSomething(); }
    catch (Exception) { /* 啥也不干,异常?不存在的 */ }
    

    异常被吞掉了,日志里没有,监控看不到,问题却真实存在。今天介绍一个 .NET 的"隐藏技能",让你无论异常是否被 catch,都能在第一时间感知到。

    为了不说空话,这次我们直接打开一个真实的开源项目——OpenClaw.NET(一个 NativeAOT 友好的 .NET AI Agent 运行时与网关,GitHub 搜 clawdotnet/openclaw.net),看看它的源码里藏着多少"被优雅吞掉"的异常,以及我们如何用 FirstChanceException 把它们全部揪出来。
    image


    〇、先看源码:Agent 运行时里,异常都去哪了?

    跑过 AI Agent 的朋友可能有这种体验:Agent 突然"变笨了"——工具调用失败它不说,记忆检索挂了她也不提,只是回答质量肉眼可见地下降。

    这不是玄学。打开 OpenClaw.NET 的源码,你会发现这是一个刻意设计的结果:Agent 运行时为了保证对话不中断,会在各个层面把异常"降级"掉。

    形态一:工具执行失败 → 变成一句字符串

    src/OpenClaw.Agent/OpenClawToolExecutor.cs 中,工具执行用一个 catch-all 兜底:

    catch (Exception ex)
    {
        failureCode = ClassifyToolFailureCode(tool, ex.Message);
        failureMessage = ex.Message;
        toolFailed = true;
        // ……部分已知故障类型走 Blocked 分支……
        else
        {
            result = "Error: Tool execution failed.";   // ← 异常被压缩成一句话
            resultStatus = ToolResultStatuses.Failed;
        }
        _metrics?.IncrementToolFailures();
        _logger?.LogWarning(ex, "[{CorrelationId}] Tool {Tool} failed", ...);
    }
    

    异常对象在这里被"翻译"成 Error: Tool execution failed. 喂回给大模型。对话不会断,但真实的堆栈只有日志里才有——如果日志级别没配对,它就消失了。

    形态二:MCP 工具失败 → 同样变成字符串

    src/OpenClaw.Agent/Tools/McpNativeTool.cs:

    catch (Exception ex)
    {
        return $"Error: MCP tool '{localName}' failed: {ex.Message}";
    }
    

    形态三:记忆召回失败 → 打个 Warning 继续跑

    src/OpenClaw.Agent/AgentRuntime.cs 里大量这样的代码:

    catch (Exception ex)
    {
        _logger?.LogWarning(ex, "Memory recall injection failed; continuing without recall.");
        return false;
    }
    

    记忆召回失败,Agent 只是"失去记忆"继续回答。功能上不崩,效果上降级——这就是典型的"静默异常"。

    形态四:LLM 调用重试耗尽 → 一句客套话

    catch (Exception ex) when (IsExpectedLlmFailure(ex))
    {
        _metrics?.IncrementLlmErrors();
        _logger?.LogError(ex, "[{CorrelationId}] LLM call failed after all retries and fallbacks", ...);
        return AgentTurnResult.Completed(
            "Sorry, I'm having trouble reaching my AI provider right now. Please try again shortly.");
    }
    

    平心而论,这些写法没有问题——Agent 运行时必须韧性优先,不能因为一个工具报错就把整个对话炸掉。但代价是:异常被层层包装、降级、消化之后,排障时你看到的只是"Agent 不太好使"这个结果,而不是原因。

    有没有办法在不改这些代码的前提下,看到每一个异常"出生"的瞬间?

    有。


    一、异常处理的两个阶段

    .NET 的异常处理机制,实际上分为两个阶段:

    阶段一:First Chance Exception(第一现场)

    异常刚刚被 throw 的那一瞬间,CLR 会先触发一个"通知事件"——此时异常还没有被任何 catch 块处理。你可以把它理解为"案发现场的第一时间报警"。

    阶段二:Stack Walking(栈回溯)

    CLR 从当前栈帧开始向上遍历,寻找匹配的 catch 处理器。如果找到了(比如 OpenClaw.NET 里那些降级 catch),异常被"消化";如果没找到,最终演变为未处理异常,进程可能终止。

    异常抛出
        ↓
    【FirstChanceException 触发】← 你在这里可以观测,但不能阻止
        ↓
    CLR 开始 Stack Walking,寻找 catch 块
        ↓
    找到 catch → 执行 → 异常消失(被降级、被吞掉、被转成字符串)
    未找到 catch → UnhandledException → 进程终止
    

    二、代码实战:挂一个全局"监听者"

    .NET 提供了 AppDomain.FirstChanceException 事件,注册方式极其简单:

    using System;
    using System.Runtime.ExceptionServices;
    
    class Program
    {
        static void Main()
        {
            // 注册第一现场监听器
            AppDomain.CurrentDomain.FirstChanceException += (_, e) =>
            {
                Console.WriteLine($"[FirstChance] 捕获到异常:{e.Exception.Message}");
            };
    
            try
            {
                throw new Exception("这是一个测试异常");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"[Catch 块] 异常被处理了:{ex.Message}");
            }
    
            Console.WriteLine("程序正常结束");
        }
    }
    

    输出结果:

    [FirstChance] 捕获到异常:这是一个测试异常
    [Catch 块] 异常被处理了:这是一个测试异常
    程序正常结束
    

    看到了吗?即使异常在 try-catch 里被"完美处理"了,FirstChanceException 依然能提前感知到它。

    把它放到 OpenClaw.NET 的语境里:在网关启动处挂上这个 handler,那么无论是 MCP 工具超时、记忆召回失败、还是 LLM 重试过程中的每一次中间失败——只要异常被 throw 过,你都能看到它的原始形态,而不是被降级后的那句 "Error: Tool execution failed."。


    三、重要:你不能在这里"吞掉"异常

    很多开发者第一次用时会误以为可以在这里拦截异常,这是错误的。

    // 错误示范:试图阻止异常传播
    AppDomain.CurrentDomain.FirstChanceException += (_, e) =>
    {
        // e.Exception 是只读的,你无法修改或清除它
        // 异常会继续向上传播,不受你控制
    };
    

    FirstChanceException 的定位是"观测",不是"处理"。 你可以记录日志、发送告警、统计指标,但不能阻止异常去找它的 catch 块。

    这一点在 Agent 场景下尤其要想清楚:OpenClaw.NET 那些降级 catch 是有意为之的韧性设计,你观测归观测,别想着绕过它们。


    四、边界情况一览

    场景 FirstChance 是否触发 备注
    异常被 catch 并吞掉 ✅ 触发 这正是它的核心价值
    throw;(保留栈的重新抛出) ✅ 再次触发 会走完整的新一轮流程
    throw ex;(破坏栈的重新抛出) ✅ 触发 但 stack trace 会被重置
    ThreadAbortException ✅ 触发 通常不建议特别处理
    损坏进程状态异常(如 AccessViolationException) ⚠️ 视版本而定 .NET 4+ 默认不触发,需特殊配置

    注意第二行:OpenClaw.NET 的 AgentToolCallLoop 里并行执行多个工具调用时,任何一个工具崩了会 linkedCts.Cancel(); throw; 取消其他兄弟任务再重新抛出——重抛会再次触发 FirstChance,所以你在日志里可能看到同一个异常出现多次,这是正常现象,别误判成"异常发生了两次"。


    五、生产环境使用注意事项

    1. 防止递归死循环

    如果你在 FirstChance handler 里自己的代码又抛异常了,会无限递归。

    AppDomain.CurrentDomain.FirstChanceException += (_, e) =>
    {
        // ✅ 加上防护,防止日志代码自身异常导致递归
        if (e.Exception.StackTrace?.Contains("MyLogger") == true)
            return;
    
        // 安全地记录日志...
        Logger.Warn("FirstChance 异常捕获", e.Exception);
    };
    

    2. 性能开销

    每次异常抛出都会触发这个事件。异常抛出的成本本身就不低,加上 handler 的执行,频繁异常会严重影响性能。

    这在 Agent 运行时里是个现实问题:一个陷入"调用失败 → 重试 → 再失败"循环的工具,一轮对话可能抛出几十上百次异常。建议配合采样或限流策略使用——比如按异常类型 + 工具名做聚合,每分钟最多上报 N 条,而不是每条都写日志。

    3. 与 UnhandledException 的区别

    特性 FirstChanceException UnhandledException
    触发时机 每次异常抛出时 异常即将导致进程终止时
    能否阻止进程崩溃 不能 有限(视 IsTerminating)
    异常是否已被 catch 尚未确定 已确定没有 catch
    用途 观测/监控/诊断 临终遗言/兜底处理

    OpenClaw.NET 这类有完善降级机制的运行时,UnhandledException 可能很久都不会响一次——但这不代表系统健康,只代表异常都被消化了。想看真实水位,得靠 FirstChance。


    六、实际应用场景

    场景 1:根治"静默异常"

    项目中有些老代码喜欢这样写:

    try { CallThirdPartyApi(); }
    catch { /* 静默失败,以为很优雅 */ }
    

    上线后接口经常超时,但没有任何日志。挂上 FirstChanceException 后,所有被吞掉的异常都无所遁形。

    场景 2:诊断 Agent 的"悄悄降级"

    这就是 OpenClaw.NET 给我们展示的典型场景:Agent 回答质量下降,但没有错误日志。挂上 FirstChance 后你可能会发现——原来是记忆召回每次都在抛超时异常然后 continuing without recall,Agent 其实一直在"失忆"状态下工作。降级的路径是设计好的,但降级发生的频率和原因,只有观测了才知道。

    场景 3:全链路异常监控

    配合 APM 工具(如 SkyWalking、Elastic APM),在 FirstChance 中打上标记,构建异常热力图,哪怕异常被内部消化了,也能知道哪里是"异常高发区"。

    场景 4:诊断偶发 Bug

    某些难以复现的问题,往往是因为异常在某个深层库被 catch 后走了降级逻辑。通过 FirstChance 日志,你能看到异常原本长什么样,而不是被包装后的版本——比如不是 "Error: Tool execution failed.",而是底层的 HttpRequestException: Connection refused。


    七、总结

    AppDomain.FirstChanceException 是 .NET 提供给我们的一个"上帝视角"——它不参与异常处理决策,但让你有机会看到每一个异常诞生的瞬间。

    记住三个关键点:

    1. 它能让你观测到所有异常,无论是否被 catch
    2. 它不能阻止异常传播或吞掉异常
    3. 生产环境使用要注意递归防护和性能影响

    而 OpenClaw.NET 的源码则给了我们一个很好的参照系:一个健壮的系统,必然到处都是有意的 catch;异常被消化不等于问题不存在。 观测与韧性,是一个硬币的两面——降级逻辑保证系统不崩,FirstChance 保证你能看见它为什么降级。

    下次当你面对一个"明明感觉有问题但日志里什么都没有"的系统——或者一个"突然变笨"的 AI Agent——不妨试试挂上这个 handler,说不定会有意外收获。


    本文涉及的源码:github.com/clawdotnet/openclaw.net(MIT 协议,NativeAOT 友好的 .NET AI Agent 运行时,感兴趣的朋友可以 star 一下)

    欢迎大家扫描下面二维码成为我的客户,扶你上云

  • 相关阅读:
    ModStart系统安全规范建议
    企业级环境部署:在 Linux 服务器上如何搭建和部署 Python 环境?
    Github 2024-04-22 开源项目日报Top10
    代数与逻辑:作业三 贝叶斯决策
    如何把照片转成pdf文件,支持合并转换
    P1634 禽兽的传染病
    SpringBoot使用WebSocket收发实时离线消息
    基于51单片机NEC协议红外遥控发送接收仿真设计( proteus仿真+程序+原理图+报告+讲解视频)
    QT内存管理
    map转换成JSON的方法
  • 原文地址:https://www.cnblogs.com/shanyou/p/22814535
  • 最新文章
  • 给Claude Code装上40个Skill后,我才发现之前都白用了
    数字化转型的价值迷思:从技术指标到业务结果
    DBA 经验:MySQL性能最重要的参数只有2个!
    嵌入式里面互相嵌套的结构体
    Python 创建 Excel 下拉列表的两种方法
    数字航空摄影:经典计算与参数推演
    BrowserAct,给AI Agent配一个真实浏览器,从安装到实战
    Whim 是什么?一门把 PHP 被否决的设想真跑起来的实验语言
    模型聪明了 10 倍,为什么你的 Agent 每天还在过《土拨鼠之日》?
    FastAPI 使用 session 认证
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
小工具 小游戏
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1

京公网安备 11010502049817号