第七章:生产环境 — 03 可观测性与监控#

生产环境中的 LLM 应用需要可观测性:日志、追踪、指标、告警

没有可观测性,你就是在黑盒中飞行。当系统出问题时,你不知道:

  • 是哪个请求失败了?

  • 延迟在什么时候开始变高?

  • 今天花了多少钱?

  • 有多少次因为限流而重试?

可观测性的三大支柱:

日志(Logs)  → 每次事件的详细记录(调试用)
指标(Metrics)→ 聚合数据(p99延迟、错误率、成本)
追踪(Traces)→ 一次请求的完整链路(分布式系统)

本章目标: 从零构建一套 LLM 应用的可观测性基础设施。

import json
import os
import time
import uuid
import random
from datetime import datetime, timedelta
from collections import defaultdict, deque
import litellm
litellm.drop_params = True
from dotenv import load_dotenv

load_dotenv()
MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini")
litellm.set_verbose = False

print(f"使用模型: {MODEL}")
print("可观测性工具包初始化完成")
print(f"当前时间: {datetime.now().isoformat()}")
# gpt-5/o系列不支持自定义temperature值,统一用安全wrapper
def _c(**kw):
    _m = kw.get('model', MODEL)
    if any(_m.startswith(p) for p in ('openai/gpt-5','openai/o1','openai/o3','openai/o4')):
        kw.pop('temperature', None)
    return litellm.completion(**kw)
使用模型: openai/gpt-5-mini
可观测性工具包初始化完成
当前时间: 2026-03-14T21:20:18.942874

Section 1:基础日志#

结构化 JSON 日志是机器可读的,便于后续查询分析。

class LLMLogger:
    """
    LLM 调用日志包装器
    
    每次 LLM 调用自动记录:
    - 时间戳、模型、请求内容
    - Token 用量(prompt + completion + total)
    - 延迟(毫秒)
    - 完成原因(finish_reason)
    - 错误信息(如有)
    """
    
    def __init__(self, log_file: str = None, print_logs: bool = True):
        self.log_file = log_file or "/tmp/llm_calls.jsonl"
        self.print_logs = print_logs
        self.logs = []
        
        # 打开日志文件
        self._file = open(self.log_file, "a", encoding="utf-8")
    
    def completion(self, model: str = None, messages: list = None,
                   request_id: str = None, **kwargs):
        """带日志的 litellm.completion 包装"""
        model = model or MODEL
        request_id = request_id or str(uuid.uuid4())[:8]
        
        start_time = time.time()
        error_info = None
        response = None
        
        try:
            response = _c(
                model=model,
                messages=messages,
                **kwargs
            )
            return response
        
        except Exception as e:
            error_info = {
                "type": type(e).__name__,
                "message": str(e)[:200]
            }
            raise
        
        finally:
            latency_ms = int((time.time() - start_time) * 1000)
            
            log_entry = {
                "timestamp": datetime.now().isoformat(),
                "request_id": request_id,
                "model": model,
                "prompt_tokens": response.usage.prompt_tokens if response else 0,
                "completion_tokens": response.usage.completion_tokens if response else 0,
                "total_tokens": response.usage.total_tokens if response else 0,
                "latency_ms": latency_ms,
                "finish_reason": response.choices[0].finish_reason if response else None,
                "error": error_info,
                "status": "success" if error_info is None else "error"
            }
            
            self.logs.append(log_entry)
            self._file.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
            self._file.flush()
            
            if self.print_logs:
                status_icon = "✅" if log_entry["status"] == "success" else "❌"
                print(f"{status_icon} [{log_entry['timestamp'][:19]}] "
                      f"req={request_id} model={model} "
                      f"tokens={log_entry['total_tokens']} "
                      f"latency={latency_ms}ms "
                      f"finish={log_entry['finish_reason']}")
    
    def summary(self):
        """打印调用摘要"""
        if not self.logs:
            print("无日志")
            return
        
        success_logs = [l for l in self.logs if l["status"] == "success"]
        total_tokens = sum(l["total_tokens"] for l in success_logs)
        avg_latency = sum(l["latency_ms"] for l in success_logs) / len(success_logs) if success_logs else 0
        
        print(f"\n日志摘要({len(self.logs)} 次调用):")
        print(f"  成功: {len(success_logs)}, 失败: {len(self.logs) - len(success_logs)}")
        print(f"  总 tokens: {total_tokens:,}")
        print(f"  平均延迟: {avg_latency:.0f}ms")
        print(f"  日志文件: {self.log_file}")
    
    def __del__(self):
        if hasattr(self, '_file') and self._file:
            self._file.close()


# 演示:记录 5 次 LLM 调用
logger = LLMLogger(print_logs=True)

test_queries = [
    "用一句话解释什么是 REST API",
    "Python 中如何读取文件?",
    "什么是 Docker 容器?",
    "解释 Git 的分支管理",
    "什么是微服务架构?",
]

print("记录 LLM 调用日志:")
print("-" * 80)

for i, query in enumerate(test_queries):
    logger.completion(
        model=MODEL,
        messages=[{"role": "user", "content": query}],
        request_id=f"req-{i+1:03d}",
        max_tokens=60,
        temperature=0
    )

logger.summary()
记录 LLM 调用日志:
--------------------------------------------------------------------------------
✅ [2026-03-14T21:20:20] req=req-001 model=openai/gpt-5-mini tokens=74 latency=1523ms finish=length
✅ [2026-03-14T21:20:21] req=req-002 model=openai/gpt-5-mini tokens=72 latency=1423ms finish=length
✅ [2026-03-14T21:20:23] req=req-003 model=openai/gpt-5-mini tokens=73 latency=1414ms finish=length
✅ [2026-03-14T21:20:24] req=req-004 model=openai/gpt-5-mini tokens=72 latency=1321ms finish=length
✅ [2026-03-14T21:20:26] req=req-005 model=openai/gpt-5-mini tokens=73 latency=1526ms finish=length

日志摘要(5 次调用):
  成功: 5, 失败: 0
  总 tokens: 364
  平均延迟: 1441ms
  日志文件: /tmp/llm_calls.jsonl

Section 2:litellm Callbacks#

litellm 原生支持 callback 机制,无需修改每个调用点,全局自动捕获所有调用。

from litellm.integrations.custom_logger import CustomLogger

class LLMCallbackLogger(CustomLogger):
    """
    全局 LLM 回调日志器
    
    注册后,所有 litellm.completion 调用都会自动触发,
    无需在每个调用点手动记录。
    """
    
    def __init__(self):
        super().__init__()
        self.call_log = []
        self.error_log = []
    
    def log_success_event(self, kwargs, response_obj, start_time, end_time):
        """每次成功调用后触发"""
        latency_ms = int((end_time - start_time).total_seconds() * 1000)
        
        entry = {
            "timestamp": end_time.isoformat(),
            "model": kwargs.get("model"),
            "prompt_tokens": response_obj.usage.prompt_tokens,
            "completion_tokens": response_obj.usage.completion_tokens,
            "latency_ms": latency_ms,
            "finish_reason": response_obj.choices[0].finish_reason,
            "status": "success"
        }
        self.call_log.append(entry)
        print(f"  [Callback] ✅ {entry['model']} | {entry['total_tokens'] if 'total_tokens' in entry else entry['prompt_tokens'] + entry['completion_tokens']}tok | {latency_ms}ms")
    
    def log_failure_event(self, kwargs, response_obj, start_time, end_time):
        """每次失败调用后触发"""
        entry = {
            "timestamp": end_time.isoformat() if end_time else datetime.now().isoformat(),
            "model": kwargs.get("model"),
            "error": str(response_obj)[:200] if response_obj else "Unknown error",
            "status": "error"
        }
        self.error_log.append(entry)
        print(f"  [Callback] ❌ {entry['model']} | Error: {entry['error'][:50]}")
    
    def get_stats(self):
        """获取统计信息"""
        if not self.call_log:
            return {}
        
        latencies = [e["latency_ms"] for e in self.call_log]
        tokens = [e["prompt_tokens"] + e["completion_tokens"] for e in self.call_log]
        
        return {
            "total_calls": len(self.call_log),
            "error_count": len(self.error_log),
            "avg_latency_ms": sum(latencies) / len(latencies),
            "total_tokens": sum(tokens),
        }


