跳到主要内容

LLM-as-a-Judge 评估:11 种类型与「上下文即真相」

30 秒导读: OpenLIT 用「一个 LLM 当判官(judge)去评审另一个 LLM 的回答」。判官引擎跑在服务端,Python SDK 只是个薄 HTTP 客户端——把 prompt/response 发过去,收回一组结构化评分。判官被提示词硬性约束:Contexts 区块永远是真相,哪怕它和现实矛盾;并且每发现一个 [X evaluation context] 块,就产出恰好一个评估对象。内置 11 种评估类型(幻觉、偏见、毒性……),默认只开前三种。

本章讲清四件事:评估引擎在哪、三条运行入口怎么分、判官提示词为什么这么设计、人工打分怎么进来。规则如何把 trace 属性映射成 context,留给 06-rule-engine


1. 这是什么(零基础也能懂)

一句话定义: LLM-as-a-Judge = 拿一个 LLM 当考官,给另一个 LLM 的输出打分,判断它有没有幻觉、偏见、毒性等问题。

解决什么问题: 你上线了一个聊天机器人,每天几万条回答。你没法人工一条条看「这条有没有胡编」。于是让一个模型自动读每条「问题 + 回答(+ 参考资料)」,输出「幻觉 0.8 分,判定:是」这样的结构化结论,再进面板统计。

它能做什么:

  • 内置 11 种评估维度(幻觉 / 偏见 / 毒性 / 相关性 / 忠实度 / 安全 / 指令遵循 ……)。
  • 三种运行方式:SDK 离线批量、Cron 定时自动、面板手动单条。
  • 支持多家模型厂商当判官(OpenAI / Anthropic / Google / Mistral / Cohere / Groq ……)。
  • 人工也能补一个「反馈分」(用户点了赞还是踩)。

用起来什么样——SDK 侧一行调用,评审逻辑全在服务端跑:

import openlit

# prompt/response 发给 OpenLIT 服务端的判官引擎,收回结构化评分
result = openlit.eval(
prompt="法国的首都是哪?",
response="法国的首都是柏林。",
contexts=["法国的首都是巴黎。"], # 这就是「真相」
)
# result.evaluations → [{type:"Hallucination", score:0.9, verdict:"yes", ...}]

一句话直觉: 把它当成「给 AI 输出配的自动质检员」。质检标准不是质检员脑子里的常识,而是你递给它的那份参考资料(Contexts)——资料说柏林是首都,它就得按柏林判。

本节不碰底层。记住一点:判官是个 LLM,标准是你喂的 Contexts。


2. 顶层全景(它大概怎么转)

怎么读这张图: 左边三种触发方式殊途同归,都汇进服务端的判官引擎;判官用 Vercel AI SDK 调真实厂商模型,吐 JSON,落 ClickHouse。SDK 自己不做任何评审

触发方(客户端 / 定时 / UI) OpenLIT 服务端(判官引擎)
───────────────────────── ──────────────────────────────
openlit.eval() ───────┐
openlit.eval_batch() ─┼─HTTP─▶ /api/evaluation/offline ─┐
openlit.get_eval_types()┘ (runOfflineEvaluation) │
├─▶ runEvaluation()
Cron 定时 ────────────▶ /api/evaluation/auto ────────────┤ (Vercel AI SDK
(autoEvaluate → …ForTrace) │ generateText,
Dashboard 按钮 ───────▶ setEvaluationsForSpanId ──────────┘ temperature=0)
(…ForTrace) │

LLM 厂商(OpenAI/Anthropic/…)
│ 返回 JSON 数组

解析 → ClickHouse eval 表落库

部件一句话职责:

