Files
WeKnora/docs/Langfuse集成.md
wizardchen c12296aa88 feat(observability): instrument agent ReAct loop and tool calls in Langfuse
The existing Langfuse integration covered Chat / Embedding / Rerank / VLM /
ASR generations plus the HTTP + asynq spans, but the agent's own execution
tree was invisible: tool calls never appeared, multi-round ReAct iterations
were flat under the HTTP trace, and there was no single node representing
"one agent run".

This change adds three levels of agent-side spans:

  - agent.execute       — wraps AgentEngine.Execute, records query preview,
                           knowledge bases, allowed tools, final-answer length
                           and totals on finish.
  - agent.round.<N>     — wraps each ReAct iteration; records finish_reason,
                           tool-call count, token usage and duration.
  - agent.tool.<name>   — wraps each tool invocation; records arguments,
                           success, duration, output preview (rune-safe, 4KB
                           cap), error, data keys and image count.

To keep the loop's many exit paths (natural stop, stuck loop, empty-content
retry, final_answer, context cancellation) span-safe, the iteration body was
extracted into runReActIteration with a single defer span.Finish() and an
iterOutcome sentinel driving the outer loop. database_query arguments are
redacted (keys only) to avoid leaking raw SQL into the observability
backend, mirroring the existing UI hint policy.

Adds unit tests for the new helpers (truncateForLangfuse, argKeys, dataKeys,
finishToolSpan nil-safety, iterOutcome.String).
2026-04-24 19:58:08 +08:00

20 KiB
Raw Blame History

Langfuse 集成

WeKnora 内置了对 Langfuse 的轻量级集成,用于统计 token 消耗、追踪 LLM 调用链路、并为每个对话生成可在 Langfuse 控制台查看的 trace。该集成解决 issue #497token 使用量统计)和 discussion #620(接入 Langfuse

1. 特性

  • 自动上报 chat / embedding / rerank / VLM视觉语言模型/ ASR语音识别 全部 5 类模型调用的 prompt、响应和 token 使用量。
  • 为每个对话、检索、文件上传及后续异步处理创建一条端到端 trace。HTTP 请求是根asynq 任务以 SPAN 的形式挂在同一条 trace 下,文档解析 → chunk embedding → 多模态 OCR/Caption → 摘要 / 问题生成全部在同一棵树里可见。
  • 支持 流式响应:记录首 token 延迟Time-To-First-Token完整响应在流结束后一次性写入。
  • 跨进程 trace 透传HTTP 层把 trace_id / parent_observation_id 注入 asynq payloadworker 在 asynq middleware 层自动 resume定时任务例如数据源同步则退化为独立 trace依然按任务类型asynq.<type>)聚合。
  • 完全可选:不配置 LANGFUSE_* 环境变量时Langfuse 相关代码路径是 no-op不产生任何性能开销。
  • 异步批量上报:不阻塞业务请求;队列满时静默丢弃,观测数据不会影响用户对话。
  • 开箱即用的部署方式Docker Composedocker-compose.yml 已内置环境变量、Helm Chart通过 extraEnv、Lite 版本(本地单机)均支持。

2. 快速开始

2.1 获取 Langfuse 凭证

  1. 登录 cloud.langfuse.com 或自建 Langfuse 实例。
  2. 进入 Project Settings → API Keys,生成一对 Public Key / Secret Key

2.2 按部署方式配置

ADocker Compose 部署(推荐)

docker-compose.yml 已经把所有 LANGFUSE_* 环境变量串到 app 服务。下面提供两种选择。

A-1) 接入 Langfuse Cloud最简单

只需要在 .env 里加 3 行:

LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
LANGFUSE_HOST=https://cloud.langfuse.com    # 美区用 https://us.cloud.langfuse.com

然后重启服务:

docker compose up -d app
docker compose logs -f app | grep Langfuse

看到下面这行就说明已启用:

[Langfuse] enabled host=https://cloud.langfuse.com flush_at=15 flush_interval=3s sample_rate=1.00
A-2) 自建 Langfuse 栈(离线 / 内网 / 数据合规)

docker-compose.yml 内置了一个可选的 langfuse profile用一条命令就能拉起 Langfuse v3。

设计上已尽可能复用 WeKnora 已有容器,避免资源浪费

