Day 55 / 共 68 天 · 阶段 10 可信可观测
可观测:给 Agent 装一台行车记录仪
昨天(Day 54)学了失败闸门和护栏,让 Agent「出事不崩、危险先审批」。可它上线后到底跑得怎么样、花了多少钱、有没有偷偷变笨?你现在两眼一抹黑。今天补上可观测(Observability)这一课:用结构化日志留痕、用 trace 还原一次完整运行、用指标盯住 token/延迟/错误率、还要盯「输出漂移」。学完你就能把线上 Agent 从「黑箱」变成「有仪表盘的驾驶舱」;明天(Day 56)接着讲怎么把这笔账管起来、少花钱。
📍 你在阶段 10(可信可观测 D53-56)的位置
D53 防幻觉 Critic→
D54 失败闸门&护栏→
D55 可观测→
D56 成本治理&缓存
💡 用一个类比兜住今天(今天全程沿用「开车上路」的世界观)
线上跑一个 Agent = 开一辆车跑长途。日志(log) = 行车记录仪,把一路上每件事都录下来,出事能回放;trace(链路追踪) = 一整趟行程的轨迹回放,从上车到下车每个路口都串成一条线;span = 轨迹里的一段路(比如「过隧道」这一段用了多久);指标(metrics) = 仪表盘上的数字(时速=延迟、油耗=token 花费、故障灯=错误率);输出漂移 = 车子悄悄跑偏了方向你却没发现。今天你从「盲开」升级成「盯着仪表盘开」。
L01
为什么要可观测:黑箱上路太危险
🤔 痛点Agent 上线了,一周后老板问你:「昨天有个用户说答案是瞎编的,哪一步错了?」「这个月 API 花了多少钱?」「最近是不是变慢了?」——你打开代码,啥都查不到,只能干瞪眼。程序不像人,不会自己喊疼。
💡 本质可观测 = 让系统自己「说出」它的内部状态,不用你拆开看。就像给车装行车记录仪 + 仪表盘:平时不用管,一旦出事故,回放录像 + 看仪表数据就能定位问题。三大支柱:日志(发生了啥)、trace(一次请求走了哪条路)、指标(整体健康数字)。
图注:三者互补——日志看细节,trace 看一次请求的来龙去脉,指标看整体趋势。
👶 「可观测」和「监控」有啥区别?简单记:监控(monitoring)是「盯着已知的坏事」(比如错误率超 5% 就报警);可观测(observability)是「有能力回答没预料到的问题」(比如「为啥偏偏这个用户的请求慢?」)。可观测是地基,监控是盖在上面的报警器。今天两个都会碰到,不用抠字眼。
L02
结构化日志:录像要「能检索」才有用
🤔 痛点很多新手用
print("出错了") 打日志。上线后几十万行 print 混在一起,想找「用户 A 昨晚那次报错」如同大海捞针,而且 print 没时间、没级别、没上下文。💡 本质结构化日志 = 把日志写成机器能读的「一条条 JSON 记录」,而不是随口一句话。就像行车记录仪不仅录画面,还标上「时间、GPS、车速」——每条日志都带字段,以后能按字段搜、按级别筛。级别从轻到重:
DEBUG(调试碎碎念)→INFO(正常流水)→WARNING(有点不对劲)→ERROR(出事了)。import logging, json
# 配一个最简单的日志器:带时间、级别
logging.basicConfig(level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("agent")
# ❌ 新手写法:一句大白话,事后没法按字段搜
# print("用户问了问题,模型答完了")
# ✅ 结构化写法:把关键信息拼成 JSON,一条日志一个「事件」
log.info(json.dumps({
"event": "llm_call", # 事件类型:这次是「调用大模型」
"user_id": "u_123", # 谁触发的
"model": "claude-3.5", # 用了哪个模型
"input_tokens": 820, # 输入花了多少 token
"output_tokens": 156, # 输出多少 token
"latency_ms": 1340, # 耗时(毫秒)
"ok": True # 成功还是失败
}, ensure_ascii=False))
# 输出:2026-07-12 10:20:01 [INFO] {"event":"llm_call","user_id":"u_123",...}
📝 举个例子:结构化日志的威力
线上排查时,你可以一句命令过滤出所有「失败的、耗时超 3 秒的」调用:
因为每条日志都是规整的 JSON,你还能把它们喂给分析工具画图。散装 print 做不到这些——从今天起,凡是「以后可能要查」的事,都写结构化日志。
cat app.log | grep '"ok": false' | grep latency因为每条日志都是规整的 JSON,你还能把它们喂给分析工具画图。散装 print 做不到这些——从今天起,凡是「以后可能要查」的事,都写结构化日志。
👶 一条好日志该带哪些字段?记个口诀「谁、何时、干了啥、结果如何、花了多少」:
user_id(谁)、timestamp(何时,日志器自动加)、event(干了啥)、ok/error(结果)、tokens/latency_ms(花费)。再加一个 trace_id(下一讲讲)把同一次请求的所有日志串起来,就完美了。L03
trace:把一次运行的轨迹串成一条线
🤔 痛点一个 Agent 回答一个问题,背后可能走了七八步:改写问题 → 检索知识库 → 调模型 → 调工具 → 再调模型总结。用户说「这次好慢」,到底卡在哪一步?散装日志根本串不起来。
💡 本质trace(链路追踪) = 给「一次完整请求」发一个身份证号(trace_id),它经过的每一步都盖上这个号,最后把所有步骤按时间拼成一条轨迹。其中每一步叫一个 span(一段路),记录它自己开始/结束的时间。这样你一眼就能看出「哪段路最堵」。就像快递单号:一个包裹从下单到签收,每个中转站都扫这个单号,你查单号就能看到全程。
图注:一眼看出「调模型总结」这段 span 占了大头,优化就该从这里下手——这就是 trace 的价值。
import uuid, time, logging, json
log = logging.getLogger("agent")
# 一次请求进来,先发一个「身份证号」
trace_id = str(uuid.uuid4())[:8] # 例如 "a1b2c3d4"
def span(name): # 一个小工具:给某一步计时并打日志
t0 = time.time()
return t0, name
def end_span(t0, name):
ms = int((time.time() - t0) * 1000)
# 每一步都带上同一个 trace_id → 事后能串成一条线
log.info(json.dumps({"trace_id": trace_id, "span": name, "ms": ms}))
t0, n = span("检索知识库")
# ... 这里执行真正的检索 ...
end_span(t0, n) # 输出 {"trace_id":"a1b2c3d4","span":"检索知识库","ms":300}
真实项目里你不用手写这套——LangSmith、Langfuse、OpenTelemetry 这类工具会自动帮你记 span。但先理解「trace_id 把一次请求串起来、span 是其中一段」这个骨架,看工具面板才不懵。
L04
指标 metrics:仪表盘上的四个关键数字
🤔 痛点日志和 trace 是「一次一次」看的,可老板要的是「整体怎么样」:平均多快?这月烧了多少钱?多少请求失败了?一条条翻日志翻不过来。
💡 本质指标 = 能加总、能画成曲线的数字。把成千上万次请求聚合成几个关键数,像车的仪表盘:不用看每一秒,扫一眼就知道健康度。Agent 最该盯的四个:① token 用量(油耗/花钱)、② 延迟(时速)、③ 错误率(故障率)、④ 输出漂移(跑偏,下一讲)。
| 指标 | 怎么理解 | 看什么信号 |
|---|---|---|
| Token 用量 | 每次调模型的输入+输出 token,直接换算成钱 | 突然飙高 = 有人恶意刷、或 prompt 变臃肿 |
| 延迟 P50 / P95 | 一半请求快过 P50;95% 请求快过 P95 | P95 高 = 少数用户体验很差(长尾) |
| 错误率 | 失败请求 ÷ 总请求 | 突然升高 = 上游 API 挂了 / 代码有 bug |
| 输出漂移 | 回答质量悄悄下滑 | 模型升级、prompt 被改后常发生 |
👶 小白:为什么看延迟要分 P50、P95,不直接看「平均值」?
👨🏫 老师:因为平均值会骗人!假设 100 个请求里 99 个都是 1 秒,但有 1 个卡了 100 秒——平均值算出来「约 2 秒」,看着还行,可那 1 个用户已经气疯了。P95=2 秒 意思是「95% 的人 2 秒内拿到结果」,P95 才反映大多数人的真实体验。行话叫「长尾延迟」:平均值把最惨的那批人藏起来了,P95/P99 把他们揪出来。开车看平均时速没用,你得知道最堵那段有多堵。
📝 举个例子:一块最朴素的指标看板
把每次请求的
真实项目里 Prometheus + Grafana 或 Langfuse 会自动算这些并画成曲线,你只要会看。
latency_ms 和 ok 攒进内存,定时算一算:错误率 = 失败数 / 总数 = 12 / 400 = 3%(超 5% 就该报警)P95 延迟 = 把 400 个延迟排序,取第 380 个 = 2100ms真实项目里 Prometheus + Grafana 或 Langfuse 会自动算这些并画成曲线,你只要会看。
L05
监控输出漂移:车在悄悄跑偏
🤔 痛点延迟、错误率都正常,请求也都成功了——可用户就是抱怨「答案越来越水」。因为 Agent「没报错地变笨了」:模型供应商偷偷升级了版本、你改了个 prompt、或知识库过期了。传统监控只看「有没有崩」,看不见「质量下滑」。
💡 本质输出漂移(drift) = 系统没报错,但输出的「质量/风格/格式」悄悄偏离了正常。就像车子方向盘慢慢跑偏,仪表盘一切正常,可你正一点点偏离车道。对付它靠三招:① 抽样人工看、② 用 LLM 当裁判打分(Day 50 学过)、③ 盯格式/长度等硬指标。
图注:漂移是「温水煮青蛙」,只盯错误率永远发现不了——必须持续给输出质量打分。
👶 我一个人怎么盯质量?不用全盯!每天随机抽 10~20 条线上问答,让 LLM 裁判按你的评分标准打分(1~5),把平均分画成一条曲线。分数一旦持续往下掉,就去查最近改了啥。这一招把 Day 49~50 学的「评测」从上线前搬到了上线后——评测不是一次性的,是天天做的体检。
🔗 想看真实系统怎么把「护栏 + 可观测」工程化?去看《gov-agents 多 Agent 框架教程》→ 学完回来继续 Day 55
L06
现成工具:LangSmith / Langfuse 帮你自动记账
🤔 痛点前面那些 trace_id、span、指标聚合,难道每个项目都自己手搓一遍?工作量巨大,还容易漏。
💡 本质业界已经有专门的 LLM 可观测平台:你只要接一下,它就自动帮你记录每次调用的 prompt、回答、token、延迟,画好 trace 树和指标看板。就像给车装一套「原厂行车记录仪 + 云端仪表盘」,不用自己焊线。两个常见的:LangSmith(LangChain 官方,和 LangGraph 无缝)、Langfuse(开源,可自己部署,不绑框架)。
| 工具 | 特点 | 适合谁 |
|---|---|---|
| LangSmith | LangChain/LangGraph 官方,接入近乎零成本,trace 树漂亮 | 已经在用 LangChain 生态的人 |
| Langfuse | 开源、能自己部署(数据不出门)、不绑定框架 | 想自托管、对数据合规敏感的团队 |
| OpenTelemetry | 通用可观测标准,不止 LLM,能对接各种后端 | 公司已有统一监控体系 |
# 以 Langfuse 为例,接入常常只要一个「装饰器」——套在函数上就自动记 trace
from langfuse.decorators import observe # 概念示意,实际以官方文档为准
@observe() # 加这一行,这次调用的耗时/输入/输出就被自动记录
def answer(question: str) -> str:
# ... 你原本的 Agent 逻辑,一行不用改 ...
return "生成的回答"
answer("退货政策是什么?") # 跑完,Langfuse 面板里就能看到这条 trace
具体的 API 名、参数会随版本变,别背——记住「接一个 SDK/装饰器 → 自动上报 → 上平台看 trace 和指标」这个套路就够了。面试被问「你们怎么做 LLM 可观测」,答「结构化日志 + LangSmith/Langfuse 记 trace + 监控 token/延迟/错误率/质量漂移」就是满分。
📝 举个例子:接了平台后你能一键回答
「用户 u_123 昨晚 10 点那次为啥答错?」→ 搜 trace,看到检索只召回了 1 条无关文档 → 定位到是知识库切分问题。从「查不到」到「点几下就定位」,这就是接工具的回报。
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- 可观测的三大支柱是什么?(日志、trace、指标)各自看什么?
- 为什么
print不如结构化日志?一条好日志该带哪些字段? - trace 和 span 是什么关系?
trace_id起什么作用? - Agent 最该监控哪四个指标?为什么看延迟要用 P95 而不是平均值?
- 「输出漂移」是什么?错误率正常为什么还要担心质量?怎么盯它?
- LangSmith / Langfuse 帮你省了什么活?
✋ 动手 10 分钟:给一次调用打一条完整的结构化日志
新建 day55.py,把「trace_id + span 计时 + token/延迟 + 成败」串成一条像样的日志(不接任何平台,先手搓一遍找感觉):
import uuid, time, json, logging, random
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("agent")
def call_agent(question):
trace_id = str(uuid.uuid4())[:8] # 本次请求身份证
t0 = time.time()
ok = random.random() > 0.1 # 假装 10% 概率失败
time.sleep(random.uniform(0.2, 1.5)) # 假装在干活,随机耗时
latency_ms = int((time.time() - t0) * 1000)
log.info(json.dumps({ # 一条结构化日志:谁、干了啥、花多少、成不成
"trace_id": trace_id,
"event": "answer",
"question": question,
"input_tokens": random.randint(200, 900),
"output_tokens": random.randint(50, 300),
"latency_ms": latency_ms,
"ok": ok
}, ensure_ascii=False))
return ok, latency_ms
# 跑 20 次,再自己算错误率和 P95,体会「指标」是怎么从日志聚合出来的
results = [call_agent(f"问题{i}") for i in range(20)]
err_rate = sum(1 for ok, _ in results if not ok) / len(results)
lat_sorted = sorted(ms for _, ms in results)
p95 = lat_sorted[int(len(lat_sorted) * 0.95) - 1]
print(f"错误率 = {err_rate:.0%} P95 延迟 = {p95}ms")
加分题:把这 20 条日志重定向到文件 python day55.py > app.log,再用 grep '"ok": false' app.log 把失败的挑出来——体会结构化日志「可检索」的好处。
明日预告 · Day 56:今天你学会了「看见」Agent 花了多少 token。明天(Day 56)接着解决「怎么少花钱」——成本治理 & 缓存:把账算清楚(预算归因,谁花的)、贵活派给贵模型便宜活派给便宜模型(模型路由)、重复问题直接查缓存不再花钱(prompt/结果缓存)、能攒一批一起发的用批处理省钱。可观测是「看表」,成本治理是「省钱」,连起来才是线上运维的完整闭环。