wth2jhl.online

Back

Langfuse 用户旅程闭环

“Wide Events, not Metrics, Logs and Traces.” —— Charity Majors on Observability 2.0,写进 Langfuse 的架构原则文档


一、先讲个故事#

如果你把 LLM 功能从 demo 推到生产后,经历过这种周一早晨——

周五上线了一个 RAG/Agent 功能,周一一上班发现某类请求质量下降,但没人知道是哪条 prompt、哪次模型调用、哪个 chunk 引起的。

打开 Datadog:指标说”latency 上升”,没说在哪; 打开 OpenAI 账单:花多了,没说在哪; 打开应用日志:全是 request_id,拼不出完整调用链。

这时候你需要的不是更多 metric,而是沿着 trace 把一条请求拆成 observation、看它当时用了哪个 prompt version、关联到 dataset run 和 score。

这就是 langfuse/langfuse ↗ 真正在做的事。它不叫”LLM logger”,叫 LLM engineering platform。

YC W23 出品,v4.16.0,MIT 协议,主仓库 56MB,主语言 TypeScript(Node 24 + TS 7 + tsc 6 side-by-side + pnpm 9 + turbo)。

我花了 7 分 42 秒派子代理把整个 monorepo 翻完,看完之后最想说的不是”它功能多”——而是它把”cost 与 operational simplicity”当一等架构约束的那种克制,才让它能跑得久。


二、它不是 LLM logger,而是 production feedback loop#

Langfuse 把自己定位成 LLM engineering platform——develop / monitor / evaluate / debug AI applications。

但读完仓库后我发现,真正的分析单位不是 trace,是 observation。trace 只负责把相关 observation 串成一个 correlation handle,真正能 query 的最小单元是一条 wide event:

{
  trace_id, project_id, env=prod,
  user_id, session_id, org_id,
  model=gpt-4-turbo, prompt_version=17,
  input, output, latency_ms=420,
  tokens_in=812, tokens_out=143, cost=$0.012,
  retriever=topk5, status=ok,
  tags=[experiment-A], release=2026.08
}
plaintext

一个 JSON 一条 record,任何维度都能直接 query,不用 join。

这跟传统”metrics + log + trace”三个表 join 的范式是反过来的。传统范式:

Wide Events vs Metrics/Logs/Trades

左边:metric 表说 latency=420ms,log 表说 request_id=abc,trace 表说有个 gpt-4 span。排障时必须 join。

右边:一条 wide event 把所有上下文都收齐了。这条原则被 Langfuse 写进了 .agents/ARCHITECTURE_PRINCIPLES.md 的第一条:

“Prefer wide, richly attributed events over fragmented metrics, logs, and trace records that require later reconstruction.”

这条文档不是营销话术——它对工程实践有约束力。.agents/ARCHITECTURE_PRINCIPLES.md:18-30 列了 5 条硬原则:

1. observations 是主要分析单位,trace 只是 correlation handle
2. wide events > 碎片化指标/日志/追踪
3. 保留高基数上下文,方便对 unknown unknowns 切片
4. immutable / append 优先,update 必须经业务 queue
5. 把 cost 和 operational simplicity 也当架构约束
text

每一条都有对应代码位置。我在分析报告里把它们都标了 文件:行号,不是说说而已。


三、Monorepo 是怎么”拆”的:单向 DAG#

Langfuse pnpm workspace

很多团队把 monorepo 拆成 web / worker / packages,但拆完之后依赖关系会变成一团乱麻。Langfuse 不是——它把依赖方向写成机器可读的协议:

web      → @langfuse/shared, @langfuse/ee
worker   → @langfuse/shared
ee       → @langfuse/shared
shared   → (no imports from web/worker/ee)
text

来源是 AGENTS.md:74-80。shared 不允许反向 import web 的页面组件,worker 不允许自己定义 domain schema——这条线比”建几个目录”重要得多。

pnpm workspace + turbo.json + packageExtensions 共同表达三件事:

  1. 依赖图可见(shared 是共同底座)
  2. 构建顺序可推导(依赖包先编译)
  3. 开发反馈更快(filter + graph task)