# 注册全局 callback
callback_logger = LLMCallbackLogger()
litellm.callbacks = [callback_logger]

print("全局 Callback 已注册,以下调用无需手动记录:")
print("-" * 60)

# 普通调用,callback 自动触发
for query in ["解释 Python 装饰器", "什么是 TCP/IP?", "Git rebase 和 merge 的区别"]:
    response = _c(
        model=MODEL,
        messages=[{"role": "user", "content": query}],
        max_tokens=50,
        temperature=0
    )

# 清除 callback(避免影响后续 section)
litellm.callbacks = []

stats = callback_logger.get_stats()
print(f"\nCallback 统计:")
print(f"  总调用次数: {stats.get('total_calls', 0)}")
print(f"  错误次数:   {stats.get('error_count', 0)}")
print(f"  总tokens:   {stats.get('total_tokens', 0):,}")
print(f"  平均延迟:   {stats.get('avg_latency_ms', 0):.0f}ms")
全局 Callback 已注册,以下调用无需手动记录:
------------------------------------------------------------
  [Callback] ✅ gpt-5-mini | 62tok | 1998ms
  [Callback] ✅ gpt-5-mini | 61tok | 1241ms
Callback 统计:  [Callback] ✅ gpt-5-mini | 63tok | 1505ms

  总调用次数: 2
  错误次数:   0
  总tokens:   123
  平均延迟:   1620ms

Section 3:延迟追踪#

P50/P90/P99 延迟是衡量 LLM 服务性能的核心指标。P99 = 99% 的请求都在这个时间内完成。

class LatencyTracker:
    """
    延迟追踪器:记录和分析 LLM 调用延迟
    """
    
    def __init__(self, window_size: int = 1000):
        """window_size: 保留最近 N 次记录"""
        self.latencies = deque(maxlen=window_size)
        self.by_model = defaultdict(list)
        self.slow_threshold_ms = 2000  # 超过 2s 视为慢请求
    
    def record(self, latency_ms: float, model: str = "unknown",
                request_id: str = ""):
        """记录一次延迟"""
        entry = {
            "latency_ms": latency_ms,
            "model": model,
            "request_id": request_id,
            "timestamp": datetime.now().isoformat(),
            "is_slow": latency_ms > self.slow_threshold_ms
        }
        self.latencies.append(entry)
        self.by_model[model].append(latency_ms)
    
    def percentile(self, p: float) -> float:
        """计算第 p 百分位延迟"""
        if not self.latencies:
            return 0.0
        sorted_latencies = sorted(e["latency_ms"] for e in self.latencies)
        idx = int(len(sorted_latencies) * p / 100)
        idx = min(idx, len(sorted_latencies) - 1)
        return sorted_latencies[idx]
    
    def stats(self) -> dict:
        """延迟统计"""
        if not self.latencies:
            return {}
        
        all_latencies = [e["latency_ms"] for e in self.latencies]
        slow_count = sum(1 for e in self.latencies if e["is_slow"])
        
        return {
            "count": len(self.latencies),
            "min_ms": min(all_latencies),
            "max_ms": max(all_latencies),
            "avg_ms": sum(all_latencies) / len(all_latencies),
            "p50_ms": self.percentile(50),
            "p90_ms": self.percentile(90),
            "p99_ms": self.percentile(99),
            "slow_count": slow_count,
            "slow_rate": slow_count / len(self.latencies)
        }
    
    def print_histogram(self, bins: int = 10):
        """打印 ASCII 延迟直方图"""
        if not self.latencies:
            print("无数据")
            return
        
        all_latencies = sorted(e["latency_ms"] for e in self.latencies)
        min_val, max_val = all_latencies[0], all_latencies[-1]
        bin_size = (max_val - min_val) / bins if max_val > min_val else 1
        
        bucket_counts = defaultdict(int)
        for lat in all_latencies:
            bucket = int((lat - min_val) / bin_size)
            bucket = min(bucket, bins - 1)
            bucket_counts[bucket] += 1
        
        max_count = max(bucket_counts.values()) if bucket_counts else 1
        bar_width = 30
        
        print("延迟分布直方图:")
        print("-" * 60)
        for b in range(bins):
            range_start = int(min_val + b * bin_size)
            range_end = int(min_val + (b + 1) * bin_size)
            count = bucket_counts.get(b, 0)
            bar_len = int(count / max_count * bar_width)
            bar = "█" * bar_len
            print(f"  {range_start:>5}-{range_end:<5}ms │{bar:<{bar_width}} {count}")


# 演示:记录 20 次实际 LLM 调用的延迟
tracker = LatencyTracker()

print("实测延迟追踪(20次调用):")
print("-" * 50)

queries = [f"用10字解释:{topic}" for topic in [
    "云计算", "机器学习", "API", "微服务", "容器化",
    "DevOps", "CI/CD", "Kubernetes", "Serverless", "边缘计算",
    "区块链", "大数据", "数据湖", "流处理", "批处理",
    "负载均衡", "服务网格", "API网关", "事件驱动", "函数计算"
]]

for i, query in enumerate(queries):
    start = time.time()
    _c(
        model=MODEL,
        messages=[{"role": "user", "content": query}],
        max_tokens=20,
        temperature=0
    )
    latency = (time.time() - start) * 1000
    tracker.record(latency, model=MODEL, request_id=f"req-{i+1:02d}")
    print(f"  [{i+1:02d}] {latency:>7.0f}ms")

print("\n" + "=" * 60)
s = tracker.stats()
print(f"延迟统计:")
print(f"  P50:  {s['p50_ms']:>7.0f}ms")
print(f"  P90:  {s['p90_ms']:>7.0f}ms")
print(f"  P99:  {s['p99_ms']:>7.0f}ms")
print(f"  平均: {s['avg_ms']:>7.0f}ms")
print(f"  最大: {s['max_ms']:>7.0f}ms")
print(f"  慢请求(>{tracker.slow_threshold_ms}ms): {s['slow_count']} ({s['slow_rate']:.0%})")

