资讯动态

LLM应用可观测性实战:Langfuse+WebSocket构建实时对话监控架构

发布时间:2026/9/11 4:25:37 来源:尧图企业网站定制
上个月我在排查一个线上问题用户反馈“AI 回答突然变得特别慢而且开始胡说八道”。我翻完服务端日志只看到一条 prompt 和一条 final response中间到底调了什么工具、填了什么上下文、token 消耗了多少完全是一片空白。那个瞬间我就决定了必须给 LLM 应用装一套自己的监控仪表盘而不是继续靠 print 和猜。这篇文章就是那次重构的记录基于 Langfuse LangChain DeepSeek FastAPI WebSocket 从零搭建一套实时对话监控架构包含核心源码思路和我在实际部署中踩过的坑。适合正在做 AI 应用、但可观测性还停留在日志阶段的团队和个人开发者阅读。我会把为什么选这些组件、消息链路怎么串、前端怎么实时刷新、哪些版本坑必须绕开全部摊开讲。1. 为什么要给 LLM 对话装“黑匣子”监控的边界与目标很多人觉得 LLM 应用上线只需要关注接口通不通、回复有没有出来。真正跑到生产环境你会发现跟传统 Web 接口完全是两码事模型调用是一个概率系统同样的 prompt 每次都可能输出不同结果token 消耗直接等于真金白银。所以监控的第一步不是选工具而是想清楚“我要看什么”。1.1 传统日志方案为什么撑不住 LLM 应用传统日志是“打点式”的你在关键代码里写 logger.info然后在 ELK 或者 Loki 里按关键字搜。这个模式在 LLM 场景里有三个致命问题。第一调用链太深。一次用户提问LangChain 内部可能经历了 Retriever 召回、Prompt 组装、LLM 调用、输出解析、工具调用每一步都有独立耗时。如果只在路由层打一个 info中间某一跳超时了根本定位不到。第二token 计量是结构化数据不是字符串。输入了多少字符、缓存命中多少、总计费多少这些字段用日志文本记录再解析既浪费存储又容易错。第三无法回放。用户说“回答变差了”你得能还原出当时模型到底看到了什么完整的 prompt、用的什么参数、有没有触发工具调用。传统日志很难做到按 trace 把整条链路串起来。1.2 监控体系需要覆盖的三层指标我在设计监控目标的时候把数据拆成了三层这个分层决定了后面所有代码的组织方式。第一层是调用链追踪也就是 trace。一次用户会话对应一条 tracetrace 下面挂 chain 节点、LLM 节点、检索节点。每个节点记录输入、输出、延迟、token还有父子关系。这一层解决“发生了什么”。第二层是实时状态包括在线连接数、当前正在处理的请求数、最近一分钟的平均首字延迟。这一层解决“现在是不是在出问题”。它并不需要精确到 100%只需要趋势正确所以我用 WebSocket 推送最近的事件和聚合指标到仪表盘。第三层是成本与质量分析比如按小时统计 token 消耗、按模型维度对比延迟和错误率、记录每次调用的分数评价。这一层解决“下周我要不要换模型”和“这个功能到底烧多少钱”。这三层里第一层和第三层是 Langfuse 最擅长的它本身就是专门做 LLM 可观测性的自带 trace 树和 token 成本统计。第二层则必须自己实现Langfuse 面板不会主动把数据推到你的浏览器仪表盘的实时刷新还是得靠 WebSocket 自建一条私有通道。1.3 技术选型对比Langfuse 与自建埋点的取舍在选型的时候我纠结过 5 个方向直接给 LangChain 打日志、自建 MySQL 表存 trace、接 SkyWalking 之类的通用 APM、用 Langfuse、用 LangSmith。通用 APM 对 LLM 的 token 成本计算支持几乎为零而且没有 prompt 模板的概念很快排除。自建 MySQL 表在最早期也试过问题在于 LangChain 的回调事件非常多一个 chain 里嵌套两三个 LLM 调用就会产生十几条 event你得一五一十维护 parent 关系工作量完全划不来。LangSmith 功能最强但它是 SaaS 产品数据要传到 LangChain 官方服务很多公司过不了这一关。Langfuse 胜在三点开源可自托管、数据落在自己服务器、对 LangChain callback 的接入几乎是零侵入。它提供 Langfuse CallbackHandler直接塞进 LangChain 的 callbacks 数组就能生成完整 trace。我最后选 Langfuse准确说是自托管的 Langfuse后来跑熟了发现这个选择非常值。2. 架构怎么串一次对话到仪表盘渲染的完整数据流这一章先说清楚整体骨架。整套系统分为数据面和控制面数据面是 FastAPI 提供的对话 API控制面是 Langfuse 和 WebSocket 组成的观测通道。2.1 五个组件各管一段FastAPI 负责接收用户请求创建会话上下文调用 LangChain 链同时维护 WebSocket 连接池。LangChain 负责编排把 prompt、模型、工具串成一个可执行的链。DeepSeek 是实际产生回复的模型通过 OpenAI 兼容协议提供 chat completion 能力。Langfuse 负责把 LangChain 产生的 trace 事件持久化并计算 token 成本、延迟、错误率。WebSocket 负责把监控事件从后端实时推到前端仪表盘。2.2 消息链路的四站接力一次完整的数据流是这样的第一步前端把用户消息通过 HTTP POST 发到 FastAPI 的 /api/chat 接口。FastAPI 生成一个 trace_id把它注入 LangChain 的 config 对象。第二步LangChain 链在运行过程中同一个 config 里的 callbacks 列表会同时包含两个处理器Langfuse CallbackHandler 负责把事件上报给 Langfuse 服务自定义的 DashCallbackHandler 负责把结构化事件推送到 asyncio.Queue。第三步FastAPI 的 WebSocket 推送任务从队列里取出事件通过 manager 广播给所有已经连接的前端页面。这一步用的是 WebSocket full-duplex 通道前端不需要轮询。第四步Langfuse 服务完成 trace 的聚合和存储前端也可以从 Langfuse 的 API 拉取历史数据用于画趋势图。这套设计的核心思想是双写实时仪表盘走 WebSocket 通道拿到的是一手事件延迟基本在毫秒级历史分析和成本报表走 Langfuse 通道拿到的是一致性数据适合做聚合查询。两条链路互不阻塞Langfuse 挂了下游服务对话 API 也不会挂。2.3 为什么实时性必须靠 WebSocket而不是轮询最朴素的方案是前端每隔三秒调一次 FastAPI 的 GET /api/metrics把最新的指标拿回来。这个方案在对话 QPS 低于 5 的时候完全够用代码还简单。但有两个问题会让你被迫升级到 WebSocket。一个是首字延迟。用户发出问题后你希望监控面板上立刻出现“请求到达”的状态然后看到 LLM 调用开始、结束。如果轮询周期是 3 秒那你看到的信息总是滞后的出现故障时的现场感基本消失。另一个是连接成本。WebSocket 建立一次连接后双向复用而 HTTP 轮询每次都要重新握手当面板数量增多、对话 QPS 上涨服务器花在维持轮询上的资源会非常难看。尤其是当你想在面板上同时显示多个在线服务节点时轮询压力会线性增长WebSocket 的长连接优势会越来越明显。3. 地基先打好FastAPI、LangChain 与 DeepSeek 的最小对话闭环监控系统是建立在正常对话服务之上的先把基础跑通再接观测层。如果基础链路都磕磕绊绊后面所有监控数据都没有意义。3.1 环境准备和依赖版本我建议用 Python 3.11三个主要依赖的版本坑我在后面会单独讲先给一份能用的版本组合fastapi0.115.6 uvicorn[standard]0.30.6 langchain0.3.14 langchain-community0.3.14 langchain-deepseek0.2.3 langfuse2.54.1 openai1.57.4 websockets14.1这里的关键是 langchain-deepseek 这个包它是 DeepSeek 官方维护的 LangChain 集成内部走 OpenAI 兼容协议但不需要手写 base_url。如果你用旧的 ChatOpenAI 自定义 base_url 方式也能跑通但类型提示和回调事件里很多字段是空的监控数据会不完整。3.2 DeepSeek 接入 LangChain 的三种姿势接入 DeepSeek 我试过三种办法给后来的人对比一下。第一种直接调用 OpenAI SDK把 base_url 指向 DeepSeek 的接口地址然后手动封装成函数。灵活但没有任何调用链追踪能力放弃。第二种langchain-openai 包里的 ChatOpenAI通过自定义 base_url 走 DeepSeek 协议。能用但在回调事件里返回的 model_name 可能不太准确需要自己额外处理。第三种langchain-deepseek 包这也是我最终用的。代码很简洁from langchain_deepseek import ChatDeepSeek llm ChatDeepSeek( modeldeepseek-chat, api_keyDEEPSEEK_API_KEY, temperature0.7, max_tokens2048, timeout60, max_retries2, )注意 max_retries 这个参数LangChain 默认的 retry 策略对 DeepSeek 是生效的但如果你的下游系统有重复请求的隐患最好在 API 层做幂等否则一个超时会造成多次计费。3.3 会话链与上下文组装基础对话服务我用 LangChain 的 prompt template 加 chat history 组装上下文。核心逻辑是每次请求从请求体里拿 session_id然后从内存里或者 Redis 里取历史消息组装成 messages 列表。from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser prompt ChatPromptTemplate.from_messages([ (system, 你是一个智能助手请用中文回答用户问题。), MessagesPlaceholder(variable_namehistory), (human, {input}), ]) chain prompt | llm | StrOutputParser()这里有个细节值得说MessagesPlaceholder 的变量名必须叫 history这样 LangChain 才能把历史消息列表正确地序列化成 OpenAI 格式的 messages。如果你用别的变量名LangChain 不会报错但历史消息会被整体当成一个字符串拼进输入token 消耗直接翻倍。FastAPI 侧暴露一个同步接口app.post(/api/chat) async def chat_api(req: ChatRequest): trace_id uuid.uuid4().hex history await load_history(req.session_id) config {configurable: {session_id: req.session_id, trace_id: trace_id}} result await chain.ainvoke( {input: req.message, history: history}, configconfig, ) await save_history(req.session_id, req.message, result) return {answer: result, trace_id: trace_id}先跑通这一层然后我再接 Langfuse。4. Langfuse 接入与事件落库把 Token 和内部调用变成可查的数据Langfuse 接入是整个监控体系里收益最高、代码改动最小的一步。它跟 LangChain 的 callback 机制深度集成几乎不需要改业务代码。4.1 自托管 Langfuse 的部署与密钥配置我用 docker compose 自托管了一个 Langfuse环境变量里核心是数据库配置和 SALT。生产环境我建议至少四个容器web 服务、worker、PostgreSQL、Redis。Langfuse 的 worker 负责异步处理 trace 事件如果 worker 挂了事件会在 Redis 里堆积不会丢失。部署完成后进入项目设置创建 API Key获取三个值public_key、secret_key、host。前两个相当于 Langfuse 的用户凭证host 指向你自己的服务地址。接入 LangChain 只需要初始化一个 handlerfrom langfuse.callback import CallbackHandler langfuse_handler CallbackHandler( public_keypk-lf-xxxx, secret_keysk-lf-xxxx, hosthttp://localhost:3000, flush_at1, # 每 1 条就批量上报 flush_interval5, # 最多 5 秒上报一次 )这里 flush_at 和 flush_interval 会影响实时性。面板要求尽可能低的延迟所以我调低到 flush_at1。如果只是为了事后分析不需要这么激进调成 flush_at10 能明显减少网络请求数。4.2 Callback 接入在 LangChain 的 config 里同时挂两个处理器Langfuse 官方文档通常会告诉你在 chain.invoke 里传 callbacks。你需要理解的是callbacks 是一个数组可以同时挂多个处理器。我把 Langfuse handler 和后面要支持 WebSocket 推送的自定义 handler 都放进同一个数组dash_handler DashCallbackHandler(user_idreq.user_id) config { callbacks: [langfuse_handler, dash_handler], configurable: { session_id: req.session_id, trace_id: trace_id, }, } result await chain.ainvoke( {input: req.message, history: history}, configconfig, )Langfuse 会自动从 config 里取 trace_id 作为这次调用的 trace 主键。这样 Langfuse 面板里的 trace 就能跟 WebSocket 推送的事件对应上排查问题的时候两边对着看效率非常高。4.3 Trace、Observation、Span 三个概念映射到对话场景Langfuse 里的数据结构刚开始容易混淆我用自己的话梳理一遍。Trace 代表一次端到端的会话请求从用户发消息开始到最终回答返回结束。对应到我们的代码就是一次 /api/chat 请求trace_id 由我们自己生成保证系统里的对话记录和 Langfuse 记录能对上。Observation 是 Trace 下的子节点表示一次具体的操作。在 LangChain 链路里一个模型调用是一个 LLM observation一次检索是一个 retrieval observation一个 sub-chain 也是一个 observation。Langfuse v4 之后的 UI 把 observation 概念进一步整合但本质上还是树形结构。Span 就是带耗时区间的 observation。我们在仪表盘上看到的延迟瀑布图底层数据就来自这些 span 的开始时间和结束时间。LangChain 回调的一个优势是这些树形关系是自动维护的你不需要手动传 parent_observation_id。LangChain 内部用 run_id 关联父子关系Langfuse 的 callback 会读取这个 run_id 并建立正确的树。4.4 成本和采样的控制Langfuse 会把输入输出的完整内容存进数据库这是在默认情况下就生效的。对于高并发对话服务如果你的需求只是监控错误和延迟不需要存全量 prompt 内容建议开启采样langfuse_handler CallbackHandler( public_keypk-lf-xxxx, secret_keysk-lf-xxxx, hosthttp://localhost:3000, sample_rate0.2, )sample_rate0.2 表示只有 20% 的 trace 会被上传。这样做让实时监控的覆盖度打折但存储成本直降 80%。我个人建议线上分两层全量上报但只把 trace 的 metadata 上报不带完整 prompt对错误 trace 再开启完整内容上报。Langfuse 支持出错时自动捕获完整内容配置一下即可。5. WebSocket 实时通道从后端监听回调到浏览器刷新的最后一公里Langfuse 解决了历史数据和分析但它的面板是服务端渲染页不会主动推事件。为了实现“用户一发消息监控面板上立刻出现对应状态”必须自己搭一条实时通道。这就是整套架构里 WebSocket 价值最大的一段。5.1 FastAPI WebSocket 端点与连接管理FastAPI 对 WebSocket 的支持很成熟但连接管理要自己写。我维护了一个简单的 ConnectionManagerclass ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) def disconnect(self, websocket: WebSocket): self.active_connections.remove(websocket) async def broadcast_json(self, data: dict): for ws in self.active_connections[:]: try: await ws.send_json(data) except Exception: self.active_connections.remove(ws)这里有一个重要细节send_json 在连接异常时会直接抛异常必须在循环里捕获并清理失效连接。如果不做清理一个断线的前端页面会让广播任务越跑越慢最终阻塞整个事件循环。端点部分很简单app.websocket(/ws/dash) async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: while True: await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(websocket)receive_text 在这个场景下只是为了维持连接可用来接收前端的心跳 ping。5.2 自定义回调处理器把 LangChain 事件变成 WebSocket 消息实时推送的关键是从 LangChain 的回调事件中提取结构化数据。我这里用的是 LangChain 的 AsyncCallbackHandler 接口它会在异步链运行的不同阶段被调用。from langchain_core.callbacks import AsyncCallbackHandler class DashCallbackHandler(AsyncCallbackHandler): def __init__(self, manager: ConnectionManager, user_id: str): self.manager manager self.user_id user_id async def on_llm_start(self, serialized, prompts, **kwargs): run_id kwargs.get(run_id) await self.manager.broadcast_json({ type: llm_start, run_id: str(run_id), ts: time.time(), }) async def on_llm_end(self, response, **kwargs): llm_output response.llm_output or {} token_usage llm_output.get(token_usage, {}) await self.manager.broadcast_json({ type: llm_end, run_id: str(kwargs.get(run_id)), input_tokens: token_usage.get(prompt_tokens, 0), output_tokens: token_usage.get(completion_tokens, 0), total_tokens: token_usage.get(total_tokens, 0), model: llm_output.get(model_name, ), }) async def on_llm_error(self, error, **kwargs): await self.manager.broadcast_json({ type: llm_error, run_id: str(kwargs.get(run_id)), error: str(error), })这里需要理解 LangChain 的 response 对象和 Langfuse 拿到的数据是同一份事件源。换句话说Langfuse 负责落库DashCallbackHandler 负责实时广播两份数据的一致性来自 LangChain 内部同一套 run 机制。实际项目里我还会在 on_chain_start 和 on_chain_end 里把整个链的阶段上报。例如“检索开始”“检索结束”“生成开始”“生成结束”前端面板就能画出对话的瀑布图。5.3 事件与 WebSocket 推送的并发模型这个架构最容易出错的地方是同步回调与异步推送之间的线程模型。LangChain 的 AsyncCallbackHandler 是在异步链中被调用的所以 on_llm_end 本身就运行在事件循环线程里。我在 on_llm_end 里直接调用 manager.broadcast_json 是安全的因为 async 函数可以 await而且没有跨线程问题。如果你用的是同步链也就是 chain.invoke 而不是 chain.ainvoke那么 AsyncCallbackHandler 不会被调用。那样需要换用 BaseCallbackHandler并在同步回调里把事件放到一个线程安全的队列由一个后台线程负责 WebSocket 推送。两套方案不要混。5.4 前端连接、心跳与断线重连前端我用原生 WebSocket没有引入额外的库。连接地址是 ws://host/ws/dash连接成功后立即发一条 ready 消息然后服务器开始推送。心跳机制非常重要。普通 HTTP 协议有连接超时WebSocket 如果没有定期通信会被中间设备杀掉。我让浏览器每隔 20 秒发一条 ping 字符串服务器收到后原样返回。FastAPI 端点里只调用了 receive_text()没有回包逻辑但为了兼容一般网关系最好也回一下while True: message await websocket.receive_text() if message ping: await websocket.send_text(pong)断线重连这里我踩过一个坑如果浏览器因为网络抖动断开WebSocket 的 onclose 事件会触发如果不做重连面板会一直停在旧数据上。正确做法是在 onclose 里设置定时器延迟两秒后重新连接。但要注意重连不能无限频繁否则服务端会累积大量 TIME_WAIT 连接。我用指数退避第一次重连等 2 秒第二次 4 秒最多 60 秒用户手动刷新可以随时重置。6. 仪表盘核心实现连接管理、消息协议与三块关键面板前端是整个项目直观价值所在。我没有用重型框架一个 Vue 3 的简易项目配合 ECharts 和原生 WebSocket足够支撑这套监控需求。6.1 消息协议设计WebSocket 是长连接所有消息都是 JSON 对象。为了区分不同事件类型我在每个消息里带一个 type 字段。目前定义了六种type说明关键字段llm_start模型开始调用run_id, tsllm_end模型调用完成run_id, input_tokens, output_tokens, total_tokens, model, latency_msllm_error模型调用失败run_id, errorchain_start链开始执行run_id, namechain_end链执行完成run_id, name, latency_mssystem_status系统聚合状态online_users, active_requests, last_minute_qps前端根据 type 分发给不同的处理函数这个模式非常干净。6.2 实时对话流面板对话流面板是仪表的中心区域有点像一个简易聊天日志。用户发起的每次请求在 chain_start 事件到达时新建一条记录显示请求内容和用户 IDllm_end 事件到达后补上 token 数和耗时并在旁边用一个绿色状态标签显示“完成”。这样做最大的价值是把异步产生的孤立事件串成了有上下文的时间线。之前排查问题需要在后端日志里按 trace_id 搜索现在所有信息都在面板上实时滚动一眼就能看到哪条链路卡在哪里。6.3 Token 趋势与调用链表格两个次要面板分别是 Token 曲线和调用链表格。Token 曲线用的是 ECharts 的折线图数据源就是 llm_end 事件的 total_tokens。每次收到事件就往数组尾部追加一个点窗口保留最近 500 条。我还会画一条滑动平均值线方便观察趋势。这里要注意token 数据到界面上不需要额外聚合展示最近一分钟的总和更有参考价值。调用链表格则直接复用 Langfuse 的树形 trace 概念前端展示 run_id 、开始时间、耗时、输入 token、模型名。点击任一节点可以跳转到 Langfuse 的 trace 详情页——因为我在推送事件里存了 trace_id拼接一下 URL 就能用。7. 踩坑实录我从“能跑”到“稳定跑”过程中排掉的五个雷这一部分是整套方案里最有参考价值的内容。很多问题不是看文档能发现的得等线上跑一段时间才暴露。7.1 事件循环被同步调用阻塞WebSocket 假死第一个踩到的坑是服务在正常运行但 WebSocket 推送经常延迟几秒有时候甚至完全断掉浏览器里报WebSocket onclose code 1006。排查后发现原因是 LangChain 链里混入了一个同步的检索函数比如直接用的 requests.get 或者普通的 Elasticsearch 客户端。这个同步函数阻塞在 FastAPI 的 async 事件循环里导致 loop 被卡住几百毫秒甚至更长WebSocket 的心跳包无法及时回复中间网关就判定连接超时直接断开。解决方案是把所有同步检索函数用anyio.to_thread包一层让它们在线程池里运行或者统一改用异步客户端。1006 错误的核心不是 WebSocket 代码本身而是你的事件循环“呼吸不畅”。7.2 Langfuse 异步回调在 uvloop 下的兼容问题Langfuse 的 CallbackHandler 在异步环境下使用 httpx 上传事件。当 uvicorn 配置了--loop uvloop时某些旧版 langfuse 会和 uvloop 的事件循环策略产生冲突表现为偶发的 trace 丢失控制台打印event loop is closed。我当时的处理方式是升级 langfuse 到 2.x 最新版问题消失。如果你不想升级也可以在启动时改回 asyncio 事件循环uvicorn.run(app, loopasyncio)。但建议还是升级老版本在新 Python 上的问题只会越来越多。7.3 多进程部署下 WebSocket 连接只能连到随机进程本地单进程跑得好好的一上生产多 worker 就出问题。FastAPI 如果用 uvicorn --workers 2 启动两个进程各自维护自己的 ConnectionManager用户 A 连到了 worker1但请求被负载均衡分到了 worker2worker2 的 manager 里没有这个连接事件推送自然发不出去。解决办法有两个按场景选。低并发场景最省事把 uvicorn 的 workers 设为 1一台实例一个进程WebSocket 管理器就是进程内的。需要多实例横向扩展时必须引入 Redis Pub/Sub两个 worker 各自订阅同一个 channel任一 worker 产生事件往 Redis 里 publish所有 worker 收到后广播给各自持有的连接。我使用的是第二种方案核心逻辑是把 broadcast_json 改为先 publish 到 Redis再由订阅任务调用真正的 manager.broadcast_json。注意这样会带来一次 Redis 往返约增加 0.5ms 到 1ms对监控面板完全可接受。7.4 流式输出时追踪 ID 传播中断我的对话服务后来从一次性返回改成了 SSE 流式输出结果发现 Langfuse 里的 trace 断成了好几截有的 LLM 调用没有父节点。原因是在流式场景下我用 create_stream 单独创建了 LLM 对象而不是复用主链的 config。LangChain 在流式分支里生成的新 run 没有继承父 run 的 metadata 和 callbacksLangfuse 就无法把它挂到同一个 trace 下。解决方法是把 callbacks 和 configurable 明确传入流式调用的 ainvoke 参数不要在链里靠全局隐式传播。一旦涉及并发或流式显式传递才是稳定的。7.5 DeepSeek 请求超时与重试的幂等设计DeepSeek 高峰期偶尔会返回 503 或超时。LangChain 的 max_retries 会后台自动重试但这会带来一个隐蔽问题你的对话服务自身如果还有一层重试机制两层重试叠加可能让用户等待时间翻两三倍而且造成重复计费。我最后的策略是三层控制外层 FastAPI 接口不重试直接报错并生成一条 error traceLangChain 内部 max_retries 固定为 1DeepSeek 的 timeout 默认不调低保持 60 秒。同时把“是否重试过”作为 trace 的一个 metadata 打点监控面板上可以一眼看到哪些请求消耗了重试次数。8. 压测结果与后续想做的三件事按上面的架构跑通后我用 locust 压了一轮模型调用 mock 成随机延迟主要压 WebSocket 推送和 HTTP 对话接口的耦合度。8.1 压测数据测试环境是 4 核 8G 的云主机PostgreSQL 和 Redis 都跑在同一台机器上。200 并发压测 10 分钟结论是对话接口 P95 延迟约 400msWebSocket 事件推送延迟中位数在 8ms 左右P99 在 33ms 左右。Langfuse 上报和 WebSocket 广播在事件循环里因为都走异步没有明显互相拖累。压测中我观察到 Langfuse 的 HTTP 上报偶尔会出现 batch 超时导致一小部分 trace 延迟入库。这个不影响实时面板因为面板靠的是 DashCallbackHandler 直接广播Langfuse 仓库即便晚两秒事后也能查。8.2 两个还可以继续深入的点一个是把 WebSocket 推送改成按 trace_id 订阅的频道模式而不是全量广播。现在所有连到面板的浏览器都能看到所有用户的事件内部使用没问题但一旦要做多租户或者给客户开放面板必须改成按会话订阅。另一个是引入 LangGraph 的节点级监控。LangChain 的 callback 能覆盖 chain 层面的 span但 LangGraph 的节点编排更适合复杂 agent 场景节点内部再挂 langfuse 可以拿到更细的执行图。如果你正在做 agentLangGraph 配合 Langfuse 的收益比纯 LangChain 更高。8.3 仪表盘与业务告警的联动后续我想把 WebSocket 推送的消息再接一层告警规则引擎。比如当连续五条 trace 都出现 llm_error或者单次 total_tokens 超过预设阈值时主动推一条 alert 事件到面板同时通过飞书或企业微信 webhook 发送通知。这样监控就不再只是“事后查看”而是变成了“事前预警”。有了这套实时数据通道做告警只需要在广播逻辑里加一个 if 判断成本很低。回到开头那个案例现在再遇到“AI 变慢、胡说八道”我的标准排查路径是先看面板上的实时 token 曲线和错误事件定位是哪条链路再从 Langfuse 里按 trace_id 调出完整输入输出和子步骤延迟最后根据 context 判断是上下文爆炸、模型问题还是某一步外部依赖抖动。整个过程基本在三分钟之内。个人最大的体会是Langfuse 这类可观测工具配合 WebSocket 自建实时通道把 LLM 应用从“黑盒”变成了“透明盒子”这比在代码里加一百行 logger 更有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价