仓库里有个细节很有意思:它故意把 TypeScript 7 作为部分包的主编译链,同时保留 tsc 6 作为兼容侧链——pnpm-workspace.yaml:25-30 里直接写了注释:

“TypeScript 7 has no JavaScript compiler API yet. Keep Storybook’s toolchain on the TypeScript 6 compatibility package while Next builds with 7.”

翻译:他们在主动试验编译器新语法,但没把全仓变成不可回滚的迁移。这种克制让大 monorepo 活得久。


四、数据流:事实层 + 配置层 + 解释层#

数据流全景图

把链路拆开看更清楚——

SDK / OTLP  ──┐
              ▼
       Web API  ────> S3 batch ────> BullMQ ────> ClickHouse (events)
       (auth + 校验)                                  │
              │                                       ▼
              ▼                                 Dashboard / Eval
       Postgres (元数据)                            │
              │                                       ▼
              └──────────>  Prompt/Dataset/Monitor (配置层) ──> Dashboard 解释层
text

Langfuse 把数据分了三层:

层例子特性
事实层events / observations / scoresappend-only + retention policy
配置层prompt / dataset / monitorCRUD + 版本化
解释层dashboard / saved view可重建

这是从 [.agents/ARCHITECTURE_PRINCIPLES.md:18-33] 提炼出的核心——事件事实不变、控制面可变。

你改 prompt 时不需要改历史 trace;删 dataset item 时不污染已经发生的 observation;trace 删除走专门的 TraceDelete / ProjectDelete 队列(packages/shared/src/server/queues.ts:380-382),不是每条记录上做 update 竞争。

事件”删除”是有语义的异步操作——这条细节体现了 Langfuse 对 immutability 的理解不是教条,而是工程权衡。


五、28 个 BullMQ 队列:分片是为了隔离热点#

28 队列 8 类

worker/src/queues/ 下面挂着 28 个独立的 BullMQ 队列文件——我数过——按业务职责归为 8 类:

类别代表队列
采集ingestionQueue, otelIngestionQueue
评测evalQueue, experimentQueue
导出batchExportQueue, coreDataS3ExportQueue
数据集datasetDelete
监控monitorQueue, cloudSpendAlertQueue
集成mixpanelIntegrationQueue, postHogIntegrationQueue
AgentinAppAgentRunQueue
清理dataRetentionQueue, projectDelete, traceDelete

28 个不算夸张——但 worker/src/queues/shardedQueueRegistry.ts 把其中 9 类按 project/job type 拆成多个物理 shard:

Queue Sharding

逻辑 IngestionQueue 在 registry 里解析成 ingestion-0 / ingestion-1 / ingestion-2 三个物理队列——热点 project 切走,避免一个客户打满整个 BullMQ。

这套设计的取舍是:BullMQ 单点 → 加分片 → 多数据库 → 一致性靠 queue name 约定而非 runtime 强约束。

worker 自己遵守一条原则叫 “delivery-only + durable run row”:queue 只负责送达,状态回写 Postgres。这样即使队列重启,run 状态也能从数据库恢复。


六、ClickHouse + Postgres,为什么不是 TimescaleDB?#

三存储对比

这是 ARCHITECTURE_PRINCIPLES.md 里没明说但代码里能看出来的选择:他们用 Postgres 管关系事实,用 ClickHouse 管分析事实。

为什么不直接上 Postgres + TimescaleDB?我把对比表列出来你就懂了:

维度PostgresClickHouseTimescaleDB
写吞吐中小 · 强事务极高 · append + batch中等 · hypertable
聚合扫描B-tree/全表列裁剪 · 稀疏索引BRIN · chunk 索引
OLTP/分析冲突独立独立⚠ 同一实例争资源
运维复杂度低新增分布式列存中(扩展)

Langfuse 的体量(一周几百万 trace + 几千万 observation + 多租户 + 复杂聚合 dashboard)下,TimescaleDB 会让 OLTP 和分析在同一实例争抢连接和 CPU。ClickHouse 是有意的复杂度——他们赌的是”未来数据量会大到必须用列存”。

