面试知识库

20 · 对标主流改造 · 实现设计(实施参考)#

配套文档:19-对标主流产品改造报告(调研与方案)。本篇是实施参考,落到具体文件/签名/删除清单/验证。

范围:架构层四刀。已排除:索引期 Contextual Retrieval、答案缓存→检索缓存/prompt caching、eval 护栏 + 反馈信号。

状态:A / C / B / E 已实施(2026-06-07);D 权限下推——数据权限部分已落地(KB 级「部门 + 公开/私有」,见 21-权限系统对齐报告)。详见文末「实施状态」节。


Context#

DocMind 现为「预定义推理型 RAG 管线」:4 前置闸 + 7 阶段主管线 + 4 路径 Mode + 大量 flag 短路。对标 Anthropic / Glean / RAGFlow / Vertex / Claude-agentic-search 后,本次做架构层四刀,目标是合并重复造的轮子、把软实现换成强实现,组件更少、能力更强、更贴主流。

已锁定的方向决策:

  1. 三条检索分支(多跳/拆解/CRAG-web)→ 真·LLM 工具调用循环(Spring AI .tools() + DashScope function calling,复用现有 @Tool bean,模型自驱拆/跳/补/停)。
  2. [n] 引用 → 稳健化 [n]→结构化(单次生成,后端解析为结构化引用对象 + 前端可点击,置信度 = 引用覆盖率 × rerank-top1)。
  3. 纳入检索层权限下推(kbIds 归属校验 + MCP 端点鉴权;userId 已是 auth 派生)。
  4. 完全移除自反思子系统(生成即终态,靠 citations 覆盖率 + CRAG 兜底)。

关键技术事实(已验证):

  • Spring AI 1.1.4:OpenAiChatOptions implements ToolCallingChatOptions,支持 internalToolExecutionEnabledtoolCallbackstoolNamesToolCallbacks.from(...)ToolCallingManager.executeToolCalls内部工具循环无迭代上限 → 必须 internalToolExecutionEnabled(false) 手动控环。
  • @Tool bean(DocSearchTool.searchDocs / WebSearchTool.webSearch / MemoryTool.recallMemory|storeMemory / KeywordSearchTool / KbMetaTool)已存在;执行时写 AgentToolContext(ThreadLocal)并强制 context kbIds(覆盖 LLM 传入,安全边界)。
  • AgentToolContext javadoc 明言为「ChatClient function-calling loop」设计,已有 activate/clear/get/isActivegetChunks/getMemoryContext/getCalledToolsaddChunks/putMemory
  • 契约 SupervisorResult(compressedChunks, needsFallback, retrievalLog, memoryContext, gradeResult),被 DocMindAgent.stageRetrieval → RetrievalContext 消费,必须保持。
  • 引用契约:PromptAssembler 注入 [i+1]SourcePayloadFactory{index:i+1,...}sources[n-1] ↔ prompt [n],顺序强相关。RetrievedChunk.id 是稳定 id(dedupeKey() id 优先)。
  • 权限:DocMindChatController.streamuserService.getCurrentUserId()(JWT 派生)取 userId ✓;但 kbIds 来自请求参数 KbIdsParser.parse未校验归属 ✗。会话类接口均已做 conv.getUserId().equals(userId) 校验。

Stage A — 结构化 span 级引用(P0-1,先做,C 依赖它)#

思路:保持单次流式生成、模型仍输出 [n];生成结束后用纯解析(无 LLM)把 [n] 提取为结构化引用对象,算覆盖率,前端可点击。

