3.7 业务实体关系:从 Trace 到 Experiment
学习目标
完成本节后,你将能够:
- 说清
Observation -> Trace -> Session在业务中表达什么。 - 解释 dataset、dataset item、dataset run、dataset run item 为什么不是一张表就能说完。
- 区分 Score、ScoreConfig、Evaluator、EvalTemplate、JobConfiguration、JobExecution 的职责。
- 从“线上问题沉淀为 dataset,再跑 experiment,再产出 score”的完整链路理解 Langfuse 的数据抽象。
3.7.1 先给结论:Langfuse 有四类实体,不是一条 Trace 线
初学者很容易把 Langfuse 理解成“trace 管理系统”。这只说对了入口,没有说对抽象。更准确的分组是:
| 实体组 | 解决的问题 | 代表实体 |
|---|---|---|
| 观测实体 | 应用运行时实际发生了什么 | Observation、Trace、Session |
| 评价实体 | 结果好不好,谁给的评价,按什么 schema 评价 | Score、ScoreConfig、Evaluator、AnnotationQueue |
| 实验实体 | 如何把线上样本变成可重复测试,再比较多个版本 | Dataset、DatasetItem、DatasetRun、DatasetRunItem |
| 执行实体 | 异步任务如何调度、重试、记录状态 | JobConfiguration、JobExecution、BullMQ job、S3 snapshot |
这张图的读法是:
Observation是运行事实的细粒度节点。Trace是把一组 observations 聚成一次请求或工作流的相关性边界。Session是把多个 traces 聚成一次长交互的边界。Score是所有评价结果的统一落点。DatasetRun是当前 repo 中承载 experiment 语义的对象。JobConfiguration和JobExecution不等于 score,它们只是“如何产生 score”的配置和执行状态。
3.7.2 Observation:最小运行事实节点
源码中的 observation 类型来自 packages/shared/src/domain/observations.ts:
| 类型 | 业务含义 | 常见字段重点 |
|---|---|---|
SPAN | 通用过程、函数、工作流节点 | startTime、endTime、input、output、metadata |
EVENT | 一个瞬时事件或日志化操作 | startTime、level、statusMessage |
GENERATION | LLM 调用 | model、modelParameters、usageDetails、costDetails、prompt* |
AGENT | agent 级别的执行节点 | 可有模型、工具、输入输出和子节点 |
TOOL | 工具/函数调用 | toolCalls、toolCallNames、input、output |
CHAIN | chain 或组合步骤 | 输入输出、子 observation |
RETRIEVER | 检索步骤 | query、返回文档、metadata |
EVALUATOR | evaluator 自身的运行节点 | evaluator 执行 trace 里的内部步骤 |
EMBEDDING | embedding 调用 | model、usage、cost |
GUARDRAIL | 安全/策略检查 | pass/fail、策略输出、metadata |
其中 GENERATION、AGENT、TOOL、CHAIN、RETRIEVER、EVALUATOR、EMBEDDING、GUARDRAIL 在源码里被归为 generation-like:它们可能带模型、usage、cost、prompt 或工具调用语义。
一个 observation 不是孤立行。它至少要回答五个问题:
| 问题 | 关键字段 | 说明 |
|---|---|---|
| 属于哪次请求? | traceId / trace_id | 把节点归到同一条 trace。 |
| 在树里的父节点是谁? | parentObservationId / parent_span_id | 还原嵌套调用关系。 |
| 它做了什么? | type、name、level、statusMessage | 用于阅读、筛选、错误定位。 |
| 输入输出是什么? | input、output、metadata | 用于 debug、评估、沉淀 dataset。 |
| 成本和性能如何? | startTime、endTime、usageDetails、costDetails | 用于 latency、token、cost 分析。 |
3.7.3 Observation 如何构成 Trace,Trace 如何构成 Session
在 LLM 应用里,不要把这三层理解成普通后端里的“HTTP 请求 -> span -> 子 span”。Langfuse 兼容 OpenTelemetry,但它要表达的核心对象是 LLM 应用的一次交互和其中的模型/工具/检索/agent 节点。
更贴近业务的理解是:
Session:同一个用户和 AI 应用的一段连续对话或长期任务
Trace:其中一个用户 turn 触发的一次 agent run / workflow run
Observation:这次 agent run 里的一个 LLM 运行节点
子 Observation:这个节点内部继续调用的模型、工具、检索、guardrail 或子 agent这张图的重点不是“每个方块都是一个后端 span”,而是:
| 层 | 在 LLM 应用里的含义 | 例子 |
|---|---|---|
Session | 一段连续对话或长期任务,用来观察跨多轮的质量 | 用户从咨询退款政策,到提供订单信息,再到确认退款。 |
Trace | 一个用户 turn 触发的一次完整 agent/workflow run | “用户问能不能退款”这一轮,包含意图识别、检索、生成回答和 guardrail。 |
Observation | trace 内部可单独调试、计费、评价的 LLM 节点 | 一次 generation、一次 retriever、一次 tool 调用、一次 guardrail、一个子 agent。 |
落到当前 repo 的实现,要分清三层:
| 层 | 当前表示 | 读源码入口 |
|---|---|---|
| 业务关系 | session 包含多条 trace,trace 包含 observation tree | ../langfuse-docs/content/docs/observability/data-model.mdx |
| domain 契约 | TraceDomain.sessionId、ObservationSchema.traceId、parentObservationId | packages/shared/src/domain/traces.ts、observations.ts |
| 存储/查询 | legacy traces、observations;v4 events_full/events_core | ClickHouse migrations、dev-tables.sh、events repository |
结论不是“trace 是最低单位”。 对运行事实来说,最细的可分析节点是 observation/event row;对用户体验来说,trace 是更好阅读的一次请求;对长会话来说,session 是更高一层聚合。
3.7.4 为什么 agent/workflow 更适合 Observation Tree
一条 agent 请求可能跨多个模型、工具、检索、子 agent。用一行可变 trace 记录所有细节会遇到三个问题:
| 问题 | 如果只维护一行 trace | 用 observation tree |
|---|---|---|
| 并发步骤 | 多个工具或子 agent 同时更新同一行,顺序和覆盖语义复杂 | 每个步骤是一行事实,天然可并发写入 |
| 局部评价 | 想评价“最终回答”或“retrieval 质量”时粒度太粗 | evaluator 直接命中某个 observation |
| 成本拆分 | 总 cost 有了,但不知道哪个步骤贵 | 每个 generation-like observation 自带 usage/cost |
这也是 v4 抽象变化的收益:trace 没有消失,但更多变成 trace_id 这个相关性字段;事实本身落在 observation/event row 上。查询 trace 页面时,再按 trace_id 把这些 rows 取出来并按 parent_span_id 组装成树。
3.7.5 Dataset:不是结果表,而是可复用测试材料
Dataset 的业务含义是“可重复运行的测试材料集合”。它不是实验结果,也不是 score。
源码里对应:
| 实体 | 存储 | 关键字段 | 业务解释 |
|---|---|---|---|
Dataset | Postgres datasets | name、metadata、inputSchema、expectedOutputSchema | 一组测试材料和 schema 约束。 |
DatasetItem | Postgres dataset_items | input、expectedOutput、metadata、sourceTraceId、sourceObservationId | 一个测试样本,可以从线上 trace/observation 沉淀而来。 |
| 版本字段 | Postgres dataset_items | validFrom、validTo、isDeleted | item 更新不是简单覆盖,而是保留版本边界。 |
从 trace 添加到 dataset 的链路可以这样看:
这个链路体现了产品闭环:线上失败样本不是只停留在 trace 页面,而是被复制成 dataset item,成为后续 experiment 的固定输入。
3.7.6 Dataset Run / Experiment:一次运行怎么表达
当前 repo 中 DatasetRun 基本就是 experiment run 的实现形态。产品上说“跑一次实验”,源码里通常是:
Dataset
-> DatasetRun / Experiment
-> DatasetRunItem
-> Trace
-> Observation tree
-> Score注意 score 有两种常见层次:
| 层次 | Score 怎么挂 | 业务含义 |
|---|---|---|
| item/result 级 score | 挂到 traceId 或 traceId + observationId | 某个 dataset item 的一次运行输出好不好。UI 可通过 dataset_run_items_rmt.trace_id 把它归到 run。 |
| run 级 score | 直接挂 datasetRunId | 整个 experiment run 的聚合评价,例如 pass rate、平均质量、run evaluator 输出。 |
这就是为什么 scores 表既有 trace_id/observation_id,也有 dataset_run_id。前者评价某次执行结果,后者评价整个实验运行。
3.7.7 DatasetRunItem:实验里的连接表,也是分析投影
DatasetRunItem 是理解 experiment 的关键。它不是普通 join table,因为它承载了“一个测试样本在一次 run 中产生了哪个运行结果”。
| 字段 | 含义 |
|---|---|
datasetRunId | 属于哪一次 experiment run。 |
datasetItemId | 这次运行用的是哪个测试样本。 |
traceId | 应用跑这个样本时产生的 trace。 |
observationId | 可选,老 SDK 或特定输出可以指向具体 observation。 |
error | 这次 item 运行是否因配置或执行失败。注意它在 ingestion event 和 ClickHouse 读模型中存在,当前 Prisma DatasetRunItems 模型没有这个字段。 |
当前 repo 中它有两个落点:
| 落点 | 作用 |
|---|---|
Postgres dataset_run_items | 事务型关系和历史兼容,表达 run item、dataset item、trace、observation 的基础连接。 |
ClickHouse dataset_run_items_rmt | 列表、比较、score 聚合的高性能读模型,并且反规范化了 run/item 的部分字段,也承载 run item error。 |
这种设计的目的不是重复造表,而是把不同查询代价拆开:Postgres 负责关系和版本,ClickHouse 负责按 run、item、trace、score 的分析查询。
3.7.8 Score:统一评价结果,不等于 Scorer
Score 是结果,不是规则。它回答的是“某个对象在某个维度上的评价值是什么”。
源码中的 ScoreSource 有三类:
| source | 谁产生 | 典型场景 |
|---|---|---|
API | SDK/API 或外部 pipeline | 自定义 evaluator、CI、业务系统回写用户反馈。 |
EVAL | Langfuse 内部 evaluator job | LLM-as-a-Judge、code evaluator。 |
ANNOTATION | 人工标注 UI / annotation queue | 领域专家打分、纠正输出。 |
Score 可以挂到四类对象,但约束是“只能选一个主目标”:
在源码里,applyScoreValidation 约束了这个目标关系:
| 目标 | 合法字段组合 | 用途 |
|---|---|---|
| Trace | traceId | 评价一次请求/工作流。 |
| Observation | traceId + observationId | 评价 trace 中某一步,例如最终 generation 或 retrieval。 |
| Session | sessionId | 评价整个多轮对话。 |
| DatasetRun | datasetRunId | 评价一次 experiment run 的整体表现。 |
ScoreConfig 是 score 的 schema 约束,不是 evaluator 规则。它定义:
| 字段 | 用途 |
|---|---|
name | 分数名,例如 correctness、toxicity。 |
dataType | NUMERIC、CATEGORICAL、BOOLEAN、TEXT。 |
minValue/maxValue | numeric 分数范围。 |
categories | categorical/boolean 的合法标签和值。 |
isArchived | 停止新 score 使用,但保留历史。 |
所以要分清:
ScoreConfig:定义分数长什么样
Evaluator / Scorer:定义如何产生分数
Score:某次评价产生的结果3.7.9 Scorer / Evaluator:规则在哪里,结果在哪里
用户常说 scorer,当前 repo 里更多叫 evaluator。它的实现可以拆成三层:
| 层 | 实体 | 存在哪里 | 负责什么 |
|---|---|---|---|
| 评分方法 | EvalTemplate | Postgres eval_templates | LLM judge prompt、code、变量、输出定义、模型配置。 |
| 命中规则 | JobConfiguration / evaluation rule | Postgres job_configurations | 对什么 target 跑、filter、mapping、sampling、delay、timeScope、状态。 |
| 执行状态 | JobExecution | Postgres job_executions | 某一次执行是否 pending/completed/error、输入 trace/observation/dataset item、输出 score id。 |
| 执行输入快照 | observation snapshot | S3/blob | observation eval 执行时使用的稳定输入。 |
| 执行结果 | Score | ClickHouse scores | 最终评分结果,source=EVAL。 |
新旧 evaluator target 也要分清:
| legacy target | 新 target | 业务含义 | 当前实现入口 |
|---|---|---|---|
trace | event | 从评价整条 trace,转向评价某个 observation/event row。 | evalService.createEvalJobs 与 observationEval/* 并存。 |
dataset | experiment | 从 dataset run item 语义,转向 experiment item/root observation 语义。 | DatasetRunItemUpsert 与 scheduleExperimentObservationEvals 并存。 |
这里的 event 不要理解成普通日志。它在 evaluator 语义里基本等价于 observation-scoped target:按 observation 的 type、name、input/output、metadata 和 trace context 命中规则。
3.7.10 一次 Observation-level Evaluator 怎么工作
新 evaluator 路径的核心是:在 ingestion 或 experiment 运行过程中,直接拿命中的 observation 调度 evaluator。
这个设计回答了两个常见问题:
| 问题 | 答案 |
|---|---|
| evaluator 的规则存在哪里? | EvalTemplate 存“怎么评分”,JobConfiguration 存“评谁和何时评”。两者都在 Postgres。 |
| evaluator 的结果存在哪里? | 结果是 Score,最终进入 ClickHouse scores。JobExecution 只记录执行状态和输出 score id。 |
3.7.11 一次 Experiment 怎么跑
以 UI prompt experiment 为例,可以把源码链路压缩成下面这张图:
把这条链路翻译成实体:
| 阶段 | 产生或读取的实体 | 说明 |
|---|---|---|
| 准备材料 | Dataset、DatasetItem | 固定输入和期望输出。 |
| 创建运行 | DatasetRun | 这一次实验的名字、描述、metadata。 |
| 跑每个样本 | DatasetRunItem | 把 item 和本次运行产生的 trace 连接起来。 |
| 记录执行 | Trace、Observation/Event | 真正的 LLM/tool/span 运行过程。 |
| 评价结果 | Score | item/result 级 score 或 run 级 score。 |
所以 “dataset run 一次怎么表达” 的答案是:一行 DatasetRun 表示一次 experiment,N 行 DatasetRunItem 表示这个 run 中 N 个样本的运行结果,每个 run item 指向运行产生的 trace/observation,score 再挂到 trace/observation 或 dataset run 上。
3.7.12 存储视角:哪些是事实,哪些是配置
实体落点可以总结成:
| 对象 | 主要权威存储 | 为什么 |
|---|---|---|
Dataset、DatasetItem、DatasetRun | Postgres | 需要事务、版本、唯一约束和管理 UI。 |
Trace、Observation/Event | ClickHouse | 高频写入和按时间/project/type/metadata 查询。 |
Score | ClickHouse | 分析型事实,常做聚合、过滤和对比。 |
ScoreConfig、EvalTemplate、JobConfiguration | Postgres | 配置型对象,变更频率低,需要权限和事务。 |
JobExecution | Postgres | 执行状态需要可查询、可更新、可审计。 |
| queue payload | Redis/BullMQ | 短期调度、重试、延迟、并发控制。 |
| raw event / snapshot / media | S3/blob | payload 大或需要可重放,不适合塞进 Redis job。 |
ClickHouse 设计上能看到几条基础原则:
| 设计点 | 对应原则 | 在 Langfuse 中的体现 |
|---|---|---|
| 查询常按 project、时间、type、trace 过滤 | schema-pk-prioritize-filters、schema-pk-cardinality-order | events_full/events_core 的排序键以 project_id、时间和 trace_id hash 组织。 |
| 重复字符串用字典编码 | schema-types-lowcardinality | type、environment、level 等字段使用 LowCardinality(String)。 |
| 高频写入不做单行 insert | insert-batch-size | ingestion 请求侧异步入队,worker 的 ClickhouseWriter 批量写入。 |
| 更新/删除避免频繁 mutation | insert-mutation-avoid-update | 使用 ReplacingMergeTree(event_ts, is_deleted) 表达更新版本和删除标记。 |
这些不是数据库技巧的装饰,而是会反过来影响产品抽象:当事实数据必须高吞吐查询时,把 trace context 铺到 observation/event 宽表上,比每次查询都 join 可变 trace row 更适合分析型系统。
3.7.13 从产品动作反推实体关系
只看表名很容易乱。更好的方法是从用户动作倒推:用户做了什么,系统要记住什么,最后落到哪个实体。
| 产品动作 | 系统要记住什么 | 核心实体 | 主要存储 |
|---|---|---|---|
| SDK 上报一次请求 | 这次请求的整体输入输出、每个模型/工具/检索步骤、父子调用关系 | Trace、Observation/Event | ClickHouse events_full/events_core,legacy traces/observations |
| 多轮对话继续发生 | 多条 trace 属于同一个长会话 | TraceSession、trace/event 上的 session_id | Postgres trace_sessions 管 session 标记;ClickHouse facts 带 session_id |
| 把线上样本加入 dataset | 固定输入、期望输出、元数据,以及来源 trace/observation | Dataset、DatasetItem | Postgres datasets/dataset_items |
| 跑一次 experiment | 本次 run 的名字、metadata,每个样本跑出来的 trace | DatasetRun、DatasetRunItem、Trace、Observation/Event | Postgres 存 run 关系;ClickHouse 存 run item 投影和执行 facts |
| 自动或人工评分 | 谁评的、评哪个对象、分数名和值、是否符合 schema | Score、ScoreConfig、AnnotationQueue | Score 在 ClickHouse;配置和人工队列在 Postgres |
| 配置 evaluator/scorer | 用什么模板评分、命中哪些对象、如何抽样和延迟执行 | EvalTemplate、JobConfiguration、JobExecution | Postgres 配置/状态,S3 存执行输入快照,最终 Score 写 ClickHouse |
这张图的重点是:dataset 不是 trace 的子表,score 也不是 dataset run item 的子表。它们通过 id 形成业务关系,分别承担“材料”“执行”“评价”的职责。
3.7.14 Experiment 有两条线:关系线和事件线
Experiment 最容易误解的地方,是把 DatasetRunItem 当成“运行结果本身”。更准确地说,它是连接对象,真正的运行过程还是 trace/observation tree。
这两条线对应不同问题:
| 问题 | 看哪条线 | 为什么 |
|---|---|---|
| 这个 experiment 跑了哪些 dataset items? | 关系线 | DatasetRun -> DatasetRunItem -> DatasetItem 最直接。 |
| 某个 item 的模型调用、工具调用、错误在哪里? | 事件线 | 运行细节在 trace/observation tree。 |
| 为什么一个 run 可以做列表、比较、过滤 score? | 分析线 | dataset_run_items_rmt 反规范化 run/item 字段,events_* 铺了 experiment context。 |
| experiment evaluator 为什么只评 root? | 事件线 + 分析线 | targetObject=experiment 仍然拿 observation 作为输入,但要求 span_id = experiment_item_root_span_id。 |
这里的 experiment_item_root_span_id 很关键。一次实验运行会产生一棵 observation tree,但 experiment item 的输出语义通常在 root observation 上。源码里的 scheduleObservationEvals 会对 targetObject=experiment 增加 root 判断,避免同一个样本的每个 generation/tool 都触发一次 experiment evaluator。
3.7.15 Score 的归属:不要把 item score 和 run score 混在一起
Score 的目标约束是:traceId、traceId + observationId、sessionId、datasetRunId 四种主目标里只能选一种。对 experiment 来说,这会形成三种常见写法:
| 写法 | 字段 | 表达的业务含义 | 怎么回到 experiment |
|---|---|---|---|
| item 结果级 score | traceId | 某个 dataset item 本次执行的整体输出好不好 | 通过 dataset_run_items_rmt.trace_id 找到 run item |
| 步骤级 score | traceId + observationId | 某个生成、检索、工具步骤好不好 | 先回到 trace,再按 observation 定位步骤 |
| run 聚合级 score | datasetRunId | 整次 experiment run 的总体表现 | 直接挂在 DatasetRun 上 |
因此,DatasetRunItem 本身通常不直接“拥有 score”。它通过 traceId 把 score 归到某个样本的运行结果。列表页需要比较不同 runs 的分数时,会把 dataset_run_items_rmt 和 scores 按 project_id + trace_id 聚合起来看。
这也是为什么 scores 表既有 trace_id/observation_id/session_id,又有 dataset_run_id:Langfuse 需要同时支持“单个输出质量”和“整次实验质量”两种评价层级。
3.7.16 Scorer / Evaluator 的新语义:event 和 experiment 都是 observation 视角
当前 repo 里 evaluator target 有四个值:
| targetObject | 语义 | 输入对象 | 变量映射怎么取值 |
|---|---|---|---|
trace | legacy online eval | 一条 trace,外加可按名字找 observation | availableTraceEvalVariables,需要说明用哪类 object/name |
dataset | legacy dataset eval | dataset item + trace | availableDatasetEvalVariables,可以取 dataset item 的 input/expected output |
event | 新 observation-level eval | 一行 observation/event | observationVariableMapping,直接取这行的 input/output/metadata 等 |
experiment | 新 experiment item eval | experiment item 的 root observation/event | 同 event,但额外能取 expected output 和 experiment item metadata |
这解释了“trace -> event、dataset -> experiment”的变化:
| 旧理解 | 新理解 | 收益 |
|---|---|---|
| eval 一条 trace,然后在 trace 里找某个 generation | eval 一行 observation/event | 命中对象更明确,不需要在执行时重新解析整棵树。 |
| dataset eval 以 dataset/run item 为中心 | experiment eval 以 root observation 为中心,并带上 dataset item context | 评分输入同时拥有实际 output 和 expected output。 |
| variableMapping 要描述“从哪类对象的哪个字段取变量” | variableMapping 直接描述“从当前 observation 的哪个字段取变量” | 配置更短,也更适合 agent/workflow 中的多节点评价。 |
| 每次评价可能需要重新查 trace 详情 | ingestion/experiment 阶段上传 observation snapshot | evaluator 使用稳定快照,避免后续事实行更新影响本次评分。 |
这里不要把 event 理解成普通日志。它在 Langfuse v4 语义里更接近“统一 observation row”:span、generation、tool、retriever、agent、guardrail 都可以作为 evaluator target。
3.7.17 为什么很多关系不是数据库外键
Langfuse 的核心 facts 在 ClickHouse,配置和关系对象大量在 Postgres。两边不会像单库业务系统那样处处建外键,而是用稳定 id 形成逻辑关联。
| 关系 | 为什么不是普通 FK 思维 |
|---|---|
DatasetItem.sourceTraceId/sourceObservationId 指向线上事实 | trace/observation 在 ClickHouse,dataset item 在 Postgres,来源是可追溯引用,不是事务级子表。 |
DatasetRunItem.traceId/observationId 指向运行结果 | run item 关系在 Postgres/ClickHouse 都可能出现,运行事实在 ClickHouse。删除和重放靠 project 边界、队列和清理逻辑处理。 |
Score.configId 指向 ScoreConfig | score 是分析事实,config 是 schema 配置。读分数时可按 config 展示/校验,但 score 表本身服务聚合查询。 |
JobExecution.jobInputTraceId/jobInputObservationId 指向被评价对象 | JobExecution 记录“这次 evaluator 为什么跑、跑到哪一步”,不拥有 trace/observation 的生命周期。 |
events_* 行带 experiment_* 字段 | 这是为了查询和过滤快,不是因为 event 在概念上属于 dataset。它仍然是一条运行事实。 |
这套设计的代价是:读源码时不能只沿 Prisma relation 找关系。你要同时看三类关系:
对初学者来说,判断一个实体关系时可以问三个问题:
| 问题 | 如果答案是 yes,通常意味着 |
|---|---|
| 这个对象需要事务、唯一约束、权限管理吗? | 优先在 Postgres 建模。 |
| 这个对象是高频运行事实,需要按时间、project、type、trace 查吗? | 优先在 ClickHouse 建模。 |
| 这个查询经常要跨多个对象做列表/聚合吗? | 可能需要 ClickHouse 反规范化投影。 |
3.7.18 三种评分入口:同样落成 Score,不同的是规则来源
ScoreSource 有 API、EVAL、ANNOTATION 三类。它们最后都是 Score,但“谁决定怎么打分”完全不同。
三条入口的差别:
| 入口 | 规则在哪里 | 状态在哪里 | 结果在哪里 | 适合什么 |
|---|---|---|---|---|
| API / SDK | 外部业务系统自己决定,configId 可选 | 外部系统自己管;Langfuse ingestion 只校验 score body | ClickHouse scores,source=API | CI、线上 feedback、业务规则 pipeline |
| Evaluator / Scorer | EvalTemplate + JobConfiguration | Postgres job_executions,S3 snapshot,BullMQ job | ClickHouse scores,source=EVAL | LLM-as-a-Judge、code evaluator、自动化 online/offline eval |
| Annotation Queue | AnnotationQueue.scoreConfigIds 约束表单 | Postgres annotation_queue_items 的 pending/completed/lock 状态 | ClickHouse scores,source=ANNOTATION | 人工 review、ground truth、专家标注 |
ScoreConfig 不是 scorer。它只定义结果长什么样,例如:
| ScoreConfig 类型 | 约束什么 | 例子 |
|---|---|---|
NUMERIC | 数值范围 | correctness 只能在 0 到 1 之间 |
CATEGORICAL | 标签和值映射 | toxicity = safe/unsafe |
BOOLEAN | True/False 两类 | contains_citation = True/False |
TEXT | 文本长度和字段形态 | reviewer comment 或 reasoning |
Evaluator 使用 ScoreConfig 时,是为了保证 evaluator 输出能被稳定比较;Annotation Queue 使用 ScoreConfig 时,是为了让人工标注表单知道该展示哪些字段;API/SDK 使用 ScoreConfig 时,是为了让外部打回来的分数和团队统一 schema 对齐。
3.7.19 三条源码追踪路线
读这个 repo 时,不建议从所有表开始扫。按下面三条路线读,能把业务语义和实现对应起来。
路线 A:一次线上 trace 如何变成 observation tree
源码入口:
| 看什么 | 入口 |
|---|---|
| ingestion event schema | packages/shared/src/server/ingestion/types.ts |
| worker 如何分流 trace/observation/score/run item | worker/src/services/IngestionService/index.ts |
| observation domain 字段 | packages/shared/src/domain/observations.ts |
| v4 event 查询 | packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts |
路线 B:线上样本如何沉淀成 dataset,再跑 experiment
源码入口:
| 看什么 | 入口 |
|---|---|
| public dataset item/run item API schema | web/src/features/public-api/types/datasets.ts |
| API service 如何创建 dataset item/run item | web/src/features/datasets/server/publicDatasetService.ts |
| dataset item 版本和 source 字段 | packages/shared/src/server/repositories/dataset-items.ts |
| UI prompt experiment worker | worker/src/features/experiments/experimentServiceClickhouse.ts |
| run item ingestion event 转 ClickHouse 投影 | worker/src/services/IngestionService/index.ts |
路线 C:evaluator 如何从规则变成 score
源码入口:
| 看什么 | 入口 |
|---|---|
| evaluator target 枚举和 variable mapping | packages/shared/src/features/evals/types.ts |
| event/experiment eval 可用过滤字段 | packages/shared/src/features/evals/observationForEval.ts |
| 调度 observation eval | worker/src/features/evaluation/observationEval/scheduleObservationEvals.ts |
| experiment root 触发 evaluator | worker/src/features/experiments/scheduleExperimentEvals.ts |
| evaluator 写 score event | worker/src/features/evaluation/evalScoreEvent.ts |
| score config 校验和落库前 inflate | packages/shared/src/server/ingestion/validateAndInflateScore.ts |
这三条路线串起来,就是“观测 -> 沉淀 -> 实验 -> 评价 -> 再迭代”的源码版。
3.7.20 读源码的实体索引
| 想理解的实体 | 先读 | 再读 |
|---|---|---|
| Observation 类型和字段 | packages/shared/src/domain/observations.ts | packages/shared/src/server/repositories/definitions.ts、ClickHouse observations / events_* |
| Trace / Session | packages/shared/src/domain/traces.ts、Prisma TraceSession | trace/session repositories、events query |
| Score 目标和数据类型 | packages/shared/src/domain/scores.ts、packages/shared/src/utils/scores.ts | validateAndInflateScore.ts、ClickHouse scores |
| ScoreConfig | packages/shared/src/domain/score-configs.ts | Prisma ScoreConfig、annotation form |
| Dataset / DatasetItem | Prisma Dataset、DatasetItem | packages/shared/src/server/repositories/dataset-items.ts、NewDatasetItemForm.tsx |
| DatasetRun / Experiment | Prisma DatasetRuns、DatasetRunItems | worker/src/features/experiments/experimentServiceClickhouse.ts |
| DatasetRunItem 分析读模型 | packages/shared/src/domain/dataset-run-items.ts | ClickHouse dataset_run_items_rmt、dataset run item repository |
| Evaluator / Scorer | Prisma EvalTemplate、JobConfiguration、JobExecution | worker/src/features/evaluation/**、unstable evaluator API types |
| Observation-level eval | packages/shared/src/features/evals/types.ts | worker/src/features/evaluation/observationEval/** |
3.7.21 用一句话串起来
Langfuse 的业务数据流可以压缩成一句话:
线上应用产生 observations,这些 observations 通过 trace_id 组成 trace,通过 session_id 组成 session;
人或 evaluator 对 trace/observation/session/experiment run 写 score;
有价值的线上样本被沉淀成 dataset item;
dataset run 把一组 dataset items 重新跑一遍,产出新的 traces/observations 和 scores;
score analytics 再把这些结果聚合,帮助决定下一次 prompt、model 或 workflow 怎么改。这就是当前 repo 的核心抽象:不是单纯存 trace,而是围绕“观测、评价、实验、迭代”构建一套数据基础设施。