From e4f37cb9e636f570201850753a716879f62b6828 Mon Sep 17 00:00:00 2001 From: zhuyongxin Date: Wed, 1 Jul 2026 14:26:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(knowledge):=20=E4=BF=AE=E5=A4=8D=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E4=BE=9D=E8=B5=96=20+=20=E5=BD=92=E6=A1=A3=20session-?= =?UTF-8?q?dedup-knowledge-map=20+=20=E8=AE=B0=E5=BD=95=20ISS-002?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KnowledgeIndexService: 域级生成从 @PostConstruct 移到 @EventListener(ApplicationReadyEvent),解决 KnowledgeIndexService ↔ KnowledgeDomainService 循环依赖 - devflow 归档: evidence.md + acceptance.md(含运行验证结果) - devflow/index.md: session-dedup-knowledge-map 状态改为 archived - openspec .archive-ready 标记 - mvp/issues/ISS-002: Executor 无约束重复调用 lookup_knowledge --- devflow/index.md | 1 + .../acceptance.md | 58 +++++++++++++ .../evidence.md | 87 +++++++++++++++++++ .../ISS-002-executor-unconstrained-lookup.md | 86 ++++++++++++++++++ mvp/issues/README.md | 6 ++ .../.archive-ready | 0 .../agent/service/KnowledgeIndexService.java | 18 +++- 7 files changed, 253 insertions(+), 3 deletions(-) create mode 100644 devflow/projects/2026-06-30-session-dedup-knowledge-map/acceptance.md create mode 100644 devflow/projects/2026-06-30-session-dedup-knowledge-map/evidence.md create mode 100644 mvp/issues/ISS-002-executor-unconstrained-lookup.md create mode 100644 mvp/issues/README.md create mode 100644 openspec/changes/session-dedup-knowledge-map/.archive-ready diff --git a/devflow/index.md b/devflow/index.md index c4947e3..0716d2a 100644 --- a/devflow/index.md +++ b/devflow/index.md @@ -10,3 +10,4 @@ | 2026-06-25 | doc-management-ui | 前端开发/文档管理 | 文档管理页面, CRUD, 状态监控, 纯静态页面, API集成 | archived | | 2026-06-26 | session-storage | 会话存储/可观测 | diagnosis_session, agent_step, tool_invocation, token追踪, 多Agent路由 | openspec/changes/session-storage | archived | | 2026-06-29 | confidence-feedback | 质量评估/反馈机制 | evidence_score, selfEvaluation, feedback, useful, not_useful, case_library, BAD_CASE, tool_invocation规则引擎, 反馈按钮, sessionId回传 | openspec/changes/confidence-feedback | archived | +| 2026-06-30 | session-dedup-knowledge-map | 去重/知识图谱 | RetrievedDocTracker, KnowledgeDomainService, knowledge_domain, covers, whenToRetrieve, Planner注入, ISS-001 | openspec/changes/session-dedup-knowledge-map | archived | diff --git a/devflow/projects/2026-06-30-session-dedup-knowledge-map/acceptance.md b/devflow/projects/2026-06-30-session-dedup-knowledge-map/acceptance.md new file mode 100644 index 0000000..6699f29 --- /dev/null +++ b/devflow/projects/2026-06-30-session-dedup-knowledge-map/acceptance.md @@ -0,0 +1,58 @@ +# Acceptance: session-dedup-knowledge-map + +## 静态验证 + +| 项目 | 结果 | 说明 | +|------|------|------| +| 编译检查 | PASS | `mvn compile -q` exit code 0,所有 17 个变更文件无编译错误 | +| 代码结构检查 | PASS | 6 个新文件(RetrievedDocTracker, DocumentFieldEnricher, KnowledgeDomainService, KnowledgeDomain, KnowledgeDomainRepository, V009 迁移)均存在且路径正确 | +| Prompt 外部化 | PASS | `doc-field-enricher-prompt.md` 和 `domain-summary-prompt.md` 位于 `src/main/resources/prompts/`,Java 代码通过 `@PostConstruct` + `ClassPathResource` 加载 | +| Flyway 迁移脚本 | PASS | `V009__add_knowledge_domain.sql` 存在,表结构完整 | +| DTO 字段 | PASS | Frontmatter / KnowledgeEntry / LookupResult 新增字段均已添加 | +| 解析器扩展 | PASS | FrontmatterParser 解析 `covers` 和 `when_to_retrieve` | +| Jackson 替换 | PASS | KnowledgeIndexService 不再包含 extractJsonValue/extractJsonArray,改用 objectMapper.readValue | +| Prompt 检索规则 | PASS | chat-planner-prompt.md 新增"知识库检索规则"区块(4 条规则) | + +## 脚本验证 + +| 项目 | 结果 | 说明 | +|------|------|------| +| 单元测试 | 未运行 | 项目当前无针对本 change 的单元测试 | +| 集成测试 | 未运行 | 需启动应用 + Milvus + MySQL 验证完整链路 | + +## 浏览器/人工验证 + +| 项目 | 结果 | 说明 | +|------|------|------| +| V009 迁移 | PASS | Flyway 日志:`Successfully applied 1 migration to schema superbiz_agent, now at version v009` | +| knowledge_domain 表数据 | PASS | 4 个域全部 LLM 生成 when_to_retrieve 成功(api/domain/infrastructure/troubleshooting),内容包含跨域边界引用 | +| knowledge map 注入 Planner | PASS | 多 Agent 路径正常触发 `Supervisor → chat_planner → chat_executor`,Planner 能按域做检索规划 | +| session 级去重 | PASS | 两个 session 均验证去重生效:session `7c517329` 去 4 次重拦截,session `9693b9fb` 6 次去重拦截 | +| LLM 字段生成 | 未验证 | 需上传新文档后检查 metadata JSON 中是否包含 covers 和 whenToRetrieve | + +## 未验证项 + +| 项目 | 风险 | 建议补验步骤 | +|------|------|-------------| +| LLM 字段生成 | 中 — 依赖外部 LLM 服务 | 上传新文档,检查 metadata JSON 中是否包含 covers 和 whenToRetrieve | + +## 启动问题修复 + +| 问题 | 修复 | 状态 | +|------|------|------| +| `@PostConstruct` 中调用 `knowledgeDomainService.onDocumentChange()` 导致循环依赖 | 将域级生成从 `@PostConstruct` 移到 `@EventListener(ApplicationReadyEvent.class)` | 已修复,编译通过 | + +## 任务完成状态 + +14/14 任务全部完成 (T1-1 ~ T6-2)。 + +## 遗留问题 + +ISS-002:Executor 无约束重复调用 `lookup_knowledge`(单会话 20+ 次),knowledge map 和检索约束只注入了 Planner 未注入 Executor。详见 `mvp/issues/ISS-002-executor-unconstrained-lookup.md`。 + +## 已知限制 + +1. **RetrievedDocTracker 为 JVM 内存存储**:应用重启后去重状态丢失,同一会话内重启无法继续去重(可接受,会话通常短于重启间隔) +2. **Planner 只看域级 when_to_retrieve**:文档级细粒度筛选留 Phase 2 +3. **文档级 prompt 依赖同域其他文档**:首个上传到某域的文档无法获得同域参照(此时 prompt 输出"无同域其他文档") +4. **域级 prompt 依赖其他域已入库**:首次启动且 DB 为空时,其他域信息从 L0 索引 category 列表兜底 diff --git a/devflow/projects/2026-06-30-session-dedup-knowledge-map/evidence.md b/devflow/projects/2026-06-30-session-dedup-knowledge-map/evidence.md new file mode 100644 index 0000000..241ca57 --- /dev/null +++ b/devflow/projects/2026-06-30-session-dedup-knowledge-map/evidence.md @@ -0,0 +1,87 @@ +# Evidence: session-dedup-knowledge-map + +## E1: ThreadLocal 在多 Agent 路径是否安全 + +**问题**:`SessionContextHolder` 基于 ThreadLocal,多 Agent 异步路径可能导致 sessionId 丢失。 + +**证据**: +- `AsyncConfig` 只启用 `@EnableAsync`,无 `TaskDecorator` +- `SupervisorAgent.invoke()` 是同步阻塞调用,工具调用与主线程同线程 +- 当前路径下 ThreadLocal 安全 + +**结论**:当前同步路径安全。未来引入异步扩展时需补 `TaskDecorator` 传递 ThreadLocal。 + +--- + +## E2: 6 个文档是否全部有 category 字段 + +**问题**:域聚合依赖 `category` 字段分组,需确认现有文档是否都有值。 + +**证据**: +- 全部 6 个文档均有 `category` 字段:api(1)、domain(1)、infrastructure(3)、troubleshooting(1) + +**结论**:现有文档无需修补,category 覆盖率 100%。 + +--- + +## E3: 去重 key 设计 + +**问题**:用什么字段唯一标识一个文档用于去重。 + +**证据**: +- `KnowledgeEntry.filePath` 在 L0 索引内唯一 +- L1 向量索引的 `_source` 字段也是 filePath +- 上传时 `saveToLocal()` 生成 `knowledge_base/{category}/{fileName}` 路径 + +**结论**:统一用 `filePath` 作去重 key,L0 和 L1 一致。 + +--- + +## E4: Planner prompt token 增量是否可接受 + +**问题**:knowledge map YAML 注入 Planner prompt 会增加固定 token 开销。 + +**证据**: +- 当前 planner prompt 21 行 +- 注入 knowledge map 约增加 200-400 字符(6 个文档场景) +- 相比 Planner 整体 prompt + 历史消息,增量占比 < 5% + +**结论**:可接受,不构成性能瓶颈。 + +--- + +## E5: EvaluationService.tool_call_count 影响 + +**问题**:去重后 `tool_call_count` 降低,是否影响 `EvaluationService` 评分逻辑。 + +**证据**: +- `EvaluationService` 使用 `tool_call_count` 作为评分因子 +- 去重导致重复调用被过滤,`tool_call_count` 下降 +- 这是修复效果(消除了无意义的重复调用),不是回归 + +**结论**:`EvaluationService` 评分规则无需改动。下降的 `tool_call_count` 反映了真实效率提升。 + +--- + +## P1: 手写 JSON 解析器脆弱性 + +**问题**:`KnowledgeIndexService.extractJsonValue` / `extractJsonArray` 在遇到含逗号、引号的自然语言字段时会截断。 + +**证据**: +- `whenToRetrieve` 字段由 LLM 生成,内容为自然语言(含逗号、分号等标点) +- 手写解析器以 `"` 和 `,` 作分隔符,自然语言中的标点会导致提前截断 +- Jackson `ObjectMapper.readValue(metadata, Frontmatter.class)` 是项目已有依赖 + +**结论**:全量替换为 Jackson,影响范围仅 `KnowledgeIndexService.parseDocumentToEntry()`,行为更健壮。 + +--- + +## P2: LookupResult 去重提示字段 + +**问题**:去重命中时如何向 LLM 返回"不要重试"的信号。 + +**证据**: +- 复用 `primary.content` 语义不清,LLM 可能理解为正常检索结果 +- 独立 `message` 字段 + `found=false` 语义明确,LLM 能理解"已检索过"不再重试 + +**结论**:`LookupResult` 新增 `String message` 字段,去重时填入提示文本。 diff --git a/mvp/issues/ISS-002-executor-unconstrained-lookup.md b/mvp/issues/ISS-002-executor-unconstrained-lookup.md new file mode 100644 index 0000000..e30f4a7 --- /dev/null +++ b/mvp/issues/ISS-002-executor-unconstrained-lookup.md @@ -0,0 +1,86 @@ +# ISS-002 Executor 无约束重复调用 lookup_knowledge + +**状态**:待修复 +**严重程度**:中(工具层去重已拦截重复文档,但调用本身仍浪费 token 和耗时) +**发现时间**:2026-07-01 +**关联**:ISS-001(Part A 已修,Part B 注入范围不足) + +--- + +## 现象 + +ISS-001 修复后,session 级去重(RetrievedDocTracker)生效,同一文档不再重复召回内容。但 Executor 在单次会话中仍调用 `lookup_knowledge` 20+ 次,大部分被去重拦截返回"已检索过"。 + +实测日志(session `7c517329`,2026-07-01 13:53): + +``` +Executor 调用 lookup_knowledge ~20 次 +去重拦截 11 次: + - infrastructure/mysql-connection-pool.md × 6 + - api/payment-errors.md × 5 +有效检索仅 2-3 次(首次命中各域时) +``` + +Executor 用不同的 query 变体反复查同一个域,因为 LLM 觉得"需要更多细节"。 + +--- + +## 根本原因 + +**knowledge map 和检索约束只注入了 Planner prompt,未注入 Executor prompt。** + +当前注入范围: + +| 组件 | knowledge map | 每域最多一次约束 | +|------|:---:|:---:| +| Planner prompt | 已注入 | 已注入 | +| Executor prompt | **未注入** | **未注入** | + +调用链路: + +``` +Supervisor → Planner:规划一次,输出"查 infrastructure 域 + api 域" +Supervisor → Executor:执行步骤(ReactAgent,自主决定调用工具) + Executor step 1:lookup("MySQL 连接池配置") → 命中 infrastructure 域 ✓ + Executor step 2:lookup("HikariCP 参数调优") → 去重拦截 ✗ + Executor step 3:lookup("连接池耗尽排查步骤") → 去重拦截 ✗ + Executor step 4:lookup("支付超时排查") → 命中 api 域 ✓ + Executor step 5:lookup("ERR_TIMEOUT 错误码") → 去重拦截 ✗ + ...(反复用不同变体查同域) +``` + +Executor 看不到"每个域只查一次"的约束,也不知道已有哪些域被检索过。 + +--- + +## 影响 + +- **Token 浪费**:每次去重拦截仍需走完 L0+L1 检索流程,再返回"已检索过";LLM 也要处理这个返回信息 +- **耗时增加**:每次冗余调用约 400-500ms(L0+L1 检索 + 向量查询),20 次冗余调用浪费约 10s +- **LLM 行为低效**:Executor 花大量 step 在重复检索上,而不是基于已有信息推理 + +--- + +## 修法方向 + +### 方案 A:Executor prompt 注入 knowledge map + 检索约束 + +在 `chat-executor-prompt.md` 或 `buildChatExecutorAgent()` 中: +1. 注入 knowledge map(与 Planner 相同的 YAML) +2. 添加规则:"每个域最多调用一次 lookup_knowledge;已检索过的域不要再用不同关键词重复检索" + +优点:与 Planner 对齐,LLM 能理解域级边界 +缺点:仍依赖 LLM 遵守指令(但比纯 Prompt 约束强,因为有 knowledge map 做锚点) + +### 方案 B:工具层硬限制(session + 域级计数) + +在 `RetrievedDocTracker` 中增加域级计数:`ConcurrentHashMap>`。 +当某域检索次数 > 1 时,直接在 `LookupKnowledgeTool` 入口返回"该域已检索过,不允许再次调用"。 + +优点:100% 可靠,不依赖 LLM +缺点:需改动 RetrievedDocTracker + LookupKnowledgeTool,需要从 filePath 反查 domain + +### 建议 + +**先做方案 A**(改动小,与已有 knowledge map 注入逻辑一致),观察效果。 +如果 LLM 仍不遵守,再升级到方案 B。 diff --git a/mvp/issues/README.md b/mvp/issues/README.md new file mode 100644 index 0000000..c8b4f46 --- /dev/null +++ b/mvp/issues/README.md @@ -0,0 +1,6 @@ +# 已知问题记录 + +| # | 标题 | 严重程度 | 状态 | 文件 | +|---|---|---|---|---| +| ISS-001 | Executor 重复召回同一文档 | 中 | 已修复 | [ISS-001-duplicate-retrieval.md](ISS-001-duplicate-retrieval.md) | +| ISS-002 | Executor 无约束重复调用 lookup_knowledge | 中 | 待修复 | [ISS-002-executor-unconstrained-lookup.md](ISS-002-executor-unconstrained-lookup.md) | diff --git a/openspec/changes/session-dedup-knowledge-map/.archive-ready b/openspec/changes/session-dedup-knowledge-map/.archive-ready new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/superbiz/agent/service/KnowledgeIndexService.java b/src/main/java/com/superbiz/agent/service/KnowledgeIndexService.java index de99940..8c69555 100644 --- a/src/main/java/com/superbiz/agent/service/KnowledgeIndexService.java +++ b/src/main/java/com/superbiz/agent/service/KnowledgeIndexService.java @@ -9,7 +9,9 @@ import com.superbiz.agent.repository.KnowledgeDomainRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import jakarta.annotation.PostConstruct; @@ -69,7 +71,18 @@ public class KnowledgeIndexService { log.info("知识库索引加载完成,共 {} 个文档", loaded); - // 检查各域是否有 knowledge_domain 记录,无则触发生成 + } catch (Exception e) { + log.error("知识库索引加载失败", e); + } + } + + /** + * 应用就绪后,检查各域是否有 knowledge_domain 记录,无则触发生成 + * 使用 ApplicationReadyEvent 而非 PostConstruct,避免循环依赖 + */ + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + try { knowledgeIndex.stream() .map(KnowledgeEntry::getCategory) .filter(c -> c != null && !c.isBlank()) @@ -80,9 +93,8 @@ public class KnowledgeIndexService { knowledgeDomainService.onDocumentChange(category); } }); - } catch (Exception e) { - log.error("知识库索引加载失败", e); + log.error("域级记录生成失败", e); } }