Appearance
可观测性与日志
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 应用评测的关系是:观测负责"看到现在发生了什么",评测负责"判断现在的行为是不是我们想要的"。
从生产日志里采样,是构建评测数据集的常用路径。具体做法:
- 从日志里找出有代表性的输入(不同类型的问题、边界情况、历史上出过问题的场景)
- 收集这些输入对应的模型输出
- 人工标注或用 LLM 评判这些输出是否符合预期
- 把这批数据固定下来,作为回归测试集
每次改动(换模型、改 prompt、调 RAG 参数)之前,跑一遍这个测试集,看指标有没有退化。可观测性和评测结合,才能形成"改 → 测 → 确认 → 部署"的完整闭环。
哪些事容易被推迟
我观察到的规律是:可观测性几乎总是被推迟到出了问题之后才着手建。
这不是懒惰,而是优先级判断的结果——早期功能都还不完整,先把核心功能跑通再说观测。这个判断本身没错,但如果一直推迟,等到你真的需要观测数据时,往往是系统已经上线、出了一个你追不到的问题。
比较稳的做法是:从第一天就打结构化日志,至少记录输入输出和延迟。其他观测面可以后补,但有了最基础的调用记录,事后追溯的能力就不会完全缺失。
工程化不是等系统完善之后再做的事。AI 应用系统设计这章里有更完整的视角——可观测性是系统里的一等模块,和 Prompt、RAG、安全一样,应该在设计阶段就考虑进去。