主题 5 · 可信生成与评测#
定位:RAG 系统如何从检索质量评估、生成约束、结构化引用、到离线评测形成闭环,保证答案”有据可查、质量可测”。
一、通用知识#
1.1 核心概念与原理#
RAG 幻觉分类#
RAG 系统的幻觉分为两大类:
- Intrinsic Hallucination(内在幻觉):模型生成的内容与检索到的来源直接矛盾——搞错数字、日期、实体名称、因果关系等。例如来源写”2024 年发布”,模型却说”2023 年发布”。这类幻觉在企业场景最为危险,因为用户容易被带有具体数据的错误结论误导,且难以肉眼识别(看起来”言之凿凿”)。检测方法主要靠 NLI(自然语言推理)模型对 (来源, 生成) 做蕴含/矛盾分类,或 LLM-as-Judge 逐句核对。
- Extrinsic Hallucination(外在幻觉):模型凭空编造来源中没有覆盖的内容——编造不存在的论文、虚构功能描述、捏造统计数据。虽然不一定”错”,但在 RAG 场景下违背了”基于来源回答”的契约。检测方法主要靠引用覆盖率(答案中哪些句子没有被任何来源支撑)和 Groundedness 评分。
面试怎么讲:先分类再论危害——“Intrinsic 是与来源矛盾的错误,Extrinsic 是来源没覆盖的臆造;企业场景 Intrinsic 更危险,因为看起来有理有据但数据是错的。检测上 Intrinsic 靠 NLI 蕴含判断,Extrinsic 靠覆盖率衡量。“
Grounding(接地)技术#
Grounding 的核心目标是让 LLM 的生成内容可追溯到具体来源,不同厂商方案差异很大:
- Claude Citations:Anthropic 的句子级 span 引用方案。模型在流式输出中返回
citations_delta,每条 citation 包含cited_text(原文 span)和document_index。被引用的源文本不计入 output token,经济性好。粒度到句子级甚至子句级,是目前商用 API 中 grounding 粒度最细的方案。 - Google Vertex Grounding:通过
groundingMetadata返回结构化接地信息,包含groundingChunks(匹配到的来源片段)和groundingSupports(含startIndex/endIndex的文本区间 +confidenceScores)。适合与 Google Search 或企业数据源集成。 - 传统 [n] 标记:在 system prompt 中约束模型用
[n]引用来源编号。优点是兼容任何模型,缺点是纯 prompt 约束不保证覆盖——模型可能遗漏引用或编造越界编号。需要后处理解析和校验。
面试怎么讲:“Grounding 从弱到强分三档:传统 [n] 标记是 prompt 约束不保证覆盖;Google Vertex 返回结构化 groundingSupports 带区间和置信度;Claude Citations 粒度最细到句子级 span 且引用文本不计 output token。我们项目用传统 [n] 方案因为要兼容 DashScope qwen 系列,但加了纯 Java 的后处理解析器补齐校验和覆盖率计算。“
CRAG(Corrective RAG)#
Yan et al. 2024(arXiv:2401.15884)提出的检索后质量评估框架:
- 用一个独立的 evaluator 对检索结果打三档分:Correct(与查询高度相关)/ Ambiguous(部分相关或不确定)/ Incorrect(基本不相关)
- 根据分档触发不同的条件补偿策略:Correct 直接使用检索结果生成;Ambiguous 补 Web 搜索增强;Incorrect 完全替换为 Web 搜索结果
- evaluator 可以是独立的小模型(如 T5-based),也可以复用已有的 reranker 分数——后者零额外调用但粒度粗,前者更准但引入延迟
面试怎么讲:“CRAG 核心是在检索和生成之间插一个质量闸门,三档分流:好的直接用、模糊的补 Web、差的替换为 Web。关键设计决策是 evaluator 用什么——独立小模型准但慢(约 300ms),复用 reranker 分数快(0ms)但只能做粗粒度判断。“
Self-RAG#
Asai et al. 2023(NeurIPS)在词表中加入特殊的 reflection tokens([Retrieve]、[IsRel]、[IsSup]、[IsUse]),让模型在生成过程中自行决定:是否需要检索、检索结果是否相关、生成是否忠于来源。
优点是端到端,模型自驱决策不需要外部编排。局限性是需要对基座模型做微调,且微调后的模型在非 RAG 任务上可能退化,工程落地门槛高。
面试怎么讲:“Self-RAG 的理念很美——让模型自己判断要不要检索、生成是否忠实,靠特殊 token 实现。但需要微调,实用性受限,所以业界实际落地更多用 CRAG 这种外部评估器方案。“
FLARE(Forward-Looking Active Retrieval)#
Jiang et al. 2023 提出的主动检索策略:生成过程中监控 token 级概率,当连续低置信度 token 出现时触发检索。
- 主动检索(如 FLARE):模型边生成边决定何时检索,响应延迟不确定,但避免了不需要检索时的无谓调用
- 被动检索(传统 RAG):检索前置于生成,延迟可预测,但可能检索了用不上的内容
- 权衡:FLARE 适合长文档生成场景(如报告写作),短答案 QA 场景收益有限
同类方案对比#
| 方案 | Corrective RAG | Adaptive-RAG | Self-RAG | REPLUG | FLARE |
|---|---|---|---|---|---|
| 核心机制 | 检索后三档评估+条件补偿 | 路由器决定检索策略(无/单次/多次) | 词表加 reflection tokens 自决 | 检索文档作为前缀拼接,ensemble 概率 | 生成中低置信度触发检索 |
| 是否需微调 | 否 | 否(用 LLM 做路由) | 是(加特殊 token) | 否(但需训练 retriever) | 否 |
| evaluator | 独立模型或 reranker | LLM 分类器 | 模型内置 | 无显式评估 | token 概率 |
| 适用场景 | 通用 QA | 复杂度差异大的混合查询 | 研究/已微调场景 | 开放域 QA | 长文档生成 |
| 工程复杂度 | 低 | 中 | 高 | 中 | 高 |
1.2 业界主流方案对比(表格形式)#
Grounding 技术对比#
| 维度 | Claude Citations | Google Vertex Grounding | 传统 [n] 标记 | DocMind 实现 |
|---|---|---|---|---|
| 粒度 | 句子/子句级 span | 文本区间(startIndex/endIndex) | 句子级(不保证) | 句子级(后处理保证) |
| 实现方式 | API 原生支持 | API groundingMetadata | Prompt 约束 | Prompt 约束 + CitationParser |
| 覆盖率保证 | 模型原生保证 | API 返回 confidenceScores | 无保证 | 后处理计算+observability |
| 越界校验 | API 层校验 | API 层校验 | 无 | invalidRefs 检测 |
| 模型兼容性 | 仅 Claude | 仅 Gemini/PaLM | 任意模型 | 任意 OpenAI-compatible |
| 成本 | 引用文本不计 output token | 标准计费 | 标准计费 | 标准计费 |
CRAG 实现方案对比#
| 维度 | 原论文方案 | 独立小模型评估 | 复用 Reranker 分数 | DocMind 实现 |
|---|---|---|---|---|
| 评估延迟 | 依实现而定 | ~300ms | 0ms(已有分数) | 快路径 0ms / CE 仲裁 ~50ms |
| 分档 | Correct/Ambiguous/Incorrect | 三档 | 二档(高/低)+灰区 | 三档(HIGH/AMBIGUOUS/LOW) |
| 灰区处理 | Web 补偿 | 按置信度分流 | 无灰区概念 | heuristic(分布)/CE(二次打分)/disabled 三模式 |
| Web 补偿 | Incorrect 全替换 | 类似 | 无 | AMBIGUOUS 触发 Web 兜底 |
| 降级策略 | 未说明 | 按模型设计 | 无 | 任意失败 → AMBIGUOUS(保守) |
评测框架对比#
| 框架 | RAGAS | TruLens | DeepEval | Arize Phoenix | DocMind 评测 |
|---|---|---|---|---|---|
| 核心维度 | faithfulness/relevancy/precision/recall | groundedness/relevance/harmfulness | 14+指标全覆盖 | trace+eval 一体化 | 对照梯(V1-V4)+LLM-as-Judge |
| LLM 依赖 | 是(GPT-4 推荐) | 是 | 是 | 可选 | 是(qwen-turbo 降本) |
| 中文支持 | 一般 | 一般 | 较好 | 较好 | 原生中文 |
| 集成方式 | Python SDK | Python SDK | Python SDK | Python SDK + UI | Java 脚本驱动组件对照 |
| 成本 | 每条 ~$0.05 | 每条 ~$0.05 | 每条 ~$0.03 | 免费基础版 | 单次全量 ¥18.4 |
| 离线/在线 | 均可 | 均可 | 离线为主 | 在线为主 | 离线对照 + 在线 Langfuse |
1.3 关键论文与技术要点#
| 论文 | 年份/会议 | 核心贡献 | 面试关键词 |
|---|---|---|---|
| Corrective RAG (Yan et al.) | 2024 / arXiv:2401.15884 | 三档质量评估 + 条件补偿(Web fallback) | “检索后质量闸门” |
| Self-RAG (Asai et al.) | 2023 / NeurIPS | 词表加 reflection tokens,模型自决检索/生成忠实度 | ”端到端但需微调” |
| FLARE (Jiang et al.) | 2023 / arXiv:2305.06983 | 生成中低置信度 token 触发检索 | ”主动检索 vs 被动检索” |
| Adaptive-RAG (Jeong et al.) | 2024 / arXiv:2403.14403 | 查询复杂度分类 → 路由到不同检索策略 | ”复杂度驱动路由” |
| REPLUG (Shi et al.) | 2023 / NAACL | 检索文档作前缀拼接,ensemble 输出概率 | ”黑盒 LLM 适配” |
| RAGAS (Es et al.) | 2023 / arXiv:2309.15217 | RAG 四维评测框架(faithfulness/relevancy/precision/recall) | “标准评测框架” |
| FActScore (Min et al.) | 2023 / EMNLP | 将答案拆成原子事实逐条验证 | ”细粒度幻觉检测” |
1.4 评测指标全景表#
| 指标 | 定义 | 公式/计算方式 | 适用场景 |
|---|---|---|---|
| Recall@K | 前 K 个检索结果中包含相关文档的比例 | |relevant ∩ retrieved@K| / |relevant| | 召回质量评估 |
| MRR (Mean Reciprocal Rank) | 第一个相关结果排名的倒数的均值 | mean(1/rank_first_relevant) | 排序质量(首条命中) |
| NDCG@K | 归一化折扣累积增益 | DCG@K / IDCG@K | 多级相关性排序评估 |
| Faithfulness (忠实度) | 答案中陈述与来源一致的比例 | 忠实陈述数 / 总陈述数 (NLI/LLM-Judge) | 检测 Intrinsic 幻觉 |
| Answer Relevancy | 答案与问题的相关程度 | LLM-Judge 或 embedding 相似度 | 答案质量评估 |
| Context Precision | 检索结果中相关内容排在前面的程度 | 加权 precision@k 的均值 | 检索排序有效性 |
| Context Recall | 回答所需信息在检索结果中的覆盖程度 | golden answer 中可归因到 context 的比例 | 检索召回充分性 |
| Hallucination Rate | 答案中无法归因到来源的陈述比例 | 1 - Groundedness | 幻觉程度监控 |
| Groundedness | 答案中可归因到来源的陈述比例 | 有来源支撑的陈述数 / 总陈述数 | 接地程度评估 |
| Citation Coverage | 含有效引用标注的实质句比例 | cited_sentences / substantive_sentences | 引用完整性(DocMind) |
1.5 离线评测 vs 在线评测#
| 维度 | 离线评测 | 在线评测 |
|---|---|---|
| 时机 | 开发阶段,版本发布前 | 线上运行时持续 |
| 数据 | 预构建测试集(golden answer) | 真实用户查询 |
| 目的 | 组件对照(A/B)、回归检测 | 漂移监控、质量趋势 |
| 指标 | Recall/MRR/Faithfulness/Relevancy | 置信度分布、CRAG 分级比例、覆盖率 |
| 成本 | 高(LLM-as-Judge 费用) | 低(利用已有 trace 数据) |
| 代表工具 | RAGAS / DeepEval / 自建脚本 | Langfuse / Arize Phoenix / Datadog LLM |
| 适用场景 | ”加了 rerank 涨了多少?" | "最近一周幻觉率有没有上升?“ |
1.6 常见面试问答#
Q1: RAG 系统的幻觉怎么解决?
A: 分三层防御。第一层是检索质量把关——用 CRAG 风格的三档评估(HIGH/AMBIGUOUS/LOW),低分直接走兜底模板,避免把垃圾上下文喂给模型。第二层是 prompt 约束——强制引用标注 [n],low_confidence 模板明确禁止否认来源中存在的实体。第三层是后处理——用 CitationParser 解析 [n] 标注,计算覆盖率,识别越界引用。零覆盖率 + CRAG LOW 的组合标记为 ungrounded 信号。本质上是把”不可靠的生成”变成”有据可查的生成”,同时通过可观测性发现漏网之鱼。
Q2: CRAG 和 Self-RAG 有什么区别?怎么选?
A: 核心区别在评估者的位置。CRAG 是外部评估器在检索和生成之间插一道闸门,不需要修改模型;Self-RAG 是在模型内部加 reflection tokens,让模型自己判断。选型看约束条件:如果你能微调模型且只做 RAG 任务,Self-RAG 端到端效果可能更好;如果用的是商用 API(如 qwen/GPT),CRAG 是唯一可行方案。我们项目用 CRAG 风格,因为用的是 DashScope API 无法微调,且 CRAG 的三档分流可以精确控制 Web 补偿和模板切换策略。
Q3: 评测指标怎么选?Faithfulness 和 Groundedness 有什么区别?
A: Faithfulness 衡量”答案是否忠于来源”——答案中的每条陈述是否能在检索到的上下文中找到依据,主要检测 Intrinsic Hallucination。Groundedness 衡量”答案是否有据可查”——关注的是哪些陈述有来源支撑,哪些是模型自由发挥。两者视角不同但高度相关,Hallucination Rate 约等于 1 - Groundedness。选指标看目标:检测事实错误选 Faithfulness,监控模型”脱离来源”的程度选 Groundedness/Citation Coverage。我们项目两个维度都覆盖——离线用 LLM-as-Judge 评 Faithfulness,在线用 CitationParser 算 Citation Coverage 近似 Groundedness。
Q4: Faithfulness 具体怎么算?
A: 业界主流两种做法。第一种是 NLI-based:把答案拆成原子陈述,每条陈述和检索上下文做自然语言推理(蕴含/矛盾/中立),蕴含比例即 Faithfulness。第二种是 LLM-as-Judge:让一个评判模型(通常比生成模型弱一档以降本,如 qwen-turbo)逐句判断是否有来源支撑,输出 0-1 分数。我们项目用第二种,Judge 模型用 qwen-turbo(成本是 qwen-plus 的 1/10),单次完整评测 52 条 ¥18.4。
Q5: 没有 golden answer 怎么评测?
A: 分场景处理。召回阶段用”文档名子串匹配”做近似标注——预先标注每条查询的 expectedDocNames,检索结果的 sourceName 命中即视为召回。粒度粗但成本低,配合 keyword recall(答案中期望关键词出现率)做补充。生成阶段用 LLM-as-Judge 做 reference-free 评测,不依赖 golden answer,而是让 Judge 模型基于检索上下文判断答案的忠实度和相关性。不需要标注答案,只需要标注查询+期望文档+期望关键词,标注成本大幅降低。
Q6: 评测数据哪来的?
A: 52 条查询覆盖 7 类(事实类/概念类/对比类/推理类/操作类/多跳类/超范围),每类 7-8 条。构建思路:先从真实用户历史查询中抽样(去重去噪),补充覆盖不到的类别(特别是超范围和多跳),每条标注 expectedDocNames + expectedKeywords。数据集规模不大但要”覆盖边界”——超范围类专门测拒答能力,多跳类暴露链路短板。52 条 x 4 档组件变体做对照,足以定位”加了哪个组件提升最大”。
Q7: 评测成本怎么控制?
A: 三个手段。第一,Judge 模型用小模型(qwen-turbo 而非 qwen-plus),成本降 10 倍。第二,对照设计让同一次检索结果复用——V1-V4 共享同一批 chunks 只是后处理不同,避免重复调用 embedding/reranker。第三,评测脚本绕开 DocMindAgent 的缓存/SSE/早停机制,直接组合组件调用,结果可复现。单次全量成本约十几元人民币,可以频繁跑回归。
Q8: 评测发现了什么反直觉的结论?
A: 两个反直觉发现。第一,首轮 V2(RRF 融合)竟然低于 V1(纯向量)——排查后发现不是算法问题,是 BM25 索引没跟着文档重新入库刷新,暴露了运维缺口而非技术缺陷,这就是为什么必须做评测。第二,Self-Reflection 首轮评测 V4 忠实度 +0.000(零提升),但反思延迟 956ms 占总延迟 29%——后来改造为条件重写后才拉开差距(+0.037),但代价仍然是额外一整条 LLM 往返。这直接推动了我们后来用结构化引用覆盖率(纯 Java 解析 0ms)替代 Self-Reflection 的决策。
二、DocMind 实践#
30 秒口述版#
DocMind 的可信生成链路分三段。生成前:CRAG 三档检索质量闸,阈值 HIGH 大于等于 0.65 / LOW 小于等于 0.25,灰区仲裁复用 reranker 分数分布做 heuristic 判断(0ms),分档驱动三套 prompt 模板选择——标准 / 低置信带证据 / 零证据兜底。生成后:CitationParser 纯 Java 解析 [n] 标记为结构化引用,按句切分计算覆盖率,越界编号计入 invalidRefs。置信度公式 confidenceScore = clamp(coverage) * rerank-top1,无有效句时退回 rerank-top1。整套方案替代了原 Self-Reflection(省掉一整条 LLM 往返 956ms),通过 Langfuse Scores API 推送四维质量信号实现在线可观测。
详细展开#
背景/痛点(Situation)#
项目早期(V4 阶段)的可信生成依赖 Self-Reflection:每次生成后调一次 LLM 做忠实度评分,!passed 时触发流式重写。问题有三个:
- 延迟代价大:反思+评分额外 956ms,占端到端延迟的 29%,但忠实度只提升 +0.037——ROI 极低
- 置信度不可靠:靠 LLM 自评分数做置信度判断,本质是”让犯错的人给自己打分”,且分数缺乏校准——模型倾向给自己打高分
- 检索质量无闸门:所有检索结果不分好坏直接喂给生成模型,低质量 context 导致幻觉,事后反思修补效果有限
同时,评测体系暴露了多个问题:首轮评测 V4(Self-Reflection 仅评分不重写)忠实度 +0.000,BM25 索引漏刷导致 V2 低于 V1——说明没有系统化的质量保障机制,只靠事后补丁不可持续。
做了什么(Action)#
1. CRAG 三档检索质量闸门(生成前拦截)
在 RetrievalGrader 中实现 CRAG 风格的三档评估:
- 快路径:rerank-top1 分数 >= 0.65 直判 HIGH,<= 0.25 直判 LOW,零额外调用
- 灰区仲裁三模式可配置(
grader.arbitration_mode):heuristic(默认):基于已有 rerank 分数分布——top1 >= 中位且与 top2 差距 >= 0.15 → HIGH;avg <= low + 0.05 → LOW;其余 → AMBIGUOUS。0ms 零调用cross_encoder:拼接 top-3 chunk 做单对重排,但发现 gte-rerank 对拼接长文本是 OOD 输入,常返回 0.000,加了 anomaly 检测自动退回 heuristicdisabled:直接判 AMBIGUOUS 让 Web 兜底
- 降级原则:任意仲裁失败一律 AMBIGUOUS,保守处理而非阻塞
2. 三套 Prompt 模板(分级约束)
根据 CRAG 分级 + 证据是否为空,在 stagePromptAssembly 中三选一:
knowledge_qa:标准模板,强制[n]引用标注 + 双源区分(KB 和 Web 分区展示)knowledge_qa_low_confidence:CRAG LOW 但证据非空时使用,额外强化”严禁否定来源中存在的实体”规则——解决 trace 中发现的”chunk 里有 OpenClaw 介绍但模型回答不存在”的反常识现象assembleFallback:证据为空时的纯兜底,不喂任何 chunk,明确标注”知识库未匹配”并禁止断言实体不存在
3. CitationParser 结构化引用解析(生成后校验)
纯 Java 实现,不依赖 LLM,核心流程:
- 正则扫描
[n]标记,区分有效引用(n 在 sources 范围内)和越界引用(invalidRefs) - 按句末标点切句,
isSubstantive过滤过短句 / Markdown 结构符号 / 寒暄句(剥掉[n]和结构符号后 >= 8 字符) - 覆盖率 = 含有效引用的实质句数 / 实质句总数
- 无实质句时(纯寒暄/兜底回复)标记
coverageComputed=false,上层退回 rerank-top1
4. 覆盖率驱动的置信度公式
confidenceScore = coverageComputed ? clamp01(coverage) * rerankTop1 : rerankTop1
confidenceBand = needsFallback || lowConfidence ? "LOW" : bandFromScore(confidenceScore)plaintext- 双因子乘积:引用覆盖率(衡量生成质量)x rerank-top1(衡量检索质量),一维低则整体低
- band 分档:>= 0.85 HIGH / >= 0.60 MEDIUM / 其他 LOW
- 兜底/低置信通路强制 band=LOW,不受分数影响
5. ungrounded 信号检测
!needsFallback && CRAG LOW && coverageComputed && coverage == 0.0 时标记 ungrounded——意味着模型拿到了证据但一条都没引用。这是比简单 LOW 更严重的质量信号,对应 Langfuse span 级别 WARNING + status_message: "low confidence answer (ungrounded: 零引用覆盖)"。
6. 在线质量可观测(Langfuse Scores API)
通过 LangfuseScoreClient 以 fire-and-forget 模式推送四维质量 Score:
confidence(numeric, 0-1)citation_coverage(numeric, 0-1)rerank_top1(numeric, 0-1)crag_grade(categorical, HIGH/AMBIGUOUS/LOW)
这些 Score 可在 Langfuse 跨 trace 聚合、出趋势图、设告警——OTel span 属性做不到的(只能逐条看)。
量化结果(Result)#
- 删除 Self-Reflection 后端到端延迟减少 956ms(29%),零忠实度回归(覆盖率 + prompt 模板补偿)
- CRAG 灰区 heuristic 仲裁 0ms vs 独立小模型方案 ~300ms
- 置信度计算从”LLM 自评分数(不可校准)“改为”覆盖率 x rerank-top1(可计算、可解释)”
- 离线评测采用手工对照实验:52 条查询(7 类覆盖)x 4 档组件变体(V1 纯向量 → V4 +反思),通过脚本直接组合各组件跑对照、用 qwen-turbo 做 LLM-as-Judge 评分。不是自动化评测平台,但控制变量严格,能定位”加了这个组件值不值”
- 在线通过 Langfuse Scores API 实现四维质量信号的跨 trace 聚合监控
代码锚点#
| 类/方法 | 路径 | 职责 |
|---|---|---|
CitationParser.parse() | service/rag/CitationParser.java | 解析 [n] 标记为结构化引用,计算覆盖率,统计 invalidRefs |
CitationParser.isSubstantive() | service/rag/CitationParser.java | 实质句判定:剥掉标记和结构符号后 >= minLen |
CitationParser.CitationResult | service/rag/CitationParser.java | 解析结果 record:usedSources / occurrences / coverage / invalidRefs |
RetrievalGrader.grade() | service/rag/RetrievalGrader.java | CRAG 三档评估入口:快路径 + 灰区仲裁分发 |
RetrievalGrader.arbitrateByHeuristic() | service/rag/RetrievalGrader.java | 灰区 heuristic 仲裁:top1/top2 差距 + avg 分布判断 |
RetrievalGrader.arbitrateByCrossEncoder() | service/rag/RetrievalGrader.java | 灰区 CE 仲裁:拼接 top-3 重排,含 0 分异常检测退回 heuristic |
GradeResult | service/rag/GradeResult.java | 评估结果 record:Tier(HIGH/AMBIGUOUS/LOW) / topScore / avgScore / reason |
PromptAssembler.assemble() | service/rag/PromptAssembler.java | 标准 prompt:双源透明(KB/Web 分区) + 强制 [n] 引用 |
PromptAssembler.assembleLowConfidence() | service/rag/PromptAssembler.java | 低置信 prompt:喂证据但强化”禁止否认来源中实体” |
PromptAssembler.assembleFallback() | service/rag/PromptAssembler.java | 兜底 prompt:不喂 chunk,禁止断言实体不存在 |
PromptAssembler.buildDualSourceContext() | service/rag/PromptAssembler.java | 双源透明构建:KB 区 + Web 区,编号全局连续 |
DocMindAgent.stagePromptAssembly() | agent/DocMindAgent.java:898-933 | CRAG 分级 → 三套模板选择的路由逻辑 |
DocMindAgent.stageGenerateAndPersist() | agent/DocMindAgent.java:935-1104 | 生成 + 引用解析 + 置信度计算 + ungrounded 检测 + Langfuse 推送 |
ConfidenceBands.clamp01() / bandFromScore() | agent/ConfidenceBands.java | 置信度公式辅助:分数 clamp + band 分档(>=0.85 HIGH / >=0.60 MEDIUM);#31 从 DocMindAgent 析出 |
SourcePayloadFactory.build() | service/rag/SourcePayloadFactory.java | chunk → sources 映射,保证 [n] ↔ sources[n-1] 对齐 |
SafetyGuard.getFallbackNotice() | service/rag/SafetyGuard.java | 兜底场景追加的免责提示文案 |
LangfuseScoreClient.numeric() / categorical() | config/LangfuseScoreClient.java | fire-and-forget 推送 Langfuse Score(置信度/覆盖率/CRAG 分级) |
量化数据#
| 指标 | 数值 | 基线 | 来源 |
|---|---|---|---|
| CRAG 阈值 HIGH | >= 0.65 | - | RetrievalGrader + sys_ai_config(id 43) |
| CRAG 阈值 LOW | <= 0.25 | - | RetrievalGrader + sys_ai_config |
| 灰区 heuristic gap 阈值 | >= 0.15 | - | grader.high_gap 配置 |
| heuristic 仲裁延迟 | 0ms | 独立小模型 ~300ms | 零额外 API 调用 |
| cross_encoder 仲裁延迟 | ~50ms | 独立小模型 ~300ms | 单次 reranker 调用 |
| Self-Reflection 延迟(历史,已删) | 956ms(占端到端 29%) | - | 10-评测结果与发现.md |
| Self-Reflection 忠实度增量(历史) | +0.037(条件重写后) | V3 基线 0.812 | 10-评测结果与发现.md |
| 覆盖率最小实质句长度 | 8 字符 | - | citation.coverage_min_sentence_len |
| 置信度 HIGH 阈值 | >= 0.85 | - | bandFromScore() |
| 置信度 MEDIUM 阈值 | >= 0.60 | - | bandFromScore() |
| 离线评测数据集 | 52 条 x 7 类 | - | 手工构建 |
| 评测方式 | 脚本驱动组件对照 + qwen-turbo Judge | - | 非自动化平台,手工编排 |
| RRF 融合 Recall@5 增量 | 约 +10~15 pp | V1 纯向量 | 52 条对照测试 |
| Cross-Encoder 重排 MRR 增量 | 约 +14 pp | V2 RRF | 52 条对照测试 |
三、追问应对#
面试官想听到的信号#
- 对 RAG 幻觉有体系化认知(Intrinsic/Extrinsic 分类 + 对应检测方法),而不是笼统说”加 prompt 约束”
- 理解 CRAG 的设计权衡:evaluator 选型(独立模型 vs 复用 reranker)、灰区策略(保守 AMBIGUOUS vs 激进二分)
- 置信度方案从”LLM 自评”演进到”可计算公式”的思考过程,理解 Self-Reflection 为什么被替代
- 评测不是”跑个分”——强调对照梯设计思想(控制变量)、评测暴露运维 bug 的故事
- 可信生成是端到端的链路而非单点优化:检索质量闸门(前) → prompt 分级约束(中) → 引用解析+覆盖率(后) → 在线可观测(运行时)
追问预判与应答#
Q1: 为什么不直接用 Claude Citations 或 Google Vertex Grounding?
A: 两个原因。第一,模型绑定——Claude Citations 只能配 Claude 模型,Vertex Grounding 只能配 Gemini/PaLM,我们用的是 DashScope qwen 系列,需要跨模型兼容的方案。第二,可控性——传统 [n] 标记虽然不保证覆盖,但配合后处理的 CitationParser 可以做到:覆盖率可量化、越界引用可检测、置信度可计算,而且解析逻辑完全在我们手里可调——比如 isSubstantive 的阈值、Markdown 结构符号的过滤都是根据实际 trace 数据调出来的。如果未来切模型到 Claude,可以直接用原生 Citations 替换 CitationParser,架构上是兼容的。
Q2: CRAG 的阈值 0.65/0.25 怎么定的?会不会太武断?
A: 不是拍脑袋的。初始值参考了 reranker(gte-rerank)在我们评测集上的分数分布——事实类查询 top1 集中在 0.65-0.85,无关查询在 0.10-0.30。0.65 取在事实类下界,0.25 取在无关类上界附近,留出 0.25-0.65 的灰区走仲裁。而且这两个值不是硬编码——存在 sys_ai_config 表里,通过 AiConfigHolder 热加载,不用重启就能调。灰区还额外设了 high_gap=0.15 和 low_avg_buffer=0.05 两个微调参数。
Q3: heuristic 仲裁靠 rerank 分数分布,会不会信息不够?
A: 会,所以是三模式可选。heuristic 的优势是 0ms 零调用,适合大多数情况;但对于 top1 和 top2 分数接近、avg 在中间地带的边缘 case,确实判不准。这时候可以切到 cross_encoder 模式做二次打分。但实践中发现 gte-rerank 对拼接长文本(top-3 chunk 拼成一条 passage)是 OOD 输入,经常返回 0.000,所以加了 anomaly 检测(CE_ZERO_ANOMALY_EPSILON = 1e-4)自动退回 heuristic。最保守的是 disabled 模式——直接判 AMBIGUOUS 触发 Web 兜底,宁可多查一次 Web 也不误判。降级原则是任意失败都保守处理,不阻塞主流程。
Q4: CRAG LOW 但有证据时为什么不走 fallback?
A: 这是一个踩过坑的设计决策。早期版本 CRAG LOW 直接走 fallback(不喂任何 chunk),结果发现 Web 搜索补来的低分结果被完全丢弃——模型退回到基座知识回答,反而比”喂低分证据”更差(因为基座知识可能过时)。所以改成:CRAG LOW + 证据非空走 assembleLowConfidence(带证据但强化约束),CRAG LOW + 证据为空才走 assembleFallback(真正的零证据兜底)。assembleLowConfidence 模板里加了”严禁否定来源中存在的实体”规则——这是 trace 里真实发现的 bug:chunk 里明明有 OpenClaw 的完整介绍,模型却回答”OpenClaw 不存在,可能是拼写错误”。
Q5: 覆盖率公式 coverage * rerank-top1 为什么是乘法不是加法?
A: 乘法的语义是”两个维度都要好才给高分”。如果用加法(即使加权),一个维度 0.9 另一个 0.1 还能得 0.5——但实际上 rerank 0.9(检索很准)但覆盖率 0.1(模型几乎没引用来源)的情况说明生成严重脱离检索结果,不应该给中等置信度。乘法下 0.9 x 0.1 = 0.09 直接 LOW,符合直觉。反过来覆盖率 1.0 但 rerank-top1 0.2(来源不太相关但模型全都引了)也只得 0.2,因为虽然”忠于来源”但来源本身不可靠。
Q6: 评测框架为什么不直接用 RAGAS?
A: 三个原因。第一,RAGAS 是 Python 生态,我们项目是 Java 全栈,引入 Python 评测脚本增加工程复杂度。第二,RAGAS 默认用 GPT-4 做 Judge,成本高且在国内有网络问题。第三,也是最重要的——RAGAS 评测的是端到端效果,但我们需要的是”组件级对照”(V1 纯向量 → V2 +RRF → V3 +重排 → V4 +反思)。我们的做法是写脚本把组件拆开按 variant 重新组合,控制变量跑对照——不是一个可复用的评测平台,但胜在控制变量严格,能定位每个组件的增量价值。RAGAS 的评测指标定义(faithfulness/relevancy 等)我们有参考,只是 Judge 实现和对照编排是手工脚本驱动的。
Q7: 在线评测和离线评测怎么配合?
A: 各有分工。离线评测回答”加了这个组件值不值”——通过对照梯(V1-V4)在同一数据集上比较 Recall/MRR/Faithfulness 的增量,发布前跑回归。在线评测回答”线上质量有没有变化”——通过 Langfuse Scores API 推送 confidence / citation_coverage / crag_grade / rerank_top1 四维信号,在 Langfuse 里做跨 trace 聚合、趋势图、设告警。典型工作流:离线评测发现问题(如 CRAG LOW 比例异常高)→ 修改阈值/模板 → 离线回归确认 → 上线后看在线趋势是否改善。两套评测的指标有交集(如 coverage 线上线下都看)但侧重点不同。
Q8: 如果模型完全不输出 [n] 标记怎么办?
A: CitationParser 会返回 usedSources=[]、coverage=0.0、coverageComputed=true(如果有实质句的话)。置信度公式 clamp01(0.0) * rerankTop1 = 0,band 直接 LOW。如果连实质句都没有(纯寒暄/兜底),coverageComputed=false,退回 rerankTop1 作为置信度。所以最差情况是置信度降低 + SSE 事件带 confidenceWarning 提示前端,不会崩溃或误报 HIGH。prompt 模板里”引用标注(强制)“的约束在实际测试中覆盖率还不错——qwen-plus 在有来源的标准模板下基本都会标 [n],覆盖率通常在 0.6-0.9 之间。
Q9: 双源透明模式为什么要把 KB 和 Web 分区展示?
A: 信源可信度不同需要在 prompt 层面让模型感知到。KB 来源经过人工上传和审核,可作为确定性结论引用;Web 搜索结果未经审核,模型引用时必须加限定语(“据网络公开信息”)。如果混在一起编号,模型无法区分信源等级。buildDualSourceContext 把 KB 区放前面、Web 区用分隔线 --- 隔开并标注”未经知识库审核,仅供参考”,全局编号连续保证 [n] 对齐。前端引用卡片也通过 SourcePayloadFactory.build() 的 type 字段区分 knowledge_base / web,展示不同样式。
反击引导#
“说到 CRAG 的灰区仲裁,这个设计和我们 Agentic RAG 的循环终止条件是相互配合的——AMBIGUOUS 时触发 Web 兜底,对应 agentic 循环中 supervisor 的 webSearch 工具调用决策,这块可以展开讲一下 agentic 循环的设计。”
“覆盖率 x rerank-top1 的置信度公式,其中 rerank-top1 涉及我们的双路召回(向量 + BM25)+ RRF 融合 + Cross-Encoder 重排的整条检索链路——如果感兴趣可以聊聊检索排序的设计。”
“在线可观测用 Langfuse Scores API 推送,这是我们整套 OTel + Langfuse 可观测体系的一部分——从 StageEmitter 统一三套埋点到 LangfuseScoreClient 的 fire-and-forget 推送,可以展开讲可观测架构。”