Java转AI Agent教程(第九周)错误矩阵

W9 · 错误矩阵:重试、幂等、限流

本周交付 :RetryingLlm(重试装饰器)+ RateLimiter(令牌桶)+ 幂等账本 + drill w9

预计耗时 :主块 4h(重构装饰器 1h / 限流 1h / 幂等 0.5h / 演练 1.5h)

前置 :W8 通过;W2 的 RetryPolicy / ErrorClassifier 还在


1. 一句话

把重试从"每个实现里各写一遍"提成一个装饰器,然后用"实际调用次数"而不是"有没有抛异常"来验收。


2. 为什么要把重试提出来

W2 的重试写在 LlmClient 内部。到 W9 出问题了:

错误矩阵要统计"30 次请求里实际发生了多少次重试"。如果重试藏在模型实现内部,脚本模型和真模型就得各写一遍重试逻辑,两套逻辑迟早不一致,测出来的数字也就不敢信。

改成装饰器之后:

java 复制代码
Llm llm = new RetryingLlm(new ScriptedLlm(), policy);   // 脚本模型 + 真重试
Llm llm = new RetryingLlm(new LangChain4jLlm(...), policy);  // 真模型 + 同一套重试

无论底层是谁,重试行为完全一致。 这是能测的前提。

顺带的好处:W11 之后加"多模型降级",也只是再套一层装饰器,不用改任何实现。


3. 四条验收线

3.1 重试次数有界------断言"调用次数",不是"有没有抛异常"

java 复制代码
for (int i = 0; i < 10; i++) {
    p.run("SN202610200001 我要退货 #[429]");
}
c.eq("实际调用次数有界(= 10 × 3)", 30, scripted.totalCalls());

如果只断言"抛了异常",你测不出"重试了 300 次"这种雪崩。

实测:429 / 500 / timeout 三种故障各 10 次请求,每次都是 3 次调用后放弃,共 30 次,全部降级为转人工,无异常逃逸。

3.2 不可重试的错误一次都不该重试

错误 码 调用次数
429 RATE_LIMIT 3
500 SERVER_ERROR 3
超时 TIMEOUT 3
401 AUTH_FAILED 1

401 重试三次只会让你多等 2 秒、多刷三条日志、多烧 3 次 token------鉴权失败重试没有意义。

这条能力来自 W2 的 ErrorClassifier。它默认"认不出来的错按 NEED_HUMAN 处理",就是为了防止这种情况。

3.3 幂等:重复提交不产生第二笔

java 复制代码
var p1 = guard.propose("u1", "SN202610200001", new BigDecimal("50"), "外壳划痕");
var e1 = guard.execute(p1, Approval.system());   // R2-0001
var p2 = guard.propose("u1", "SN202610200001", new BigDecimal("50"), "外壳划痕");
var e2 = guard.execute(p2, Approval.system());   // R2-0001(同一个单号)

实测:两次返回同一个退款单号,movedMoney 仍然是 50 元。

为什么幂等键是 (userId, orderId) 而不是 proposalId?

因为重试场景下,第二次请求根本拿不到第一次的 proposalId------它是新生成的。业务键才是稳定的。

propose 遇到重复时返回原提案而不是抛异常,也是故意的:重复提交是正常业务(用户连点、消息重投),抛异常会让上层以为出错了,进而触发更多重试------正好放大问题。

3.4 限流:令牌桶 vs 漏桶

算法 特点 适用
漏桶 输出速率恒定,能整形流量 下游要求匀速时
令牌桶 允许突发(桶里攒令牌),长期速率受限 售后场景(用户一急就连发三条)

实测:容量 5、每秒补 0.5,连发 6 次放行 5 次,第 6 次被拒并给出 retryAfterMs = 2000。

代价要说清楚:突发上限 = 桶容量,所以容量必须给得小,否则"突发"就等于"没限流"。容量 5 / 每秒 0.5 ≈ 每分钟 30 次。

为什么按用户限流而不是全局?

全局限流只保护自己不被网关打死,保护不了"一个用户吃光额度、其他人全部排队"。按用户限流的真正作用是:把一次异常行为的影响面锁在单个人身上。


4. 验收

bash 复制代码
cd 教程/W1-W4/ecom-agent-lab
bash run.sh drill w9

实际输出:

复制代码
故障         错误码          调用次数     最终结果       说明
429        LLM_RATE_LIMIT 30       全部降级       10 次请求 × 3 次尝试 = 30 次实际调用
500        LLM_SERVER_ERROR 30       全部降级
timeout    LLM_TIMEOUT  30       全部降级

--- 不可重试的错误 ---
  401 → LLM_AUTH_FAILED,实际调用 1 次

--- 幂等 ---
  第一次:R2-0001,第二次:R2-0001,累计退款 50.00 元

--- 限流 ---
  连发 6 次,放行 5 次,建议等待 2000ms

--- 雪崩检查 ---
  20 次故障请求,最长状态路径 2 个状态

W9 错误矩阵:通过 15 / 失败 0

判定标准:

  1. 三种可重试故障各 10 次请求,实际调用次数 恰好 30(不是 10,也不是 300)
  2. 401 只调用 1 次
  3. 幂等:同一订单两次执行返回同一退款单号,金额只扣一次
  4. 限流:容量 5 时第 6 次被拒,且 retryAfterMs > 0
  5. 故障场景下最长状态路径 ≤ 4(说明没有雪崩式重入)

5. 本周踩的坑

坑 1:脚本模型抛自定义异常,分类器走不到第一层。

第一版 ScriptedLlm 抛 new RuntimeException("429 ..."),ErrorClassifier 只能走"文案兜底"那一层,连 code 都是 UNKNOWN。

改成抛 LangChain4j 自己的异常类型:

java 复制代码
raw = new RateLimitException("429 Too Many Requests(脚本注入)");
throw wrap(raw);   // wrap 里走 ErrorClassifier.verdict(),得到 RATE_LIMIT

这样测到的分类路径和真网关完全一致。自定义异常测出来的结论换个环境就不成立。

坑 2:脚本模型 + 装饰器不匹配,异常逃逸了。

RetryingLlm 只 catch LlmCallException。脚本模型直接抛 RuntimeException,于是"重试"根本没发生,异常一路冒到 drill 的 main。修法是脚本模型统一用 wrap() 包成 LlmCallException。

这条坑的教训是通用的:装饰器的契约是"只认一种异常",那么所有实现都必须遵守。 契约不写清楚,第三个实现就会破。

坑 3:把"没有雪崩"理解成"重试次数少"。

真正的雪崩不是重试次数多,是没有总预算 :每个请求重试 3 次,并发 100 就是 300 次同时打向一个正在 429 的网关。RetryPolicy.totalBudget + 总步数上限 + 按用户限流,三者缺一不可。


6. 自检清单

  • 能说出为什么重试要做成装饰器而不是写在实现里
  • 能说出 401 为什么一次都不该重试
  • 能解释幂等键为什么用业务键而不是 proposalId
  • 能说出令牌桶和漏桶各适合什么场景
  • 能解释"按用户限流"防的是什么(不是防自己被打死)
  • drill w9 15 条断言全绿
  • 把 maxAttempts 从 3 改成 1,看哪条断言变红(理解"次数有界"的测法)

7. 下一步

到这里,系统已经很稳了。但它有一个根本性缺口:

以上所有机制,都只能让模型"说错话"的概率下降。

没有一条能阻止"钱被退出去"。

W10 是 B 阶段唯一真正能防住资损的一周。

相关推荐
文丑颜不良啊25 分钟前
LeetCode1901~1920题目解析
java·leetcode
lisw0528 分钟前
作为人类代理的大语言模型及其角色分类!
人工智能·学习·语言模型
高洁0130 分钟前
自然语言处理的技术演进与核心任务
人工智能·python·深度学习·机器学习·迁移学习
Jsundoku34 分钟前
第十四章 深度学习 -- 基于模型的方法
人工智能·强化学习
tealcwu38 分钟前
【Unity基础】为什么游戏需要物理按键映射?从 Unity Input Manager 到跨引擎输入设计
java·游戏·unity
狂师43 分钟前
AI 测试 Skill 大全,我日常在用的 27个,夯爆了!
人工智能·测试·全栈
AI源码44 分钟前
基于机器学习的酒店数据分析及流失预测系统
人工智能·机器学习·数据分析
敢敢のwings1 小时前
Super Odometry 2.0:相机与激光雷达退化后,机器人如何继续估计运动
人工智能·数码相机·机器人
vipxieliang1 小时前
Spring Boot 性能优化实战:从启动加速到运行时调优
java·spring boot