Liking cljdoc? Tell your friends :D

im.ttalk.agent.retry

通用重试:指数退避 + 满抖动,判据取自 canonical error

落点在 ChatModel,位于整个 filter 栈之下(与 beamai beamai_chat_model 同一取舍)。这不是随手放的——filter 看到的必须是「一次逻辑调用」:

:chat filter(memory / 计时 / 记账) └ ChatModel.call ──┐ ├ 尝试 1 ✗ → 退避 → 尝试 2 ✓ ← 重试在这里面 └ 返回一个响应

若重试在 filter 链之上(或做成一个 filter),一次网络抖动就会让 memory-filter 把同一轮 delta 写两遍、让计时 filter 记出两条记录。放在链下, 重试重入碰不到任何 filter,around_chat 的副作用每轮只发生一次。 要观测每次真实尝试,用 :on-retry 回调——它是为此存在的,不是日志糖。

流式只在首个 token 之前重试:token 已经投递给 sink 之后重跑会让下游看到 重复内容,那之后的失败原样抛;要容错请在 turn 层重跑整轮。首个 token 之前 (建连失败、503、首字节超时)没有任何东西出门,重发安全——stream-call 用 :retry-gate + 独立预算 :stream-retries 表达这一点(designs/provider-reliability.md §2)。

空补全(HTTP 200 但无有效内容)走独立预算 :empty-retries,不占 :max-retries。

判据是 canonical error 的 :retryable?,不是 HTTP 状态码——那是 provider 边界的事(见 provider.http.client/response->error,它顺带把 Retry-After 解析成 :retry-after-ms 塞进错误 map,core 只读那个数)。 core 因此对 HTTP 一无所知;本 ns 无外部依赖,只读 canonical error 里的两个键 (:retryable? / :retry-after-ms)。(异步版 run-async 用到 core 内的链结果 组合子 im.ttalk.agent.filter,仍无第三方依赖。)

同步 / 异步两个入口共用一套判据:run 与 run-async 的重试次数、 retryable-ex?、退避曲线(含 Retry-After 与满抖动)都走同一批函数, 只差「等待」的实现——同步 Thread/sleep,异步 CompletableFuture/delayedExecutor (绝不能在异步路径上 sleep:那会睡掉传输层 executor 的线程)。

参数三级取值:单次 opts > provider config > 框架默认;:max-retries 0 即关闭(等价于直接调用,零开销)。

通用重试:指数退避 + 满抖动,判据取自 canonical error

**落点在 ChatModel,位于整个 filter 栈之下**(与 beamai `beamai_chat_model`
同一取舍)。这不是随手放的——filter 看到的必须是「一次逻辑调用」:

  :chat filter(memory / 计时 / 记账)
    └ ChatModel.call ──┐
                       ├ 尝试 1 ✗ → 退避 → 尝试 2 ✓   ← 重试在这里面
                       └ 返回一个响应

若重试在 filter 链**之上**(或做成一个 filter),一次网络抖动就会让
memory-filter 把同一轮 delta 写两遍、让计时 filter 记出两条记录。放在链下,
重试重入碰不到任何 filter,`around_chat` 的副作用每轮只发生一次。
要观测每次**真实尝试**,用 `:on-retry` 回调——它是为此存在的,不是日志糖。

**流式只在首个 token 之前重试**:token 已经投递给 sink 之后重跑会让下游看到
重复内容,那之后的失败原样抛;要容错请在 turn 层重跑整轮。首个 token 之前
(建连失败、503、首字节超时)没有任何东西出门,重发安全——`stream-call` 用
`:retry-gate` + 独立预算 `:stream-retries` 表达这一点(designs/provider-reliability.md §2)。

**空补全**(HTTP 200 但无有效内容)走独立预算 `:empty-retries`,不占 `:max-retries`。

**判据是 canonical error 的 `:retryable?`**,不是 HTTP 状态码——那是
provider 边界的事(见 `provider.http.client/response->error`,它顺带把
`Retry-After` 解析成 `:retry-after-ms` 塞进错误 map,core 只读那个数)。
core 因此对 HTTP 一无所知;本 ns **无外部依赖**,只读 canonical error 里的两个键
(`:retryable?` / `:retry-after-ms`)。(异步版 `run-async` 用到 core 内的链结果
组合子 `im.ttalk.agent.filter`,仍无第三方依赖。)

**同步 / 异步两个入口共用一套判据**:`run` 与 `run-async` 的重试次数、
`retryable-ex?`、退避曲线(含 `Retry-After` 与满抖动)都走同一批函数,
只差「等待」的实现——同步 `Thread/sleep`,异步 `CompletableFuture/delayedExecutor`
(绝不能在异步路径上 sleep:那会睡掉传输层 executor 的线程)。

参数三级取值:**单次 opts > provider config > 框架默认**;`:max-retries 0`
即关闭(等价于直接调用,零开销)。
raw docstring

compute-backoffclj

(compute-backoff attempt opts)
(compute-backoff attempt
                 {:keys [base-delay multiplier max-delay]
                  :or {base-delay 1000 multiplier 2.0 max-delay 30000}}
                 rand-fn)

