Skip to content

可观测性与日志

AI 应用上线后,"看起来能跑"和"真的可信"之间还差一套观测体系。这篇讲 AI 应用的可观测性设计:怎么记录每次 LLM 调用的输入输出、延迟、token 用量和错误;怎么用日志做回归分析;以及主流的观测工具(LangSmith、Langfuse、OpenLLMetry 等)的使用场景。

为什么 AI 应用特别需要可观测性

普通接口出了问题,你可以看报错日志、查堆栈、打断点。AI 应用也会出问题,但很多"出问题"不会触发任何报错——系统正常响应了 200,只是回答质量悄悄下降了。

几个典型的无报错但有问题的场景:

  • Prompt 里的措辞改了一句,某类问题的拒答边界开始松动
  • RAG 的 top-k 调整后,一部分问题的召回内容变差,回答开始不准
  • 换了一个模型版本,整体感觉还行,但引用格式开始不稳定
  • 对话轮数多了,上下文被裁剪的方式开始影响答案一致性

这些问题靠人工随机测几次很难发现。你需要的是:每次调用都有完整记录,能对比版本之间的变化,能在问题出现时追溯到具体的输入输出。

可观测性不是为了"在出问题时看日志",而是为了提前发现你不知道已经存在的问题

最小观测面

对一个 AI 应用,以下字段是基础观测面——有了这些,大多数问题可以定位:

每次 LLM 调用的记录:

字段说明
call_id唯一标识符,方便关联前端请求和后端日志
timestamp调用时间,用于时序分析
model使用的模型和版本
input_messages完整的输入消息列表(含 system prompt)
output_content模型输出的完整内容
prompt_tokens输入 token 数
completion_tokens输出 token 数
latency_ms从发出请求到收到完整响应的耗时
ttft_ms流式场景下第一个 token 到达的时间
finish_reason完成原因(stop / length / content_filter 等)
error如果有报错,记录错误类型和信息
session_id对话会话 ID,方便聚合同一对话的多轮调用
user_id用户标识(可脱敏),用于分析用户级别的使用模式

Tool Calling / Agent 场景下还要加:

字段说明
tool_calls本轮调用了哪些工具,参数是什么
tool_results每个工具的返回内容
step_index在 Agent 循环中的第几步
total_steps本次任务总共用了多少步
task_id一次完整 Agent 任务的标识,聚合所有步骤

RAG 场景下还要加:

字段说明
query_embedding_model使用的 embedding 模型
retrieved_chunks召回的 chunk 内容和来源文档
retrieval_scores各 chunk 的相似度分数
rerank_applied是否做了重排,以及重排后的顺序变化

这些字段不需要每个都在第一天就全量实现。最低起点是:输入、输出、token 数、延迟、错误。其他字段根据你在排查问题时的实际需要逐步补充。

三类日志的用途不同

把日志按用途分开,会让后续分析清晰很多:

开发调试日志:用于本地开发和联调阶段。输出详细,包括完整的 messages 数组、中间步骤、变量状态。这类日志不应该在生产环境全量保留,太贵,也包含太多敏感内容。

线上回归日志:用于监控系统在生产环境的行为变化。保留足够排查问题的信息,但可以对 prompt 做摘要处理,对用户输入做脱敏。重点是结构化、可查询、可聚合。

评测数据:从真实调用里采样,用于构建评测集和做版本对比。选取有代表性的输入,附上模型的实际输出,定期人工标注或自动评分。AI 应用评测里讲的 Eval Dataset,很多时候就是从这里来的。

很多团队刚开始只有一种日志——把所有东西都塞到一个结构化 JSON 里打印出来。这当然比没有强,但等系统复杂起来,你会发现混在一起的日志很难区分"这条是用来排错的"还是"这条是用来做评测的"。早点做概念上的区分,后面省事。

代码里怎么记

最简单的方式是在模型调用外面包一层,统一记录:

python
import time
import uuid
import logging
from openai import AsyncOpenAI

client = AsyncOpenAI()
logger = logging.getLogger("llm_calls")

async def tracked_chat_completion(
    messages: list[dict],
    model: str = "gpt-4o",
    session_id: str = None,
    **kwargs
) -> dict:
    """包装 OpenAI 调用,自动记录观测数据"""
    call_id = str(uuid.uuid4())
    start_time = time.monotonic()

    try:
        response = await client.chat.completions.create(
            model=model,
            messages=messages,
            **kwargs
        )
        latency_ms = (time.monotonic() - start_time) * 1000

        usage = response.usage
        output_content = response.choices[0].message.content
        finish_reason = response.choices[0].finish_reason

        # 记录结构化日志
        logger.info({
            "call_id": call_id,
            "session_id": session_id,
            "model": model,
            "input_messages": messages,  # 生产环境可以只记录摘要
            "output_content": output_content,
            "prompt_tokens": usage.prompt_tokens,
            "completion_tokens": usage.completion_tokens,
            "latency_ms": round(latency_ms, 2),
            "finish_reason": finish_reason,
            "error": None
        })

        return {
            "content": output_content,
            "call_id": call_id,
            "usage": usage
        }

    except Exception as e:
        latency_ms = (time.monotonic() - start_time) * 1000
        logger.error({
            "call_id": call_id,
            "session_id": session_id,
            "model": model,
            "input_messages": messages,
            "output_content": None,
            "latency_ms": round(latency_ms, 2),
            "error": str(e),
            "error_type": type(e).__name__
        })
        raise

