Kibana Evals 评估器模式详解:从 CODE 断言到 LLM-as-Judge 与 RAG 评估的完整实践
【免费下载链接】kibanaYour window into all of your data项目地址: https://gitcode.com/GitHub_Trending/ki/kibana
Kibana 仓库内置了一套基于@kbn/evals包的 LLM 评估(Evals)体系,用于对 Agent、RAG 检索、工具调用链等 AI 功能进行系统化打分。本文以技能参考文档 evaluator-patterns.md 为主体,完整讲解其中五种评估器模式——CODE 确定性评估器、LLM-as-Judge 准则评估器、工具调用评估器、RAG 检索评估器与基于 Trace 的评估器,并结合仓库中真实存在的kbn-evals-suite-workflows评估套件源码,说明这些模式在 Kibana 项目内的落地方式,帮助你在编写新的 eval spec 时正确选择与组合评估器。
评估体系背景:runExperiment 与两种评估器入口
在 Kibana 的 eval 规范文件中,评估器不是孤立存在的,而是与 dataset(数据集)和 task(任务函数)一起,通过executorClient.runExperiment提交给执行器。评估器的职责是:拿到每个 example 的input(输入)、output(任务实际输出)、expected(期望输出)和metadata(示例级元数据),返回一个带score(0/1 或数值分)、可选label(PASS/FAIL)和explanation(解释文本)的结果。
runExperiment接受评估器的入口有两种(见配套文档 SKILL.md 的 “Evaluators” 一节):
- 内联数组—— 直接传入评估器对象数组,适合简单的套件;
selectEvaluators—— 类型化的包装器,强制Example/TaskOutput泛型,为input、output、expected、metadata提供完整的 TypeScript 类型安全。
下文各节即参考文档中给出的各类型评估器的扩展示例,均来自真实 eval 套件。
CODE 评估器:用 selectEvaluators 获得类型安全的确定性检查
kind: 'CODE'的评估器不调用 LLM,因此速度快、结果确定、成本为零,适合二值断言(“有没有返回文档”“URL 是否合法”“是否包含必备词”等)。参考文档从llm-tasks套件提取的完整示例如下:
import { selectEvaluators, type Example, type TaskOutput } from '@kbn/evals'; type MyExample = Example & { input: { searchTerm: string; products?: string[] }; metadata?: { minDocs?: number; requiredTerms?: string[] }; }; type MyTaskOutput = TaskOutput & { success: boolean; documents: Array<{ title: string; url: string; content: string }>; }; await executorClient.runExperiment( { datasets: [dataset], task }, selectEvaluators<MyExample, MyTaskOutput>([ { name: 'NonEmptyDocuments', kind: 'CODE', evaluate: async ({ output, metadata }) => { const minDocs = typeof metadata?.minDocs === 'number' ? metadata.minDocs : 1; const count = output?.documents?.length ?? 0; return { score: count >= minDocs ? 1 : 0, metadata: { minDocs, count } }; }, }, { name: 'RequiredTermsInContent', kind: 'CODE', evaluate: async ({ output, metadata }) => { const requiredTerms = metadata?.requiredTerms ?? []; if (requiredTerms.length === 0) return { score: 1 }; const text = (output?.documents ?? []) .slice(0, 3) .map((d) => `${d.title}\n${d.content}`) .join('\n'); const ok = requiredTerms.every((term) => text.toLowerCase().includes(term.toLowerCase()) ); return { score: ok ? 1 : 0, metadata: { requiredTerms } }; }, }, { name: 'HasValidUrl', kind: 'CODE', evaluate: async ({ output }) => { const urls = (output?.documents ?? []).map((d) => d.url); const ok = urls.some((u) => typeof u === 'string' && u.startsWith('https://')); return { score: ok ? 1 : 0, metadata: { urls: urls.slice(0, 3) } }; }, }, ]) );这个示例体现了参考文档总结的三条要点:
selectEvaluators<ExampleType, OutputType>([...])提供类型安全。通过把Example与TaskOutput两个泛型参数化,evaluate回调中的output.documents、metadata.minDocs等字段访问都受 TypeScript 约束,字段拼错或类型不符会在编译期暴露;kind: 'CODE'意味着无 LLM 调用,快速且确定,适合放在每个 example 上高频运行;- 在返回结果里带上
metadata有助于调试。如示例中的{ minDocs, count }、{ urls: urls.slice(0, 3) },当某个 example 得分 0 时,报告里可以直接看到当时的阈值与命中数,而不必重放任务。
编写 CODE 评估器时值得注意的防御式写法:metadata?.minDocs用typeof === 'number'校验后再回退默认值 1;output?.documents?.length ?? 0处理任务输出缺失的情况;requiredTerms.length === 0时直接给 1 分——“没有断言就不扣分”,这与后文 LLM 评估器中 “No criteria specified” 直接 PASS 的约定是一致的。
LLM-as-Judge 准则评估器:evaluators.criteria
对于无法用确定代码判定的主观质量(“回答是否正确识别了风险最高的用户”),参考文档给出的是 LLM-as-Judge 模式:通过evaluators.criteria([...])把一组自然语言准则交给 judge 模型逐条打分。从security-solution-evals套件提取的用法:
const mainCriteriaResult = await evaluators .criteria([ 'The response correctly identifies the top users.', 'The response includes risk scores for each user.', 'The response includes risk levels for each user.', ]) .evaluate({ input, output, expected, metadata });关键语义:每条 criterion 独立评估,judge 对每条准则返回 0 或 1 的分数及解释。准则应当写成可验证的陈述句(“包含了 X”“按 Y 排序”),而不是模糊形容词,否则 judge 的打分方差会变大。
把 criteria 包装成可复用评估器
参考文档进一步示范了把criteria封装为具名评估器的惯用法——准则本身存放在数据集的expected.criteria中,由评估器在运行时取出:
function createCriteriaEvaluator({ evaluators }: { evaluators: DefaultEvaluators }) { return { name: 'Criteria', kind: 'LLM' as const, evaluate: async ({ input, output, expected, metadata }: { input: MyExample['input']; output: MyTaskOutput; expected: MyExample['output']; metadata: MyExample['metadata']; }) => { const criteria = expected.criteria ?? []; if (criteria.length === 0) { return { score: 1, label: 'PASS', explanation: 'No criteria specified.' }; } return evaluators.criteria(criteria).evaluate({ input, expected, output, metadata }); }, }; }这种工厂模式(createXxxEvaluator({ evaluators })返回{ name, kind, evaluate }对象)的意义在于:
- 评估器获得
name与kind: 'LLM'标记,最终报告中可按名称聚合得分; - 判定逻辑与准则数据分离——准则写在数据集的
expected里,同一评估器可服务整个数据集的不同 example; - 空准则短路返回 PASS,避免对无期望值的示例浪费 judge 调用。
工具调用评估器:验证 Agent 的轨迹而不仅是回答
对 Agent 类功能,光看最终文本不够,还要验证它是否调用了正确的工具。参考文档从security-solution-evals提取的createToolCallsEvaluator实现了两层断言:
function createToolCallsEvaluator({ evaluators }: { evaluators: DefaultEvaluators }) { return { name: 'ToolCalls', kind: 'LLM' as const, evaluate: async ({ input, output, expected, metadata }) => { const toolCalls = expected.toolCalls ?? []; const steps = output.steps ?? []; if (toolCalls.length === 0) { return { score: 1, label: 'PASS', explanation: 'No tool call assertions.' }; } const results = []; for (const assertion of toolCalls) { const called = steps.some( (s) => s.type === 'tool_call' && s.tool_id === assertion.id ); if (!called) { results.push({ score: 0, label: 'FAIL', explanation: `Tool "${assertion.id}" was not called.`, }); continue; } if (assertion.criteria?.length) { const criteriaResult = await evaluators .criteria(assertion.criteria) .evaluate({ input, expected: { criteria: assertion.criteria }, output, metadata }); results.push(criteriaResult); } else { results.push({ score: 1, label: 'PASS', explanation: `Tool "${assertion.id}" called.` }); } } const allPassed = results.every((r) => r.label === 'PASS'); const scores = results.map((r) => r.score ?? 0); const avg = scores.reduce((a, b) => a + b, 0) / scores.length; return { score: allPassed ? avg : 0, label: allPassed ? 'PASS' : 'FAIL', explanation: results.map((r) => r.explanation).join(' '), }; }, }; }实现逻辑值得逐点理解:
- 轨迹级检查(CODE 部分):从任务输出的
steps里筛选type === 'tool_call'且tool_id匹配断言的步骤,确认工具确实被调用过; - 质量级检查(LLM 部分):若断言附带
criteria(如“ES|QL 查询应按 risk score 降序”),则再委托给evaluators.criteria让 judge 评估工具调用的参数或结果是否符合预期; - 聚合规则:所有断言全部 PASS 时得分取各断言分的平均值,任何一条 FAIL 则整体 0 分——这是一种“一票否决”的严格聚合;
- 无断言短路:
toolCalls为空时直接 PASS,保持与 criteria 评估器一致的约定。
配套的数据集写法把断言声明在output(expected)中:
{ input: { question: 'Which users have the highest risk scores?' }, output: { criteria: [ 'Return 10 users with the highest risk scores.', 'Return the risk levels of those users.', ], toolCalls: [ { id: 'security.entity_analytics.risk_score', criteria: ['The ES|QL query should sort by risk score descending.'], }, ], }, }可以看到,一条 example 同时驱动了主答案准则(criteria)与轨迹断言(toolCalls),两者各自独立评估、互不干扰。
RAG 评估器:Precision@K / Recall@K / F1@K 与 ground truth
针对“检索质量”这一 RAG 场景,@kbn/evals提供了三个指标评估器及一个工厂:
import { createPrecisionAtKEvaluator, createRecallAtKEvaluator, createF1AtKEvaluator, createRagEvaluators, } from '@kbn/evals'; import type { GroundTruth, RetrievedDoc } from '@kbn/evals';createRagEvaluators工厂一次性创建三者,核心是把“任务原始输出”与“数据集中的 ground truth”解耦为两个抽取函数:
const ragEvals = createRagEvaluators({ k: 5, extractRetrievedDocs: (output) => output.documents.map((d) => ({ id: d.id, content: d.content })), extractGroundTruth: (expected) => expected.groundTruth, });k: 5表示只考察检索结果的前 5 条;extractRetrievedDocs负责从任务输出中抽出{ id, content }形式的检索文档列表;extractGroundTruth负责从expected中取出 ground truth 映射。
数据集中的 ground truth 采用“文档 ID → 相关性分数”的映射形式:
{ input: { question: 'How do I set up payments?' }, output: { expected: 'You can start accepting payments using Wix Payments...', groundTruth: { knowledge_base: { 'doc_hash_abc123': 1, 'doc_hash_def456': 1, }, }, }, }即每个 example 预先声明“哪些文档对本 query 是相关的(值为 1)”,评估器据此计算前 K 条检索结果命中的精度、召回与 F1。这套设计与经典 IR 评估一致,属于确定性的 CODE 型评估,不依赖 judge 模型。
Trace 评估器:从追踪集群读取 token 与延迟指标
参考文档的第六节介绍了evaluators.traceBasedEvaluators中预置的五个数值型评估器:
const { inputTokens, outputTokens, cachedTokens, toolCalls, latency } = evaluators.traceBasedEvaluators;它们按当前 example 的 trace ID 到 tracing ES 集群查询 span 并提取指标:
| 评估器 | 含义 | 数据来源 |
|---|---|---|
inputTokens | 输入 token 数 | gen_ai.usage.*span 属性 |
outputTokens | 输出 token 数 | gen_ai.usage.*span 属性 |
cachedTokens | 缓存命中 token 数 | gen_ai.usage.*span 属性 |
toolCalls | 工具调用 span 数量 | tool call spans |
latency | 总 span 时长(秒) | span duration |
运行前提:EDOT(Elastic Developer Observability Testing,可观测性追踪链路)必须在运行中,且TRACING_ES_URL指向一个实际接收 traces 的 ES 实例。若不满足该前提,这类评估器拿不到数据,本地迭代时应以 CODE 评估器为主。
组合多种评估器类型
真实的 eval 套件通常同时提交 CODE 与 LLM 两类评估器,由执行器对每个 example 逐个运行并在最终报告中聚合分数。参考文档给出的组合示例:
await executorClient.runExperiment( { datasets: [dataset], task }, [ createCriteriaEvaluator({ evaluators }), createToolCallsEvaluator({ evaluators }), { name: 'HasResponse', kind: 'CODE', evaluate: async ({ output }) => ({ score: output?.messages?.length > 0 ? 1 : 0, }), }, ] );该组合体现了参考文档推荐的分工策略:用低成本的 CODE 评估器(HasResponse)做基础健康检查,用 LLM 评估器(Criteria、ToolCalls)做质量判断,二者互不替代——LLM judge 不应承担“有没有输出”这种确定性问题,CODE 断言也不应试图判断语义质量。
仓库实证:kbn-evals-suite-workflows 套件如何应用这些模式
参考文档中的模式并非孤立示例,仓库中的kbn-evals-suite-workflows评估套件展示了同样的落地方式。
在 workflow_creation.spec.ts 中,spec 文件从@kbn/evals导入selectEvaluators与EvaluationDataset类型,通过自定义的evaluatefixture 扩展出evaluateCreateDataset,在 fixture 内部调用executorClient.runExperiment({ datasets: [dataset], task }, ...)提交实验——与参考文档 “Combining Multiple Evaluator Types” 一节的骨架完全一致。该套件的 task 会调用chatClient.converse取得messages、steps、errors,再从中提取工作流 YAML 作为任务输出,供评估器断言。
同目录的 evaluators.ts 则展示了评估器工厂在实际套件中的工程化扩展,比参考文档更进一步:
- 它从
@kbn/evals导入DefaultEvaluators、Evaluator、Example以及getToolCallSteps、createTrajectoryEvaluator,与参考文档中的evaluators.criteria、selectEvaluators出自同一包; - 定义了
skipInfraErrors高阶包装器:当任务输出中捕获到超时、ECONNREFUSED、5xx 等基础设施错误特征(见INFRA_ERROR_PATTERN)时,让被包装的评估器返回score: null, label: 'N/A',把“环境问题”与“模型质量问题”区分开,避免环境抖动拉低均分(源码注释明确说明这是 “not a model quality issue”); - 定义了
skipNegativeCases:对metadata.category === 'negative'的反向用例(模型应当拒绝的请求)返回 N/A,避免无意义的负样本污染指标; - 提供了
createCriteriaEvaluator、createValidationPassEvaluator、createToolTrajectoryEvaluator、createEfficiencyEvaluator等一组工厂,与参考文档 “Wrapping Criteria in a Reusable Evaluator” 的工厂模式一脉相承。
该套件的 spec 中还能看到这些包装器被组合使用,例如skipInfraErrors(skipNegativeCases(evaluator))这样的洋葱式封装——这正是参考文档模式在真实套件中被复用的直接证据。
运行与本地迭代
结合 SKILL.md 的 “Running Locally” 一节,编写好包含上述评估器的 spec 后可用scripts/evals入口脚本运行:
# 完整交互流程 node scripts/evals start # 指定被测模型与 judge 模型 node scripts/evals start --model <connector-id> --judge <connector-id> # 只跑匹配的单个测试(本地迭代推荐) node scripts/evals start --grep "my test name" # 服务已在运行时直接执行 node scripts/evals run --model <connector-id> --judge <connector-id>脚本入口位于 scripts/evals.js。其中--judge参数对应 LLM-as-Judge 评估器所使用的 judge 连接器——使用evaluators.criteria的套件需要单独指定评分模型;trace 评估器则要求 EDOT 与TRACING_ES_URL就绪。
小结:如何选择评估器
| 场景 | 推荐模式 | 类型 | 代价 |
|---|---|---|---|
| 输出结构断言(非空、URL 合法、含必备词) | CODE 评估器 +selectEvaluators | CODE | 低 |
| 语义质量(回答是否覆盖要点) | evaluators.criteria或包装成可复用评估器 | LLM | 中(每次 judge 调用) |
| Agent 轨迹(是否调用了指定工具/参数正确) | createToolCallsEvaluator模式:steps 检查 + criteria 复核 | CODE + LLM | 中 |
| 检索质量(有 ground truth 文档) | createRagEvaluators(Precision/Recall/F1@K) | CODE | 低 |
| Token 消耗、工具调用数、端到端延迟 | evaluators.traceBasedEvaluators | CODE(依赖 EDOT) | 低 |
参考文档给出的核心方法论可以概括为:能用确定代码判定的绝不动用 LLM judge;用 LLM judge 时把准则写成逐条独立、可验证的陈述句;通过selectEvaluators<ExampleType, OutputType>让类型系统在编译期兜底;并在评估器返回值中携带metadata/explanation,让每一次 0 分都能追溯到具体证据。掌握了这五点,即可在 Kibana 的 evals 体系中编写可维护、可调试的评估套件。
【免费下载链接】kibanaYour window into all of your data项目地址: https://gitcode.com/GitHub_Trending/ki/kibana
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考