第 attempt 次重试的退避毫秒(含满抖动)。attempt 从 0 开始。

基础退避 = min(base-delay × multiplier^attempt, max-delay),再在 [0, 基础退避] 上取满抖动——抖动是为了避免一批并发请求同时醒来把服务端 再打挂一次(重试风暴)。

rand-fn 可注入,测试用。

第 attempt 次重试的退避毫秒(含满抖动)。attempt 从 0 开始。

基础退避 = min(base-delay × multiplier^attempt, max-delay),再在
[0, 基础退避] 上取满抖动——抖动是为了避免一批并发请求同时醒来把服务端
再打挂一次(重试风暴)。

rand-fn 可注入,测试用。
sourceraw docstring

default-optsclj

框架默认重试配置。

  • :max-retries 最大重试次数(不含首次调用);0 = 关闭
  • :base-delay 首次退避基准(毫秒)
  • :multiplier 退避倍率
  • :max-delay 单次退避上限(毫秒)——Retry-After 也受它约束
  • :respect-retry-after? 是否优先采用错误里的 :retry-after-ms
  • :empty-retries 空补全的独立预算(HTTP 200 但无有效内容时补打几次;0 = 不补打,空响应原样返回)
  • :empty-exhausted 空补全预算用完仍空时怎么办::return(缺省,把空响应原样交回——与过去一致) / :throw(抛 :empty-completion-error,交给主备切换 / 上层处理)
  • :stream-retries 流式「首个 token 之前」失败的独立预算(token 出门后一律不重试)

三格预算各自记账、互不挪用(designs/provider-reliability.md §2)。

框架默认重试配置。

- :max-retries  最大重试次数(**不含**首次调用);0 = 关闭
- :base-delay   首次退避基准(毫秒)
- :multiplier   退避倍率
- :max-delay    单次退避上限(毫秒)——`Retry-After` 也受它约束
- :respect-retry-after? 是否优先采用错误里的 :retry-after-ms
- :empty-retries 空补全的独立预算(HTTP 200 但无有效内容时补打几次;0 = 不补打,空响应原样返回)
- :empty-exhausted 空补全预算用完仍空时怎么办:`:return`(缺省,把空响应原样交回——与过去一致)
                  / `:throw`(抛 :empty-completion-error,交给主备切换 / 上层处理)
- :stream-retries 流式「首个 token 之前」失败的独立预算(token 出门后一律不重试)

三格预算各自记账、互不挪用(designs/provider-reliability.md §2)。
sourceraw docstring

resolve-optsclj

(resolve-opts config)
(resolve-opts config opts)

三级合并重试配置:框架默认 < provider/model config 的 :retry < 单次 opts 的 :retry。

每一级都接受 nil(跳过)/ true(用上一级结果)/ false(关闭,等价 {:max-retries 0 :empty-retries 0 :stream-retries 0})/ map(作为配置合并)。

返回: 合并后的配置 map(:max-retries 为 0 表示不重试)

三级合并重试配置:框架默认 < provider/model config 的 :retry < 单次 opts 的 :retry。

每一级都接受 nil(跳过)/ true(用上一级结果)/ false(关闭,等价
`{:max-retries 0 :empty-retries 0 :stream-retries 0}`)/ map(作为配置合并)。

返回: 合并后的配置 map(`:max-retries` 为 0 表示不重试)
sourceraw docstring

retryable-ex?clj

(retryable-ex? e)

异常是否可重试——唯一判据是 canonical error 的 :retryable?。

provider 边界已经把 401 / 400 这类判成不可重试、把 429 / 5xx / 连接失败判成 可重试(见 model.error/http-response->error),这里不再二次猜测:没有 :retryable? 的异常(框架自身的 bug、用户工具抛的)一律不重试。 猜错的代价不对称——对一个本质不可重试的错误反复打 API 是在烧钱和触发风控。

异常是否可重试——**唯一判据是 canonical error 的 `:retryable?`**。

provider 边界已经把 401 / 400 这类判成不可重试、把 429 / 5xx / 连接失败判成
可重试(见 `model.error/http-response->error`),这里不再二次猜测:没有
`:retryable?` 的异常(框架自身的 bug、用户工具抛的)一律**不重试**。
猜错的代价不对称——对一个本质不可重试的错误反复打 API 是在烧钱和触发风控。
sourceraw docstring

runclj

(run f
     {:keys [max-retries on-retry rand-fn sleep-fn empty? empty-retries
             empty-exhausted retry-gate record-attempts? provider retry-kind]
      :or {rand-fn rand}
      :as opts})

在重试保护下执行 f(无参函数)。

