1.4 产品数据模型:先读产品,再读 OSS 实现
学习目标
完成本节后,你将能够:
- 用
../langfuse-docs的产品视角解释 Langfuse 的完整数据抽象。 - 区分产品模型、当前 OSS 实现、Cloud / Fast Preview 演进方向这三层。
- 解释为什么当前架构不能用“trace 是最低单位”来理解。
- 理解 eval 里
trace/dataset与event/experiment两套语义为什么并存。
1.4.1 先给结论:OSS repo 是产品模型的一个实现切片
langfuse 这个 repo 展示的是 OSS/self-hosting 代码实现。它很重要,但它不是理解 Langfuse 数据抽象的最高入口。
更高一层应该先看 ../langfuse-docs:
| 产品视角 | 回答什么问题 | 代表文档 |
|---|---|---|
| Observability | 应用运行数据如何组织? | content/docs/observability/data-model.mdx |
| Evaluation | 评价结果、评价方法、实验如何组合? | content/docs/evaluation/core-concepts.mdx |
| Scores | 所有评价结果最终以什么对象存储? | content/docs/evaluation/scores/data-model.mdx |
| Experiments | dataset、run、run item、trace、score 如何串起来? | content/docs/evaluation/experiments/data-model.mdx |
| Fast Preview / v4 | 为什么系统走向 observation-centric? | content/docs/v4.mdx |
本书讲的是当前 OSS repo,但读法必须放在产品层之下:先知道 Langfuse 想表达哪些对象,再看这个 repo 如何用 Postgres、ClickHouse、Redis、S3、Zod、queue 和 worker 实现它。
这也是为什么不能只从 schema.prisma 或 ClickHouse migration 反推系统。数据库表只能告诉你“当前怎么存”,产品文档才告诉你“这些对象为什么存在、应该如何组合”。
1.4.2 产品层对象图
Langfuse 的核心不是一张 traces 表,而是一套围绕 AI 应用运行、评价和迭代的对象网络。
这张图有三个重点:
- Observation 是运行事实的细粒度节点:trace 是容器和相关性边界,observation 才是 LLM call、tool call、retrieval、span 这些具体操作。
- Score 是评价结果的统一落点:人工标注、用户反馈、LLM-as-a-Judge、code evaluator、外部 pipeline 最终都写成 score。
- Experiment 是运行快照,不只是 dataset 的子页:dataset 是测试材料,experiment/run 是一次执行历史,run item 把 dataset item 和 trace/observation 结果连起来。
1.4.3 “最低单位”要按视角回答
如果问“当前项目的组织最低单位是不是 trace”,答案是:不是。trace 很重要,但它主要是相关性边界,不是所有能力的最低单位。
更准确的拆法是:
| 视角 | 更接近最低单位的对象 | 为什么 |
|---|---|---|
| 运行观测 | Observation / span / event row | LLM 调用、tool、retrieval、generic span 都是 observation。 |
| 相关性聚合 | trace_id | 用来把一组 observations 聚合回一次请求或工作流。 |
| 会话分析 | session_id | 用来把多个 traces 聚合成多轮对话或长流程。 |
| 评价结果 | Score | eval、人工标注、用户反馈最终都落成 score。 |
| 离线实验 | DatasetRunItem / experiment item | 把一个测试输入、一次运行产物、trace/observation 和 score 串起来。 |
| 异步执行 | JobExecution / queue payload | worker 实际处理的是 job,不是直接处理 UI 对象。 |
所以在源码里看到 traceId 很多,不代表 trace 是最低单位。更准确的运行模型是:
session_id 聚合多个 trace_id
trace_id 关联一组 observation
span_id 标识一个 observation
score 可以挂 trace、observation、session 或 dataset run
job 驱动 eval、ingestion、导出、删除等异步执行这个理解能解释当前 repo 的很多设计:
| 现象 | 只看 trace 会误解 | 放到产品模型后的理解 |
|---|---|---|
observations 有 parentObservationId | 以为只是 trace 详情页的子表 | observation 本身是可嵌套执行树。 |
| v4 events 宽表带 trace context | 以为 trace 字段重复冗余 | observation-centric 查询需要把上下文传播到每一行。 |
score 有 observationId | 以为 score 总是评价整条 trace | score 是统一评价结果,可挂到更细对象。 |
dataset item 有 sourceObservationId | 以为 dataset 只从 trace 生成 | 生产问题可以从某个具体操作沉淀成测试样本。 |
eval job 有 jobInputObservationId | 以为 evaluator 只能处理 trace | 新 eval 路径可以直接针对 observation 调度。 |
1.4.4 v4 的方向:observation-centric
Fast Preview / v4 的关键变化是:从“traces + observations 两类实体”走向“observation-centric”。trace ID 仍然存在,但更多承担 correlation identifier 的角色。
经典模型:
traces table 保存 trace 属性
observations table 保存操作节点
查询时 join / 补上下文observation-centric 模型:
每个 observation 行都携带必要 trace context
列表、筛选、聚合直接从 observation 行开始
trace_id 用来把同一组 observations 重新聚合成 trace 视图这就是为什么当前 OSS repo 里会同时看到:
| 实现形态 | 说明 |
|---|---|
legacy traces / observations / scores | 经典模型的 ClickHouse 表。 |
v4 events_full | 保存完整 observation/event 上下文和大字段。 |
v4 events_core | 面向列表、筛选、聚合的轻量投影。 |
observations_batch_staging | 从 legacy observations 到 v4 events 的过渡 staging。 |
FilterState + EventsQueryBuilder | 把 UI/API 过滤语义统一 lower 到 observation-centric 查询。 |
当前 repo 的 packages/shared/clickhouse/scripts/dev-tables.sh 注释里已经能看到这个迁移方向:events_full 是 full-fidelity event table,events_core 是 query-optimized projection,backfill 会把 legacy traces 转成 synthetic observation。
注意:../langfuse-docs/content/docs/v4.mdx 明确说明 Fast Preview 当前是 Langfuse Cloud 体验,OSS/self-hosting 迁移路径仍在推进。本书读的是 OSS repo,所以要同时理解两件事:
- 产品方向:Langfuse 的数据模型正在走向 observation-centric。
- 当前实现:OSS repo 中 legacy 表、v4 events、dual-write/backfill 和兼容逻辑会并存。
1.4.5 Evaluation 的产品模型
langfuse-docs 的 Evaluation 概念层可以拆成四个对象:
| 产品对象 | 含义 | 读源码时要找什么 |
|---|---|---|
| Score | 评价结果,统一落点 | packages/shared/src/domain/scores.ts、ClickHouse scores |
| Evaluation method | 产生 score 的方法 | EvalTemplate、LLM-as-a-Judge、code evaluator、annotation |
| Online evaluation | 对线上数据自动评分 | JobConfiguration、filter、sampling、worker eval queue |
| Experiment | 对一组输入做离线运行和比较 | DatasetRuns、DatasetRunItems、experiment UI/service |
Score 是关键,因为它把不同评价来源统一了。产品 docs 里说 score 可以挂到 trace、observation、session 或 dataset run;当前 repo 里也能看到 ClickHouse scores 的 trace_id、observation_id、session_id、dataset_run_id 以及 source=API/EVAL/ANNOTATION。
Experiments 也要注意两层语义:
| 层 | 怎么理解 |
|---|---|
| 经典数据模型 | Dataset 是测试集,DatasetRun 是一次 run,DatasetRunItem 连接 dataset item、trace 和可选 observation。 |
| 新产品语义 | Experiments 成为一等概念,dataset 是参考材料,experiment 是可比较、不可变的执行快照。 |
因此读当前 OSS repo 时,看到 DatasetRuns 不要只理解成“dataset 页面下的一张表”。它是当前实现里承载 experiment run 语义的物理模型。
1.4.6 Eval 的两套语义:legacy 与新路径并存
回到源码,会看到历史语义和新语义同时存在:
| 旧 target | 新 target | 产品含义 | 当前实现状态 |
|---|---|---|---|
trace | event | 从评价整条 trace,转向评价某个 observation。 | 旧 trace eval 仍可工作,新 observation eval 是主要演进方向。 |
dataset | experiment | 从 dataset run 语义,转向 experiment/run item 语义。 | Prisma 仍有 DatasetRuns 命名,新 UI/API 倾向 experiment。 |
这里的 event 不是普通日志事件,而是 observation-scoped evaluator:按 observation 的 type、name、metadata、input、output、trace context 命中规则,创建 eval job,最后写出 score。
两条路径可以这样读:
这张图解释了几个源码现象:
| 源码现象 | 说明 |
|---|---|
EvalTargetObject 同时有 TRACE/DATASET/EVENT/EXPERIMENT | 系统还在兼容 legacy target,同时支持新 target。 |
mapLegacyToModernTarget(trace)=event | UI/配置层会把旧语义映射到新语义。 |
fetchObservationEvalConfigs 只查 EVENT/EXPERIMENT | 新 scheduler 面向 observation eval。 |
evalService.createEvalJobs 只查 TRACE/DATASET | 老 scheduler 仍处理 trace/dataset eval。 |
scheduleObservationEvals 会上传 observation 到 S3 | eval 执行时不再重新查整条 trace,而是拿命中的 observation 快照。 |
completeEvalExecution 最后仍写 score | 无论新旧路径,评价结果统一沉淀为 score。 |
所以如果问“现在实际 work 的是不是新语义”,更准确的回答是:活跃演进的路径是 event/experiment,legacy 的 trace/dataset 仍保留并继续工作。 读源码时要看具体入口走的是哪条 scheduler,不能只看枚举名。
1.4.7 docs-level 到 repo-level 的映射
下面这张表是读源码时最重要的翻译表。
| 产品对象 | OSS repo 中的主要实现 | 存储倾向 |
|---|---|---|
| Project / org / member / API key | Prisma models、tRPC context、Public API auth | Postgres |
| Trace | legacy trace domain、trace router、trace context、trace_id | ClickHouse legacy + v4 context |
| Observation | observation domain、events table、observation eval | ClickHouse legacy + v4 rows |
| Session | trace/session 字段和聚合查询 | Postgres + ClickHouse context |
| Score | score domain、score config、score ingestion、score analytics | ClickHouse + Postgres config |
| Dataset / DatasetItem | Prisma models、dataset API/UI | Postgres |
| Experiment / DatasetRun | dataset run models、experiment UI/service | Postgres + ClickHouse run item links |
| Eval config / template / execution | JobConfiguration、EvalTemplate、JobExecution、worker eval queues | Postgres + Redis + S3 + Score |
| Prompt | prompt feature、prompt versions、playground | Postgres |
读源码时不要把这些对象都压成“表”。Langfuse 的抽象不是一组 CRUD 表,而是:
运行事实:Observation / Trace / Session
评价事实:Score / Evaluator / ScoreConfig
实验事实:Dataset / DatasetItem / ExperimentRun / RunItem
配置事实:Prompt / EvalTemplate / JobConfiguration
执行事实:Queue job / JobExecution / raw event / ClickHouse write
查询投影:events_core / analytics views / field maps1.4.8 读 OSS repo 时的三层翻译法
以后看到一个字段、表或类型,不要马上问“它在哪个目录”。先按三层翻译:
| 第几层 | 要问的问题 | 例子 |
|---|---|---|
| 产品层 | 这是观测、评价、实验、配置,还是执行状态? | observation_id 是观测节点;dataset_run_id 是实验运行;score 是评价结果。 |
| 契约层 | 谁需要共同理解这个结构? | queue payload 需要 producer/consumer 兼容;FilterState 需要 UI/API/query builder 兼容。 |
| 存储层 | 它的权威存储在哪里?是否只是投影? | JobConfiguration 在 Postgres;event analytics 在 ClickHouse;raw payload 在 S3。 |
用这个方法读代码,你会自然知道先打开哪里:
| 想理解什么 | 先读产品 docs | 再读 OSS 实现 |
|---|---|---|
| trace / observation | observability/data-model.mdx | packages/shared/src/domain/observations.ts、ClickHouse observations / events_* |
| score | evaluation/scores/data-model.mdx | packages/shared/src/domain/scores.ts、score ingestion、ClickHouse scores |
| experiment | evaluation/core-concepts.mdx、evaluation/experiments/data-model.mdx | Prisma DatasetRuns / DatasetRunItems、experiment repositories |
| observation eval | v4 docs、observation-level eval changelog | features/evals/types.ts、observationEval/*、worker eval queues |
| v4 query | docs/v4.mdx、Fast Preview FAQ | events_full、events_core、EventsQueryBuilder |
1.4.9 本书后续如何使用这个视角
后面读 OSS repo 时,遇到任何数据结构都先问四个问题:
- 它在
langfuse-docs的产品模型里是哪类对象? - 它是事实数据、配置数据、执行状态,还是查询投影?
- 它的权威存储在哪里,Postgres、ClickHouse、Redis 还是 S3?
- 它是否处在 legacy model 和 observation-centric model 的迁移边界?
这样读 schema.prisma、ClickHouse migrations、Zod schema 和 worker queues 时,你看到的就不是散落的实现细节,而是一套产品数据模型在 OSS repo 里的物理落地。
本节先建立产品语义。要把这些对象进一步对照到 Postgres、ClickHouse、Redis/BullMQ、S3/blob 的实际存储,请接着读 数据结构图谱。