fix: 错误记忆DAO全局化(NoTenantId)+GetByKey miss契约修复; 分析prompt补可重试白名单

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-01 11:21:34 +08:00
co-authored by Claude Code
parent 31bb973bc0
commit 0dde11dbdf
3 changed files with 55 additions and 5 deletions
+10 -3
View File
@@ -13,13 +13,19 @@ var ErrorMemory = &errorMemoryDao{}
type errorMemoryDao struct{}
// GetByKey 按记忆键查询(未命中返回 (nil, nil))
// 错误记忆为全局表:NoTenantId 绕过租户过滤,跨租户共享;r.IsEmpty() 兜底 miss 契约,
// 避免对空记录 r.Struct(&res) 上浮 sql.ErrNoRows 导致调用方 fail-closed。
func (d *errorMemoryDao) GetByKey(ctx context.Context, key string) (res *entity.ErrorMemory, err error) {
r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory).
NoTenantId(ctx).
Where(entity.ErrorMemoryCol.MemoryKey, key).
One()
if err != nil {
return
}
if r.IsEmpty() {
return nil, nil
}
err = r.Struct(&res)
return
}
@@ -27,7 +33,8 @@ func (d *errorMemoryDao) GetByKey(ctx context.Context, key string) (res *entity.
// Upsert 存在则更新 retryable/reason/analyzed_by,不存在则插入
func (d *errorMemoryDao) Upsert(ctx context.Context, m *entity.ErrorMemory) (err error) {
model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory)
n, err := model.Where(entity.ErrorMemoryCol.MemoryKey, m.MemoryKey).Count()
// Count 同样全局化:跨租户已存在的记忆键需命中更新分支,而非重复插入
n, err := model.NoTenantId(ctx).Where(entity.ErrorMemoryCol.MemoryKey, m.MemoryKey).Count()
if err != nil {
return
}
@@ -43,9 +50,9 @@ func (d *errorMemoryDao) Upsert(ctx context.Context, m *entity.ErrorMemory) (err
return
}
// List 分页查询(按 id 倒序)
// List 分页查询(按 id 倒序);全局表,管理端列表展示所有租户记忆
func (d *errorMemoryDao) List(ctx context.Context, page, pageSize int) (list []entity.ErrorMemory, total int64, err error) {
model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory)
model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory).NoTenantId(ctx)
n, err := model.Count()
if err != nil {
return
+41
View File
@@ -0,0 +1,41 @@
package dao
import (
"context"
"fmt"
"testing"
"time"
"model-gateway/consts/public"
_ "gitea.redpowerfuture.com/red-future/common/consul"
"gitea.redpowerfuture.com/red-future/common/db/gfdb"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
"github.com/gogf/gf/v2/net/gtrace"
)
// TestErrorMemoryDaoGetByKeyMiss 回归:错误记忆未命中必须返回 (nil, nil),
// 而非把空记录丢给 r.Struct(&res) 上浮 sql.ErrNoRows —— 否则 service.shouldRetryWithMemory
// 会把一切查询错误当 fail-closed,导致 分析→落库→重试 整条链路变死代码。
// 依赖真实 PG;不可达时跳过(不在无 PG 环境硬失败)。
func TestErrorMemoryDaoGetByKeyMiss(t *testing.T) {
ctx, span := gtrace.NewSpan(context.Background(), "TestErrorMemoryDaoGetByKeyMiss")
defer span.End()
// 探测连接:不可用则跳过
if _, err := gfdb.DB(ctx, public.DbNameModelGateway).
Model(ctx, public.TableNameErrorMemory).
NoTenantId(ctx).
Count(); err != nil {
t.Skipf("DB不可用,跳过: %v", err)
}
key := fmt.Sprintf("test-miss-%d", time.Now().UnixNano())
row, err := ErrorMemory.GetByKey(ctx, key)
if err != nil {
t.Fatalf("GetByKey 未命中不应报错: %v", err)
}
if row != nil {
t.Fatalf("GetByKey 未命中应返回 nil, got %+v", row)
}
}
+4 -2
View File
@@ -45,11 +45,13 @@ const analysisSystemPrompt = `你是 AI 模型网关的错误分析器。上游
## 值得重试(retryable: true
- 限流:429、rate limit、请求过密、并发超限
- 服务端瞬时故障:5xx、服务过载、上游临时不可用、连接抖动
- 服务端瞬时故障:5xx、InternalServiceError、服务过载、上游临时不可用
- 超时/取消/连接:Timeout、RequestCanceled、Error while connecting、连接抖动
- 媒体源暂不可用(视频/音频生成类上游常见):Invalid video_url、Invalid audio track、Error while downloading、download failed —— 通常是源尚未就绪或下载瞬断,重试可成功,不要误判为永久参数错误
- 资源暂时不足:quota 暂时受限
## 不值得重试(retryable: false
- 请求/参数错误:400、invalid_argument、格式错误
- 请求/参数错误:400、invalid_argument、格式错误(注意 Invalid video_url / Invalid audio track 属上类的媒体源错误,不归此类)
- 鉴权失败:401、403、invalid_api_key、签名错误
- 模型不存在:404、model_not_found
- 余额不足:insufficient_quota