feat(llm): 五层重试的次数与间隔可配

一次模型调用的失败由内到外经过五层重试:建连(SDK)、空响应(SDK)、同 provider
安全窗口、轮询熔断、意图重跑。此前五层的次数与间隔全是写死的常量,遇到脾气各异
的中转站只能改代码。现在都能在 LLM 页配置,不配就是原来的默认值。

配置语义五层统一(db.RetryRule):次数 0=内置默认 / -1=关掉这层 / >0=该值;
间隔 0=原本的指数退避 / >0=改用固定间隔。0 被"未配置"占用,故关闭用 -1。
写库前夹紧到 次数 [-1,20]、间隔 [0,1h],DB 另有 CHECK 兜底。

粒度按层的性质分:前三层跟着端点走,可在每个模型配置里逐字段覆盖全局默认(只钉
间隔的仍继承全局次数);熔断与意图重跑是进程级语义,只有全局一份。

生效路径:①②是 provider 构建参数,保存后重建缓存;③随每次 hooks.current()
解析,故障转移换配置时重试节奏跟着换;④在失败路径上每次都要读,由 applyRetryPolicy
推给 llmpool.Registry;⑤每条意图收尾读一次。全部热生效,无需重启。

连接测试刻意不带这些参数——它有 30s 硬超时,叠上用户配的重试只会把本来能用的
端点测成超时失败。

norma 升 v0.3.5(llm.Config 新增 RetryInterval / EmptyResponseRetries /
EmptyResponseInterval)。旧库升级后行为逐字节不变:新列默认 0,settings 键不
存在即全默认。设计见 docs/LLM重试设计.md。
This commit is contained in:
Autumn-27 committed 2026-09-09 13:35:04 -04:00
1 parent 377ccc7e5a
commit a50bf02428
21 files changed
+1216 -146

No files matched your search

+28
View File
@@ -74,6 +74,29 @@ type Config struct {
// 空 = 不发送。值由 transcript.WithSessionID 挂在请求 context 上,由 RoundTripper
// 读取填入,因此同一共享 provider 也能按会话发出不同的头值。
SessionHeaderKey string
// Retry 是该配置解析后的重试参数(profile 覆盖 → 全局策略 → 内置默认,由
// server 侧解析)。三层的含义见 RetryConfig;零值 = 完全沿用内置默认。
Retry RetryConfig
}
// RetryConfig 是随一个 LLM 配置走的重试参数。每层的「次数」统一语义:
// 0 = 用内置默认次数;负数 = 关闭该层重试;>0 = 用该值。每层的「间隔」:
// 0 = 用该层原本的指数退避;>0 = 改用这个固定间隔。
type RetryConfig struct {
// ConnectAttempts/ConnectInterval:SDK 建连重试(连接重置/超时/429/5xx,流开始前),
// 直接映射为 llm.Config.MaxRetries / RetryInterval。默认 3 次、0.5s 起指数(封顶 8s)。
ConnectAttempts int
ConnectInterval time.Duration
// EmptyAttempts/EmptyInterval:SDK 空响应重试(完成但无 content block,仅 openai
// 格式),映射为 llm.Config.EmptyResponseRetries / EmptyResponseInterval。
// 默认 2 次、同一条指数梯度。
EmptyAttempts int
EmptyInterval time.Duration
// StreamAttempts/StreamInterval:同 provider 安全窗口重试——本项目在 SDK 之上补的
// 一层,只在「还没向调用方交付任何输出」时重放断流/过载/流内 429。SDK 看不到它,
// 由 server/task_llm.go 消费。默认 2 次、0.5s 起指数(封顶 4s)。
StreamAttempts int
StreamInterval time.Duration
}
// compaction window resolution bounds (in K tokens). Below the floor the
@@ -249,6 +272,11 @@ func (c Config) NewProvider() (llm.Provider, error) {
// 输出上限的字段名选择(空 = 用 max_tokens)。上限的「值」不在这里:它每轮随
// agentcore.Options.MaxTokens 走,provider 只决定把它塞进哪个键。
lc.MaxTokensField = c.MaxTokensField
// 重试参数与 SDK 同语义(次数 0=默认/负=关闭,间隔 0=指数退避/>0=固定),原样透传。
lc.MaxRetries = c.Retry.ConnectAttempts
lc.RetryInterval = c.Retry.ConnectInterval
lc.EmptyResponseRetries = c.Retry.EmptyAttempts
lc.EmptyResponseInterval = c.Retry.EmptyInterval
if c.RatePerSecond > 0 || c.RatePerMinute > 0 {
lc.RateLimit = &llm.RateLimit{PerSecond: c.RatePerSecond, PerMinute: c.RatePerMinute}
}