主题:Observability、调试、评估、AgentOps
目标:展望 LangChain 的工程化能力版图,并给出"离线可跑"的最小实践样例,帮助你为生产环境做好准备。
一、为什么是 Observability / Debug / Eval / AgentOps?
在实验阶段,我们只需"跑通"。但在生产阶段,你需要:
- 可观测(Observability):谁调用了什么模型?花了多少钱?卡在哪一步?可追溯吗?
- 可调试(Debug):重放同一次对话/同一批数据;看中间态;快速定位失败路径。
- 可评估(Eval):线下离线评估 + 线上 A/B;准确率、相关性、覆盖率、安全性。
- 可运营(AgentOps):配额/预算、超时/限流、熔断/降级、合规模型选路、红蓝账成本归集。
LangSmith (观测/评估)、LangGraph (图式编排/持久化)、LangServe(服务化)组合,正把这些能力工程化、平台化。
二、离线可跑的"迷你可观测系统"与链路重放
我们先在本地实现一个极简 Tracer,记录调用树(span)、输入输出、耗时、异常。你可以把它替换成实际的 LangSmith(云端)或你自己的日志/监控基础设施。
2.1 一个最小可用的 Tracer
python
# tracer.py(可直接复制到同文件)
from __future__ import annotations
import time, json
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
class MiniTracer:
def __init__(self):
self.spans: List[Dict[str, Any]] = []
self.stack: List[int] = []
@contextmanager
def span(self, name: str, meta: Optional[dict] = None):
sid = len(self.spans)
parent = self.stack[-1] if self.stack else None
start = time.time()
rec = {"id": sid, "name": name, "parent": parent, "start": start,
"end": None, "duration": None, "meta": meta or {}, "status": "ok"}
self.spans.append(rec)
self.stack.append(sid)
try:
yield rec
except Exception as e:
rec["status"] = "error"
rec["meta"]["exception"] = repr(e)
raise
finally:
rec["end"] = time.time()
rec["duration"] = rec["end"] - rec["start"]
self.stack.pop()
def export_json(self, path="trace.json"):
with open(path, "w", encoding="utf-8") as f:
json.dump(self.spans, f, ensure_ascii=False, indent=2)
print(f"[Tracer] 导出到 {path}")
TRACER = MiniTracer()
2.2 把 Tracer 接到一个简化链路(Prompt→LLM→Parser)
python
# demo_observability.py
from tracer import TRACER
import time
def prompt_fn(vars: dict) -> str:
return f"请用一句话解释:{vars['q']}"
def mock_llm(text: str) -> str:
# 模拟延迟与生成
time.sleep(0.05)
return f"解释:{text}"
def parse_str(s: str) -> str:
return s.strip()
def pipeline(question: str) -> str:
with TRACER.span("Prompt", {"vars": {"q": question}}):
p = prompt_fn({"q": question})
with TRACER.span("LLM", {"provider": "mock"}):
out = mock_llm(p)
with TRACER.span("Parser"):
res = parse_str(out)
return res
if __name__ == "__main__":
print(pipeline("为什么需要可观测性?"))
TRACER.export_json("trace_observe.json")
运行后会生成 trace_observe.json
,你能看到 层级调用、耗时、异常 ,这就是"把链路可视化/可追踪"的第一步。实际工程中把 TRACER.span
换成 LangSmith trace
即可同步云端可视化。
json
[
{
"id": 0,
"name": "Prompt",
"parent": null,
"start": 1755651558.7223232,
"end": 1755651558.7223232,
"duration": 0.0,
"meta": {
"vars": {
"q": "为什么需要可观测性?"
}
},
"status": "ok"
},
{
"id": 1,
"name": "LLM",
"parent": null,
"start": 1755651558.7223232,
"end": 1755651558.773508,
"duration": 0.051184892654418945,
"meta": {
"provider": "mock"
},
"status": "ok"
},
{
"id": 2,
"name": "Parser",
"parent": null,
"start": 1755651558.773508,
"end": 1755651558.773508,
"duration": 0.0,
"meta": {},
"status": "ok"
}
]
三、Debug:重放、脱敏与确定性
调试要点:
- 可重放 :固定输入、固定 Prompt、固定随机种子(对可控模型);把 "输入 + 中间态 + 输出" 落盘(如 JSONL),需要时重放。
- 脱敏 :日志里避免泄露个人信息(PII),例如用
***
替换手机号/邮箱。 - 确定性:非生产场景用小温度、小采样或固定 seed,让用例可复现。
下面做个离线重放器:把样本和输出保存起来,失败时重放定位问题。
python
# demo_debug_replay.py
import json, time
from tracer import TRACER
def run_once(sample: dict) -> dict:
q = sample["q"]
with TRACER.span("Prompt"):
prompt = f"解释:{q}"
with TRACER.span("LLM"):
time.sleep(0.02)
out = f"回答:{q[:10]}"
return {"q": q, "prompt": prompt, "out": out}
def run_and_record(samples, path="replay_data.jsonl"):
with open(path, "w", encoding="utf-8") as f:
for s in samples:
res = run_once(s)
f.write(json.dumps(res, ensure_ascii=False) + "\n")
TRACER.export_json("trace_debug.json")
print(f"[Replay] 已记录到 {path}")
def replay(path="replay_data.jsonl", idx=0):
lines = open(path, encoding="utf-8").read().splitlines()
item = json.loads(lines[idx])
print("[重放样本]", item)
if __name__ == "__main__":
SAMPLES = [{"q": "LangChain 的可观测性如何实现?"}, {"q": "如何评估 RAG 的质量?"}]
run_and_record(SAMPLES)
replay(idx=1)
输出:
css
[Tracer] 导出到 trace_debug.json
[Replay] 已记录到 replay_data.jsonl
[重放样本] {'q': '如何评估 RAG 的质量?', 'prompt': '解释:如何评估 RAG 的质量?', 'out': '回答:如何评估 RAG 的'}
四、Evaluation:离线数据集 + 规则指标 + 轻量"裁判"策略
评估既可以是 规则匹配 (比如 EM/ROUGE),也可以是 模型裁判 (Judge LLM)。下面给出 可运行的离线评估脚手架(不用任何外部依赖)。
4.1 准备小数据集与被测系统
python
# demo_eval.py
from typing import List, Dict
DATASET = [
{"input": "2+2", "expected": "4"},
{"input": "3*5", "expected": "15"},
{"input": "10-7", "expected": "3"},
]
def system_under_test(x: str) -> str:
# 非安全,仅示意:不要在真实环境使用 eval
try:
return str(eval(x))
except Exception:
return "ERR"
4.2 指标:EM + 容错比较(忽略空格、全角/半角差异)
python
def normalize(s: str) -> str:
return s.strip().replace(" ", "").replace("+", "+").replace("-", "-").replace("×", "*")
def exact_match(pred: str, gold: str) -> bool:
return normalize(pred) == normalize(gold)
def evaluate(ds: List[Dict[str, str]]):
rows = []
hit = 0
for ex in ds:
y = system_under_test(ex["input"])
ok = exact_match(y, ex["expected"])
hit += ok
rows.append({"input": ex["input"], "pred": y, "gold": ex["expected"], "ok": ok})
acc = hit / len(ds)
return acc, rows
if __name__ == "__main__":
acc, rows = evaluate(DATASET)
print("Accuracy:", acc)
for r in rows: print(r)
输出:
css
Accuracy: 1.0
{'input': '2+2', 'pred': '4', 'gold': '4', 'ok': True}
{'input': '3*5', 'pred': '15', 'gold': '15', 'ok': True}
{'input': '10-7', 'pred': '3', 'gold': '3', 'ok': True}
在真实工程里:
- 把
system_under_test
换成你的链/代理;- 将
rows
连同 trace 一起送入 LangSmith 数据集 + 评估任务;- 线上做 A/B + 离线回归。
五、AgentOps:预算守护、熔断/降级、超时/限流、安全/合规
AgentOps 核心是可控的运营:
- 预算守护:单请求/会话/用户/天,限制总 tokens 与成本;
- 熔断/降级:连续失败/高延迟时,切换到便宜模型或回退策略;
- 超时/限流:防雪崩;
- 输出合规:对返回进行正则 Pydantic 校验,检测 PII,打标或拒绝。
5.1 纯 Python 的 BudgetGuard / CircuitBreaker(可直接套在任意函数外)
python
# demo_agentops.py
import time, random
from typing import Callable
class BudgetGuard:
def __init__(self, max_calls: int, on_violate: Callable[[int], str] | None = None):
self.max_calls = max_calls
self.calls = 0
self.on_violate = on_violate or (lambda n: f"[Budget] 超过调用上限 {n} 次,已拒绝")
def wrap(self, fn: Callable[[str], str]) -> Callable[[str], str]:
def inner(s: str) -> str:
if self.calls >= self.max_calls:
return self.on_violate(self.max_calls)
self.calls += 1
return fn(s)
return inner
class CircuitBreaker:
def __init__(self, failure_threshold=3, reset_time=1.0):
self.failure_threshold = failure_threshold
self.reset_time = reset_time
self.fail_count = 0
self.open_until = 0.0
def wrap(self, fn: Callable[[str], str]) -> Callable[[str], str]:
def inner(s: str) -> str:
now = time.time()
if now < self.open_until:
return "[CB] 熔断中,返回降级结果"
try:
out = fn(s)
self.fail_count = 0
return out
except Exception:
self.fail_count += 1
if self.fail_count >= self.failure_threshold:
self.open_until = now + self.reset_time
return "[CB] 失败,已计数/可能进入熔断"
return inner
# 一个不稳定的"模型调用"
def flaky_model(x: str) -> str:
if random.random() < 0.4:
raise RuntimeError("随机失败")
return f"LLM出品:{x}"
if __name__ == "__main__":
budget = BudgetGuard(max_calls=5)
cb = CircuitBreaker(failure_threshold=2, reset_time=1.0)
guarded = budget.wrap(cb.wrap(flaky_model))
for i in range(10):
print(i, "->", guarded("你好"))
time.sleep(0.2)
输出:

替换
flaky_model
为你的链/代理入口即可在本地验证"预算 + 熔断"效果。生产中再映射到计费/监控系统,与 LangSmith 观测串联。
六、与 LangGraph 的协同:任务编排、持久化与重启
我们用一个精简图引擎示例,演示:
- 节点函数:同步执行;
- 状态对象:字典合并;
- 持久化:每步落盘到 JSON,模拟"检查点";
- 流式观测:事件回调打印。
说明:这不是 LangGraph 源码,仅用于本地理解图式编排的运行时形态。
python
# demo_graph_engine.py
import json, os, time
from typing import Callable, Dict, Any, List, Tuple
class MiniGraph:
def __init__(self):
self.nodes: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {}
self.edges: List[Tuple[str, str]] = []
self.start = None
self.sink = None
self.on_event = None # 回调:("node_end", node, state)
def add_node(self, name: str, fn: Callable[[Dict], Dict]):
self.nodes[name] = fn
def add_edge(self, a: str, b: str):
self.edges.append((a, b))
def set_entry(self, start: str, sink: str):
self.start, self.sink = start, sink
def _next_nodes(self, cur: str) -> List[str]:
return [b for a, b in self.edges if a == cur]
def invoke(self, init: Dict[str, Any], ckpt_path="graph_ckpt.json") -> Dict[str, Any]:
state = dict(init)
cur = self.start
history = []
while True:
out = self.nodes[cur](state)
state.update(out)
history.append({"node": cur, "out": out, "state": dict(state)})
if self.on_event:
self.on_event(("node_end", cur, dict(state)))
# 落盘(检查点)
json.dump({"history": history, "latest": state}, open(ckpt_path, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
if cur == self.sink:
break
nxt = self._next_nodes(cur)
if not nxt:
break
cur = nxt[0] # 简化:单一路径
return state
# --- 定义节点
def plan(state):
q = state["question"]
tool = "multiply" if any(ch.isdigit() for ch in q) and ("乘" in q or "*" in q) else "reverse"
return {"plan": tool, "log": f"[plan] 选择工具={tool}"}
def act(state):
tool = state["plan"]
if tool == "multiply":
import re
nums = list(map(int, re.findall(r"\d+", state["question"])))
ans = nums[0] * nums[1] if len(nums) >= 2 else None
return {"observation": ans, "log": state["log"] + f" | [act] 计算={ans}"}
else:
txt = "".join([c for c in state["question"] if c.isalpha()])
return {"observation": txt[::-1], "log": state["log"] + f" | [act] 反转={txt[::-1]}"}
def finish(state):
return {"final": f"答案:{state['observation']}", "log": state["log"] + " | [finish]"}
if __name__ == "__main__":
g = MiniGraph()
g.add_node("plan", plan)
g.add_node("act", act)
g.add_node("finish", finish)
g.add_edge("plan", "act")
g.add_edge("act", "finish")
g.set_entry("plan", "finish")
g.on_event = lambda e: print(f"[EVENT] {e[0]} @ {e[1]} -> keys={list(e[2].keys())}")
out = g.invoke({"question": "请计算 19 乘以 21"})
print("RESULT:", out["final"])
print("CKPT saved:", os.path.abspath("graph_ckpt.json"))
输出:

通过这个"小引擎",你可以理解节点/状态/边 如何在运行时交互、如何落盘恢复(下一步即可替换为 LangGraph 的 checkpointer
与真实并行/路由能力)。
七、平台化演进方向:统一编排与治理
综合来看,未来的 LLM 应用平台在几个方向继续演进:
- 全链路可观测:从 Prompt 到 Action/Tool,到外部系统调用,颗粒到 token 级;可追溯成本、时延、错误;
- 可验证与评估:线下数据集评估、线上 A/B、实时度量(准确性/相关性/覆盖/安全/漂移);
- AgentOps 与治理:配额、限流、Budget、熔断/降级、模型路由(性价比/时延/隐私域)、输出合规模型裁判;
- 图式工作流与持久化:LangGraph 把长流程拆解为可恢复的"状态 + 检查点 + 边",与 LangSmith/Serve 打通;
- 标准化与可移植:LCEL/Runnable 接口化、工具与数据连接器抽象,便于云/私有化/多模型混部。
🔚 小结
- 本文用 离线可跑 的示例,演示了可观测(Tracer) 、重放/调试 、离线评估 、AgentOps(预算/熔断) 、以及 图式编排(检查点) 的最小实现。
- 在真实项目中,把这些"骨架"替换为 LangSmith (追踪/评估)、LangGraph (编排/持久化)、LangServe (服务化),即可获得可视化、可审计、可治理的一体化平台能力。