组件 来源 备注
PostgreSQL 复用 WeKnora-postgres 通过一次性的 langfuse-db-init 容器,在同一 pg 实例里创建独立的 langfuse 数据库。库级隔离,互不影响。
Redis 复用 WeKnora-redis 使用独立的 Redis DB 号(默认 DB 1WeKnora 用 DB 0REDIS_CONNECTION_STRING 指定 DB 后缀。
ClickHouse 新增 langfuse-clickhouse Langfuse 专有OLAP 事件存储WeKnora 不用,必须独立。
MinIO 新增 langfuse-minio 故意和 WeKnora 的 minio 分开(后者是可选 profile未必激活Langfuse S3 要专属 bucket
Web / Worker 新增 langfuse-web + langfuse-worker Langfuse 应用本体。

最终 --profile langfuse 只新增 4 个常驻容器 + 1 个一次性 init,内存开销由原先的 ~1.52.5 GB 降到约 1.01.5 GB

# 1. 启动自建栈ClickHouse 首次迁移大约需要 1-2 分钟)
docker compose --profile langfuse up -d

# 2. 浏览器打开 http://localhost:3000 注册管理员账号
#    然后在 Project Settings → API Keys 生成 Public/Secret Key

# 3. 把 key 填回 .env 并把 HOST 改成容器内部地址
cat >> .env <<'EOF'
LANGFUSE_HOST=http://langfuse-web:3000
LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
EOF

# 4. 让 app 重新加载配置
docker compose up -d app

⚠️ 生产部署安全提示.env.example 里的默认密码 / SALT / ENCRYPTION_KEY 都是开发占位符,生产环境必须用以下命令重新生成:

echo "LANGFUSE_SALT=$(openssl rand -base64 32)"
echo "LANGFUSE_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "LANGFUSE_NEXTAUTH_SECRET=$(openssl rand -base64 32)"

同时把 LANGFUSE_DB_PASSWORD / LANGFUSE_CLICKHOUSE_PASSWORD / LANGFUSE_REDIS_PASSWORD / LANGFUSE_MINIO_PASSWORD 全部换成强密码。完整变量清单见 .env.example 的 "Langfuse 自建栈配置" 段。

通用调优

可选调优变量(LANGFUSE_FLUSH_ATLANGFUSE_SAMPLE_RATE 等)都已经在 docker-compose.yml 中预设直通,只要在 .env 追加对应行即可生效。完整列表见 .env.example 的 Langfuse 段,或本文第 3 节。

资源开销估算A-2 自建方案)
组件 类型 典型 RSS 备注
langfuse-db-init 一次性 创建 langfuse 数据库后立即退出
langfuse-web 常驻 300500 MB Next.js
langfuse-worker 常驻 200400 MB Node.jsQueue consumer
langfuse-clickhouse 常驻 500 MB1 GB 首次迁移稍高,稳态约 500 MB
langfuse-minio 常驻 100200 MB
复用WeKnora-postgres +~50 MB 多一个 langfuse 数据库
复用WeKnora-redis +3080 MB 共用实例的 DB 1
新增合计 ≈ 1.01.5 GB 推荐 3 GB+ 可用内存

和"完全隔离各建一套 pg/redis"方案相比,这里节省了约 400500 MB 内存。代价是 WeKnora 的 pg/redis 容量规划需要为 Langfuse 预留一点余量Langfuse 写入量并不大(只是元数据 + 任务队列,事件主体走 ClickHouse实际影响很小。

对单机部署而言,若只想使用 Langfuse Cloud 方案A-1完全不需要这些容器;原有服务 CPU/内存占用不变。

生产环境下的注意事项
  • WeKnora-redis 的驱逐策略Langfuse 建议 maxmemory-policy noeviction(避免 Redis 在内存紧张时丢弃队列任务)。如果 WeKnora 的 redis 未配置该策略,建议在 docker-compose.yml 的 redis command 中加上 --maxmemory-policy noeviction
  • 备份pg_dump -d langfuse 可独立备份 Langfuse 的元数据;事件数据在 ClickHouse 卷(langfuse_clickhouse_data)中。
  • 想彻底隔离(跨机部署、强运维隔离):可以直接把 langfuse-web / langfuse-workerDATABASE_URLREDIS_CONNECTION_STRING 指向任意外部 pg/redis例如 RDS + ElastiCachelangfuse-db-init 容器可以选择不启动,手动在目标 pg 上 CREATE DATABASE langfuse 即可。

BWeKnora Lite单机

.env.lite(或启动脚本导出的环境变量)里加:

LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
LANGFUSE_HOST=https://cloud.langfuse.com

启动 weknora-lite(或 macOS .app)后效果同上。

CHelm Chart 部署

values.yamlapp.extraEnv 添加:

app:
  extraEnv:
    - name: LANGFUSE_PUBLIC_KEY
      valueFrom:
        secretKeyRef:
          name: langfuse-credentials
          key: public_key
    - name: LANGFUSE_SECRET_KEY
      valueFrom:
        secretKeyRef:
          name: langfuse-credentials
          key: secret_key
    - name: LANGFUSE_HOST
      value: https://cloud.langfuse.com

建议把 Secret Key 放到 Kubernetes Secret 中,切勿写进 values.yaml。

D二进制 / 源码运行

export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
export LANGFUSE_HOST="https://cloud.langfuse.com"
./weknora-server

E本地开发docker-compose.dev.yml + go run

docker-compose.dev.yml 只启动基础设施容器postgres/redis/docreader 等),app 走本地 go run ./cmd/server。Langfuse 的两种接入方式:

E-1) 直连 Langfuse Clouddev 最常见)

