Skip to content

9.4 Evaluation 子系统:从 Evaluator 到 Score Analytics ​

学习目标 ​

完成本节后,你将能够:

  1. 说清 evaluator、scorer、score、score config、evaluation rule 的区别。
  2. 从 UI/API 定义 evaluator,一路追到 worker 执行和 ClickHouse scores 落库。
  3. 理解 legacy trace/dataset 和新语义 event/experiment 为什么并存。
  4. 区分 experiment item score、trace/observation score、dataset run score。
  5. 看懂 experiment comparison 和 score analytics 如何基于同一张 scores 事实表工作。
  6. 复用本节模板去设计类似的自动评价 infra。

9.4.1 先给结论 ​

Evaluation 子系统不是“一个 evaluator 跑完返回一个数字”这么简单。它是 Langfuse 把观测数据变成质量反馈的闭环:

text
事实数据
  -> 命中评价规则
  -> 异步执行 evaluator
  -> 生成标准 Score
  -> 进入 ClickHouse
  -> 被 experiment comparison / score analytics / dashboard 查询

当前 repo 已经包含 OSS 版本的主链路抽象:

层核心对象主要文件
定义层EvalTemplate、JobConfiguration、ScoreConfigpackages/shared/prisma/schema.prisma
目标层EvalTargetObject、变量映射、filterpackages/shared/src/features/evals/types.ts
触发层trace/dataset legacy eval,event/experiment observation evalworker/src/features/evaluation/evalService.ts、worker/src/features/evaluation/observationEval
执行层JobExecution、BullMQ、S3 snapshot、LLM-as-judge/code evaluatorworker/src/queues/evalQueue.ts
落库层SCORE_CREATE、validateAndInflateScore、ClickHouse scoresworker/src/features/evaluation/evalScoreEvent.ts、worker/src/services/IngestionService/index.ts
查询层experiment metrics、dataset run comparison、score analyticspackages/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表示“评分器”这个概念不是最终分数记录
EvaluatorEvalTemplate + JobConfiguration 组合定义如何评分,以及命中哪些对象不直接代表某一次评分结果
EvalTemplatePostgres eval_templatesLLM-as-judge prompt、code、变量、输出定义、模型配置不定义何时运行
Evaluation RulePostgres job_configurations,public API 中叫 ruletarget、filter、mapping、sampling、enabled不保存最终 score 值
JobExecutionPostgres job_executions某次 evaluator 执行状态、输入对象、输出 score id不是分析事实表
ScoreConfigPostgres score_configsscore schema:名字、类型、范围、分类不是 evaluator prompt/code
ScoreClickHouse scores某个对象上的一次评价事实不是规则,也不是执行状态

可以把它们的关系记成一句话:

text
EvalTemplate 定义“怎么评”,JobConfiguration 定义“评谁”,JobExecution 记录“这次有没有评完”,Score 记录“评出来什么”。

9.4.4 子系统总图 ​

读这张图时注意两点:

  1. Score 是所有评价结果的统一落点,不管来源是 API、内部 evaluator 还是人工 annotation。
  2. DatasetRun / Experiment 不直接包含 session;experiment 主要通过 DatasetRunItem -> Trace -> Observation 和 score 发生关系。

按存储落点再读一遍:

存储放什么为什么放这里
Postgreseval_templates、job_configurations、job_executions、score_configs、dataset_runs、关系型 dataset_run_items这些是配置、状态和关系,需要事务、权限、唯一约束和可更新性。
ClickHousescores、observations/events_full、traces、dataset_run_items_rmt这些是高吞吐事实或分析投影,常按 project/time/trace/score 聚合查询。
Redis/BullMQeval execution jobs、SCORE_CREATE ingestion jobs这些是短期异步调度状态,不是最终事实。
S3/blobObservationForEval snapshotevaluator 输入可能很大,且 retry 需要稳定可重放。

这也是 Evaluation 子系统的核心分层:Postgres 定义和记录“要怎么评、评到哪一步”,ClickHouse 保存“实际发生了什么和评出来什么”,Redis/S3 承担异步执行过程中的临时载体。

9.4.5 定义层格式:Evaluator、Rule、ScoreConfig ​

Evaluation 的控制面主要在 Postgres,因为这些对象需要事务、权限、唯一约束和可编辑状态。

EvalTemplate:评分方法 ​

源码入口:packages/shared/prisma/schema.prisma 的 EvalTemplate。