print()
tracker.print_histogram()
实测延迟追踪(20次调用):
--------------------------------------------------
  [01]    1555ms  [Callback] ✅ gpt-5-mini | 33tok | 1554ms
  [02]    1100ms  [Callback] ✅ gpt-5-mini | 33tok | 1099ms
  [03]     799ms  [Callback] ✅ gpt-5-mini | 32tok | 797ms
  [04]     747ms  [Callback] ✅ gpt-5-mini | 33tok | 746ms
  [05]     902ms  [Callback] ✅ gpt-5-mini | 34tok | 901ms
  [06]     876ms  [Callback] ✅ gpt-5-mini | 33tok | 875ms
  [07]     749ms  [Callback] ✅ gpt-5-mini | 33tok | 748ms
  [08]     781ms  [Callback] ✅ gpt-5-mini | 33tok | 780ms
  [09]     851ms  [Callback] ✅ gpt-5-mini | 33tok | 850ms
  [10]     968ms  [Callback] ✅ gpt-5-mini | 34tok | 966ms
  [11]     742ms  [Callback] ✅ gpt-5-mini | 34tok | 741ms
  [12]     757ms  [Callback] ✅ gpt-5-mini | 33tok | 756ms
  [13]     774ms  [Callback] ✅ gpt-5-mini | 33tok | 772ms
  [14]     886ms  [Callback] ✅ gpt-5-mini | 33tok | 885ms
  [15]     756ms  [Callback] ✅ gpt-5-mini | 33tok | 755ms
  [16]     752ms  [Callback] ✅ gpt-5-mini | 35tok | 751ms
  [17]    1231ms  [Callback] ✅ gpt-5-mini | 34tok | 1229ms
  [18]     867ms  [Callback] ✅ gpt-5-mini | 34tok | 867ms
  [19]     751ms  [Callback] ✅ gpt-5-mini | 34tok | 749ms
  [20]     823ms  [Callback] ✅ gpt-5-mini | 33tok | 822ms


============================================================
延迟统计:
  P50:      823ms
  P90:     1231ms
  P99:     1555ms
  平均:     883ms
  最大:    1555ms
  慢请求(>2000ms): 0 (0%)

延迟分布直方图:
------------------------------------------------------------
    741-823  ms │██████████████████████████████ 10
    823-904  ms │██████████████████             6
    904-985  ms │███                            1
    985-1067 ms │                               0
   1067-1148 ms │███                            1
   1148-1229 ms │                               0
   1229-1311 ms │███                            1
   1311-1392 ms │                               0
   1392-1473 ms │                               0
   1473-1555 ms │███                            1
# matplotlib 延迟可视化
try:
    import matplotlib.pyplot as plt
    import matplotlib
    matplotlib.rcParams['font.family'] = ['Arial Unicode MS', 'DejaVu Sans']
    
    all_latencies = [e["latency_ms"] for e in tracker.latencies]
    
    fig, axes = plt.subplots(1, 2, figsize=(14, 4))
    
    # 图1:时序延迟
    ax1 = axes[0]
    ax1.plot(all_latencies, marker="o", markersize=4, color="#3498db", linewidth=1.5)
    ax1.axhline(y=tracker.stats()["p50_ms"], color="green", linestyle="--",
                label=f"P50={tracker.stats()['p50_ms']:.0f}ms")
    ax1.axhline(y=tracker.stats()["p90_ms"], color="orange", linestyle="--",
                label=f"P90={tracker.stats()['p90_ms']:.0f}ms")
    ax1.axhline(y=tracker.stats()["p99_ms"], color="red", linestyle="--",
                label=f"P99={tracker.stats()['p99_ms']:.0f}ms")
    ax1.set_xlabel("请求序号")
    ax1.set_ylabel("延迟 (ms)")
    ax1.set_title("LLM 调用延迟时序图")
    ax1.legend()
    ax1.grid(True, alpha=0.3)
    
    # 图2:分布直方图
    ax2 = axes[1]
    ax2.hist(all_latencies, bins=10, color="#2ecc71", edgecolor="white", linewidth=0.5)
    ax2.axvline(x=tracker.stats()["p50_ms"], color="green", linestyle="--", label="P50")
    ax2.axvline(x=tracker.stats()["p90_ms"], color="orange", linestyle="--", label="P90")
    ax2.axvline(x=tracker.stats()["p99_ms"], color="red", linestyle="--", label="P99")
    ax2.set_xlabel("延迟 (ms)")
    ax2.set_ylabel("次数")
    ax2.set_title("延迟分布直方图")
    ax2.legend()
    ax2.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig("/tmp/llm_latency.png", dpi=150, bbox_inches="tight")
    plt.show()
    print("图表已保存到 /tmp/llm_latency.png")

except ImportError:
    print("matplotlib 未安装,跳过可视化(pip install matplotlib)")
../../_images/02e56ac9d95b8c7e43884c97551775e50f8902ec27a07d3a92ec2b359df4da00.png
图表已保存到 /tmp/llm_latency.png

Section 4:成本追踪#

按模型、按天追踪 LLM 成本,设置预算告警。

# 模型价格表
MODEL_PRICES_PER_TOKEN = {
    "gpt-4o": {"input": 2.50 / 1e6, "output": 10.00 / 1e6},
    "gpt-4o-mini": {"input": 0.15 / 1e6, "output": 0.60 / 1e6},
    "claude-3-5-sonnet": {"input": 3.00 / 1e6, "output": 15.00 / 1e6},
    "claude-3-haiku": {"input": 0.25 / 1e6, "output": 1.25 / 1e6},
}


class CostTracker:
    """
    LLM 成本追踪器
    - 按模型分组统计
    - 按天统计
    - 超预算告警
    """
    
    def __init__(self, daily_budget_usd: float = 100.0):
        self.daily_budget = daily_budget_usd
        self.records = []
        self.alerts = []
    
    def _estimate_cost(self, model: str, prompt_tokens: int, completion_tokens: int) -> float:
        """估算单次调用成本"""
        # 标准化模型名称
        model_key = None
        for key in MODEL_PRICES_PER_TOKEN:
            if key in model:
                model_key = key
                break
        
        if model_key:
            prices = MODEL_PRICES_PER_TOKEN[model_key]
            return prompt_tokens * prices["input"] + completion_tokens * prices["output"]
        
        # 使用 litellm 获取成本
        try:
            return litellm.completion_cost(
                model=model,
                prompt_tokens=prompt_tokens,
                completion_tokens=completion_tokens
            )
        except:
            return 0.0
    
    def record(self, model: str, prompt_tokens: int, completion_tokens: int,
                timestamp: datetime = None):
        """记录一次调用成本"""
        timestamp = timestamp or datetime.now()
        cost = self._estimate_cost(model, prompt_tokens, completion_tokens)
        
        self.records.append({
            "timestamp": timestamp,
            "date": timestamp.date().isoformat(),
            "model": model,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "cost_usd": cost
        })
        
        # 检查今日预算
        today_cost = self.daily_cost(timestamp.date().isoformat())
        if today_cost > self.daily_budget:
            alert = f"⚠️ 预算告警:今日成本 ${today_cost:.2f} 超过预算 ${self.daily_budget:.2f}!"
            if alert not in self.alerts:
                self.alerts.append(alert)
                print(alert)
        elif today_cost > self.daily_budget * 0.8:
            print(f"🔔 预警:今日成本 ${today_cost:.4f},已达预算的 {today_cost/self.daily_budget:.0%}")
    
    def daily_cost(self, date_str: str = None) -> float:
        """获取某天的总成本"""
        date_str = date_str or datetime.now().date().isoformat()
        return sum(r["cost_usd"] for r in self.records if r["date"] == date_str)
    
    def daily_cost_report(self) -> dict:
        """生成每日成本报告"""
        report = defaultdict(lambda: {"cost": 0.0, "calls": 0, "tokens": 0})
        
        for r in self.records:
            date = r["date"]
            report[date]["cost"] += r["cost_usd"]
            report[date]["calls"] += 1
            report[date]["tokens"] += r["prompt_tokens"] + r["completion_tokens"]
        
        return dict(report)
    
    def model_cost_report(self) -> dict:
        """按模型统计成本"""
        report = defaultdict(lambda: {"cost": 0.0, "calls": 0})
        for r in self.records:
            report[r["model"]]["cost"] += r["cost_usd"]
            report[r["model"]]["calls"] += 1
        return dict(report)