但这个赌也不是没成本。多数据库一致性靠约定、不靠 runtime 强约束——你在代码里看到 trace_delete_queue 这种命名,而不是 middleware 自动保证。

.agents/ARCHITECTURE_PRINCIPLES.md:31-33 把这条写得很硬:

“Treat cost and operational simplicity as architectural constraints. Extra databases, queues, materialized views, and migrations must earn their long-term operational burden.”

翻译:每个新组件都要为长期的运维成本自证价值。这种克制在 5 年期的项目里特别重要。


七、Prompt 版本化:历史不能被”覆盖”#

Prompt 版本闭环

LLM 项目里最痛的不是 prompt 写不出来,是线上跑着的 prompt 不知道哪天被人改了。

Langfuse 的解法是:Prompt 是带 version 的对象,prompt_name + prompt_version 是 trace 的关联键。label 单独管理”当前指向谁”:

  • v17 在 production 跑着
  • v18 在 staging
  • 实验 X 临时切到 v18 跑 eval
  • score 0.82 vs 0.74 写回 dashboard
  • 决定 promote v18 后,只改 label,不删 v17

packages/shared/src/domain/prompts.ts 配合 packages/shared/src/server/services/PromptService/index.ts 实现。读路径有 cache,写路径走专门的 prompt 更新队列。

这条对生产稳定性的影响:你随时能”回滚” prompt 到任何一个历史版本,而且对应那条历史 trace 不会被改写。


八、In-app Agent:worker 只发 runId,sandbox 隔离执行#

In-app Agent + Sandbox

packages/shared/src/in-app-agent/ + worker/src/queues/inAppAgentRunQueue.ts + @repo/in-app-agent-sandbox-runtime 共同构成了 Langfuse 的”内置 agent 运行环境”。

设计上很妙:

Web UI  ──>  Worker (inAppAgentRunQueue)  ──>  Sandbox Runtime
   │                  │                          (CPU/Mem/网络隔离)
   │                  │                              │
   ▼                  ▼                              ▼
   UI 状态        Postgres: typed              执行结果回写
                  request / claim / result        Postgres
text

worker 不知道 agent 在 sandbox 里跑什么——它只投递 runId。typed request / claim / result 都写进 Postgres,所以状态可回放、可恢复。

sandbox-runtime 这个独立 package(packages/in-app-agent-sandbox-runtime)只依赖 zod,体积小、边界清晰。Langfuse 把”agent 在 sandbox 里跑什么”封装成可独立部署、可替换的 runtime——这是 open-core 的边界艺术。


九、最让我想偷的设计:.agents/ 目录#

.agents 治理闭环

仓库根目录有个 .agents/——不是某个 IDE 的插件目录,是 repo-owned, tool-neutral 的 agent source of truth:

.agents/
├── README.md                  # 怎么用这套约定
├── AGENTS.md                  # 项目结构、命令、验证、PR/ticket 规则
├── ARCHITECTURE_PRINCIPLES.md # 5 条硬原则 + 默认做法
└── skills/                    # 可复用的 agent skill
text

里面有几条让我看到时拍桌子的规约:

▸ 严禁把 LFE-1234 等内部 ticket 写进 public docs / commit / PR / changelog
▸ 每个 PR 自动起一份完整 preview 环境,agent 可以直接 kubectl 看日志
▸ specs/ 目录:Linear ticket → 轻量 RFC(schema + tRPC + RBAC + UI + 待决议题)
▸ 测试必须先写最小的失败测试,禁止"ad-hoc 脚本"或"raw ClickHouse insert"
text

这些不是技术约束,是协作协议——把”大型 monorepo 的知识流失”当工程问题来治。

我读了 specs/lfe-8485-project-level-default-saved-views.md——一份完整的 RFC:产品行为、Postgres schema、tRPC contract、RBAC 表、UI 行为、未决问题,全部写在一份文档里。Linear ticket 转出来 5 分钟后任何 agent 都能接力。

