RAG 效果评估体系设计与实现
text
学习日期:2026-08-04
今日主题:RAG 效果评估体系设计与实现
所属领域:RAG 与企业知识库
适合角色:Java 后端架构师、AI 应用架构师
预计阅读时间:15~30 分钟
适用版本:RAGAS 0.4.x(2026-01-13 发布;docs.ragas.io)、LangFuse 2.x+、OpenTelemetry GenAI SemConv 1.30+
资料检索日期:2026-08-04
免责声明:本文涉及的具体阈值、API、CLI 名称为 2026-08-04 当日检索结果,RAGAS 已从 0.3 迁移到 0.4 的
ragas.metrics.collections命名空间,迁移过程中存在破坏性变更,生产环境使用前请以本地安装版本的官方文档为准。
1. 今日主题
RAG 效果评估体系设计与实现。
为什么 AI 应用架构师 / Java 后端架构师必须掌握它
在企业落地 RAG 的过程中,"看上去答得还行"是最不可控的判断方式。检索质量、生成忠实度、答案相关性这三种失败模式往往耦合在同一段对话里,但根因完全不同:可能来自 Chunking、Embedding、Prompt、模型选型甚至文档版本治理。
很多团队的 RAG 项目在 0 → 1 阶段"看着能跑",进入生产后却不知道效果有没有退化、新上的 rerank 究竟有没有正向收益------根因就是没有评估体系 。评估体系不是给模型打分,而是给整个 RAG 流水线打分:它是 Chunking、Embedding、Rerank、Prompt、模型五个变量同时调节时唯一能告诉你"为什么变好 / 为什么变差"的度量基础。
对 Java 后端架构师来说,RAG 评估体系还有一层特殊价值:它把"AI 质量"翻译成了传统后端擅长的"指标 + 卡点 + 基线"------刚好可以和现有的 CI、监控、告警体系直接对齐。
2. 核心问题
学完后应能回答:
- 评估 RAG 时到底要测什么指标?检索层和生成层为什么必须分开?
- RAGAS / DeepEval / TruLens / LangFuse 在 2026 年各自承担什么职责,能力边界在哪里?
- 离线 gold set、在线采样、人工校验这三种评估模式各自解决什么问题?
- LLM-as-Judge 在生产评估中可信吗?什么时候必须替换或校准?
- 评估指标如何接进 CI 卡点和生产告警,形成"评估 → 决策 → 回归"的闭环?
3. 前置知识
阅读本文需要你已经理解:
- RAG 基础流程:文档解析 → Chunking → Embedding → 向量检索 → 可选 Rerank → Prompt 组装 → 模型生成。
- 向量检索和关键词检索的差异:为什么需要混合检索与 Rerank(在 2026-07-22《混合检索与 Rerank》一文中已覆盖)。
- Spring AI / LangChain4j 基本调用范式:ChatClient、QuestionAnswerAdvisor、向量存储抽象。
- CI、基线对比、可观测性基本概念:指标(Metric)、日志(Log)、追踪(Trace)、告警阈值。
如果你还不熟悉 RAG 的整体链路,建议先补一下基础流程,避免在评估时把链路环节和评估指标搞混。
4. 核心原理
4.1 RAG 为什么需要"单独"的评估体系
RAG 由"检索 + 生成"两部分组成,三类故障模式互相纠缠:
| 故障类型 | 现象 | 根因位置 |
|---|---|---|
| 检索失败(Retrieval Failure) | 答案正确但 support 不到 context | Retriever/Embedding/Chunking |
| 生成失败(Generation Failure) | 检索到正确 chunk,模型编造细节 | Prompt/Model |
| 覆盖失败(Coverage Failure) | 正确答案在语料中,但从未被检索到 | Embedding/Rerank/Index |
只评估最终答案会把这三类故障混在一起------CI 无法判断应该改 Chunking 还是改 Prompt。RAG 评估的本质是把每类故障量化到独立指标,让优化闭环精准落在正确环节。
4.2 评估指标的诊断矩阵
2026 年业界主流依然是 RAGAS 四指标 + 几个 IR 指标的组合。指标不是越高越好,关键是配合使用:
| 指标 | 数学含义 | 诊断什么 | 典型调优环节 |
|---|---|---|---|
| Context Precision | 检索到的 chunk 中"有用"的占比 | 检索器是否把噪声一并拽进来 | top-k、Rerank、Metadata 过滤 |
| Context Recall | 答案需要的"有用 chunk"被检索到的占比 | 是否漏召 | Embedding、Chunking、Query Rewriting |
| Faithfulness | 答案中的"声明"被 context 支持的比例 | 模型是否在凭空捏造 | Prompt、temperature、模型选型 |
| Answer Relevancy | 答案语义对齐问题的程度 | 是否答非所问 | Prompt、Query Rewriting |
| Answer Correctness | F1 + 语义相似度(与 ground truth 比) | 答案的事实正确性 | 端到端,多变量 |
| Precision@K / Recall@K / MRR / NDCG | 经典 IR 指标 | 有标签时的检索质量 | 同 Context Precision/Recall |
关键的"指标组合诊断"
这是评估体系最值钱的部分,单独看任何一个数字都不够。
#mermaid-svg-jIwgqNvzplXVmzPx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jIwgqNvzplXVmzPx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jIwgqNvzplXVmzPx .error-icon{fill:#552222;}#mermaid-svg-jIwgqNvzplXVmzPx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jIwgqNvzplXVmzPx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jIwgqNvzplXVmzPx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jIwgqNvzplXVmzPx .marker.cross{stroke:#333333;}#mermaid-svg-jIwgqNvzplXVmzPx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jIwgqNvzplXVmzPx p{margin:0;}#mermaid-svg-jIwgqNvzplXVmzPx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jIwgqNvzplXVmzPx .cluster-label text{fill:#333;}#mermaid-svg-jIwgqNvzplXVmzPx .cluster-label span{color:#333;}#mermaid-svg-jIwgqNvzplXVmzPx .cluster-label span p{background-color:transparent;}#mermaid-svg-jIwgqNvzplXVmzPx .label text,#mermaid-svg-jIwgqNvzplXVmzPx span{fill:#333;color:#333;}#mermaid-svg-jIwgqNvzplXVmzPx .node rect,#mermaid-svg-jIwgqNvzplXVmzPx .node circle,#mermaid-svg-jIwgqNvzplXVmzPx .node ellipse,#mermaid-svg-jIwgqNvzplXVmzPx .node polygon,#mermaid-svg-jIwgqNvzplXVmzPx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jIwgqNvzplXVmzPx .rough-node .label text,#mermaid-svg-jIwgqNvzplXVmzPx .node .label text,#mermaid-svg-jIwgqNvzplXVmzPx .image-shape .label,#mermaid-svg-jIwgqNvzplXVmzPx .icon-shape .label{text-anchor:middle;}#mermaid-svg-jIwgqNvzplXVmzPx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jIwgqNvzplXVmzPx .rough-node .label,#mermaid-svg-jIwgqNvzplXVmzPx .node .label,#mermaid-svg-jIwgqNvzplXVmzPx .image-shape .label,#mermaid-svg-jIwgqNvzplXVmzPx .icon-shape .label{text-align:center;}#mermaid-svg-jIwgqNvzplXVmzPx .node.clickable{cursor:pointer;}#mermaid-svg-jIwgqNvzplXVmzPx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jIwgqNvzplXVmzPx .arrowheadPath{fill:#333333;}#mermaid-svg-jIwgqNvzplXVmzPx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jIwgqNvzplXVmzPx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jIwgqNvzplXVmzPx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jIwgqNvzplXVmzPx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jIwgqNvzplXVmzPx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jIwgqNvzplXVmzPx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jIwgqNvzplXVmzPx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jIwgqNvzplXVmzPx .cluster text{fill:#333;}#mermaid-svg-jIwgqNvzplXVmzPx .cluster span{color:#333;}#mermaid-svg-jIwgqNvzplXVmzPx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jIwgqNvzplXVmzPx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jIwgqNvzplXVmzPx rect.text{fill:none;stroke-width:0;}#mermaid-svg-jIwgqNvzplXVmzPx .icon-shape,#mermaid-svg-jIwgqNvzplXVmzPx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jIwgqNvzplXVmzPx .icon-shape p,#mermaid-svg-jIwgqNvzplXVmzPx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jIwgqNvzplXVmzPx .icon-shape .label rect,#mermaid-svg-jIwgqNvzplXVmzPx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jIwgqNvzplXVmzPx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jIwgqNvzplXVmzPx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jIwgqNvzplXVmzPx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Recall 低
Precision 高
检索漏召
正确答案根本没进来
提高 top-k
换 Embedding
加 Rerank
改 Chunking
Precision 低
Recall 高
检索噪声多
模型被干扰
收紧 top-k
调阈值
优化 metadata 过滤
Faithfulness 低
模型胡编
prompt 宽松
强化 grounding prompt
temp=0
必要时换更强的 reasoning 模型
Answer Relevancy 低
答非所问
query 改写
system prompt 优化
问题理解前置
全部指标健康
p95 仍高
体感差
看 trace:
检索慢 / rerank 慢 /
context 拼接过长
生产参考阈值(经验值,需以本业务基线为锚)
这些是行业 2026 年的常见经验基线,不是普适真理:每个业务必须先测一遍自己的基线,再围绕基线设阈值。
| 指标 | 经验下限 |
|---|---|
| Faithfulness | ≥ 0.85(高风险域 ≥ 0.92) |
| Answer Relevancy | ≥ 0.80 |
| Context Precision | ≥ 0.70 |
| Context Recall | ≥ 0.75 |
| Citation Accuracy(业务自评) | ≥ 0.95 |
4.3 评估的"三层闭环"
离线 + 在线 + 人工,构成完整的评估闭环:
#mermaid-svg-15LpViw8b76XEXIN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-15LpViw8b76XEXIN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-15LpViw8b76XEXIN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-15LpViw8b76XEXIN .error-icon{fill:#552222;}#mermaid-svg-15LpViw8b76XEXIN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-15LpViw8b76XEXIN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-15LpViw8b76XEXIN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-15LpViw8b76XEXIN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-15LpViw8b76XEXIN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-15LpViw8b76XEXIN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-15LpViw8b76XEXIN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-15LpViw8b76XEXIN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-15LpViw8b76XEXIN .marker.cross{stroke:#333333;}#mermaid-svg-15LpViw8b76XEXIN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-15LpViw8b76XEXIN p{margin:0;}#mermaid-svg-15LpViw8b76XEXIN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-15LpViw8b76XEXIN .cluster-label text{fill:#333;}#mermaid-svg-15LpViw8b76XEXIN .cluster-label span{color:#333;}#mermaid-svg-15LpViw8b76XEXIN .cluster-label span p{background-color:transparent;}#mermaid-svg-15LpViw8b76XEXIN .label text,#mermaid-svg-15LpViw8b76XEXIN span{fill:#333;color:#333;}#mermaid-svg-15LpViw8b76XEXIN .node rect,#mermaid-svg-15LpViw8b76XEXIN .node circle,#mermaid-svg-15LpViw8b76XEXIN .node ellipse,#mermaid-svg-15LpViw8b76XEXIN .node polygon,#mermaid-svg-15LpViw8b76XEXIN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-15LpViw8b76XEXIN .rough-node .label text,#mermaid-svg-15LpViw8b76XEXIN .node .label text,#mermaid-svg-15LpViw8b76XEXIN .image-shape .label,#mermaid-svg-15LpViw8b76XEXIN .icon-shape .label{text-anchor:middle;}#mermaid-svg-15LpViw8b76XEXIN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-15LpViw8b76XEXIN .rough-node .label,#mermaid-svg-15LpViw8b76XEXIN .node .label,#mermaid-svg-15LpViw8b76XEXIN .image-shape .label,#mermaid-svg-15LpViw8b76XEXIN .icon-shape .label{text-align:center;}#mermaid-svg-15LpViw8b76XEXIN .node.clickable{cursor:pointer;}#mermaid-svg-15LpViw8b76XEXIN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-15LpViw8b76XEXIN .arrowheadPath{fill:#333333;}#mermaid-svg-15LpViw8b76XEXIN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-15LpViw8b76XEXIN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-15LpViw8b76XEXIN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-15LpViw8b76XEXIN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-15LpViw8b76XEXIN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-15LpViw8b76XEXIN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-15LpViw8b76XEXIN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-15LpViw8b76XEXIN .cluster text{fill:#333;}#mermaid-svg-15LpViw8b76XEXIN .cluster span{color:#333;}#mermaid-svg-15LpViw8b76XEXIN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-15LpViw8b76XEXIN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-15LpViw8b76XEXIN rect.text{fill:none;stroke-width:0;}#mermaid-svg-15LpViw8b76XEXIN .icon-shape,#mermaid-svg-15LpViw8b76XEXIN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-15LpViw8b76XEXIN .icon-shape p,#mermaid-svg-15LpViw8b76XEXIN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-15LpViw8b76XEXIN .icon-shape .label rect,#mermaid-svg-15LpViw8b76XEXIN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-15LpViw8b76XEXIN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-15LpViw8b76XEXIN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-15LpViw8b76XEXIN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Layer 3 人工校验
Layer 2 在线流量采样
调整
Layer 1 离线 Gold Set
补数据集
100~500 条
人工标注 QA
运行流水线
产生 question/answer/contexts
RAGAS 跑分
CI 卡点:
超过阈值才合并
5~20% 生产请求
异步 judge 评分
接 OpenTelemetry span
触发漂移告警
每周抽 50~100 条
最低分 trace
领域专家复评
Cohen's kappa
≥ 0.6 视为校准通过
离线层解决"我这次改动有没有让事情变差",跑分稳定、可重放,是 CI 的灵魂。
在线层解决"线上用户在问什么、我们一直在哪些问题上失败",这是 gold set 永远覆盖不到的部分。常见做法是用 LangFuse / OpenLLMetry 等给每个生产 span 异步挂上评估分数,没有 ground truth 也能跑 answer_relevancy、context_relevance、faithfulness 等无参考指标。
人工层 是 LLM-as-Judge 的唯一可靠校准方式。LLM-as-Judge 与人类一致性通常能达到 0.8+,但位置偏差(position bias)、冗长度偏差(verbosity bias)、自我偏好偏差(self-preference) 都会让自动化打分失真。Human-in-the-loop 的真正作用不是打标,而是给 judge 模型出考卷。
4.4 LLM-as-Judge 的工程边界
这是评估体系最容易翻车的地方,必须把以下结论当硬约束:
- Judge 模型必须和生产模型解耦:用同一个模型打自己和别人,会系统性偏高;
- Judge 模型要定期校准:每季度用人工标注的子集算 Cohen's kappa,低于 0.6 必须重写 prompt 或换模型;
- 不要让 judge 评 judge:自反馈循环一定漂移;
- 高昂的 QPS 优先用小模型:用 GPT-4o 当 judge 一次 150 条评测就要 ~0.85 美元,规模化后会反噬成本控制;
- 置信度区间必须打印:单次跑分的 score 没有意义,至少要看"过去 30 天 95% 分位数 + 最大回撤"。
4.5 评估体系的工程化收益
把评估变成可工程化的能力后,它会"反向重塑"整个 RAG 系统:
- Chunking 优化 不再凭直觉,而是看 Context Recall 提升曲线;
- Embedding 模型 不再盲换,而是用 NDCG@10 + Context Precision 量化对比;
- Prompt 版本治理 不再靠"感觉",而是 Faithfulness 数值化对比;
- 多模型路由 不再拍脑袋,而是不同模型在 Answer Correctness / Cost 上的 Pareto 比较;
- 新文档入库 不再"塞进去就行",而是监控 Context Recall 的滚动基线。
这是评估体系从"质量部门报表"变成"架构师工具盘"的过程。
5. Java 工程实践
案例版本:Spring Boot 3.4.x + Spring AI 1.0.x(Spring AI 在 2026 年已经稳定到 1.0 主版本),RAGAS 0.4.x 通过 Python Sidecar 调用(Java 侧用 HTTP/RPC 桥接)。这里给两条工程路径:(a) 纯 Java + Spring AI + 自研评估器;(b) Python RAGAS Sidecar。
5.1 数据结构与对象模型
java
public record EvalSample(
String question,
String generatedAnswer,
List<String> retrievedContexts,
String referenceAnswer, // 可空,但有它能跑 context_recall/answer_correctness
Map<String, Object> metadata // tenant, traceId, model, promptVersion, etc.
) {}
public record EvalResult(
double faithfulness,
double answerRelevancy,
double contextPrecision,
double contextRecall,
double answerCorrectness,
Map<String, Double> extras // citation accuracy 等业务自评指标
) {}
metadata 里必须 带 promptVersion 和 embeddingModelVersion,否则基线回归无从谈。
5.2 调用 RAGAS 0.4 的 Python Sidecar
RAGAS 0.4 仍然是 Python 库。生产 Java 工程通常用一个轻量 Sidecar 提供 REST 接口,主进程通过 HTTP 客户端调用。
Sidecar 接口设计
python
# app.py (FastAPI)
from fastapi import FastAPI
from ragas.metrics.collections import Faithfulness, AnswerRelevancy, ContextPrecision, ContextRecall
from ragas.llms import llm_factory
from openai import AsyncOpenAI
app = FastAPI()
llm = llm_factory("gpt-4o-mini", client=AsyncOpenAI())
faithfulness = Faithfulness(llm=llm)
answer_relevancy = AnswerRelevancy(llm=llm)
context_precision = ContextPrecision(llm=llm)
context_recall = ContextRecall(llm=llm)
@app.post("/evaluate")
async def evaluate(samples: list[dict]):
results = []
for s in samples:
f = await faithfulness.ascore(
user_input=s["question"],
response=s["generatedAnswer"],
retrieved_contexts=s["retrievedContexts"],
)
ar = await answer_relevancy.ascore(
user_input=s["question"],
response=s["generatedAnswer"],
retrieved_contexts=s["retrievedContexts"],
)
# context_precision/recall 需要 reference_contexts
results.append({
"question": s["question"],
"faithfulness": f.value,
"answer_relevancy": ar.value,
})
return results
注意:RAGAS 0.4 移到了
ragas.metrics.collections,旧的from ragas.metrics import faithfulness在 0.4 中不再适用 ,必须在pyproject.toml锁定ragas==0.4.3并以本地安装版本为准。
5.3 Java 侧的评估 Job
java
@Component
public class RagEvaluationJob {
private final RestClient ragasClient;
private final RagPipeline pipeline; // 你的业务 RAG 流水线
private final EvalDatasetStore datasetStore; // gold set 仓库
private final MeterRegistry meterRegistry;
/**
* 每周一凌晨 03:00 跑全量离线评估
*/
@Scheduled(cron = "0 0 3 ? * MON")
public void runWeeklyOfflineEval() {
List<EvalSample> samples = datasetStore.loadActiveDataset();
// 1) 用当前流水线生成答案和 context
List<EvalSample> generated = samples.stream()
.map(s -> runPipeline(s))
.toList();
// 2) 调 RAGAS Sidecar
List<Map<String, Object>> raw = ragasClient.post()
.uri("/evaluate")
.body(generated)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
// 3) 聚合 + 写入指标
EvalAggregate agg = aggregate(raw);
publishMetrics(agg);
// 4) 基线对比,差异超过阈值就告警
EvalBaseline baseline = datasetStore.loadBaseline();
diffAndAlert(agg, baseline);
}
private void publishMetrics(EvalAggregate agg) {
meterRegistry.gauge("rag.eval.faithfulness", agg, a -> a.faithfulness());
meterRegistry.gauge("rag.eval.answer_relevancy", agg, a -> a.answerRelevancy());
meterRegistry.gauge("rag.eval.context_precision", agg, a -> a.contextPrecision());
meterRegistry.gauge("rag.eval.context_recall", agg, a -> a.contextRecall());
meterRegistry.gauge("rag.eval.answer_correctness", agg, a -> a.answerCorrectness());
}
}
5.4 在 Spring AI 中自动产出 trace + 评估
要支撑在线采样评估,RAG 调用必须携带 trace ID 。Spring AI 的 QuestionAnswerAdvisor 配合 Micrometer Observation 可以直接给出检索和生成的可观测数据:
java
@Configuration
public class RagConfig {
@Bean
public Advisor qaAdvisor(VectorStore store, EmbeddingModel embeddingModel) {
return QuestionAnswerAdvisor.builder(store)
.searchRequest(SearchRequest.builder()
.topK(8) // 与评估指标 top-k 对齐
.similarityThreshold(0.55f) // 阈值也是评估维度
.build())
.build();
}
}
在请求侧通过 ChatClient 触发,并把 trace 上下文绑进 metadata:
java
@RestController
public class ChatController {
private final ChatClient chatClient;
private final Tracer tracer;
private final EvalSamplePublisher publisher; // -> Kafka
@PostMapping("/v1/chat")
public ChatResponse chat(@RequestBody ChatRequest req, HttpServletRequest http) {
String traceId = http.getHeader("X-Trace-Id");
var span = tracer.nextSpan().name("rag.chat").start();
try (var ignored = tracer.withSpan(span)) {
String answer = chatClient.prompt()
.advisors(a -> a.param("traceId", traceId))
.user(req.question())
.call()
.content();
// 抽样发到评估 topic
if (shouldSample(req)) {
publisher.publish(new EvalSample(
req.question(),
answer,
extractContexts(span),
null,
Map.of(
"tenant", req.tenant(),
"model", ragProps.getChatModel(),
"promptVersion", ragProps.getPromptVersion()
)
));
}
return new ChatResponse(answer);
} finally {
span.end();
}
}
}
5.5 CI 卡点
把评估接进 GitHub Actions / GitLab CI,PR 触碰到 RAG 代码就强制跑:
yaml
- name: Run RAG evaluation
run: |
python scripts/run_ragas.py \
--dataset held_out_v4 \
--out result.json
- name: Compare to baseline
run: |
python scripts/compare_metrics.py \
--new result.json \
--baseline s3://evals/main/latest.json \
--thresholds faithfulness=0.85,answer_relevancy=0.80,context_precision=0.70,context_recall=0.75 \
--max_drop 0.02
--max_drop 是关键。即使 PR 指标高于阈值,下降超过 2 个百分点也要拒绝合并。这是防止"千次 0.5 分慢漂移"的唯一机制。
5.6 与现有 Java 基础设施的对接
- Kafka:评估样本流,作为异步 judge 的上游;
- Redis:在线评估的语义缓存层("过去 24h 内答过同一个问题的评估分数如何");
- MySQL / 指标库 :gold set、评估结果版本化(带
commit_sha、prompt_version、embedding_version); - Prometheus + Grafana:评估指标成图,业务方和模型团队都能看。
6. 生产环境案例
业务背景
某 SaaS 客服机器人,上线 6 个月。技术栈 Spring Boot + Spring AI,retrieval 用 Qdrant,rerank 用开源 bge-reranker-v2-m3,Chat 模型按 prompt 复杂度路由到 GPT-4o-mini 或 GPT-4o。7 月初 PM 要求"接入飞书知识库中 8 月更新的 200 个新 FAQ"。
问题现象
上线飞书文档后,D 团队反馈:bot 在回答"如何申请退款"这类问题时,会出现前后不一致的答案------上午问一次说 30 天,下午问同一条就说 15 天。
可能原因(按概率排序)
- 新文档 chunking 策略与旧文档不一致:飞书的导出格式和旧 wiki 格式差异大,导致同一主题被切成不同长度的 chunk;
- Embedding 模型没变,但 chunk size 变了:向量空间分布迁移,旧 query 命中旧 chunk 的概率降低,新 chunk 也未被稳定命中;
- 没有引用溯源:多 chunk 拼接时模型随机选其一,造成答案漂移;
- 没有评估体系:根本没人知道 Context Recall 在新文档接入后是高了还是低了。
排查步骤
- 拉两周 trace:看"退款"相关 query 命中的 chunk 来源占比;
- 离线评测对比(幸亏此前 gold set 已有 80 条退款场景 QA):用旧索引跑一次、新索引跑一次,看指标;
- 指标诊断矩阵 :
- Context Recall 旧 0.91 → 新 0.78 ← 关键信号:漏召
- Faithfulness 旧 0.93 → 新 0.94 ← 模型没问题
- 答案漂移 = 模型每次挑不同的 chunk,组合输出不同结论
- 检查 chunking :飞书导出是 HTML 转 Markdown,每个
<table>后强制切分,导致一个完整退款流程被切成 4 段。
解决方案
- 统一 Chunking 策略:自定义 DocumentTransformer,按"标题 + 段落 + 表格"语义切分,不再按字符数硬切;
- 加 Rerank:用 bge-reranker 给"包含完整退款步骤"的 chunk 提权,提升 Context Precision;
- 加引用溯源:在 Prompt 模板里要求模型标注每个结论来自哪条 chunk,并在 user 端展示;
- 加评估卡点:CI 必须跑 RAGAS,任何 chunking / embedding / prompt 改动都触发离线评估。
验证方法
| 指标 | 修复前 | 修复后 |
|---|---|---|
| Context Recall | 0.78 | 0.90 |
| Context Precision | 0.62 | 0.78 |
| Faithfulness | 0.94 | 0.95 |
| 答案前后一致率(业务自定义) | 71% | 96% |
如何避免再次发生
- 评估数据集版本化:每接入一批新文档,先跑一次评估看 Context Recall 变化;
- 新文档先影子模式:新索引先在 5% 流量上做 A/B,对照评估指标,达标后再切换;
- Answer Correctness 不能只看均值:必须看分位数(p10、p50、p95),长尾问题往往最先暴露;
- 人工校验每周一次:业务方 + AI 工程师一起 review 评估分数最低的 50 条 trace。
7. 架构选型与权衡
7.1 评估框架对比
| 框架 | 语言 | 主要职责 | 适合场景 |
|---|---|---|---|
| RAGAS 0.4 | Python | LLM-as-Judge 指标四件套 + Testset Generator | 离线 RAG 评估主引擎 |
| DeepEval | Python | 多维度 + G-Eval | 复杂 prompt 评分、合成测试集 |
| TruLens | Python | trace 上挂反馈 | 已有 trace 体系、需要反馈函数 |
| LangFuse | TS/Python(支持 Java SDK) | 可观测 + 在线评估 + 数据集 | 生产 trace + 在线评分一体化 |
| 自研 Java 评估器 | Java | 简单指标(Citation、覆盖率) | 不愿引入 Python 依赖、要求低延迟 |
经验建议:离线用 RAGAS + 自研混合;在线用 LangFuse 或 OpenLLMetry + LLM-as-Judge;人工校验永远是最后一公里。
7.2 Prompt / RAG / 微调,如何选
这个三角在评估体系成熟之后才有真正的可比性。
| 选择 | 适用 | 不适用 |
|---|---|---|
| 仅 Prompt Engineering | 简单事实问答、规则清晰 | 需要反复一致的高风险域 |
| RAG + 评估 | 知识不断更新、答案需可溯源 | 写作 / 创造性任务 |
| 微调 | 风格 / 输出格式固定、需要低延迟 | 知识频繁更新、追求泛化 |
7.3 云端 vs 本地 judge 模型
| 维度 | 云端 GPT-4o / Claude Opus | 本地 Llama 70B/405B |
|---|---|---|
| 评估质量 | 显著高 | 与 prompt 工程和领域强相关 |
| 单次成本 | 高(核心成本来源) | 中(GPU 摊销) |
| 数据合规 | 受供应商政策约束 | 完全可控 |
| 工程复杂度 | 低 | 高 |
| 一致性 | 较高 | 需固定种子 + 全量校验 |
生产实践经验:日常跑分用 GPT-4o-mini 或本地 14B 模型,每个季度做一次"GPT-4o 高质量裁判样本"做校准。如果业务是金融、医疗,必须在私有化 judge 上手动做 kappa 校验。
7.4 评估体系要不要做"预发送门"
| 取舍 | 优 | 劣 |
|---|---|---|
| 预发送门(每次回答前 judge) | 阻止严重幻觉 | 显著增加延迟和成本 |
| 异步挂载 trace | 不影响首字延迟 | 错误答案可能短时间内已经到达用户 |
| 抽样事后评估 | 影响最小 | 难以 SLA 兜底 |
推荐组合 :预发送门仅用于高风险问题(涉及金钱、法律、医疗),其余异步 + 抽样。
7.5 是否需要人工兜底
凡是"答案直接面向终端用户"且"失效会造成可见损失"的场景,都必须有规则兜底:
- 检测"未命中 context" → 返回标准兜底文案;
- 检测"Faithfulness 低于阈值" → 改走人工工单;
- 检测"问题包含敏感操作动词"(删除、转账、关闭账户)→ 必须人工确认。
8. 安全、成本与可观测性
8.1 安全与权限
- 评测数据集涉密:gold set 通常含敏感问答,必须与生产数据隔离存储;
- 评测样本审计:评估样本流同样进审计日志,保留 ≥ 90 天;
- Judge 模型越权风险:LLM-as-Judge 看到的也是真实问题,配置独立的访问控制和保留策略;
- 不要把业务 Gold set 发给共享 judge:法律域、医疗域的 ground truth 不能交给 SaaS judge。
8.2 成本
- LlamaIndex + RAGAS 在 150 条样本评测一次约 0.85 美元(GPT-4o-mini,2026 年实际数据);
- 在线采样评估一旦 5% 流量就跑满 judge:单条 judge 调用 ≈ Chat 调用的 60%~80% 成本;
- 建议:1) judge 与 chat 分账;2) 在线采样分层(5% 普通、1% 高风险细评);3) 优先用 mini / flash 小模型做初筛,仅"低分样本"才升级大模型复评。
8.3 可观测性指标
必监控:
rag.eval.faithfulness.p50/p95rag.eval.answer_relevancy.p50/p95rag.eval.context_recall.p50rag.eval.answer_correctness.p10(长尾敏感)rag.pipeline.retrieval_recall_at_k(离线打点)rag.pipeline.ci.max_drop(CI 卡点自身状态)
必日志:
- 每个 EvalSample 完整快照(含 traceId、promptVersion、embeddingVersion、retrievedContextIds);
- CI 跑分异常与基线对比结果;
- Judge LLM 的 prompt / model id / token 消耗;
- 人工校验的 kappa 趋势。
8.4 异常降级与兜底
- Judge 服务不可用:不阻塞主链路,回退到"结果可信但不评分"模式;
- 评估数据集过期:每周自动检查基线距今超过 90 天就告警;
- Judge 模型版本飘移:观察评估指标无理由抖动时,优先怀疑 judge 模型被上游悄悄切换;
- Context Recall 暴跌:触发紧急回滚到上一个 baseline 的索引配置。
9. 常见错误
错误 1:只看最终答案对不对
现象:上线前随机挑 20 个问题人工 review,"感觉能用"就上线。
原因:忽视三类故障分离。答案对 ≠ Faithfulness 高;答案流畅 ≠ Recall 高。
正确做法:用指标矩阵分离检索层和生成层。Question 拆为"命中了哪些 chunk""chunk 是否支撑结论""结论是否切中问题"三层。
错误 2:用业务问答当 gold set,但永远不更新
现象:gold set 一年没动,但文档库已变更 30%。
原因:gold set 漂移是评估体系最常见的慢性病。
正确做法:(a) 在线采样低分样本自动回流;(b) 每季度抽 8% 样本人工复核,约 8% 通常是"问题变模糊/答案过期"的评估 bug;© 把 gold set 变更记入审计日志。
错误 3:用 Chat 模型自身当 judge
现象:直接拿同一个 LLM 当判官。
原因:自评打分系统性偏高。LLM 会偏好自己风格的输出(verbosity bias、self-preference)。
正确做法:judge 与 chat 必须解耦,并且用人类子集季度校准(kappa ≥ 0.6)。
错误 4:评估指标只跑一次,发布后再也不看
现象:上线前跑分 0.92,上线后从不复测。
原因:把评估当"准入门槛"而非"治理工具"。
正确做法:评估是 7×24 流程,CI 卡点 + 周离线跑分 + 在线采样 + 月人工核验四件事必须长期跑。
错误 5:盲目引入 N 个指标,CI 完全跑不动
现象:一口气接入 faithfulness、answer_relevancy、context_precision、context_recall、answer_correctness、citation_accuracy、toxicity、NDCG@10、MRR... 每个 PR 跑 30 分钟。
原因:评估不是越多越好。
正确做法:每个项目最多 5 个一级指标,CI 关注"准入门槛 + max_drop";其余指标走周离线 / 月离线。
10. 架构师视角总结
什么时候应该用
- 任何对外服务超过 1 个月:必须建评估体系;
- 多人协作的 RAG 项目:没有评估体系就无法跨人交接;
- RAG-as-a-Service 形态:评估是 SLA 的核心证据。
什么时候不应该用(或要谨慎)
- 纯内部 demo / 验证:可以手工 review,但需要明确"这是 demo 不是产品";
- POC 阶段 :1 周内出活不必搭全套评估体系,但接口契约必须留扩展点(metadata + 版本号);
- 业务量极小(< 100 QPS):在线评估成本可能超过实际业务价值。
哪些能力不应完全交给模型
- 数据权限过滤:必须企业侧规则;
- Citation Accuracy:必须可验证的引用,不能完全相信模型"我引用了 X";
- 敏感操作的执行确认:永远人工兜底;
- 评估指标本身的可信度:必须人工校准。
方案评审时应该重点询问
- gold set 在哪、谁维护、规模多大?
- 离线评估何时跑?基线版本怎么跟踪?
- CI 卡点的指标和阈值是多少?max_drop 策略?
- 在线采样比例多少?judge 与 chat 模型是否解耦?
- 人工校准频率是多少?kappa 阈值?
- 评估指标出问题时的应急回滚路径?
故障发生时首先检查什么
- 指标矩阵:哪个指标暴跌?快速定位是检索层还是生成层;
- 指标突变时间点:与最近一次 prompt / embedding / chunking / 索引版本变更对齐;
- trace 抽样:低分样本 vs 高分样本的 retrievedContexts 差异;
- gold set 状态:是否最近更新过?是否标注者换了?
- judge 模型:是否被上游悄悄切换?是否触发 rate limit 改用降级模型?
哪些风险必须提前设计兜底
- Judge 服务不可用:必须不阻塞主链路;
- Context Recall 长期下降:必须自动回滚索引;
- CI 卡点自身漂移:阈值要随基线自动调整或人工复核;
- 评估数据集被污染:必须版本化 + 审计;
- 评估与生产 LLM 耦合:必须解耦并对 judge 单独做 SLO。
11. 今日练习
先独立思考,再翻到末尾看参考答案。
Q1 概念理解题
为什么 RAG 评估必须把"检索层指标"和"生成层指标"分开?为什么只看最终答案准确率不够?
Q2 生产故障分析题
业务方反馈 RAG 上线两周后"答案整体变差",但没有具体 bad case。指标显示:Faithfulness 不变、Context Precision 下降、Context Recall 持平、Answer Correctness 下降。
- 你怀疑根因是什么?
- 第一步排查动作是什么?
- 给出一个不需要换模型的优化方向。
Q3 AI 系统设计题
请设计一个"高风险问题(如:删除账户、转账、解除绑定)"的预发送评估与兜底架构。要求覆盖:触发条件、评估者、降级路径、审计日志、可观测性指标,并解释为什么此场景绝不接受纯 LLM-as-Judge 兜底。
Q4 Java 小实践
用 Spring AI + Kafka + RAGAS Python Sidecar,实现一个"上传 PDF → 入库 → 自动跑评估"的最小可用闭环。
具体要求:
- 上传接口接收 PDF,写 Kafka
rag.documentstopic; - 消费者读取后调用 Spring AI 的
PagePdfDocumentReader→ 自定义 Chunking → 写入 Milvus / Qdrant; - 入库完成后发
rag.eval.requesttopic; - 评估 Job 收到后调用 Python Sidecar 跑 RAGAS,并把结果写数据库 + Prometheus gauge。
12. 今日知识卡片
不超过 200 字。
- 核心原理:RAG 评估体系把"答案对不对"拆成"Recall、Precision、Faithfulness、Relevancy、Correctness"五个独立指标,组成诊断矩阵,让 Chunking/Embedding/Rerank/Prompt/Model 五个变量可以精准调优。
- 工程实践建议 :离线 RAGAS + 在线 LangFuse/OpenLLMetry + 每周人工核验,CI 卡点必须用
max_drop防慢漂移,judge 模型与 chat 模型必须解耦并季度校准。 - 主要风险:LLM-as-Judge 自评系统性偏高 + judge 与 chat 耦合会污染数据 + 在线评估一旦规模化极易吞噬成本。
- 架构选型判断:超过 1 个月对外服务的 RAG 必建评估体系;评估成本不超过业务价值 5%;评估指标永远不与生产 LLM 同源。
参考答案
Q1 参考答案
RAG 由"检索"和"生成"两个子系统组成,三类故障模式独立:
- 检索漏召导致答案无依据;
- 检索到位但模型胡编导致 faithfulness 低;
- 检索和生成都正常但答案飘走导致 relevancy 低。
只看最终答案准确率会把这些耦合在同一段对话里,无法定位根因。
正确的设计:检索层(Context Precision / Recall / NDCG@10)+ 生成层(Faithfulness / Answer Relevancy / Answer Correctness)+ 端到端评估,组成诊断矩阵。
Q2 参考答案
根因:检索噪声增加。新加入的文档让 top-k 里"看起来相关但实际与答案无关"的 chunk 比例上升,模型 Faithfulness 没掉(因为它可以忽略这些 chunk 并选择另一种说法),但多份不同 chunk 给出不同结论导致 Answer Correctness 下降。
第一步排查:抽样比对"修改前"和"修改后"的两份 trace,看 retrievedContexts 中"有用 chunk"占比是否显著下降;如果答案正确但 retrievedContexts 噪声大,根因就是 retriever / top-k。
不换模型的优化:
- 降 top-k:从 10 降到 6,让噪声比例下降;
- 加 Metadata 过滤:把同一文档的多个 chunk 合并送入;
- 加 Rerank:让真正相关的 chunk 往上排;
- 强化 Prompt:要求模型只在被显式 support 的结论上回答。
Q3 参考答案
核心架构:
#mermaid-svg-Odn4OPhaUuzxIUzc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Odn4OPhaUuzxIUzc .error-icon{fill:#552222;}#mermaid-svg-Odn4OPhaUuzxIUzc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Odn4OPhaUuzxIUzc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Odn4OPhaUuzxIUzc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Odn4OPhaUuzxIUzc .marker.cross{stroke:#333333;}#mermaid-svg-Odn4OPhaUuzxIUzc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Odn4OPhaUuzxIUzc p{margin:0;}#mermaid-svg-Odn4OPhaUuzxIUzc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster-label text{fill:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster-label span{color:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster-label span p{background-color:transparent;}#mermaid-svg-Odn4OPhaUuzxIUzc .label text,#mermaid-svg-Odn4OPhaUuzxIUzc span{fill:#333;color:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc .node rect,#mermaid-svg-Odn4OPhaUuzxIUzc .node circle,#mermaid-svg-Odn4OPhaUuzxIUzc .node ellipse,#mermaid-svg-Odn4OPhaUuzxIUzc .node polygon,#mermaid-svg-Odn4OPhaUuzxIUzc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Odn4OPhaUuzxIUzc .rough-node .label text,#mermaid-svg-Odn4OPhaUuzxIUzc .node .label text,#mermaid-svg-Odn4OPhaUuzxIUzc .image-shape .label,#mermaid-svg-Odn4OPhaUuzxIUzc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Odn4OPhaUuzxIUzc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Odn4OPhaUuzxIUzc .rough-node .label,#mermaid-svg-Odn4OPhaUuzxIUzc .node .label,#mermaid-svg-Odn4OPhaUuzxIUzc .image-shape .label,#mermaid-svg-Odn4OPhaUuzxIUzc .icon-shape .label{text-align:center;}#mermaid-svg-Odn4OPhaUuzxIUzc .node.clickable{cursor:pointer;}#mermaid-svg-Odn4OPhaUuzxIUzc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Odn4OPhaUuzxIUzc .arrowheadPath{fill:#333333;}#mermaid-svg-Odn4OPhaUuzxIUzc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Odn4OPhaUuzxIUzc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Odn4OPhaUuzxIUzc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Odn4OPhaUuzxIUzc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Odn4OPhaUuzxIUzc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Odn4OPhaUuzxIUzc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster text{fill:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc .cluster span{color:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Odn4OPhaUuzxIUzc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Odn4OPhaUuzxIUzc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Odn4OPhaUuzxIUzc .icon-shape,#mermaid-svg-Odn4OPhaUuzxIUzc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Odn4OPhaUuzxIUzc .icon-shape p,#mermaid-svg-Odn4OPhaUuzxIUzc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Odn4OPhaUuzxIUzc .icon-shape .label rect,#mermaid-svg-Odn4OPhaUuzxIUzc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Odn4OPhaUuzxIUzc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Odn4OPhaUuzxIUzc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Odn4OPhaUuzxIUzc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 高风险
普通
通过
不通过
用户问
规则层:
关键词 + regex + 意图分类
进入预发送门
常规链路
调用小 judge LLM
Chat 生成
人工工单/拒绝
审计日志
含 traceId, promptVersion, modelId
异步评估流
触发条件:基于规则层(关键词 + 意图分类 + 用户角色权限)的"高风险标签"决定是否走预发送门,避免所有问题都跑评估(成本不可控)。
评估者:双裁判------小 judge LLM(GPT-4o-mini)做初筛 + 业务规则(白名单敏感字段)做最终决策。LLM 不直接拒绝,只输出"风险分"。
降级路径:
- judge 不可用 → 保守拒绝,要求人工确认;
- judge 触发 rate limit → 退化为纯规则 + 保守拒绝;
- chat 触发 rate limit → 返回业务降级文案 + 引导人工工单。
审计日志:必须包含 userId、tenantId、question、retrievedContexts、judge score、judge model id、final decision、human reviewer id(如果有人工介入)。
可观测性指标 :rag.sensitive.eval.precision、rag.sensitive.eval.recall(必须有人工标注子集)、rag.sensitive.override_rate、rag.sensitive.human_takeover_rate。
为什么不能纯 LLM 兜底:
- judge 与 chat 耦合 → 自评系统性偏高;
- 关键的"删除/转账/解除"操作即使 0.1% 误判率乘以巨大访问量也是不可接受的损失;
- 没有法规上的"可解释"路径,纯 LLM 拒绝无法提供合规证据;
- LLM 不应承担"执行确认"------这从来都是架构层面的人工兜底责任。
Q4 参考答案(要点)
java
// 1. Producer
@PostMapping("/upload")
public void upload(@RequestParam MultipartFile pdf) {
kafka.send("rag.documents", new DocumentEvent(
UUID.randomUUID().toString(), pdf.getOriginalFilename(),
tenantContext.current(), Instant.now()
));
}
// 2. Consumer
@KafkaListener(topics = "rag.documents")
public void onDoc(DocumentEvent evt) {
Resource pdf = storage.get(evt.fileName());
List<Document> docs = new PagePdfDocumentReader(pdf).read();
docs = new CustomSectionChunker().apply(docs);
vectorStore.accept(docs);
kafka.send("rag.eval.request", new EvalRequest(evt.docId()));
}
// 3. Eval Job
@KafkaListener(topics = "rag.eval.request")
public void onEval(EvalRequest req) {
List<EvalSample> samples = datasetStore.holdoutFor(req.docId());
EvalResult result = ragasClient.evaluate(samples);
db.save(req.docId(), result);
meterRegistry.gauge("rag.eval.faithfulness", result.faithfulness());
meterRegistry.gauge("rag.eval.context_precision", result.contextPrecision());
}
关键工程细节:
- Kafka topic 用分层避免循环:
rag.documents、rag.eval.request; - vectorStore 写入与评估异步,避免新文档写入阻塞上传链路;
- Prometheus gauge 用 docId / 时段打 label,便于追溯单次入库后评估结果;
- 评估 Sidecar 用 jvm 外部服务,Java 只做调度和指标归档,避免把 Python 嵌入主进程。
学习资料引用:本文涉及的 RAGAS、DeepEval、TruLens、LangFuse 等信息综合自各项目官网、PyPI、GitHub 与 2026 年上半年的工程实践文。请以具体使用时的
pip show ragas/pip show langfuse与官方最新文档为准;本文不构成任何生产 SLA 承诺或商业背书。