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:
1 parent
377ccc7e5a
commit
a50bf02428
21 files changed
+1216
-146
No files matched your search
@@ -157,6 +157,9 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
|
||||
profAgents: map[int64]*profBundle{}, profChatAgents: map[int64]*agent.ChatAgent{},
|
||||
provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg),
|
||||
taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1)}
|
||||
// 熔断阈值/冷却是失败路径上的热参数,启动时把全局重试策略推给 Registry 一次;
|
||||
// 之后每次保存策略再推一次(saveLLMRetryPolicy)。
|
||||
s.applyRetryPolicy()
|
||||
// Every task uses a stable task router. An empty explicit chain is resolved by
|
||||
// that router through Agent bindings and then the global provider, so adding a
|
||||
// first chain to a running task takes effect on its very next LLM call.
|
||||
@@ -314,6 +317,7 @@ func (s *Server) loadLLMConfig() (agent.Config, bool) {
|
||||
cfg.Stream = p.Streaming
|
||||
cfg.MaxTokens, cfg.MaxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
cfg.SessionHeaderKey = p.SessionHeaderKey
|
||||
s.applyProfileRetry(&cfg, p)
|
||||
if cfg.APIKey == "" {
|
||||
return cfg, false
|
||||
}
|
||||
@@ -338,12 +342,14 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error {
|
||||
var maxTokens int
|
||||
var maxTokensField string
|
||||
var sessionHeaderKey string
|
||||
var retry db.RetryOverride
|
||||
if profs, _ := s.m.pg.ListProfiles(); profs != nil {
|
||||
for _, p := range profs {
|
||||
if p.Name == "default" {
|
||||
id, priority, poolExclude, streaming = p.ID, p.Priority, p.PoolExclude, p.Streaming
|
||||
maxTokens, maxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
sessionHeaderKey = p.SessionHeaderKey
|
||||
retry = p.Retry
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -354,6 +360,7 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error {
|
||||
ContextWindowK: cfg.ContextWindowK, ThinkingType: cfg.ThinkingType, ReasoningEffort: cfg.ReasoningEffort, IsDefault: true,
|
||||
Priority: priority, PoolExclude: poolExclude, Streaming: streaming,
|
||||
MaxTokens: maxTokens, MaxTokensField: maxTokensField, SessionHeaderKey: sessionHeaderKey,
|
||||
Retry: retry,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -516,6 +523,7 @@ func (s *Server) loadProfileConfig(id int64) (agent.Config, bool) {
|
||||
cfg.Stream = p.Streaming
|
||||
cfg.MaxTokens, cfg.MaxTokensField = p.MaxTokens, p.MaxTokensField
|
||||
cfg.SessionHeaderKey = p.SessionHeaderKey
|
||||
s.applyProfileRetry(&cfg, p)
|
||||
if cfg.APIKey == "" {
|
||||
return cfg, false
|
||||
}
|
||||
@@ -925,6 +933,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /api/llm/profiles", s.pgSaveProfile)
|
||||
mux.HandleFunc("DELETE /api/llm/profiles/{id}", s.pgDeleteProfile)
|
||||
mux.HandleFunc("POST /api/llm/profiles/active", s.pgActivateProfile)
|
||||
mux.HandleFunc("GET /api/llm/retry-policy", s.pgGetLLMRetryPolicy)
|
||||
mux.HandleFunc("POST /api/llm/retry-policy", s.pgSaveLLMRetryPolicy)
|
||||
mux.HandleFunc("GET /api/llm/pool", s.pgLLMPoolStatus)
|
||||
mux.HandleFunc("POST /api/llm/pool/reset", s.pgLLMPoolReset)
|
||||
mux.HandleFunc("POST /api/llm/models", s.pgListModels)
|
||||
@@ -1411,6 +1421,9 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]any{"ok": false, "error": "未提供 API Key"})
|
||||
return
|
||||
}
|
||||
// 重试参数【不】带进连接测试:测试有 30s 硬超时,把配置的重试次数/长间隔叠上去
|
||||
// 只会让一个本来能用的端点测成"超时失败"。测试看的是"这个端点通不通",重试节奏
|
||||
// 是跑起来之后的事。
|
||||
lat, reply, err := agent.TestConnection(r.Context(), cfg)
|
||||
if err != nil {
|
||||
writeJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
||||
|
||||
Reference in new issue
Block a user