参数:

  • f: 无参函数;失败时抛 ex-info(data 即 canonical error)
  • opts: resolve-opts 的结果,另可带
    • :on-retry (fn [{:keys [attempt delay-ms error exception kind]}]) 每次重试之前调用,用来观测真实尝试次数。:kind ∈ :error(按 :max-retries 退避重试)/ :empty-completion(空补全补打)/ opts 的 :retry-kind(流式路径传 :stream)
    • :rand-fn 随机数函数(测试用)
    • :sleep-fn (fn [ms])(测试用;缺省 Thread/sleep) 以下为 designs/provider-reliability.md §2 新增,不给即旧行为,逐字不变:
    • :empty? (fn [result] bool);真 ⇒ 按 :empty-retries(缺省 1)立即补打 (不退避、不占 :max-retries、不抬高退避指数);超预算按 :empty-exhausted: :throw 抛 :empty-completion-error,否则(缺省)把空结果原样返回
    • :retry-gate (fn [] bool);每次决定重试前问一次,假 ⇒ 不重试(流式:本次尝试已吐过 token)
    • :record-attempts? 真 ⇒ 成功结果(map/record)上 assoc :attempts(含首次)
    • :provider 空补全错误的 :provider

返回: f 的返回值(:record-attempts? 时带 :attempts)

抛出: 最后一次失败的异常——canonical error 原样不变(:retryable? / :status 全程不丢, README 的错误模型承诺「各边界可单向转换」,重试层没有资格改写它);试过不止一次时 ex-data 追加 :attempts(经 errors/with-ex-data,消息 / cause / 栈逐字保留)。

在重试保护下执行 `f`(无参函数)。

参数:
- f:    无参函数;失败时**抛** ex-info(data 即 canonical error)
- opts: `resolve-opts` 的结果,另可带
  - :on-retry (fn [{:keys [attempt delay-ms error exception kind]}])
              每次重试**之前**调用,用来观测真实尝试次数。`:kind` ∈
              `:error`(按 :max-retries 退避重试)/ `:empty-completion`(空补全补打)/
              opts 的 `:retry-kind`(流式路径传 `:stream`)
  - :rand-fn  随机数函数(测试用)
  - :sleep-fn (fn [ms])(测试用;缺省 Thread/sleep)
  以下为 designs/provider-reliability.md §2 新增,**不给即旧行为,逐字不变**:
  - :empty?            (fn [result] bool);真 ⇒ 按 `:empty-retries`(缺省 1)**立即**补打
                       (不退避、不占 :max-retries、不抬高退避指数);超预算按 `:empty-exhausted`:
                       `:throw` 抛 :empty-completion-error,否则(缺省)把空结果原样返回
  - :retry-gate        (fn [] bool);每次决定重试前问一次,假 ⇒ 不重试(流式:本次尝试已吐过 token)
  - :record-attempts?  真 ⇒ 成功结果(map/record)上 assoc `:attempts`(含首次)
  - :provider          空补全错误的 :provider

返回: `f` 的返回值(`:record-attempts?` 时带 :attempts)

抛出: 最后一次失败的异常——canonical error 原样不变(`:retryable?` / `:status` 全程不丢,
README 的错误模型承诺「各边界可单向转换」,重试层没有资格改写它);试过不止一次时
ex-data 追加 `:attempts`(经 `errors/with-ex-data`,消息 / cause / 栈逐字保留)。
sourceraw docstring

run-asyncclj

(run-async f
           {:keys [max-retries on-retry rand-fn sleep-fn empty? empty-retries
                   empty-exhausted retry-gate record-attempts? provider
                   retry-kind]
            :or {rand-fn rand}
            :as opts})

run 的异步孪生:f 是无参函数,返回链结果(deferred 或普通值)。

与 run 的差别只有两处,其余(判据 retryable-ex?、三格预算、退避曲线含 Retry-After 与满抖动、:on-retry 回调时机、:attempts 记录)逐字共用:

  1. 失败经 deferred 的 error channel 传播,用 flt/fcatch 接(同步抛出也接得住);
  2. 退避不 Thread/sleep——用 async/delayed(CompletableFuture/delayedExecutor)。注入了 :sleep-fn 时仍走它(测试用:立即返回、只记账)。

返回: deferred(或普通值——f 同步返回且没触发重试时就是普通值,形态保持见 filter-chain-design.md §2.6.4 契约 C1)。 抛出/落 error channel: 最后一次失败的异常(规则同 run)。

递归深度 ≤ 总尝试次数,不吃栈。

`run` 的异步孪生:`f` 是无参函数,返回**链结果**(deferred 或普通值)。

与 `run` 的差别只有两处,其余(判据 `retryable-ex?`、三格预算、退避曲线含
`Retry-After` 与满抖动、`:on-retry` 回调时机、`:attempts` 记录)**逐字共用**:

1. 失败经 deferred 的 error channel 传播,用 `flt/fcatch` 接(同步抛出也接得住);
2. 退避不 `Thread/sleep`——用 `async/delayed`(`CompletableFuture/delayedExecutor`)。注入了 `:sleep-fn` 时仍走它(测试用:立即返回、只记账)。

返回: deferred(或普通值——`f` 同步返回且没触发重试时就是普通值,形态保持见
filter-chain-design.md §2.6.4 契约 C1)。
抛出/落 error channel: 最后一次失败的异常(规则同 `run`)。

递归深度 ≤ 总尝试次数,不吃栈。
sourceraw docstring

wrapclj

(wrap f config opts)

把 f 包成带重试的同名函数。config/opts 语义同 resolve-opts。

把 `f` 包成带重试的同名函数。config/opts 语义同 `resolve-opts`。
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close