部件干什么在哪个文件
SDK 离线客户端组 payload、POST、解析、重试、终端摘要sdk/python/src/openlit/evals/offline.py:130 run_eval
SDK 结果模型Pydantic 定义 OfflineEvalResult 等类型sdk/python/src/openlit/evals/offline_types.py:43
离线路由鉴权 + 取配置 + 调 runOfflineEvaluationsrc/client/src/app/api/evaluation/offline/route.ts:12
判官引擎(核心)拼系统提示、调厂商模型、解析结果src/client/src/lib/platform/evaluation/run-evaluation.ts:139 runEvaluation
评估编排组装 context、跑评审、落库(离线/自动/手动共用)src/client/src/lib/platform/evaluation/index.ts:732 runOfflineEvaluation
类型定义11 种内置类型 + 默认开关src/client/src/constants/evaluation-types.ts:10 EVALUATION_TYPES
类型 context每种类型的默认判官指令src/client/src/constants/evaluation-type-contexts.ts:27

主线走一遍(高层): 请求带 prompt/response(可选 contexts)→ 服务端取该项目的评估配置(哪些类型开着、用哪个判官模型)→ 把「各类型的指令块 + 规则引擎/用户给的真相」拼成一大段 Contexts → 塞进系统提示 → 调判官模型 → 判官对每个类型块产出一个评分对象 → 落库 + 回传。


3. 三条运行入口(offline / auto / manual)

评审只有一个引擎,但有三种把活儿喂进去的方式。它们最终都走到 runEvaluation(判官那一下),区别在谁触发、同步还是批、结果打什么 source 标记

入口触发方服务端函数source同步?用的 spanId
offlineSDK 走 HTTPrunOfflineEvaluationoffline_sdk是,当场返回offline_<uuid>(合成)
autoCron 定时autoEvaluategetEvaluationConfigForTraceauto否,批处理真实 trace 的 SpanId
manual面板按钮setEvaluationsForSpanIdgetEvaluationConfigForTracemanual是,单条真实 trace 的 SpanId

offline(SDK 主路径): run_eval 组 payload POST 到 /api/evaluation/offline,带一次 401/429/5xx 的简单重试(sdk/python/src/openlit/evals/offline.py:180for attempt in range(2))。它不绑定任何已有 trace——服务端用合成的 offline_<uuid> 当 spanId 落库(src/client/src/lib/platform/evaluation/index.ts:870)。

auto(定时批): autoEvaluate 从 ClickHouse 捞出「操作类型在白名单内、且还没有 source='auto' 评估」的 trace,逐条评审。注意它用 LEFT JOIN 排除已评过的,但不因人工反馈而跳过(src/client/src/lib/platform/evaluation/index.ts:573-585 注释与 SQL)。

manual(面板单条): 用户在某条 trace 上点「评估」,setEvaluationsForSpanId 取该 span 的 prompt/response 现场评。auto 和 manual 共用 getEvaluationConfigForTrace(index.ts:404),只是 source 参数不同。

别混进来的第四个路由——/api/evaluation/llm/[evalType]: 名字像「跑评估」,其实是分析读接口。它调 getEvaluationDetectedByType(index.ts:668),对 ClickHouse 里 verdict='yes' 的记录按类型计数,给面板画「本周检出多少条幻觉」。它不触发任何判官调用。别被路径名骗了。


4. 判官引擎:一次评审怎么跑

这是整章工程含量最高的一支,全在 run-evaluation.ts。分三步:选模型 → 拼系统提示 → 调用并解析。

4.1 多厂商适配(getModel)

判官可以是任意一家的模型。getModel 是个 provider→构造器的 switch,把 Vercel AI SDK 的各家 client 统一成一个可调用的 model 实例(src/client/src/lib/platform/evaluation/run-evaluation.ts:68 getModel):

// run-evaluation.ts:70 —— 按 provider 分派;OpenAI 兼容的厂商复用 createOpenAI + baseURL
switch (mappedProvider) {
case "openai": return createOpenAI({ apiKey })(model);
case "anthropic": return createAnthropic({ apiKey })(model);
case "google": return createGoogleGenerativeAI({ apiKey })(model);
case "groq": return createOpenAI({ baseURL: "https://api.groq.com/openai/v1", apiKey })(model);
// … mistral / cohere / perplexity / deepseek / xai / together / fireworks
}

