- 移动 session-dedup-knowledge-map OpenSpec 到 archive 目录 - 提交 ISS-001 遗留的 devflow 档案文件 - 更新 ISS-002 状态为已修复 - 新增 mvp/architecture/action-memory-relevance.md 设计文档 - 更新 mvp/README.md 文档导航 - 更新 devflow/index.md OpenSpec 链接指向 archive
276 lines
8.5 KiB
Markdown
276 lines
8.5 KiB
Markdown
# 行动记忆与检索质量归一化
|
||
|
||
Executor 行动记忆 + 归一化质量等级设计,解决 ISS-002 Executor 无约束重复检索问题。
|
||
|
||
---
|
||
|
||
## 一、问题背景
|
||
|
||
ISS-001 修复文档级去重后,Executor 在单次会话中仍调用 `lookup_knowledge` 20+ 次。根因:
|
||
|
||
1. **行动记忆缺失**:Executor 不知道自己已检索过哪些域
|
||
2. **质量信号缺失**:检索结果没有给 LLM 判断"结果够不够"的信号
|
||
3. **Prompt 缺少合法出口**:原 prompt 要求"所有外部信息都必须调用工具",LLM 不敢停止检索
|
||
|
||
---
|
||
|
||
## 二、整体架构
|
||
|
||
```
|
||
lookup_knowledge(query)
|
||
│
|
||
├─ Step 1: L0 精确匹配(keywords 索引)
|
||
├─ Step 2: L1 语义检索(Milvus 向量)
|
||
├─ Step 3: computeRelevance()
|
||
│ ├─ 归一化:L2 → similarity [0,1]
|
||
│ └─ 判定:PRECISE / HIGHLY_RELEVANT / REFERENCE
|
||
├─ Step 4: RetrievedDocTracker 检查
|
||
│ ├─ 文档级去重 → isDocRetrieved(sessionId, docKey)
|
||
│ ├─ 域级检查 → isDomainRetrieved(sessionId, domain)
|
||
│ └─ 记录 → markRetrieved(sessionId, domain, docKey)
|
||
└─ Step 5: 返回 LookupResult
|
||
├─ primary / supplement(原始内容,不含分数)
|
||
├─ relevanceLevel(PRECISE / HIGHLY_RELEVANT / REFERENCE)
|
||
├─ completenessHint(兜底信号)
|
||
└─ retrievedDomainsThisSession(行动记忆)
|
||
```
|
||
|
||
### 设计原则
|
||
|
||
| 原则 | 说明 |
|
||
|------|------|
|
||
| **Agent 边界清晰** | 不给 Executor 注入 knowledge map,Executor 只知道做了什么,不用知道有什么 |
|
||
| **分数封装** | L0/L1 原始分数不在 LookupResult 中返回 LLM,只在归一化层内部使用 |
|
||
| **原始分数只入库** | 原始 L2 距离写进 `tool_invocation.retrieval_details` JSON 用于可观测 |
|
||
| **软约束 + 硬拦截** | Prompt 约束(软)+ 工具层域级去重(硬)两层防御 |
|
||
|
||
---
|
||
|
||
## 三、归一化质量等级
|
||
|
||
### L2 距离归一化
|
||
|
||
BGE-M3 输出为 L2 归一化单位向量(实测范数=1.00000002),L2 距离数学硬上界 = 2.0。
|
||
|
||
```
|
||
similarity = 1 - min(l2Score, maxL2Distance) / maxL2Distance
|
||
```
|
||
|
||
| L2 距离 | similarity | 等级 |
|
||
|---------|-----------|------|
|
||
| 0.0 | 1.0 | PRECISE |
|
||
| 0.383 | 0.8085 | HIGHLY_RELEVANT |
|
||
| 0.5 | 0.75 | HIGHLY_RELEVANT |
|
||
| 0.6031 | 0.6984 | REFERENCE |
|
||
| 1.0 | 0.5 | REFERENCE 边界 |
|
||
| 2.0+ | 0.0 | 不视为有效结果 |
|
||
|
||
### 三等级判定
|
||
|
||
| 等级 | 条件 | completenessHint | LLM 行为 |
|
||
|------|------|-----------------|---------|
|
||
| PRECISE | L0 matchCount == 1 | "知识库中不存在比上述结果更精准的文档" | 直接使用,禁止再检索 |
|
||
| HIGHLY_RELEVANT | L0 命中 + similarity ≥ 0.75,或仅 L1 similarity ≥ 0.75 | "当前结果已高度相关,继续检索不太可能找到更精准的文档" | 可综合推理,大概率不需要继续查 |
|
||
| REFERENCE | 其余命中(similarity ≥ 0.5) | "当前结果为相关参考,如需更精准信息请明确缺少的具体维度" | 可参考,如需更精准请指出缺少的维度后定向补充 |
|
||
|
||
### 阈值配置
|
||
|
||
```yaml
|
||
retrieval:
|
||
normalization:
|
||
max-l2-distance: 2.0 # L2 距离上界
|
||
highly-relevant-threshold: 0.75 # similarity ≥ 0.75 → HIGHLY_RELEVANT
|
||
reference-threshold: 0.5 # similarity ≥ 0.5 → REFERENCE
|
||
```
|
||
|
||
---
|
||
|
||
## 四、行动记忆
|
||
|
||
### RetrievedDocTracker 数据结构
|
||
|
||
```java
|
||
// 从单层升级为双层:session → domain → filePath 集合
|
||
ConcurrentHashMap<String, Map<String, Set<String>>> sessionRetrievals;
|
||
```
|
||
|
||
### API
|
||
|
||
| 方法 | 作用 |
|
||
|------|------|
|
||
| `markRetrieved(sessionId, domain, filePath)` | 记录一次检索 |
|
||
| `isDocRetrieved(sessionId, filePath)` | 文档级去重 |
|
||
| `isDomainRetrieved(sessionId, domain)` | 域级检查 |
|
||
| `getRetrievedDomains(sessionId)` | 获取已检索域列表 |
|
||
| `clearSession(sessionId)` | 清理会话记录 |
|
||
|
||
### LookupResult 返回
|
||
|
||
```java
|
||
LookupResult.builder()
|
||
.found(true)
|
||
.primary(primaryResult)
|
||
.supplement(supplementResult)
|
||
.relevanceLevel("HIGHLY_RELEVANT") // PRECISE / HIGHLY_RELEVANT / REFERENCE
|
||
.completenessHint("当前结果已高度相关...") // 兜底信号
|
||
.retrievedDomainsThisSession(["infrastructure", "api"]) // 行动记忆
|
||
.message("...")
|
||
.build();
|
||
```
|
||
|
||
---
|
||
|
||
## 五、Executor Prompt 约束
|
||
|
||
### 4 条检索约束
|
||
|
||
1. **判断重复**:基于 `retrievedDomainsThisSession` 判断语义重叠
|
||
2. **重复了怎么办**:禁止换关键词重查;先指缺少的维度,再定向补充
|
||
3. **合法出口**:"不查全不会被追责,重复检索才会被惩罚"
|
||
4. **利用质量信号**:PRECISE → 停止;HIGHLY_RELEVANT + 域已检索 → 禁止;REFERENCE → 指出缺少维度
|
||
|
||
### 关键变化
|
||
|
||
原有 prompt:"所有需要外部信息的地方,都必须调用对应的工具"
|
||
→ 改为:"需要外部信息时调用工具,但须遵守下方的检索约束"
|
||
|
||
---
|
||
|
||
## 六、数据库变更
|
||
|
||
### V010
|
||
|
||
```sql
|
||
ALTER TABLE tool_invocation
|
||
ADD COLUMN relevance_level VARCHAR(20) COMMENT 'PRECISE/HIGHLY_RELEVANT/REFERENCE/DEDUPED',
|
||
ADD COLUMN dedup_reason VARCHAR(32) COMMENT 'doc_retrieved/domain_retrieved/null';
|
||
```
|
||
|
||
### retrieval_details JSON 扩展
|
||
|
||
```json
|
||
{
|
||
"l0_titles": ["MySQL 数据库连接池配置", "Redis 缓存配置指南"],
|
||
"l1_scores": [0.383, 0.4502, 0.7011],
|
||
"l1_top_score": 0.383,
|
||
"l1_top_similarity": 0.8085,
|
||
"relevance_level": "HIGHLY_RELEVANT",
|
||
"completeness_hint": "当前结果已高度相关,继续检索不太可能找到更精准的文档",
|
||
"retrieved_domains": ["infrastructure"]
|
||
}
|
||
```
|
||
|
||
扩展字段使用方式:
|
||
|
||
| 字段 | 用途 |
|
||
|------|------|
|
||
| `l1_top_score` | 原始 L2 距离最小值(可观测性) |
|
||
| `l1_top_similarity` | 归一化后的相似度 [0,1] |
|
||
| `relevance_level` | 归一化质量等级 |
|
||
| `completeness_hint` | 兜底信号 |
|
||
| `retrieved_domains` | 已检索域列表 |
|
||
| `dedup_reason` | 去重原因(如有) |
|
||
|
||
---
|
||
|
||
## 七、使用场景
|
||
|
||
### 场景 1:正常检索
|
||
|
||
```
|
||
用户:数据库连接池怎么配置?
|
||
|
||
Executor 内部:
|
||
1. lookup_knowledge("数据库连接池配置")
|
||
→ relevanceLevel=HIGHLY_RELEVANT (similarity=0.8085)
|
||
→ completenessHint="当前结果已高度相关..."
|
||
→ retrievedDomainsThisSession=["infrastructure"]
|
||
2. 基于已有信息直接回答,不再检索
|
||
```
|
||
|
||
### 场景 2:行动记忆阻止重复
|
||
|
||
```
|
||
Executor 步骤列表:
|
||
- 查数据库连接池配置
|
||
- 查 HikariCP 参数
|
||
- 查连接池耗尽排查
|
||
|
||
实际行为:
|
||
1. lookup("数据库连接池") → relevance=HIGHLY_RELEVANT, domains=["infrastructure"]
|
||
2. lookup("HikariCP 参数") → retrievedDomainsThisSession=["infrastructure"]
|
||
LLM 判断:infrastructure 域已检索过,禁止换关键词重查
|
||
→ 基于已有信息回答,指出缺少的具体维度
|
||
3. lookup("连接池耗尽") → 同域,被 prompt 约束拦截或工具层去重拦截
|
||
```
|
||
|
||
### 场景 3:PRECISE 精确匹配
|
||
|
||
```
|
||
用户:ERR_TIMEOUT 是什么?
|
||
|
||
Executor 内部:
|
||
1. lookup_knowledge("ERR_TIMEOUT")
|
||
→ L0 matchCount=1(唯一精确匹配)
|
||
→ relevanceLevel=PRECISE
|
||
→ completenessHint="知识库中不存在比上述结果更精准的文档"
|
||
2. 直接使用,不再检索
|
||
```
|
||
|
||
### 场景 4:REFERENCE + 定向补充
|
||
|
||
```
|
||
用户:如何排查生产故障?
|
||
|
||
Executor 内部:
|
||
1. lookup_knowledge("故障排查")
|
||
→ relevanceLevel=REFERENCE (similarity=0.6)
|
||
→ retrievedDomainsThisSession=["troubleshooting"]
|
||
2. LLM 判断:信息不足,缺少"日志分析"维度的具体步骤
|
||
3. lookup_knowledge("日志分析步骤")
|
||
→ 定向补充,不盲目换关键词
|
||
```
|
||
|
||
---
|
||
|
||
## 八、可观测性
|
||
|
||
### 查询质量分布
|
||
|
||
```sql
|
||
SELECT relevance_level, COUNT(*) AS cnt
|
||
FROM tool_invocation
|
||
WHERE tool_name = 'lookup_knowledge'
|
||
GROUP BY relevance_level;
|
||
```
|
||
|
||
### 去重原因分布
|
||
|
||
```sql
|
||
SELECT dedup_reason, COUNT(*) AS cnt
|
||
FROM tool_invocation
|
||
WHERE tool_name = 'lookup_knowledge'
|
||
GROUP BY dedup_reason;
|
||
```
|
||
|
||
### 归一化分数分布
|
||
|
||
```sql
|
||
SELECT
|
||
JSON_EXTRACT(retrieval_details, '$.l1_top_similarity') AS similarity,
|
||
COUNT(*) AS cnt
|
||
FROM tool_invocation
|
||
WHERE tool_name = 'lookup_knowledge'
|
||
AND retrieval_details IS NOT NULL
|
||
GROUP BY similarity
|
||
ORDER BY similarity;
|
||
```
|
||
|
||
---
|
||
|
||
## 九、扩展方向(Phase 2)
|
||
|
||
- **域级硬限流**:`isDomainRetrieved` 已就绪,在 LookupKnowledgeTool 入口直接拦截同域调用,不依赖 LLM 遵守 prompt
|
||
- **DEDUPED 等级**:去重时单独标记为 DEDUPED 等级,与 REFERENCE 区分
|
||
- **分数反馈调优**:基于 feedback 数据优化归一化阈值
|