# 模拟 7 天的使用数据
cost_tracker = CostTracker(daily_budget_usd=0.10)  # 演示用低预算

print("模拟 7 天的 LLM 成本数据")
print("=" * 60)

# 生成模拟数据
base_date = datetime.now() - timedelta(days=6)
models_to_simulate = ["gpt-4o", "gpt-4o-mini", "gpt-4o-mini"]

for day in range(7):
    day_date = base_date + timedelta(days=day)
    calls_per_day = random.randint(50, 200)
    
    for _ in range(calls_per_day):
        model = random.choice(models_to_simulate)
        prompt_tokens = random.randint(100, 500)
        completion_tokens = random.randint(50, 200)
        call_time = day_date + timedelta(hours=random.randint(0, 23))
        cost_tracker.record(model, prompt_tokens, completion_tokens, call_time)

# 生成报告
daily_report = cost_tracker.daily_cost_report()

print(f"\n每日成本报告:")
print(f"{'日期':<12} {'成本':>10} {'调用次数':>10} {'总tokens':>12} {'状态'}")
print("-" * 60)

for date in sorted(daily_report.keys()):
    data = daily_report[date]
    cost = data["cost"]
    budget_ratio = cost / cost_tracker.daily_budget
    if budget_ratio > 1.0:
        status = "❌ 超预算"
    elif budget_ratio > 0.8:
        status = "⚠️ 临近上限"
    else:
        status = "✅ 正常"
    print(f"{date:<12} ${cost:>8.4f} {data['calls']:>10,} {data['tokens']:>12,} {status}")

total_cost = sum(d["cost"] for d in daily_report.values())
print("-" * 60)
print(f"{'7天总计':<12} ${total_cost:>8.4f}")

print("\n按模型成本分布:")
model_report = cost_tracker.model_cost_report()
for model, data in sorted(model_report.items(), key=lambda x: -x[1]["cost"]):
    ratio = data["cost"] / total_cost
    bar = "█" * int(ratio * 30)
    print(f"  {model:<20} ${data['cost']:>6.4f} ({ratio:.0%}) {bar}")
模拟 7 天的 LLM 成本数据
============================================================
🔔 预警:今日成本 $0.0818,已达预算的 82%
🔔 预警:今日成本 $0.0836,已达预算的 84%
🔔 预警:今日成本 $0.0856,已达预算的 86%
🔔 预警:今日成本 $0.0869,已达预算的 87%
🔔 预警:今日成本 $0.0892,已达预算的 89%
🔔 预警:今日成本 $0.0921,已达预算的 92%
🔔 预警:今日成本 $0.0939,已达预算的 94%
🔔 预警:今日成本 $0.0961,已达预算的 96%
🔔 预警:今日成本 $0.0972,已达预算的 97%
🔔 预警:今日成本 $0.0996,已达预算的 100%
⚠️ 预算告警:今日成本 $0.10 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.11 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.12 超过预算 $0.10!
🔔 预警:今日成本 $0.0813,已达预算的 81%
🔔 预警:今日成本 $0.0831,已达预算的 83%
🔔 预警:今日成本 $0.0851,已达预算的 85%
🔔 预警:今日成本 $0.0868,已达预算的 87%
🔔 预警:今日成本 $0.0895,已达预算的 90%
🔔 预警:今日成本 $0.0918,已达预算的 92%
🔔 预警:今日成本 $0.0928,已达预算的 93%
🔔 预警:今日成本 $0.0938,已达预算的 94%
🔔 预警:今日成本 $0.0954,已达预算的 95%
🔔 预警:今日成本 $0.0972,已达预算的 97%
🔔 预警:今日成本 $0.0991,已达预算的 99%
⚠️ 预算告警:今日成本 $0.13 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.14 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.15 超过预算 $0.10!
🔔 预警:今日成本 $0.0802,已达预算的 80%
🔔 预警:今日成本 $0.0828,已达预算的 83%
🔔 预警:今日成本 $0.0854,已达预算的 85%
🔔 预警:今日成本 $0.0866,已达预算的 87%
🔔 预警:今日成本 $0.0891,已达预算的 89%
🔔 预警:今日成本 $0.0900,已达预算的 90%
🔔 预警:今日成本 $0.0922,已达预算的 92%
🔔 预警:今日成本 $0.0953,已达预算的 95%
🔔 预警:今日成本 $0.0974,已达预算的 97%
🔔 预警:今日成本 $0.0984,已达预算的 98%
⚠️ 预算告警:今日成本 $0.16 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.17 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.18 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.19 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.20 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.21 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.22 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.23 超过预算 $0.10!
🔔 预警:今日成本 $0.0814,已达预算的 81%
🔔 预警:今日成本 $0.0838,已达预算的 84%
⚠️ 预算告警:今日成本 $0.24 超过预算 $0.10!
🔔 预警:今日成本 $0.0858,已达预算的 86%
🔔 预警:今日成本 $0.0875,已达预算的 88%
🔔 预警:今日成本 $0.0889,已达预算的 89%
🔔 预警:今日成本 $0.0907,已达预算的 91%
🔔 预警:今日成本 $0.0937,已达预算的 94%
🔔 预警:今日成本 $0.0950,已达预算的 95%
🔔 预警:今日成本 $0.0960,已达预算的 96%
🔔 预警:今日成本 $0.0969,已达预算的 97%
🔔 预警:今日成本 $0.0993,已达预算的 99%
🔔 预警:今日成本 $0.0829,已达预算的 83%
🔔 预警:今日成本 $0.0860,已达预算的 86%
🔔 预警:今日成本 $0.0878,已达预算的 88%
🔔 预警:今日成本 $0.0887,已达预算的 89%
🔔 预警:今日成本 $0.0914,已达预算的 91%
🔔 预警:今日成本 $0.0946,已达预算的 95%
🔔 预警:今日成本 $0.0961,已达预算的 96%
🔔 预警:今日成本 $0.0985,已达预算的 99%
⚠️ 预算告警:今日成本 $0.25 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.26 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.27 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.28 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.29 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.30 超过预算 $0.10!
⚠️ 预算告警:今日成本 $0.31 超过预算 $0.10!
🔔 预警:今日成本 $0.0814,已达预算的 81%
🔔 预警:今日成本 $0.0823,已达预算的 82%
🔔 预警:今日成本 $0.0844,已达预算的 84%
🔔 预警:今日成本 $0.0865,已达预算的 86%
🔔 预警:今日成本 $0.0880,已达预算的 88%
🔔 预警:今日成本 $0.0902,已达预算的 90%
🔔 预警:今日成本 $0.0928,已达预算的 93%
🔔 预警:今日成本 $0.0949,已达预算的 95%
🔔 预警:今日成本 $0.0964,已达预算的 96%
🔔 预警:今日成本 $0.0988,已达预算的 99%
⚠️ 预算告警:今日成本 $0.32 超过预算 $0.10!
🔔 预警:今日成本 $0.0813,已达预算的 81%
🔔 预警:今日成本 $0.0837,已达预算的 84%
🔔 预警:今日成本 $0.0850,已达预算的 85%
🔔 预警:今日成本 $0.0867,已达预算的 87%
🔔 预警:今日成本 $0.0893,已达预算的 89%
🔔 预警:今日成本 $0.0903,已达预算的 90%
🔔 预警:今日成本 $0.0925,已达预算的 92%
🔔 预警:今日成本 $0.0954,已达预算的 95%
🔔 预警:今日成本 $0.0968,已达预算的 97%
🔔 预警:今日成本 $0.0990,已达预算的 99%

