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 版本的主链路抽象:

核心对象主要文件
定义层EvalTemplateJobConfigurationScoreConfigpackages/shared/prisma/schema.prisma
目标层EvalTargetObject、变量映射、filterpackages/shared/src/features/evals/types.ts
触发层trace/dataset legacy eval,event/experiment observation evalworker/src/features/evaluation/evalService.tsworker/src/features/evaluation/observationEval
执行层JobExecution、BullMQ、S3 snapshot、LLM-as-judge/code evaluatorworker/src/queues/evalQueue.ts
落库层SCORE_CREATEvalidateAndInflateScore、ClickHouse scoresworker/src/features/evaluation/evalScoreEvent.tsworker/src/services/IngestionService/index.ts
查询层experiment metrics、dataset run comparison、score analyticspackages/shared/src/server/repositories/experiments.tsweb/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_templatesjob_configurationsjob_executionsscore_configsdataset_runs、关系型 dataset_run_items这些是配置、状态和关系,需要事务、权限、唯一约束和可更新性。
ClickHousescoresobservations/events_fulltracesdataset_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.prismaEvalTemplate

字段作用
nameversionevaluator family 和版本。
typeLLM_AS_JUDGECODE
promptLLM-as-judge 的 prompt 模板。
outputDefinitionLLM 输出如何解析成 score。
sourceCodesourceCodeLanguagecode evaluator 的代码和语言。
modelprovidermodelParamsLLM 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.prismaJobConfiguration,以及 web/src/features/evals/server/unstable-public-api/evaluation-rule-service.ts

字段作用
jobType当前只有 EVAL
evalTemplateId使用哪个 evaluator template。
scoreName生成的 score 名称。
targetObjecttracedataseteventexperiment
filter哪些对象会触发。
variableMapping从被评价对象取哪些字段填入模板变量。
sampling触发后按比例采样。
delaylegacy 路径可延迟执行。
timeScopeNEW / EXISTING,区分实时和历史批处理。
statusblockedAtblockReason是否启用,以及为什么被阻塞。

一个 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.prismaScoreConfigpackages/shared/src/domain/scores.ts

字段作用
name分数名,例如 correctnesshelpfulness
dataTypeNUMERICCATEGORICALBOOLEANTEXT
minValuemaxValuenumeric 的合法范围。
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 的 typenameinputoutputmetadata、tool calls、trace context 命中。

UI 层为了兼容历史,会把用户看到的选择映射成实际 target:

UI 选择可能落到的 target说明
Observationsevent新路径,评价 observation/event。
Tracestracelegacy,UI 标注为 legacy。
Experimentsexperimentdatasetcode/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 可以直接取 inputoutputmetadata
  • experiment evaluator 可以额外取 experiment_item_expected_outputexperiment_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 为 eventexperiment 的 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 为 tracedataset 的 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.prismaJobExecution

字段作用
iddeterministic job execution id,避免重复调度。
jobConfigurationId哪条 rule 触发。
jobTemplateId使用哪个 template。
statusPENDINGCOMPLETEDERRORCANCELLEDDELAYED
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.tspackages/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。
6variableMapping 提取变量生成 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-leveltraceIdtraceId + 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.prismaEvalTemplateJobConfigurationJobExecutionScoreConfig
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/observationsdataset_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. EvalTemplateJobConfiguration 必须分开:模板可复用,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 等子系统。