Laminar 是什么 · 全景与阅读地图
30 秒导读: Laminar 是一个开源的、专为 AI agent 打造的可观测性平台。你的 agent 代码只加一行, 它产生的每一次 LLM 调用、每一步工具调用都会变成一条 span(一段带时间、输入、输出的执行记录); 这些 span 通过 OpenTelemetry 送进 Laminar 的 Rust 后端,去重、落库,再实时显示在一个网页 UI 上。 本参考聚焦它的 Rust 后端引擎(
app-server),不讲前端 UI 细节。
1. 这是什么(零基础也能懂)
一句话定义: Laminar 是给 AI agent 用的"行车记录仪 + 仪表盘"——把 agent 运行时发生的一切录下来, 让你事后能回放、搜索、评测、报警。
解决什么问题 / 给谁用。 假设你写了一个会自己调用 LLM、查数据库、点浏览器的 AI agent。 它跑起来是个黑盒:模型到底看到了什么 prompt?哪一步慢?哪次工具调用报错了? Laminar 让你把这些执行细节自动录下来,在网页上像看调用树一样一层层展开。
它能做什么(功能), 对照官方 README(README.md):
| 功能 | 白话解释 |
|---|---|
| Tracing(追踪) | OpenTelemetry 原生,一行代码自动追踪 Vercel AI SDK、OpenAI、Anthropic、LangChain 等 |
| Evals(评测) | 本地或 CI 里跑评测,UI 里对比结果 |
| Signals(AI 监控) | 用自然语言描述"要盯的事件",自动在 trace 里发现逻辑错误/异常 |
| SQL 访问 | 内置 SQL 编辑器,直接查 trace / 指标 / 事件 |
| Dashboards | 用 SQL 搭自定义看板 |
| 数据标注 / 数据集 | 把 trace 数据挑出来做标注、建评测数据集 |
说明:Signals(含事件聚类)是企业版功能,代码在闭源的
lmnr-private仓,本参考的克隆里只有公共桩(stub)。 本参考只讲 OSS 仓lmnr里真实存在、能读到源码的部分。
用起来什么样。 接入就是"装包 + 初始化 + 标注要追踪的函数"三步(取自 README.md):
// TS:装 @lmnr-ai/lmnr 后,一行初始化就开始追踪 LLM 调用
import { Laminar, observe } from '@lmnr-ai/lmnr';
Laminar.initialize({ projectApiKey: process.env.LMNR_PROJECT_API_KEY });
// 用 observe 包住要追踪输入/输出的函数
const poemWriter = observe({ name: 'poemWriter' }, async (topic) => { /* ... 调 OpenAI ... */ });
# Python:同样一行初始化,再用 @observe() 装饰要追踪的函数
from lmnr import Laminar, observe
Laminar.initialize(project_api_key="<LMNR_PROJECT_API_KEY>")
@observe() # 标注后,这个函数的输入/输出和内部 LLM 调用都会被追踪
def poem_writer(topic):
...
自托管则是 docker compose up -d,浏览器打开 http://localhost:5667 就是 UI(README.md、docker-compose.yml)。
一句话直觉/类比。 把它当成 agent 世界的 Datadog / Jaeger:SDK 是埋在代码里的探针, 后端是收集器和存储,前端是你盯着看的大屏——只不过它对 "LLM 调用" 这种 span 有专门的理解。
2. 顶层全景(它大概怎么转)
2.1 部件与依赖
Laminar 是一个多服务的单体仓(monorepo),真正的"引擎"是 Rust 写的 app-server。
| 部件 | 干什么 | 在哪 |
|---|---|---|
| app-server | Rust 后端引擎:Actix-web 收 HTTP、Tonic 收 gRPC、后台消费者落库。本参考主角 | app-server/src/,入口 app-server/src/main.rs |
| frontend | Next.js/TypeScript 网页 UI,经 SSE 实时展示 trace | frontend/ |
| pii-redactor | 独立的 Rust gRPC 服务,用 ONNX 跑 PII 脱敏模型;不与 app-server 静态链接,靠 gRPC 通信 | pii-redactor/(见 06 章) |
app-server 依赖这几样外部服务(对照 docker-compose.yml 与 docker-compose-full.yml;
带 * 的有自动降级 fallback):
| 依赖 | 角色 | 缺了会怎样 |
|---|---|---|
| Postgres | 事务主库:项目、trace 级聚合、队列元数据 | 必需,无 fallback |
| ClickHouse | 列式分析库:海量 span、指标、事件 | 必需,无 fallback |
| RabbitMQ * | 异步处理的消息队列 | 降级为进程内 Tokio mpsc 队列 |
| Redis * | 缓存 + Pub/Sub(驱动 SSE) | 降级为进程内缓存 / 内存 Pub/Sub |
| Quickwit * | span 全文搜索的索引引擎 | 搜索/索引功能优雅关闭 |
| S3 * | 大对象存储 | 降级为 MockStorage |
这些 fallback 在
app-server/src/main.rs的服务装配段一处处判断:Redis 见main.rs:238-274, 队列在 RabbitMQ 与TokioMpscQueue之间二选一(main.rs:313-837),Quickwit 连不上就None(main.rs:948-961),存储在S3Storage与MockStorage间切换(main.rs:878-886)。 所以最小自托管只要 Postgres + ClickHouse 就能跑起来。
2.2 一张数据流图:一条 span 从 SDK 到屏幕
先说怎么读这张图:从左到右是数据流向;三条竖线是 app-server 内部的三个网络监听口 + 一组后台消费者线程。
┌──────────────────────── app-server (Rust 引擎) ─────────────────────────┐
│ │
SDK / OTel exporter │ ①收数据 ②排队 ③消费落库 │
(你的 agent 代码) │ ┌───────────────┐ ┌──────────┐ ┌──────────────────────┐ │
─── gRPC :8001 ──▶│ │ Tonic gRPC │──push──┐ │ │ │ SpanHandler │ │
─── HTTP :8000 ──▶│ │ TraceService │ ├──▶│ RabbitMQ │──▶ │ process_span_messages│ │
/v1/traces │ │ Actix HTTP │──push──┘ │ (或内存 │ │ 去重 → 落库 │ │
│ └─── ────────────┘ │ 队列) │ └──────────┬───────────┘ │
│ └──────────┘ │ │
│ ┌─────────┴─────────┐ │
│ ④实时推送 ▼ ▼ │
前端 Next.js ◀────│ ┌───────────────┐ Redis Pub/Sub ┌──────────┐ ┌──────────┐ │
(:5667) ◀SSE:8002─│ │ SSE 端点 │◀──────────────────────│ Postgres │ │ClickHouse│ │
│ └───────────────┘ │ 事务主库 │ │ 列存 │ │
│ └──────────┘ └──────────┘ │
└──────────────────────────────────── ──────────────────────────────────────┘
三个监听口的角色在 main.rs 里泾渭分明:HTTP 在 PORT(默认 8000)、gRPC 在 GRPC_PORT(默认 8001)、
消费者线程另起一个只挂 SSE 端点的 HTTP server 在 CONSUMER_PORT(默认 8002)。producer 模式起 HTTP+gRPC
(main.rs:1618-1933),consumer 模式起后台 worker + SSE(main.rs:1034-1616);默认两者都开。
2.3 主线走一遍(高层,不进代码)
一条 span 从产生到显示,经过这几站:
-
进门。 SDK 把 span 用 OTLP 发过来。gRPC 走 Tonic 的
TraceService(traces/grpc_service.rs:48的impl TraceService for ProcessTracesService,导出方法export在:49); HTTP 走/v1/traces(main.rs:1776挂api::v1::traces::process_traces)。两条路最后都调 producer 的push_spans_to_queue(traces/producer.rs:180)。 -
排队。 span 被投进 "observations" 队列(
OBSERVATIONS_EXCHANGE/OBSERVATIONS_QUEUE, 来自traces模块)。有 RabbitMQ 用 RabbitMQ,没有就用进程内队列——接口一致,上层无感。 -
消费落库。 后台的
SpanHandler(traces/consumer.rs:27)批量取出,交给process_span_messages(traces/processor.rs:117):在这里做结构化去重、把 trace 级统计 upsert 进 Postgres、把 span 明细写进 ClickHouse、并(可选)喂给 Quickwit 建全文索引。 -
实时上屏。 落库时通过 Redis Pub/Sub 广播;app-server 启动时起的订阅者 (
realtime::start_redis_subscriber,realtime/mod.rs:229)收到后,推给所有挂在 SSE 端点(routes/realtime.rs:18的sse_endpoint)上的前端连接。连接表是一张并发 mapSseConnectionMap(realtime/mod.rs:24)。 -
事后查询。 你在 UI 里写 SQL 查历史,请求先过进程内 SQL 校验器/查询引擎 (
QueryEngine,query_engine/mod.rs:23)——它既转换 JSON↔SQL,也是拦在用户 SQL 和 ClickHouse 之间的安全边界,再打到 ClickHouse。
3. 阅读地图(建议顺序)
本参考共 7 章(含本章),按"数据流向 + 由浅入深"排序。聚焦 Rust 后端引擎,前端只在必要时点到。
| 顺序 | 章节 | 一句话 |
|---|---|---|
| 0 | index.md(本章) | 大盘、部件、数据流图、导航——先读这里判断要不要往下钻 |
| 1 | 01-ingestion-pipeline.md | 接入管线:一条 span 从 gRPC/HTTP 进门,经 producer 投队列,再被 consumer 批量落库的完整链路 |
| 2 | 02-span-llm-extraction.md | Span 数据模型,以及怎么从一堆 OTel 属性里认出"这是 LLM 调用"并抽出输入/输出/工具定义 |
| 3 | 03-dedup-content-storage.md | 结构化去重:用 BLAKE3 对消息内容哈希,靠 shared_content 表让重复的对话历史只存一份 |
| 4 | 04-storage-realtime.md | 为什么同时用 Postgres 和 ClickHouse,以及自研 SSE 实时引擎(Redis Pub/Sub → SSE)怎么工作 |
| 5 | 05-sql-query-engine.md | 进程内 SQL 查询引擎:JSON↔SQL 转换 + 把用户 SQL 限制成"只读、只碰白名单表"的沙箱校验器 |
| 6 | 06-pii-redactor.md | 独立的 PII 脱敏 gRPC 服务:在 Rust 里加载 HuggingFace 的 ONNX token 分类模型给 span 打码 |
建议路径: 想懂"数据怎么流"就顺着 01→04 读;只关心某个巧妙点,用下面第 4 节直接跳章。
4. 巧妙之处清单(读完要带走的精华 → 指向对应章)
每条先说"妙在哪",再指到讲透它的那一章:
-
结构化去重,而不是整段存。 agent 的对话历史每轮都把前面所有消息重发一遍;Laminar 对每 条消息按 规范化 JSON 做 BLAKE3 哈希,重复内容只在
shared_content里存一份、span 只存哈希引用。去重甚至前移到 producer 端(span 上队列前就把已见过的消息剥掉,省网络)。→ 03 章 -
双存储各司其职(冷热分离)。 Postgres 扛事务性的 trace 级聚合、项目、队列元数据;ClickHouse 扛海量 span 的列式分析写入(还专门调了 async-insert 参数压低 p50 延迟)。同一条 span 的两半各去各家。 → 04 章
-
SQL 沙箱是安全边界,不只是 linter。 用户能写 SQL 直查数据,校验器强制 SELECT-only、封杀危险函数、 把表名重写成按
project_id隔离的_v0视图——是拦在用户 SQL 和 ClickHouse 之间唯一的一道墙。 → 05 章 -
自研 SSE 实时引擎。 不轮询、不靠第三方推送:落库即经 Redis Pub/Sub 广播,进程内订阅者把消息扇出到 一张并发连接表上的所有 SSE 客户端,trace 边发生边上屏。→ 04 章
-
PII 脱敏用 Rust 跑 ONNX,且是独立进程。 不把 Python 推理塞进后端,而是单独一个 gRPC 服务加载 HuggingFace 导出的 ONNX 模型,best-effort 脱敏——连不上/超时都不阻塞 trace 落库。 → 06 章