每日成本报告:
日期                   成本       调用次数      总tokens 状态
------------------------------------------------------------
2026-03-08   $  0.0147          7        3,237 ✅ 正常
2026-03-09   $  0.1314         64       28,417 ❌ 超预算
2026-03-10   $  0.1764         87       37,252 ❌ 超预算
2026-03-11   $  0.2444        121       52,095 ❌ 超预算
2026-03-12   $  0.2158        108       45,372 ❌ 超预算
2026-03-13   $  0.3225        154       67,730 ❌ 超预算
2026-03-14   $  0.2878        142       60,269 ❌ 超预算
2026-03-15   $  0.1394         71       30,037 ❌ 超预算
------------------------------------------------------------
7天总计         $  1.5323

按模型成本分布:
  gpt-4o-mini          $1.0332 (67%) ████████████████████
  gpt-4o               $0.4991 (33%) █████████

Section 5:错误监控#

自动追踪错误类型、实现指数退避重试。

class ErrorMonitor:
    """
    LLM 错误监控器
    - 追踪错误类型分布
    - 计算错误率
    - 指数退避自动重试
    """
    
    # 已知错误类型映射
    ERROR_CATEGORIES = {
        "RateLimitError": "rate_limit",
        "ContextWindowExceededError": "context_length",
        "Timeout": "timeout",
        "APIConnectionError": "api_error",
        "AuthenticationError": "auth_error",
        "ServiceUnavailableError": "service_unavailable",
    }
    
    def __init__(self):
        self.error_log = []
        self.success_count = 0
        self.error_counts = defaultdict(int)
    
    def record_success(self):
        self.success_count += 1
    
    def record_error(self, error: Exception):
        error_type = type(error).__name__
        category = self.ERROR_CATEGORIES.get(error_type, "unknown")
        
        entry = {
            "timestamp": datetime.now().isoformat(),
            "error_type": error_type,
            "category": category,
            "message": str(error)[:200]
        }
        self.error_log.append(entry)
        self.error_counts[category] += 1
        return entry
    
    @property
    def error_rate(self) -> float:
        total = self.success_count + sum(self.error_counts.values())
        return sum(self.error_counts.values()) / total if total > 0 else 0.0
    
    def error_report(self):
        total_errors = sum(self.error_counts.values())
        print(f"错误报告:")
        print(f"  成功: {self.success_count}, 失败: {total_errors}")
        print(f"  错误率: {self.error_rate:.2%}")
        if self.error_counts:
            print("  错误类型分布:")
            for category, count in sorted(self.error_counts.items(), key=lambda x: -x[1]):
                pct = count / total_errors
                print(f"    {category:<25}: {count:>3} ({pct:.0%})")


def completion_with_retry(messages: list, model: str = MODEL,
                           max_retries: int = 3, monitor: ErrorMonitor = None,
                           **kwargs):
    """
    带指数退避重试的 LLM 调用
    
    重试策略:
    - 第1次失败:等待 1s
    - 第2次失败:等待 2s
    - 第3次失败:等待 4s
    - 之后放弃
    """
    for attempt in range(max_retries + 1):
        try:
            response = _c(
                model=model,
                messages=messages,
                **kwargs
            )
            if monitor:
                monitor.record_success()
            return response
        
        except litellm.RateLimitError as e:
            if monitor:
                monitor.record_error(e)
            if attempt < max_retries:
                wait_time = (2 ** attempt)  # 指数退避
                print(f"  ⚠️ 限流错误,{wait_time}s 后重试(第 {attempt+1}/{max_retries} 次)")
                time.sleep(wait_time)
            else:
                print(f"  ❌ 已达最大重试次数,放弃")
                raise
        
        except litellm.ContextWindowExceededError as e:
            if monitor:
                monitor.record_error(e)
            print(f"  ❌ 上下文窗口超限,无法重试")
            raise  # 这类错误重试无意义
        
        except Exception as e:
            if monitor:
                monitor.record_error(e)
            if attempt < max_retries:
                wait_time = (2 ** attempt)
                print(f"  ⚠️ 错误: {type(e).__name__}{wait_time}s 后重试")
                time.sleep(wait_time)
            else:
                raise


# 演示正常调用(含重试逻辑)
monitor = ErrorMonitor()

print("带重试的 LLM 调用演示")
print("=" * 60)

for i, query in enumerate(["解释 Python GIL", "什么是事件循环?", "解释协程"]):
    print(f"\n[{i+1}] 查询: {query}")
    response = completion_with_retry(
        messages=[{"role": "user", "content": query}],
        model=MODEL,
        monitor=monitor,
        max_tokens=60,
        temperature=0
    )
    print(f"  ✅ 回答: {response.choices[0].message.content[:80]}...")

print()
monitor.error_report()

# 展示重试逻辑(模拟错误)
print("\n指数退避重试策略:")
print("  第1次失败 → 等待 2^0 = 1s")
print("  第2次失败 → 等待 2^1 = 2s")
print("  第3次失败 → 等待 2^2 = 4s")
print("  第4次失败 → 放弃,抛出异常")
print("  总等待时间上限: 7s(之后还可以配合 jitter 加随机延迟)")
带重试的 LLM 调用演示
============================================================

[1] 查询: 解释 Python GIL
  ✅ 回答: ...  [Callback] ✅ gpt-5-mini | 70tok | 1204ms


[2] 查询: 什么是事件循环?
  ✅ 回答: ...  [Callback] ✅ gpt-5-mini | 71tok | 1378ms


[3] 查询: 解释协程
  ✅ 回答: ...  [Callback] ✅ gpt-5-mini | 69tok | 1562ms


错误报告:
  成功: 3, 失败: 0
  错误率: 0.00%

指数退避重试策略:
  第1次失败 → 等待 2^0 = 1s
  第2次失败 → 等待 2^1 = 2s
  第3次失败 → 等待 2^2 = 4s
  第4次失败 → 放弃,抛出异常
  总等待时间上限: 7s(之后还可以配合 jitter 加随机延迟)

Section 6:结构化追踪(Trace)#

对于复杂的 LLM 管道(如 RAG),需要追踪每个步骤的耗时和状态。

import uuid

class Span:
    """表示一个追踪步骤"""
    
    def __init__(self, name: str, trace_id: str, parent_id: str = None):
        self.span_id = str(uuid.uuid4())[:8]
        self.trace_id = trace_id
        self.parent_id = parent_id
        self.name = name
        self.start_time = time.time()
        self.end_time = None
        self.metadata = {}
        self.status = "running"
    
    def finish(self, status: str = "success", **metadata):
        self.end_time = time.time()
        self.status = status
        self.metadata.update(metadata)
        return self
    
    @property
    def duration_ms(self) -> float:
        end = self.end_time or time.time()
        return (end - self.start_time) * 1000