文件与改动:

  • service/rag/SourcePayloadFactory.java:每个 source 增加 idchunk.getId())字段,让引用可锚定稳定 id(保留现有 index)。
  • 新增 service/rag/CitationParser.java(纯 Java,无 LLM):
    • 入参:最终答案文本 + sources(含 index/id)。
    • 正则解析 \[(\d+)\](含连续 [1][3])→ 映射 sources[n-1];越界编号丢弃并计入 invalidRefs(替代旧反思的编号校验)。
    • 产出 CitationResult{ usedSources:List<{index,id,name}>, perOccurrence:List<{charStart,charEnd,index}>, coverage:double, invalidRefs:int }
    • coverage = 含 ≥1 个有效 [n] 的句子数 / 实质陈述句数(按句末标点切句,过滤问候/过短句;阈值可配)。
  • agent/DocMindAgent.java#stageGenerateAndPersist:流式结束后调用 CitationParser,将 citations(结构化)+ coverage 放入 done payload;confidenceScore = clamp(coverage) × rerankTop1confidenceBand 由分数映射(复用现有 bandToChinese / band 阈值)。
  • Prompt 模板 resources/prompts/knowledge_qa.txt(及 low_confidence):[n] 强制标注指令已存在(line 30),无需改;可补一句「每个事实句都要带 [n]」以提高覆盖率。
  • 前端 DocMind-frontend/src/views/chat/ChatView.vue
    • renderMd() 后置处理:把渲染后 HTML 里的 [n] 包成 <span class="citation-ref" data-index="n">[n]</span>
    • 点击 .citation-ref → 滚动/高亮对应来源卡(msg.sources[n-1],来源卡区已存在 ~274-304 行)。
    • done 事件读取 citations/confidenceScore(已有字段,无需新结构)。

复用SourcePayloadFactory、来源卡组件、done payload 既有 confidenceScore/confidenceBand

风险:qwen-plus 偶发漏标 [n] → 覆盖率偏低误判 → 用可配阈值 + 仅对「事实句」计覆盖;解析失败降级为「覆盖率=null,confidence 退回 rerankTop1」。


Stage B — Agentic 检索循环(P1-1,本次核心)#

新增 agent/supervisor/AgenticSearchOrchestrator.java,用手动控环的 ChatClient 工具调用替换三分支。

核心调用形态(手动循环,已验证 API):

工具层:直接复用现有 MCP @Tool bean(已强制 kbIds、已写 AgentToolContext、已有好描述)。只暴露 3 个读工具searchDocs 向量召回 / webSearch / recall_memory),用 toolNames 白名单硬排除 store_memory(防 LLM 误写记忆,CLAUDE.md 约束)和 kb_meta。BM25 关键词精度由 finalize 期补偿(见下),暂不暴露 keywordSearch

System prompt 要点:说明三工具用途;策略=先 searchDocs、多焦点分别检索、多跳先查中间事实再构下一跳、KB 不足才 webSearch、够了立刻停且不重复检索;约束最多 N 轮、不负责作答、不调写工具。seed = rewrittenQuery + 原问题 + 已选 KB 名(复用 QueryUnderstandingService 的 kb-brief 渲染);不传 kbIds(工具强制)。

