Langfuse 自托管:AI agent 追踪与 VPS 配置
在自有 VPS 上运行 Langfuse v4,了解最低内存、固定镜像标签、TLS 配置,以及 ClickHouse 保留策略和可恢复备份,避免磁盘被数据填满。
为何要跟踪 AI agent
您可以自行托管 Langfuse,查看 agent 在一次运行中实际执行了什么。Langfuse 是一个开源 LLM(大语言模型)可观测性工具。它会记录每条提示词、每次模型响应、每次工具调用和每个 token,然后将这些记录归入一个可打开查看的 trace。将其运行在您自己的 VPS 上,可以确保这些提示词不会离开您控制的服务器。
原因很简单。无法看到的问题,就无法修复,无论是成本问题还是质量问题。服务商账单只能告诉您,周二的成本是周一的 4 倍。trace 则会告诉您是哪次 agent 运行导致了这一结果,哪条提示词增长到 40,000 个 token,以及哪个重试循环在放弃前运行了 9 次。账单给出的是数字。trace 给出的是产生该数字的代码。
本指南会使用 3 个术语。trace 是 agent 的一次端到端运行。observation 是该运行中的一个步骤:普通代码对应一个 span,调用模型对应一次 generation。score 是附加到 trace 上的一个数字,可以来自人工审核或自动评估器。Langfuse 使用 OpenTelemetry(OTel),这是分布式跟踪的厂商中立标准,因此您现有的埋点可以将数据发送到 Langfuse。
实际运行 Langfuse 自托管所需的组件
Langfuse v4 不是单个容器。它由两个应用容器和四个存储服务组成。在单台 VPS 上,这 6 个组件都会运行在您的服务器中。
langfuse-web提供 Web 界面和数据摄取 API。langfuse-worker在后台处理队列。它解析数据摄取批次、计算成本,并运行每晚的保留任务。- Postgres 保存用户、组织、项目、API keys 和 prompts 等事务数据。
- ClickHouse 保存 trace 数据本身,即 observations 和 scores。它是为分析查询构建的列式存储,因此即使仪表板查询超过一亿行,仍能快速返回结果。
- Redis 是 Web 与 worker 之间的队列和缓存。
- MinIO 在服务器上提供 S3 兼容的对象存储。它保存所有原始传入事件,以及您附加的媒体文件。
Langfuse 为实际执行工作的 3 个组件公布了最低资源要求。
The data behind this chart
[
{
"label": "ClickHouse",
"cpu_cores": 2,
"memory_gib": 8
},
{
"label": "Langfuse web",
"cpu_cores": 2,
"memory_gib": 4
},
{
"label": "Langfuse worker",
"cpu_cores": 2,
"memory_gib": 4
}
]仅 ClickHouse 就需要 8 GiB 内存。Web 容器和 worker 各需要 4 GiB。这些是 Langfuse 标注资源需求的 3 个组件的最低要求。Postgres、Redis 和 MinIO 还需要额外占用内存。项目自己的 Docker Compose 指南建议使用 4 核、16 GiB 内存和约 100 GiB 存储空间的服务器。这个建议正好符合上述计算结果,并没有额外留出太多余量。
不要在 2 GiB 规格的计划上部署。ClickHouse 启动后会在一段时间内接受写入,但会在后台合并期间退出,因为合并操作会将表的大型数据部分载入内存。您会看到 docker compose ps 将 clickhouse 容器报告为 restarting,dmesg 携带类似 Out of memory: Killed process 1234 (clickhouse-serv) 的日志行,并且 Langfuse 的所有仪表板都返回 500。在负载较轻时,ClickHouse 可能直接拒绝查询,并记录 DB::Exception: Memory limit (total) exceeded。如果每天只发送几千条 trace,8 GiB 足以供一名开发者使用。规划部署时应按 16 GiB 计算。如果同一台 VPS 还要运行其他服务,请单独为这些服务预留资源,因为即使是 自托管 AFFiNE 工作区 这样相对轻量的技术栈,也需要占用数 GiB 内存,而 ClickHouse 不会释放这些资源。
使用 Docker Compose 部署 Langfuse
克隆代码仓库。该堆栈、服务连接配置和默认环境变量都位于其中的 docker-compose.yml。
git clone https://github.com/langfuse/langfuse.git
cd langfuse该文件中所有必须修改的值都标记为 # CHANGEME。首先生成 3 个应用密钥。
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -base64 32 # SALT
openssl rand -hex 32 # ENCRYPTION_KEYENCRYPTION_KEY 必须是使用 64 个十六进制字符表示的 256 位值,而 openssl rand -hex 32 输出的内容正好符合要求。它用于加密静态存储的敏感值,包括您保存在实例中的 LLM 提供商密钥。数据写入后再修改它将导致相关数据无法解密,因此从首次启动起就应将其视为永久值。SALT 用于哈希 Langfuse API 密钥,因此修改它会使代理当前使用的所有密钥失效。
然后设置 POSTGRES_PASSWORD、CLICKHOUSE_PASSWORD、REDIS_AUTH 和 MINIO_ROOT_PASSWORD。MinIO 密码会出现 4 次:第一次是 MINIO_ROOT_PASSWORD,之后还要设置为 LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY、LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY 和 LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY。漏掉任何一处,MinIO 都会使用 SignatureDoesNotMatch 拒绝该客户端;此错误会出现在 worker 日志中,而 Web 界面仍可能看起来正常。将这些值保存在 env 文件中,而不是写入受版本控制的 compose 文件,是 Docker Compose 环境文件和密钥 中介绍的做法。
开始前固定镜像标签
随附文件使用 langfuse/langfuse:4 和 langfuse/langfuse-worker:4。这些标签会继续移动。Langfuse 启动时会自动执行 Postgres 和 ClickHouse 迁移,因此几个月后例行执行一次 docker compose pull,可能会在当天没有备份的数据库上触发计划外的架构迁移。在 docker-compose.override.yml 中将两者固定到同一个版本。Compose 会将该文件合并到随附文件之上,因此后续执行 git pull 时不会覆盖您的修改。
services:
langfuse-web:
image: docker.io/langfuse/langfuse:4.3.1
langfuse-worker:
image: docker.io/langfuse/langfuse-worker:4.3.1截至 2026 年 8 月,4.3.1 是当前的 4.3 版本(之后已发布 4.4.0)。查看项目的 GitHub releases 页面,固定部署当天的当前版本,然后有计划地更新该版本号。随附文件中的存储镜像已经固定到主版本,包括 postgres:17、clickhouse-server:25.12 和 redis:7,也应采用相同做法。该规则并不只适用于 Langfuse:自托管的 openGym 训练记录器 只运行该堆栈的一小部分,但仍应使用明确命名的 git 标签,因为任何启动时会自行迁移数据库的应用,都可能将一次例行拉取变成架构变更。
启动服务。
docker compose up -d
docker compose ps
docker compose logs -f langfuse-worker首次启动会执行迁移,因此请等待 1 到 2 分钟后再检查服务是否响应。docker compose ps 应列出 6 个状态为 running 的服务。如果 worker 循环重启,请查看其日志获取原因:CLICKHOUSE_MIGRATION_URL 使用 ClickHouse 的原生协议连接 9000 端口,而不是 HTTP 端口 8123。将其指向 8123 会导致该服务失败,但 Web 容器仍可能看起来正常。
从服务器本机检查健康状态。
curl -s "http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/public/ready普通的 /api/public/health 请求只能证明 API 进程仍在运行,因为它会主动跳过数据库检查,以便 Postgres 短暂中断时服务仍能继续提供服务。failIfDatabaseUnavailable=true 形式才适合交给监控系统检查;数据库无法访问时,它会返回 503。迁移完成且容器可以接受流量后,/api/public/ready 会返回 200。这两项都是普通的 HTTP 检查,因此 Uptime Kuma 状态页 可以监控它们,并在您的代理发现问题前通知您堆栈已停止服务。
在前端配置 TLS 并关闭多余端口
随附的 Compose 文件会为 Web 容器发布 3000:3000,为 MinIO 发布 9090:9000。这两个端口都会绑定到所有网络接口。对于公网 IP,这意味着任何扫描 3000 端口的人都能访问注册页面,任何扫描 9090 端口的人都能访问存放原始提示词的存储桶。
仅添加防火墙规则并不能关闭这些端口。Docker 会将自己的 DNAT 规则写入 nat 表,并且这些规则会在数据包到达 ufw 的 filter 规则之前生效,因此 ufw deny 3000 仍会使已发布的端口保持开放。这个问题很常见,因此已有专门的指南说明为什么 Docker 发布的端口会绕过 ufw。改为在 override 文件中将端口绑定到 loopback。
services:
langfuse-web:
ports:
- "127.0.0.1:3000:3000"
environment:
NEXTAUTH_URL: https://langfuse.example.com
minio:
ports:
- "127.0.0.1:9090:9000"
- "127.0.0.1:9091:9001"NEXTAUTH_URL 必须是包含 scheme 的完整公网地址,因为登录流程会根据该值构造回调 URL。如果在 HTTPS 代理后仍将其保留为 http://localhost:3000,登录往返过程会将浏览器重定向到无法访问的地址。
现在让反向代理指向 127.0.0.1:3000,并由反向代理管理证书。同一 Compose 项目中的 Traefik 通常是合适的选择,其路由标签见通过一个 Traefik 反向代理运行多个应用。如果 Langfuse 是这台服务器上唯一的应用,Caddy 用两行配置即可完成相同工作。使用 curl -sI https://langfuse.example.com/api/public/ready 验证,然后从另一台机器确认 curl http://YOUR_IP:3000 现在会超时。
MinIO 有一个注意事项。Langfuse 会通过指向该 S3 端点的预签名 URL,将关联的媒体文件提供给浏览器。因此,如果您使用包含图像或音频的多模态追踪,只有 loopback 的 MinIO 会导致这些附件无法加载。在为 MinIO 配置代理前,请先阅读 blob 存储配置页面,因为写入预签名 URL 的端点必须与您发布的端点一致。纯文本追踪不受影响。
首次访问时创建您的账户,然后确保该实例只由您使用。将 LANGFUSE_ALLOWED_ORGANIZATION_CREATORS 设置为您自己的电子邮件地址,这样访问该页面的陌生人就无法在您的服务器上创建组织。如果您已经在使用Authentik 作为您自己的身份提供商,Langfuse 支持标准 OIDC 连接,因此账户会与其他应用一起创建和移除,而不必维护只有这台服务器知道的独立密码列表。
发送第一条追踪记录
在 Web 界面中创建项目,然后从项目设置中复制其公钥和密钥。Python SDK 会读取 3 个环境变量。
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://langfuse.example.com"LANGFUSE_BASE_URL 是 SDK v4 中的变量名。SDK v4 于 2026 年 3 月发布。旧代码和旧指南使用 LANGFUSE_HOST。如果追踪记录被发送到 Langfuse Cloud,而不是您的服务器,原因是基础 URL 未设置,因为默认值指向托管实例。
pip install langfuse opentelemetry-instrumentation-anthropic anthropicimport os
from anthropic import Anthropic
from langfuse import get_client, observe
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
AnthropicInstrumentor().instrument()
langfuse = get_client()
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
@observe(as_type="tool")
def lookup_order(order_id: str) -> str:
return f"order {order_id}: shipped"
@observe()
def handle_request(question: str) -> str:
context = lookup_order("A-1042")
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{"role": "user", "content": f"{context}\n\n{question}"}],
)
return message.content[0].text
if __name__ == "__main__":
assert langfuse.auth_check()
print(handle_request("Where is my order?"))
langfuse.flush()@observe 装饰器会在函数周围创建一个观测,捕获函数参数和返回值,并将其嵌套在当前已激活的观测下。AnthropicInstrumentor 是 Anthropic 客户端的 OpenTelemetry 插桩,它会将每次 messages.create 调用转换为一条 generation,记录模型名称、token 用量和延迟,无需修改调用位置。
这两个调用会为您完成检查。langfuse.auth_check() 会在密钥无效或基础 URL 错误时返回 False。这样可以更快定位问题,而不必猜测仪表板为何为空。langfuse.flush() 会阻塞,直到队列中的 span 全部发送完毕。短生命周期进程必须调用它,因为 SDK 会在后台批量发送数据;如果脚本立即退出,尚未发送的批次也会随进程一起丢失。
如果整个团队都在发送 trace,而不是只有一个脚本发送,请在所有 agent 已经通过的网关中统一设置这 3 个变量,不要在每个人的 shell 中分别设置。这样,自托管的 OneCLI harness 就能为每位同事的运行保留监控,同时将密钥集中存放。
ClickHouse 为什么会持续增长?
Trace 是大多数人自行托管时增长最快的数据。每次 agent 运行都会为每个步骤写入一行记录,并完整保存输入和输出。因此,提示词较长且输出频繁的 agent 每天产生的字节数,可能远超其监控的应用。任其增长,ClickHouse 最终会占满磁盘;磁盘满后,数据写入会停止,而不是只是变慢。
这里有两类数据会增长,需要分别处理。
第一类是您自己的 trace 数据,解决方法是配置保留期限。在 Web 界面中打开项目设置,并按天设置数据保留期限。Langfuse 接受的最短期限为 3 天。随后,夜间任务会选出超过该期限的 traces、observations、scores 和媒体资源,并从 ClickHouse 和 blob storage 中删除。该任务需要对 bucket 具有 DeleteObject 权限,默认 compose 文件中的 MinIO root 凭据已经具备此权限。删除不可恢复,因此如果需要长期历史记录,请先配置 blob storage 导出。不要直接为 Langfuse 自有表编写 TTL 子句:保留任务负责让 ClickHouse 与 bucket 保持同步,而手动 TTL 只会删除其中一侧的数据。
根据实际使用方式选择保留期限。成本和质量审核通常针对几天前的数据,而不是几个月前的数据。对于小团队,30 天是合理的起点;如果您只在出现故障时查看 trace,14 天通常就够用。
第二类是 ClickHouse 自己的系统日志表。配置保留期限后磁盘仍持续增长,通常就是因为这些表。ClickHouse 会为自身诊断写入 trace_log、text_log、opentelemetry_span_log、metric_log 和 asynchronous_metric_log。这些表默认没有 TTL,Langfuse 也不会读取它们。先确认磁盘空间实际被什么占用。
SELECT table, formatReadableSize(size) AS size, rows FROM (
SELECT table, database, sum(bytes) AS size, sum(rows) AS rows
FROM system.parts
WHERE active
GROUP BY table, database
ORDER BY size DESC
)使用 docker compose exec clickhouse clickhouse-client --password "$CLICKHOUSE_PASSWORD" 运行。若系统表接近列表顶部,请通过配置覆盖文件关闭它们,因为 ClickHouse 启动时会将 /etc/clickhouse-server/config.d/ 中的每个文件合并到主配置中。
<clickhouse>
<trace_log remove="1"/>
<text_log remove="1"/>
<opentelemetry_span_log remove="1"/>
<asynchronous_metric_log remove="1"/>
<metric_log remove="1"/>
</clickhouse>挂载该配置文件,然后重启 ClickHouse。
services:
clickhouse:
volumes:
- ./clickhouse-config.d/system-logs.xml:/etc/clickhouse-server/config.d/system-logs.xml:ro这会阻止新的写入。磁盘上已有的行不会自动删除,因此请使用 DROP TABLE IF EXISTS system.trace_log 显式回收空间;对于移除的每个表,也执行相同操作。如果您希望保留诊断数据,可以改为为每个表配置激进的 TTL,而不是使用 remove="1";Langfuse 的扩展文档对此有具体说明。
还有一张表值得了解。blob_storage_file_log 会记录上传到 bucket 的事件文件。如果您同时为 bucket 配置了生命周期策略,请为该表设置匹配的 TTL,避免两者逐渐不一致。
ALTER TABLE blob_storage_file_log MODIFY TTL created_at + INTERVAL 30 DAY DELETE;同时为数据磁盘配置一个简单的 df -h 告警。Trace 不会平稳增长。发布新 agent 的当天,数据量可能突然增加;数据写入失败不应成为您发现这一问题的第一个信号。
备份 Postgres 和 ClickHouse
Langfuse 备份由三部分组成。Postgres 保存用户、组织、项目和 API 密钥。ClickHouse 保存追踪记录。MinIO 保存原始事件。只恢复 Postgres,您可以正常登录,但没有历史记录。只恢复 ClickHouse,您可以恢复历史记录,但没有人能够登录查看。这种拆分并非 Langfuse 独有,自托管的 Chatwoot 支持台也采用相同结构:如果创建 Postgres 转储时没有包含 uploads 目录,恢复后的会话将全部丢失附件。
Postgres 是普通的 pg_dump,这也是 Langfuse 备份文档推荐的方法。
docker compose exec -T postgres pg_dump -U postgres postgres \
| gzip > langfuse-pg-$(date +%F).sql.gzClickHouse 需要更加谨慎,因为在合并运行期间复制正在使用的数据目录,得到的备份并不一致。在单台服务器上,最简单的方法是停止容器并归档卷。
docker compose stop clickhouse
docker volume ls | grep clickhouse
docker run --rm -v langfuse_langfuse_clickhouse_data:/data -v "$PWD":/backup alpine \
tar czf /backup/langfuse-ch-$(date +%F).tar.gz -C /data .
docker compose start clickhouse请使用 docker volume ls 输出的卷名称,而不是 YAML 中写入的名称。文件声明了 langfuse_clickhouse_data,Compose 会在其前面添加项目名称,因此位于名为 langfuse 的目录中的副本会生成 langfuse_langfuse_clickhouse_data。如果名称错误,docker run 会直接创建一个新的空卷,不会报告错误,您的归档文件也将不包含任何数据。
Web 容器会先将每个传入事件写入存储桶,再由 worker 处理,因此短时间停止 ClickHouse 通常只会导致 worker 稍后重试。在低峰时段执行,并尽量缩短停止时间。对于负载较高的实例,可以使用 ClickHouse 自带的 BACKUP DATABASE default TO S3(...) 语句,在不停止服务器的情况下创建一致的备份。MinIO 是第三部分,使用 mc mirror 或将 MinIO 复制到服务器外部的存储桶即可覆盖这部分。无论采用哪种方式,都要将备份移出服务器;VPS 上的加密 restic 备份就是为此准备的。
Redis 不需要备份。它保存队列和缓存,因此丢失 Redis 只会导致当前正在处理的事件丢失,不会影响更早的数据。
一致性限制确实存在,应该明确说明。Postgres 和 ClickHouse 在不同时间点创建转储,因此恢复后可能出现项目记录没有追踪记录,或者追踪记录所属的项目已经不存在的情况。Langfuse 可以容忍这种情况,但应尽量在相近时间创建两个转储,并选择低流量时段执行。事件存储桶才是真正的安全保障,因为 Langfuse 会在处理每个传入事件前先将其持久化到该存储桶中。
至少应将备份恢复到一次临时环境中。这样可以现在发现错误的卷名称,而不是等到服务中断时才发现。
首先查看什么
以下4项应在第一周完成检查。
- 每次追踪的成本。 Langfuse 根据模型名称和令牌用量计算成本,因此应按成本对追踪记录排序,并从头到尾查看成本最高的一条。问题通常出在不断增长的提示词上:将完整文档粘贴到上下文中,或保留了无人清理的对话历史。看清问题后,控制 AI 代理的成本就会从猜测变成工程任务。
- 按输入和输出拆分的令牌用量。 输入令牌数量多且价格低,输出令牌数量少但价格高,缓存的输入令牌价格更低。Claude Code 如何计算令牌用量对相同的计费方式进行了详细说明,该方式也适用于您自行编写的任何代理。
- 延迟百分位数。 中位数会掩盖问题。超时通常出现在 p95 和 p99;在代理循环中,p95 延迟较高的工具调用会随着迭代次数增加而被重复放大。
- 失败的工具调用。 按级别
ERROR筛选观测记录。失败率为 5% 的工具在总体成功率中可能不明显,但在追踪记录中非常明显;您可以看到模型不断重试,随后消耗令牌来绕过该工具。
在部署当天设置数据保留窗口,并选择一个每周同一天检查的仪表板。无人打开的可观测性工具最终只会变成一个填满磁盘的数据库。
FAQ
自托管 Langfuse 需要多少内存?
计划使用 4 个 CPU 核心和 16 GiB 内存。这是 Langfuse Docker Compose 指南对单台虚拟机的建议配置,此外还需要约 100 GiB 存储空间。已发布的组件最低内存要求为:ClickHouse 需要 8 GiB,web 和 worker 容器各需要 4 GiB。Postgres、Redis 和 MinIO 还需要额外占用内存。8 GiB 可以运行单个开发人员的实例。2 GiB 不够:内核会在后台合并期间终止 ClickHouse,并且 dmesg 会显示 Out of memory: Killed process。
设置数据保留期后,为什么 ClickHouse 磁盘仍会持续占满?
保留设置只涵盖 Langfuse 自身的数据。ClickHouse 还会单独写入诊断表 trace_log、text_log、opentelemetry_span_log、metric_log 和 asynchronous_metric_log,这些表默认没有 TTL。按表对 system.parts 分组查询,找出占用空间最大的表;然后在 /etc/clickhouse-server/config.d/ 下的文件中通过 remove="1" 条目禁用未使用的表,重启 ClickHouse,并删除现有表,以回收已经占用的空间。
Langfuse 中的最短数据保留期是多少?
3 天。可以在项目设置中按项目设置保留期,也可以通过 projects API 设置。夜间任务会从 ClickHouse 和 blob 存储中删除超过该期限的 traces、observations、scores 和媒体资源。删除操作无法撤销。如果需要保留超过该期限的历史数据,请先配置 blob 存储导出。
是否必须同时备份 Postgres 和 ClickHouse?
是,因为两者保存的数据不同。Postgres 保存用户、组织、项目和 API keys,ClickHouse 保存实际的 trace 数据。仅恢复 Postgres 后,您可以登录实例,但其中没有任何数据。还应备份 MinIO bucket,因为其中保存了 Langfuse 接收后持久化的原始事件,这是该技术栈中最接近事实来源的数据。
可以将现有的 OpenTelemetry 配置指向自托管 Langfuse 吗?
可以。Langfuse v4 及其 v4 SDK 基于 OpenTelemetry 构建,Anthropic 和 OpenAI OTel instrumentation 可以直接将数据导出到 Langfuse。在 Python 中运行 pip install langfuse opentelemetry-instrumentation-anthropic,在启动时调用一次 AnthropicInstrumentor().instrument(),并将 LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY 和 LANGFUSE_BASE_URL 设置为您自己的主机。先使用 langfuse.auth_check() 确认,再排查缺失的 dashboard。