这套”让 AI agent 给自己贡献代码”的设计哲学,跟 OpenClaw 我们自己用的 AGENTS.md 是同一个母题:把隐性协作规则写进仓库,机器和人共读。


十、和同类项目对比#

项目定位和 Langfuse 的边界
Arize Phoenix开源 LLM observabilityPhoenix 更轻,缺 eval/experiment
HeliconeLLM gateway + observability偏网关层,缺 prompt/dataset 版本化
LangSmithLangChain 配套绑定 LangChain,闭源
LangfuseLLM engineering platformobservability + eval + dataset + prompt + RBAC 一体

vs 我们之前看过的几个项目,Langfuse 的定位很清晰:

  • vs OpenViking:OpenViking 是 Agent 的记忆系统,Langfuse 是 Agent 的生产工作流可观测性
  • vs TencentDB Agent Memory:TencentDB 把 memory 资产化,Langfuse 把 LLM 调用 可运营化
  • vs Strix:Strix 是用 AI 做安全测试,Langfuse 是给 AI 应用做生产监控

Langfuse 能 YC W23 + Cloud 版本 + 商业化,别的 OSS 同类不行——我觉得核心是:它一开始就把”控制面元数据 + 分析事实 + RBAC + EE 边界”这四件事做对了,而不是只做一个 logger 然后想着后面加。


十一、最值得偷的 5 个思想#

不抄代码,抄思想:

  1. 代码层 architecture manifest:写一份 ARCHITECTURE_PRINCIPLES.md,把”我们如何写代码”变成可读可引用的协议。新成员 onboarding 第一件事是读这个。
  2. 轻量 RFC = ticket + schema + API + UI + 待决议题。一份文档打通关,省去”开了 5 个会议还没对齐”的成本。
  3. 事实层 + 配置层 + 解释层三分法。事实 append,配置 CRUD,解释可重建。immutability 是原则不是教条——可变性要分层。
  4. Wide event 作为跨服务诊断总线。一条 record 包含所有维度,排障时直接 query,不用 join。比 metrics+log+trace 三表 join 强一个数量级。
  5. Queue 的”delivery-only + durable run row”模式。队列只负责送达,状态回写 Postgres——这样即使队列重启,run 也能恢复。

十二、风险与缺点#

客观说几条:

自托管并不轻。看 docker-compose.yml:web + worker + Postgres + ClickHouse + Redis + MinIO 一套部署下来,对中小团队是真实门槛。Langfuse Cloud 的存在不是偶然。

EE 与 license check 带来 open-core 边界。ee/src/ee-license-check/ + 各种 hasEntitlement 调用——商业化不可避免,但社区 fork 难度上升。

双存储的规模拐点。Postgres + ClickHouse 在体量小时显得重,TimescaleDB 更轻;但体量一大,OLTP 和分析在同一实例争资源就成了真实问题。Langfuse 赌的是前者。


十三、我会推荐这个项目吗?#

会,但只推荐给三类人:

  1. 正在把 LLM 功能从 demo 推到生产的团队——Langfuse 的 prompt 版本化 + score 回写 + eval queue 是真能救火的。
  2. 维护大型 monorepo 的工程团队——.agents/ 目录的设计哲学可以整套学。
  3. 在做 AI agent 给自己贡献代码这件事的人——specs/ + ARCHITECTURE_PRINCIPLES.md + PR preview 这套”机器可读协作协议”是工程化样本。

我读完最想偷的不是某个脚本,而是它把”cost 与 operational simplicity”也当一等架构约束的那种克制。

LLM 项目里不缺”功能强大”的 framework,缺的是能跑 5 年还不出大事故的工程骨架。

Langfuse 至少示范了一件事:克制 > 聪明。


项目地址:github.com/langfuse/langfuse ↗

引用与延伸阅读:

YC W23 的 Langfuse,把 LLM 工程化做成了"可运营系统"
https://wth2jhl.online/blog/langfuse-llm-engineering-platform/
Author 小吴同学
Published at August 21, 2026