finalize(累积 chunks → SupervisorResult)

  1. RetrievedChunk.copyAll 防御拷贝 → 按 dedupeKey() 去重。
  2. 对并集做一次 Cross-Encoder rerank(跨多次工具调用、KB vs Web 分数不可比,需统一标尺——同 CrossEncoderReranker 设计意图)→ MMRDiversifier.diversifyreranker.compress(maxTokens)
  3. RetrievalGrader.gradeGradeResult(契约需要)。
  4. memoryContext 合并;needsFallback = compressed.isEmpty()(四刀落地时为 || grade.tier==LOW#28 收紧为仅”无任何证据”——CRAG 判 LOW 但有证据改走 assembleLowConfidence 带证据作答,不再丢弃 KB/Web 证据)。
  5. retrievalLoggetCalledTools() + 计数构建(retrievalMode="agentic_loop";#28 起补 kbChunks/webChunks/cragGrade 来源拆分与评分)。

ToolCallingManager bean:优先注入 Spring AI 自动配置的(classpath 有 spring-ai-autoconfigure-model-tool);缺失时在 config/McpToolsConfig.java 或新 AgenticToolingConfig 声明 DefaultToolCallingManager

SSE/可观测:循环内每轮发新增 agentic 事件 {iteration,maxIterations,toolCalls:[{name,args}]}(替代旧 multihop);finalize 发既有 retrieval/rerank 事件(#28 起 retrievalkbChunks/webChunks 来源拆分,前端可直接看出”KB 失明/纯联网”形态);grader 仍由 DocMindAgent.stageGrading 从返回的 GradeResult 发。OTel 包 agentic_search span,finalize 复用 cross_encoder_rerank/crag_grading span 名(Langfuse 看板不变)。

失败兜底agentic.max_iterations(默认4)、agentic.iteration_timeout_ms(20000)、agentic.total_timeout_ms(45000);循环异常/空结果 → 回退一次性检索(复用 slim 后的 oneShotRetrieval);线程中断(SSE 断开)每轮顶检测 break;可选「重复 query 去抖」Set 防小模型重复检索。agentic.enabled(默认true) 总开关。

复用:现有 DocSearchTool/WebSearchTool/MemoryToolCrossEncoderRerankerMMRDiversifierRetrievalGraderRetrievedChunk.copyAll/dedupeKeyAgentToolContextSupervisorResultrunSupervisorRetrieval 包装(把其 Supplier 指向新 orchestrator)。


Stage C — 完全移除自反思(P1-2,依赖 A)#

文件与改动:

  • agent/DocMindAgent.java#stageGenerateAndPersist:删除 runSelfReflection 调用与 self_reflection span;生成结束直接走 Stage A 的 CitationParser 算 confidence。删 runSelfReflectionreflectionSkipReasonreadReflectionConfidence/Band(confidence 改由覆盖率给)、topicMismatch→fallback 分支。
  • 删类 agent/SelfReflection.java 及其测试(denial 正则、跑题护栏、重写、4 维打分全去)。
  • SSE:移除 reflection_start/reflection_token/reflection_done 事件;前端 ChatView.vue 删对应监听与 reflection 步骤渲染。
  • 跑题/低质兜底改由:coverage≈0 且 CRAG=LOW → needsFallback=true(既有 fallback 模板 assembleFallback 照常触发)。[#28 已修正] 此设计被证明会丢弃证据:CRAG 低分但有证据时清空上下文走 0-chunk 兜底,模型退回过时基座记忆产生时效性幻觉。现 coverage≈0 且 CRAG=LOW 不再翻 needsFallback(真·无证据已由 compressed.isEmpty() 覆盖),仅记 ungrounded 观测信号,证据照常带进 assembleLowConfidence
  • 死配置清理:reflection.skip_on_high_gradereflection.skip_thresholdreflection.rewrite_enabledreflection.topic_check_enabledconfig/AiConfigInitializer.java)。
  • 更新记忆/文档:project_supervisor_refactor.md 中「reflection.skip_on_high_grade 默认 true」已过时。

保留:CRAG RetrievalGrader 作为生成前质量闸(不动)。


Stage D — 检索层权限下推(P2-2 部分,独立,可先做)#

文件与改动:

  • kbIds 归属校验(核心洞):在 DocMindChatController.stream(或 DocMindAgent.execute 入口)解析 kbIds 后,用 KbKnowledgeBaseMapperid IN (kbIds) AND user_id = currentUserId AND deleted=0剔除/拒绝不属于当前用户的 kbId(越权返回 PARAM_ERROR 或静默过滤)。可抽 KbAccessGuard.filterOwned(userId, kbIds) 复用。
  • MCP 端点鉴权config/SecurityConfig.java 给 spring-ai MCP server 路径(spring-ai-starter-mcp-server-webmvc 暴露路径,确认实际 path)加 JWT/API-Key 过滤,禁止匿名访问 6 个工具端点。
  • userId 已是 auth 派生(getCurrentUserId),内部工具经 AgentToolContext 拿 userId,确认 recall/store memory 的 userId 不再接受外部传值覆盖(agentic 循环里 recall_memory 的 userId 由 context 注入,不用 LLM 传参)。

复用userService.getCurrentUserId()KbKnowledgeBaseMapper、既有会话归属校验范式。


Stage E — 分类器瘦身(P1-3,依赖 B)#

agentic 循环在运行时自决拆解/多跳/web,分类器不再需要这些路由字段。

文件与改动:

  • service/rag/QueryClassification.java:删 needDecomposemultiHop 字段。
  • service/rag/QueryUnderstandingService.java:删 Phase 1b decompose()/parseDecomposition()applyDeterministicSignals 的拆解强制逻辑、STRONG_DECOMPOSE_PATTERNMIN/MAX_SUB_QUERIES;KB-brief 保留(rewrite 指代消解要用)。
  • resources/prompts/query_understanding.txt:删 needDecompose/multiHop 字段与说明。
  • 删拆解链路(grep 确认仅 orchestrator 用后):DecompositionResultSubQuerySubQueryMergerSubQueryRetrievalResultHopAnswerExtractor + resources/prompts/query_decompose.txthop_answer_extract.txt
  • agent/DocMindAgent.java#routePath:简化为 directRead→SELECTED_DOC(范围内);否则 SIMPLE && !isAmbiguous → one-shotelse → agentic
  • agent/PathDecision.javaModeMULTI_HOP/DECOMPOSED,加 AGENTIC(保留 SELECTED_DOC/RULE_PLANNER)。
  • 保留字段:rewritten/intent/complexity/specificity/timeAware/memoryAware/isAmbiguous/scope/scopeConfidence/memoryWriteHints(仍供 one-shot 的 RetrievalPlanner 选工具 + scope 路由 + 记忆写入)。

集中删除清单(Stage B+C+E 落地后)#

  • SupervisorAgent.javahandleDecomposedRetrievalorchestrateMultiHop/handleSequentialMultiHopretrieveOneSubQuerySubQueryRundowngradeIfDecomposedCoverageWeakbuildMultiHopLognormalizeForDeduphandleStandardRetrieval 内联 CRAG-web 回溯块;字段 subQueryMerger/hopAnswerExtractor保留并改名 handleStandardRetrievaloneShotRetrieval(去 web 回溯,留 dispatch+fuse+rerank+compress+grade)及其辅助(dispatchWorkers/fuseRerankCompress/rerankMmrCompress/safeGrade/withWorkerTimeout)。
  • SelfReflection.java(整类)+ 测试。
  • HopAnswerExtractor.java + SubQuery*/DecompositionResult/SubQueryMerger + 相关 prompts + 测试。
  • PromptAssembler.assembleDecomposed + knowledge_qa_decomposed.txt(agentic 产物为扁平 chunk,无 decomposed 路径)。

新增配置键(sys_ai_config,AiConfigInitializer#

agentic.enabled(true)、agentic.max_iterations(4)、agentic.iteration_timeout_ms(20000)、agentic.total_timeout_ms(45000)、agentic.search_top_k(8)、agentic.web_max_results(5)、citation.coverage_min_sentence_len(可选)、citation.confidence_uses_coverage(true)。均经 aiConfigHolder 带默认值读取(同现有范式)。


实施顺序与依赖#

  1. Stage D(权限) — 独立、低风险、安全收益快,先落。
  2. Stage A(citations) — 独立;为 C 提供覆盖率信号。
  3. Stage C(删反思) — 依赖 A 的 confidence。
  4. Stage B(agentic 循环) — 核心;先 agentic.enabled 灰度,与一次性路径并存。
  5. Stage E(分类器瘦身 + 删拆解链路) — 依赖 B 稳定后再删旧分支驱动字段。

验证#

  • 单测(mock ChatModel):B 的环——返回「工具调用→无工具」断言执行一轮 + AgentToolContext 清理;返回 5 轮断言在 max_iterations 截停;异常断言回退 one-shot 且 SupervisorResult 非空。
  • 工具白名单:断言 store_memory/kb_meta 不在 toolNames;跑一次循环断言 MemoryStore.save 从未被调用。
  • kbIds 越权:context kbIds=[1],mock 模型传 searchDocs(kbIds=[999]),断言底层用 [1](context 覆盖);Stage D:传他人 kbId 断言被过滤/拒绝。
  • finalize 排序:构造 向量分 0.4 + web 原始分 1.0,断言并集 rerank 按 Cross-Encoder 重排而非原始分;compress 不超 rag.context_max_tokens
  • citations:构造含 [1][3] 与越界 [9] 的答案,断言 usedSources 正确、invalidRefs 计数、coverage 合理;前端断言 [n] 渲染为可点击并定位来源卡。
  • 契约:返回的 SupervisorResult 有非空 gradeResult/compressedChunksstagePromptAssembly 正常消费。
  • SSE 顺序:2 轮运行断言 agentic*retrievalrerankgraderstarttoken*done(无 reflection_*)。
  • 端到端冒烟(真 qwen-plus):对比题「对比A和B」→ ≥2 次 searchDocs 后停;多跳「X的主教练在球员时代拿过几次世界杯」→ 串行 searchDocs;简单事实题 → one-shot,断言 orchestrator 的 getChatModel().call 未被调用。
  • 构建/回归:mvn clean compilemvn test、前端 npm run type-check

关键文件索引#

  • 新增:agent/supervisor/AgenticSearchOrchestrator.javaservice/rag/CitationParser.java、(可选)config/AgenticToolingConfig.javasupport/KbAccessGuard.java
  • 改:agent/DocMindAgent.java(routePath/stageRetrieval/stageGenerateAndPersist)、agent/supervisor/SupervisorAgent.java(删分支+slim oneShot)、agent/PathDecision.javaservice/rag/QueryUnderstandingService.javaservice/rag/QueryClassification.javaservice/rag/SourcePayloadFactory.javaconfig/SecurityConfig.javaconfig/AiConfigInitializer.javacontroller/DocMindChatController.javaresources/prompts/query_understanding.txt、前端 views/chat/ChatView.vue
  • 删:agent/SelfReflection.javaservice/rag/HopAnswerExtractor.javaservice/rag/SubQueryMerger.java/SubQuery.java/SubQueryRetrievalResult.java/DecompositionResult.javaresources/prompts/query_decompose.txt/hop_answer_extract.txt/knowledge_qa_decomposed.txt 及相关测试

实施状态(2026-06-07 落地)#

四刀已实施 A / C / B / ED(权限下推)按用户要求暂缓(单独评估,详见下方)。全程 mvn test 126 个单测通过、前端 npm run type-check + 生产构建通过。

状态关键落地
A 结构化引用✅ 已实施新增 CitationParser(纯解析,无 LLM):[n]→结构化 citations + 越界计入 invalidRefs + 覆盖率;SourcePayloadFactoryid;置信度 = clamp(coverage) × rerank-top1(解析失败退回 rerank-top1);前端 [n] 渲染为可点击 .citation-ref,点击滚动高亮来源卡。测试 CitationParserTest(6)
C 删自反思✅ 已实施SelfReflection 整类 + AgentState 反思字段 + reflection.* 死配置 + SSE reflection_* 事件 + 前端监听;跑题/低质兜底改由结构化引用覆盖率接管(后 #28 收紧:CRAG 低分但有证据不再兜底、走 assembleLowConfidence 带证据作答;零覆盖仅记 ungrounded 信号)。eval 压测哈帺整包删除(用户拍板,含 PipelineRunner/EvalRunner/PipelineVariant 等)
B agentic 循环✅ 已实施新增 AgenticSearchOrchestrator:Spring AI ToolCallingManager 手动控环(internalToolExecutionEnabled(false)agentic.max_iterations 默认 4);白名单过滤 toolCallbacks 只留 searchDocs/webSearch/recall_memory,硬排除 store_memory/kb_meta;finalize 对并集 rerank→MMR→压缩→CRAG;异常/空结果回退 oneShotRetrievalagentic.enabled 灰度。新增 AgenticToolingConfig@ConditionalOnMissingBean 兜底 ToolCallingManager)。测试 AgenticSearchOrchestratorTest(3:循环终止 / max_iters 截停 / 工具白名单)
E 分类器瘦身✅ 已实施QueryClassificationneedDecompose/multiHop(12→10 字段);QueryUnderstandingService 删 Phase 1b 拆解 + applyDeterministicSignals 改为只升 complexity;PathDecision.Mode 改为 SELECTED_DOC/RULE_PLANNER/AGENTICroutePath 3 路;删 DecompositionResult/SubQuery/SubQueryMerger/SubQueryRetrievalResult/HopAnswerExtractor + 3 个 prompt;SupervisorAgent 删拆解/多跳/CRAG-web 分支,handleStandardRetrievaloneShotRetrievalrag.decompose.* 配置清理
D 权限下推🔶 部分落地21-权限系统对齐报告 拍板:功能权限走 RBAC、数据权限按「KB 级 + 部门 + 公开/私有」,存量一律 PUBLIC(不破坏全员可读)。数据权限已落地sys_dept + sys_user.dept_id + kb_knowledge_base.dept_id/visibility;新增 KbAccessGuard(数据权限唯一收口);DocMindChatController.stream 进 agent 前 filterAccessible 收敛 kbIds(空≠全库);listKnowledge 加部门过滤。关键洞察:KB 级粒度下只需收敛 kbIds,检索层/AgentToolContext 零改动即实现 security trimming。仍待办(P0,独立):RBAC 四表接线(UserDetailsServiceImpl 仍只读单 role)、/mcp/** 端点鉴权、Memory userId 收口

新路径决策(routePath):directRead 命中 → SELECTED_DOC;SIMPLE 且非歧义 → RULE_PLANNER(一次性检索);其余(复杂/中等/歧义/多焦点)→ AGENTIC。

SSE 协议变化:新增 agentic(每轮工具调用);移除 reflection_start/reflection_token/reflection_donemultihopplandonecitations/citationCoverage/invalidRefs