这种"包一层"的方式不改变原有调用逻辑,但每次调用都会留下可查询的记录。随着需求增加,可以在这个函数里加更多字段,或者换成调用外部观测平台的 SDK。

主流观测工具

做 AI 应用观测,有几个专门工具比自建方案省很多事,各有侧重:

LangSmith(LangChain 官方出的)适合已经在用 LangChain 或 LangGraph 的项目。与框架深度集成,配置几行代码就能自动采集所有链路和 Agent 步骤。可视化做得很好,能直接在界面上看到 Agent 的每一步做了什么。免费层够个人项目用。

python
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-api-key"
# 之后所有 LangChain 调用自动被追踪,不需要改代码

Langfuse 是开源的,支持自托管,适合对数据隐私有要求的团队。功能覆盖追踪、评测、提示词管理,做 A/B 测试和版本对比很方便。不绑定特定框架,任何 LLM 调用都能接进来。

python
from langfuse import Langfuse

langfuse = Langfuse()

# 手动追踪
trace = langfuse.trace(name="rag-query", session_id=session_id)
span = trace.span(name="retrieval")
# ... 执行检索 ...
span.end(output={"chunks": retrieved_chunks})

generation = trace.generation(
    name="llm-call",
    model="gpt-4o",
    input=messages,
    output=response.content,
    usage={"promptTokens": usage.prompt_tokens, "completionTokens": usage.completion_tokens}
)

OpenLLMetry 是基于 OpenTelemetry 标准的 LLM 观测方案,适合已有 OpenTelemetry 基础设施的团队。数据可以发到 Datadog、Grafana、Jaeger 等你熟悉的平台,不需要引入新的存储系统。

什么时候用哪个

  • 个人项目或快速原型:LangSmith(如果用 LangChain),或者自己写个简单的 logging 封装
  • 团队项目,有数据隐私要求:Langfuse 自托管
  • 企业内部,已有可观测性基础设施:OpenLLMetry

不需要非选一个"最好的"。从最简单的 logging 封装开始,等你真的开始频繁需要"查某次调用的上下文"或"对比两个版本的效果"时,再引入专门工具。

关键指标与警报

记录下来的数据,需要转化成可以告警的指标才有实际价值。几个对 AI 应用特别有用的指标:

P95/P99 延迟:中位数延迟可能看起来不错,但 P99 延迟会暴露出偶发的极端慢请求,这些慢请求可能是 token 超限、工具超时或上下文过长导致的。

错误率:按错误类型分类(rate limit、timeout、content filter),可以帮你判断是模型 API 侧的问题还是你自己的调用逻辑问题。

每日/每用户 token 消耗:突然的 token 消耗上涨,可能是某个输入场景触发了意外的长上下文,也可能是有滥用。

finish_reason 分布:如果 length(被截断)的比例突然上升,说明 max_tokens 设定可能太小,或者某类用户输入特别长。

RAG 拒答率:如果 RAG 系统里"资料不足"的拒答比例突然升高,可能是知识库没有及时更新,或者用户开始问超出知识库范围的问题。

和评测的关系

可观测性和AI 应用评测的关系是:观测负责"看到现在发生了什么",评测负责"判断现在的行为是不是我们想要的"。

从生产日志里采样,是构建评测数据集的常用路径。具体做法:

  1. 从日志里找出有代表性的输入(不同类型的问题、边界情况、历史上出过问题的场景)
  2. 收集这些输入对应的模型输出
  3. 人工标注或用 LLM 评判这些输出是否符合预期
  4. 把这批数据固定下来,作为回归测试集

每次改动(换模型、改 prompt、调 RAG 参数)之前,跑一遍这个测试集,看指标有没有退化。可观测性和评测结合,才能形成"改 → 测 → 确认 → 部署"的完整闭环。

哪些事容易被推迟

我观察到的规律是:可观测性几乎总是被推迟到出了问题之后才着手建。

这不是懒惰,而是优先级判断的结果——早期功能都还不完整,先把核心功能跑通再说观测。这个判断本身没错,但如果一直推迟,等到你真的需要观测数据时,往往是系统已经上线、出了一个你追不到的问题。

比较稳的做法是:从第一天就打结构化日志,至少记录输入输出和延迟。其他观测面可以后补,但有了最基础的调用记录,事后追溯的能力就不会完全缺失。

工程化不是等系统完善之后再做的事。AI 应用系统设计这章里有更完整的视角——可观测性是系统里的一等模块,和 Prompt、RAG、安全一样,应该在设计阶段就考虑进去。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发