作者:来自 Elastic Jeffrey Rengifo
了解你的 RAG agent 是否已准备好投入生产。仅使用 Elasticsearch Workflows 和两个 Claude 模型,从正确性(correctness)、忠实性(faithfulness)和检索质量(retrieval quality)三个维度对其进行评分。
Agent Builder 现已正式发布。立即开始你的 Elastic Cloud Trial,并查看 Agent Builder 文档。
在 Elasticsearch Workflows 内部构建完整的 RAG 评测流水线:无需外部评测框架,也无需额外基础设施。一个小模型基于你的知识库回答问题;一个更强的模型则从正确性(correctness)、忠实性(faithfulness)和上下文相关性(context relevance)三个维度对每个回答进行评分。
在一个包含 35 个 HotpotQA 测试用例的测试中,Claude Haiku 4.5 获得了 0.74 的正确性评分和 0.90 的忠实性评分,足以精确定位 retrieval 或推理过程究竟在哪一步出现问题。知识库、评判标准、workflow 执行过程以及评分结果全部保存在同一个系统中。
前提条件
-
一个 Elastic Cloud 集群,或自托管的 Elasticsearch / Kibana 9.4+。如果还没有,可以免费试用。
-
Python 3.13
什么是 LLM-as-a-Judge?
LLM-as-a-Judge 使用一个更强大的语言模型,自动对较弱模型生成的结果进行评分,从而替代一致性较差且难以扩展的人工评审。
手工评估一个 RAG 系统对于少量问题来说是可行的,但无法扩展。如果你修改了检索策略、提示词(prompt)或模型,就需要重新检查所有答案。这不仅耗时,而且不同评审人员之间的评分标准也往往不一致。
LLM-as-a-Judge 模式通过使用一个更强大的语言模型来评估较弱模型的输出,从而解决这一问题。其核心思想非常简单:如果一个语言模型能够可靠地判断一个答案是否正确,那么你就可以在几分钟内自动完成数百个测试用例的质量评估。
在本文中,整个流水线包含两个阶段:
-
阶段 1(回答):Claude Haiku 4.5 接收问题以及从知识库中检索出的相关内容(passages),生成回答。这是需要进行评测的模型。
-
阶段 2(评判):Claude Sonnet 4.6 接收相同的问题、标准答案(ground-truth answer,即已知正确答案)、候选答案(阶段 1 的输出)以及检索到的 passages,并从三个维度对回答进行评分。
RAG 的三个评测指标:正确性(correctness)、忠实性(faithfulness)和上下文相关性(context relevance)
| 指标 | 衡量内容 | 低分意味着什么 |
|---|---|---|
| correctness(正确性) | 候选答案是否与标准答案一致? | agent 回答错误。 |
| faithfulness(忠实性) | 回答是否基于检索到的 passages? | agent 产生了幻觉(编造了内容)。 |
| context_relevance(上下文相关性) | 检索到的 passages 是否与问题相关? | retrieval 检索到了错误的文档。 |
这三个指标结合起来,能够告诉你问题出在哪里。如果 correctness 很高,而 context_relevance 很低,这就是一个危险信号:尽管检索效果不佳,模型仍然回答正确,很可能是依赖自身训练数据而不是检索结果完成了回答。对于像 Wikipedia 这样的公开知识,这种方式通常没有问题;但对于模型从未见过的私有数据,这种做法就会失效。
另一种互补的方法是结合 Elasticsearch 使用 Ragas 指标,从基于框架(framework-based)的角度探索这些评测维度。
如何加载并索引 HotpotQA 数据集
该流水线使用 HotpotQA 的 distractor 配置:多跳(multi-hop)问题,每个问题都配有 10 个上下文段落,并存储在两个 Elasticsearch 索引中。
你可以参考本文配套的完整 notebook,其中包含环境配置、连接以及所有辅助代码。
完成本教程需要以下信息:
-
ELASTICSEARCH_URL -
ELASTICSEARCH_API_KEY(可按照连接信息指南获取) -
KIBANA_URL(可在Kibana 访问页面找到)
该流水线使用在 Kibana 中配置好的两个 AI connectors:
-
Anthropic Claude Haiku 4.5:作为回答模型(体积小、成本低、速度快)。
-
Anthropic Claude Sonnet 4.6:作为评判模型(能力更强)。
整个流水线要回答的问题是:
Haiku 在这项任务上的表现是否足够好,可以用于生产环境?/Is Haiku good enough on this task to be used in production?
我们使用 HotpotQA 的 distractor 配置。每个问题都包含 10 个上下文段落:其中两个支持正确答案,另外八个是干扰项(distractors)。这些段落会被索引到知识库中,供第一阶段(Stage 1)进行 retrieval。
这些问题属于多跳(multi-hop)问题,也就是说,回答一个问题需要综合来自两个不同段落的信息。
我们会抽取部分问题作为样本,将其上下文段落展开后索引到知识库索引中,并将问题—答案对存储到单独的评判列表(judgement list)中。最终数据会存储到两个 Elasticsearch 索引中:
知识库:上下文段落
hotpot-knowledge-base 保存回答模型进行 retrieval 时使用的上下文段落。每个 document 都包含一个 title 和一个 passage:
`{"title": "Ed Wood (film)", "passage": "Ed Wood is a 1994 American biographical period comedy-drama film..."}`AI写代码
passage 字段会复制到 semantic_content(映射为 semantic_text),这样第一阶段(Stage 1)就可以使用自然语言 query 检索相关段落,而无需手动管理 embeddings。
Elasticsearch 会自动为已索引的段落生成 embeddings,并在执行 语义搜索(semantic search) 时自动为查询生成 embeddings。
`
1. INDEX_NAME = "hotpot-knowledge-base"
3. if es_client.indices.exists(index=INDEX_NAME):
4. es_client.indices.delete(index=INDEX_NAME)
6. es_client.indices.create(
7. index=INDEX_NAME,
8. mappings={
9. "properties": {
10. "title": {"type": "keyword"},
11. "passage": {
12. "type": "text",
13. "copy_to": "semantic_content",
14. },
15. "semantic_content": {
16. "type": "semantic_text",
17. "inference_id": ".jina-embeddings-v5-text-small",
18. },
19. }
20. },
21. )
22. print(f"Created index: {INDEX_NAME}")
`AI写代码
评判列表:问题与标准答案
hotpot-judgement-list 保存评测用例。每个 document 都包含一个 question 和一个 answer(标准答案,ground truth):
`{"question": "Were Scott Derrickson and Ed Wood of the same nationality?", "answer": "yes"}`AI写代码
answer 字段包含正确答案。第二阶段(Stage 2)的 judge 会将回答模型的输出与该答案进行比较。
Elasticsearch Workflow 如何运行评测
这篇使用 Elastic Workflows 构建自动化的介绍文章详细介绍了 trigger、step 和数据流的核心概念。
Workflow 定义
该 workflow 使用 YAML 定义,并通过 Workflows API 上传(Elastic 9.4+)。以下是完整定义:
`
1. WORKFLOW_YAML = """
2. name: agent_accuracy_eval
3. description: >
4. Batch evaluation. Loads the judgement list, iterates each case with
5. a foreach, runs stage 1 (RAG answer with Haiku) and stage 2 (LLM
6. judge with Sonnet), and indexes every score into the eval results
7. index.
8. enabled: true
10. consts:
11. kbIndex: hotpot-knowledge-base
12. judgeIndex: hotpot-judgement-list
13. resultsIndex: eval-results
15. triggers:
16. - type: manual
18. steps:
19. # Load the judgement list (all cases we want to evaluate).
20. - name: load_cases
21. type: elasticsearch.search
22. with:
23. index: "{{ consts.judgeIndex }}"
24. query:
25. match_all: {}
26. size: 35
28. # One iteration = one evaluation.
29. - name: eval_loop
30. type: foreach
31. foreach: "{{ steps.load_cases.output.hits.hits }}"
32. steps:
33. - name: retrieve
34. type: elasticsearch.search
35. with:
36. index: "{{ consts.kbIndex }}"
37. query:
38. semantic:
39. field: semantic_content
40. query: "{{ foreach.item._source.question }}"
41. size: 4
43. - name: agent_answer
44. type: ai.prompt
45. with:
46. connector-id: Anthropic-Claude-Haiku-4-5
47. prompt: >
48. You are a Wikipedia QA assistant. Answer the question
49. using ONLY the passages provided. Keep the answer short
50. (one line). If the passages do not contain the answer,
51. reply "unknown".
53. Question: {{ foreach.item._source.question }}
55. Passages:
56. 1. {{ steps.retrieve.output.hits.hits[0]._source.passage }}
57. 2. {{ steps.retrieve.output.hits.hits[1]._source.passage }}
58. 3. {{ steps.retrieve.output.hits.hits[2]._source.passage }}
59. 4. {{ steps.retrieve.output.hits.hits[3]._source.passage }}
61. - name: judge
62. type: ai.prompt
63. with:
64. connector-id: Anthropic-Claude-Sonnet-4-6
65. prompt: >
66. You are a STRICT evaluator. Score the candidate answer
67. against the ground truth on three axes. Each score MUST
68. be exactly one of these three values: 0.0, 0.5, or 1.0.
69. Do not return any other number.
71. correctness:
72. 1.0 = candidate contains the ground truth answer exactly
73. or an unambiguous synonym, and nothing factually wrong.
74. 0.5 = partially correct (one side of a multi-hop right,
75. or mostly right with minor noise).
76. 0.0 = wrong, missing, or contradicts the ground truth.
78. faithfulness:
79. 1.0 = every factual claim is supported by the passages.
80. 0.5 = mostly supported, one minor unsupported claim.
81. 0.0 = contains at least one unsupported claim.
83. context_relevance:
84. 1.0 = the passages contain enough to answer the question.
85. 0.5 = partial coverage (one hop covered, the other missing).
86. 0.0 = passages do not cover the answer.
88. Be harsh. If in doubt between two scores, pick the lower one.
90. Question: {{ foreach.item._source.question }}
91. Ground truth: {{ foreach.item._source.answer }}
92. Candidate: {{ steps.agent_answer.output.content }}
93. Passages:
94. 1. {{ steps.retrieve.output.hits.hits[0]._source.passage }}
95. 2. {{ steps.retrieve.output.hits.hits[1]._source.passage }}
96. 3. {{ steps.retrieve.output.hits.hits[2]._source.passage }}
97. 4. {{ steps.retrieve.output.hits.hits[3]._source.passage }}
98. schema:
99. type: object
100. properties:
101. correctness:
102. type: number
103. minimum: 0
104. maximum: 1
105. faithfulness:
106. type: number
107. minimum: 0
108. maximum: 1
109. context_relevance:
110. type: number
111. minimum: 0
112. maximum: 1
113. required:
114. - correctness
115. - faithfulness
116. - context_relevance
118. # Persist each scored case so the notebook can query it later.
119. - name: save
120. type: elasticsearch.index
121. with:
122. index: "{{ consts.resultsIndex }}"
123. document:
124. qid: "{{ foreach.item._source.qid }}"
125. question: "{{ foreach.item._source.question }}"
126. ground_truth: "{{ foreach.item._source.answer }}"
127. candidate: "{{ steps.agent_answer.output.content }}"
128. correctness: "{{ steps.judge.output.content.correctness }}"
129. faithfulness: "{{ steps.judge.output.content.faithfulness }}"
130. context_relevance: "{{ steps.judge.output.content.context_relevance }}"
131. """
`AI写代码收起代码块
让我们逐步了解关键部分:
-
consts:用于定义索引名称的命名常量。这样可以保持 YAML 文件简洁,并且方便将同一个 pipeline 指向不同的数据集。 -
load_cases:一个elasticsearch.searchstep,用于从评判列表中获取所有测试用例。 -
eval_loop:一个foreachstep,用于遍历搜索结果。每次迭代都会运行以下四个嵌套 step:-
retrieve:使用问题作为 query,在知识库上运行语义搜索,检索相关内容。 -
agent_answer:一个ai.promptstep,将问题以及检索到的前四个 passages 发送给 Haiku。connector-id字段引用 Kibana AI Connector。 -
judge:另一个ai.promptstep,这次调用 Sonnet。schema代码块强制模型返回一个具有固定字段的 JSON 对象。无需 markdown 代码块,无需 regex,也无需在读取时进行解析。minimum和maximum约束确保评分保持在 0–1 范围内。 -
save:一个 elasticsearch.index step,用于将每个评分后的测试用例写入结果索引。由于 judge 输出已经通过schema解析为 JSON,因此可以直接引用各个字段:{{ steps.judge.output.content.correctness }}。
-
上传并运行 workflow
Elastic 9.4 提供了用于 workflow 的 REST API。我们使用三个端点:
-
POST /api/workflows:根据 YAML 创建 workflow。 -
POST /api/workflows/{id}/run:启动一次执行。 -
GET /api/workflows/executions/{id}:轮询查询,直到执行完成。
对于 35 个测试用例,该 workflow 需要几分钟完成。每次迭代都会执行一次语义搜索、一次用于生成答案的大语言模型(LLM)调用、另一次用于评判的大语言模型调用,以及一次索引操作。
如何读取和解读 RAG 评测结果
每个评分后的测试用例已经作为一个 document 存储在 eval-results 索引中,并包含类型化的评分字段。无需进行解析:我们只需要查询该索引,并计算每个指标的平均值。
35 个测试用例的聚合 RAG 评测分数
下面的柱状图展示了 35 个测试用例中每个指标的平均分(使用 Haiku 作为回答模型):
在这次使用 Claude Haiku 4.5 作为回答模型的评测中,结果如下:
-
correctness:0.74:大约有 70% 的问题回答正确。对于多跳 Wikipedia 问题来说,这是一个合理的表现,但仍有提升空间。
-
faithfulness:0.90:当 Haiku 能够回答时,它能够保持基于 passages 生成答案。幻觉并不是这里的主要问题。
-
context_relevance:0.86:大多数情况下,语义检索能够找到相关 passages,但一些多跳问题需要的 passages 并没有出现在前四个检索结果中。
faithfulness(0.90)与 correctness(0.74)之间的差距说明,模型并不是在编造内容,而是在较难的问题上没有找到正确答案。这是 retrieval 或推理问题,而不是幻觉问题。增加检索 passages 的数量,或者针对最困难的问题切换到更强的模型,都是可行的改进方向。
RAG 评测结果对生产环境的意义
我们构建了一个完全运行在 Elasticsearch 内部的完整评测流水线。知识库、评判列表、workflow 执行过程以及评分结果全部存储在同一个系统中。
workflow YAML 可以进行版本控制,可以重复执行,并且可以通过 API 触发。如果你更新了知识库或更换了回答模型,只需要重新运行相同的 pipeline,然后将新的评分结果与旧结果进行比较。
对于这个特定数据集,结果表明 Haiku 具有较高的忠实性,但在多跳问题上并不总能回答正确。是否足够满足你的需求,取决于你的具体使用场景以及你自己的数据。评测 pipeline 的价值在于提供数据,让你能够基于可靠依据做出决策,而不是凭感觉判断。
下一步
接下来,你可以:
-
扩展评判列表:添加符合实际生产流量的领域专属问题。
-
替换回答模型:比较不同模型在同一测试集上的表现。
-
调度 workflow:在新文档被索引后自动运行。
-
构建 Kibana 仪表盘:基于
eval-results索引跟踪质量随时间的变化。
相关文章:
这篇内容对你有多大帮助?
原文:LLM-as-a-Judge in Elasticsearch Workflows: RAG evaluation - Elasticsearch Labs