3.2 分层图解
学习目标
完成本节后,你将能够:
- 按“平面”和“链路”阅读架构图。
- 区分控制面、数据面、契约面、执行面和状态面。
- 用图解释组件为什么这样组合。
3.2.1 不要把图读成目录树
架构图里的 web、shared、worker 看起来像三层目录,但真正的边界是运行职责:
web负责同步入口和控制面;shared负责跨运行时契约;worker负责异步执行;- Postgres/ClickHouse/Redis/S3 负责不同类型的状态。
如果只按目录读,会误以为 web -> shared -> worker 是普通函数调用。实际不是。web 和 worker 的运行时连接点是 Redis/BullMQ job,类型连接点是 packages/shared/src/server/queues.ts。
3.2.2 图例升级
| 图中元素 | 架构含义 | Langfuse 例子 |
|---|---|---|
| 外部入口 | 系统边界外的调用者 | Browser、SDK、OTel、API client |
| 同步入口 | 必须在 HTTP 请求内完成的控制逻辑 | tRPC、Public API、ingestion API |
| 契约中心 | 多运行时共享的规则 | Zod schema、queue payload、FilterState、field map |
| 异步执行 | 可重试、可限速、可批量的后台处理 | ingestion worker、eval worker、export worker |
| 状态存储 | 持久化或临时状态 | Postgres、ClickHouse、Redis、S3 |
| 实线 | 主路径 | UI 查询、API 调用、ingestion 写入 |
| 虚线 | 条件路径 | secondary queue、dual-write、legacy fallback、feature/env gate |
3.2.3 按五条线读图
每条线都要回答三件事:
- 入口是谁:HTTP route、tRPC procedure、queue processor 还是 query builder。
- 契约在哪里:Zod schema、queue payload、filter state、field set 还是 DB schema。
- 状态落哪里:Postgres、ClickHouse、Redis、S3 或外部系统。
3.2.4 三种边界最重要
运行时边界
web 和 worker 是不同进程。web 不能假设 worker 立即执行,worker 也不能假设 producer 的本地 env 和自己完全一致。因此 ingestion job payload 里出现了 bucketPrefix 这种字段:producer 写 S3 时把绝对前缀放进 payload,consumer 不再自己猜路径。
这类字段不是“冗余”,而是跨进程一致性设计。
租户边界
Langfuse 是多租户系统。无论 tRPC、Public API 还是 ClickHouse query,都必须把 project/org scope 带进去。protectedProjectProcedure 会从 input 里读 projectId 并写入 ctx.session.projectId;Public API wrapper 从 API key 推出 project scope;query builder 和 repository 负责把 project filter 推到数据层。
漏掉租户边界,是读这类 repo 时最危险的问题。
存储边界
同一个用户动作可能触发多个存储:
- Postgres 保存组织、项目、session metadata、prompt 等事务状态;
- ClickHouse 保存 trace、observation、score、events 等分析状态;
- Redis 保存 BullMQ job、短期去重和调度状态;
- S3/blob 保存 raw payload 和大对象。
读链路时不要只问“写到哪个表”,还要问“这个状态为什么不放到另一个存储”。
3.2.5 shared 的位置怎么理解
shared 在图中央,是因为它把“编译时依赖”和“运行时通信”隔离开:
| 问题 | 如果没有 shared | 当前做法 |
|---|---|---|
| queue payload | producer/consumer 各自定义,滚动部署容易漂移 | queues.ts 统一 QueueName、QueueJobs、payload type |
| filter 语义 | UI、API、SQL 各自理解 operator | FilterState 和 query builder 共同解释 |
| domain schema | route、worker、repository 各自校验 | domain Zod schema 统一输入输出形状 |
| DB client | 每层自己创建连接和 error 类型 | shared server package 提供 client、repository、error |
所以 shared 的设计重点不是“减少重复代码”,而是“让跨边界协议有唯一来源”。
3.2.6 看图时的自检问题
读任意新增功能时,把它放回图上:
- 它从哪个入口进入?
- 它是否需要同步返回?
- 它是否会进入队列?
- 它的 payload/schema 在哪里定义?
- 它写入哪个状态存储?
- 它的失败由 HTTP error、BullMQ retry、secondary queue、还是业务状态处理?
- 它会不会影响 Public API、SDK 或 generated client?
能回答这七个问题,才算看懂架构图。