在功能还未实现时就要梳理失败路径,包括超时、检索失败、输出格式错误等每一步的可能性,以此驱动 API 契约、遥测和测试设计。
考虑一个准备回复并可选择保存为草稿的支持功能:
解析工单并评估访问权限
-> 检索已授权的工单内容和策略上下文
-> 调用模型
-> 验证生成的回复
-> 按请求保存草稿
-> 返回结果
这个序列很容易理解。但它也几乎回避了所有困难的问题。
当检索没有返回任何策略文档时,用户看到的是什么?模型是否还有机会回答?如果输出验证失败,应用能否修复它?如果保存超时,草稿是不存在、已存储、还是仍在处理中?重试是否会产生第二条草稿?哪种失败应该触发运维人员介入?
这些都是功能契约的一部分。如果团队把这些推迟到实现阶段,答案往往会变成异常处理器或 SDK 默认值碰巧吐出来的东西。
我更愿意先把那些别扭的case摆到台面上。一旦这些决策被写下来,实现成功路径往往就是简单的部分了。
在列举基础设施故障之前,先写下用户认为这个操作在做什么。
对于支持功能,承诺可能是:
根据工单和已批准的支持策略准备回复。仅在用户请求时将其保存为草稿。
现在写下系统绝不能呈现为成功的那些结果:
这个简短的列表会改变设计。缺失的检索上下文不能悄悄变成正常的模型请求。模型响应在通过输出契约验证之前不能变成持久化草稿。保存成功需要一个确认的写入,而不是没有异常。
不可接受的结果比"优雅地处理错误"这种泛泛目标更重要。它们阐明了当流程某部分失败时,应用必须保留哪些属性。
严格来说,这个练习中的每一行不一定是系统故障。授权拒绝可能是正确行为。NoEvidence 可以是合法的领域结果。我把这些 adverse outcomes 也包括进来,因为它们仍然可能阻止功能兑现承诺,而且应用需要一个明确的响应。
如果已经绘制了围绕模型的依赖关系图,用那张交互图作为输入。否则,为这个流程勾勒出重要的调用和状态变化。另一份 Azure 服务、SDK 客户端和数据库的清单不会告诉你这个功能应该如何表现。
同一个服务在不同交互中可能有不同的失败方式。读取策略上下文可能允许返回简化结果。授权访问则不行。数据库读取和草稿写入可能使用同一个数据库,但当结果未知时,它们有不同的后果。
一次取一个交互,问自己:
这个交互可能变慢、不可用、出错、过时、未授权、重复或模糊吗?
这对用户可见的操作有什么影响?
数据或外部状态是否可能已经改变了?
什么响应能保留功能的承诺?
哪个信号能在生产环境中区分这种情况?
如何在测试中强制它发生?
用类别列表来提醒自己;当更多行只是描述相同的效果和响应时,就停止。一个模型调用可以返回有效 JSON 但包含不支持的声明。写入可以在其确认丢失后成功。调用者取消可能在工作开始后才到达。这些 case 比又一行只说"依赖不可用"的条目有用得多。
对于支持流程,第一版可能是这样的:
| 交互 | 可能的失败 | 用户可见效果 | 应用响应 | 诊断信号 | 测试方式 |
|---|---|---|---|---|---|
| 检索策略上下文 | 无结果 | 模型无法生成有依据的回复 | 标记 NoEvidence,降低能力或拒绝 | EvidenceStatus=Missing | mock 返回空列表 |
| 调用模型 | 超时 | 回复超时 | 在剩余预算内重试或返回 TimedOut | LatencyMs 超限 | 注入延迟 |
| 验证输出 | 无效 JSON / 违反契约 | 不能使用模型输出 | 标记 InvalidGeneration,通知用户 | ValidationError | mock 返回畸形输出 |
| 保存草稿 | 写入失败 | 用户以为保存了但实际没有 | 标记 Failed,使用 Unconfirmed 处理边界 | PersistenceError | mock 抛出写入异常 |
如果这些格子不影响实现,这张表就是徒劳的。"记录错误并重试"没有回答重试是否安全、调用者收到什么、以及操作何时停止。
外部效果和本地审计记录不在同一个事务边界内。在效果之前记录持久化意图。之后,协调并记录最终结果。根据操作不同,那条恢复路径可能还需要幂等键或补偿。
不要试图枚举每种异常类型。当失败有相同的效果和响应时,将它们分组。当系统必须表现不同时,将它们拆分。
团队经常从技术症状直接跳到弹性机制:
timeout -> retry
invalid output -> retry
dependency unavailable -> fallback
这跳过了真正重要的决策:失败对操作做了什么?
在幂等的策略读取上超时,和在草稿写入提交后超时,不是一回事。两者在应用边界都可能呈现为超时。读取没有副作用,可以在剩余预算内再次尝试。写入的结果是模糊的。用新身份重试可能产生重复。
根据效果和已知状态选择响应。异常名称只是一个输入。
对于每一行,我使用一小套响应形状之一:
具体集合属于应用本身。调用者应该收到应用结果,而不必解释异常文本。
一旦失败表稳定下来,就把端点或 UI 需要处理的结果编码进去。
public enum ReplyOutcome
{
Prepared,
NoEvidence,
InvalidGeneration,
TimedOut,
TemporarilyUnavailable
}
public enum DraftSaveOutcome
{
NotRequested,
Saved,
RejectedByPolicy,
Failed,
Unconfirmed
}
public sealed record PrepareReplyResult(
ReplyOutcome ReplyOutcome,
string? Reply,
DraftSaveOutcome SaveOutcome,
Guid? SaveOperationId);
RejectedByPolicy 意味着应用在持久化之前故意拒绝了保存。Failed 意味着应用知道持久化没有提交。Unconfirmed 意味着写入可能已提交,但应用尚未验证结果。
在写入之前分配操作 ID:
Guid operationId = Guid.NewGuid();
var command = new SaveDraftCommand(
OperationId: operationId,
TicketId: ticketId,
Reply: reply);
DraftSaveResult saveResult = await draftStore.SaveAsync(
command,
cancellationToken);
存储必须将 OperationId 与草稿或操作记录一起持久化,并强制执行唯一性。重用 ID 执行不同命令必须失败。持久化足够的命令标识(如 ticket ID 和规范命令指纹)以检测冲突重用。如果响应在提交后消失,应用按该标识查询,任何安全的重试都会重用它。否则,ID 给 worker 没有任何权威的东西来协调。
这个紧凑的结果类型仍然允许无效组合。生产代码可能使用工厂方法或结果层次结构来防止这些问题。即便如此,它使两个决策变得明确:回复生成和草稿持久化有独立的结果,未确认的保存不会报告为成功或失败。
端点现在可以 deliberateness 地映射这些结果。UI 可以说回复已准备好,但其保存状态仍在检查中。后台 worker 可以协调同一个操作 ID。遥测可以记录稳定的結果名称,而不是特定提供商的异常消息。
完整的失败模式分析可能快速增长。不要给每一行同等的关注。
在评审中,我从三个问题开始:
我更喜欢 critical、high、normal、low 这样简单的优先级标签,而不是将猜测的分数相乘成一个看起来科学的数字。未知的写入结果、跨租户数据暴露、静默使用过时策略、以及看起来像成功的失败,这些比应用已经诚实暴露的干净提供商错误更值得优先关注。
把处理决策分开:缓解、接受或推迟。高优先级风险在团队理解后果并认为缓解不合理时仍然可以被接受。"我们在第一版不支持草稿恢复"可能是合法决策,只要 UI 从不声称未确认的写入成功,且后果可接受。未记录的差距与已接受的风险不是一回事。
对于每个重要行,团队应该能够强制或模拟相关条件和效果。一些提供商和基础设施故障无法精确重现,但它们的应用程序可见行为通常可以。
第一版不需要生产规模的混沌平台。使用 fake 客户端和受控的测试替身来返回无上下文、延迟模型调用、产生畸形输出、拒绝授权、或在提交后丢失写入确认。
对于每个高优先级行,验证四件事:
至少将确定性 case 保留在正常测试套件中。以团队实际会维持的节奏运行较慢的依赖项和恢复演练。
同样的行指导代码和测试。在事故期间,它们也告诉运维人员哪些行为是故意的。
分析应该确定哪里可能需要进行另一次尝试。之后整体决定重试行为。这不意味着重试整个工作流。
一次尝试是否安全取决于幂等性、失败类别、剩余时间、提供商节流和任何副作用的范围。一个重试可能对一个行是有效响应,但会使下一行变得更糟。
五秒的模型超时本身说明不了什么。它必须适合检索、模型调用、验证、工具、持久化和任何同步恢复尝试的请求预算。持久化恢复超出请求寿命需要自己的截止日期或运营目标。
在失败遍历期间,记下哪些行需要重试或预算决策。不要为了填格子而发明本地重试计数或超时。
当 AI 功能读取受保护数据、依赖检索、做出权威声明、调用工具、更改状态、或创建关于延迟和可用性的有意义承诺时,使用失败优先遍历。在更改 prompt、模型或提供商时也很有用,因为这些更改可能改变输出契约或运行时行为。
使用合成数据且没有副作用的一次性本地实验不需要研讨会或大型登记册。写下少数可能使实验失效的 case,保持错误可见,然后继续。
让方法和功能成比例。小实验需要一个简短的清单,而不是一个可靠性计划。
选择一个重要的 AI 流程,在添加更多快乐路径代码之前,花 45 分钟处理其不高兴的路径。
写下用户承诺和绝不能呈现为成功的结果。遍历每个交互,记录合理失败的效果,分配应用响应、诊断信号和测试中强制 case 的方式。
如果一行没有定义响应或无法测试,那就是未完成的设计工作。
Calling a Model Is Easy. Running an AI System Is Not
The Model Is Only One Dependency. Map the Rest.
When an AI Request Should Become a Background Job
Microsoft Learn: Architecture strategies for performing failure mode analysis
Microsoft Learn: Transient fault handling
Microsoft Learn: Connection resiliency in Entity Framework Core
Microsoft Learn: Architecture strategies for designing a reliability testing strategy