9.4 Evaluation 子系统:从 Evaluator 到 Score Analytics
学习目标
完成本节后,你将能够:
- 说清 evaluator、scorer、score、score config、evaluation rule 的区别。
- 从 UI/API 定义 evaluator,一路追到 worker 执行和 ClickHouse
scores落库。 - 理解 legacy
trace/dataset和新语义event/experiment为什么并存。 - 区分 experiment item score、trace/observation score、dataset run score。
- 看懂 experiment comparison 和 score analytics 如何基于同一张
scores事实表工作。 - 复用本节模板去设计类似的自动评价 infra。
9.4.1 先给结论
Evaluation 子系统不是“一个 evaluator 跑完返回一个数字”这么简单。它是 Langfuse 把观测数据变成质量反馈的闭环:
事实数据
-> 命中评价规则
-> 异步执行 evaluator
-> 生成标准 Score
-> 进入 ClickHouse
-> 被 experiment comparison / score analytics / dashboard 查询当前 repo 已经包含 OSS 版本的主链路抽象:
| 层 | 核心对象 | 主要文件 |
|---|---|---|
| 定义层 | EvalTemplate、JobConfiguration、ScoreConfig | packages/shared/prisma/schema.prisma |
| 目标层 | EvalTargetObject、变量映射、filter | packages/shared/src/features/evals/types.ts |
| 触发层 | trace/dataset legacy eval,event/experiment observation eval | worker/src/features/evaluation/evalService.ts、worker/src/features/evaluation/observationEval |
| 执行层 | JobExecution、BullMQ、S3 snapshot、LLM-as-judge/code evaluator | worker/src/queues/evalQueue.ts |
| 落库层 | SCORE_CREATE、validateAndInflateScore、ClickHouse scores | worker/src/features/evaluation/evalScoreEvent.ts、worker/src/services/IngestionService/index.ts |
| 查询层 | experiment metrics、dataset run comparison、score analytics | packages/shared/src/server/repositories/experiments.ts、web/src/features/score-analytics |
最重要的抽象是:Evaluator 不是结果,Score 才是结果。Evaluation 子系统的核心产物是一条标准化 score fact。
9.4.2 业务闭环:为什么要有 Evaluation
Trace/Observation 告诉你“发生了什么”:
- 用户输入是什么;
- agent 走了哪些步骤;
- 调用了哪些模型、工具、检索器;
- 花了多少 token、cost、latency;
- 哪一步报错。
但它不能直接回答“这次结果好不好”。Evaluation 子系统补的是这一层:
- 这次回答是否正确;
- 检索有没有召回关键证据;
- tool 调用是否成功;
- 最终回答是否符合安全和格式要求;
- 换一个 prompt/model/workflow 后,整体质量是否更好。
所以 Evaluation 是连接 observability 和 experimentation 的桥。没有 score,系统只能看日志;有了 score,系统才能做质量分析和版本比较。
9.4.3 术语表:先把容易混的名词分开
| 名词 | 当前 repo 中的主要表达 | 负责什么 | 不负责什么 |
|---|---|---|---|
| Scorer | 产品语义里的叫法,源码里更多叫 evaluator | 表示“评分器”这个概念 | 不是最终分数记录 |
| Evaluator | EvalTemplate + JobConfiguration 组合 | 定义如何评分,以及命中哪些对象 | 不直接代表某一次评分结果 |
| EvalTemplate | Postgres eval_templates | LLM-as-judge prompt、code、变量、输出定义、模型配置 | 不定义何时运行 |
| Evaluation Rule | Postgres job_configurations,public API 中叫 rule | target、filter、mapping、sampling、enabled | 不保存最终 score 值 |
| JobExecution | Postgres job_executions | 某次 evaluator 执行状态、输入对象、输出 score id | 不是分析事实表 |
| ScoreConfig | Postgres score_configs | score schema:名字、类型、范围、分类 | 不是 evaluator prompt/code |
| Score | ClickHouse scores | 某个对象上的一次评价事实 | 不是规则,也不是执行状态 |
可以把它们的关系记成一句话:
EvalTemplate 定义“怎么评”,JobConfiguration 定义“评谁”,JobExecution 记录“这次有没有评完”,Score 记录“评出来什么”。9.4.4 子系统总图
读这张图时注意两点:
Score是所有评价结果的统一落点,不管来源是 API、内部 evaluator 还是人工 annotation。DatasetRun / Experiment不直接包含 session;experiment 主要通过DatasetRunItem -> Trace -> Observation和 score 发生关系。
按存储落点再读一遍:
| 存储 | 放什么 | 为什么放这里 |
|---|---|---|
| Postgres | eval_templates、job_configurations、job_executions、score_configs、dataset_runs、关系型 dataset_run_items | 这些是配置、状态和关系,需要事务、权限、唯一约束和可更新性。 |
| ClickHouse | scores、observations/events_full、traces、dataset_run_items_rmt | 这些是高吞吐事实或分析投影,常按 project/time/trace/score 聚合查询。 |
| Redis/BullMQ | eval execution jobs、SCORE_CREATE ingestion jobs | 这些是短期异步调度状态,不是最终事实。 |
| S3/blob | ObservationForEval snapshot | evaluator 输入可能很大,且 retry 需要稳定可重放。 |
这也是 Evaluation 子系统的核心分层:Postgres 定义和记录“要怎么评、评到哪一步”,ClickHouse 保存“实际发生了什么和评出来什么”,Redis/S3 承担异步执行过程中的临时载体。
9.4.5 定义层格式:Evaluator、Rule、ScoreConfig
Evaluation 的控制面主要在 Postgres,因为这些对象需要事务、权限、唯一约束和可编辑状态。
EvalTemplate:评分方法
源码入口:packages/shared/prisma/schema.prisma 的 EvalTemplate。
| 字段 | 作用 |
|---|---|
name、version | evaluator family 和版本。 |
type | LLM_AS_JUDGE 或 CODE。 |
prompt | LLM-as-judge 的 prompt 模板。 |
outputDefinition | LLM 输出如何解析成 score。 |
sourceCode、sourceCodeLanguage | code evaluator 的代码和语言。 |
model、provider、modelParams | LLM judge 使用的模型配置。 |
vars | 模板变量名。 |
可以把它理解成:
{
"name": "correctness-judge",
"version": 3,
"type": "LLM_AS_JUDGE",
"prompt": "Compare {{output}} with {{expected_output}} ...",
"vars": ["output", "expected_output"],
"outputDefinition": {
"score": "NUMERIC",
"reasoning": "TEXT"
},
"provider": "openai",
"model": "gpt-4.1"
}这个对象只回答“怎么评分”,不回答“哪些 traces 要跑这个评分器”。
JobConfiguration / Evaluation Rule:命中规则
源码入口:packages/shared/prisma/schema.prisma 的 JobConfiguration,以及 web/src/features/evals/server/unstable-public-api/evaluation-rule-service.ts。
| 字段 | 作用 |
|---|---|
jobType | 当前只有 EVAL。 |
evalTemplateId | 使用哪个 evaluator template。 |
scoreName | 生成的 score 名称。 |
targetObject | trace、dataset、event、experiment。 |
filter | 哪些对象会触发。 |
variableMapping | 从被评价对象取哪些字段填入模板变量。 |
sampling | 触发后按比例采样。 |
delay | legacy 路径可延迟执行。 |
timeScope | NEW / EXISTING,区分实时和历史批处理。 |
status、blockedAt、blockReason | 是否启用,以及为什么被阻塞。 |
一个 rule 的语义可以表示成:
{
"jobType": "EVAL",
"evalTemplateId": "eval_template_123",
"scoreName": "correctness",
"targetObject": "experiment",
"filter": [
{
"column": "experiment_dataset_id",
"operator": "equals",
"value": "dataset_abc"
}
],
"variableMapping": [
{
"templateVariable": "output",
"selectedColumnId": "output"
},
{
"templateVariable": "expected_output",
"selectedColumnId": "experimentItemExpectedOutput"
}
],
"sampling": 1,
"timeScope": ["NEW"],
"status": "ACTIVE"
}ScoreConfig:分数 schema
源码入口:packages/shared/prisma/schema.prisma 的 ScoreConfig 和 packages/shared/src/domain/scores.ts。
| 字段 | 作用 |
|---|---|
name | 分数名,例如 correctness、helpfulness。 |
dataType | NUMERIC、CATEGORICAL、BOOLEAN、TEXT。 |
minValue、maxValue | numeric 的合法范围。 |
categories | categorical/boolean 的标签和值。 |
isArchived | 停止新 score 使用,保留历史。 |
ScoreConfig 不负责执行 evaluator。它只保证某个 score 名称和值符合约束。
9.4.6 Target 语义:旧链路和新链路并存
源码入口:packages/shared/src/features/evals/types.ts。
EvalTargetObject 有四个值:
| target | 语义代际 | 评价对象 | 当前理解 |
|---|---|---|---|
trace | legacy | 整条 trace | 对一次请求/agent run 打分。 |
dataset | legacy | dataset run item | 对 dataset item 运行结果打分,旧路径通过 DatasetRunItemUpsert 触发。 |
event | 新语义 | observation/event row | 对 trace 内某个 LLM 节点打分。 |
experiment | 新语义 | experiment item 的 root observation | 对一次 experiment item 结果打分,可使用 expected output。 |
这里的 event 不是普通日志事件。它在 evaluation 语义里基本就是 observation-scoped target:可以按 observation 的 type、name、input、output、metadata、tool calls、trace context 命中。
UI 层为了兼容历史,会把用户看到的选择映射成实际 target:
| UI 选择 | 可能落到的 target | 说明 |
|---|---|---|
| Observations | event | 新路径,评价 observation/event。 |
| Traces | trace | legacy,UI 标注为 legacy。 |
| Experiments | experiment 或 dataset | code/OTel experiment 使用新 experiment,旧实验路径可用 dataset。 |
源码入口:web/src/features/evals/components/inner-evaluator-form.tsx。
9.4.7 变量映射:Evaluator 怎么拿到输入
Evaluator 执行前必须把被评价对象转换成模板变量。新路径使用 ObservationForEval。
源码入口:packages/shared/src/features/evals/observationForEval.ts。
ObservationForEval 的格式
ObservationForEval 把 observation 自身、trace context、model/tool 信息、experiment context 合在一个对象里:
{
"span_id": "obs_root_1",
"trace_id": "trace_1",
"project_id": "project_1",
"parent_span_id": null,
"type": "GENERATION",
"name": "final-answer",
"environment": "default",
"trace_name": "dataset-run-item-ab123",
"user_id": "user_42",
"session_id": "session_1",
"input": {"question": "怎么退款?"},
"output": {"answer": "你可以在订单页申请退款。"},
"metadata": {"promptVersion": 4},
"provided_model_name": "gpt-4.1-mini",
"tool_call_names": ["order-lookup"],
"experiment_id": "run_123",
"experiment_dataset_id": "dataset_abc",
"experiment_item_id": "item_1",
"experiment_item_expected_output": "说明退款入口和条件",
"experiment_item_metadata": {"category": "refund"},
"experiment_item_root_span_id": "obs_root_1"
}它的设计目的不是“多复制字段”,而是让 evaluator 执行时不再到处 join:
- event evaluator 可以直接取
input、output、metadata; - experiment evaluator 可以额外取
experiment_item_expected_output和experiment_item_metadata; - filter 可以按 trace/session/model/tool/experiment 字段判断;
- snapshot 写入 S3 后,后续重试使用同一份输入,避免事实数据变化导致执行不一致。
event target 可映射字段
| 变量来源 | 字段 | 常见用途 |
|---|---|---|
| observation input | input | 给 judge 看用户问题或 tool 输入。 |
| observation output | output | 评价模型回答、tool 结果、retrieval 结果。 |
| observation metadata | metadata | 传 prompt version、业务标签、上下文。 |
experiment target 额外字段
| 变量来源 | 字段 | 常见用途 |
|---|---|---|
| expected output | experiment_item_expected_output | correctness、faithfulness、格式一致性判断。 |
| item metadata | experiment_item_metadata | 按样本类别、难度、来源辅助评分。 |
这就是为什么 experiment evaluator 不需要一套完全独立的数据结构。它复用 observation evaluator,只是 observation 上多带了 experiment context。
9.4.8 触发层:什么事实会触发 evaluator
Evaluation 有两条触发链路。
新链路:event / experiment
源码入口:
worker/src/features/evaluation/observationEval/fetchObservationEvalConfigs.tsworker/src/features/evaluation/observationEval/scheduleObservationEvals.tsworker/src/queues/otelIngestionQueue.tsworker/src/features/experiments/scheduleExperimentEvals.ts
新链路只读取 target 为 event 或 experiment 的 active config:
projectId 匹配
targetObject in ["event", "experiment"]
status = ACTIVE
blockedAt = null
evalTemplateId is not null然后对每个 observation 做三步判断:
| 步骤 | 作用 |
|---|---|
| executable check | rule 是否 active、是否被 block、是否符合 execution mode。 |
| filter check | observation 字段是否匹配 rule filter。 |
| sampling check | 命中后是否被采样执行。 |
对 experiment target 还有一个额外约束:
observation.span_id === observation.experiment_item_root_span_id也就是说,experiment evaluator 只对每个 experiment item 的 root observation 触发,避免同一个 item 内部的每个 tool/generation 都重复触发 experiment 级评价。
旧链路:trace / dataset
源码入口:worker/src/features/evaluation/evalService.ts。
旧链路读取 target 为 trace 或 dataset 的 config:
| 触发源 | sourceEventType | 典型来源 |
|---|---|---|
| Trace upsert | trace-upsert | ingestion 写入 trace 后投递 TraceUpsert queue。 |
| Dataset run item upsert | dataset-run-item-upsert | dataset run item 连接 trace 后触发。 |
| UI batch eval | ui-create-eval | 对历史 trace 或 dataset item 批量创建 eval jobs。 |
旧链路仍然重要,因为它承载历史 evaluator 语义和兼容路径。但新建 observation/experiment evaluator 时,主要应该理解新链路。
9.4.9 调度格式:JobExecution、S3 snapshot、queue payload
新 observation eval 的调度过程在 scheduleObservationEvals 里完成:
JobExecution 的格式
源码入口:packages/shared/prisma/schema.prisma 的 JobExecution。
| 字段 | 作用 |
|---|---|
id | deterministic job execution id,避免重复调度。 |
jobConfigurationId | 哪条 rule 触发。 |
jobTemplateId | 使用哪个 template。 |
status | PENDING、COMPLETED、ERROR、CANCELLED、DELAYED。 |
jobInputTraceId | 被评价 trace。 |
jobInputObservationId | 被评价 observation。 |
jobInputDatasetItemId | legacy dataset 路径使用。 |
jobOutputScoreId | 执行完成后第一条 score id。 |
executionTraceId | evaluator 自己执行过程的 trace id。 |
S3 snapshot 的格式
新链路不会把完整 observation body 塞进 Redis job,而是上传到 S3/blob:
evals/{projectId}/traces/{safeTraceId}/observations/{safeObservationId}.jsonqueue payload 只带指针:
{
"projectId": "project_1",
"jobExecutionId": "job_abc",
"observationS3Path": "evals/project_1/traces/trace_1/observations/obs_1.json"
}这样做有三个好处:
- Redis job 小,队列内存压力低。
- retry 时读同一份 snapshot,输入稳定。
- observation payload 可能很大,不适合放进 queue。
queue 选择
源码入口:worker/src/features/evaluation/observationEval/createSchedulerDeps.ts 和 packages/shared/src/server/queues.ts。
| EvalTemplate type | QueueName | Worker processor |
|---|---|---|
LLM_AS_JUDGE | LLMAsJudgeExecution | llmAsJudgeExecutionQueueProcessorBuilder |
CODE | CodeEvalExecution | code eval execution processor |
| legacy trace/dataset | EvaluationExecution / secondary queue | evalJobExecutorQueueProcessorBuilder |
9.4.10 执行层:processObservationEval 做什么
源码入口:worker/src/features/evaluation/observationEval/observationEvalProcessor.ts。
执行步骤可以拆成 8 步:
| 步骤 | 做什么 | 为什么 |
|---|---|---|
| 1 | 读取 JobExecution | job 可能被删除或已经失败。 |
| 2 | 读取 JobConfiguration + EvalTemplate | 确认 rule 和 template 仍存在,且 type 匹配。 |
| 3 | 检查 executable | 如果 evaluator 被 block/inactive,标记 CANCELLED。 |
| 4 | 从 S3 下载 observation snapshot | 使用调度时固定的输入。 |
| 5 | parse ObservationForEval | 防止脏 snapshot 进入 evaluator。 |
| 6 | 按 variableMapping 提取变量 | 生成 prompt/code runtime 的输入。 |
| 7 | 运行 LLM-as-judge 或 code evaluator | 真正产生 score 值和 metadata。 |
| 8 | completeEvalExecution | 写 score event,更新 job 状态。 |
运行时分支很清楚:
这里的 evaluator 运行本身也可能被 Langfuse trace 记录下来,executionTraceId 就是把“评分器自己的运行过程”和“被评分对象”分开的关键字段。
9.4.11 Score 落库:Evaluation 的统一输出格式
Evaluator 输出后,不直接 insert ClickHouse。它先构造标准 SCORE_CREATE ingestion event。
源码入口:
worker/src/features/evaluation/evalScoreEvent.tsworker/src/features/evaluation/evalCompletion.tspackages/shared/src/server/ingestion/validateAndInflateScore.tsworker/src/services/IngestionService/index.ts
SCORE_CREATE body
{
"id": "score_123",
"traceId": "trace_1",
"observationId": "obs_1",
"name": "correctness",
"value": 0.92,
"dataType": "NUMERIC",
"source": "EVAL",
"configId": "score_config_1",
"environment": "default",
"executionTraceId": "eval_execution_trace_1",
"comment": "The answer matches the expected refund policy.",
"metadata": {
"jobExecutionId": "job_abc",
"jobConfigurationId": "rule_123"
}
}这个格式和 API/annotation 产生的 score 共用同一条 ingestion 路径。差别主要是 source:
| source | 谁产生 | 典型场景 |
|---|---|---|
API | SDK/API 或外部 pipeline | 自定义 evaluator、业务系统回写用户反馈。 |
EVAL | Langfuse 内部 evaluator job | LLM-as-judge、code evaluator。 |
ANNOTATION | 人工标注 UI / annotation queue | 领域专家 review 后提交。 |
Score target 约束
源码入口:packages/shared/src/utils/scores.ts。
Score 只能有一个主 target:
| 合法组合 | 表达什么 |
|---|---|
traceId | 对整条 trace 打分。 |
traceId + observationId | 对 trace 内某个 observation 打分。 |
sessionId | 对整个 session 打分。 |
datasetRunId | 对整个 dataset run / experiment 打分。 |
observationId 不能单独存在,因为 observation 的业务身份总是依附 trace。
ClickHouse scores row
IngestionService 最终写入 ClickHouse scores 表的关键字段:
| ClickHouse 字段 | 来源 | 说明 |
|---|---|---|
id | score id | score record id。 |
project_id | auth scope | 租户边界。 |
environment | score body | 环境维度。 |
timestamp / event_ts | event timestamp | 查询和 ReplacingMergeTree 排序使用。 |
name | score body 或 ScoreConfig | 分数名。 |
value | inflated score value | numeric/boolean 或 categorical 映射值。 |
string_value | inflated score value | categorical/boolean/text 展示值。 |
data_type | body/config | score 类型。 |
source | body | API / EVAL / ANNOTATION。 |
trace_id | target | trace 或 observation score。 |
observation_id | target | observation score。 |
session_id | target | session score。 |
dataset_run_id | target | experiment run score。 |
config_id | optional | score schema。 |
execution_trace_id | evaluator output | evaluator 执行 trace。 |
metadata | body | evaluator metadata、reasoning 辅助信息。 |
这就是为什么后续所有查询都能复用 scores:它把“谁评的、评谁、评什么、结果是什么”压成统一事实表。
9.4.12 Experiment 评分:item-level 和 run-level 是两层
这是最容易混淆的地方。
Dataset
-> DatasetRun / Experiment
-> DatasetRunItem
-> Trace
-> Observation tree
-> Score
DatasetRun / Experiment
-> Score也就是说,experiment 相关 score 有两种层级:
| 层级 | score target | 说明 | 当前成熟度 |
|---|---|---|---|
| item/result-level | traceId 或 traceId + observationId | 对某个 dataset item 本次运行产生的 trace/root observation 打分。 | 当前自动 evaluator 新链路主要走这里。 |
| run-level | datasetRunId | 对整个 experiment run 直接打总分。 | 数据模型、查询和 analytics 支持;通常来自 API/外部聚合或额外任务。 |
Experiment item evaluator 为什么落到 root observation
Prompt experiment 运行时,每个 dataset item 会产生一条 trace,并写入 experiment context:
| context 字段 | 作用 |
|---|---|
experiment_id | 当前 dataset run id。 |
experiment_dataset_id | 所属 dataset。 |
experiment_item_id | 当前 dataset item。 |
experiment_item_expected_output | 期望输出。 |
experiment_item_metadata | 样本 metadata。 |
experiment_item_root_span_id | 本 item 的 root observation。 |
targetObject=experiment 的 rule 只命中 root observation,因为 root observation 代表这个 item 的整体运行结果。这样 evaluator 能拿到:
- 模型输出;
- expected output;
- item metadata;
- trace context;
- tool/model 使用情况。
它比直接对 DatasetRunItem 打分更灵活,因为 evaluator 仍然可以读完整 observation/event 语义。
Experiment comparison 怎么读这些 score
Experiment 页面聚合三类指标:
| 指标 | 查询方式 | 说明 |
|---|---|---|
| cost/latency | 从 experiment events 聚合 | 比较运行成本和性能。 |
| trace item scores | experiment item trace join scores | observation_id 为空时视为 trace-level item score。 |
| observation item scores | experiment item trace join scores | observation_id 非空时视为 observation-level item score。 |
| experiment scores | scores.dataset_run_id = experiment_id | 直接挂在 run 上的总分。 |
源码入口:
packages/shared/src/server/repositories/experiments.tspackages/shared/src/server/repositories/scores.tsweb/src/features/experiments/server/router.tsweb/src/features/experiments/components/table/ExperimentsTable.tsx
所以,DatasetRun 不是多个 Session 组成的。Session 线和 Experiment 线在 Trace 处相交:
9.4.13 Score Analytics:两个 score 如何比较
Score analytics 是通用 score 分析,不是 experiment 专属能力。
源码入口:
web/src/features/score-analytics/server/scoreAnalyticsRouter.tsweb/src/features/score-analytics/server/buildEstimateQuery.tsweb/src/features/score-analytics/server/buildScoreComparisonQuery.tsweb/src/features/score-analytics/hooks/useScoreAnalyticsQuery.ts
它支持的 objectType:
| objectType | 过滤条件 |
|---|---|
trace | trace_id IS NOT NULL 且没有 observation/session/dataset_run。 |
observation | observation_id IS NOT NULL。 |
session | session_id IS NOT NULL 且没有 trace/observation/dataset_run。 |
dataset_run | dataset_run_id IS NOT NULL 且没有 trace/observation/session。 |
all | 不限制 target 类型。 |
为什么两个 score 能比较
Score analytics 不是按时间随便把两条 score 拼起来,而是先把两个 score 过滤到同一个对象身份,再做 matched pairs:
score1.name/source/dataType
score2.name/source/dataType
objectType
time range
-> score1_filtered
-> score2_filtered
-> 按 trace_id / observation_id / session_id / dataset_run_id 做 NULL-safe join
-> matched_scores输出指标
| 数据类型 | 单 score 分析 | 双 score 比较 |
|---|---|---|
NUMERIC | mean、std、分布、时间趋势 | Pearson、Spearman、MAE、RMSE、heatmap。 |
CATEGORICAL | category 分布、mode、时间趋势 | confusion matrix、stacked distribution。 |
BOOLEAN | true/false 分布、比例、时间趋势 | confusion matrix、stacked distribution。 |
Score analytics 会先做 estimate,再决定是否 sampling、是否使用 ClickHouse FINAL。这不是 UI 小优化,而是大表分析的工程取舍:
| 机制 | 目的 |
|---|---|
| estimate query | 预估两组 score 数量和 matched count。 |
adaptive FINAL | 小数据用 FINAL 保准确,大数据跳过以控制延迟。 |
| hash sampling | 用对象身份做确定性采样,保留 matched pairs。 |
| safety limit | 防止同一对象上多条同名 score 造成笛卡尔爆炸。 |
9.4.14 错误、重试和一致性
Evaluation 是异步系统,不是同步函数调用。它必须处理失败和重复。
| 场景 | 当前处理 |
|---|---|
| 没有 active config | 缓存 no-config,减少每条 observation 都查 Postgres。 |
| filter 不命中 | 不上传 S3,不创建 job。 |
| sampling 未命中 | 不创建 job。 |
| evaluator 被 block/inactive | 执行时标记 CANCELLED。 |
| LLM rate limit / 5xx | 可重试错误会延迟重试或交给 BullMQ retry。 |
| LLM 配置错误 | 标记 ERROR,必要时 block evaluator config。 |
| S3 snapshot 下载失败 | retryable。 |
| snapshot JSON 无法 parse | unrecoverable。 |
| evaluator 返回多个 score | 全部写入,第一条 score id 记录到 jobOutputScoreId。 |
| evaluator 返回 0 个 score | 执行失败。 |
幂等性主要靠三点:
- observation eval 的
jobExecutionId由 config id、trace id、observation id deterministic 生成。 - score id 由 jobExecutionId 和 score 输出 deterministic 生成。
- ClickHouse
scores使用事件时间和 delete/version 语义读最新记录。
这保证重复调度和 retry 不会无限制造语义不同的 score。
9.4.15 从源码阅读的一条路线
如果你要从零读 Evaluation 子系统,不建议从 UI 组件开始。按下面顺序读:
| 顺序 | 文件 | 读什么 |
|---|---|---|
| 1 | packages/shared/src/domain/scores.ts | Score 是什么,source/dataType/target 字段。 |
| 2 | packages/shared/src/utils/scores.ts | score target 的互斥约束。 |
| 3 | packages/shared/src/features/evals/types.ts | targetObject、legacy/new target、变量映射。 |
| 4 | packages/shared/src/features/evals/observationForEval.ts | 新链路 evaluator 的输入快照格式。 |
| 5 | packages/shared/prisma/schema.prisma | EvalTemplate、JobConfiguration、JobExecution、ScoreConfig。 |
| 6 | web/src/features/evals/server/unstable-public-api | evaluator 和 evaluation rule 的 API 控制面。 |
| 7 | web/src/features/evals/components/inner-evaluator-form.tsx | UI 如何把 Observations/Traces/Experiments 映射成 target。 |
| 8 | worker/src/features/evaluation/observationEval | 新 event/experiment evaluator 的调度和执行。 |
| 9 | worker/src/features/evaluation/evalService.ts | legacy trace/dataset evaluator。 |
| 10 | worker/src/features/evaluation/evalScoreEvent.ts | evaluator 输出如何变成 SCORE_CREATE。 |
| 11 | worker/src/services/IngestionService/index.ts | score ingestion 如何校验并写 ClickHouse。 |
| 12 | packages/shared/src/server/repositories/experiments.ts | experiment comparison 如何聚合 score。 |
| 13 | web/src/features/score-analytics | score analytics 如何比较两组 score。 |
这条路线从数据契约开始,最后才看 UI。读完后再回到 UI,才会知道每个表单字段为什么存在。
9.4.16 如果要做类似 infra,应该抽出哪些设计
Evaluation 子系统可以抽象成一个通用模式:
事实表
+ 规则表
+ 输入快照
+ 异步执行表
+ 统一结果事实表
+ 分析查询对应到 Langfuse:
| 通用模式 | Langfuse 实现 |
|---|---|
| 事实表 | events_full/events_core、legacy traces/observations、dataset_run_items_rmt。 |
| 规则表 | job_configurations。 |
| 方法模板 | eval_templates。 |
| 输入快照 | S3 observation snapshot。 |
| 异步执行表 | job_executions。 |
| 统一结果事实表 | ClickHouse scores。 |
| 分析查询 | experiment repositories、score analytics router。 |
如果你要自己做一个类似系统,最小可行版本可以这样设计:
- 先定义
Score作为唯一评价结果表,不要为每个 evaluator 建一张结果表。 - 给 score 一个严格 target contract,例如只能挂到 request、step、session、experiment run 之一。
- 把 evaluator definition 和 evaluation rule 分开:一个管“怎么评”,一个管“评谁”。
- 异步执行时保存输入 snapshot,不要让 retry 重新读易变事实数据。
- evaluator 输出走统一 ingestion/validation,不要绕过 score schema。
- experiment comparison 不需要独立结果表,先用 run item 到 trace/step 的关系,再聚合 score。
- score analytics 应该围绕统一 target key 做 matched pairs,而不是按时间硬拼。
9.4.17 关键不变量
Score是评价事实,不是 evaluator,也不是 score schema。EvalTemplate和JobConfiguration必须分开:模板可复用,rule 决定命中范围。- 新 evaluator target 的主线是
event/experiment,legacytrace/dataset仍然存在。 experimenttarget 实际命中 experiment item 的 root observation。- 自动 experiment item 评分通常落到 trace/observation score;run-level score 使用
datasetRunId。 - score target 必须互斥:trace、observation、session、datasetRun 只能选一个主目标。
- evaluator 输出必须变成标准
SCORE_CREATE,再走统一 score ingestion。 JobExecution记录执行状态,不是分析事实表。- Score analytics 比较两个 score 时,必须按同一个对象身份 join。
- 大表分析必须考虑
FINAL、sampling、matched-pair 保真和笛卡尔爆炸。
下一节
本篇后续可以按同样模板补充 Ingestion、Prompt、Dataset、Search、Annotation Queue 等子系统。