class RequestTrace:
    """
    请求追踪:追踪一次完整请求的所有步骤
    适用于 RAG、Agent 等多步骤流水线
    """
    
    def __init__(self, request_name: str):
        self.trace_id = str(uuid.uuid4())[:8]
        self.request_name = request_name
        self.spans = []
        self.start_time = time.time()
    
    def start_span(self, name: str, parent_id: str = None) -> Span:
        """开始一个新的追踪步骤"""
        span = Span(name, self.trace_id, parent_id)
        self.spans.append(span)
        return span
    
    def print_trace(self):
        """打印完整追踪树"""
        total_ms = (time.time() - self.start_time) * 1000
        
        print(f"\n追踪: {self.request_name}")
        print(f"Trace ID: {self.trace_id}")
        print(f"总耗时: {total_ms:.0f}ms")
        print("-" * 70)
        
        # 按时间顺序打印 spans
        for span in self.spans:
            indent = "  " if span.parent_id else ""
            status_icon = "✅" if span.status == "success" else "❌"
            duration = f"{span.duration_ms:.0f}ms"
            
            pct = span.duration_ms / total_ms * 100
            bar = "█" * int(pct / 4)
            
            print(f"{indent}{status_icon} [{span.span_id}] {span.name:<30} "
                  f"{duration:>8} ({pct:.0f}%) {bar}")
            
            for key, val in span.metadata.items():
                val_str = str(val)[:60]
                print(f"{indent}     {key}: {val_str}")


def simulate_rag_pipeline(query: str, trace: RequestTrace) -> str:
    """
    模拟 RAG 管道:展示完整追踪
    步骤:query_analysis → retrieval → reranking → generation
    """
    
    # Step 1: 查询分析
    span_analyze = trace.start_span("query_analysis")
    response = _c(
        model=MODEL,
        messages=[{"role": "user",
                   "content": f"提取以下查询的关键词,输出3个词,用逗号分隔:{query}"}],
        max_tokens=20, temperature=0
    )
    keywords = response.choices[0].message.content
    span_analyze.finish("success", keywords=keywords, tokens=response.usage.total_tokens)
    
    # Step 2: 文档检索(模拟)
    span_retrieve = trace.start_span("vector_retrieval")
    time.sleep(0.05)  # 模拟向量检索耗时
    retrieved_docs = [
        "文档1:LLM 延迟受 token 数量影响...",
        "文档2:减少延迟的方法包括流式输出...",
        "文档3:模型量化可以降低推理延迟...",
    ]
    span_retrieve.finish("success", docs_retrieved=len(retrieved_docs), top_k=3)
    
    # Step 3: 重排序(模拟)
    span_rerank = trace.start_span("reranking", parent_id=span_retrieve.span_id)
    time.sleep(0.02)  # 模拟重排序
    span_rerank.finish("success", docs_after_rerank=2, reranker="bge-reranker-v2")
    
    # Step 4: 生成回答
    span_generate = trace.start_span("answer_generation")
    context = "\n".join(retrieved_docs[:2])
    gen_response = _c(
        model=MODEL,
        messages=[
            {"role": "system", "content": "根据以下文档回答问题,简洁回答。"},
            {"role": "user", "content": f"文档:{context}\n\n问题:{query}"}
        ],
        max_tokens=100, temperature=0
    )
    answer = gen_response.choices[0].message.content
    span_generate.finish(
        "success",
        prompt_tokens=gen_response.usage.prompt_tokens,
        completion_tokens=gen_response.usage.completion_tokens,
        finish_reason=gen_response.choices[0].finish_reason
    )
    
    return answer


# 运行完整 RAG 追踪
query = "如何降低大模型的推理延迟?"
trace = RequestTrace(f"RAG查询: {query[:20]}...")

print("=" * 70)
print("RAG 管道完整追踪")
print("=" * 70)
print(f"用户查询: {query}")

answer = simulate_rag_pipeline(query, trace)

trace.print_trace()
print(f"\n最终回答:")
print(f"  {answer}")
======================================================================
RAG 管道完整追踪
======================================================================
用户查询: 如何降低大模型的推理延迟?
  [Callback] ✅ gpt-5-mini | 54tok | 908ms
追踪: RAG查询: 如何降低大模型的推理延迟?...  [Callback] ✅ gpt-5-mini | 163tok | 1868ms

Trace ID: 72614578
总耗时: 2859ms
----------------------------------------------------------------------
✅ [92dbdf00] query_analysis                    909ms (32%) ███████
     keywords: 
     tokens: 54
✅ [790280d1] vector_retrieval                   51ms (2%) 
     docs_retrieved: 3
     top_k: 3
  ✅ [a09becc2] reranking                          29ms (1%) 
       docs_after_rerank: 2
       reranker: bge-reranker-v2
✅ [aafc235d] answer_generation                1869ms (65%) ████████████████
     prompt_tokens: 63
     completion_tokens: 100
     finish_reason: length

最终回答:
  

Section 7:仪表板概念#

生产环境 LLM 监控仪表板应该展示哪些内容?

┌─────────────────────────────────────────────────────────────────────┐
│                    LLM 生产监控仪表板                                │
│                    更新时间: 2024-01-15 14:30:22                     │
├─────────────────┬──────────────────┬─────────────────┬─────────────┤
│ 今日调用        │ 今日成本         │ 平均延迟        │ 错误率      │
│ 1,234,567       │ $127.45          │ 856ms           │ 0.12%       │
│ ↑ 5.2% vs昨日  │ ↑ 8.1% vs昨日   │ → 正常          │ ✅ 健康     │
├─────────────────┴──────────────────┴─────────────────┴─────────────┤
│ 延迟趋势(最近 24 小时)                                             │
│  2000ms│          .*                                                 │
│  1500ms│        ..  ..                                               │
│  1000ms│  ......      ......                    P99                  │
│   500ms│──────────────────────────────────────  P90                  │
│         0    4    8    12   16   20   24         P50                  │
├─────────────────────────────────────────────────────────────────────┤
│ 成本趋势(最近 7 天)          │ 错误类型分布                        │
│ $200│         ██               │ rate_limit     ████ 45%            │
│ $150│  ██  ██ ██               │ timeout        ██   22%            │
│ $100│  ████████ ██ ██          │ context_length █    18%            │
│  $50│──────────────────────    │ api_error      █    15%            │
│      Mon Tue Wed Thu Fri Sat Sun│                                    │
├─────────────────────────────────────────────────────────────────────┤
│ 模型使用分布                    │ 缓存命中率趋势                      │
│ gpt-4o-mini  ████████████  78% │ 60%│      ...........              │
│ gpt-4o       ████          18% │ 40%│  ....                         │
│ claude-haiku ██             4% │ 20%│..                             │
│                                │     Mon  Tue  Wed  Thu  Fri        │
├─────────────────────────────────────────────────────────────────────┤
│ 告警记录                                                             │
│ [14:25] ⚠️  P99 延迟超过 2000ms,持续 5 分钟                         │
│ [12:10] 💰  今日成本超过预算的 80%($100 预算,已用 $89.23)          │
│ [09:45] ❌  限流错误率 > 5%,触发告警                                │
└─────────────────────────────────────────────────────────────────────┘

推荐的监控工具栈#

工具

用途

特点

Langfuse

LLM 专用可观测性

追踪、评估、数据集管理

LangSmith

LangChain 官方

与 LangChain 深度集成

Prometheus + Grafana

通用指标监控

强大灵活,需自建

OpenTelemetry

标准追踪协议

厂商无关,可导出到任何后端

Datadog LLM Observability

企业级

贵但功能全面

