主题 7 · 架构收敛——“加了又删”的工程判断力#
定位:从朴素 RAG 到三条手写分支再到一个 agentic 循环的全过程,展示”能加是能力、知道何时该删是判断力”。
一、通用知识#
1.1 核心概念与原理#
Workflow vs Agent(Anthropic 定义)
Anthropic 在 “Building Effective Agents”(2024)中严格区分两类系统:
- Workflow:预定义代码路径编排 LLM 和工具,可预测、可调试、可回滚。开发者显式写分支——哪条路走哪个工具,执行顺序都是固定的。
- Agent:模型自主驱动工具调用,灵活但不可预测。模型自己决定”拆不拆""跳不跳""搜什么""何时停”。
面试怎么讲:“Workflow 像流程图——开发者画好每个菱形分支;Agent 像给模型一把工具箱——它自己决定掏哪个。选哪个不是技术信仰,而是看你的场景对’可预测性’和’灵活性’哪个要求更高。”
五种可组合模式
Anthropic 提出 5 种从简到繁的构建块,不是互斥的,而是可以组合叠加:
| 模式 | 机制 | 适用场景 | 面试口述要点 |
|---|---|---|---|
| Prompt Chaining | 串行 LLM 调用,前一步输出是后一步输入 | 任务可分解为固定序列 | ”最简单的编排,调试方便” |
| Routing | 分类后路由到不同子流程 | 输入类型差异大,需不同处理 | ”一个 if-else,但判定交给 LLM” |
| Parallelization | 多路并行执行后合并 | 多视角投票、分段处理 | ”fan-out + aggregation” |
| Orchestrator-Workers | 中心调度器分派任务给 Worker | 子任务动态、不可预先枚举 | ”接近 Agent 但调度逻辑是代码” |
| Evaluator-Optimizer | 生成后评估、不达标重来 | 有明确可量化评测标准 | ”性价比最低——只在评测标准清晰且一轮不够时用” |
面试怎么讲:“Anthropic 的核心观点是从增强型 LLM 起步,只在需要时加复杂度。五种模式不是阶梯要一级级爬,而是工具箱——需要哪个拿哪个。框架的层层抽象会掩盖底层 prompt、增加调试难度、诱导过度设计。”
“简单可组合 > 复杂框架”原则
这不是空洞口号,而是有具体含义:
- 正例:Claude Code 的 agentic search 不用任何 RAG 框架,直接用 grep/view 让模型自己找上下文——工具足够简单,模型就能自驱。
- 反例:LangGraph/CrewAI 等框架把”节点""边""状态机”包了一层又一层,开发者写的是图定义而不是 prompt——出了 bug 你不知道是你的 prompt 错了还是框架的状态转移错了。调试成本远高于直接写 API 调用。
- 工程含义:如果你的系统是 Workflow(路径可预测),直接写代码比用框架更清晰。如果你的系统是 Agent(模型自驱),框架也帮不了你——因为核心挑战在 prompt 和工具设计,不在状态机。
面试怎么讲:“这个原则说白了就是——你需要的是 LLM + 好工具 + 好 prompt,不是一个’Agent 框架’。框架解决的是发现问题之前的问题,真正的难题——prompt 调不准、工具描述不清、上下文太长——框架一个都帮不了你。“
1.2 业界主流方案对比(表格形式)#
Agentic RAG 设计模式对比#
| 模式 | 核心论文 | 机制 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|---|
| ReAct | Yao et al. 2022 | Thought → Action → Observation 循环 | 简单直观,工程实现容易 | 推理深度有限,长链容易跑偏 | 1-3 跳的简单查询 |
| Plan-and-Execute | Wang et al. 2023 | 先生成完整计划再逐步执行 | 适合可分解任务,并行潜力大 | 计划可能过时(执行中发现新信息),修改计划成本高 | 明确可拆解的多子任务 |
| Reflexion | Shinn et al. 2023 | 自我反思 + 记忆,失败后”复盘”再试 | 增加推理深度,可学习历史错误 | 成本高(每次反思是一次 LLM 调用),反思质量取决于模型能力 | 需要高准确率的任务 |
| LATS | Zhou et al. 2023 | 树搜索 + 反思,探索多条路径后选最优 | 最强推理能力 | 最贵(指数级调用),延迟最高 | 复杂推理 / 数学证明 |
| LLM Compiler | Kim et al. 2023 | 依赖分析 + 并行函数调用编排 | 并行度高,延迟低 | 需要 LLM 准确判断依赖关系 | 多工具并行场景 |
| Tool-Calling Loop(实用派) | Anthropic 2024 / Claude Code | 给模型工具 + 上限,模型自驱循环 | 最简实现,无框架依赖 | 依赖模型工具调用能力 | 实际生产系统的主流选择 |
面试怎么讲:“论文里方案很多,但生产上主流就是最后一行——给工具、设上限、模型自己循环。因为其他方案的额外复杂度(计划生成器、反思模块、树搜索)在实际系统中很难稳定工作,收益不如把 prompt 和工具描述写好。“
1.3 关键论文与技术要点#
| 论文/来源 | 核心贡献 | 面试记忆点 |
|---|---|---|
| ReAct (Yao et al., 2022, ICLR 2023) | 统一推理和行动——模型先”想”再”做”,交替进行 | ”所有 agentic 模式的基石——Thought-Action-Observation 循环” |
| Anthropic “Building Effective Agents” (2024) | 5 种可组合模式 + “简单 > 框架”原则 | ”不是论文但比论文影响力更大——直接定义了工业界怎么做 Agent” |
| Self-RAG (Asai et al., 2023) | 检索时机自适应 + 生成时自我评估 | ”开创了’不是每个问题都需要检索’的思路” |
| CRAG (Yan et al., 2024) | 检索质量分档(Correct/Ambiguous/Wrong) + 按档处理 | ”比简单的 top-k 截断精细——让系统对’搜到但不确定’有处理策略” |
| Agentic RAG Survey (arXiv 2501.09136) | 系统梳理 Single/Multi-agent RAG 架构 | ”综述级参考——把所有模式放在统一分类框架下” |
工具设计三原则(生产经验):
- 最小权限:只给读工具,写操作走代码路径。模型调
searchDocs没问题,调store_memory就可能乱写——所以白名单硬排除。 - 约束循环:
max_iterations设上限。没上限的模型循环是生产事故——模型可能无限换近义词重搜。 - 失败兜底:agentic 失败必须有 fallback。模型不可靠,但系统必须可靠——异常/空结果时回退到确定性路径。
面试怎么讲:“这三条不是理论,是被线上 bug 教出来的。不设上限→模型跑满预算;不限权限→模型误写记忆;不设兜底→一个 timeout 整条请求挂。“
1.4 常见面试问答#
Q1: ReAct 和 Plan-and-Execute 怎么选?
A: 看任务是否可预先分解。如果子任务在执行前就能枚举清楚(比如”对比 A、B、C 三个方案”),Plan-and-Execute 可以并行执行效率更高。如果子任务依赖中间结果(比如”查到 X 的负责人之后再查他的入职时间”),Plan-and-Execute 的计划会过时,ReAct 式的”边做边决定”更合适。但实际生产中,两者的区别在收窄——现代 LLM 的工具调用能力已经足够让一个简单的 tool-calling loop 同时覆盖这两种场景,不需要显式写”计划生成器”。
Q2: Agent 容易过度设计,怎么避免?
A: 两条检验标准:(1) 你的 Workflow 分支是否在重复实现同一个能力——如果是,说明该合并成 Agent 让模型自驱;(2) Agent 比 Workflow 的成本和延迟增加是否被质量提升覆盖——简单题用 Agent 是浪费,只有复杂/多跳/对比题才值得多轮工具调用。所以成熟系统一定有”路由”:简单走快路径,复杂走 Agent。
Q3: 为什么不用 LangGraph / CrewAI 这类框架?
A: 三个原因:(1) 框架解决的问题我不需要——我的 Agent 就是一个 tool-calling loop,不需要状态机/图定义/角色分配;(2) 框架增加的调试成本我承受不起——出问题时我需要看到完整的 prompt 和 tool call,而不是框架的状态转移日志;(3) Anthropic 自己说了——“框架层层抽象掩盖底层 prompt、增加调试难度、诱导过度设计”。不是框架不好,而是我的场景不需要。
Q4: Agentic 循环怎么防止无限循环?
A: 多层防线:(1) 硬上限 max_iterations(我用 4)——到了就停,不管模型想不想继续;(2) 空转护栏——本轮工具调用没带来任何新 chunk 但已有累积资料→提前收尾(防模型换近义词重复检索同一来源);(3) System prompt 约束——告诉模型”用尽量少的轮数""搜空即止""不要用近义改写重复检索同一来源”;(4) 不暴露预算——不告诉模型 max=4,否则它会锚定到用满 4 轮。但最后一条是软约束,所以前三条是硬兜底。
Q5: Workflow 和 Agent 什么时候用哪个?
A: Workflow 用于路径可预测的场景——分类、路由、管道式处理。Agent 用于路径不可预测的场景——复杂检索、开放式任务、需要模型”边做边判断”的场景。很多系统是混合的:外层是 Workflow(分类→路由→生成),中间检索环节可以是 Agent(模型自驱拆/跳/补/停)。关键判断标准:如果你发现自己在 Workflow 里写越来越多的 if-else 来覆盖模型应该自己判断的事情,那就该换成 Agent 了。
Q6: 怎么评估 Agent 的效果?
A: 三个维度:(1) 任务完成质量——答案正确率、引用准确率,用 golden set 评测;(2) 效率——平均迭代次数、每问成本、P95 延迟——Agent 比 Workflow 贵是预期,但不能贵 10 倍只好 5%;(3) 可观测性——每轮工具调用都要有 trace,否则出问题你不知道是模型决策错了还是工具返回错了。我的做法是每轮 agentic 循环发一个 SSE 事件 + 一个 OTel span,前端和 Langfuse 都能看到完整链路。
Q7: Self-Reflection(自反思)有没有价值?什么时候该用?
A: 有价值,但成本远高于收益的场景很多。Anthropic 把它归类为 Evaluator-Optimizer 模式,明确说”只在有明确可量化评测标准且迭代有可量化收益时用”。在 RAG 场景里,反思的核心目的是”检查模型有没有引对来源、有没有编造”——但如果你有结构化引用(每句话必须带 [n] 指向来源),无引用自然就是低置信,天然实现了反思的核心功能,而且零 LLM 成本。所以该不该用取决于你有没有更便宜的替代方案来实现同一个目标。
二、DocMind 实践#
30 秒口述版#
DocMind 的检索架构经历了三次演进:先用朴素 RAG 发现对比题和多跳题搞不定,于是手写了三条分支——查询拆解解对比、串行多跳解链式、Self-Reflection 解幻觉。每条单独评测都有效,但三条开始互相重叠、分类器一判错全链路错、SupervisorAgent 膨胀到 1683 行。调研 Anthropic “Building Effective Agents” 后发现三条分支本质上都在重复实现”按需再检索”,于是大刀阔斧删掉三条,收敛为 1 个 agentic 工具调用循环——模型自驱拆/跳/补/停,SupervisorAgent 瘦身到 579 行,Self-Reflection 用 CitationParser 零 LLM 替代。能加是能力,知道何时该删是判断力。
详细展开#
背景/痛点(Situation)#
DocMind 的检索管线从朴素 RAG 起步后,为了解决评测暴露的三类问题,逐步加了三条手写分支:
| 问题 | 解法 | 实现 |
|---|---|---|
| 对比题/多焦点题(“对比 A 和 B”)——单 query 只能召回一侧 | Query Decomposition 并行 fan-out | DecompositionResult + SubQuery + SubQueryMerger,分类器判 needDecompose=true 后触发 |
| 多跳题(“X 的主教练在球员时代拿过几次世界杯”)——需要中间事实 | 串行多跳 Self-Ask 式循环 | HopAnswerExtractor + SupervisorAgent.handleSequentialMultiHop,分类器判 multiHop=true 后触发 |
| 幻觉/引用偏差——模型引错来源编号 | Self-Reflection 四维打分 + 跑题正则 + 否定矛盾正则 + 条件重写 | SelfReflection 类,生成后额外 1-2 次 LLM 调用 |
每条单独看都有效。但三条加在一起后出现了严重的系统性问题:
-
分支重叠:拆解、多跳、CRAG-web 回溯本质都是”按需再检索”——三套控制流在重复实现同一个能力,但各写各的 merge/dedup/rerank,维护成本指数增长。
-
分类器成为单点故障:12 字段巨型分类器(含
needDecompose/multiHop/isAmbiguous等)做路由决策,一判错全链路错——本该走多跳的题被路由到拆解,结果拆出来的子问题互相不依赖,中间事实丢失。 -
代码膨胀:
SupervisorAgent膨胀到 ~1683 行(含handleDecomposedRetrieval/orchestrateMultiHop/handleSequentialMultiHop/ CRAG-web 回溯块),每条分支都有自己的日志构建、SSE 事件、异常处理——改一处要看三处会不会冲突。 -
Self-Reflection 性价比低:生成后额外 1-2 次 LLM 调用做四维打分,但跑题正则(
topicMismatch)误报率高,否定矛盾正则(detectDenialContradiction)补丁叠补丁,条件重写的触发条件越调越复杂——本质上在用一个不稳定的模块来修另一个不稳定的模块。
做了什么(Action)#
调研阶段:系统对标 Anthropic “Building Effective Agents” / Glean / Claude agentic search / RAGFlow / Azure agentic retrieval(详见 interview-docs/19-对标主流产品改造报告),核心发现:
- 三条分支是典型的”accreted workflow”——很多分支是同一能力的重复手写,该合并。
- 主流已转向”搜索即工具”:把检索原语变成薄工具,由一个聪明模型统一编排,而不是 reranker/分类器/融合器各自为政。
- Self-Reflection 在有结构化引用时完全可替代——“无引用即未 grounding”天然成为抗幻觉机制。
四刀改造(对应 interview-docs/20-改造实现设计 的 Stage A/B/C/E):
刀 A:结构化引用替代 Self-Reflection 的置信度来源
- 新增
CitationParser(纯 Java,零 LLM):解析答案中的[n]→ 结构化 citations,计算句子级 coverage = 含有效引用的实质句数 / 实质句总数。 confidenceScore = clamp(coverage) * rerank-top1,coverage 不可算时退回 rerank-top1。- 越界编号(
[n]中 n > 来源数)丢弃并计入invalidRefs——取代旧反思的编号校验,零 LLM 成本。
刀 B:三条手写分支 → 1 个 agentic 工具调用循环
- 新增
AgenticSearchOrchestrator(354 行):Spring AIToolCallingManager手动控环(internalToolExecutionEnabled(false)关闭 Spring AI 无上限的内部循环),agentic.max_iterations默认 4。 - 工具白名单硬排除:暴露 4 个读工具
searchDocs/keywordSearch/webSearch/recall_memory+ executeCode 沙箱计算工具(sandbox.enabled默认关),过滤toolCallbacks列表排除store_memory/kb_meta(Spring AItoolNames是追加语义不是过滤语义,不能用 toolNames 排除)。 - 模型自驱:拆解、多跳、web 补充、何时停——全部由模型运行时决定,不再有手写分支。
finalize():累积所有工具调用的 chunks → 防御拷贝 +dedupeKey()去重 → 并集做一次 Cross-Encoder rerank(KB 向量分 vs Web 原始分不可比,需统一标尺)→ MMR → 压缩 → CRAG 评分。- 空转护栏:本轮工具调用零增益且已有累积资料→提前收尾(防模型换近义词空转)。
- System prompt 不暴露轮数上限——暴露会让模型锚定到用满预算。
刀 C:完全删除 Self-Reflection
- 删
SelfReflection整类 + 反思字段 +reflection.*死配置 + SSEreflection_*事件。 - 跑题/低质兜底改由 citations 覆盖率 + CRAG 评分组合接管。
needsFallback仅在compressed.isEmpty()(真正无证据)时置位;CRAG LOW 但有证据 → 走assembleLowConfidence带证据作答,不丢弃证据(被 trace 验证的关键修正——见 #28)。
刀 E:分类器瘦身 + 删拆解链路
QueryClassification删needDecompose/multiHop(12 → 10 字段),拆解/多跳是否发生由 agentic 循环运行时自决。applyDeterministicSignals只做 complexity 升级:多焦点信号(对比/分别/vs/多问号)把 SIMPLE 升到 MEDIUM,从而路由进 agentic 循环。PathDecision.Mode从 5 模式(含 MULTI_HOP/DECOMPOSED)收敛为 3 模式:SELECTED_DOC / RULE_PLANNER / AGENTIC。- 删
DecompositionResult/SubQuery/SubQueryMerger/SubQueryRetrievalResult/HopAnswerExtractor+ 3 个 prompt 模板。
关键约束——不是无约束放飞:
- 白名单 4 读工具 + executeCode 沙箱计算工具(默认关,最小权限)
max_iterations=4(约束循环)- 空转护栏 + 不暴露预算(防空转)
- 异常/空结果回退一次性检索(失败兜底)
agentic.enabled灰度总开关
量化结果(Result)#
SupervisorAgent从 ~1683 行瘦身到 579 行(-65%),删除拆解/多跳/CRAG-web 三条分支及全部辅助类。PathDecision.Mode从 5 种收敛到 3 种,路由逻辑从多层嵌套 if-else 简化为三行判断。- Self-Reflection 删除后,每次问答少 1-2 次 LLM 调用(反思打分 + 条件重写),常规问答延迟降低。
QueryClassification删 2 个字段,QueryUnderstandingService删 Phase 1b 拆解逻辑 +STRONG_DECOMPOSE_PATTERN等正则。- 引用覆盖率 + CRAG 评分的组合取代四维反思打分,置信度计算从”需要 LLM”变为”纯 Java 解析”——
CitationParser151 行、零 LLM 调用。 - 全程
mvn test126 单测通过 + 前端 type-check / 生产构建通过 + 真 qwen-plus 端到端冒烟通过。
代码锚点#
| 类 / 方法 | 路径 | 职责 |
|---|---|---|
AgenticSearchOrchestrator | agent/supervisor/AgenticSearchOrchestrator.java | 收敛产物:agentic 工具调用循环(354 行),手动控环 + 白名单 + finalize 统一收尾 |
AgenticSearchOrchestrator.search() | 同上 L100 | 循环入口:逐轮 call → 检查 hasToolCalls → executeToolCalls → 空转护栏 → finalize |
AgenticSearchOrchestrator.whitelistedToolCallbacks() | 同上 L173 | 工具白名单过滤:从全部工具 bean 生成 ToolCallback 后只留 ALLOWED_TOOLS(4 读 + executeCode 共 5 个),硬排除 store_memory/kb_meta |
AgenticSearchOrchestrator.finalize() | 同上 L182 | 统一收尾:防御拷贝 → 去重 → Cross-Encoder rerank → MMR → 父块展开(#30)→ query-aware 压缩 → CRAG 评分(+ CRAG LOW 单轮 Web 补偿) |
SupervisorAgent.oneShotRetrieval() | agent/supervisor/SupervisorAgent.java L75 | 一次性检索(兜底路径):并行 Worker 派发 → RRF → rerank → MMR → 压缩 → CRAG |
CitationParser.parse() | service/rag/CitationParser.java L57 | 纯 Java 解析 [n] → 结构化 citations + 覆盖率,替代 Self-Reflection 置信度 |
PathDecision | agent/PathDecision.java | 3 模式路由:SELECTED_DOC / RULE_PLANNER / AGENTIC |
DocMindAgent.routePath() | agent/DocMindAgent.java L1117 | 路径决策:directRead→SELECTED_DOC;SIMPLE && !ambiguous→RULE_PLANNER;其余→AGENTIC |
DocMindAgent.agenticWithFallback() | agent/DocMindAgent.java L841 | agentic 异常/空结果 → 回退 oneShotRetrieval |
DocMindAgent.stageRetrieval() | agent/DocMindAgent.java L780 | 检索阶段入口:按 pathDecision.mode() 分派到直读/agentic/一次性 |
PromptAssembler.assembleLowConfidence() | service/rag/PromptAssembler.java L129 | CRAG LOW 但有证据时的模板——带证据作答,不丢弃 |
QueryUnderstandingService.applyDeterministicSignals() | service/rag/QueryUnderstandingService.java | 瘦身后只做 complexity 升级(多焦点→MEDIUM),不再强制 needDecompose |
量化数据#
| 指标 | 数值 | 基线 | 来源 |
|---|---|---|---|
SupervisorAgent 代码行数 | 579 行 | ~1683 行(改造前) | wc -l SupervisorAgent.java |
PathDecision.Mode 种类 | 3 种 | 5 种(含 MULTI_HOP/DECOMPOSED) | PathDecision.java 源码 |
QueryClassification 字段数 | 10 字段 | 12 字段(含 needDecompose/multiHop) | 源码 |
| Self-Reflection LLM 调用 | 0 次/问 | 1-2 次/问 | 删除 SelfReflection 整类 |
CitationParser 代码量 | 151 行(纯 Java) | — | wc -l CitationParser.java |
AgenticSearchOrchestrator 代码量 | 354 行 | 三条分支合计数百行 | wc -l AgenticSearchOrchestrator.java |
| 单测通过 | 126 个 | — | mvn test |
| agentic 默认最大迭代 | 4 轮 | — | agentic.max_iterations 配置 |
三、追问应对#
面试官想听到的信号#
- 工程判断力:“加了又删”不是折腾,而是基于评测数据和调研结论做出的架构决策——能加是能力,知道何时该删是判断力。
- 对标业界不是抄:调研 Anthropic/Glean/Claude 不是照搬它们的方案,而是提取原则(“简单可组合 > 复杂框架""搜索即工具”)后结合自己的约束(qwen-plus 不如 Claude 强、成本敏感、需要灰度)落地。
- 系统性思考:不是只删了代码,而是重新设计了整个路径决策体系——分类器瘦身、路由简化、置信度替换、兜底链路都是同步调整的。
- 安全边界意识:agentic 循环不是无约束放飞——白名单/上限/空转护栏/异常兜底,每一层都有具体实现。
- 可观测性保障:删了复杂度但没删可见性——每轮 agentic 循环都有 SSE 事件 + OTel span,决策从”显式代码分支”变”模型内隐”后用 trace 补偿。
追问预判与应答#
Q1(通用): 你怎么评估删掉三条分支后没有退化?
A: 三个层次验证。第一层:126 个单测全部通过,包括专门为 agentic 循环写的白名单测试、循环终止测试、max_iterations 截停测试。第二层:端到端冒烟用真 qwen-plus 跑三类典型题——简单事实题走 one-shot(确认 orchestrator 未被调用)、对比题模型自主多焦点检索(2+ 次 searchDocs)、多跳题模型自主串行(先查中间事实再构造下一跳)。第三层:needsFallback 语义收紧是被真实 trace 驱动的——#28 发现 CRAG LOW 但有证据时原来会丢弃证据退回基座模型幻觉,改为走 assembleLowConfidence 带证据作答后验证通过。
Q2(通用): Spring AI 的 toolNames 为什么不能用来排除工具?
A: Spring AI 的 toolNames 是追加语义(与 toolCallbacks 取并集),不是过滤语义。也就是说如果你 toolCallbacks 里已经有了 store_memory,即使 toolNames 里不写它,它仍然会被暴露给模型。所以必须在 toolCallbacks 生成阶段就过滤——从三个工具 bean 生成的所有 ToolCallback 里,只保留白名单 ALLOWED_TOOLS 集合内的,store_memory 根本不进入列表。这是读源码发现的,Spring AI 文档没说清楚。
Q3(项目): 为什么 max_iterations 设 4 而不是 8?
A: 经验值,两个考量。第一:实测 qwen-plus 在 4 轮内能覆盖绝大多数场景——简单题 1 轮就停,对比题 2-3 轮,多跳题 3-4 轮;超过 4 轮通常是模型在空转(换近义词重复检索同一来源)。第二:成本——agentic 每轮都是一次 LLM 调用 + 至少一次工具执行,4 轮已经比 one-shot 贵 4-5 倍,继续加轮数边际收益递减但成本线性增长。另外,空转护栏(本轮零增益则提前收尾)使得实际平均迭代次数低于 4。
Q4(项目): 为什么不把 BM25 关键词检索也暴露给 agentic 循环?
A: 简化工具集,降低模型决策复杂度。向量检索(searchDocs)已经是语义匹配,BM25 是精确关键词匹配——在 agentic 循环里让模型自己决定”这个 query 该走向量还是关键词”增加了一个不必要的决策点。one-shot 路径有 RRF 融合向量和 BM25 两路结果,agentic 路径的 finalize 做 Cross-Encoder rerank 也能统一不同来源的分数。如果后续评测发现向量检索在某些精确术语上漏召回,可以考虑加入,但当前三工具已经够用。
Q5(项目): Self-Reflection 删掉后,幻觉怎么防?
A: 两道防线。第一道:CitationParser 解析引用覆盖率——模型每个事实句都需要带 [n] 指向来源,覆盖率低自然暴露”模型在编造”。这和 Claude Citations / Vertex Grounding 的思路一致:“无引用即未 grounding”天然成为抗幻觉机制。第二道:CRAG 评分——检索质量分档,LOW 时走 assembleLowConfidence 模板,开头就告诉用户”相关性偏低,基于有限参考内容”。关键点是零覆盖 + CRAG LOW 的组合(ungrounded 信号)会记到 Langfuse 观测面板(span level WARNING),虽然不翻 needsFallback(因为确实有证据),但运维侧可以监控。
Q6(通用): 你说的”对标业界”具体怎么做的?不是看了篇博客就照搬吧?
A: 系统性调研了 6 个来源:Anthropic “Building Effective Agents”(五种可组合模式 + 原则)、Anthropic Contextual Retrieval(索引增强)、Claude/Vertex Citations(结构化引用)、Glean(企业级检索工程)、RAGFlow(索引期 LLM 富化)、Azure agentic retrieval(LLM 拆子查询并行)。产出了一份 4000 字的对标报告(interview-docs/19),核心结论不是”他们怎么做我就怎么做”,而是提取了两条可执行原则:(1) 三条分支在重复实现”按需再检索”→合并为一个工具调用循环;(2) Self-Reflection 在有结构化引用时完全可替代→删掉换 CitationParser。这两条都是结合 DocMind 自己的约束(qwen-plus 模型能力、成本敏感、需要灰度开关)做的落地决策。
Q7(项目): agentic 循环和 one-shot 路径并存,成本怎么控制?
A: 路由分层。routePath() 三行逻辑:directRead 命中→SELECTED_DOC(零检索成本);SIMPLE && !ambiguous→RULE_PLANNER(一次性检索,最便宜);其余→AGENTIC。简单事实题占大多数查询量,它们走 one-shot 不进 agentic——所以整体成本增加有限。AGENTIC 路径自身也有成本控制:max_iterations=4、空转护栏、temperature=0.0(减少随机探索)。另外 agentic.enabled 是灰度总开关,紧急时可以一键关闭回退全 one-shot。
Q8(项目): 你提到”不暴露预算给模型”,为什么?
A: 实测发现的。早期版本在 seed prompt 里告诉模型”你最多可以调用 4 次工具”,结果模型几乎每次都跑满 4 轮——即使第 1 轮就搜到了足够的资料。这是 LLM 的”锚定效应”——告诉它有预算,它就会花完预算。改为只说”用尽量少的工具调用把资料检索齐全”后,简单题模型 1 轮就停了。但 prompt 约束终究是软约束(模型不一定遵守),所以代码层的 max_iterations 和空转护栏是硬兜底。
反击引导#
讲完架构收敛后,可以主动引到以下强项故事:
- “这次改造过程中还有一个有意思的发现——CRAG LOW 但有证据时不应该丢弃证据(#28),这涉及可信生成和证据保留策略,我可以展开讲。”(→ 05-可信生成与评测)
- “agentic 循环上线后,可观测性从’代码分支可见’变成’模型决策内隐’——我们做了一套 StageEmitter + OTel span 的埋点体系来补偿。”(→ 可观测性相关故事)
- “四刀改造之前,我们做了 Cross-Encoder rerank + MMR + 上下文压缩 的优化,这是检索质量的基础。”(→ 04-Rerank和截断优化)