巧点:凡是 OpenAI 兼容协议的厂商(Groq/DeepSeek/xAI/Together/Fireworks/Perplexity),都靠 createOpenAI 换个 baseURL 搞定,不必各写一个 SDK。PROVIDER_MAP(run-evaluation.ts:13)只做一个别名:geminigoogle

4.2 调用与解析

拿到 model 实例后,用 Vercel AI SDK 的 generateText,温度固定 0(判官要可复现,不要发散),再 JSON.parse 出结果数组(run-evaluation.ts:165-175):

// run-evaluation.ts:165 —— temperature:0 保证判官尽量确定性
const { text, usage } = await generateText({ model: modelInstance, prompt: systemPrompt, temperature: 0 });
const parsed = JSON.parse(text) as { success?: boolean; result?: Evaluation[] };

兜底陷阱(值得记): 只要 apiKey/provider/model 缺失、或解析失败、或抛异常,函数都返回一个写死的 DEFAULT_RESULT——只含 Hallucination / Bias / Toxicity 三个全 0 分的对象(run-evaluation.ts:116 DEFAULT_RESULT)。也就是说判官失灵时,你会看到「三项全过」的假绿灯,而不是报错。排查评估异常时要警惕这个。

4.3 输出契约

判官必须对每个类型块吐出这五个字段(run-evaluation.ts:36-41 定义,src/client/src/types/evaluation.ts:40Evaluation 接口):

字段类型含义
scorefloat 0–1越高 = 问题越严重(不是「越高越好」)
evaluationstring类型名,取自 [X evaluation context] 里的 X
classificationstring具体子类,snake_case(如 factual_inaccuracy);无问题填 "none"
explanationstring打分理由,须引用文本证据,禁用「未发现问题」这种套话
verdict"yes" / "no"score > thresholdScore"yes"(默认阈值 0.5)

最反直觉的一点:verdict:"yes" 是「检出问题 = 失败」,不是「通过」。 SDK 侧的 passed 属性据此判定:成功且没有任何一项 verdict 为 yes 才算通过(sdk/python/src/openlit/evals/offline_types.py:52 passed):

# offline_types.py:52
@property
def passed(self) -> bool:
if not self.success:
return False
return all(e.verdict.lower() != "yes" for e in self.evaluations)

5. 「上下文即真相」的判官提示词设计

这是 OpenLIT 评估体系最核心的设计决策,全写在 getSystemPrompt(run-evaluation.ts:17)里。理解它,就理解了整套评估的"世界观"。

5.1 两条硬规矩

规矩一:Contexts 永远是真相,哪怕它错。 提示词原文(run-evaluation.ts:28-30)明确要求:即使 Contexts 和现实知识矛盾,也必须当绝对事实。举的例子是——若 context 写「2+2=5」,而回答说「2+2=4」,判官必须判回答有问题,因为它违背了给定 context。

为什么这么设计?因为评估的目标是「回答是否忠于你给的资料」,而不是「回答是否符合判官模型自己的知识」。判官模型的知识可能过时、可能幻觉;你给的资料(检索到的文档、知识库)才是这次任务的地面真相(ground truth)。把真相权交给 Contexts,评估才可控、可复现。

规矩二:每个 [X evaluation context] 块 → 恰好一个评估对象。 提示词(run-evaluation.ts:32-34)命令判官扫描整个 Contexts,找出每一个 [X evaluation context] 开头的块,对每块产出且仅产出一个 evaluation:"X" 的对象,一个不能漏、一个不能多。

这就是「11 种类型」如何工作的机关:类型不是硬编码进判官的,而是以「context 块」的形式动态注入的。开了 5 种类型,就往 Contexts 里放 5 个块,判官就回 5 个对象。加一种新类型 = 加一个块,判官提示词一个字都不用改。

5.2 一个块长什么样

每种类型的默认块由 ctx() 生成,块头 [<Label> evaluation context]运行时从类型标签拼的,不写死(src/client/src/constants/evaluation-type-contexts.ts:21 ctx):

// evaluation-type-contexts.ts:21 —— 块头动态取 label,块体是判官指令
function ctx(id: EvaluationTypeId, body: string): string {
const label = EVALUATION_TYPES.find((t) => t.id === id)?.label ?? id;
return `[${label} evaluation context]\n${body}`;
}

以幻觉为例,它的 body 再次强调「context 是真相,回答只要偏离 context 就要被标记」(evaluation-type-contexts.ts:31-38)——即块级指令和全局提示词的"真相观"是一致的、双重加固的。

5.3 输出格式的防线

提示词还给了完整的 JSON 示例结构,并明确交代别用 ```json 包裹、别加代码围栏(run-evaluation.ts:63-64),因为服务端要直接 JSON.parse(text),多一个围栏就炸。这是「让 LLM 稳定吐可解析 JSON」的实战细节。


6. 11 种内置评估类型与默认配置

全部类型在 EVALUATION_TYPES 常量里声明(src/client/src/constants/evaluation-types.ts:10)。共 11 种,默认只开前三种:

id标签检测什么默认开
hallucinationHallucination事实错误、与 context 矛盾、胡编
biasBias性别 / 种族 / 年龄等偏见
toxicityToxicity有害、冒犯、仇恨言论
relevanceRelevance回答是否切题
coherenceCoherence逻辑连贯、前后一致
faithfulnessFaithfulness严格忠于给定 context
safetySafety越狱、注入、危险指令
instruction_followingInstruction Following是否精确遵循指令与约束
completenessCompleteness是否答全所有子问题
concisenessConciseness是否简洁无冗余
sensitivitySensitivityPII / 密钥 / 隐私泄露

默认三种的兜底逻辑: 若配置里一个 enabled 的类型都没有,编排层会强制回落到幻觉/偏见/毒性三种(src/client/src/lib/platform/evaluation/index.ts:764-768,离线路径;auto/manual 路径在 index.ts:426-434 有同款回落)。这和 §4.2 的 DEFAULT_RESULT 兜底、以及引擎自述(src/client/src/constants/evaluation-engines.ts:9 写着「Evaluates Hallucination, Bias, Toxicity」)三处呼应——这三种是全系统的最小保底集

加新类型只需两步(源码注释 evaluation-types.ts:5-8 自述):在 EVALUATION_TYPES 加一条,再到 evaluation-type-contexts.ts 加默认 prompt——迁移脚本会自动把它塞进 ClickHouse 的默认表。

默认 prompt 存哪: 类型的默认判官指令种子在 evaluation-type-contexts.ts,但运行时是从 ClickHouse 的 defaults 表读的,带进程内缓存(src/client/src/lib/platform/evaluation/evaluation-type-defaults.ts:7 loadDefaults)。配置层 buildEvaluationTypesWithPrompts 把「内置类型 + 用户覆盖 + 默认 prompt」合并成一份完整的类型列表(src/client/src/lib/platform/evaluation/config.ts:34)。


7. 上下文怎么组装(context assembly)

判官提示词里那段 Contexts 不是凭空来的,是编排层拼出来的。这里只讲拼的骨架;规则引擎怎么按 trace 属性选出真相块,是 06-rule-engine 的活。

怎么读: 两股来源汇成一根字符串,前面是「各类型的判官指令块」,后面是「地面真相块」,中间用空行连。

① 每个 enabled 类型
├─ t.prompt(用户自定义) 优先
└─ 否则 t.defaultPrompt(默认块)

prebuiltParts[] ← 每段以 "[Label evaluation context]" 开头(→ 判官逐块产出对象)

② 规则引擎命中的 context 实体 / 用户传入的 contexts

contextContents[] ← 地面真相(ground truth)

finalContextString = [...prebuiltParts, ...contextContents].join("\n\n")

runEvaluation({ contexts: finalContextString, ... })

对应源码(离线路径 src/client/src/lib/platform/evaluation/index.ts:819-826):

// index.ts:819 —— 类型指令块在前,真相块在后,拼成一根 contexts 字符串
const prebuiltParts: string[] = [];
for (const t of enabledTypes) {
const promptToUse = t.prompt?.trim() || t.defaultPrompt?.trim();
if (promptToUse) prebuiltParts.push(promptToUse);
}
const allContextParts = [...prebuiltParts, ...contextContents];
const finalContextString = allContextParts.length > 0 ? allContextParts.join("\n\n") : "";

真相块的来源有二:规则引擎(从 trace 属性匹配出 context 实体,src/client/src/lib/platform/evaluation/rule-engine-context.ts:74 getContextFromRuleEngineForTrace)和用户直接传的 contexts(离线场景,index.ts:815-817)。auto/manual 走 getEvaluationConfigForTrace(index.ts:470-482)拼法一致,只是真相全来自规则引擎。

关键点:类型指令和地面真相被塞进同一个 Contexts 区段,判官都当真相看。所以「§5 的两条硬规矩」既约束了对真相的忠诚,也保证了逐块产出。


8. 人工反馈打分(log_score / record_evaluation_score)

LLM 判官之外,人也能补分——比如用户点了「赞/踩」,或人工复核给了个质量分。这条路在 SDK 侧,和判官引擎完全独立:它不调服务端评审,而是往当前 GenAI span 上挂一个 OTel 事件

入口: openlit.log_score(...)(sdk/python/src/openlit/__init__.py:785),薄薄一层转发到 record_evaluation_score(sdk/python/src/openlit/score.py:127)。三种值都收:

# __init__.py:806 —— 布尔=用户反馈、浮点=质量分、字符串=分类标签
openlit.log_score("user_feedback", True, comment="Helpful response")
openlit.log_score("quality", 0.85, metadata={"reviewer": "human"})
openlit.log_score("category", "accurate")

值怎么归一化: _normalize_score_value(score.py:28)按类型拆成 OTel 语义约定属性——布尔转 1.0/0.0 + 文字标签、数值转 float 值、字符串只给标签:

传入落成的属性
True/Falsegen_ai.evaluation.score.value=1.0/0.0 + .score.label="true"/"false"
0.85(数值)gen_ai.evaluation.score.value=0.85
"accurate"(字符串)gen_ai.evaluation.score.label="accurate"

(语义键定义在 sdk/python/src/openlit/semcov/__init__.py:1066-1071。)

挂到哪个 span: _resolve_target_span(score.py:87)按优先级找目标——显式传的 span > 当前正在记录的活跃 span > 用 trace_id/span_id 十六进制重建一个 NonRecordingSpan。所以既能在 LLM 请求内联打分(自动挂活跃 span),也能事后对历史 trace 补反馈。事件名固定 gen_ai.evaluation.result(score.py:115),comment 进 explanation,idempotency_key 防重复。

返回值语义: 只有真发出了事件才返回 True;没有可挂的 span、或事件系统被禁,返回 False(score.py:137-173),不抛异常(除了 ValueError/TypeError)。

和面板手动反馈的区别: 上面是 SDK 遥测侧(打 OTel 事件,随 trace 流走)。面板上点赞踩走的是另一条——服务端 storeManualFeedback 直接写 ClickHouse eval 表,evaluation:"manual_feedback"、正=0/负=1/中=0.5(src/client/src/lib/platform/evaluation/index.ts:312)。两者殊途:一个是客户端遥测,一个是服务端存储。


9. 巧妙之处(可借鉴)

  • 类型即数据,不入提示词。 评估类型不写死在判官提示里,而是以 [X evaluation context] 块动态注入;判官被命令「每块产一对象」。加类型零改提示词(run-evaluation.ts:32-34 + evaluation-type-contexts.ts:21)。
  • 真相权外置。 把「什么是对的」交给 Contexts 而非判官的内部知识,换来可控、可复现的评估;哪怕 context 写错也照判(run-evaluation.ts:28-30)。
  • OpenAI 兼容协议一把梭。 六七家厂商靠 createOpenAIbaseURL 复用,少写一堆 SDK(run-evaluation.ts:81-110)。
  • SDK 极薄。 Python 端不含任何评审逻辑,只组包 / POST / 解析 / 重试 / 终端上色摘要(offline.py:130offline_types.py:64 summary),升级判官逻辑无需发新 SDK。
  • 温度锁 0 + 禁围栏。 判官要确定性(temperature:0),输出禁 ```json 围栏以便直接 JSON.parse(run-evaluation.ts:63-64,168)。

