ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LLM应用可观测性实战:Langfuse+Langchain+DeepSeek监控系统

LLM应用可观测性实战:Langfuse+Langchain+DeepSeek监控系统 如果你也在用 DeepSeek 这类大模型做线上服务一定遇到过类似的场景用户跑来问“为什么今天的回答质量变差了”你打开日志一看HTTP 200、响应正常、耗时也正常——可问题就是复现不出来。我上个月做 AI 客服时就卡在这个坑里最后被逼着把对话监控这件事从零搭了一遍整套方案就是题目里这套组合Langfuse Langchain DeepSeek FastAPI WebSocket。这套东西做完之后我才算真正“看见”了每次对话的全过程prompt 长什么样、模型返回了什么、Token 烧了多少、单个请求延迟多久、上下文窗口用了多少。出现异常时不用再靠猜打开仪表盘就能定位是哪一段链路出了问题。这篇记录会从架构选型、后端实现、Langfuse 埋点、WebSocket 实时推送、Vue3 前端展示一直写到上线后的踩坑复盘代码都是可以直接复制运行的最小工程适合正在用大模型做项目、想给服务加一层可观测性的读者。1. 起因AI客服上线第二周我连“回答变差”的原因都查不到事情是这样的。我们做的是一个面向 C 端的智能客服底层对话能力用的 DeepSeek。上线第一周一切正常第二周开始用户投诉变多集中在“重复回答”“答非所问”这两类。我去翻后端日志除了状态码和耗时什么都看不到——没有完整的输入输出、没有提示词版本、没有 token 消耗统计更没有一个统一的视角去看一次会话里到底发生了什么。当时最崩溃的一点是你根本不知道问题是出在模型本身、提示词、上下文拼接还是用户输入太模糊。因为没有任何基线数据你甚至没法验证“昨天和今天模型表现是否有差异”。后来我去社区逛了一圈发现这不是我们一家的问题做 LLM 应用的团队普遍存在这个盲区。传统的 APM 工具只能看到容器和接口层面的健康度看不到 prompt 和模型输出这一层。于是决定给整个对话链路加一套可观测系统。我调研了 Langfuse、LangSmith、Helicone 这几个方案Langfuse 是开源的可以自部署能把 Trace、Span、Token 用量、延迟、成本全部记录下来还提供了 Python SDK 的CallbackHandler能和 Langchain 无缝对接。这就是整篇文章最核心的监控底座。实际方案里还缺一个面向运营和开发团队的实时展示层。Langfuse 自带的 UI 偏调试和回溯不适合直接丢给产品同事看。所以我用 FastAPI 做后端聚合服务Vue3 写了一个精简仪表盘WebSocket 负责把每次对话的关键指标实时推到前端。整体下来这套组合的定位就是Langfuse 负责记录一切FastAPI 负责把记录变成接口WebSocket 把新事件推到仪表盘Langchain 负责串联 DeepSeek 调用。如果你只是自己调试模型Langfuse 单独就够用。但如果你要的是“团队可看、实时刷新、能定位线上问题”的监控面板就值得照着这套架构自己搭一版。2. 架构拆解Langfuse记录、Langchain编排、FastAPI与WebSocket搬运2.1 五个组件各自管什么先看整体链路我把它分成三层。用户聊天页面发起提问请求到 FastAPI 的/api/chat接口。FastAPI 调用 Langchain 构建的链链的内部是提示词模板、模型调用的组合最终请求打到 DeepSeek API。这个调用过程通过 Langfuse 的CallbackHandler自动上报记录成一次 Trace。拿到模型返回后FastAPI 把回答返回给聊天页面同时把这次的 token 用量、延迟、模型名等指标打包成一条监控事件通过 WebSocket 广播给所有在线的仪表盘客户端。用一张表看各组件职责更清楚组件扮演的角色核心价值DeepSeek对话模型服务提供 deepseek-chat 和 deepseek-reasoner 能力LangchainLLM 应用编排框架把提示词、模型调用、输出解析串成链方便统一挂回调LangfuseLLM 可观测性平台记录 Trace、token 用量、耗时、成本提供回溯查询 APIFastAPI后端聚合服务承接聊天请求、管理 WebSocket 连接、转发监控事件Vue3前端仪表盘实时展示指标、对话列表、Trace 详情这套设计最关键的一点是对话链路和监控链路是解耦的。对话链路走 HTTP监控链路走 WebSocket两者在 FastAPI 内部交汇。即使 WebSocket 断开了聊天服务也不会挂即使 Langfuse 暂时不可用模型调用照样能完成。Langfuse 的回调失败默认不会阻断主流程这很重要。2.2 为什么选 WebSocket 而不是 HTTP 轮询最初我也考虑过最省事的方案前端每 5 秒调一次/api/metrics拉取最新数据。但实际做下来发现问题很多。LLM 调用本身是不定时的用户可能连续问 5 句也可能 10 分钟不开口。轮询模式下没有新事件时每次请求都在空转白白耗费服务端资源有新事件时又最多延迟 5 秒展示监控面板的“实时性”就打了折扣。更麻烦的是轮询接口要维护“上次拉取到哪条消息”的游标多客户端在线时消息会重复或者丢失。WebSocket 解决的是“服务端主动推送”的问题。FastAPI 原生支持 WebSocket不需要额外装库连接建立后双向通信聊天事件产生的一瞬间就能推到前端。对监控仪表盘这种场景它就是比轮询更合适的传输层。2.3 关于 LangGraph这轮先不上选型时团队里有人提议直接用 LangGraph 代替 Langchain因为 LangGraph 支持复杂的状态流转未来做 Agent 更方便。我的判断是这个监控仪表盘项目的核心诉求是“在现有 LLM 调用链路里插入可观测性”不是构建 Agent 状态机。Langchain 用prompt | llm这种 LCEL 表达式就能把链路组织好代码量小回调机制也是现成的。LangGraph 更擅长的场景是有多步骤、条件分支、循环、跨多轮保持状态的编排。如果后面要上“规划-调用工具-再规划”这种 Agent 架构再把 LangGraph 引进来不迟。监控层不必提前为业务层的复杂度买单。3. 后端落地从空目录到 Langchain DeepSeek 跑通对话3.1 初始化环境和依赖假设你是在一台干净的 Linux 服务器或者自己电脑上操作。我用的是uv这个 Python 包管理器比 pip 快很多而且创建虚拟环境非常省事。如果你没装过先装它curl -LsSf https://astral.sh/uv/install.sh | sh然后建项目目录和环境mkdir ai-dialog-monitor cd ai-dialog-monitor uv venv .venv source .venv/bin/activate依赖清单如下我直接全部装好uv pip install fastapi uvicorn[standard] langchain langchain-openai langfuse python-dotenv这里有几个版本上的体会。langchain-openai是独立包ChatOpenAI从这里面导入不再用老版本的langchain.llms.OpenAIlangfuse的 SDK 版本我用的是 v3 以上对应 Langfuse v4 服务端。FastAPI 本身对新版本 Python 支持很好建议直接用 Python 3.10避免在老版本上遇到类型语法兼容问题。3.2 DeepSeek 接入 Langchain 的正确姿势DeepSeek 的 API 兼容 OpenAI 格式所以不需要任何定制封装直接借助ChatOpenAI把base_url指向 DeepSeek 的地址即可。这就是为什么市面上各种工具能轻松接入 DeepSeek——它遵循的是标准协议。# llm.py import os from langchain_openai import ChatOpenAI llm ChatOpenAI( modeldeepseek-chat, api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com/v1, temperature0.7, )注意base_url写完整路径带不带/v1我都试过带上的兼容性最稳。Langchain 底层走的是 OpenAI SDK有时候版本差异会导致补路径的行为不一致干脆直接写全。然后是构建链。为了能拿到 token 用量和响应元数据我这里没有接StrOutputParser而是让链直接返回AIMessagefrom langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI import os llm ChatOpenAI( modelos.getenv(DEEPSEEK_MODEL, deepseek-chat), api_keyos.getenv(DEEPSEEK_API_KEY), base_urlhttps://api.deepseek.com/v1, temperature0.7, ) prompt ChatPromptTemplate.from_messages([ (system, 你是一个友好的在线客服助手请用简洁的中文回答用户问题。), (human, {question}), ]) chain prompt | llm调用时这样拿结果result await chain.ainvoke( {question: 你们的退货政策是什么}, config{callbacks: [langfuse_handler]} ) answer result.content metadata result.response_metadataresult.response_metadata里就有 token 用量和模型名Langchain 对 OpenAI 兼容接口会自动填充{ token_usage: {prompt_tokens: 120, completion_tokens: 46, total_tokens: 166}, model_name: deepseek-chat, finish_reason: stop, }3.3 后端的三个接口设计这个后端服务只做三件事接收聊天请求、广播监控事件、对外提供 Trace 查询。三个接口分别对应这三种职责。# main.py import uuid, time from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel app FastAPI(titleAI Dialog Monitor) app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) class ChatRequest(BaseModel): session_id: str | None None question: str app.post(/api/chat) async def chat(req: ChatRequest): session_id req.session_id or str(uuid.uuid4()) start time.time() result await chain.ainvoke( {question: req.question}, config{callbacks: [langfuse_handler]} ) meta result.response_metadata usage meta.get(token_usage, {}) event { type: chat.completed, session_id: session_id, question: req.question, answer: result.content, model: meta.get(model_name, deepseek-chat), prompt_tokens: usage.get(prompt_tokens, 0), completion_tokens: usage.get(completion_tokens, 0), total_tokens: usage.get(total_tokens, 0), latency_ms: int((time.time() - start) * 1000), timestamp: int(time.time() * 1000), } await manager.broadcast(event) return {session_id: session_id, answer: result.content}心跳和 WebSocket 放一起app.websocket(/ws) async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: while True: data await websocket.receive_text() if data ping: await websocket.send_text(pong) except WebSocketDisconnect: manager.disconnect(websocket)Trace 查询接口我会在第 4 章详细说这里先按下不表。3.4 启动服务与热更新启动命令是uvicorn main:app --host 0.0.0.0 --port 8000 --reload--reload开启热更新修改代码后自动重启服务。如果你在 PyCharm 里用运行按钮启动注意在 Configuration 的 Parameters 里写上--reload否则改代码不会生效。这个坑我在第 7 章细讲。4. Langfuse 接入回调不是“配置一下”就完事4.1 Langfuse v4 自部署的环境变量Langfuse v4 和之前的 v3 在自部署上差别很大。v3 依赖 ClickHouse资源要求高v4 改成了 PostgreSQL S3 对象存储用 Docker Compose 一套三个容器就能拉起来对中小团队来说部署成本低了很多。我用的是 MinIO 作为 S3 兼容存储。关键环境变量如下变量说明DATABASE_URLPostgreSQL 连接串S3_ACCESS_KEY_ID对象存储 AccessKeyS3_SECRET_ACCESS_KEY对象存储 SecretKeyS3_BUCKET_NAMETrace 数据存储桶S3_ENDPOINTMinIO 或阿里云 OSS 的 endpointNEXTAUTH_URL登录跳转地址填 http://localhost:3000NEXTAUTH_SECRET用于 Session 加密的随机串ENCRYPTION_KEY用于 SDK 传输数据加密启动后在管理后台创建一个项目拿到 Public Key 和 Secret Key这两个 Key 是后端 SDK 初始化用的。4.2 把 CallbackHandler 挂到 Langchain 链上Langfuse 官方提供了两个对象容易搞混。Langfuse类负责主动查询 TraceCallbackHandler负责在 Langchain 调用链路上做埋点。两个对象用同一组 Key 初始化但用途不同。# langfuse_client.py import os from langfuse import Langfuse from langfuse.callbacks import CallbackHandler LANGFUSE_HOST os.getenv(LANGFUSE_HOST, http://localhost:3000) LANGFUSE_PUBLIC_KEY os.getenv(LANGFUSE_PUBLIC_KEY) LANGFUSE_SECRET_KEY os.getenv(LANGFUSE_SECRET_KEY) langfuse Langfuse( public_keyLANGFUSE_PUBLIC_KEY, secret_keyLANGFUSE_SECRET_KEY, hostLANGFUSE_HOST, ) langfuse_handler CallbackHandler( public_keyLANGFUSE_PUBLIC_KEY, secret_keyLANGFUSE_SECRET_KEY, hostLANGFUSE_HOST, )重点来了。我在 Langchain 链上挂回调时没有把 handler 放进ChatOpenAI的参数里而是选择在每次ainvoke的config里传递。原因是同一个 LLM 实例可能被多条链复用把回调挂在链调用级别可以控制“哪一次调用需要被追踪”粒度更灵活。await chain.ainvoke( {question: req.question}, config{callbacks: [langfuse_handler]} )第一次跑通之后去 Langfuse 后台刷新能看到每次对话生成了一条 Trace里面自动拆出了 LLM 调用的 Span输入输出、token 用量、延迟都记录得很清晰。Langchain 的 LCEL 链每执行一个组件CallbackHandler 就会生成对应的 Span所以你能看到“提示词模板执行”和“ChatOpenAI 调用”两个节点。4.3 Trace 详情的拉取与索引延迟Langfuse 的写入是异步的刚写入的 Trace 不会立刻能被查询到经常要等个一两秒。仪表盘点击某条对话想看 Trace 详情时直接用fetch_trace会偶发查不到。我最终的做法是加一个重试机制最多试 5 次间隔 1 秒import asyncio from fastapi import HTTPException app.get(/api/trace/{trace_id}) async def get_trace(trace_id: str): for _ in range(5): try: trace langfuse.fetch_trace(trace_id) if trace is not None: return trace.data except Exception: pass await asyncio.sleep(1) raise HTTPException(status_code404, detailtrace not found)这里有一个小细节fetch_trace返回的 Trace 对象结构很深前端要的是可读性更好的树形结构。我会在后端做一层裁剪提取每个节点的名称、类型、输入、输出、token 用量和耗时再抛给前端。这样前端不需要理解 Langfuse 的数据模型。4.4 除了 LLM 调用还能记录什么Langchain 的回调会自动记录 LLM 调用但一次完整的客服对话往往还有别的环节比如检索、日志、人工标记。Langfuse 的Langfuse对象也支持手动创建 Generation 或 Span用来补充记录非 Langchain 环节。generation langfuse.generation( namemanual-search, input{query: 退货政策}, output{hits: 3}, metadata{source: knowledge_base}, modelbm25, ) generation.end()这样就做到了一次会话全链路都能追踪。对排查问题来说能看到“模型回答前有没有正确命中知识库”往往比看模型输出本身更重要。但也要提醒一句不要为了埋点而埋点。监控的核心目标是快速定位异常埋点字段过多反而会让 Trace 可读性变差。我一般只在关键节点记录输入、输出、耗时和错误信息业务元数据放进metadata按需查看。5. WebSocket 实时通道连接管理、心跳和反代缺一不可5.1 ConnectionManager 的写法FastAPI 的原生 WebSocket 接口很简单但你要自己管理连接集合。我写了一个简单的 ConnectionManager支持连接、断开、广播三个操作。# connection_manager.py from fastapi import WebSocket class 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): if websocket in self.active_connections: self.active_connections.remove(websocket) async def broadcast(self, message: dict): dead [] for conn in self.active_connections: try: await conn.send_json(message) except Exception: dead.append(conn) for conn in dead: self.disconnect(conn) manager ConnectionManager()广播时给每个连接套一层 try/except是为了防止某个客户端异常断开导致广播中断。如果不处理一个连接断开会让send_json抛RuntimeError后面所有客户端都收不到消息了。实际生产环境里前端断网、用户直接关闭标签页这种场景非常频繁这个保护是必备的。主路由里用await manager.broadcast(event)就能把监控事件推给所有在线仪表盘。聊天接口和 WebSocket 通道都指向同一个manager单例对象模块之间循环导入的问题要留意我建议把manager实例放在单独文件里。5.2 心跳为什么由客户端发起WebSocket 连接如果长时间没有数据交互中间的网络设备尤其是云厂商的负载均衡器、NAT 网关会在超时后静默断开连接。对仪表盘来说用户打开页面挂一个上午不刷新是常态心跳保活必须有。我的做法是前端每 30 秒发送一次ping后端收到后回复pong。后端代码里 while 循环已经实现了这个逻辑任何非ping的文本消息也可以忽略——或者你后续扩展成前端给后端发指令比如“订阅某个 session 的监控事件”。有一个经验之谈保活尽量让客户端主动发不要依赖服务端定时推。经过 Nginx 和云负载均衡时客户端主动发包能有效“刷新”连接状态服务端主动推很容易被误判为闲置流量。5.3 Nginx 反代和超时FastAPI 服务通常不直接暴露公网前面挂一层 Nginx。WebSocket 和普通 HTTP 在反代配置上有个关键区别必须显式设置 Upgrade 和 Connection 两个头否则浏览器端口上的连接会被当普通 HTTP 处理WebSocket 握手直接失败。location /ws { proxy_pass http://127.0.0.1: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; }proxy_read_timeout我一开始没配用的默认 60 秒结果前端控制台定期报 1006 异常断开排查了很久才发现是 Nginx 服务端掐断的。设置为 3600 秒后长连接一整天不掉线。5.4 前端断线重连网络不可能永远稳定。前端必须做自动重连否则仪表盘开着一个下午中途断网一次恢复后数据就再也不刷新了监控等于废了。我用指数退避策略重连避免服务端重启时前端疯狂发请求打满连接数let retryCount 0; const maxRetry 10; function connectMonitorSocket() { const protocol location.protocol https: ? wss : ws; const ws new WebSocket(${protocol}://${location.host}/ws); ws.onopen () { retryCount 0; heartbeatTimer setInterval(() { if (ws.readyState WebSocket.OPEN) ws.send(ping); }, 30000); }; ws.onmessage (event) { if (event.data pong) return; const msg JSON.parse(event.data); handleMonitorEvent(msg); }; ws.onclose () { clearInterval(heartbeatTimer); if (retryCount maxRetry) { const delay Math.min(1000 * 2 ** retryCount, 30000); retryCount; setTimeout(connectMonitorSocket, delay); } }; ws.onerror () ws.close(); }6. 仪表盘长什么样Vue3 实时展示 Token、延迟与对话轨迹6.1 页面结构前端用的 Vite Vue3 TypeScript没有引入重型 UI 库组件全部自己写包体积小、加载快。页面分成三个区域顶部一排指标卡今天对话总数、总 Token 数、平均延迟、估算成本左侧对话列表每次对话的摘要包括问题、模型、Token 用量、耗时主区域对话详情点击列表项后展示 Langfuse Trace 的完整调用链实时数据流是这样/api/chat返回后FastAPI 广播chat.completed事件前端收到后做三件事——更新指标卡数字、把新对话插入列表头部、如果当前处于“只看未读”模式就提示用户。6.2 WebSocket 客户端封装我在websocket.ts里封装了一套带重连、心跳、事件分发的客户端第 5 章的代码可以塞进这个模块。收到不同类型的事件后通过回调分发到对应的状态更新函数。function handleMonitorEvent(msg: any) { if (msg.type chat.completed) { chatList.value.unshift({ sessionId: msg.session_id, question: msg.question, answer: msg.answer, model: msg.model, totalTokens: msg.total_tokens, latencyMs: msg.latency_ms, timestamp: msg.timestamp, }); } }有一个细节要注意如果用户正在查看某条会话的详情新事件到来时不要强制把视图切走只更新数字和列表就够了。抢焦点的设计会让人很烦躁。6.3 指标卡与成本估算指标卡的数据不必重新请求接口直接基于 WebSocket 推送的事件计算即可。前端的全局状态用一个reactive对象维护。const metrics reactive({ totalCalls: 0, totalTokens: 0, totalPromptTokens: 0, totalCompletionTokens: 0, avgLatencyMs: 0, estimatedCost: 0, }); function updateMetrics(msg: any) { metrics.totalCalls; metrics.totalTokens msg.total_tokens; metrics.totalPromptTokens msg.prompt_tokens; metrics.totalCompletionTokens msg.completion_tokens; metrics.avgLatencyMs Math.round( (metrics.avgLatencyMs * (metrics.totalCalls - 1) msg.latency_ms) / metrics.totalCalls ); }成本估算要按模型的定价规则算。DeepSeek deepseek-chat 的计费特点可以总结成两行公式const cost (promptTokens / 1000000) * inputPricePerM (completionTokens / 1000000) * outputPricePerM;每次后端推送事件时算一次前端实时累加。这样运营同学可以看到“今天又烧了多少钱”比起月底看账单监控起来要直观太多。6.4 对话详情把 Langfuse 的 Trace 拉过来对话列表里的每一项点击后向后端请求/api/trace/{session_id}。等等这里有个坑要说明Langfuse 的 Trace ID 和我的业务 session_id 不是同一个东西。Langfuse 会在每次ainvoke时自动生成一个新的 Trace ID但我要在前端列表里按“一次会话”聚合所以需要让业务 session_id 出现在 Trace 的元数据里。有两种办法一是调用ainvoke时传入一个自定义run_id二是结合 LangChain 的run_name和 Langfuse 的session_id参数。我用的是第二种。在版本较新的 Langfuse SDK 中可以直接在回调里传session_id这样 Langfuse 后台的 Session 视图会自动把同一 session 的多轮对话聚合在一起。初始化CallbackHandler时加一个参数langfuse_handler CallbackHandler( public_keyLANGFUSE_PUBLIC_KEY, secret_keyLANGFUSE_SECRET_KEY, hostLANGFUSE_HOST, session_idsession_id, )不过前端列表想拿 Trace 详情时还是得用 Trace ID。所以我让 Langchain 的ainvoke返回值带上本次的 Trace ID。改造一下chat接口from langfuse.callbacks import get_trace_id result await chain.ainvoke(...) trace_id get_trace_id()在同一个上下文中调用get_trace_id()能拿到 Langfuse 刚分配的 Trace ID存到本地列表里前端点详情时用这个 ID 请求 Langfuse 的 Trace 树。7. 实测排查热更新失效、1006断线、流式中断的全链路复盘7.1 uvicorn --reload 不生效现象在 PyCharm 里直接点运行按钮启动命令是uvicorn main:app --reload但改代码后服务不自动重启非要手动停掉再起。排查后发现是 PyCharm 运行配置的问题。当你在 Run Configuration 里指定了“Script path”为main.py并且内容只有uvicorn.run(app, ...)时--reload参数不会自动带上。对话框中也没有可视化的 reload 开关。解决办法有三个一是运行配置里用“Module name”为uvicornParameters 写main:app --reload二是在代码里直接写uvicorn.run(main:app, reloadTrue)注意这里的第一个参数要传字符串不能传app对象三是我现在最常用的——不用 PyCharm 的运行按钮直接用终端启动简单粗暴不出错。7.2 WebSocket 1006 断线1006 是 WebSocket 协议里的异常断开码表示连接没有经过正常的关闭握手就断了。前端控制台最常见的就是这一段[websocket] onclose, code: 1006, reason: , reconnect: true我把这个坑完整复盘一下因为它牵扯到三个不同的原因。第一次遇到是在本地联调前端页面放着不动几分钟后 1006。排查到是 Nginx 代理超时proxy_read_timeout默认 60 秒空闲连接被掐断。加上 3600 秒配置后解决。第二次是后端服务重启导致的。本地调试时改了代码--reload触发服务重启进程断掉所有 WebSocket 连接全部异常关闭。这个没法完全避免只能在前端做自动重连保证服务恢复后仪表盘自动恢复。第三次是云服务器负载均衡导致的。云厂商的 SLB 默认空闲超时时间也比较短需要去控制台调长 WebSocket 会话保活时间后端心跳包 30 秒间隔基本能覆盖。7.3 stream disconnected before completion做流式输出时控制台冒出来一个错误stream disconnected before completion: failed to send websocket request: io一开始我以为是后端 WebSocket 坏了后来定位到是 DeepSeek 流式接口的正常现象用户在前端还没等模型回答完就关闭了页面或者切走了连接客户端侧的 WebSocket 断开服务端那边的流式读取还在继续底层就抛出 io 异常。这个问题有两个处理方向。一是健康检查时忽略这一类异常它是业务层面的“用户取消”不是系统故障二是在前端组件销毁时主动调用ws.close()并通知后端取消当前 LLM 调用避免资源白白占用。后端的处理方式是在chat接口的协程里包一层 try/except捕获流式中断后记录一条日志并清理引用不要把整个 FastAPI 进程带崩。7.4 H5能连打包App连不上我们把仪表盘做成了移动端应用发现一个诡异的问题网页端 H5 连 WebSocket 一切正常打包成 App 安装到 Android 手机上就永远处于重连状态。打开日志发现是明文流量限制。从 Android 9 开始系统默认禁止应用使用未加密的明文网络流量。网页端用ws://可以连App 端直接拦截连接建立不了。解决办法是把 WebSocket 地址从ws://换成wss://并给域名配好 HTTPS 证书。如果你只是在本地联调临时可以在 AndroidManifest 里加android:usesCleartextTraffictrue但正式包千万别这么干安全审计过不去。7.5 PyCharm 装 FastAPI 失败报错有读者问过我 PyCharm 里安装 FastAPI 一直报错。大部分情况是 pip 源的问题国外的默认源在国内网络环境下经常超时。换成清华源pip install fastapi -i https://pypi.tuna.tsinghua.edu.cn/simple还有一种是 Python 解释器版本太旧。FastAPI 新版要求 Python 3.8 以上有些新语法需要 3.10 才能跑。如果你用的是 PyCharm 自带的旧解释器建议新建虚拟环境时选 3.10。8. 扩展与体会上下文占用预警、Agent 监控和成本治理8.1 上下文窗口占用预警DeepSeek 对话模型有上下文窗口上限长时间对话很容易触顶。触顶之后模型要么报错要么开始“失忆”。我在仪表盘里加了一个“上下文占用条”每一轮对话用prompt_tokens累计估算当前会话的占用比例。后端把 total_tokens 推给前端后前端按会话维度累加if (!contextUsage[sessionId]) contextUsage[sessionId] 0; contextUsage[sessionId] msg.total_tokens;占用超过模型窗口的 80% 时列表里出现黄色提示提醒运营引导用户开启新会话。这个功能上线后直接减少了“DeepSeek 达到对话长度上限请开启新对话”这类用户投诉。8.2 从 LLM 链路扩展到 Agent 追踪如果你后续要上 AgentLangfuse 同样能接。LangGraph 的执行节点建议画成一个单独的 Span每个工具调用也是一个 Span。这样监控面板可以看到 Agent 的完整思考路径调用了哪个工具、返回了什么、下一步决策是什么。这里要注意工具调用的输入输出可能包含敏感数据传到 Langfuse 之前最好做脱敏。我的做法是在 callback 里加一个metadata过滤器把手机号、身份证之类的字段打码后再上报。8.3 成本估算实时化DeepSeek 的价格是动态调整的硬编码进前端不可取。我把单价配置放在后端环境变量里前端通过一个/api/config接口去拉{ deepseek-chat: { input_price_per_m: 2.0, output_price_per_m: 8.0 } }价格单位用人民币“元 / 百万 tokens”。每次价格变动时后端只改环境变量就能生效前端不用重新构建发布。成本监控的最大价值不是“算得准”而是建立“每次对话都在花钱”的意识倒逼开发团队优化 prompt 长度、控制上下文累加。8.4 一点个人体会踩完这些坑之后我的体会是大模型应用的可观测性核心不是“加日志”而是把一次对话变成可以被回溯、被分析、被量化的事件流。Langfuse 解决了记录的问题WebSocket 解决了实时性的问题FastAPI 把两者粘起来Vue3 让数据变得能看懂。这套架构不大但每一个环节都有它存在的理由。最后分享一个使用习惯所有监控数据都该设置保留期。Langfuse 的 Trace 默认全量保留时间长了存储成本会涨。我用定时任务把超过 30 天的 Trace 异步清理只保留指标聚合结果。监控系统本身也需要运维控制成本要从第一天开始想。
返回列表