无需改任何 compose 文件,本地 shell 导出即可:

export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
export LANGFUSE_HOST="https://cloud.langfuse.com"
go run ./cmd/server

E-2) 本地自建栈调试

dev compose 也支持对称的 langfuse profile复用同一个 dev postgres + redis

# 拉起基础设施 + Langfuse 栈
docker compose -f docker-compose.dev.yml up -d postgres redis docreader
docker compose -f docker-compose.dev.yml --profile langfuse up -d

# 浏览器打开 http://localhost:3000 注册并生成 key

# 本地 app 接入(注意是 localhost不是 langfuse-web因为 go run 跑在宿主机)
export LANGFUSE_HOST=http://localhost:3000
export LANGFUSE_PUBLIC_KEY=pk-lf-xxxxxxxx
export LANGFUSE_SECRET_KEY=sk-lf-xxxxxxxx
go run ./cmd/server

Dev 相关容器都带 -dev 后缀、用独立网络 WeKnora-network-dev,和生产 compose 不冲突

2.3 验证

发起一次知识问答(POST /api/v1/knowledge-chat/:session_id)或知识检索(POST /api/v1/knowledge-search)。等待 3 秒(或批量大小达到 flush_atLangfuse 控制台的 Traces 页面会出现对应的 trace

  • 顶层节点HTTP 请求(带 userId / sessionId)。
  • 子节点依次为 rerank、chat、VLM 等具体模型调用,点击可查看 prompt、响应以及 usageprompt/completion/total tokens
  • 流式对话会额外标注 Time-To-First-Token。

3. 环境变量参考

变量名 默认值 说明
LANGFUSE_ENABLED 自动 显式开关。未设置时,只要 PUBLIC_KEY + SECRET_KEY 都存在就自动启用。支持 true/false/1/0/yes/no
LANGFUSE_HOST https://cloud.langfuse.com Langfuse 实例地址。美区用 https://us.cloud.langfuse.com,自建实例填 https://langfuse.your-domain.com
LANGFUSE_PUBLIC_KEY 项目 Public Keypk-lf-...)。
LANGFUSE_SECRET_KEY 项目 Secret Keysk-lf-...),请走密钥管理工具注入,不要提交到仓库。
LANGFUSE_RELEASE 可选,上报到 Langfuse 的版本号,例如 CI 构建号。
LANGFUSE_ENVIRONMENT 可选,环境标签(production / staging / dev),方便在 UI 过滤。
LANGFUSE_FLUSH_AT 15 批处理大小:缓冲区积累到该数量立即上报。
LANGFUSE_FLUSH_INTERVAL 3s 定时刷新间隔。支持 500ms5s1m 等 Go duration 写法;纯数字按秒处理。
LANGFUSE_QUEUE_SIZE 2048 内存队列容量。队列满时新事件会被静默丢弃(避免拖慢业务)。
LANGFUSE_REQUEST_TIMEOUT 10s 单次 HTTP ingest 请求超时。
LANGFUSE_SAMPLE_RATE 1.0 采样率 (0..1)。0 视为 1.0。高流量环境可下调。
LANGFUSE_DEBUG false 打开后会在 WeKnora 日志里打印上报失败的详细原因,排障期间临时开启。