10. 边界与局限(诚实)

  • 判官失灵会假绿灯。 任何异常都回落到 DEFAULT_RESULT 三项全 0 分(run-evaluation.ts:116),表现为「全过」而非报错——静默失败,排查时易被误导。
  • 强依赖判官模型质量。 评估准不准取决于你选的判官模型;弱模型可能漏判或误判,temperature:0 只保证复现,不保证正确。
  • JSON 解析脆弱。 直接 JSON.parse(text),判官若不听话加了围栏或多余文字就整批失败落兜底(run-evaluation.ts:171)。
  • verdict 语义反直觉。 yes = 检出问题 = 失败;score 越高越糟。接指标时极易接反。
  • auto 只按 source='auto' 去重。 同一 trace 的人工反馈不阻止自动评估;跑批以 SUPPORTED_EVALUATION_OPERATIONS 白名单圈定 trace(index.ts:573-585)。
  • 成本是估算。 评估花费用一张写死的「每 1K token 单价表」估(index.ts:40 EVAL_COST_PER_1K),表里没有的模型退回默认费率,非账单真值。

11. 代码地图(导航索引)

主题文件路径符号名
SDK 顶层入口sdk/python/src/openlit/__init__.pyeval / eval_batch / get_eval_types / log_score
SDK 离线客户端sdk/python/src/openlit/evals/offline.pyrun_eval / run_eval_batch / fetch_eval_types
SDK 结果类型sdk/python/src/openlit/evals/offline_types.pyOfflineEvalResult / OfflineEvaluation / BatchEvalResult / EvalType / passed
人工打分sdk/python/src/openlit/score.pyrecord_evaluation_score / _normalize_score_value / _resolve_target_span
遥测语义键sdk/python/src/openlit/semcov/__init__.pyGEN_AI_EVALUATION_RESULT / GEN_AI_EVALUATION_SCORE_VALUE
判官引擎(核心)src/client/src/lib/platform/evaluation/run-evaluation.tsrunEvaluation / getSystemPrompt / getModel / DEFAULT_RESULT
评估编排src/client/src/lib/platform/evaluation/index.tsrunOfflineEvaluation / autoEvaluate / getEvaluationConfigForTrace / storeEvaluation / storeManualFeedback / getEvaluationDetectedByType
规则→contextsrc/client/src/lib/platform/evaluation/rule-engine-context.tsgetContextFromRuleEngineForTrace / getContextFromRulesWithPriority
配置合并src/client/src/lib/platform/evaluation/config.tsbuildEvaluationTypesWithPrompts / getEvaluationConfigByDbConfigId
默认 prompt 缓存src/client/src/lib/platform/evaluation/evaluation-type-defaults.tsgetEvaluationTypeDefaultPrompts / loadDefaults
11 种类型定义src/client/src/constants/evaluation-types.tsEVALUATION_TYPES
类型默认 contextsrc/client/src/constants/evaluation-type-contexts.tsEVALUATION_TYPE_CONTEXTS / ctx
引擎清单src/client/src/constants/evaluation-engines.tsEVALUATION_ENGINES
离线路由src/client/src/app/api/evaluation/offline/route.tsPOST
自动路由src/client/src/app/api/evaluation/auto/route.tsPOST
分析路由(非运行)src/client/src/app/api/evaluation/llm/[evalType]/route.tsPOSTgetEvaluationDetectedByType
类型查询路由src/client/src/app/api/evaluation/offline/types/route.tsGET

相邻章节: 装配看 01-instrumentation-core,span/metric 生成看 02-span-cost-metrics,落库看 03-backend-collector-clickhouse,护栏看 05-guardrails,规则引擎怎么供给 context 看 06-rule-engine