8.4.4 日志与监控
很多 LLM 应用本地 Demo 跑起来都不错,但一到线上就会暴露一个问题:
出了问题,你根本不知道是哪里坏了。
日志与监控的价值,不在“多记点东西”,而在:
让系统出问题时,你有办法定位、解释、回放、修复。
学习目标
- 理解日志、指标、追踪三者分别解决什么问题
- 学会设计结构化日志字段
- 理解 LLM 系统里最值得监控的指标有哪些
- 看懂一个最小日志 + 监控示例
新人术语桥
可观测性会容易很多,只要先把下面几个词分开:
| 术语 | 它回答什么问题 | 在 LLM 应用里的例子 |
|---|---|---|
log | 某个时刻发生了什么 | 检索开始、模型调用失败、导出完成 |
metric | 整体趋势怎么样 | 错误率、P95 延迟、平均 token 成本 |
trace | 一条请求走过哪些步骤 | API -> 检索 -> 模型 -> 模板渲染 -> 返回 |
P95 / P99 | 最慢的 5% 或 1% 请求有多慢 | 用户偶尔觉得很慢时,比平均延迟更有参考价值 |
observability | 能否从外部理解系统行为 | 日志、指标、trace、仪表盘和告警的组合 |
新人先记住这个区别就够了:日志是单个事件,指标是聚合数字,trace 把一条请求的多个步骤串起来。
先建立一张地图
日志与监控更适合按“发生了什么 -> 整体表现怎样 -> 单条请求经历了什么”来理解:
所以这节真正想解决的是:
- 出问题时你到底先看哪一层
- 为什么日志、指标、trace 缺一块都会很难排障
为什么这件事特别重要?
LLM 系统的故障比普通接口更隐蔽
普通接口错误通常比较直接:
- 500
- 超时
- 参数错
而 LLM 系统还会有这些“软故障”:
- 回答变差
- 检索飘了
- token 成本暴涨
- 只在某些场景失误
所以如果你没有观测能力,系统经常会变成:
看起来还活着,但其实已经坏了一半。
日志与监控到底在解决什么?
可以先粗略分三层:
- 日志:发生了什么
- 指标:发生得多不多、快不快、贵不贵
- 追踪:一条请求完整走了哪些步骤
一个更适合新人的总类比
你可以把可观测性理解成:
- 给系统装仪表盘、行车记录仪和维修日志
没有这些东西时,系统坏了你只能说:
- 好像有点不对
有了之后你才可能知道:
- 哪里开始异常
- 是偶发还是持续
- 是单条请求的问题,还是整个系统的问题
先看日志:最基础也最常被写坏
什么叫“结构化日志”?
相比只打一行字符串:
print("request received")
更有价值的是记录结构化字段:
- request_id
- user_id
- stage
- latency_ms
- model_name
一个最小结构化日志示例
log = {
"trace_id": "trace_001",
"stage": "retrieval",
"query": "退款政策是什么",
"latency_ms": 120,
"top_k": 3
}
print(log)
预期输出:
{'trace_id': 'trace_001', 'stage': 'retrieval', 'query': '退款政策是什么', 'latency_ms': 120, 'top_k': 3}
这种日志最大的好处是:
后面你可以按字段查、按字段聚合,而不只是人工看文本。
指标:系统整体表现的体温计
最值得监控的几类指标
对 LLM 系统来说,最常见的指标包括:
- 请求量
- 错误率
- 平均延迟
- P95 / P99 延迟
- token 使用量
- 工具调用次数
- 检索命中率
一个最小指标汇总示例
requests = [
{"latency_ms": 800, "tokens": 600, "ok": True},
{"latency_ms": 1200, "tokens": 750, "ok": True},
{"latency_ms": 3000, "tokens": 900, "ok": False}
]
avg_latency = sum(r["latency_ms"] for r in requests) / len(requests)
error_rate = sum(not r["ok"] for r in requests) / len(requests)
avg_tokens = sum(r["tokens"] for r in requests) / len(requests)
print("avg_latency_ms =", avg_latency)
print("error_rate =", error_rate)
print("avg_tokens =", avg_tokens)
预期输出:
avg_latency_ms = 1666.6666666666667
error_rate = 0.3333333333333333
avg_tokens = 750.0
这就是监控面板最小的雏形。
一个很适合初学者先记的指标表
| 指标 | 更像在回答什么问题 |
|---|---|
| 请求量 | 系统忙不忙 |
| 错误率 | 系统是否经常失败 |
| 平均 / P95 延迟 | 用户是否在等太久 |
| token 使用量 | 成本是否异常 |
| 检索命中率 | RAG 链路是否在变差 |
| 工具调用成功率 | Agent 行动层是否稳定 |
这个表很适合新人,因为它会把“指标很多”重新压回到几个能理解的问题上。