4. 观测数据说明

Langfuse 概念 WeKnora 对应 备注
Trace 一次 HTTP 请求(含其触发的所有 asynq 任务) 对于 knowledge-chatagent-chatknowledge-searchgenerate_titleevaluation、模型连通性测试等在线请求;以及文件上传/URL 入库/manual/reparse/move/copy、FAQ 导入、知识修改、wiki auto-fix、数据源手工触发等入库请求HTTP 层都会开启 trace并把 trace_id / parent_observation_id 注入 asynq payload。
Spantype=SPAN 每个 asynq 任务的执行窗口 / 每次 Agent 执行及其每一轮 / 每次工具调用 internal/tracing/langfuse/AsynqMiddlewaremux.Use 注册;对每个 handler 自动创建 asynq.<task_type> 的 SPAN并记录 task_id / queue / retry / payload_bytes。定时任务(无上游 trace会退化为 asynq.<task_type> 独立 trace。Agent 相关AgentEngine.Execute 会开 agent.execute 顶层 SPAN其下每一轮 ReAct 循环开 agent.round.N SPAN每次工具调用开 agent.tool.<tool_name> SPAN参数、输出、耗时、成败、错误都会写入
Generationtype=GENERATION 每次 chat / embedding / rerank / VLM / ASR 调用 若位于 span 下会自动设置 parentObservationId,所以 Langfuse UI 呈现 trace → asynq-span → generation 的树状结构Agent 模式下是 trace → agent.execute → agent.round.N → (chat.completion.stream + agent.tool.X → rerank/embedding...) 的完整树。
Input Tokens TokenUsage.PromptTokens 来自模型返回的 usage 字段。
Output Tokens TokenUsage.CompletionTokens 来自模型返回的 usage 字段。
Total Tokens TokenUsage.TotalTokens 大多数厂商返回;未返回时自动求和。
userId X-User-ID / 租户 ID 未登录时退化为 tenant:<id>方便按租户汇总消耗enqueue 时会写入 payloadworker 在无上游 trace 的场景也能保留归属。
sessionId URL 中的 :session_id(或 RequestID 兜底) 可以在 Langfuse 的 Sessions 视图聚合一整场对话,或按单次异步批次聚合。
Time-To-First-Token 流式调用首条有效 chunk 的时间 通过 generation-update.completionStartTime 上报。

覆盖到的 asynq 任务类型

下表列出当前会在 Langfuse 里自动出现对应 SPAN 的 asynq 任务;每种任务的 payload 均已嵌入 types.TracingContextenqueue 时由 langfuse.InjectTracing(ctx, &payload) 从当前 HTTP trace 拷出 trace_id / parent_observation_id

任务类型常量 Handler 典型触发来源
document:process knowledgeService.ProcessDocument 文件 / URL / 文本 / file_url 四种入库reparse知识库克隆内部重派发
manual:process knowledgeService.ProcessManualKnowledge 手工知识新建 / 更新
image:multimodal ImageMultimodalService.Handle 文档解析时发现图片
knowledge:post_process KnowledgePostProcessService.Handle 文档解析完成后统一调度 summary/question
summary:generation / question:generation KnowledgePostProcessService 子任务 knowledge:post_process 派发
chunk:extract ChunkExtractor.Handle 图谱提取NEO4J 启用时)
datatable:summary DataTableSummaryService.Handle 表格文件解析
faq:import FAQ 批量导入 handler FAQ 导入 / 批量创建
knowledge:move / knowledge:list_delete / index:delete / kb:clone / kb:delete 知识移动 / 批量删除 / 索引清理 / 知识库复制 / 知识库删除 对应 HTTP 路由
wiki:ingest wikiIngestService.ProcessWikiIngest Wiki auto-fix / 重建链接
datasource:sync dataSourceSyncService.Handle 数据源手动触发 + 定时调度(定时场景下 trace 为 standalone

各模型的 usage 处理策略

模型类型 上报名称 Token 计量方式 备注
Chat chat.completion / chat.completion.stream 直接使用模型返回的 prompt_tokens / completion_tokens / total_tokens 流式请求会记录 TTFT。
Embedding embedding.embed / embedding.batch_embed 模型未返回 usage 时按 rune_count/4 + 1 估算 input tokens 批量接口会上报批量大小和前 5 条文本预览,避免把整批内容塞进 trace。
Rerank rerank query + 所有文档 的 rune 数估算 input tokens 输出只上报前 10 条 (index, score)
VLM vlm.predict prompt/result 分别按 rune/4 估算 input/output 不上传原始图片字节;仅记录图片数量与总字节大小。
ASR asr.transcribe SECONDS)为计量单位,取转录结果最后一个 segment 的 end 作为音频时长 便于 Langfuse 按"分钟"结算 Whisper 类 API。

