TT Lab
开始
学习 学习路径 课程

AI 智能体 — 是图不是模型

连被拒的那笔也重发了三次

在 TT Lab 中继续学习

目标

给节点加上 RetryPolicy,让它只重试可以重试的失败。附上幂等键,使同一个请求不被处理两次;统计调用预算,耗尽时不再重试而是放弃,并留下结果。判定全部靠次数——不测量时间或速度。

为什么重要

重试一行就能开启。所以开了就不去确认。然而 RetryPolicy 的默认 retry_on 不会重试 ValueError、TypeError、RuntimeError、OSError 这类异常(这是在本实验环境的 langgraph 0.2.60 中确认过的事实)。我们自己写的异常多半继承这些类,所以即使加了策略,尝试记录里也只会留下一行。既没有错误,也没有警告。 反过来,retry_on 设得宽,就连永久性失败也会重复到上限。一次银行卡被拒,会积累三行尝试,正常条目能用的份额就相应被吃掉。所以需要一个区分“可以重试的失败”的标准——把同一个请求原样再发一次,有没有可能得到不同的答案。 要让重试安全,还有一个条件。超时断开的请求只是没收到响应,对方那边可能已经处理了。原样再发,就会被扣款两次。必须给每个请求附上键,由接收方用这个键来看“是否已经处理过了”。 最后,重试会原样消耗调用预算。前面的条目只要一抖动,后面的条目连尝试的机会都没有。预算按工具主体运行的次数而不是条数来数,耗尽时不抛异常,而是作为结果记录中的一行留下。 评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的键、金额和失败计划来运行,并把 ATTEMPTS 中积累的行数和账本中生效的金额,与评分器另外计算的值对照。

步骤

  1. 在 /root/work/agbudget/budget.py 中创建 MAX_ATTEMPTS、TransientError、PermanentError、ATTEMPTS、LEDGER、PLAN、reset()、applied_total()、charge()、State、build_plain()、run_once()。因为没有重试,失败的话尝试只有一次。
  2. 增加 DEFAULT_RETRY、build_default_retry()、run_default()。确认给节点加上没有写 retry_on 的 RetryPolicy 后,尝试仍然只有一次。
  3. 增加 RETRY_ALL、build_retry_all()、run_retry_all()。明确写出 retry_on=(ValueError,),暂时性失败就会重新生效,超出上限时最后一个异常原样抛出。
  4. 增加 is_retryable(exc)、RETRY_SPLIT、build_split()、run_split(),让永久性失败不再重试。永久性失败的尝试次数要从 MAX_ATTEMPTS 降到 1。
  5. 在 charge() 中加入幂等键。即使用同一个 request_key 被调用两次,账本里也只留一行,第二次调用会带着 duplicate 标记返回当时的那张收据。
  6. 增加 BudgetExhausted、call_with_budget()、build_budgeted()、run_budgeted()。预算不剩时不调用工具,即使放弃,也要留下 outcome 为 "예산초과"(韩文,意为“超出预算”)的记录。
  7. 增加 settle_batch(orders, budget),让多条共用一个预算。预算耗尽之后的条目记为 skipped。
  8. 在 /root/work/agbudget/budget_report.json 和 /root/work/agbudget/budget_report.md 中记录测得的次数。

参考

没有重试,尝试就只有一次

在 /root/work/agbudget/budget.py 中创建 MAX_ATTEMPTS、TransientError、PermanentError、ATTEMPTS、LEDGER、PLAN、reset()、applied_total()、charge()、State、build_plain()、run_once()。build_plain() 返回没有加重试的、已经 compile() 的图。

charge 每次被调用,都先往 ATTEMPTS 追加一行,再看 PLAN——因为失败的尝试也要算。节点不要吞掉失败,要把异常原样抛出。在节点内用 try/except 捕获的话,重试就根本不会生效。run_once 用 try/except 包住 invoke,把异常转成记录。节点名称和状态键重名的话,编译会崩溃,所以要取得不同。

加了策略,为什么只做一次

增加 DEFAULT_RETRY = RetryPolicy(max_attempts=MAX_ATTEMPTS, initial_interval=0.01, backoff_factor=1.0, jitter=False) 和 build_default_retry()、run_default()。retry_on 不写。运行一次暂时性失败,确认尝试仍然是 1 次。

