8.5 KiB
8.5 KiB
行动记忆与检索质量归一化
Executor 行动记忆 + 归一化质量等级设计,解决 ISS-002 Executor 无约束重复检索问题。
一、问题背景
ISS-001 修复文档级去重后,Executor 在单次会话中仍调用 lookup_knowledge 20+ 次。根因:
- 行动记忆缺失:Executor 不知道自己已检索过哪些域
- 质量信号缺失:检索结果没有给 LLM 判断"结果够不够"的信号
- 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) | "当前结果为相关参考,如需更精准信息请明确缺少的具体维度" | 可参考,如需更精准请指出缺少的维度后定向补充 |
阈值配置
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 数据结构
// 从单层升级为双层: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 返回
LookupResult.builder()
.found(true)
.primary(primaryResult)
.supplement(supplementResult)
.relevanceLevel("HIGHLY_RELEVANT") // PRECISE / HIGHLY_RELEVANT / REFERENCE
.completenessHint("当前结果已高度相关...") // 兜底信号
.retrievedDomainsThisSession(["infrastructure", "api"]) // 行动记忆
.message("...")
.build();
五、Executor Prompt 约束
4 条检索约束
- 判断重复:基于
retrievedDomainsThisSession判断语义重叠 - 重复了怎么办:禁止换关键词重查;先指缺少的维度,再定向补充
- 合法出口:"不查全不会被追责,重复检索才会被惩罚"
- 利用质量信号:PRECISE → 停止;HIGHLY_RELEVANT + 域已检索 → 禁止;REFERENCE → 指出缺少维度
关键变化
原有 prompt:"所有需要外部信息的地方,都必须调用对应的工具" → 改为:"需要外部信息时调用工具,但须遵守下方的检索约束"
六、数据库变更
V010
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 扩展
{
"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("日志分析步骤")
→ 定向补充,不盲目换关键词
八、可观测性
查询质量分布
SELECT relevance_level, COUNT(*) AS cnt
FROM tool_invocation
WHERE tool_name = 'lookup_knowledge'
GROUP BY relevance_level;
去重原因分布
SELECT dedup_reason, COUNT(*) AS cnt
FROM tool_invocation
WHERE tool_name = 'lookup_knowledge'
GROUP BY dedup_reason;
归一化分数分布
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 数据优化归一化阈值