TipLangfuse 的 Settings → Models 页面可以为自定义模型(本地 Ollama、阿里云百炼等配置单价每 1K tokens、每分钟等Langfuse 会据此自动核算费用。

5. 高流量部署建议

  • 调高 LANGFUSE_FLUSH_AT 到 50100降低 ingest HTTP 调用频率。
  • 采样:把 LANGFUSE_SAMPLE_RATE=0.1 只采样 10% 的对话,生产成本与信噪比通常能得到较好的平衡。
  • 扩大 LANGFUSE_QUEUE_SIZE 至 8192防止短时峰值触发事件丢弃。
  • 将 Langfuse 实例部署在离 WeKnora 同机房(例如自建 Langfuse + 内网地址),可以显著降低上报延迟。
  • 打开 LANGFUSE_DEBUG=true 几分钟即可确认链路,生产环境常态下关闭,避免日志噪音。

6. 禁用

删除或留空 LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY,或显式设置 LANGFUSE_ENABLED=false,再重启服务即可。所有 Langfuse 相关代码路径会回退到 no-op不会影响其他观测组件OpenTelemetry、LLM Debug Log

7. 故障排查

现象 建议排查步骤
启动日志没有 [Langfuse] enabled 检查 LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY 是否被服务进程读到;容器里可 env | grep LANGFUSE 验证。
控制台看不到 trace 打开 LANGFUSE_DEBUG=true,观察日志中是否有 [Langfuse] flush ... failed。常见原因:LANGFUSE_HOST 错误、企业防火墙拦截 HTTPS、Secret Key 轮换后未更新。
部分 chunk 缺失 调大 LANGFUSE_QUEUE_SIZE;确认 Langfuse ingest API 没有返回 429/503。
token 数为 0 该模型在返回中未提供 usage常见于部分本地 Ollama / 自建模型)。可在模型侧开启 usage 统计,或在 Langfuse 配置里为该模型提供 tokenizer。

8. 代码位置

  • internal/tracing/langfuse/ — Langfuse 客户端、异步批量上报、Gin 中间件、asynq middleware、Span / Trace resume 实现。
    • tracer.go — 暴露 Trace / Span / Generation + StartTrace / StartSpan / StartGeneration / ResumeTrace
    • asynq.goAsynqMiddleware() 统一在 mux 上包 handlerInjectTracing(ctx, payload) 在 enqueue 侧把 trace/span ID 注入 payload。
    • middleware.go — Gin 中间件 + shouldTrace 白名单(覆盖 chat / 入库 / FAQ / wiki / 数据源等路径)。
  • internal/types/tracing.goTracingContext POCO所有 asynq payload 通过嵌入此结构携带 lf_trace_id / lf_parent_obs_id / lf_user_id / lf_session_id
  • internal/models/chat/langfuse_wrapper.go — Chat 调用装饰器(含流式)。
  • internal/models/embedding/langfuse_wrapper.go — Embedding 调用装饰器。
  • internal/models/rerank/langfuse_wrapper.go — Rerank 调用装饰器。
  • internal/models/vlm/langfuse_wrapper.go — VLM视觉语言模型调用装饰器。
  • internal/models/asr/langfuse_wrapper.go — ASR语音识别调用装饰器。
  • internal/agent/engine.goagent.execute 顶层 SPAN 和 agent.round.<N> 每轮 SPAN。
  • internal/agent/act.goagent.tool.<tool_name> 工具调用 SPAN包含参数、输出、耗时、成败
  • internal/router/router.go — 注册 langfuse.GinMiddleware()
  • internal/router/task.go — 在 asynq mux 上 mux.Use(langfuse.AsynqMiddleware()),使所有 handler 自动被 trace。
  • internal/container/container.go — 初始化 + 资源清理。
  • docker-compose.yml / .env.example / .env.lite.example — 预置 LANGFUSE_* 环境变量直通。