# Design: executor-action-memory-relevance ## 架构设计 ### 整体数据流 ``` 用户问题 → Supervisor → Planner(规划查哪些域) → Supervisor → Executor(自主调用 lookup_knowledge) ↓ LookupKnowledgeTool ├─ L0 精确匹配 → l0Matches (含 category) ├─ L1 语义检索 → l1Results (含 L2 score) ├─ 归一化层 → computeRelevanceLevel(l0Count, l1TopScore) │ L2 距离 → similarity = 1 - min(score, 2.0) / 2.0 │ L0 唯一匹配 → PRECISE │ L0 命中 + L1 similarity ≥ 0.75 → HIGHLY_RELEVANT │ 仅 L1 similarity ≥ 0.75 → HIGHLY_RELEVANT │ L0 多匹配 + L1 similarity [0.5, 0.75) → REFERENCE │ 仅 L1 similarity [0.5, 0.75) → REFERENCE ├─ 域级行动记忆 → RetrievedDocTracker.markRetrieved(sessionId, domain, filePath) │ getRetrievedDomains(sessionId) → retrievedDomainsThisSession ├─ 文档级去重 → 保留现有逻辑 └─ 组装 LookupResult(含 relevanceLevel, completenessHint, retrievedDomainsThisSession) ↓ LLM 看到: relevanceLevel: PRECISE completenessHint: "知识库中不存在比上述结果更精准的文档" retrievedDomainsThisSession: ["infrastructure", "api"] ``` ### Agent 边界(保持清晰) | Agent | 知道什么 | 不知道什么 | |-------|---------|-----------| | Planner | 全域知识边界(knowledge map) | 执行细节、检索结果 | | Executor | 自己的行动记忆(已检索域列表) | 全域知识边界(不注入 knowledge map) | 行动记忆通过**工具返回值**传递,不通过 prompt 注入。 ### 数据结构设计 #### 1. RetrievedDocTracker 升级 ```java // 现有:sessionId → Set(文档级) ConcurrentHashMap> retrieved // 新增:sessionId → { domain → Set }(域级 + 文档级) ConcurrentHashMap>> sessionRetrievals ``` 方法列表: - `markRetrieved(sessionId, domain, filePath)` — 一次记录两层 - `isDocRetrieved(sessionId, filePath)` → boolean — 文档级去重(替代现有 isAlreadyRetrieved) - `isDomainRetrieved(sessionId, domain)` → boolean — 域级检查(Phase 2 硬限制用) - `getRetrievedDomains(sessionId)` → List — 行动记忆(返回给 LLM) - `clearSession(sessionId)` — 清理(不变) #### 2. LookupResult 扩展 ```java @Data @Builder public class LookupResult { boolean found; PrimaryResult primary; // 不变,不暴露原始分数 SupplementResult supplement; // 不变,不暴露原始分数 // ---- 新增 ---- String relevanceLevel; // PRECISE / HIGHLY_RELEVANT / REFERENCE String completenessHint; // 兜底信号 List retrievedDomainsThisSession; // 行动记忆 String message; // 不变 } ``` **PrimaryResult 和 SupplementResult 不加任何分数字段**。原始分数在归一化层内部消化。 #### 3. 归一化计算 `RelevanceNormalizer`(LookupKnowledgeTool 内部静态方法): ``` 输入:l0MatchCount, l1TopScore (L2 距离) 输出:RelevanceAssessment { relevanceLevel, completenessHint } 归一化公式(BGE-M3 输出 L2 归一化单位向量,已实测验证): similarity = 1 - min(l2Score, maxL2Distance) / maxL2Distance maxL2Distance 默认 2.0,yml 可覆盖 判定逻辑: if l0MatchCount == 1 → PRECISE if l0MatchCount > 1 && l1Similarity >= highlyRelevantThreshold → HIGHLY_RELEVANT if l0MatchCount == 0 && l1Similarity >= highlyRelevantThreshold → HIGHLY_RELEVANT if l0MatchCount > 1 && l1Similarity >= referenceThreshold → REFERENCE if l0MatchCount == 0 && l1Similarity >= referenceThreshold → REFERENCE else → 无结果 completenessHint 映射: PRECISE → "知识库中不存在比上述结果更精准的文档" HIGHLY_RELEVANT → "当前结果已高度相关,继续检索不太可能找到更精准的文档" REFERENCE → "当前结果为相关参考,如需更精准信息请明确缺少的具体维度" ``` 配置项(application.yml): ```yaml retrieval: normalization: max-l2-distance: 2.0 # L2 距离上界(单位向量 = 2.0) highly-relevant-threshold: 0.75 # similarity ≥ 0.75 → HIGHLY_RELEVANT reference-threshold: 0.5 # similarity ≥ 0.5 → REFERENCE ``` #### 4. 入库记录扩展 `tool_invocation` 表新增列: | 列名 | 类型 | 说明 | |------|------|------| | `relevance_level` | VARCHAR(20) | PRECISE / HIGHLY_RELEVANT / REFERENCE / DEDUPED | | `dedup_reason` | VARCHAR(32) | doc_retrieved / domain_retrieved / null | `retrieval_details` JSON 扩展: ```json { "l0_match_count": 2, "l0_titles": ["MySQL连接池配置", "HikariCP参数调优"], "l1_top_score": 0.52, "l1_top_similarity": 0.74, "l1_match_count": 3, "l1_scores": [0.52, 0.68, 0.91], "relevance_level": "HIGHLY_RELEVANT", "completeness_hint": "当前结果已高度相关...", "retrieved_domains": ["infrastructure"], "dedup_reason": null } ``` 原始 L2 score 和归一化后的 similarity 都入库,保留可观测性。 ### Executor Prompt 设计 不加 knowledge map,只加基于行动记忆的行为规则: ```markdown ## 检索约束 ### 1. 判断重复:基于已检索上下文 每次 lookup_knowledge 返回值中包含 retrievedDomainsThisSession, 表示本次会话已检索过的知识域。如果当前问题与已检索域语义重叠, **禁止再次调用 lookup_knowledge**。 ### 2. 重复了该怎么办 如果当前想检索的内容与【已检索上下文】语义相似: - 禁止换关键词重新检索 - 直接基于已有事实回答 - 如果信息不足,先明确指出缺少什么具体维度 (如:"缺少 HikariCP 具体配置参数"、"缺少连接池耗尽的日志样例"), 再针对该维度进行一次定向补充检索——而非盲目换词重查 ### 3. 合法出口:允许信息不全时给出结论 如果你认为已有信息足以回答核心问题,即使细节不全, 也请直接给出结论并说明局限性(如:"基于已有信息,连接池配置建议如下, 但具体参数值需结合实际负载调整")。 **不查全不会被追责,重复检索才会被惩罚。** ### 4. 利用质量信号判断 - relevanceLevel=PRECISE → 信息精准,直接使用,不再检索 - relevanceLevel=HIGHLY_RELEVANT + 域已在 retrievedDomainsThisSession → 禁止再次调用 - relevanceLevel=REFERENCE → 先指出缺什么维度,再定向补充一次 - completenessHint 是知识库给你的天花板信号,信任它 ``` ### 关键决策 1. **L0/L1 原始分数不暴露给 LLM** — 在归一化层内部消化,避免 LLM 混淆尺度 2. **BGE-M3 L2 归一化已实测验证** — 范数 1.00000002,maxL2Distance=2.0 是数学硬上界 3. **行动记忆通过工具返回值传递** — 不通过 prompt 注入,不修改 ReactAgent prompt 构建方式 4. **不给 Executor knowledge map** — 保持 Agent 边界:Planner 知道全域,Executor 只知道自己做了什么 5. **Phase 2 域级硬限制暂不实施** — 先观察 prompt 约束 + 归一化信号的效果 ### 接口影响分级 | 变更 | 级别 | 说明 | |------|------|------| | RetrievedDocTracker 数据结构升级 | L2 内部接口 | 消费者只有 LookupKnowledgeTool,在同一实现范围内 | | LookupResult 新增 3 个字段 | L2 内部接口 | 消费者是 LLM(工具返回值),无跨模块调用方 | | tool_invocation 表新增 2 列 | L2 内部接口 | Flyway 迁移,nullable,不影响现有查询 | | chat-executor-prompt.md 更新 | L1 内部实现 | Prompt 文本变更,不改变接口 |