跳到主要内容

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.mddocker-compose.yml)。

一句话直觉/类比。 把它当成 agent 世界的 Datadog / Jaeger:SDK 是埋在代码里的探针, 后端是收集器和存储,前端是你盯着看的大屏——只不过它对 "LLM 调用" 这种 span 有专门的理解。


2. 顶层全景(它大概怎么转)

2.1 部件与依赖

Laminar 是一个多服务的单体仓(monorepo),真正的"引擎"是 Rust 写的 app-server

部件干什么在哪
app-serverRust 后端引擎:Actix-web 收 HTTP、Tonic 收 gRPC、后台消费者落库。本参考主角app-server/src/,入口 app-server/src/main.rs
frontendNext.js/TypeScript 网页 UI,经 SSE 实时展示 tracefrontend/
pii-redactor独立的 Rust gRPC 服务,用 ONNX 跑 PII 脱敏模型;不与 app-server 静态链接,靠 gRPC 通信pii-redactor/(见 06 章)

app-server 依赖这几样外部服务(对照 docker-compose.ymldocker-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),存储在 S3StorageMockStorage 间切换(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 从产生到显示,经过这几站:

  1. 进门。 SDK 把 span 用 OTLP 发过来。gRPC 走 Tonic 的 TraceService (traces/grpc_service.rs:48impl TraceService for ProcessTracesService,导出方法 export:49); HTTP 走 /v1/traces(main.rs:1776api::v1::traces::process_traces)。两条路最后都调 producer 的 push_spans_to_queue(traces/producer.rs:180)。

  2. 排队。 span 被投进 "observations" 队列(OBSERVATIONS_EXCHANGE / OBSERVATIONS_QUEUE, 来自 traces 模块)。有 RabbitMQ 用 RabbitMQ,没有就用进程内队列——接口一致,上层无感

  3. 消费落库。 后台的 SpanHandler(traces/consumer.rs:27)批量取出,交给 process_span_messages(traces/processor.rs:117):在这里做结构化去重、把 trace 级统计 upsert 进 Postgres、把 span 明细写进 ClickHouse、并(可选)喂给 Quickwit 建全文索引。

  4. 实时上屏。 落库时通过 Redis Pub/Sub 广播;app-server 启动时起的订阅者 (realtime::start_redis_subscriber,realtime/mod.rs:229)收到后,推给所有挂在 SSE 端点(routes/realtime.rs:18sse_endpoint)上的前端连接。连接表是一张并发 map SseConnectionMap(realtime/mod.rs:24)。

  5. 事后查询。 你在 UI 里写 SQL 查历史,请求先过进程内 SQL 校验器/查询引擎 (QueryEngine,query_engine/mod.rs:23)——它既转换 JSON↔SQL,也是拦在用户 SQL 和 ClickHouse 之间的安全边界,再打到 ClickHouse。


3. 阅读地图(建议顺序)

本参考共 7 章(含本章),按"数据流向 + 由浅入深"排序。聚焦 Rust 后端引擎,前端只在必要时点到。

顺序章节一句话
0index.md(本章)大盘、部件、数据流图、导航——先读这里判断要不要往下钻
101-ingestion-pipeline.md接入管线:一条 span 从 gRPC/HTTP 进门,经 producer 投队列,再被 consumer 批量落库的完整链路
202-span-llm-extraction.mdSpan 数据模型,以及怎么从一堆 OTel 属性里认出"这是 LLM 调用"并抽出输入/输出/工具定义
303-dedup-content-storage.md结构化去重:用 BLAKE3 对消息内容哈希,靠 shared_content 表让重复的对话历史只存一份
404-storage-realtime.md为什么同时用 Postgres 和 ClickHouse,以及自研 SSE 实时引擎(Redis Pub/Sub → SSE)怎么工作
505-sql-query-engine.md进程内 SQL 查询引擎:JSON↔SQL 转换 + 把用户 SQL 限制成"只读、只碰白名单表"的沙箱校验器
606-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 章


5. 顶层代码地图(导航索引)

想直接跳进源码的,从这张表开始(符号名可 grep,比行号抗漂移):

主题文件关键符号
服务装配 / 进程入口app-server/src/main.rsmain(依赖初始化 + producer/consumer 线程装配)
gRPC trace 收口app-server/src/traces/grpc_service.rsProcessTracesServiceimpl TraceServiceexport
HTTP trace 收口app-server/src/api/v1/traces.rsprocess_traces
gRPC logs 收口app-server/src/logs/grpc_service.rsProcessLogsService(LogsServiceServer 注册于 main.rs:1920)
投递到队列(producer)app-server/src/traces/producer.rspublish_span_messagespush_spans_to_queue
消费落库(consumer)app-server/src/traces/consumer.rsprocessor.rsSpanHandlerprocess_span_messages
消息队列抽象app-server/src/mq/MessageQueue(rabbit.rs / tokio_mpsc.rs 双实现)
内容去重app-server/src/traces/input_dedup.rstool_dedup.rsbuild_dedup_batchextract_tool_definitions
实时 SSEapp-server/src/realtime/mod.rsroutes/realtime.rsSseConnectionMapstart_redis_subscribersse_endpoint
SQL 查询引擎 / 沙箱app-server/src/query_engine/QueryEnginevalidate_queryvalidator/
缓存 / Pub-Sub / 存储抽象app-server/src/cache/pubsub/storage/CachePubSubStorage(各带 Redis/内存、S3/Mock 双实现)
PII 脱敏客户端app-server/src/pii_redactor/PiiRedactorClient(服务端在 pii-redactor/,见 06 章)
依赖清单 / 构建app-server/Cargo.tomlRust edition 2024;signals 为可选 feature
部署编排仓根 docker-compose.yml / docker-compose-full.ymllite(无 RabbitMQ/Redis)与 full 两套

各机制的细节留给后续章,index 只给大盘与跳转。想读"一条 span 端到端怎么走",直接进 01-ingestion-pipeline.md