# Functional Spec: session-dedup-knowledge-map ## REQ-01:工具层去重(Part A) **触发**:`LookupKnowledgeTool.lookupKnowledge(query)` 被调用 **行为**: 1. 从 `SessionContextHolder.getSessionId()` 获取当前 sessionId;若为 null(非会话上下文)跳过去重逻辑,正常检索 2. L0+L1 检索完成后,将结果中已在 `RetrievedDocTracker` 中标记的 filePath 过滤掉 3. 若过滤后 L0 结果为空、L1 结果也为空(全部已召回),返回 `LookupResult.found=false`,并在结果中附带提示文本:"以下文档已在本会话中检索过:[列表],无需重复召回" 4. 未被过滤的文档正常返回后,将其 filePath 写入 `RetrievedDocTracker` 5. `SessionContextHolder.clear()` 调用时,`RetrievedDocTracker.clearSession(sessionId)` 同步清理 **验收**: - 同一 session 内同一文档第二次命中时,返回去重提示而非完整文档内容 - 不同 session 之间互不影响 - sessionId 为 null 时不影响正常检索流程 --- ## REQ-02:文档级 LLM 字段生成(Part B - 文档级) **触发**:`DocumentManagementService.uploadDocument()` 完成 frontmatter 解析后 **行为**: 1. 若文档 frontmatter 中已包含 `covers` 和 `whenToRetrieve`,跳过 LLM 生成(作者手动填写优先) 2. 否则,调用 LLM,输入为文档 title + summary + 正文前 1000 字符 3. Prompt 要求 LLM 返回 JSON:`{"covers": [...], "whenToRetrieve": "..."}` 4. 解析结果,回填到 `Frontmatter` 对象 5. 序列化存入 `api_document.metadata`;同步更新 `KnowledgeEntry` 写入 L0 索引 6. LLM 调用失败时,`covers` 置为空列表,`whenToRetrieve` 置为 summary(降级),不阻断上传流程 **验收**: - 上传后 `api_document.metadata` 中包含 `covers` 和 `whenToRetrieve` 字段 - frontmatter 已有这两个字段时不覆盖 - LLM 调用异常时文档仍上传成功,字段降级填充 --- ## REQ-03:域级聚合与存储(Part B - 域级) **触发**:文档上传成功后;文档删除后;`loadIndex()` 时发现某域在 `knowledge_domain` 表无记录 **行为**: 1. `KnowledgeDomainService.onDocumentChange(category)` 读取该 category 下所有 `KnowledgeEntry` 的 title + covers + whenToRetrieve 2. 调用 LLM,生成域级 `when_to_retrieve`(要求 LLM 识别域内文档边界,输出含区分语义的路由描述) 3. 写入 `knowledge_domain` 表(upsert by domain_id),同时更新 `document_count` 4. LLM 调用失败时,`domain.when_to_retrieve` 保留上次 DB 记录;若无历史记录则置为空字符串 **验收**: - 上传文档后,对应 category 的 `knowledge_domain` 记录被更新 - 删除文档后,对应 category 的 `document_count` 减少,`when_to_retrieve` 重新生成 - `loadIndex()` 时无 DB 记录的域自动触发生成 --- ## REQ-04:knowledge map 注入 Planner(Part B - 注入) **触发**:`ChatService.buildChatPlannerAgent()` 调用时 **行为**: 1. 调用 `KnowledgeDomainService.buildKnowledgeMap()` 生成 YAML 文本 2. YAML 结构:域列表,每个域包含 domain_id、description、when_to_retrieve、documents(title + covers) 3. 若 knowledge_domain 表为空(无任何域记录),跳过注入,不修改 prompt 4. 注入位置:Planner system prompt 末尾,独立区块 **Planner prompt 附加规则**: - 制定步骤时,先查看 `available_knowledge_domains`,按 `when_to_retrieve` 判断是否需要检索该域 - 每个域最多指示 Executor 检索一次;已检索过的域不再安排检索步骤 **验收**: - Planner prompt 包含 `available_knowledge_domains` 区块 - 无域记录时 prompt 不包含该区块(不注入空结构) - knowledge map 文本长度 < 1000 字符(6 个文档场景下) --- ## REQ-05:Frontmatter 字段扩展 **行为**: - `Frontmatter.java` 新增 `List covers` 和 `String whenToRetrieve` - `KnowledgeEntry.java` 新增同名字段 - `FrontmatterParser.java` 解析 `covers`(YAML 数组)和 `when_to_retrieve`(YAML 字符串) **验收**: - 现有文档(无新字段)上传/解析不报错,字段为 null 或空列表 - 含新字段的文档正确解析