# 行动记忆与检索质量归一化 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>> 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 数据优化归一化阈值