代码语言

知识点思维导图

39 个知识节点

可观测性(03) - AI 应用日志与可观测性

读完后,你应能完成以下任务:

  • 绘制“可观测性(03) - AI 应用日志与可观测性 / 散文日志 vs 结构化日志”的关键对象与数据流,解释“串联:同一次请求的所有日志带同一个 request_id,按它能还原整个链路。 -> 聚合:所有日志字段统一,能算「命中率」「平均耗时」「总 token」这些指标。”,并用源码位置、日志或 Trace 标注证据。
  • 为“可观测性(03) - AI 应用日志与可观测性 / AI 应用非记不可的字段”设计正常与异常输入,验证“普通 Web 应用记 URL、状态码、耗时就差不多了。”,输出首个偏差位置与回归测试结果。
  • 实现“可观测性(03) - AI 应用日志与可观测性 / 串联一次请求:requestId 是主线”的最小代码或配置,检验“排查那个"说不知道"的 case 时,你只要 grep req-e1a84ec6,这次请求的 start、retrieve、generate、end 四条日志全出来,一眼看出是 retrieve 的 hit_count=0——检索没命中,问题在检索不在模型。”,输出命令、结果与 Diff,并说明不适用边界。

一、AI 应用日志与可观测性的真实应用场景

用户反馈:"我昨天下午问'报销政策',它说不知道,但你们明明有这文档。"

你想复盘这次到底哪一步出了问题:是检索没命中?还是命中了但模型没用上?耗时多久?花了多少 token?打开日志一看——只有一行 print("生成回答"),什么都查不到。你连用户那次请求的记录都定位不到。

AI 应用的坏 case 排查,全靠日志。而且它比普通 Web 应用更难排查:链路长(检索、拼 prompt、调模型、解析)、结果不确定(同样的输入模型可能答得不一样)、成本敏感(每次调用都在花钱)。没有结构化日志,线上的 AI 应用就是个黑盒。

二、散文日志 vs 结构化日志

散文日志(给人读):
  print("用户问了报销,检索失败了")
  → 没法检索、没法统计、没法串联,出问题只能干瞪眼

结构化日志(给机器读):
  {"request_id": "req-e1a84ec6", "stage": "retrieve", "cost_ms": 55, "hit_count": 0}
  → 能按 request_id 串、能按字段聚合、能 grep

结构化日志的本质:每条日志是一个固定字段的 JSON 对象,而不是一句话。 这带来两个能力,都是排查的命根子:

  1. 串联:同一次请求的所有日志带同一个 request_id,按它能还原整个链路。
  2. 聚合:所有日志字段统一,能算「命中率」「平均耗时」「总 token」这些指标。

三、AI 应用非记不可的字段

普通 Web 应用记 URL、状态码、耗时就差不多了。AI 应用要多记三样它独有的:

字段 为什么非记不可
request_id 串起一次请求的所有阶段,排查的起点
各阶段耗时 cost_ms 模型调用是最大延迟源,要能定位是检索慢还是生成慢
token 用量 token 直接等于钱,不记就不知道成本花哪了
检索命中 hit_count RAG 答得对不对的前提,命中率是核心质量指标
answered(是否真答了) 区分「拒答」和「乱答」,坏 case 分类靠它

记日志的代码很朴素,关键是每个阶段都带上 request_id 和该阶段的关键字段:

四、串联一次请求:requestId 是主线

一次 RAG 请求分好几步,每步都用同一个 request_id 记日志,它们就被串成了一条线:

排查那个"说不知道"的 case 时,你只要 grep req-e1a84ec6,这次请求的 start、retrieve、generate、end 四条日志全出来,一眼看出是 retrievehit_count=0——检索没命中,问题在检索不在模型。

五、TraceID:跨服务时比 request_id 更重要

单体应用里一个 request_id 就够串起日志;多 Agent、多服务、MCP 网关里,一次用户请求会拆成多次检索、模型调用和工具调用。这时要有全链路 trace_id,每个子步骤再带自己的 span_id

最小字段可以这样设计:

字段 用途
trace_id 串起一次用户请求的全链路
span_id 标记某个子步骤
parent_span_id 还原调用树
stage retrieve / rerank / generate / tool / judge
cost_ms 当前步骤耗时

六、从日志聚合指标:可观测性的回报

结构化日志攒起来,就能算出运营关心的指标,这是散文日志做不到的:

命中率掉了说明检索出问题,token 飙了说明有人在滥用或 prompt 太长,耗时涨了说明模型或检索变慢。这些都是日志字段直接算出来的。

延迟不要只看平均值。P50 代表常规体验,P90/P99 代表尾延迟;平均耗时没变但 P99 飙升,通常说明少数请求卡在模型排队、向量库查询或外部工具调用上。

七、工程上真正会踩的坑

  • 把整段 prompt 和回答原文打进日志。prompt 可能很长、可能含用户隐私(手机号、订单),全量打日志既占空间又有合规风险。记长度、记摘要、记 hash,敏感字段脱敏。
  • 不记 request_id。各阶段日志没法串联,排查时只能靠时间戳猜哪条是同一次请求,多并发下根本对不上。
  • token 不落日志。月底账单超支了,不知道是哪个接口、哪类请求烧的钱,没法优化。每次模型调用的 usage 必须记。
  • 日志级别不分。正常请求和错误用同一级别,线上 ERROR 被海量 INFO 淹没。正常走 INFO,拒答/检索为空走 WARN,异常走 ERROR。

