LLM可观测性实战:从日志到调用链追踪的完整方案
在 LLM 应用进入生产环境后最让人头疼的往往不是模型效果本身而是“调用过程不透明”。很多团队仍旧靠print()和日志文件来定位问题一旦并发上来或者提示词被持续迭代就很难判断某次回答是模型问题、参数问题、上下文问题还是上游调用链出了问题。这类需求现在被统称为 LLM 可观测性也有人叫它 LLM 可见性、LLM 链路追踪。本文就从这个标题里的问题出发完整梳理 LLM 可见性的概念、常见工具、落地思路并用开源工具 OpenAI SDK 的完整示例演示如何把一次 LLM 调用变成可查询、可分析、可告警的链路数据。1. LLM 可见性到底要解决什么问题1.1 传统日志为什么不够用在传统后端服务里一个请求进来从网关到业务代码再到数据库链路是相对可控的。你可以在 Nginx 日志里看到状态码在业务日志里看到异常堆栈在数据库慢日志里看到 SQL 执行时间。这些日志拼在一起基本能还原一次请求的全貌。到了 LLM 应用里情况会变得复杂很多。一次完整的用户请求往往不是简单地调用一次大模型就返回而是先做检索增强把检索结果拼到提示词里再调用大模型最后还可能把模型输出经过一层后处理再返回给前端。哪怕只调一次大模型你也需要知道提示词到底被拼成了什么样模型返回的原始内容是什么消耗了多少 token首字延迟和总延迟各是多少是否经过了重试如果模型报错错误信息是否能和当时的请求参数关联上。这些信息如果只靠传统业务日志很难完整记录。更麻烦的是部分团队直接把 API Key 写在代码里调用失败时大模型服务商只会在报错信息里给出一个 request id而本地日志里找不到这个 id也没法把一次失败的请求和业务订单号关联起来。这种“黑盒”状态就是 LLM 可见性要解决的核心痛点。1.2 可见性应该覆盖哪些数据所谓提升 LLM 可见性不是简单地多打几行日志而是要给每次 LLM 调用建立一套完整的观测数据。至少要包含以下内容数据维度说明典型问题请求原文messages、system prompt、工具调用参数排查提示词被错误拼接、工具参数为空响应原文模型输出、stop reason、工具调用结果判断模型为何答非所问Token 消耗prompt_tokens、completion_tokens成本突增、上下文过长模型参数temperature、max_tokens、response_format参数被串改导致输出异常延迟指标首 token 时间、总耗时、重试次数接口变慢、上游超时链路关联业务订单号、会话 id、trace id、request id无法从前端问题定位到具体调用异常信息HTTP 状态码、原始 provider error模型厂商拒绝请求的原因不明确这 7 类数据聚到一起才能回答“一次 LLM 调用发生了什么”。在继续看工具之前可以先明确一个结论:LLM 可见性不是某一个固定系统能完全解决的它需要工具 代码规范 组织流程一起配合。2. 提升 LLM 可见性的工具选型2.1 专用 LLM 可观测性平台目前市场上已经有不少专门面向 LLM 的可观测性平台最常见的有 Langfuse、LangSmith、Helicone、WB Weave、Comet Opik 等。它们的设计目标基本一致把一次完整的 LLM 应用执行过程拆成 trace、span、observation 等结构化数据并提供可视化界面查询。工具开源情况核心优势适合场景Langfuse开源可自托管支持 trace、评测、数据集、成本计算、角色权限中小团队、对数据隐私要求高LangSmith托管为主与 LangChain 生态集成深度高主力技术栈是 LangChainHelicone开源 托管网关方式接入替换 base_url 即可不希望改业务代码WB Weave开源/托管实验跟踪与模型评估能力强已有 Weights Biases 使用习惯Comet Opik开源/托管提供测评、提示词管理和 trace需要把评测与监控放一起在选择时最重要的判断标准不是功能数量而是数据放在哪里。如果公司对数据出境有严格要求那应该优先考虑可以自托管的开源工具如果团队已经重度使用某个生态就选那个生态内的方案。不要为了“全家桶”布置一堆平台最终却没有人去维护和查看。2.2 轻量替代方案如果团队暂时没有条件部署专门平台也可以通过中间件或自行封装 SDK 的方式提升可见性。比如在 FastAPI 里写一个依赖注入的通用日志函数通过 OpenTelemetry 把 LLM 调用作为 Span 上报到现有监控系统在统一网关层拦截 LLM 调用记录请求和响应摘要。这种方式的问题是开发量较大且无法开箱即用地获得评分、数据集、提示词版本对比等功能。因此如果你只是刚开始尝试建议先用开源专用平台验证是否能解决团队痛点再决定是否需要自研。3. 核心原理一次 LLM 调用应该被记录成什么3.1 Trace、Span 与 Observation大多数 LLM 可观测性平台都借用了分布式链路追踪里的概念。一个用户请求对应一个 TraceTrace 下面可以有多个 Span。比如一个“智能问答”请求可以拆成Span 1检索知识库Span 2拼接 PromptSpan 3调用大模型Span 4解析模型输出。其中调用大模型这个 Span在 Langfuse 里又被称为 Generation Observation。平台会为每个 Span 记录开始时间、结束时间、输入、输出、元数据、错误信息等。这样在界面里展开一个 Trace就能按时间顺序看到每一步的耗时和输入输出。我们需要理解这个概念不是因为“专业”而是因为排查问题时只有把调用过程拆成结构化的 Span才能快速定位慢在哪一步、错在哪一步。如果只是把一个大字符串塞进日志那么即使有数据也不是真正的可见性。3.2 一个理想的 Trace 数据结构下面这个 JSON 示例展示了理想情况下一次 LLM 调用应该什么样。这里用了自定义简化结构方便理解真实平台存储结构会更复杂。{ trace_id: a1b2c3d4e5f6, name: rag_chat_answer, timestamp: 2025-01-01T10:00:0008:00, input: { question: 什么是大模型上下文窗口 }, output: 上下文窗口是模型一次能处理的 token 数量上限。, spans: [ { name: retrieve_knowledge, type: retrieval, input: { question: 什么是大模型上下文窗口 }, output: { chunk_ids: [chunk_001, chunk_002] }, duration_ms: 120 }, { name: llm_chat, type: generation, model: gpt-4o-mini, input: { model: gpt-4o-mini, messages: [ { role: user, content: 请根据资料回答问题…… } ] }, output: { message: 上下文窗口是模型一次能处理的 token 数量上限。, usage: { prompt_tokens: 1520, completion_tokens: 130, total_tokens: 1650 } }, metadata: { temperature: 0.3, max_tokens: 1024 } } ] }只要数据落到这个粒度前端反馈“回答很慢”你就可以从 Trace 里看到慢在检索还是慢在模型调用前端反馈“某些问题答得不对”你就能翻出当时的 Prompt 和模型原始输出。这就是 LLM 可见性的价值。4. 实战用 Langfuse 接入 OpenAI 调用下面这部分是全文的核心实践。我选择 Langfuse 作为示例工具因为它开源、可自托管、不绑定特定框架也适合做二次开发。示例代码基于 Python并接入 OpenAI SDK。如果你用的是 Azure OpenAI、Ollama、vLLM 等兼容接口原理是一样的。4.1 准备环境与项目结构建议先创建一个干净的虚拟环境并安装依赖。mkdir llm-visibility-demo cd llm-visibility-demo python -m venv .venv source .venv/bin/activatepip install langfuse openai python-dotenv项目结构可以保持简单llm-visibility-demo/ ├── .env ├── requirements.txt └── main.py在.env中配置 Langfuse 和 OpenAI 的密钥。如果你是本地 Docker 方式启动 Langfusehost 通常是http://localhost:3000。LANGFUSE_PUBLIC_KEYpk-lf-demo LANGFUSE_SECRET_KEYsk-lf-demo LANGFUSE_HOSThttp://localhost:3000 OPENAI_API_KEYsk-your-openai-key关于版本我建议不要追求最新版本而是使用当前项目依赖已验证的版本。Langfuse 和 OpenAI SDK 更新都比较频繁接口如果发生变化请以你实际安装版本的官方文档为准。4.2 初始化 Langfuse 客户端在主程序中先初始化 Langfuse 客户端并通过auth_check()验证密钥是否有效。# main.py import os from dotenv import load_dotenv from langfuse import Langfuse load_dotenv() langfuse Langfuse( public_keyos.getenv(LANGFUSE_PUBLIC_KEY), secret_keyos.getenv(LANGFUSE_SECRET_KEY), hostos.getenv(LANGFUSE_HOST, http://localhost:3000), ) try: langfuse.auth_check() print(Langfuse 连接成功) except Exception as e: print(Langfuse 连接失败, e)这里的auth_check()会向 Langfuse 服务端发一个请求验证公钥和私钥是否匹配也能顺便检查网络连通性。4.3 手动追踪一次 Chat Completions 调用手动追踪是最灵活的方式也最能帮助理解平台的底层模型。核心思路是先创建 Trace再在 Trace 下创建 Generation请求成功或失败时调用对应方法结束并记录输出或错误。# main.py import os from openai import OpenAI from langfuse import Langfuse from dotenv import load_dotenv load_dotenv() langfuse Langfuse( public_keyos.getenv(LANGFUSE_PUBLIC_KEY), secret_keyos.getenv(LANGFUSE_SECRET_KEY), hostos.getenv(LANGFUSE_HOST, http://localhost:3000), ) client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) def ask_llm(question: str): trace langfuse.trace( nameopenai_chat_demo, input{question: question}, session_idsession-001, user_iduser-001, metadata{env: test}, ) messages [ {role: system, content: 你是一个乐于助人的助手。}, {role: user, content: question}, ] generation trace.generation( namechat_completion, modelgpt-4o-mini, model_parameters{temperature: 0.7}, input{messages: messages}, ) try: response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0.7, ) content response.choices[0].message.content generation.end( outputcontent, usage{ input: response.usage.prompt_tokens, output: response.usage.completion_tokens, total: response.usage.total_tokens, }, metadata{finish_reason: response.choices[0].finish_reason}, ) trace.update(outputcontent) return content except Exception as e: generation.end( levelERROR, status_messagestr(e), ) trace.update(levelERROR, status_messagestr(e)) raise if __name__ __main__: result ask_llm(用一句话介绍大模型可观测性。) print(result)这段代码有几个关键点值得展开首先trace.generation()只是创建了一条空的 Generation 记录输入信息在这一步已经写入。真正完成记录是在generation.end()时此时会把输出、token 消耗、错误状态写进去。如果业务代码在调用模型时抛出异常除了把异常继续抛出还要在except中进行generation.end(levelERROR)否则这条 Generation 会一直处于未完成状态缺失错误信息。其次usage参数里的字段Langfuse 支持input和output分别表示 prompt token 数和 completion token 数。虽然不同版本字段命名可能有差异但核心意图是让平台按 token 计算成本。最后代码最后调用了langfuse.flush()吗示例里没有显式写因为 Langfuse 默认是异步上报。不过对于简单脚本建议在脚本末尾显式调用langfuse.flush()否则可能出现程序退出后数据还没上报完的情况。在长时间运行的服务中不需要每次请求都调用 flushLangfuse 会按批处理发送。4.4 使用装饰器自动追踪如果不想手写很多 Trace 和 Generation 代码Langfuse 还提供了装饰器模式。下面这个示例展示了最简用法from langfuse.decorators import observe, langfuse_context observe() def ask_llm(question: str) - str: client OpenAI() response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: question}], ) content response.choices[0].message.content langfuse_context.update_current_trace( input{question: question}, outputcontent, session_idtrace-demo-session, ) return contentobserve()会让 Langfuse 自动把函数调用记录为一个 Span。如果你的函数内部还调用了 OpenAI SDKSDK 调用会作为子 Span 被追踪。装饰器模式的好处是侵入性低适合快速覆盖已有代码。但装饰器模式也有代价它对异常处理、重试、并发等场景的精细控制不如手动方式。生产项目中我会建议在工具封装层用手动追踪在业务快速验证时用装饰器。4.5 在 trace 中查看成本与错误信息Langfuse 默认支持成本计算但前提是你正确传入了模型名称和 usage。部分模型名称不在内置价格表里可以在 Langfuse 项目设置里配置自定义模型和单价。这样每次调用后平台就能自动估算成本。排错时可以在界面里打开某条 trace点击对应的 Generation Span查看原始请求和响应。Langfuse 提供了丰富筛选条件比如按模型名、按用户 id、按 session、按时间范围。查找问题时优先先去页面把 trace 打开看状态是否 ERROR再点击对应 Span 看原始报错。5. 在 LangChain 和 Dify 生态中提升可见性大多数团队不会只用原生 OpenAI SDK而是会依赖 LangChain、Dify 这类上层框架。这里就涉及到更复杂的调用链你无法保证 Agent 内部会按什么顺序调用模型、检索器和工具。如果缺少全局可见性一个多步 Agent 的“思考过程”几乎不可排查。5.1 LangChain 接入 LangfuseLangChain 对 Langfuse 有官方回调集成。用法很简单创建一个CallbackHandler然后通过config传入即可。from langfuse.callback import CallbackHandler from langchain_openai import ChatOpenAI langfuse_handler CallbackHandler() llm ChatOpenAI( modelgpt-4o-mini, temperature0.7, ) response llm.invoke( 请解释 RAG 的工作原理, config{callbacks: [langfuse_handler]}, )在这个模式下LangChain 内部发起的多次 LLM 调用、工具调用、检索操作都会被自动包装成 Span整体形成一个 Trace。对使用者来说几乎不用改业务代码就能在 Langfuse 界面里看到一次 Agent 执行的完整时间线。需要注意的是不同 LangChain 版本的 callback 机制变化较大。如果你在老版本上升级 LangChain 后发现 callback 不生效优先检查 langfuse 回调类是否与当前 LangChain 版本兼容。5.2 Dify 中的 LLM 可见性配置Dify 是很多团队搭建 LLM 应用时用的低代码平台。Dify 也提供了可观测性配置入口通常位于“设置”里的可观测性或监控相关页面。你可以在其中选择 Langfuse、LangSmith 或 Opik然后填入对应平台的公钥、私钥和 API 地址。配置完成后Dify 里创建的 Workflow 或 Chatflow 在运行时会自动把多次 LLM 调用、知识库检索、工具节点调用上报到外部可观测性平台。这样即使你没有去改 Dify 内部代码也能在 Langfuse 页面里看到完整执行过程。如果你用的 Dify 版本与这里描述不完全一致以你实际版本页面为准。Dify 的功能迭代很快尽量不要凭旧版本经验去点击不存在的入口。5.3 自己封装一个轻量版追踪器如果暂时不想引入第三方平台也可以通过一个简单的装饰器把每次请求记录到本地结构化日志。核心思路仍然是把输入、输出、耗时、错误、token 情况串起来。下面这段代码适用于“先最小成本提升可见性”的场景。import json import logging import time from functools import wraps logger logging.getLogger(llm_visibility) logger.setLevel(logging.INFO) def trace_llm_call(func): wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() trace_id str(int(time.time() * 1000)) payload { trace_id: trace_id, function: func.__name__, args_preview: str(args)[:500], kwargs_preview: str(kwargs)[:500], } try: result func(*args, **kwargs) payload[result_preview] str(result)[:1000] payload[duration_ms] round((time.perf_counter() - start) * 1000, 2) payload[level] INFO return result except Exception as e: payload[error] str(e) payload[duration_ms] round((time.perf_counter() - start) * 1000, 2) payload[level] ERROR logger.error(json.dumps(payload, ensure_asciiFalse)) raise finally: if level in payload and payload[level] INFO: logger.info(json.dumps(payload, ensure_asciiFalse)) return wrapper这种轻量方案虽然没有前端可视化界面但好处是数据格式可控、查询方便。如果以后迁移到 Langfuse 等平台只需要把这里的结构化日志转换成 trace 数据结构即可。6. 常见问题缺了可见性的三大典型故障6.1 请求失败provider rejected the request schema or tool payload这个报错现在非常常见。它一般发生在你给模型传入了自定义response_format或设置了tools参数时模型供应商认为你传递的 JSON Schema 非法或 tool payload 不符合规范。常见原因有JSON Schema 里带了$schema、title、format等被严格校验拒绝的字段使用了 OpenAI 的 strict function calling却没有给 parameters 设置additionalProperties: false某些 OpenAI 兼容服务比如本地部署的 vLLM不支持部分参数直接拒绝请求tool payload 里包含了None值或者不符合参数类型的字段。调试时要先看原始 provider error再对照完整请求参数。如果缺少可见性你可能根本不知道传给 provider 的完整 payload 是什么。使用 Langfuse 后可以在 Trace 的 Generation Span 里直接看到 input 字段中记录的完整 messages、tools 和 response_format定位会迅速很多。一个比较稳妥的 JSON Schema 写法如下response_format { type: json_schema, json_schema: { name: weather_response, strict: True, schema: { type: object, properties: { city: {type: string}, temperature: {type: number}, }, required: [city, temperature], additionalProperties: False, }, }, }这段代码在使用 OpenAI 新版本 SDK 时是可用的。如果你用的模型或 SDK 版本较老也许不支持type: json_schema就需要退回 JSON Mode 或者普通函数调用。一定要根据你的实际模型和 SDK 调整。6.2 请求超时LLM request timed out“LLM request timed out. The model did not produce a response before the timeout”也是高频问题。出现这个错误时先别急着调大超时时间应该先问三个问题是偶发还是持续是首个 token 慢还是完整返回慢是否所有模型请求都超时排查时在 Trace 里找到该次 Generation查看持续时间和 error 信息。如果持续时间非常接近你的超时阈值那么大概率是上游模型响应慢如果请求在几十毫秒内就报错那可能是网络连接被重置或鉴权失败。调整 OpenAI SDK 超时时间的常见写法如下client OpenAI( api_keyos.getenv(OPENAI_API_KEY), timeout60.0, max_retries2, )但超时设置不是越大越好。生产环境建议结合你的业务场景设置合理阈值并为不同调用设置不同优先级。例如普通问答 30 秒长文本生成可以放宽到 120 秒同时配合流式输出提升用户体验。6.3 Trace 数据缺失或不完整有的团队接入 Langfuse 后发现页面里只能看到部分 Trace或者同一 Trace 里缺失了部分 Span。常见原因有以下几种错误现象常见原因解决思路Trace 创建成功但 Generation 缺失程序异常退出未调用 generation.end()用 try/except/finally 确保 end 被调用调用结束后数据没出现在界面没有 flush进程直接退出脚本执行完调用 flush()无法和业务订单关联Trace 里没有业务 ID在 trace 的 metadata 中写入订单号多个用户请求混在一起没有设置 session_id 或 user_id创建 trace 时显式传入 session_id7. 最佳实践与工程落地建议7.1 统一请求 ID 规范LLM 应用里涉及多层 ID前端请求 ID、后端 Trace ID、模型服务商 Request ID。只有把这些 ID 串起来才能实现高效的排查链路。建议在每个需要调用模型的服务中都生成一个全局唯一的请求 ID并把它写入 trace metadata、业务日志和返回给前端的响应头里。如果你的服务通过 HTTP 暴露可以这样把请求 ID 透传下去headers { X-Request-Id: request_id, X-Trace-Id: trace_id, } response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, extra_headersheaders, )OpenAI SDK 支持通过extra_headers传入自定义请求头。这样当模型服务商返回错误时你可以把服务商侧 request id 和本地 trace id、业务订单号关联起来。7.2 日志要区分敏感数据和可展示数据LLM 请求里可能包含用户隐私信息、企业内部文档、甚至密钥片段。直接把这些内容无脑写入 Trace 是非常危险的做法。建议做两层处理第一层脱敏。在进入可观测性平台前把手机号、邮箱、身份证等信息替换为掩码或哈希值第二层权限。通过平台的角色权限限制普通开发只能查看 unstripped 之外的数据只有 trace 管理员能看到完整原始输入输出。如果团队对数据合规要求很高也可以选择自托管 Langfuse把数据完全留在内网并在网关层配置访问白名单。7.3 不要全量采样要有策略不是每一次调用都值得记录下来。在生产环境你可以按业务重要性设置采样策略失败请求100% 记录新功能灰度请求100% 记录普通用户请求按 10% 到 30% 采样高延迟或异常请求自动提高采样率。采样可以显著降低平台存储成本同时不影响核心问题定位。很多可观测性平台也支持服务端采样率配置建议优先使用平台原生能力。7.4 从“追踪”走向“评估与告警”把 Trace 接好只是第一步。更进一步可以基于 Trace 数据做三个动作对每次模型输出做离线或在线评估判断回答是否与参考答案一致、是否有幻觉内容把 token 消耗、错误率、平均延迟作为监控指标接入现有告警系统对高频失败的工具调用、特定错误 schema 进行归因分析推动模型配置或代码修复。这样LLM 可观测性就不再只是排错工具而变成持续优化模型效果的基础设施。8. 如果你还没开始建议从哪里入手回到标题里的问题。如果你确实正在考虑某个工具来提升 LLM 可见性我建议按照下面顺序做快速验证先不要直接铺到全部业务。选一个低频非核心接口接入 Langfuse 或 LangSmith记录原始请求、响应、token、耗时、错误。跑两天后你会发现原本“只看日志完全摸不到头脑”的问题突然变得有迹可循。然后再把链路 ID 规范、脱敏策略、采样策略逐步补齐。等这套机制稳定了再推广到 Agent、Dify 工作流、生产主链路。LLM 应用的开发节奏很快模型厂商和框架几乎每个版本都在变所以可观测性方案不要试图一步到位。核心是把“每次调用都有据可查”变成团队默认习惯而不是只靠一两次事后日志排查。