使用 from langgraph.types import RetryPolicy,以 add_node(..., retry=DEFAULT_RETRY) 加上。这一步是有意复现不生效的情形——默认的 retry_on 不会重试 ValueError 一系。评分器会同时检查策略是否真正加在了节点上(compile() 后的图的 nodes["settle"].retry_policy)和尝试次数。所以不能原样返回 build_plain()。

自己写出要重试的内容

增加 RETRY_ALL = RetryPolicy(retry_on=(ValueError,), max_attempts=MAX_ATTEMPTS, ...) 和 build_retry_all()、run_retry_all()。暂时性失败 n 次之后成功,尝试就是 n+1 次,超过 MAX_ATTEMPTS,最后一个异常就原样抛出。

给 retry_on 的是异常类的元组——只给一个类时也别漏掉逗号。max_attempts 是总尝试次数,所以是 3 的话就是第一次加重试两次。上限用完仍然失败,抛出的是没有附带重试标记的最后一个异常。那个痕迹必须数 ATTEMPTS 来亲自留下。

不把永久性失败发三次

创建 is_retryable(exc),并增加 RETRY_SPLIT = RetryPolicy(retry_on=is_retryable, ...)、build_split()、run_split()。永久性失败的尝试次数要从 MAX_ATTEMPTS 降到 1。

retry_on 也可以给 (예외) -> bool(占位符为异常)函数。区分的标准只有一个——把同一个请求原样再发一次,有没有可能得到不同的答案。只把可以重试的种类设为真,不认识的异常设为假。把真作为默认值,永久性失败每次换个新名字出现,都会悄悄漏过去。评分器会把同一个永久性失败分别用 run_retry_all 和 run_split 运行,比较尝试次数。

来两次,也只生效一次

让 charge() 在收到同一个 request_key 两次时,不在账本里新建一行,而是带着为真的 duplicate 返回当时的那张收据。账本的行数和 applied_total() 不能变成两倍。

先用键扫描账本,看是否已经存在。seq 也必须原样保持——如果另外编新号,同一条就会看起来是两条。必须在产生失败的位置之后去查找。键是由调用方确定的值,这一点很重要。评分器会混入两条不同的记录以及其中一条的重复投递,同时检查账本的行数、合计和 seq。

耗尽就放弃,但留下记录

增加 BudgetExhausted、call_with_budget(request_key, amount, budget)、build_budgeted()、run_budgeted(request_key, amount, budget)。len(ATTEMPTS) 达到或超过 budget 时不调用工具,这种情况下返回 outcome 为 "예산초과"(韩文,意为“超出预算”)的记录。

预算不是条数,而是工具主体运行的次数。不要让 is_retryable 把 BudgetExhausted 判为真——因为没有预算而失败,重试也没有用。预算检查必须在调用 charge 之前,这样尝试才不会再多一格。run_budgeted 不要把异常抛到外面,要转成包含 outcome 的记录。

前面条目的重试吃掉后面条目的份额

增加 settle_batch(orders, budget)。多条共用一个预算,预算不剩时,剩下的条目不调用工具,而是放入 skipped。返回值是 {"done", "failed", "skipped", "calls", "applied", "budget"}。

不要对每一条都调用 reset()——预算是整个批次共用的,所以 ATTEMPTS 必须连续。在开始处理一条之前,也先看一次剩余预算,没有的话就根本不要调用 run_budgeted。done、failed、skipped 中只按顺序放入 request_key。评分器会另外计算同样的规则,把列表和 calls 对照。

用测得的次数来记录

在 /root/work/agbudget/budget_report.json 中写入 max_attempts、no_retry_attempts、default_policy_attempts、retry_all_permanent_attempts、split_permanent_attempts、split_transient_attempts、duplicate_delivery_entries、batch_budget、batch_calls、batch_done、batch_skipped、batch_applied,在 /root/work/agbudget/budget_report.md 中用 ## 무엇을 다시 해도 되는가(韩文,意为“什么可以重试”)、## 상한을 어디에 두었나(韩文,意为“上限设在了哪里”)、## 같은 요청이 두 번 와도 안전한 이유(韩文,意为“同一个请求来两次也安全的原因”)、## 예산이 바닥났을 때 무엇을 남기는가(韩文,意为“预算耗尽时留下什么”)四节来写。

数字不要手写,要用实际运行你的模块得到的值来填。split_transient_attempts 是暂时性失败 2 次后成功时的尝试次数,duplicate_delivery_entries 是用同一个键发送两次之后账本里留下的行数。批次要按说明的参考一节所定的 orders、plan、budget 原样运行。评分器会另外计算同样的内容来对照。