八、一句话面试答法

AI 应用的日志和普通应用有什么不一样,你怎么做可观测性? 我用结构化日志,每条是带固定字段的 JSON,不是一句话。除了常规的 requestId 和耗时,AI 应用我一定会记三样:token 用量(直接等于成本)、检索命中数(RAG 质量的前提)、是否真的回答了(区分拒答和乱答)。用 requestId 把一次请求的检索、生成各阶段串起来,排查坏 case 时 grep 一个 id 就能还原全链路;字段统一了还能聚合出命中率、平均耗时、总 token 这些指标。prompt 和回答里的隐私字段会脱敏,不全量打日志。

九、动手实践:40 AI 应用日志与可观测性

结构化日志:把每次 RAG 请求的全过程记成 JSON,带 request_id / 耗时 / token / 命中,再从日志里聚合出命中率、总 token、平均耗时。

9.1 在线运行

零依赖,纯标准库。处理三个请求(含命中和未命中),打印每条结构化日志,再聚合指标。

9.2 预期输出

=== 每次请求的结构化日志 ===
{"request_id": "req-e1a84ec6", "stage": "start", "ts": 1781505032.714, "question": "报销发票几天内提交"}
{"request_id": "req-e1a84ec6", "stage": "retrieve", "ts": 1781505032.769, "cost_ms": 55.0, "hit_count": 1, "query": "报销发票几天内提交"}
{"request_id": "req-e1a84ec6", "stage": "generate", "ts": 1781505032.851, "cost_ms": 81.3, "tokens": 33, "answered": true}
{"request_id": "req-e1a84ec6", "stage": "end", "ts": 1781505032.851, "total_ms": 136.6, "hit_count": 1, "total_tokens": 33}
... (年假未命中、迟到命中的日志) ...

=== 从结构化日志聚合出的指标 ===
总请求数:3
命中率:2/3 = 67%
总 token:73
平均耗时:138.4 ms

每个 request_id 把同一次请求的 start/retrieve/generate/end 四条日志串起来,按它就能还原一次请求的全过程。

9.3 代码 ↔ 概念对应

概念 在 main.py 哪里
结构化日志(JSON 一行一条) StructuredLogger.log
requestId 串联一次请求 handle_request 里生成的 request_id
分阶段记录(retrieve/generate) fake_retrieve / fake_generate 各自的 logger.log
记录耗时 每个阶段的 cost_ms / total_ms
记录 token(等于钱) generate 阶段的 tokens
记录检索命中(RAG 质量) retrieve 阶段的 hit_count
从日志聚合指标 print_metrics

9.4 为什么 AI 应用必须结构化日志

普通日志 print("查询失败了") 是给人读的散文,没法检索、没法统计。结构化日志是给机器读的固定字段 JSON:

  • request_id 能串起一次请求的全过程 —— 排查时的起点
  • 按字段能聚合 —— 算命中率、总 token、平均耗时,全靠它

AI 应用有三个独有的观测点,普通 Web 应用没有:token 用量(直接等于钱)、检索命中(RAG 答得对不对的前提)、模型耗时(最大的延迟来源)。这三个不记,线上出问题就是抓瞎。

9.5 动手改

  • logger.records 写到文件(open("app.log", "a")),体会真实日志落盘。
  • print_metrics 里加「P95 耗时」统计(排序后取 95 分位)。
  • generate 加一个 model 字段,按模型聚合各自的 token 消耗。

9.6 可运行源码:AI 应用日志与可观测性

main.py

十、总结

  • 散文日志 vs 结构化日志:串联:同一次请求的所有日志带同一个 request_id,按它能还原整个链路。 -> 聚合:所有日志字段统一,能算「命中率」「平均耗时」「总 token」这些指标。
  • 串联一次请求:requestId 是主线:排查那个"说不知道"的 case 时,你只要 grep req-e1a84ec6,这次请求的 start、retrieve、generate、end 四条日志全出来,一眼看出是 retrieve 的 hit_count=0——检索没命中,问题在检索不在模型。
  • 从日志聚合指标:可观测性的回报:这些都是日志字段直接算出来的。
  • 工程上真正会踩的坑:prompt 可能很长、可能含用户隐私(手机号、订单),全量打日志既占空间又有合规风险。
  • 一句话面试答法:我用结构化日志,每条是带固定字段的 JSON,不是一句话。

学完自测

选择所有正确答案;提交后逐项核对判断依据。

1在“AI 应用日志与可观测性”中,需要同时满足“AI 应用日志与可观测性的真实应用场景”与“散文日志 vs 结构化日志”。给定正文约束“"我昨天下午问'报销政策',它说不知道,但你们明明有这文档。”,哪些判断保持了原有处理机制?多选
2“AI 应用日志与可观测性”出现偏差:“在“AI 应用日志与可观测性 / AI 应用非记不可的字段”中,即使不满足“普通 Web 应用记 URL、状态码、耗时就差不多了”,结果与副作用仍会保持不变。”已成为实际行为。围绕“AI 应用非记不可的字段”与“串联一次请求:requestId 是主线”,哪些判断能定位被改变的职责或边界?多选
3评审“AI 应用日志与可观测性”方案时,验收条件包含“单体应用里一个 requestid 就够串起日志;”。关于“TraceID:跨服务时比 requestid 更重要”与“从日志聚合指标:可观测性的回报”的哪些决策符合正文机制?多选