日志回答“发生了什么”,指标回答“整体趋势怎样”,trace 回答“单条请求走过哪里”。LLM 系统排障时三者要连起来看,不能只盯 500 和超时。
追踪(trace):一条请求到底经历了什么?
为什么 LLM 系统特别需要 trace?
因为一条请求往往不只过一个模块,而可能经过:
- API 接入
- 检索
- 工具调用
- 模型生成
- 后处理
如果最后回答错了,你需要知道:
- 是检索错了
- 还是模型生成错了
- 还是工具层挂了
一个最小 trace 示例
trace = [
{"trace_id": "trace_001", "stage": "api_in", "latency_ms": 20},
{"trace_id": "trace_001", "stage": "retrieval", "latency_ms": 120},
{"trace_id": "trace_001", "stage": "llm_generate", "latency_ms": 850},
{"trace_id": "trace_001", "stage": "response_out", "latency_ms": 15}
]
for item in trace:
print(item)
预期输出:
{'trace_id': 'trace_001', 'stage': 'api_in', 'latency_ms': 20}
{'trace_id': 'trace_001', 'stage': 'retrieval', 'latency_ms': 120}
{'trace_id': 'trace_001', 'stage': 'llm_generate', 'latency_ms': 850}
{'trace_id': 'trace_001', 'stage': 'response_out', 'latency_ms': 15}
trace 的核心价值是:
让你看到“同一个请求的完整旅程”。
第一次做线上排障时,最稳的默认顺序
更稳的顺序通常是:
- 先看指标有没有整体异常
- 再看日志里具体哪一类请求在出错
- 最后顺着 trace 看完整链路
这样会比一开始就翻满屏日志更容易定位问题。
一个更贴近真实的最小观测闭环
import time
def timed_stage(name, fn, *args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
latency_ms = int((time.time() - start) * 1000)
log = {
"trace_id": "trace_demo_001",
"stage": name,
"latency_ms": latency_ms
}
print(log)
return result
def fake_retrieve(query):
time.sleep(0.1)
return ["退款政策"]
def fake_llm(docs):
time.sleep(0.2)
return f"根据 {docs} 生成回答"
docs = timed_stage("retrieval", fake_retrieve, "退款政策是什么")
answer = timed_stage("llm_generate", fake_llm, docs)
print(answer)
示例输出;实际 latency_ms 可能有轻微差异:
{'trace_id': 'trace_demo_001', 'stage': 'retrieval', 'latency_ms': 100}
{'trace_id': 'trace_demo_001', 'stage': 'llm_generate', 'latency_ms': 200}
根据 ['退款政策'] 生成回答
这个例子虽然小,但已经把:
- trace_id
- stage
- latency
这几个核心字段带起来了。
LLM 系统最值得额外监控的东西
相比传统 API,LLM 系统通常还值得多监控这些:
token 成本
因为它直接决定:
- 钱花了多少
- prompt 是否越来越长
检索质量
例如:
- top-1 是否命中
- 检索为空比例
工具调用质量
例如:
- 工具调用成功率
- 参数校验失败率
- 重试率
回答质量信号
例如:
- 用户追问率
- 用户纠错率
- 点踩率
这类指标不能替代离线评估,但非常重要。
告警为什么不能只看“服务挂没挂”?
LLM 系统很多问题不会直接 500
例如:
- 回答质量持续下降
- token 用量突然翻倍
- 检索命中率掉得很厉害
这些问题系统可能仍然“活着”,但业务已经明显坏了。
所以告警最好分两层
-
基础可用性告警
- 错误率
- 超时率
-
业务质量告警
- 检索命中率下降
- 平均 token 数异常上升
- 用户负反馈异常增加
一个很适合初学者先记的告警分层表
| 告警类型 | 典型例子 |
|---|---|
| 可用性告警 | 错误率高、超时率高 |
| 成本告警 | token 暴涨、调用次数异常 |
| 质量告警 | 检索命中率下降、用户追问率上升 |
这个表很适合新人,因为它会提醒你:
- LLM 系统的“坏掉”不只是一种坏法
如果你的目标是“知识库驱动的课件生成助手”,最值得先监控什么?
这类系统比普通问答更容易出现“看起来还行,但其实已经歪了”的问题。
第一次做时,特别值得先盯这几类字段:
| 监控点 | 更像在看什么 |
|---|---|
retrieved_count | 内部资料有没有召回到内容 |
example_count | 有没有真的抽到例题 |
source_origin_mix | 内部资料和外部资料谁占主导 |
export_success | Word 导出是否成功 |
schema_valid | 结构化结果是否符合模板要求 |
一个最小日志对象可以先写成:
log = {
"trace_id": "trace_001",
"topic": "折扣应用题",
"retrieved_count": 5,
"example_count": 2,
"schema_valid": True,
"export_success": True,
}
print(log)
预期输出:
{'trace_id': 'trace_001', 'topic': '折扣应用题', 'retrieved_count': 5, 'example_count': 2, 'schema_valid': True, 'export_success': True}
这个例子特别适合新人,因为它会帮助你先明白:
- 这类项目的监控重点,不只是模型快不快
- 还包括资料有没有找对、结构有没有成型、文档有没有导出成功
一个很实用的日志字段清单
如果你在做 LLM 服务,最实用的一组字段通常包括:
| 字段 | 作用 |
|---|---|
| trace_id | 串起整条链路 |
| user_id / session_id | 定位用户或会话 |
| stage | 当前在哪个环节 |
| latency_ms | 这一步耗时多少 |
| model_name | 用了哪个模型 |
| prompt_tokens / completion_tokens | 成本分析 |
| tool_name | 调了什么工具 |
| retrieval_topk | 检索设置 |
| error_code | 失败类型 |
不是每一条日志都要全打,但这份清单很适合作为设计起点。
初学者最常踩的坑
只打字符串,不打字段
后面很难聚合分析。
只记成功,不记失败
这会让错误定位非常痛苦。
没有 trace_id
出了问题无法追完整链路。
只监控系统可用性,不监控业务质量
这是 LLM 项目里特别常见的问题。
小结
这一节最重要的不是“学会打日志”,而是理解:
日志、指标、trace 共同组成了系统的可观测性,它们决定你能不能真正维护一个上线后的 LLM 服务。
没有观测,很多故障只能靠猜; 有了观测,系统才真正可维护。
如果把它做成项目或系统设计,最值得展示什么
最值得展示的通常不是:
- “我接了日志系统”
而是:
- 一条请求的 trace
- 一组关键指标
- 一个典型错误案例怎么被定位出来
- 质量告警和可用性告警是怎么分层的
这样别人会更容易看出:
- 你理解的是可观测性闭环
- 不只是会打印日志
练习
- 给本节的
timed_stage()再加一个error_code字段。 - 设计一个你自己的日志结构,专门记录检索阶段。
- 想一想:如果服务错误率没变,但用户追问率突然上升,这通常意味着什么?
- 用自己的话解释:为什么 LLM 系统的告警不能只看 500 和超时?