做落地大模型应用的工程师,早晚会遇到一个问题:应用跑起来了,但没人能说清楚每一次请求为什么是这个输出,token烧了多少,哪一步明显变慢。我在把 langfuse 这套大模型可观测平台搬进内网服务器、做完全离线部署的整个过程里,把这类问题一次性解决了大半。这篇文章就是那次实操的完整复盘,从组件依赖、镜像迁移、compose 编排到 SDK 接入和日常踩坑,全部按离线私有化环境来讲,适合正在做大模型应用落地、LLM 微调对比、Dify 接入以及自建可观测体系的工程师参考。
网上讲 langfuse 的教程不少,但绝大多数都默认你有公网、能 docker pull、能顺手连个 SaaS 服务。真正到了生产内网,很多前提都不成立。离线环境部署 langfuse 的关键不是“会点 docker”,而是要把它的依赖、数据模型、环境变量和迁移机制都理清楚,否则装完起不来、起来跑不通、跑通又丢数据,每一个坑都会真实地消耗你的业余时间。
1. 为什么离线环境反而更需要可观测平台
1.1 LLM 应用和传统后端“观测”不一样在哪
传统服务端的可观测性,核心看的是 HTTP 状态码、错误率、接口耗时、日志堆栈这些。只要系统没报错,服务通常就算正常。LLM 应用完全不是这么回事——你的函数可能每次都返回 200,但返回的内容是错的、引用了不存在的文档、格式突然变成 JSON 以外的诡异文本,甚至同一个问题上午答得挺好下午就开始胡编。
这时候传统监控基本是瞎子。你需要的是把每一次请求“完整还原”的能力:用户问了什么、你拼了什么 prompt、检索回来哪些片段、模型到底看了哪些上下文、返回内容是什么、token 消耗多少、整条链路各段花了多久。这些数据在 langfuse 里被叫做 trace,它是 LLM 应用可观测的地基。
我经常给团队打一个比方:传统监控像是服务器的仪表盘,trace 则像是航班黑匣子。仪表盘告诉你引擎转得稳不稳,黑匣子才能告诉你飞行过程中每一个操作为什么发生。对于大模型应用,黑匣子比仪表盘更接近刚需。
1.2 langfuse 是什么,能覆盖哪些链路
langfuse 是目前最主流的开源 LLM 可观测平台,核心定位是“LLM 工程的全链路追踪与评测”。它内部用 trace 和 observation 两层模型组织数据:一条 trace 对应一次完整业务请求,trace 下面挂 span、generation、event 这些 observation,用来记录检索、模型调用、工具执行、中间事件等细分节点。
一个典型的“文档问答”请求,在 langfuse 里看起来会是这样:一个 trace 代表用户提问;下面挂一个 span 叫“检索相关文档”,再挂一个 generation 叫“调用 LLM 生成回答”,generation 里能看到完整的模型名、prompt、输出、token 计数、延迟和成本。如果后续还要人工判断回答质量,也可以直接在 trace 上打分和标注。
除了追踪,langfuse 还包含提示词版本管理、数据集管理和在线评测能力。你可以把一组评测问题放进数据集,批量跑一次推理后,在平台里对比不同模型版本、不同 prompt 版本的效果。这一点在做微调前后对比、模型选型和 prompt 迭代时非常有用,而且是离线环境里完全可用的功能——因为整套体系都是自包含的,不依赖任何外部 API。
1.3 离线部署的三种典型动机
我见到的离线部署需求,基本可以归成三类。第一类是数据合规与安全边界要求,业务数据、用户对话内容不能离开内网,任何形式的公网 SaaS 都不可接受,这个最普遍。第二类是部署环境本身就没有公网出口,比如某些机房、涉密项目或者隔离网络,机器能跑 docker,但 pull 不了镜像、调不了外部接口。第三类是想彻底掌握平台的运维主动权,不希望关键路径依赖某个第三方服务的可用性和限额。
不管动机是哪一种,离线部署的本质都一样:把 langfuse 自己及其依赖的组件全部搬进内网,并且保证它不偷偷依赖外部网络。这个“不偷偷依赖”看起来简单,实际操作里恰恰是最容易出问题的地方,比如 SDK 默认回连公网、容器启动时尝试拉镜像、回调地址配置成公网域名等。后面我会在踩坑章节里逐个讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 离线部署前必须想清楚的四件事
2.1 理清 langfuse 的组件依赖
离线部署不能上来就写 compose 文件,先把依赖理清,才能知道你要准备多少个镜像、多少份数据目录。langfuse 自托管版本的核心组件是这些:
| 组件 | 作用 | 离线部署建议 |
|---|---|---|
| langfuse/web | 主服务,包含前端页面、API 接口、SDK 上报入口 | 必须部署,核心镜像 |
| PostgreSQL | 存储项目、trace、评分、prompt、用户等全部业务数据 | 必须部署或复用内部数据库 |
| Redis | 队列、速率限制、后台任务缓存 | 强烈建议部署,能规避很多玄学问题 |
| S3 兼容对象存储 | 保存 trace 相关文件、媒体资源、导出文件 | 建议部署自建 MinIO,或复用内部对象存储 |
| SMTP(可选) | 发送邮件通知 | 离线环境通常没有,不配置也能跑 |
我见过不少人只部署 langfuse-web 和 postgres,没配 Redis 和对象存储,看起来能启动,但后台任务、文件展示和部分性能功能会处于残缺状态。既然都做离线私有化了,多带两个容器一次性部署完整,后续省心得多。如果你的内网已经有一套成熟的 PostgreSQL 或 MinIO,也可以复用,但强烈建议给 langfuse 建独立数据库,不要和业务库混在一起,原因后文会讲。
2.2 版本与镜像准备细节
离线部署最核心的准备动作,是在一台能联网的机器上提前拉好所有镜像,然后打成 tar 包搬运到内网。这里第一个教训就是:不要用 latest 标签。
latest 每次拉取都可能指向不同版本,你在联网机器上拉到的版本、在内网导入的版本、未来排查问题时查到的文档版本,三者很可能对不上。生产环境一定要固定版本标签。线上我用的是一组相对保守的组合:langfuse 2.x 稳定版、postgres:15、redis:7-alpine、minio 最新稳定版。postgres 不要随便用 17 或更新的大版本,langfuse 的 Prisma 迁移脚本对不同版本 PostgreSQL 的兼容性需要时间验证,用官方 compose 同款的 15 最稳。
镜像搬运的标准流程是:
bash复制# 在联网机器上拉取镜像
docker pull langfuse/langfuse:2.x
docker pull postgres:15
docker pull redis:7-alpine
docker pull minio/minio:latest
# 打成 tar 包
docker save \
langfuse/langfuse:2.x \
postgres:15 \
redis:7-alpine \
minio/minio:latest \
-o langfuse-images.tar
# 拷贝到内网服务器后导入
docker load -i langfuse-images.tar
打包完建议顺手算一下 md5 校验值,拷贝到内网后先校验再导入。镜像几个 GB 很常见,U 盘或内网传输过程中静默损坏的概率比你想象的高。另外要注意架构一致性:如果内网服务器是 ARM 架构,你在 x86 机器上拉下来的镜像导入后是跑不起来的,最好直接在相同架构的联网机器上拉取。
2.3 持久化与目录规划
离线环境里,镜像丢了可以重新导入,容器挂了可以重新启动,但数据没了就是真没了。规划持久化时要把数据分成三类:
- PostgreSQL 数据:所有业务核心数据,必须持久化到 volume,并纳入备份体系;
- 对象存储数据:trace 关联的图片、文件、导出物,同样必须持久化;
- Redis 数据:缓存和队列数据,丢了不致命,但既然用了 appendonly 模式,也顺手挂个 volume。
用 docker volume 还是宿主机目录?我的习惯是:简单场景用 volume,需要明确知道数据落点、便于备份时用目录映射。比如把数据统一放在 /data/langfuse/ 下面,postgres、minio、redis 各一个子目录,备份时直接打包这个目录即可。这个习惯在离线环境尤其重要——你不会有云厂商帮你托管,备份和恢复全靠自己。
2.4 密钥与安全配置
langfuse 有几个关键环境变量,配置错了不会立刻报错,但会让你在某个凌晨突然抓狂。
ENCRYPTION_KEY:用来加密 API Key 等敏感字段。如果你每次重启容器都重新生成,或者配置错误,之前写入的加密数据将无法解密;SALT:用户密码哈希的盐值,同样要求稳定不变;NEXTAUTH_SECRET:登录会话签名密钥,变了之后所有登录态失效。
这三个值在首次部署时就要固定下来,并且单独保存到安全位置。生成方式很简单:
bash复制openssl rand -base64 32
每次运行都会输出一串随机值,跑三次,分别作为上述三个变量的值。注意手动记录好,不要等容器重建之后才想起来找不到了。生产环境还建议把 NEXT_PUBLIC_SIGN_UP_DISABLED 设为 true,关闭开放注册——离线环境并不意味着内网所有人都能注册你的平台,先完成第一个管理员账号的注册,再关掉注册入口是最稳的操作顺序。
3. 离线安装实操:从镜像到启动
3.1 最简文件清单
整个部署过程,我会准备这些文件:
| 文件 | 用途 |
|---|---|
| langfuse-images.tar | 离线镜像包 |
| docker-compose.yml | 服务编排文件 |
| .env | 环境变量文件 |
| 备份脚本 backup.sh | 定期备份数据库与对象存储数据 |
不需要额外的安装包,docker 和 docker compose 插件在内网服务器上预先装好即可。如果服务器没有 docker compose 插件,用独立的 docker-compose 二进制也一样,命令上把 docker compose up -d 换成 docker-compose up -d。
3.2 第一台机器上怎么准备镜像
这个环节在前面讲版本时已经说了标准流程,这里再补充几个实际执行细节。联网机器上拉镜像时要先确认 docker daemon 正常,然后逐个拉取并查验镜像 ID。打包时如果 tar 包太大,可以分几个 tar 打包,避免单个文件超过传输介质限制。
内网服务器导入镜像后,用 docker images 确认所有镜像都已经就位,重点关注仓库名和 tag 是否与 compose 文件中的 image 字段完全一致。大小写、tag 有没有带 v 前缀,这些细节都会导致 compose 启动时依然尝试从远程拉取,而在离线环境里拉取必然失败。这就是一个很典型的“看起来导入了但起不来”的原因。
bash复制# 离线服务器上验证
docker images | grep -E "langfuse|postgres|redis|minio"
确认输出里每个镜像都存在且 tag 正确,再进行下一步。
3.3 compose 编排文件与参数说明
这是我实际用下来最简但完整的编排文件,直接复制参考时可以按需调整密码和路径:
yaml复制version: "3.8"
services:
langfuse-web:
image: langfuse/langfuse:2.x
restart: always
depends_on:
- langfuse-db
- langfuse-redis
- langfuse-s3
ports:
- "3000:3000"
env_file:
- .env
environment:
DATABASE_HOST: langfuse-db
DATABASE_PORT: 5432
DATABASE_NAME: langfuse
DATABASE_USER: langfuse
DATABASE_PASSWORD: ${DB_PASSWORD}
SHADOW_DATABASE_URL: postgresql://langfuse:${DB_PASSWORD}@langfuse-db:5432/langfuse_shadow
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
SALT: ${SALT}
NEXTAUTH_URL: http://内网IP:3000
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
S3_ENDPOINT: http://langfuse-s3:9000
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
S3_BUCKET_NAME: langfuse
S3_REGION: us-east-1
S3_FORCE_PATH_STYLE: "true"
TZ: Asia/Shanghai
volumes:
- langfuse-uploads:/app/uploads
langfuse-db:
image: postgres:15
restart: always
environment:
POSTGRES_USER: langfuse
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: langfuse
TZ: Asia/Shanghai
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U langfuse"]
interval: 10s
timeout: 5s
retries: 5
langfuse-redis:
image: redis:7-alpine
restart: always
command: redis-server --appendonly yes
volumes:
- redisdata:/data
langfuse-s3:
image: minio/minio:latest
restart: always
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${S3_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}
TZ: Asia/Shanghai
volumes:
- miniodata:/data
ports:
- "9000:9000"
- "9001:9001"
volumes:
pgdata:
redisdata:
miniodata:
langfuse-uploads:
对应的 .env 文件:
bash复制DB_PASSWORD=内网专用强密码
ENCRYPTION_KEY=用openssl rand -base64 32生成
SALT=用openssl rand -base64 32生成
NEXTAUTH_SECRET=用openssl rand -base64 32生成
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=内网专用强密码
几个关键点解释一下。SHADOW_DATABASE_URL 是给 Prisma 迁移用的影子库地址,首次启动时主库迁移 schema 会用到,不配置或者指向不存在的库,迁移会直接失败。影子库和主库建议保持同一个 PostgreSQL 实例,但建一个单独库,比如 langfuse_shadow,里面不需要提前建表,迁移工具会自动处理。NEXTAUTH_URL 在内网环境里必须写成你实际访问页面的地址,也就是 http://内网IP:3000,写成 localhost 会导致登录回调跳转异常。TZ 变量很多人会漏掉,容器默认 UTC 时区,不设置的话 trace 时间显示会比北京时间慢 8 小时,排查问题时你能看懵很久。
3.4 启动初始化与首次配置
镜像导入完成、compose 文件和 env 文件就位后,启动命令非常简单:
bash复制docker compose up -d
第一次启动不要急着访问页面,先等 1 到 2 分钟,让 PostgreSQL 初始化、Prisma 迁移完成。可以用以下命令观察:
bash复制docker compose ps
docker compose logs -f langfuse-web
日志里出现类似 Prisma schema has been synchronized 以及 Listening on port 3000 的信息,说明迁移完成、web 服务已经起来了。这时候打开浏览器,访问 http://内网IP:3000,首次访问会看到注册页面。
第一个注册的账号会被自动设置为管理员,并创建默认组织。注册完成后,我建议立刻执行两件事。第一,把 .env 里的 NEXT_PUBLIC_SIGN_UP_DISABLED 改为 true(compose 文件里没有就手动加上,值为 true),然后 docker compose up -d 重启,关闭开放注册。第二,进入项目设置,创建 API Key,你会得到一对 Public Key 和 Secret Key,这对接下来的 SDK 接入至关重要。到这里,平台本身已经可用了,但真正的战役才刚刚开始——你的应用得把数据送进来。
4. 应用接入与日常运维的那些坑
4.1 SDK 怎么接
langfuse 提供了 Python、Node.js 等语言的 SDK,接入逻辑基本一样:配置三个关键信息,即上报地址 LANGFUSE_HOST、公钥 LANGFUSE_PUBLIC_KEY 和私钥 LANGFUSE_SECRET_KEY。
python复制from langfuse import Langfuse
langfuse = Langfuse(
public_key="你的公钥",
secret_key="你的私钥",
host="http://内网IP:3000" # 离线环境一定要填内网地址
)
离线环境里最容易犯的错就是把 host 留空或者填了默认的 Cloud 地址。SDK 默认的公网地址在内网根本连不通,而且 SDK 会有重试逻辑,这个“连不通”不会立刻报错,而是表现为请求卡顿、超时、trace 迟迟不出现。我在内网排查过最长的一次,就是应用自己功能一切正常,但 langfuse 里一条数据都没有,最后发现是另一个服务把 SDK 的 host 配到了公网上。
接入后可以先用最简单的埋点验证链路:
python复制with langfuse.trace(name="快速验证") as trace:
trace.generation(
name="test-generation",
model="qwen-local",
input="你好",
output="你好,我是测试结果"
)
运行完这段代码,在平台页面的 Traces 列表里刷新,能看到一条名为“快速验证”的 trace。看到这条数据,说明网络、认证、上报、存储全链路已经打通了。
4.2 用 langfuse 做评测:离线环境也没问题
热词里很多人搜“langfuse 怎么做测评”,这里一起说清楚。langfuse 的评测能力基于数据集(Datasets)和评分(Scores)两套机制。
先在平台里创建一个 Dataset,把一组评测问题按 CSV 格式导入,每条包含输入和期望输出。然后写一个评测脚本,遍历 Dataset 里的每一条数据,调用你本地的大模型服务,把结果的 trace 关联到 Dataset item 上。跑完一轮之后,回到平台里可以在数据集详情页通过在线评测功能给每条结果打分,也可以用标注的方式人工逐个查看。
这个方式在 LLM 微调场景里特别实用。比如你对一个模型做了微调,肉眼觉得“好像变聪明了”,但无法量化。这时候建一个固定的回归数据集,微调前跑一遍、微调后跑一遍,把两次结果都记录到 langfuse,然后用同一个评分标准打分,差异立刻一目了然。离线环境里模型和平台都部署在内网,整个评测闭环不需要任何外部依赖,这是 langfuse 私有化部署最大的价值之一。
4.3 备份、时区、日志清理
日常运维里,备份怎么强调都不为过。我建议至少每天做一次数据库备份,对象存储数据每周同步一次。备份脚本很简单:
bash复制docker compose exec -T langfuse-db \
pg_dump -U langfuse langfuse \
| gzip > /data/backup/langfuse-db-$(date +%F).sql.gz
对象存储目录直接打包即可,但 MinIO 数据里可能有大量文件,建议用增量同步工具而不是每次全量打包。备份恢复的演练至少做一次,别等事故来了才发现备份文件是坏的——这个经验是我用真金白银换来的。
时区问题前面提过,容器里设置 TZ=Asia/Shanghai 之后,前端显示时间和 SDK 上报的时间都能对齐。如果你发现 trace 时间还是差了 8 小时,先检查浏览器所在机器时区,再看容器时区,最后看 SDK 所在应用服务器的时区,这三处经常有一个漏掉的。
日志清理同样要提前规划。容器长期运行后,Docker 的 json-file 日志会持续膨胀,尤其是 langfuse-web 在数据量大时,日志增长很快。在 docker daemon 配置里限制 log 文件大小,一台离线服务器没那么多磁盘可以挥霍。我常用这个配置:
json复制{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
另外,离线环境里服务器时间漂移是个容易被忽略的问题。如果内网有可用的时间同步服务,记得给宿主机和容器配置好时间同步,否则 trace 里记录的时间戳和你的排障窗口会对不上,日志顺序看着也混乱。
4.4 常见问题与排查实录速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 首次启动 web 一直无法访问 | Prisma 迁移失败,影子库未创建或连接串错误 | 检查 SHADOW_DATABASE_URL,确认指向可连接的独立数据库 |
| 页面报 500 错误,日志出现解密异常 | ENCRYPTION_KEY 与首次启动时不一致 | 恢复部署时保存的原始 ENCRYPTION_KEY |
| 登录后跳转异常或反复回到首页 | NEXTAUTH_URL 填了 localhost | 改成实际访问的内网 IP 地址并重启 |
| SDK 接入后长时间没有 trace | Host 配置成公网地址,或多个上报地址冲突 | 确认 host 为内网地址,检查应用能连通 内网IP:3000 |
| trace 时间显示比本地时间慢 8 小时 | 容器未设置 TZ 环境变量 | 给各服务补充 TZ: Asia/Shanghai 后重启 |
| 图片或附件访问 404 | S3 的 bucket region、path style 配置不一致 | 检查 S3_REGION 与 S3_FORCE_PATH_STYLE,MinIO 通常用 us-east-1 加 path style |
| 容器重启后数据丢失 | volume 未挂载或挂载到了临时目录 | 检查 compose 文件 volume 配置,确保数据目录落盘 |
以上每个问题我都至少碰到过一次,最耽误时间的是前三个——它们共同的特点是平台本身启动正常,但一进页面就异常,非常容易让人误判成前端问题,实际全是环境变量和历史配置不一致导致。
我个人在实际操作中的体会是:离线部署 langfuse 最大的难点,不是安装本身,而是把环境中所有隐形的“公网假设”掐干净。镜像拉取、SDK 上报地址、回调 URL、时区、密钥持久化,每一处都要显式地写清楚,不能赌默认值。整个部署完成之后,建议立刻做一次完整的冷备恢复演练——把容器全部停掉、数据目录打包、换一台新机器恢复一遍。很多团队栽在“天天备份但从来没恢复过”上,这个测试做一次就知道自己的方案是不是真能兜底。