# 用 matplotlib 绘制简单的监控仪表板
try:
    import matplotlib.pyplot as plt
    import matplotlib.gridspec as gridspec
    import matplotlib
    matplotlib.rcParams['font.family'] = ['Arial Unicode MS', 'DejaVu Sans']
    
    # 生成模拟数据
    hours = list(range(24))
    latency_p50 = [random.randint(400, 600) for _ in hours]
    latency_p90 = [random.randint(800, 1200) for _ in hours]
    latency_p99 = [random.randint(1500, 2500) for _ in hours]
    
    days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
    daily_costs = [89.2, 112.4, 98.7, 125.3, 134.1, 67.8, 45.2]
    
    error_types = ["rate_limit", "timeout", "context_length", "api_error"]
    error_counts = [45, 22, 18, 15]
    
    model_names = ["gpt-4o-mini", "gpt-4o", "claude-haiku"]
    model_usage = [78, 18, 4]
    
    fig = plt.figure(figsize=(15, 10))
    fig.suptitle("LLM 生产监控仪表板", fontsize=14, fontweight="bold")
    gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.4, wspace=0.3)
    
    # 图1:延迟趋势
    ax1 = fig.add_subplot(gs[0, :])
    ax1.fill_between(hours, latency_p50, alpha=0.3, color="green", label="P50")
    ax1.fill_between(hours, latency_p90, alpha=0.2, color="orange", label="P90")
    ax1.fill_between(hours, latency_p99, alpha=0.15, color="red", label="P99")
    ax1.plot(hours, latency_p50, color="green", linewidth=1.5)
    ax1.plot(hours, latency_p90, color="orange", linewidth=1.5)
    ax1.plot(hours, latency_p99, color="red", linewidth=1.5)
    ax1.axhline(y=2000, color="red", linestyle="--", alpha=0.7, label="SLA 上限 2000ms")
    ax1.set_xlabel("小时")
    ax1.set_ylabel("延迟 (ms)")
    ax1.set_title("过去 24 小时延迟分布")
    ax1.legend(loc="upper right")
    ax1.grid(True, alpha=0.3)
    
    # 图2:每日成本
    ax2 = fig.add_subplot(gs[1, :2])
    colors_cost = ["#e74c3c" if c > 120 else "#f39c12" if c > 100 else "#2ecc71" for c in daily_costs]
    ax2.bar(days, daily_costs, color=colors_cost, edgecolor="white")
    ax2.axhline(y=100, color="red", linestyle="--", alpha=0.7, label="每日预算 $100")
    ax2.set_ylabel("成本 (USD)")
    ax2.set_title("每日成本趋势")
    ax2.legend()
    for bar, cost in zip(ax2.patches, daily_costs):
        ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
                 f"${cost:.0f}", ha="center", fontsize=8)
    
    # 图3:错误类型分布
    ax3 = fig.add_subplot(gs[1, 2])
    wedges, texts, autotexts = ax3.pie(
        error_counts,
        labels=[e.replace("_", "\n") for e in error_types],
        autopct="%1.0f%%",
        colors=["#e74c3c", "#f39c12", "#3498db", "#95a5a6"],
        startangle=90
    )
    ax3.set_title("错误类型分布")
    
    # 图4:模型使用分布
    ax4 = fig.add_subplot(gs[2, :2])
    bars = ax4.barh(model_names, model_usage, color=["#2ecc71", "#3498db", "#9b59b6"])
    ax4.set_xlabel("使用占比 (%)")
    ax4.set_title("模型使用分布")
    for bar, val in zip(bars, model_usage):
        ax4.text(val + 0.5, bar.get_y() + bar.get_height()/2,
                 f"{val}%", va="center", fontsize=10)
    
    # 图5:关键指标卡片
    ax5 = fig.add_subplot(gs[2, 2])
    ax5.axis("off")
    metrics_text = (
        "  今日关键指标\n"
        "━━━━━━━━━━━━━━━━\n"
        "  调用量   1.23M\n"
        "  成本     $127.45\n"
        "  P99延迟  1850ms\n"
        "  错误率   0.12%\n"
        "  缓存命中 43%\n"
        "━━━━━━━━━━━━━━━━\n"
        "  状态: ✅ 健康"
    )
    ax5.text(0.05, 0.95, metrics_text, transform=ax5.transAxes,
             fontfamily="monospace", fontsize=9,
             verticalalignment="top",
             bbox=dict(boxstyle="round", facecolor="lightgray", alpha=0.5))
    
    plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
    plt.show()
    print("仪表板已保存到 /tmp/llm_dashboard.png")