字段作用
name、versionevaluator family 和版本。
typeLLM_AS_JUDGE 或 CODE。
promptLLM-as-judge 的 prompt 模板。
outputDefinitionLLM 输出如何解析成 score。
sourceCode、sourceCodeLanguagecode evaluator 的代码和语言。
model、provider、modelParamsLLM judge 使用的模型配置。
vars模板变量名。

可以把它理解成:

json
{
  "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 名称。
targetObjecttrace、dataset、event、experiment。
filter哪些对象会触发。
variableMapping从被评价对象取哪些字段填入模板变量。
sampling触发后按比例采样。
delaylegacy 路径可延迟执行。
timeScopeNEW / EXISTING,区分实时和历史批处理。
status、blockedAt、blockReason是否启用,以及为什么被阻塞。

一个 rule 的语义可以表示成:

json
{
  "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。
dataTypeNUMERIC、CATEGORICAL、BOOLEAN、TEXT。
minValue、maxValuenumeric 的合法范围。
categoriescategorical/boolean 的标签和值。
isArchived停止新 score 使用,保留历史。

ScoreConfig 不负责执行 evaluator。它只保证某个 score 名称和值符合约束。

9.4.6 Target 语义:旧链路和新链路并存 ​

源码入口:packages/shared/src/features/evals/types.ts。

EvalTargetObject 有四个值:

target语义代际评价对象当前理解
tracelegacy整条 trace对一次请求/agent run 打分。
datasetlegacydataset 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说明
Observationsevent新路径,评价 observation/event。
Tracestracelegacy,UI 标注为 legacy。
Experimentsexperiment 或 datasetcode/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 合在一个对象里:

json
{
  "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 inputinput给 judge 看用户问题或 tool 输入。
observation outputoutput评价模型回答、tool 结果、retrieval 结果。
observation metadatametadata传 prompt version、业务标签、上下文。

experiment target 额外字段 ​

变量来源字段常见用途
expected outputexperiment_item_expected_outputcorrectness、faithfulness、格式一致性判断。
item metadataexperiment_item_metadata按样本类别、难度、来源辅助评分。

这就是为什么 experiment evaluator 不需要一套完全独立的数据结构。它复用 observation evaluator,只是 observation 上多带了 experiment context。

9.4.8 触发层:什么事实会触发 evaluator ​

Evaluation 有两条触发链路。

新链路:event / experiment ​

源码入口:

  • worker/src/features/evaluation/observationEval/fetchObservationEvalConfigs.ts
  • worker/src/features/evaluation/observationEval/scheduleObservationEvals.ts
  • worker/src/queues/otelIngestionQueue.ts
  • worker/src/features/experiments/scheduleExperimentEvals.ts

新链路只读取 target 为 event 或 experiment 的 active config:

text
projectId 匹配
targetObject in ["event", "experiment"]
status = ACTIVE
blockedAt = null
evalTemplateId is not null

然后对每个 observation 做三步判断:

步骤作用
executable checkrule 是否 active、是否被 block、是否符合 execution mode。
filter checkobservation 字段是否匹配 rule filter。
sampling check命中后是否被采样执行。

对 experiment target 还有一个额外约束:

text
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 upserttrace-upsertingestion 写入 trace 后投递 TraceUpsert queue。
Dataset run item upsertdataset-run-item-upsertdataset run item 连接 trace 后触发。
UI batch evalui-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。

字段作用
iddeterministic job execution id,避免重复调度。
jobConfigurationId哪条 rule 触发。
jobTemplateId使用哪个 template。
statusPENDING、COMPLETED、ERROR、CANCELLED、DELAYED。
jobInputTraceId被评价 trace。
jobInputObservationId被评价 observation。
jobInputDatasetItemIdlegacy dataset 路径使用。
jobOutputScoreId执行完成后第一条 score id。
executionTraceIdevaluator 自己执行过程的 trace id。

S3 snapshot 的格式 ​

新链路不会把完整 observation body 塞进 Redis job,而是上传到 S3/blob:

text
evals/{projectId}/traces/{safeTraceId}/observations/{safeObservationId}.json

queue payload 只带指针:

json
{
  "projectId": "project_1",
  "jobExecutionId": "job_abc",
  "observationS3Path": "evals/project_1/traces/trace_1/observations/obs_1.json"
}

这样做有三个好处:

  1. Redis job 小,队列内存压力低。
  2. retry 时读同一份 snapshot,输入稳定。
  3. observation payload 可能很大,不适合放进 queue。

queue 选择 ​

源码入口:worker/src/features/evaluation/observationEval/createSchedulerDeps.ts 和 packages/shared/src/server/queues.ts。

EvalTemplate typeQueueNameWorker processor
LLM_AS_JUDGELLMAsJudgeExecutionllmAsJudgeExecutionQueueProcessorBuilder
CODECodeEvalExecutioncode eval execution processor
legacy trace/datasetEvaluationExecution / secondary queueevalJobExecutorQueueProcessorBuilder

9.4.10 执行层:processObservationEval 做什么 ​

源码入口:worker/src/features/evaluation/observationEval/observationEvalProcessor.ts。

执行步骤可以拆成 8 步:

步骤做什么为什么
1读取 JobExecutionjob 可能被删除或已经失败。
2读取 JobConfiguration + EvalTemplate确认 rule 和 template 仍存在,且 type 匹配。
3检查 executable如果 evaluator 被 block/inactive,标记 CANCELLED。
4从 S3 下载 observation snapshot使用调度时固定的输入。
5parse ObservationForEval防止脏 snapshot 进入 evaluator。
6按 variableMapping 提取变量生成 prompt/code runtime 的输入。
7运行 LLM-as-judge 或 code evaluator真正产生 score 值和 metadata。
8completeEvalExecution写 score event,更新 job 状态。

运行时分支很清楚:

这里的 evaluator 运行本身也可能被 Langfuse trace 记录下来,executionTraceId 就是把“评分器自己的运行过程”和“被评分对象”分开的关键字段。

9.4.11 Score 落库:Evaluation 的统一输出格式 ​

Evaluator 输出后,不直接 insert ClickHouse。它先构造标准 SCORE_CREATE ingestion event。

源码入口:

  • worker/src/features/evaluation/evalScoreEvent.ts
  • worker/src/features/evaluation/evalCompletion.ts
  • packages/shared/src/server/ingestion/validateAndInflateScore.ts
  • worker/src/services/IngestionService/index.ts

SCORE_CREATE body ​

json
{
  "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谁产生典型场景
APISDK/API 或外部 pipeline自定义 evaluator、业务系统回写用户反馈。
EVALLangfuse 内部 evaluator jobLLM-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 字段来源说明
idscore idscore record id。
project_idauth scope租户边界。
environmentscore body环境维度。
timestamp / event_tsevent timestamp查询和 ReplacingMergeTree 排序使用。
namescore body 或 ScoreConfig分数名。
valueinflated score valuenumeric/boolean 或 categorical 映射值。
string_valueinflated score valuecategorical/boolean/text 展示值。
data_typebody/configscore 类型。
sourcebodyAPI / EVAL / ANNOTATION。
trace_idtargettrace 或 observation score。
observation_idtargetobservation score。
session_idtargetsession score。
dataset_run_idtargetexperiment run score。
config_idoptionalscore schema。
execution_trace_idevaluator outputevaluator 执行 trace。
metadatabodyevaluator metadata、reasoning 辅助信息。

这就是为什么后续所有查询都能复用 scores:它把“谁评的、评谁、评什么、结果是什么”压成统一事实表。

9.4.12 Experiment 评分:item-level 和 run-level 是两层 ​

这是最容易混淆的地方。

text
Dataset
  -> DatasetRun / Experiment
    -> DatasetRunItem
      -> Trace
        -> Observation tree
          -> Score

DatasetRun / Experiment
  -> Score

也就是说,experiment 相关 score 有两种层级:

层级score target说明当前成熟度
item/result-leveltraceId 或 traceId + observationId对某个 dataset item 本次运行产生的 trace/root observation 打分。当前自动 evaluator 新链路主要走这里。
run-leveldatasetRunId对整个 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 scoresexperiment item trace join scoresobservation_id 为空时视为 trace-level item score。
observation item scoresexperiment item trace join scoresobservation_id 非空时视为 observation-level item score。
experiment scoresscores.dataset_run_id = experiment_id直接挂在 run 上的总分。

源码入口:

  • packages/shared/src/server/repositories/experiments.ts
  • packages/shared/src/server/repositories/scores.ts
  • web/src/features/experiments/server/router.ts
  • web/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.ts
  • web/src/features/score-analytics/server/buildEstimateQuery.ts
  • web/src/features/score-analytics/server/buildScoreComparisonQuery.ts
  • web/src/features/score-analytics/hooks/useScoreAnalyticsQuery.ts

它支持的 objectType:

objectType过滤条件
tracetrace_id IS NOT NULL 且没有 observation/session/dataset_run。
observationobservation_id IS NOT NULL。
sessionsession_id IS NOT NULL 且没有 trace/observation/dataset_run。
dataset_rundataset_run_id IS NOT NULL 且没有 trace/observation/session。
all不限制 target 类型。

为什么两个 score 能比较 ​

Score analytics 不是按时间随便把两条 score 拼起来,而是先把两个 score 过滤到同一个对象身份,再做 matched pairs:

text
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 比较
NUMERICmean、std、分布、时间趋势Pearson、Spearman、MAE、RMSE、heatmap。
CATEGORICALcategory 分布、mode、时间趋势confusion matrix、stacked distribution。
BOOLEANtrue/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 无法 parseunrecoverable。
evaluator 返回多个 score全部写入,第一条 score id 记录到 jobOutputScoreId。
evaluator 返回 0 个 score执行失败。

幂等性主要靠三点:

  1. observation eval 的 jobExecutionId 由 config id、trace id、observation id deterministic 生成。
  2. score id 由 jobExecutionId 和 score 输出 deterministic 生成。
  3. ClickHouse scores 使用事件时间和 delete/version 语义读最新记录。

这保证重复调度和 retry 不会无限制造语义不同的 score。

9.4.15 从源码阅读的一条路线 ​

如果你要从零读 Evaluation 子系统,不建议从 UI 组件开始。按下面顺序读:

顺序文件读什么
1packages/shared/src/domain/scores.tsScore 是什么,source/dataType/target 字段。
2packages/shared/src/utils/scores.tsscore target 的互斥约束。
3packages/shared/src/features/evals/types.tstargetObject、legacy/new target、变量映射。
4packages/shared/src/features/evals/observationForEval.ts新链路 evaluator 的输入快照格式。
5packages/shared/prisma/schema.prismaEvalTemplate、JobConfiguration、JobExecution、ScoreConfig。
6web/src/features/evals/server/unstable-public-apievaluator 和 evaluation rule 的 API 控制面。
7web/src/features/evals/components/inner-evaluator-form.tsxUI 如何把 Observations/Traces/Experiments 映射成 target。
8worker/src/features/evaluation/observationEval新 event/experiment evaluator 的调度和执行。
9worker/src/features/evaluation/evalService.tslegacy trace/dataset evaluator。
10worker/src/features/evaluation/evalScoreEvent.tsevaluator 输出如何变成 SCORE_CREATE。
11worker/src/services/IngestionService/index.tsscore ingestion 如何校验并写 ClickHouse。
12packages/shared/src/server/repositories/experiments.tsexperiment comparison 如何聚合 score。
13web/src/features/score-analyticsscore analytics 如何比较两组 score。

这条路线从数据契约开始,最后才看 UI。读完后再回到 UI,才会知道每个表单字段为什么存在。

9.4.16 如果要做类似 infra,应该抽出哪些设计 ​

Evaluation 子系统可以抽象成一个通用模式:

text
事实表
  + 规则表
  + 输入快照
  + 异步执行表
  + 统一结果事实表
  + 分析查询

对应到 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。

如果你要自己做一个类似系统,最小可行版本可以这样设计:

  1. 先定义 Score 作为唯一评价结果表,不要为每个 evaluator 建一张结果表。
  2. 给 score 一个严格 target contract,例如只能挂到 request、step、session、experiment run 之一。
  3. 把 evaluator definition 和 evaluation rule 分开:一个管“怎么评”,一个管“评谁”。
  4. 异步执行时保存输入 snapshot,不要让 retry 重新读易变事实数据。
  5. evaluator 输出走统一 ingestion/validation,不要绕过 score schema。
  6. experiment comparison 不需要独立结果表,先用 run item 到 trace/step 的关系,再聚合 score。
  7. score analytics 应该围绕统一 target key 做 matched pairs,而不是按时间硬拼。

9.4.17 关键不变量 ​

  1. Score 是评价事实,不是 evaluator,也不是 score schema。
  2. EvalTemplate 和 JobConfiguration 必须分开:模板可复用,rule 决定命中范围。
  3. 新 evaluator target 的主线是 event/experiment,legacy trace/dataset 仍然存在。
  4. experiment target 实际命中 experiment item 的 root observation。
  5. 自动 experiment item 评分通常落到 trace/observation score;run-level score 使用 datasetRunId。
  6. score target 必须互斥:trace、observation、session、datasetRun 只能选一个主目标。
  7. evaluator 输出必须变成标准 SCORE_CREATE,再走统一 score ingestion。
  8. JobExecution 记录执行状态,不是分析事实表。
  9. Score analytics 比较两个 score 时,必须按同一个对象身份 join。
  10. 大表分析必须考虑 FINAL、sampling、matched-pair 保真和笛卡尔爆炸。

下一节 ​

本篇后续可以按同样模板补充 Ingestion、Prompt、Dataset、Search、Annotation Queue 等子系统。