大模型应用可观测性实战:基于Langfuse与WebSocket的实时AI对话监控
上个月我们线上的一个问答机器人开始一本正经地胡说八道用户反馈截图一张接一张发过来我这边却连是哪一轮对话出的错都查不出来——日志里只有明文 request 和 response没有链路 ID没有 token 消耗没有耗时统计更别提把流式输出过程拿出来复盘了。那段时间我意识到大模型应用和传统 Web 服务在可观测性上的差距已经不只是“少打几行日志”那么简单。后来我花了一段时间把 Langfuse、Langchain、DeepSeek、FastAPI 和 WebSocket 这五样东西串成了一套实时 AI 对话监控仪表盘总算是把“看不见的对话链路”变成了能在网页上实时刷新的数据流。这篇文章就是把整个搭建过程中我觉得真正有价值的部分拆开来讲为什么选这些组件、Langfuse 追踪是怎么埋进去的、WebSocket 推送怎么和 FastAPI 配合以及我在联调时踩到的坑。如果你正准备给自己的大模型应用加上监控能力这篇文章应该能帮你少走不少弯路。1. 为什么要给 AI 对话上监控从一次线上事故说起1.1 大模型应用的“黑盒”困境传统接口排查问题有一套固定打法看一下 Nginx 访问日志、查一下应用日志、翻一下数据库慢查询基本就能锁定范围。但到了大模型应用这里情况完全变了。模型本身的参数不可见Prompt 的微小变化可能导致输出完全走样再加上 Langchain 这类框架自带的各种 Chain、Agent 调用编排一个问题可能发生在“模型返回质量差”“上下文窗口被撑爆”“工具调用环节异常”等完全不同的位置。我当时的处境很典型用户说“机器人开始乱回答”但程序没有报错接口返回码是 200数据库里也能查到这条用户消息。问题在于——我看不到模型到底收到了什么、它在哪一步开始跑偏的。没有链路追踪就没有定位问题的抓手。这正是对话监控的核心价值。它不是简单地把聊天记录落库而是要把一次从“用户输入”到“模型输出”的完整生命周期记录下来Prompt 是什么、模型返回了什么、耗时多久、消耗了多少 token、上下文里塞了多少历史消息、是否调用了外部工具。有了这层数据问题才能被看见成本才能被核算Prompt 质量才能被评估。1.2 对话监控到底该看哪些指标作为一套可落地的监控面板我给自己定了下面这几类核心指标每一类都对应一个具体的排查场景指标类型具体内容排查场景调用链信息Trace ID、会话 ID、调用时间、模型名称某次异常对话的完整回溯性能指标首 token 延迟、总耗时、流式输出速度用户感知“卡顿”或超时成本指标输入 token、输出 token、估算费用运营成本异常上涨质量信号返回内容截断、错误类型、重试次数模型回答质量问题运行状态在线连接数、推送到前端的实时事件流仪表盘本身是否健康这套指标里比较容易被忽略的是“流式输出速度”和“上下文体积”。很多应用已经切换到了流式输出用户看到的是一个字一个字蹦出来的效果如果首 token 延迟很高用户会觉得“根本没反应”。另外随着会话轮数增加塞进上下文的 token 越来越多成本和时间开销是隐性上涨的如果不监控上下文体积你很难解释为什么某个老会话越来越慢。1.3 一次“事后复盘”救回来的教训我印象最深的一次事故是某天下午生产环境的问答准确率突然下降但错误率曲线没有任何波动。事后把 Langfuse 里的 Trace 拉出来一对比才发现是发布新版本时一个 Prompt 模板里的变量名写错了导致模型收到的上下文里有一大段空字符串。这种问题放在以前可能得靠用户反复截图反馈才能发现而有了追踪链路二十个 Trace 对比下来差异一目了然。所以我现在的态度很明确大模型应用的监控不是锦上添花而是让它能上生产环境的前提条件。下面要讲的这套架构解决的就是“如何用尽量少的代码搭一套能看得见对话全过程的实时系统”。2. 技术栈选型这四个组件到底各管哪一段2.1 Langfuse大模型应用的可观测性底座Langfuse 是一个开源的大模型可观测性平台官方定位是 LLM Engineering Platform。它解决的核心问题和大模型应用监控强相关一次模型调用从输入到输出中间经历了哪些环节每环节消耗了多少资源整个链路长什么样。Langfuse 的核心概念包括 Trace一次完整请求、ObservationTrace 内部的观测单元、Span耗时区间和 Generation模型生成事件。你可以把 Trace 理解成传统应用监控里的一条请求链路Span 是链路里的一个环节Generation 则是专门针对大模型调用的数据记录点会额外记录 model、prompt、completion、token 用量这些字段。它和 Langchain 做了深度集成只要在 Langchain 的调用链上挂一个 CallbackHandler就能自动捕获链路上的关键事件。这一点非常关键——我不用去手动打点每一个环节Langchain 内部替我把编排过程暴露给了 Langfuse。如果完全抛开 Langchain直接用原生 SDK 调用 DeepSeek API那所有埋点都得自己写工作量大不是一个量级的。Langfuse 同时支持云端 SaaS 和自托管。自托管方案是用 Docker Compose 一键拉起包含 Web UI、API 服务、PostgreSQL 数据库。我选择自托管一是数据不出内网二是 Langfuse 的访问频率和数据量完全可控一台 2C4G 的小机器跑起来绰绰有余。2.2 Langchain把模型调用变成可追踪的链路Langchain 的角色是应用编排层。它解决的核心问题是一次真实的 AI 对话除了模型本身还涉及 Prompt 模板、历史消息组装、输出解析、可能的工具调用。如果把这些逻辑全部手写代码会非常散排查问题也更难。Langchain 里有两个东西在这套监控系统里作用最大。第一个是RunnableSequence也就是把“Prompt 模板 - 模型调用 - 输出解析”串成一条链整条链可以被 Langfuse 当作完整 Trace 来记录。第二个是.with_config()和回调机制Langchain 的BaseCallbackHandler会把链路的每个事件上报给 Langfuse。很多初学者会纠结“Langchain 和 Langgraph 的区别”。简单说Langchain 侧重的是“有固定顺序的调用编排”适合大多数问答场景Langgraph 则倾向于“有循环、有分支、有条件跳转”的复杂 Agent 状态机。我做对话监控仪表盘核心链路是线性的“接收请求 - 组装消息 - 调用模型 - 返回结果”用 Langchain 就够了不需要引入 Langgraph 的无谓复杂度。在这里我多说一句选型心得不要为了用框架而用框架。如果你的对话逻辑只有“发一个请求拿一个回复”Langchain 的编排优势其实不明显直接用 OpenAI SDK 或 DeepSeek 官方 SDK 更轻。但一旦你的链路里同时有 Prompt 模板、多轮历史、输出格式校验、多个模型切换Langchain 才真正体现出价值——这也是我在这套系统里保留它的原因因为后续大概率要往链路里加工具调用和路由逻辑。2.3 DeepSeek、FastAPI、WebSocket模型、后端与实时通道的分工DeepSeek 是模型层提供大模型推理能力。它在 Langchain 生态里通过langchain_openai的ChatOpenAI接入只需要把base_url指向 DeepSeek 的 API 地址。选择 DeepSeek 的核心原因是它性价比高、输出质量稳定而且在长文本理解和代码生成场景表现不错。接入方式上和 OpenAI 兼容意味着整个监控系统的模型层可以随时替换成其他兼容 OpenAI 协议的服务这是架构上预留的灵活性。FastAPI 是后端框架在这套系统里承担两个职责一是提供 HTTP 接口接收对话请求二是提供 WebSocket 接口向前端推送监控指标。FastAPI 的异步特性让它天然适合同时处理这两种 IO 密集场景。用async def定义的接口不会阻塞事件循环WebSocket 连接可以长时间保持这两点是 Flask 这类同步框架不具备的。WebSocket 解决的是“实时推送”问题。最开始我有过另一个方案——前端定时轮询后端拿最新监控数据。但这种方案的延迟受轮询间隔限制如果间隔太短服务端压力大间隔太长实时性又差。WebSocket 是全双工长连接服务端在对话结束后能立刻把指标推到前端体验完全是实时的。从整体架构上看这四层是这样配合的用户发起对话请求 ↓ FastAPI HTTP 接口接收 ↓ Langchain 链路组装 Prompt 调用 DeepSeek ↓ Langfuse CallbackHandler 自动记录 Trace ↓ 对话结束FastAPI 将结构化指标经 WebSocket 广播 ↓ 前端仪表盘实时刷新展示这套架构里的关键点在于对话主链路是 HTTP 同步请求监控数据走 WebSocket 异步推送两者互不阻塞。即便 WebSocket 连接断了对话功能不受影响监控数据最多是前端看不到链路数据依然会完整写入 Langfuse。3. 环境准备与工程骨架先把地基打牢3.1 开发环境与依赖清单我用 Python 3.11 做开发虚拟环境用uv管理比pip venv快很多依赖解析也省心。初始化项目后核心依赖如下fastapi uvicorn[standard] langchain langchain-openai langfuse openai websockets pydantic python-dotenv jinja2几个容易踩坑的依赖版本问题我提前说明第一langchain-openai和openai的版本要匹配如果openai升到了 1.x 而项目里还在用旧版风格调用方式会出问题。建议安装时让 pip 自动解析不要手动锁一些陈旧版本。第二langfuse的 SDK 版本一直在迭代v2 和 v3 的初始化方式有差异。我使用的是 v3 风格用环境变量LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY、LANGFUSE_HOST来配置初始化时直接从langfuse导入Langfuse单例即可。第三不要装websockets后又在代码里手动管理连接池。FastAPI 的 WebSocket 端点本身基于 Starlette 实现你只需要处理连接建立、消息收发、断开清理这些逻辑不需要自己再封装一层 websocket 客户端。3.2 Langfuse 自托管Docker Compose 一键拉起如果你和我一样选择自托管 LangfuseDocker Compose 是最省事的方案。Langfuse 官方仓库提供了docker-compose.yml里面包含三个服务webNext.js 前端 API、worker后台任务、dbPostgreSQL。启动前需要在.env里配置加密密钥和数据库连接串。启动命令很简单docker compose up -d首次启动后访问http://localhost:3000注册账号进入设置页面创建 API Key。这里有一个关键习惯要养成Public Key 和 Secret Key 不要写死在代码里放到项目的.env文件并加入.gitignore。Langfuse 的 Web UI 里你会看到一个完整的 Trace 列表每条 Trace 展开后能看到 Prompt、Completion、Token 消耗、耗时和自定义属性。我记得第一次自托管时踩过一个坑默认的 Docker Compose 配置里PostgreSQL 的数据目录没有挂载宿主机卷。这意味着容器一删所有追踪数据全部消失。所以一定记得检查volumes配置把数据库数据目录持久化到宿主机。3.3 工程目录结构设计这套系统的代码组织方式我按“前后端分离”的思路来实际运行时长这样ai-monitor-dashboard/ ├── backend/ │ ├── main.py # FastAPI 应用入口HTTP 与 WebSocket 端点 │ ├── chain.py # Langchain 链路组装Prompt DeepSeek │ ├── monitor.py # Langfuse 初始化与指标封装 │ ├── .env # 环境变量 │ └── requirements.txt ├── frontend/ │ ├── index.html # 仪表盘页面 │ ├── app.js # WebSocket 客户端与渲染逻辑 │ └── style.css └── docker-compose.yml # Langfuse 部署后端和前端可以分别部署后端跑在8000端口前端如果只是本地调试直接用浏览器打开index.html即可。生产环境我建议用 Nginx 把静态资源和后端 API 反代到同一个域名下避免跨域问题。我把监控逻辑单独抽到monitor.py而不是全塞在main.py原因很简单——chain.py里调用模型时也需要访问 Langfuse 的 handler如果初始化代码放在main.py就会产生循环导入。模块化的成本很低但后期扩展的时候收益很大。4. 核心链路实现从 DeepSeek 调用到 Langfuse 追踪4.1 初始化 Langfuse callbackLangfuse 官方为 Langchain 提供了一套集成方式核心就是langfuse.callback.CallbackHandler。初始化时它会自动读取LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY、LANGFUSE_HOST环境变量。代码写起来非常短# monitor.py from langfuse.callback import CallbackHandler import os # 从 .env 读取配置 from dotenv import load_dotenv load_dotenv() langfuse_handler CallbackHandler( public_keyos.getenv(LANGFUSE_PUBLIC_KEY), secret_keyos.getenv(LANGFUSE_SECRET_KEY), hostos.getenv(LANGFUSE_HOST, http://localhost:3000), )这里有一个细节值得注意CallbackHandler实例可以是全局共享的但Langfuse内部的trace_id和generation_id必须在每次请求时重新创建。在 Langchain 的链路里这个需求是通过config传递callbacks列表来实现的。我在每次请求处理时都会新建一个回调列表再传给链路避免多个用户并发时数据串线。4.2 组装 Langchain 链路Chain、Prompt 与 DeepSeek 模型DeepSeek 的 Langchain 接入代码相当直观。ChatOpenAI是 langchain-openai 里的类它允许自定义base_url和model名称所以可以指向任意兼容 OpenAI 协议的 API 服务# chain.py from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser model ChatOpenAI( modeldeepseek-chat, base_urlhttps://api.deepseek.com/v1, api_keyos.getenv(DEEPSEEK_API_KEY), temperature0.7, streamingTrue, )为什么用ChatOpenAI而不是专门的ChatDeepSeek类因为当时langchain-deepseek这个包在 Langchain 生态里还不太成熟接口兼容层不如ChatOpenAI稳定。既然 DeepSeek 官方提供了 OpenAI 兼容的 API直接走这套协议最保险。后续如果要切换到其他模型比如本地部署的模型通过 vLLM 暴露的 OpenAI 兼容接口我只改base_url和model两个参数即可。Prompt 模板和多轮消息组装我用ChatPromptTemplate完成它支持从消息列表构造也支持变量插值prompt ChatPromptTemplate.from_messages([ (system, 你是一个有用的AI助手请用中文简洁回答。), (human, {question}), ]) chain prompt | model | StrOutputParser()这条链用 LCEL 语法将三个Runnable串联起来。LCEL 的好处是天然具备流式和异步支持同时 Langfuse 能自动识别链路上每个步骤的输入输出。StrOutputParser会把模型的输出解析成字符串方便后续直接返回给前端。4.3 在 FastAPI 请求里完成调用与指标回收POST/api/chat端点是整个系统的入口。它接收用户问题调用上面那组合好的链然后返回结果和指标。这里我用了 FastAPI 的异步支持整条链路是异步的# main.py from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from chain import chain from monitor import langfuse_handler import time, uuid, asyncio app FastAPI() app.add_middleware(CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*]) class ChatRequest(BaseModel): question: str class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] [] async def connect(self, ws: WebSocket): await ws.accept() self.active_connections.append(ws) def disconnect(self, ws: WebSocket): if ws in self.active_connections: self.active_connections.remove(ws) async def broadcast(self, message: dict): for conn in self.active_connections: try: await conn.send_json(message) except Exception: pass manager ConnectionManager() app.post(/api/chat) async def chat(req: ChatRequest): trace_id str(uuid.uuid4()) start time.time() result await chain.ainvoke( {question: req.question}, config{ callbacks: [langfuse_handler], run_id: trace_id, metadata: {source: web_dashboard, trace_id: trace_id}, }, ) elapsed round((time.time() - start) * 1000, 2) payload { type: chat_metric, trace_id: trace_id, answer: result, latency_ms: elapsed, } await manager.broadcast(payload) return payload这里的核心逻辑是chain.ainvoke接受一个question变量Langchain 会自动填充 Prompt 模板config里传的callbacks让整条链上的所有步骤都会上报给 Langfuse。run_id直接指定为我自己生成的trace_id这样 Langfuse 前端展示的链路 ID 和 API 返回给用户的 ID 是同一个回查的时候对得上。指标回收有两个来源。第一个是代码里手动测的延迟和返回结果第二个是 Langfuse 内部统计的 token 消耗和模型调用明细。前者适合实时推送到仪表盘后者适合事后的深度分析。两条数据流并不冲突互相补充。4.4 Langfuse 后台长什么样当一个请求走完之后打开 Langfuse 的 Web UI你会看到一条新的 Trace点进去是这个结构Trace 级别包含本次请求的输入输出全文、总耗时、总 token。Generation 级别包含模型名、Prompt tokens、Completion tokens、模型原始返回。Span 级别包含 Langchain 链上每个环节的时间消耗。我曾经靠这个页面还原过一次用户投诉的完整现场用户问了一个很长的技术问题系统把全部历史消息都塞进了上下文结果 Prompt 里实际有效指令被上下文淹没了模型开始东拉西扯。上图之前我只能靠猜上 Trace 之后问题原因一目了然——上下文管理策略必须改。5. WebSocket 实时推送答完题立刻把成绩单发出去5.1 WebSocket 端点与前端的握手逻辑FastAPI 对 WebSocket 的支持不需要额外引入库直接在路由装饰器里用websocket参数即可。核心是一个ConnectionManager类用来维护当前所有在线连接app.websocket(/ws/metrics) async def websocket_endpoint(ws: WebSocket): await manager.connect(ws) try: while True: # 持续接收消息主要是用来保持连接活跃 await ws.receive_text() except WebSocketDisconnect: manager.disconnect(ws) except Exception as e: manager.disconnect(ws)初学者容易掉进一个误区以为 WebSocket 端点一定要主动发消息才算生效。实际上它的工作机制是“等事件发生后服务端推送”所以在while True里等待前端发来的消息没毛病真正有数据要推的时候直接调用manager.broadcast()就行。我在广播方法里用了try-except包裹每个send_json防止某个连接异常导致整个广播链路崩溃。实际运行中WebSocket 连接会因为网络抖动、页面刷新等突然断开服务端如果不加保护一个异常连接就可能导致所有指标推不出去。5.2 指标消息的统一格式为了让前端好解析我统一了 WebSocket 消息的 JSON 结构。第一版实现里我随手把 key 写成了latency,tokens,answer后来发现不同事件类型难区分就改成带type字段的封装格式{ type: chat_metric, trace_id: uuid-xxx, answer: 模型的回答内容, latency_ms: 1523.45, prompt_tokens: 128, completion_tokens: 256, total_tokens: 384, model: deepseek-chat, timestamp: 2025-01-15T10:30:00Z }如果你后续想推送“错误事件”“系统状态”等其他类型只需要扩展type字段。这样一个 WebSocket 通道就能承载多种监控事件不必每类数据单独开一个连接。token 数据哪里来Langfuse 的CallbackHandler内部会捕获但我这里为了实时推送直接在链路上手动取。Langfuse的回调数据是异步批量上报的和 WebSocket 的实时推送存在时序偏差。所以我的做法是从 Langfuse 拿模型元数据级别的统计从 Langchain 返回的response_metadata或usage_metadata里拿 token 计数。5.3 心跳保活与断线重连两处不能省的工程细节WebSocket 有两个在本地调试时不会暴露、一上生产就立刻出问题的地方。第一个是心跳保活。默认情况下WebSocket 连接如果长时间没有数据帧交互中间的网络设备比如 Nginx、云负载均衡可能会判定连接空闲并主动断开。这就是为什么很多用户会遇到“页面放着不动一段时间后再回来监控数据不刷新了”。解决办法是让前端每隔 30 秒发一个心跳包服务端收到后不用回内容这个数据帧本身就能维持连接活跃。第二个是断线重连。浏览器原生 WebSocket 对象被关掉后不会自动重连所以必须自己实现。前端onclose事件里用一个定时器重新new WebSocket()同时加退避策略防止服务端还没恢复时客户端疯狂重连把服务端打爆。我在本地实测的时候用 Python 的websockets库模拟了断连场景前端断线重连逻辑正常工作之后才放心把它部署到开发环境。这种“看着不起眼但缺了就会出事故”的细节恰恰是网上很多示例代码里不会教的。6. 前端仪表盘不引入重型框架也能做出实时效果6.1 原生 HTML JavaScript 实现 WebSocket 客户端前端我没有用 Vue 或 React原因很简单整个页面的核心功能就是“连接 WebSocket 接收消息 渲染列表”用原生 JavaScript 完全够引入重型框架反而增加构建成本。如果你喜欢 Vue3 FastAPI 的组合后端的 WebSocket 接口无需改动前端换一种连接方式而已但我这套代码保证零构建、零依赖复制一个 HTML 文件就能跑起来。核心的 WebSocket 客户端代码现在很简洁// app.js const wsUrl (location.protocol https: ? wss:// : ws://) location.host /ws/metrics; let socket null; let reconnectAttempts 0; function connect() { socket new WebSocket(wsUrl); socket.onopen () { document.getElementById(status).textContent 已连接; reconnectAttempts 0; heartbeat(); }; socket.onmessage (event) { const data JSON.parse(event.data); renderMetric(data); }; socket.onclose () { document.getElementById(status).textContent 已断开; reconnectAttempts; const delay Math.min(3000 * Math.pow(2, reconnectAttempts), 30000); setTimeout(connect, delay); }; socket.onerror (err) { // onerror 后通常跟随 onclose由 onclose 统一处理重连 }; } function heartbeat() { if (!socket || socket.readyState ! WebSocket.OPEN) return; socket.send(ping); setTimeout(heartbeat, 30000); } connect();心跳包和重连策略都放在这几十行里了。reconnectAttempts的指数退避逻辑是避免“服务端重启期间前端疯狂重连”的关键最多退避到 30 秒一次。6.2 监控面板的信息层级设计仪表盘页面我分成了三个信息区顶部状态栏显示 WebSocket 连接状态、累计对话数、平均延迟、累计 token 消耗。这是全局概况。中间实时列表按时间倒序展示每一次对话的指标卡片包含 trace_id 前八位、模型、延迟、token 数。侧边详情区点击某一条指标卡片时展开显示完整的 Prompt 和返回结果摘要。列表渲染用最简单的 DOM 操作即可不用引入虚拟滚动库。如果对话频率不高比如平均每秒不到十条DOM 节点数量完全可控如果未来频率高了再考虑用 canvas 表格或虚拟滚动方案。页面上我用 ECharts 画了一个实时延迟趋势图。WebSocket 每推送一条指标就把最新延迟塞进一个数组图表自动滑动更新。ECharts 的setOption是增量更新的性能开销很小哪怕数据点累计到几千个浏览器也不会卡顿。6.3 一个容易被忽略的跨域问题如果你和我一样前端开发时直接用浏览器打开index.htmlfile://协议WebSocket 握手时浏览器会检查 CORS 头。FastAPI 需要在CORSMiddleware里允许目标来源开发阶段我用allow_origins[*]图省事生产环境务必改成具体的域名。如果你用 Nginx 把前端静态文件和后端接口挂在同一个域名下就不存在跨域问题WebSocket 也可以用相对地址拼接大大减少配置成本。这是我最推荐的部署方式。7. 联调实测与高频避坑记录7.1 RunnableParallel 并行分支里callback 为什么丢了Langchain 的RunnableParallel可以并行执行多个子任务很适合“同时向多个模型请求答案再汇总”的场景。但我在实验阶段发现两个并行分支里只有一个分支的 Trace 被 Langfuse 记录下来另一个分支“消失”了。排查后确认是 callback 配置的传递问题。RunnableParallel的每个子任务其实是独立的Runnable如果子任务内部没有显式声明configured的 config外层的callbacks列表就不一定会传到所有分支。解决办法有两种一种是通过.with_config()把 callback 分别挂到每个分支上另一种是给并行分支加一个统一的RunnablePassthrough中转层。实际业务里如果只是做监控而不是调试并发逻辑我建议先用简单的顺序链路避免这个问题。7.2 WebSocket 连接被莫名断开Nginx 超时与客户端假死我在开发环境一切正常部署到服务器后遭遇了[websocket] onclose code: 1006的经典问题。1006 是“连接异常关闭”的代码意味着连接不是通过正常的 close 帧结束的而是网络层直接断掉了。查了两天元凶是 Nginx 的默认配置proxy_read_timeout默认只有 60 秒超过 60 秒没有任何数据通过代理Nginx 就把连接掐了。解决方法是给 WebSocket 专用的 location 单独配置location /ws/ { proxy_pass http://backend:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }配合前端的 30 秒心跳连接就能长时间稳定维持。7.3 DeepSeek 流式模式下token 统计容易失真我在第一版代码里开了streamingTrue结果发现 Langfuse 里记录的completion_tokens经常是 0 或者缺失。原因在于流式模式下token 数据是分块返回的最后的usage_metadata需要从最后一个 chunk 里汇总而 Langchain 的部分版本在流式迭代器场景下没有把 usage 数据完整传给回调。我的解决办法是在递给ChatOpenAI的参数里不依赖默认的 token 统计而是从最终返回的response_metadata或usage_metadata里手动提取再封装到指标 payload 里推送。这样即使 Langfuse 那边因为流式问题缺 token我的实时面板依然能拿到准确消耗量。Langfuse 端的数据缺失问题通过升级langfuseSDK 到较新版本后也基本解决了。7.4 FastAPI 热更新失效的几种情况开发时如果你习惯用uvicorn main:app --reload遇到改代码后不自动重启可以按下面顺序排查是不是在 IDE尤其是 PyCharm里直接点了运行按钮PyCharm 的 Python 运行配置不会自动带--reload要在 Edit Configurations 里给附加参数加入--reload。是不是uvicorn版本过旧新版 uvicorn 对 watchfiles 的依赖是自动的但过旧版本可能出现监听失效。是不是监听的目录不包含你修改的文件--reload默认监听当前目录如果你把代码放在其他目录需要手动指定--reload-dir。这类小问题单个看不严重但在连续开发调试时非常消耗耐心提前了解能省不少时间。7.5 给新手的建议先跑通最小闭环再上生产这套系统涉及五个组件如果一上来就追求“完整形态”很容易陷入“组件太多不知道哪里出错”的窘境。我自己当时的推进顺序是先用 FastAPI 写一个不含 Langchain 的/api/chat直接调用 DeepSeek 官方 SDK确认模型接口通。引入 Langchain重构服务端确认链式调用结果和直接调用一致。接入 Langfuse从后台看到第一条 Trace 的完整链路。增加 WebSocket 端点用简单的浏览器控制台客户端验证广播。最后才做前端仪表盘页面把数据可视化。每一步都有一个明确的验证节点出了问题可以很快定位到具体组件。这套“渐进式搭建”的方法比一次性把全部代码写完再调要稳妥得多也更容易积累对每个组件的直观理解。最后补一刀监控数据的价值要用起来到现在这套监控仪表盘已经在我这边稳定跑了几周。抛开技术实现我最想说的是监控系统的价值不在“能从后台看到数据”而在“数据能否推动行动”。Langfuse 后台里积累的每一条 Trace都在回答几个永恒的问题——Prompt 写得好不好模型调用贵不贵用户从哪些环节流失对话质量什么时候开始劣化我自己的习惯是每周固定花一点时间翻一遍 Langfuse 里这一周的高延迟 Trace 和高 token 消耗 Trace把规律记下来下个迭代优先处理暴露最多问题的环节。接入监控之后团队不再靠“感觉”判断系统状态而是靠数据做决策这是整个项目带给我最大的收获。你现在搭建的这套仪表盘也会是一个很好的起点。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →