本节在全册位置:可观测性消费第四章检查点。LangSmith 能自动追踪每次运行的节点、耗时、状态快照;也可以自己把检查点历史读出来做分析。没有观测,长图就是黑盒。我们的经验:上线前先确认能回放任意一次运行。
用 LangSmith 时,每次 invoke 自动上报 spans,能看到每个节点输入输出与耗时,定位慢节点。也可 get_state_history 读检查点栈,离线复盘一次运行。关键指标:单节点延迟、循环轮数、工具调用次数。类比金融:观测像风控大屏,每笔流转可见。指标要落到告警,否则只是好看的图。
# 读检查点历史做离线分析(无需 LangSmith) from langgraph.checkpoint.memory import MemorySaver cfg = {"configurable": {"thread_id": "t1"}} # g 已 compile(checkpointer=saver) for snap in g.get_state_history(cfg): print(snap.values, "ts=", snap.ts) # 可统计每步耗时、状态增长,定位异常步 # 这套数据足够做基础可观测性,不必先接商业平台
# 商业平台胜在聚合与协作,自读历史胜在零依赖
背景:某图整体 8 秒,用户嫌慢,但没人知道时间花在哪。
操作:读历史发现检索节点占 6 秒,其余均 <1 秒。
结果:只优化检索(加缓存/并行),整体降到 2 秒,改动集中。
解读:观测先定位瓶颈再优化,避免盲改把时间花在无关节点上。
变式:把耗时指标接入告警,节点超阈值自动通知,把可观测性变成运维闭环。

可观测性消费第四章检查点,LangSmith 自动追踪节点与耗时,也能自读 get_state_history 离线复盘。
关键指标:单节点延迟、循环轮数、工具调用次数,指标要落到告警才形成运维闭环。
上线前先确认能回放任意一次运行,否则长图就是黑盒,出问题只能靠猜。
只记最终输出不记轨迹,长图出问题时没有任何可观测线索,等于黑盒上线;先定位瓶颈再优化,比盲改把时间花在无关节点强得多。
不接商业平台也能做基础可观测性:从检查点历史算出每节点耗时与状态增长,定位异常步。
# 从 get_state_history 算节点耗时 snaps = list(g.get_state_history(cfg)) deltas = [] for prev, cur in zip(snaps[1:], snaps[:-1]): dt = float(cur.ts) - float(prev.ts) if (cur.ts and prev.ts) else 0 changed = set(cur.values) ^ set(prev.values) deltas.append((cur.next if hasattr(cur, "next") else "?", round(dt, 3), len(changed))) # deltas: (走到哪, 耗时秒, 状态变化量) for d in deltas: print(d) # 耗时异常大的节点,就是优化候选
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| 单节点延迟 | 该步耗时 | >1s |
| 循环轮数 | 反思次数 | >5 |
| 工具调用数 | 外呼次数 | >10 |
⚠️ 常见坑:只看整体耗时就动刀,结果改了不相关的节点;先按历史拆出每步耗时,把优化集中在最慢的那一个。
💡 关键直觉:可观测性的价值不在「好看」,而在把长图从黑盒变成可回放的时间线;回放能力先于一切优化。
补充:自建观测的代价是要自己写聚合逻辑;当团队扩大到多图并行,还是建议接 LangSmith,把这部分人力省下来做业务,自读历史更适合验证期与小团队。
可观测性不是单一动作,拆成三层各司其职,缺哪层都会在特定故障面前失明:
| 层 | 回答的问题 | 典型实现 |
|---|---|---|
| 日志 | 发生了什么 | 节点内 print 或写日志通道 |
| 轨迹 | 以什么顺序发生 | 检查点历史、LangSmith trace |
| 指标 | 发生得有多快/多频繁 | 耗时、轮数、调用次数聚合 |
定位问题时的顺序是「指标发现异常、轨迹定位环节、日志看细节」:指标告诉你哪个接口变慢,轨迹告诉你慢在哪个节点,日志告诉你那个节点里发生了什么。三层都齐了,排障才不用猜。只在某一层里打转,是观测投入最常见的问题。
埋点不应该是事后补丁,建图时就按固定模式在每个节点入口出口打点,数据才连贯:
import time, json def traced(name): """给节点包一层计时与入出记录""" def deco(fn): def wrapper(s): t0 = time.time() try: out = fn(s) emit(name, "ok", time.time() - t0) return out except Exception as e: emit(name, "err", time.time() - t0, str(e)) raise return wrapper return deco @traced("retrieve") def retrieve(s): ...
埋点输出进日志通道(而不是 print),用流式导出,线上关闭也方便。三个要记的字段:节点名、耗时、结果状态。有了这套统一埋点,任何图都能回答「每个节点跑了多久、成没成」,这是后续性能优化的数据底座。
接 LangSmith 之后,先弄清四个词,用它才不会一头雾水:
| 概念 | 含义 | 用途 |
|---|---|---|
| Run | 一次图调用 | 每次 invoke/stream 一个 Run |
| Span | Run 内的一个节点 | 定位耗时归属 |
| Trace | Run + Spans 的树 | 看整体调用链 |
| Dataset | 输入输出样本集 | 回归测试 |
LangSmith 的价值不只是看曲线,它的 Dataset 能配合评价做回归:把线上的失败样本收进数据集,改完图跑一遍,看是否修复且没引入新问题。把这套「采集-分析-回归」闭环用起来,可观测性就从「看板」升级成「质量体系」。