Skip to content

9.1 子系统精读模板

学习目标

完成本节后,你将能够:

  1. 用同一套结构拆解 Langfuse 的任意子系统。
  2. 区分“业务抽象”“数据契约”“运行链路”“存储落点”“查询入口”。
  3. 阅读子系统时避免只按目录树游走,而是从闭环和不变量出发。

9.1.1 为什么需要子系统精读

前面的章节已经讲过五层架构、运行链路、契约和数据结构。但真正读一个 infra repo 时,工程问题通常不是“这个目录是什么”,而是:

text
某个产品能力从配置到执行再到查询,完整经过哪些对象?
哪些字段是跨进程契约?
哪些表是事实数据,哪些表是配置数据?
worker 为什么这样拆队列?
如果我要做一个类似系统,哪些抽象值得复用?

所以从这一篇开始,每个子系统都按同一套格式写。第一个完整示范是 Ingestion / Event Store 子系统,第二个示范是 Experiment 子系统,第三个示范是 Evaluation 子系统

本篇后续章节统一使用两个标记:

标记含义
【当前实现】当前 Langfuse OSS repo 已经存在的表、代码路径、队列契约、运行链路或查询逻辑。
【扩展设计】基于当前抽象推导出的改造建议、未来形态或“如果自己做类似 infra 可以怎么做”。

如果一段内容在讲“应该怎么改”“建议怎么做”“未来可以怎么扩展”,必须显式标为 【扩展设计】,不能让读者误以为当前 repo 已经实现。

9.1.2 固定拆解顺序

每个子系统都按下面 10 层拆:

要回答的问题产出
业务闭环这个子系统解决哪个真实业务问题?一句话结论和业务图。
核心对象系统里有哪些名词,哪些容易混淆?术语表。
数据契约外部输入、内部 payload、DB row 的格式是什么?schema、JSON 示例、字段语义。
控制面谁定义规则、配置、状态?Postgres model、UI/API 入口。
数据面事实数据从哪里来,怎么写入?ingestion、worker、ClickHouse 表。
执行面异步任务怎么触发、排队、重试?QueueName、QueueJobs、processor。
查询面UI/API 如何读出结果,怎么聚合?repository、query builder、tRPC router。
状态与幂等如何避免重复执行、覆盖、旧 job 不兼容?deterministic id、cache、status、不变量。
设计取舍为什么不是更简单的一张表或一个同步函数?替代方案对比。
二次开发入口新增能力应该改哪里,如何验证?文件清单和 checklist。

这个顺序很重要。先看目录树会陷入局部文件;先看闭环和契约,才能知道每个文件为什么存在。

9.1.3 三张图必须画

每个子系统至少应该有三张图:

对象关系图

回答“业务对象怎么组合”。

运行链路图

回答“一次请求或任务经过哪些进程”。

存储落点图

回答“什么放 Postgres,什么放 ClickHouse,什么放 Redis/S3”。

9.1.4 每节都要讲的四类“格式”

教程不是只讲概念,还要让读者能对着源码读。所以每个子系统都要列出四类格式:

格式例子为什么要写
配置格式JobConfigurationScoreConfig、feature settings看懂控制面如何定义行为。
输入格式public API body、SDK payload、queue payload看懂 producer/consumer 之间的协议。
中间格式S3 snapshot、worker internal object、domain object看懂为什么要多一步转换。
事实格式ClickHouse row、score record、event row看懂查询和分析为什么能成立。

如果一个章节没有这些格式,读者很容易只记住名词,不能自己实现类似 infra。

9.1.5 每节都要落到“不变量”

一个 infra 子系统最有价值的地方通常不是文件名,而是不变量。比如:

  • 队列 payload 必须兼容 rolling deploy。
  • 事实数据必须带 tenant/project 边界。
  • worker 不能把用户请求同步阻塞在慢写入上。
  • score 只能有一个主 target。
  • experiment item score 和 experiment run score 是两个层级,不能混成一列。

不变量是二次开发时最不该破坏的东西。每个子系统章节最后都会总结这些规则。

已有子系统精读