我为什么把 n8n 和 Langfuse 放一起
我在 4 月份用 n8n 串了 3 个 LLM 工作流(公众号摘要、客户邮件回复、PDF 知识库问答),每条流程平均 7-12 个节点。我遇到了 3 个我必须解决的真实问题:
1. 公众号摘要工作流每周二凌晨跑失败一次,n8n 里只看到 "OpenAI node errored",不知道是 prompt 改了还是 API 限额。
2. 客户邮件回复工作流有时候回复风格突然变了,我怀疑是 prompt 注入或者模型被换。
3. PDF 知识库问答的 RAG 链路有 4 个节点串联,延迟 8 秒,但到底慢在 embedding 还是 LLM 我看不见。
Langfuse 是 MIT 协议的开源 LLM 观测平台(langfuse.com),它和 n8n 的关系在 2026 年发生了关键变化:**n8n 2.x 在 2026-04-13 官方原生支持 OpenTelemetry 导出**(N8N_OTEL_* 环境变量),所以现在 n8n 的每一个节点执行都会自动变成 Langfuse 里的一条 span,不需要装任何社区节点。
但「原生支持」≠「零配置」。我花了 3 天踩了 5 个生产环境的真实坑,本文是完整复盘。
前置环境与版本
实测的版本组合(截至 2026-06-19):
| 组件 | 版本 | 用途 |
|---|---|---|
| n8n | 2.x (N8N_OTEL_* 可用) | 工作流执行 |
| Langfuse | v3 stable (2024-12-09 发布) | LLM trace 存储与可视化 |
| PostgreSQL | 17 | Langfuse 事务数据 |
| ClickHouse | 24.x (server) | Langfuse trace 分析(v3 新增) |
| Redis | 7 | Langfuse 缓存与队列 |
| MinIO | latest (S3 兼容) | Langfuse event/media 上传 |
| Docker Compose | v2.20+ | 编排 |
官方推荐硬件(4 核 / 16 GiB / 30 GB 磁盘),但我先在 2 核 4 GiB 的开发机跑通了 3 周才迁到 4 核 16 GiB。最低跑通门槛其实只要 2 核 4 GiB,但生产建议按官方推荐。
5 个生产环境真实陷阱
坑 1:ClickHouse 在 ARM Mac 上拉不起镜像
**症状**:docker compose up 时 clickhouse-1 反复重启,错误是 Illegal instruction (core dumped)。
**根因**:Langfuse v3 的 docker-compose.yml 默认用 docker.io/clickhouse/clickhouse-server:latest(x86_64 镜像),Apple Silicon(M1/M2/M3/M4)拉取后跑不了 ClickHouse 的 SSE4.2 指令集优化。
解决方案:
services:
clickhouse:
image: clickhouse/clickhouse-server:latest # 官方多架构镜像,不是 docker.io 前缀
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse # CHANGEME
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
注意把 docker.io/clickhouse/clickhouse-server 换成 clickhouse/clickhouse-server,后者有 ARM64 manifest。验证命令:
docker manifest inspect clickhouse/clickhouse-server:latest | grep -A1 arm64
如果看到 architecture: arm64 那一段就说明多架构镜像已就位。
坑 2:langfuse-worker 启动卡 5 分钟不报"Ready"
**症状**:langfuse-worker-1 日志停在 Running database migrations... 然后无任何输出,langfuse-web-1 也卡在 Connecting to database。
根因:Langfuse v3 在第一次启动时会跑 background migrations(把 v2 的 Postgres schema 升级到 v3),但 v3 第一次启动时也会做 ClickHouse 表结构初始化,这两步加起来在 2 核机器上需要 4-6 分钟。不是死了,是在等。
解决方案:把超时阈值放到 10 分钟(不能少于 6 分钟),然后做三段验证:
# 第 1 步:worker 跑 migrations
docker logs -f langfuse-worker-1 | grep -E "migration|ready|listening"
# 第 2 步:ClickHouse 表创建
docker exec langfuse-clickhouse-1 clickhouse-client -q "SHOW TABLES FROM default"
# 第 3 步:web 启动
curl -s http://localhost:3000/api/public/health | jq .
正确顺序的日志末尾会出现:langfuse-web-1 | ✓ Ready in 2.3s。**如果你看到的是 langfuse-web-1 | ✓ Compiled successfully,那只是 Next.js 编译完成,OTLP 端点还没起来**。
坑 3:n8n 的 OTEL endpoint 写了 `localhost` 但在 Docker 网络里跑不通
**症状**:n8n 日志里看到 OpenTelemetry: Exporter failed: ECONNREFUSED 127.0.0.1:4318,但本机浏览器访问 Langfuse 正常。
**根因**:n8n 也跑在 Docker 里时,localhost 和 127.0.0.1 指向的是 n8n 容器自己,不是宿主机的 Langfuse。两个容器必须在同一 Docker network 里互相访问。
**解决方案**:用 docker-compose 把 n8n 和 langfuse-web 放到同一个 network(或者用默认 network 让两个 compose 文件共享)。我用的是第二个 compose 文件 docker-compose.n8n.yml 共享 langfuse_default network:
# docker-compose.n8n.yml
services:
n8n:
image: n8nio/n8n:2
networks:
- langfuse_default # Langfuse 启动后默认创建的 network
environment:
N8N_OTEL_ENABLED: "true"
N8N_OTEL_EXPORTER_OTLP_ENDPOINT: "http://langfuse-web:3000"
N8N_OTEL_EXPORTER_OTLP_TRACING_PATH: "/api/public/otel/v1/traces"
N8N_OTEL_TRACES_INCLUDE_NODE_SPANS: "true"
N8N_OTEL_TRACES_PRODUCTION_ONLY: "false"
networks:
langfuse_default:
external: true
name: langfuse_default # 必须和 Langfuse compose 启动时创建的 network 同名
启动顺序必须是:先 docker compose -f docker-compose.yml up -d(Langfuse),再 docker compose -f docker-compose.n8n.yml up -d(n8n),否则第二个会报 network langfuse_default not found。
坑 4:trace 进了 Langfuse 但没有 LLM token/cost 统计
症状:Langfuse UI 里能看到 n8n 每个节点的 span(HTTP Request、Set、Code),但 OpenAI/Anthropic 节点的 token 用量和 cost 全是 0。
**根因**:n8n 的 OTEL exporter 只负责"导出 span 框架",**不解析 LLM response body 里的 usage 字段**。Langfuse 要算 cost,必须拿到 input_tokens、output_tokens 和 model name。
解决方案:两条路径二选一:
**路径 A:用社区节点 rorubyy/n8n-nodes-openai-langfuse**(n8n ≥ 0.187),它会在 OpenAI 节点里直接调 Langfuse ingestion API,token 统计是自动的。装方法:
# 在 n8n 容器里
docker exec -u root n8n-n8n-1 npm install -g n8n-nodes-openai-langfuse
# 然后 n8n Settings → Community Nodes → 搜 "openai-langfuse" 安装
路径 B:在 HTTP Request 节点后插一个 Code 节点手动抓 usage(适合 Anthropic / 其他兼容 API):
// Code node, mode = "Run Once for Each Item"
const usage = $input.item.json.usage;
return {
json: {
langfuse_update: {
usage: {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens
},
model: $input.item.json.model
}
}
};
然后用 HTTP Request 节点 POST 到 http://langfuse-web:3000/api/public/ingestion 更新 trace。
坑 5:v2 升 v3 后 SDK v1.x 全报错
**症状**:之前用 Langfuse JS SDK v1.x 的应用升 Langfuse v3 后,全部 401:Authentication failed: API key not found。
根因:Langfuse 在 2024-11-11 cloud 版本 + v3 self-hosted 之后,强制使用 SDK v2+ 的 API key 格式。SDK v1.x 的 key 在新版本里直接被拒。
解决方案:
# 老项目升级 SDK
npm install @langfuse/core@latest @langfuse/tracing@latest
# 关键变更:v2 SDK 用 environment variable 区分 secret/public
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_BASEURL="http://localhost:3000" # self-hosted 必须显式设置
v1 → v2 的破坏性变更清单详见 Langfuse v2→v3 升级文档,核心 3 个:API endpoint 路径变 `/api/public/otel`、API key 校验格式变、ingestion batch endpoint 路径变 `/api/public/ingestion`。
完整 docker-compose.yml 模板
把我 3 天踩坑后的最终版本贴出来(删掉了注释,保留了所有 # CHANGEME 必改项):
# docker-compose.yml
services:
langfuse-web:
image: docker.io/langfuse/langfuse:3
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres # CHANGEME
NEXTAUTH_URL: http://localhost:3000 # CHANGEME, 必填
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-some-long-random-string} # CHANGEME
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID:-default-org}
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME:-Default}
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID:-default-project}
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY:-sk-lf-default} # CHANGEME
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME:-admin}
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD:-admin123} # CHANGEME
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL:-admin@example.com} # CHANGEME
CLICKHOUSE_URL: http://clickhouse:8123
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse # CHANGEME
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
REDIS_CONNECTION_STRING: redis://redis:6379
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret # CHANGEME
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
depends_on:
postgres: { condition: service_healthy }
clickhouse: { condition: service_healthy }
redis: { condition: service_healthy }
minio: { condition: service_healthy }
langfuse-worker:
image: docker.io/langfuse/langfuse-worker:3
environment: &langfuse-worker-env
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
CLICKHOUSE_URL: http://clickhouse:8123
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
REDIS_CONNECTION_STRING: redis://redis:6379
SALT: ${SALT:-some-other-random-string} # CHANGEME
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-must-be-32-chars-long-aaaaaaaaaa} # CHANGEME, 32 字符
depends_on:
postgres: { condition: service_healthy }
clickhouse: { condition: service_healthy }
redis: { condition: service_healthy }
minio: { condition: service_healthy }
postgres:
image: docker.io/postgres:${POSTGRES_VERSION:-17}
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres # CHANGEME
POSTGRES_DB: postgres
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 10
clickhouse:
image: clickhouse/clickhouse-server:latest # ARM Mac 必改
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ulimits:
nofile: { soft: 262144, hard: 262144 }
volumes:
- clickhouse_data:/var/lib/clickhouse
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
interval: 5s
timeout: 3s
retries: 10
redis:
image: docker.io/redis:7
command: redis-server --maxmemory-policy noeviction
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
minio:
image: docker.io/minio/minio:latest
command: server /data --console-address ":9090"
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret # CHANGEME
ports:
- "9090:9090" # MinIO console
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 3s
retries: 10
volumes:
postgres_data:
clickhouse_data:
redis_data:
minio_data:
启动后 6 个服务全部 healthy 的标准输出:
docker compose ps
# NAME SERVICE STATUS
# langfuse-web-1 langfuse-web Up (healthy)
# langfuse-worker-1 langfuse-worker Up
# langfuse-postgres-1 postgres Up (healthy)
# langfuse-clickhouse-1 clickhouse Up (healthy)
# langfuse-redis-1 redis Up (healthy)
# langfuse-minio-1 minio Up (healthy)
注意:langfuse-worker 不会显示 healthy(它没有内置 healthcheck endpoint),但如果 Up 就是正常的。
验证 trace 真的进来了
跑通后用这个最小工作流验证:
1. n8n 创建一个新 workflow
2. 加一个 Schedule Trigger(每分钟一次)
3. 加一个 HTTP Request 节点(GET https://api.github.com/zen)
4. 保存并激活
1-2 分钟后打开 Langfuse UI → 你的 project → Traces,应该看到 trace 列表里有一条记录,点进去看 span 树:
workflow.execute
└── node.execute (HTTP Request)
└── http.client.request
└── http.client.response (200, 543ms)
如果只看到 workflow.execute 一个 span 没有子节点,那是 N8N_OTEL_TRACES_INCLUDE_NODE_SPANS 没设成 true。
与 n8n 5 个集成方案横向对比
| 方案 | 接入成本 | token 统计 | 适用场景 |
|---|---|---|---|
| **n8n 2.x 原生 OTEL(本文主推)** | 低(环境变量) | 需 HTTP Request + Code 节点手动抓 | 想看完整 trace 树、不在意 cost |
| **rorubyy/n8n-nodes-openai-langfuse** | 中(社区节点安装) | 自动 | 只用 OpenAI、想看 cost |
| **rwb-truelime/n8n-langfuse-shipper**(Python) | 高(额外服务) | 自动 | 自定义 batch、已经跑 Python 服务 |
| **OpenRouter Broadcast** | 中(替换 LLM provider) | 自动 | 已经用 OpenRouter 路由多模型 |
| **HTTP Request 节点直接打 Langfuse API** | 低 | 需手动 parse | 单条工作流验证、临时调试 |
我的选择:生产环境用"原生 OTEL + OpenAI 节点替换为 rorubyy/n8n-nodes-openai-langfuse"双轨,HTTP Request 节点用 OTEL 兜底。
这套方案能解决我最初的 3 个问题吗
回到开头的 3 个真实痛点,跑通 Langfuse 后:
1. **公众号摘要工作流失败**:点 trace 能看到失败节点的 http.status_code 是 429(OpenAI 限流),加了 retry 节点解决。
2. 邮件回复风格变化:对比 trace 的 prompt 字段,发现是 Code 节点里的模板变量被同事改过,git diff 就能回滚。
3. PDF 问答延迟 8 秒:看 span 树发现 embedding 节点单独占 6.2 秒,换成 bge-m3 量化版后降到 1.8 秒。
结论:3 个问题全部可观测、可定位、可优化。这就是 Langfuse 对 n8n 用户的最大价值——把"工作流跑成功"升级为"工作流跑得对"。
常见问题 FAQ
Q:Langfuse v3 一定要 ClickHouse 吗?
A:是的。v3 强制要求 ClickHouse 做 trace 存储(v2 时代用 Postgres 存 trace,v3 拆出来)。如果磁盘 IO 顶不住,可以把 ClickHouse 数据放 S3 blob storage(Langfuse 支持 S3-as-disk 模式,配置 LANGFUSE_S3_EVENT_UPLOAD_BUCKET 等 5 个环境变量即可)。
Q:n8n 一定要 2.x 才能用 OTEL 吗?
A:是的。1.x 的 n8n 没有内置 OTEL exporter,社区方案都要求改 Dockerfile 加 npm 包。2.x 直接环境变量开关即可。n8n 2.0 stable 2025-09 发布。
Q:Langfuse v3 的最低内存是多少?
A:开发用 2 GiB 能起,但 ClickHouse 一启动就吃 1.2 GiB,Postgres 400 MiB,langfuse-web 600 MiB,langfuse-worker 800 MiB,2 GiB 会频繁 OOM kill。最低建议 4 GiB,生产建议 16 GiB。
Q:trace 量大会爆磁盘吗?
A:会。ClickHouse 默认不限制大小,建议加 max_server_memory_usage 和 max_table_size_to_drop,或者用 TTL 自动清理。Langfuse 官方有 ClickHouse 增长管理指南。
后续可以继续做的事
- **加 Langfuse 评分(Score)功能**:用 Langfuse SDK v2 给每条 trace 打 quality score(0-1),结合 LLM-as-a-judge 自动评估回答质量
- **接 Prompt Management**:把 n8n 里的 prompt 字符串替换为 Langfuse Prompt 引用,多环境(dev/staging/prod)切换不用改 n8n workflow
- **Dataset + Experiment**:把历史 trace 转成 dataset,对比新 prompt 版本在不同 dataset 上的表现
---
延伸阅读(已发布相关文章)
- n8n+Ollama+Qdrant 集成踩坑:从 Docker 网络到 RAG 实战
- n8n 自托管 5 个生产级踩坑
- Claude Code Routines + n8n 集成 5 个真实陷阱
- MCP Server 配置与调试 5 大坑
---
> 🚀 正在用 n8n 串 LLM 工作流?MiniMax Token Plan 给自托管用户每月稳定 1B token,国内直连、Anthropic/OpenAI/DeepSeek 通用额度,👉 立即参与:https://platform.minimaxi.com/subscribe/token-plan?code=E5yur9NOub&source=link
📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测
🔗 精选推荐工具
使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):