降级与容错
真实环境里的失败不是异常分支,而是常态:供应商限流、embedding 服务超时、索引尚未就绪、资料被删除、预算不够、用户取消。容错的目标不是让每个请求都成功,而是让失败可解释、可回退,并且不伪造结果。
先分类失败
| 失败 | 典型原因 | 能否重试 |
|---|---|---|
| 模型调用 | 超时、限流、5xx、鉴权失败 | 限流与瞬时故障可有界重试;鉴权失败不重试 |
| 工具执行 | 参数无效、未授权、业务拒绝、副作用失败 | 只读查询可重试;写操作必须有幂等键 |
| 检索 | embedding 超时、索引未就绪、无命中 | 可退回关键词或整篇读取 |
| 资料 | 文件缺失、空资料、revision 过期 | 不重试,明确说明资料不可用 |
| 预算与取消 | 上下文超限、用户取消、期限到达 | 不重试,按原合同结束 |
分类决定后续动作。把所有失败都当成“再试一次”,会放大限流、重复副作用,并掩盖真正的配置问题。
三种处理方式
- 有界重试:只用于瞬时、幂等、可取消的调用。次数、间隔和总期限都要有上限,并继续传播
signal。 - 降级:缩小能力范围,但保留真实来源和身份。例如检索失败时退回关键词,向量不可用时退回整篇读取。
- 明确失败:无法安全降级时直接说明失败或提出澄清,不编造结果。
降级不等于换一条更宽松的权限路径,也不等于绕过校验。身份、授权、预算和取消在任何降级路径上都应继续生效。
一张处理矩阵
| 场景 | 建议动作 | 不要做 |
|---|---|---|
| 模型限流 | 有界重试,保留原请求与取消 | 无限重试或换用未授权模型 |
| 模型鉴权失败 | 明确报错,记录配置来源 | 静默退回通用回答 |
| 工具参数无效 | 返回校验错误,必要时澄清 | 自动改写用户意图再次执行 |
| 写操作超时 | 按幂等键核对真实提交 | 直接重试造成重复写入 |
| embedding 失败 | 退回关键词检索 | 用旧向量冒充新结果 |
| 索引未就绪 | 整篇读取或关键词检索 | 把“未就绪”当成“没有资料” |
| 检索无命中 | 明确说明没有资料 | 用常识补写事实 |
| 预算超限 | 减少候选或分次读取 | 截断规则或证据 |
| 用户取消 | 停止后续调用并保留原因 | 把取消记录为成功 |
超时与取消
- 每一层使用独立超时,并让总期限覆盖整条调用链;内层超时不应晚于外层。
- 取消通过
signal传播;已经发生的外部副作用不能假装回滚。 - 超时与取消是不同结果。超时可以重试,取消通常不应重试。
- 流式输出中断时,不要补发成功结束标记。
不要做的降级
- 为了提高成功率而放宽授权、跳过来源校验或使用客户端自报身份。
- 把检索失败包装成“没有这项资料”,让用户误以为事实不存在。
- 把旧索引副本、旧缓存或上一次回答当成当前事实。
- 在取消后继续调用工具或模型,并丢弃迟到结果。
- 用固定兜底文案覆盖所有失败,导致问题无法被发现。
观测
建议记录:失败阶段、工具或检索器 ID、错误码、尝试次数、是否降级、降级原因、总耗时和取消原因。不要记录密钥、完整提示词和未脱敏个人资料。
日志应能回答“这次为什么降级”,而不只是“这次成功了没有”。降级次数持续上升时,应先查依赖健康度,而不是继续调提示词。
用 MockGPT 验证
失败路径应当可重复验证。MockGPT 离线验证 可以注入 HTTP 错误、网络拒绝、流中断与取消,用来确认:
- 重试次数是否被限制;
- 降级是否真的发生,而不是静默成功;
- 写操作失败后是否按幂等键核对;
- 取消后是否停止后续调用。
真实依赖的健康检查仍由部署环境负责,模拟只验证代码分支。
一个判断顺序
ts
type Outcome =
| { kind: 'answer'; sources: string[] }
| { kind: 'unknown'; reason: 'no-material' }
| { kind: 'degraded'; reason: 'retrieval-unavailable' }
| { kind: 'failed'; reason: string };
async function answer(question: string): Promise<Outcome> {
const sources = await retrieve(question).catch(() => undefined);
if (!sources) return { kind: 'degraded', reason: 'retrieval-unavailable' };
if (!sources.length) return { kind: 'unknown', reason: 'no-material' };
return { kind: 'answer', sources };
}示例只表达顺序:先区分“检索不可用”和“确实没有资料”,再决定回答、降级或失败。真实的授权、预算和取消检查仍由各自的接口完成。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 限流时请求越来越多 | 重试没有上限或没有退避 | 设置次数、间隔与总期限 |
| 失败被当成“没有资料” | 降级原因未区分 | 分开 retrieval-unavailable 与 no-material |
| 写操作重复 | 超时后直接重试 | 使用幂等键并核对真实提交 |
| 取消后仍有输出 | 未传播 signal | 在每层调用前检查取消 |
| 降级长期不被发现 | 只记录成功指标 | 单独统计降级与失败原因 |
下一步
- 需要注入失败做回归:MockGPT 离线验证。
- 需要定义检索失败时的资料策略:RAG 资料搭建。
- 需要发布前确认失败路径:上线前的软性检查。