mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
一次模型调用的失败由内到外经过五层重试:建连(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。
81 lines
3.2 KiB
Go
81 lines
3.2 KiB
Go
package server
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
// 重试策略的服务端解析,见 docs/LLM重试设计.md。五层里:
|
|
// - 建连 / 空响应 / 同 provider 安全窗口 是「跟着端点走」的,每个 LLM 配置可以覆盖
|
|
// 全局默认(profile 的某项留空就继承全局,全局也没配就用内置默认);
|
|
// - 熔断 / 意图重跑 是进程级的,只有全局一份。
|
|
//
|
|
// 全局策略读一次 DB 一行 settings,调用点都在低频路径(构建 provider、work 收尾、
|
|
// 保存配置),不值得再加一层缓存;熔断参数是例外——它在失败路径上每次都要读,所以
|
|
// 由 applyRetryPolicy 推给 Registry 保存。
|
|
|
|
// retryPolicy reads the global policy; a nil DB yields the zero policy (all
|
|
// layers on their built-in defaults).
|
|
func (s *Server) retryPolicy() db.LLMRetryPolicy {
|
|
if s.m == nil || s.m.pg == nil {
|
|
return db.LLMRetryPolicy{}
|
|
}
|
|
return s.m.pg.LLMRetryPolicy()
|
|
}
|
|
|
|
// resolveRetry layers one profile's override on top of the global policy and
|
|
// converts the result into the form agent.Config carries. Rules combine field by
|
|
// field, so a profile that only pins an interval still inherits the global count.
|
|
func resolveRetry(o db.RetryOverride, pol db.LLMRetryPolicy) agent.RetryConfig {
|
|
connect := o.Connect.Or(pol.Connect)
|
|
empty := o.Empty.Or(pol.Empty)
|
|
stream := o.Stream.Or(pol.Stream)
|
|
return agent.RetryConfig{
|
|
// 次数在这里保持「0=默认 / 负=关闭」的原始语义:SDK 的 MaxRetries /
|
|
// EmptyResponseRetries 与之完全同构,交给它自己解析即可。
|
|
ConnectAttempts: connect.Attempts, ConnectInterval: connect.Interval(),
|
|
EmptyAttempts: empty.Attempts, EmptyInterval: empty.Interval(),
|
|
StreamAttempts: stream.Attempts, StreamInterval: stream.Interval(),
|
|
}
|
|
}
|
|
|
|
// applyProfileRetry fills cfg.Retry for a profile read from the DB.
|
|
func (s *Server) applyProfileRetry(cfg *agent.Config, p *db.LLMProfile) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
cfg.Retry = resolveRetry(p.Retry, s.retryPolicy())
|
|
}
|
|
|
|
// 熔断(轮询冷却)的默认值,与 llmpool 内置的一致 —— 这里只在「用户配了值」时才覆盖。
|
|
// 意图重跑的默认值见 engine.go 的 modelErrorRetries / modelErrorRetryBackoff。
|
|
|
|
// applyRetryPolicy pushes the process-wide layers of the policy into the objects
|
|
// that consume them on a hot path: the circuit-breaker registry. Called at
|
|
// startup and whenever the policy is saved.
|
|
func (s *Server) applyRetryPolicy() {
|
|
pol := s.retryPolicy()
|
|
if s.llmHealth != nil {
|
|
s.llmHealth.SetPolicy(pol.Breaker.Attempts, pol.Breaker.Interval())
|
|
}
|
|
}
|
|
|
|
// modelErrorRetryPolicy resolves the intent-level replay knobs (layer ⑤): how
|
|
// many times a model_error work is re-run and how long to back off between runs.
|
|
func (e *Engine) modelErrorRetryPolicy() (retries int, backoff time.Duration) {
|
|
retries, backoff = modelErrorRetries, modelErrorRetryBackoff
|
|
if e == nil || e.m == nil || e.m.pg == nil {
|
|
return retries, backoff
|
|
}
|
|
rule := e.m.pg.LLMRetryPolicy().Intent
|
|
if rule.Attempts != 0 {
|
|
retries = max(rule.Attempts, 0)
|
|
}
|
|
if d := rule.Interval(); d > 0 {
|
|
backoff = d
|
|
}
|
|
return retries, backoff
|
|
}
|