docs: 新增模型错误 LLM 分析重试 + 持久记忆设计 spec
- 固定错误码清单改为对话模型分析判定重试 - 持久化错误→结论知识库(PostgreSQL),命中记忆不再调模型 - 覆盖同步/流式/异步任务启动三路 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# 模型网关 LLM 驱动重试决策 + 持久错误记忆 设计
|
||||
|
||||
> **日期**:2026-09-01
|
||||
> **范围**:model-gateway(仅本服务)
|
||||
> **状态**:已确认
|
||||
|
||||
## Goal
|
||||
|
||||
请求上游模型失败时,不再用固定错误码清单决定是否重试,改为**调用对话模型分析错误类型**决定是否重试,并持久化"错误 → 结论"的记忆(知识库),命中记忆时不再调用分析模型。
|
||||
|
||||
## 背景与现状
|
||||
|
||||
当前重试判定(`service/retry.go:isRetryableErrorCode`)基于固定错误码集合 `429/500/501/502/503/InvalidParameter/limit_requests/limit_tokens/rate_limit_exceeded`。判定点两处:
|
||||
|
||||
- 同步:`service/session_sync.go:60`(`CreateSession`)
|
||||
- 流式:`service/session_stream.go:88`,及 `streamRetryCodeOfError` 中对 5xx 的硬编码分支
|
||||
|
||||
异步任务启动 `service/model_task_start_service.go`(`CreateTask`)目前**没有重试**。
|
||||
|
||||
错误解析已配置驱动:`parseModelError`(按模型 `ErrorMessageMapping` 提取 code/message)。
|
||||
|
||||
## 设计决策(已确认)
|
||||
|
||||
| # | 决策 |
|
||||
|---|---|
|
||||
| 1 | **完全交给 LLM 判断**:删除固定错误码清单,所有错误(记忆未命中时)都调分析模型决定是否重试 |
|
||||
| 2 | **持久知识库**:结论存 PostgreSQL 新表,跨实例共享、重启不丢 |
|
||||
| 3 | **记忆键** = 失败上游 BaseURL + 错误码 + 归一化消息指纹(同因同键、跨实例命中) |
|
||||
| 4 | **二元输出**:`{retryable: bool, reason: str}` |
|
||||
| 5 | **永久有效**:条目无 TTL,直到同键重新分析覆盖或经管理端点手动删除 |
|
||||
| 6 | **分析模型 = 对话模型**(不新增配置段):失败模型自身 `chat_model=true` 优先复用;否则取当前用户对话模型(`GetChatModel`);都没有 → fail-closed 不重试 |
|
||||
| 7 | **三路接入**:同步 / 流式 / 异步任务启动 |
|
||||
| 8 | **内联分析 + singleflight 去重**:未命中时同步调分析模型;同键并发 miss 合并为一次分析 |
|
||||
|
||||
## 架构与数据流
|
||||
|
||||
```
|
||||
请求失败(parseModelError 得 code+msg)
|
||||
└─ shouldRetryWithMemory(ctx, modelInfo, code, msg) → bool:
|
||||
1) 归一化消息 → 记忆键 = SHA-256(upstream|code|fingerprint)
|
||||
2) 查 model_gateway_error_memory
|
||||
├─ 命中 → 返回存储的 retryable
|
||||
└─ 未命中 → singleflight 合并后调分析模型
|
||||
→ 解析 {retryable, reason} → UPSERT 落库 → 返回
|
||||
3) retryable=true → 现有指数退避重试(modelCallMaxRetries=10 预算)
|
||||
retryable=false → 记 ErrorMsg 走现有终态逻辑(不重试)
|
||||
```
|
||||
|
||||
## 记忆知识库(新表 `model_gateway_error_memory`)
|
||||
|
||||
表前缀 `model_gateway_`,DDL 追加到 `update.sql`,DAO/entity/consts 按现有惯例新建。
|
||||
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | BIGSERIAL PK | |
|
||||
| `memory_key` | CHAR(64) | SHA-256(upstream\|code\|fingerprint),**唯一索引** |
|
||||
| `upstream` | VARCHAR | 失败上游 BaseURL |
|
||||
| `error_code` | VARCHAR | 解析出的错误码 |
|
||||
| `msg_fingerprint` | CHAR(32) | 归一化消息 md5 |
|
||||
| `retryable` | BOOLEAN | LLM 结论 |
|
||||
| `reason` | VARCHAR | LLM 给出的简短原因(可观测) |
|
||||
| `analyzed_by` | VARCHAR | 分析所用模型名 |
|
||||
| `created_at` / `updated_at` | 自动 | 永久有效,无 TTL |
|
||||
|
||||
### 归一化 `normalizeErrorMsg`
|
||||
|
||||
正则剔除易变片段,使同因不同实例命中同一键:
|
||||
|
||||
- UUID(8-4-4-4-12 hex)
|
||||
- ISO 8601 时间戳 / unix 秒与毫秒数字
|
||||
- `req-xxx` / `request-xxx` 类请求 ID
|
||||
- 连续 ≥4 位数字(去掉具体数值,保留位置标记)
|
||||
|
||||
### DAO / entity / consts
|
||||
|
||||
按 `dao/model_task_start_dao.go` 模式:包名 `dao`,变量 `var ErrorMemory = &errorMemoryDao{}`;表名/库名常量进 `consts/public`;entity 带列常量(`entity.ErrorMemoryCol.*`)。
|
||||
|
||||
## 分析模型与调用(不新增 config)
|
||||
|
||||
### 分析模型选择
|
||||
|
||||
1. 失败请求的 `modelInfo` 本身 `chat_model=true` → 直接用它(复用其 baseURL/apiKey/HttpMethod)
|
||||
2. 否则 → `GetChatModel`(当前用户对话模型,现有语义 Creator + chat_model=true)
|
||||
3. 都没有 → **fail-closed 不重试** + 日志
|
||||
|
||||
依赖:重试循环内 `ctx` 需携带用户信息(与现有 X-User-Info 约定一致)。
|
||||
|
||||
### 调用方式
|
||||
|
||||
`httpclient.ModelHttpNormalRequest` 对分析模型发 OpenAI messages 格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "<分析模型 modelName>",
|
||||
"messages": [
|
||||
{"role": "system", "content": "<判定错误是否可重试的系统提示词>"},
|
||||
{"role": "user", "content": "{error_code, error_message, 截断错误响应体}"}
|
||||
],
|
||||
"max_tokens": 256,
|
||||
"temperature": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Prompt(要点)
|
||||
|
||||
- system:定义任务——判断上游模型错误是否值得指数退避重试;考虑限流/瞬时故障/配置/参数/鉴权等类型;输出严格 JSON。
|
||||
- user:携带 `error_code`、`error_message`、截断的错误响应体(≤2000 字符)。
|
||||
- 输出解析容错:剥 ```json 代码块 / 前后空白 / 首个 `{...}` 提取。
|
||||
|
||||
### 失败处理
|
||||
|
||||
分析调用超时(15s)/失败/解析失败 → **fail-closed 不重试** + 日志。分析失败不影响记忆表。
|
||||
|
||||
## 三路接入
|
||||
|
||||
| 路径 | 文件 | 改动 |
|
||||
|---|---|---|
|
||||
| 同步 | `service/session_sync.go:60` | `isRetryableErrorCode(errCode)` → `shouldRetryWithMemory(...)` |
|
||||
| 流式 | `service/session_stream.go:88` | 同上替换;`streamRetryCodeOfError` 中硬编码 5xx 分支收敛(移除) |
|
||||
| 异步启动 | `service/model_task_start_service.go` | 新增重试循环(现无重试),错误时走同一判定 |
|
||||
|
||||
统一入口落在 `service/retry.go`:
|
||||
|
||||
- 删除 `isRetryableErrorCode` 固定清单
|
||||
- 新增 `shouldRetryWithMemory(ctx, modelInfo, code, msg) (retry bool)`(内部:查记忆 → miss 则分析 → 落库)
|
||||
- 保留 `modelCallMaxRetries=10` 与 `retryWait` 指数退避
|
||||
- 新增单飞:`golang.org/x/sync/singleflight.Group` 按记忆键合并并发分析(go.sum 已有传递版本,加为直接依赖即可;避免自实现 keyed mutex)
|
||||
|
||||
### 管理端点(手动清理记忆)
|
||||
|
||||
- `GET /errorMemory/list` — 分页查看记忆条目
|
||||
- `POST /errorMemory/delete` — 按 id 删除条目(永久记忆的手动纠错途径)
|
||||
|
||||
## 边界与降级
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 记忆查询失败(DB 异常) | 不重试 + 日志,不阻塞主流程 |
|
||||
| 分析模型调用失败/超时/解析失败 | 不重试(fail-closed)+ 日志 |
|
||||
| 同键并发 miss | singleflight 合并,只调一次分析模型 |
|
||||
| 消息过长 | 截断到 2000 字符再送分析 |
|
||||
| 分析模型取不到 | 不重试 + 日志 |
|
||||
| 重试仍失败(同键) | 烧满 `modelCallMaxRetries` 预算;永久记忆条目保留,靠管理端点手动清理 |
|
||||
|
||||
## 已知取舍
|
||||
|
||||
- **永久记忆**:上游行为变化后旧结论可能过时。接受,靠管理端点手动纠错;自动"连续失败 N 次降级"明确**不做**(YAGNI,留作未来)。
|
||||
- **异步 task_start 重试**:重试=重新调用创建任务;首次调用已报错大概率未建任务,接受"响应丢失但任务已建 → 重复建任务"的既有语义(与同步/流式行为一致)。
|
||||
|
||||
## 测试策略
|
||||
|
||||
**单元测试**
|
||||
- `normalizeErrorMsg`:UUID / 时间戳 / 请求 ID / 连续数字被剔除;稳定文本不变
|
||||
- 记忆键构造:同 code+同归一化消息同键;不同上游不同键
|
||||
- 分析响应解析:纯 JSON / ```json 代码块 / 前后缀 / 非法响应(返回失败)
|
||||
|
||||
**集成测试**
|
||||
- 记忆命中:直接复用存储结论,不调分析模型
|
||||
- 未命中:调分析模型 → 落库 → 按结论重试/不重试
|
||||
- 分析失败:不重试,不落库
|
||||
- 并发同键:多个 goroutine 只触发一次分析调用
|
||||
- 管理端点:list / delete
|
||||
|
||||
**三路径**
|
||||
- 同步 / 流式:重试预算与退避行为保持现状,仅判定来源替换
|
||||
- 异步 task_start:新增重试循环行为
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不做固定错误码快路径(决策 1 已排除)
|
||||
- 不做记忆自动过期 / 命中计数降级(决策 5 + YAGNI)
|
||||
- 不新增 config 段(决策 6)
|
||||
- 不接 ai-agent / prompts-core 做分析(分析在 model-gateway 内完成)
|
||||
Reference in New Issue
Block a user