Engineering note
错误处理、超时与取消:让失败有边界
这是「Agent 工程实战」的第 12 篇。专题从一个能聊天、能调工具的 .NET Agent 出发,逐步补齐可靠性、安全、测试与发布能力。
本篇要解决的问题: 处理 HTTP 异常、流中断、工具失败和用户取消,避免一次故障拖垮会话。
本章定位:本章把异常、超时、取消和重试从偶发问题变成可设计的状态。你会学习如何划分异常边界,以及为什么副作用工具必须考虑幂等性。
建议阅读方式:先通读原理,再对照当前项目源码,最后完成本章实践。一个好的错误处理系统不是让所有请求都成功,而是让失败可解释、可恢复、可审计。
本章导读
本章把异常、超时、取消和重试从偶发问题变成可设计的状态。你会学习如何划分异常边界,以及为什么副作用工具必须考虑幂等性。
本章采用“源码观察 → 概念拆解 → 工程改造 → 实践验证”的顺序。示例中的接口和代码骨架用于说明设计方向,真正提交代码时应结合项目当前状态逐步落地。
12.1 错误分层
建议区分:
| 错误 | 处理方式 |
|---|---|
| 参数错误 | 不执行工具,把可读错误返回模型 |
| 权限错误 | 记录审计,返回拒绝原因 |
| 工具业务失败 | 返回结构化失败结果,可允许模型修正参数 |
| 网络瞬时失败 | 有限重试 |
| 认证/配置错误 | 立即终止本轮并提示用户 |
| 程序内部错误 | 记录完整异常,向模型返回通用错误 |
不要把堆栈、密钥、环境变量和任意本地路径全部暴露给模型。
12.2 取消令牌要贯穿调用链
理想调用链如下:
Console.CancelKeyPress
-> BooAgent.Start(token)
-> AgentLoop.RunAsync(token)
-> IModelClient.StreamAsync(token)
-> IToolExecutor.ExecuteAsync(token)
-> File/Process API(token)
只有最底层支持取消还不够,上层必须把 token 传下去。
12.3 重试不能重试一切
可以重试连接中断、429 和部分 5xx;不应重试参数错误、权限拒绝和明确的业务失败。工具写入操作还要考虑幂等性,否则重复执行可能产生多份文件或重复副作用。
12.4 本章交付物
- 统一错误码;
- 请求、工具和整轮 Agent 的超时;
- Ctrl+C 取消;
- 有限重试策略;
- 错误日志和用户可读提示。
12.5 异常边界应该在哪里
推荐每一层只捕获自己能处理的异常:
- HTTP 层把网络异常转换为模型请求错误;
- SSE 层把非法事件转换为协议错误;
- 工具执行层把业务异常转换为
ToolResult; - Agent Loop 决定错误是重试、交给模型还是结束会话;
- UI 层把最终状态显示给用户。
不要在最外层用一个 catch (Exception) 把所有问题都变成“发生错误”。那样虽然不会崩溃,但会丢失修复问题所需的上下文。
12.6 超时的层级
一次用户请求可能包含多个模型请求和多个工具调用,应该有层级化的 deadline:
本轮总 deadline:60 秒
模型请求:30 秒
单个工具:10 秒
单个工具超时后,剩余总时间仍然应该受到约束。不要每次循环都重新创建一个完整时长的 timeout,否则 Agent 可以通过反复工具调用绕过总时限。
12.7 重试与幂等性
读取操作通常可以重试;写入、删除、发送消息等副作用操作需要幂等键或人工确认。一个简单的规则是:只有明确标记为 idempotent 的工具允许自动重试。
public sealed record ToolPolicy(
bool IsIdempotent,
bool RequiresApproval,
TimeSpan Timeout);
重试日志要包含次数和原因,最终错误要告诉 Agent 已经尝试过几次,避免模型重复安排相同动作。
12.8 练习:故障注入
给 fake model client 和 fake tool executor 增加故障开关,分别模拟超时、取消、429、工具异常和非法 JSON。用测试验证每类故障的状态、是否重试以及最终用户看到的提示。
12.9 本章产出
完成本章后,异常不再是随机的堆栈,而是可以分类、观察、重试或安全终止的业务状态。
单篇实战作业
实践:注入网络超时、工具异常和用户取消,画出每一种错误在模型、工具和 UI 层分别如何表现。
建议把作业拆成一个独立提交,并在提交说明中写清楚:改动前的行为、改动后的行为、验证命令、尚未解决的风险。一个好的错误处理系统不是让所有请求都成功,而是让失败可解释、可恢复、可审计。
章节复盘
复盘问题:一次失败是否可能重复执行副作用?为每个可重试工具写出幂等性结论。
本章的完成标准不是把所有设计一次性做完,而是能把它变成项目中的一个明确边界,并为下一章留下可验证的接口。
下一篇:第 13 篇《记忆、会话与上下文窗口:哪些信息应该留下》
如果你正在把 Agent 放进真实工作流,建议完成本篇的实战作业后再继续:每一步都应留下可验证的代码、测试或运行记录。