except ImportError:
    print("matplotlib 未安装,跳过仪表板可视化")
    print("安装:pip install matplotlib")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 20170 (\N{CJK UNIFIED IDEOGRAPH-4ECA}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 26085 (\N{CJK UNIFIED IDEOGRAPH-65E5}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 20851 (\N{CJK UNIFIED IDEOGRAPH-5173}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 38190 (\N{CJK UNIFIED IDEOGRAPH-952E}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 25351 (\N{CJK UNIFIED IDEOGRAPH-6307}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 26631 (\N{CJK UNIFIED IDEOGRAPH-6807}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 35843 (\N{CJK UNIFIED IDEOGRAPH-8C03}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 29992 (\N{CJK UNIFIED IDEOGRAPH-7528}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 37327 (\N{CJK UNIFIED IDEOGRAPH-91CF}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 25104 (\N{CJK UNIFIED IDEOGRAPH-6210}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 26412 (\N{CJK UNIFIED IDEOGRAPH-672C}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 24310 (\N{CJK UNIFIED IDEOGRAPH-5EF6}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 36831 (\N{CJK UNIFIED IDEOGRAPH-8FDF}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 38169 (\N{CJK UNIFIED IDEOGRAPH-9519}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 35823 (\N{CJK UNIFIED IDEOGRAPH-8BEF}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 32531 (\N{CJK UNIFIED IDEOGRAPH-7F13}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 23384 (\N{CJK UNIFIED IDEOGRAPH-5B58}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 21629 (\N{CJK UNIFIED IDEOGRAPH-547D}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 20013 (\N{CJK UNIFIED IDEOGRAPH-4E2D}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 29366 (\N{CJK UNIFIED IDEOGRAPH-72B6}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 24577 (\N{CJK UNIFIED IDEOGRAPH-6001}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 9989 (\N{WHITE HEAVY CHECK MARK}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 20581 (\N{CJK UNIFIED IDEOGRAPH-5065}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_90582/298179279.py:93: UserWarning: Glyph 24247 (\N{CJK UNIFIED IDEOGRAPH-5EB7}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("/tmp/llm_dashboard.png", dpi=150, bbox_inches="tight")
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 20170 (\N{CJK UNIFIED IDEOGRAPH-4ECA}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 26085 (\N{CJK UNIFIED IDEOGRAPH-65E5}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 20851 (\N{CJK UNIFIED IDEOGRAPH-5173}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 38190 (\N{CJK UNIFIED IDEOGRAPH-952E}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 25351 (\N{CJK UNIFIED IDEOGRAPH-6307}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 26631 (\N{CJK UNIFIED IDEOGRAPH-6807}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 35843 (\N{CJK UNIFIED IDEOGRAPH-8C03}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 29992 (\N{CJK UNIFIED IDEOGRAPH-7528}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 37327 (\N{CJK UNIFIED IDEOGRAPH-91CF}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 25104 (\N{CJK UNIFIED IDEOGRAPH-6210}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 26412 (\N{CJK UNIFIED IDEOGRAPH-672C}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24310 (\N{CJK UNIFIED IDEOGRAPH-5EF6}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 36831 (\N{CJK UNIFIED IDEOGRAPH-8FDF}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 38169 (\N{CJK UNIFIED IDEOGRAPH-9519}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 35823 (\N{CJK UNIFIED IDEOGRAPH-8BEF}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 29575 (\N{CJK UNIFIED IDEOGRAPH-7387}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 32531 (\N{CJK UNIFIED IDEOGRAPH-7F13}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 23384 (\N{CJK UNIFIED IDEOGRAPH-5B58}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 21629 (\N{CJK UNIFIED IDEOGRAPH-547D}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 20013 (\N{CJK UNIFIED IDEOGRAPH-4E2D}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 29366 (\N{CJK UNIFIED IDEOGRAPH-72B6}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24577 (\N{CJK UNIFIED IDEOGRAPH-6001}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 9989 (\N{WHITE HEAVY CHECK MARK}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 20581 (\N{CJK UNIFIED IDEOGRAPH-5065}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
/Users/li.luo/dev/git/learn_llm/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 24247 (\N{CJK UNIFIED IDEOGRAPH-5EB7}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
../../_images/4113c394e4166136eb3bbca56e79454b4b9b776b3ee162eb72b44c29db09eebf.png
仪表板已保存到 /tmp/llm_dashboard.png

Section 8:集成到现有系统#

# 将所有组件集成为一个完整的 ObservabilityStack

class LLMObservabilityStack:
    """
    完整的 LLM 可观测性栈
    集成:日志 + 延迟追踪 + 成本追踪 + 错误监控
    
    使用方式:
        obs = LLMObservabilityStack(daily_budget=100)
        response = obs.completion(model=MODEL, messages=[...])
        obs.print_dashboard()
    """
    
    def __init__(self, daily_budget_usd: float = 100.0,
                  log_file: str = "/tmp/llm_obs.jsonl"):
        self.latency_tracker = LatencyTracker()
        self.cost_tracker = CostTracker(daily_budget_usd)
        self.error_monitor = ErrorMonitor()
        self.log_file = log_file
        self._call_count = 0
    
    def completion(self, model: str = None, messages: list = None,
                    request_id: str = None, **kwargs):
        """带完整可观测性的 completion"""
        model = model or MODEL
        request_id = request_id or str(uuid.uuid4())[:8]
        self._call_count += 1
        
        start = time.time()
        
        try:
            response = completion_with_retry(
                messages=messages,
                model=model,
                monitor=self.error_monitor,
                **kwargs
            )
            
            latency_ms = (time.time() - start) * 1000
            self.latency_tracker.record(latency_ms, model, request_id)
            self.cost_tracker.record(
                model,
                response.usage.prompt_tokens,
                response.usage.completion_tokens
            )
            
            # 写结构化日志
            log_entry = {
                "ts": datetime.now().isoformat(),
                "req_id": request_id,
                "model": model,
                "latency_ms": int(latency_ms),
                "prompt_tokens": response.usage.prompt_tokens,
                "completion_tokens": response.usage.completion_tokens,
                "status": "ok"
            }
            with open(self.log_file, "a") as f:
                f.write(json.dumps(log_entry) + "\n")
            
            return response
        
        except Exception as e:
            latency_ms = (time.time() - start) * 1000
            log_entry = {
                "ts": datetime.now().isoformat(),
                "req_id": request_id,
                "model": model,
                "latency_ms": int(latency_ms),
                "error": type(e).__name__,
                "status": "error"
            }
            with open(self.log_file, "a") as f:
                f.write(json.dumps(log_entry) + "\n")
            raise
    
    def print_dashboard(self):
        """打印即时仪表板"""
        lat = self.latency_tracker.stats()
        today_cost = self.cost_tracker.daily_cost()
        
        print("\n" + "═" * 55)
        print("  LLM 可观测性仪表板")
        print("═" * 55)
        print(f"  总调用次数:   {self._call_count}")
        print(f"  成功:         {self.error_monitor.success_count}")
        print(f"  错误:         {sum(self.error_monitor.error_counts.values())}")
        print(f"  错误率:       {self.error_monitor.error_rate:.2%}")
        print("  ─────────────── 延迟 ────────────────")
        if lat:
            print(f"  P50:  {lat['p50_ms']:>8.0f}ms")
            print(f"  P90:  {lat['p90_ms']:>8.0f}ms")
            print(f"  P99:  {lat['p99_ms']:>8.0f}ms")
        print("  ─────────────── 成本 ────────────────")
        print(f"  今日成本:     ${today_cost:.4f}")
        print(f"  预算上限:     ${self.cost_tracker.daily_budget:.2f}")
        print(f"  预算消耗:     {today_cost/self.cost_tracker.daily_budget:.0%}")
        print("═" * 55)


# 演示完整使用
obs = LLMObservabilityStack(daily_budget_usd=0.50)

print("使用集成可观测性栈进行 5 次调用:")
queries = ["什么是 Python", "解释 REST API", "什么是 Docker",
           "解释 Git 版本控制", "什么是 CI/CD"]

for i, q in enumerate(queries):
    response = obs.completion(
        model=MODEL,
        messages=[{"role": "user", "content": q}],
        max_tokens=40,
        temperature=0
    )
    print(f"  [{i+1}] {q[:30]:<30}{response.choices[0].message.content[:40]}...")

obs.print_dashboard()
使用集成可观测性栈进行 5 次调用:
  [Callback] ✅ gpt-5-mini | 49tok | 1066ms
  [1] 什么是 Python                     → ...
  [Callback] ✅ gpt-5-mini | 49tok | 1127ms
  [2] 解释 REST API                    → ...
  [Callback] ✅ gpt-5-mini | 49tok | 984ms
  [3] 什么是 Docker                     → ...
  [Callback] ✅ gpt-5-mini | 51tok | 1042ms
  [4] 解释 Git 版本控制                    → ...
  [Callback] ✅ gpt-5-mini | 50tok | 988ms
  [5] 什么是 CI/CD                      → ...

═══════════════════════════════════════════════════════
  LLM 可观测性仪表板
═══════════════════════════════════════════════════════
  总调用次数:   5
  成功:         5
  错误:         0
  错误率:       0.00%
  ─────────────── 延迟 ────────────────
  P50:      1043ms
  P90:      1128ms
  P99:      1128ms
  ─────────────── 成本 ────────────────
  今日成本:     $0.0000
  预算上限:     $0.50
  预算消耗:     0%
═══════════════════════════════════════════════════════

总结:生产环境可观测性检查清单#

上线前必须具备#

  • [ ] 结构化日志:每次 LLM 调用记录 JSON 日志(时间戳、模型、tokens、延迟、状态)

  • [ ] 错误处理:所有 LLM 调用有 try-catch,区分可重试/不可重试错误

  • [ ] 指数退避重试:限流错误自动重试,避免雪崩

  • [ ] 超时设置:每个调用设置合理的 timeout(推荐 30-60s)

上线后持续监控#

  • [ ] 延迟告警:P99 > 阈值时告警(通常 3-5s)

  • [ ] 错误率告警:错误率 > 1% 时告警

  • [ ] 成本告警:当日成本 > 预算 80% 时预警,> 100% 时告警

  • [ ] 每日成本报告:自动发送到 Slack/邮件

进阶监控#

  • [ ] 分布式追踪:RAG/Agent 等多步骤流水线的完整 trace

  • [ ] 缓存命中率:监控语义缓存效果

  • [ ] 模型使用分布:各模型的调用比例

  • [ ] 质量评估:定期采样调用,运行 LLM-as-Judge 评估

推荐工具#

场景

工具

说明

LLM 专用追踪

Langfuse

开源,支持自托管

通用指标

Prometheus + Grafana

工业标准

日志存储

Elasticsearch / ClickHouse

高效查询大量日志

告警

PagerDuty / OpsGenie

智能告警路由

APM

Datadog / Dynatrace

全栈可观测性