一、开篇:为什么 LLM 应用的可观测性比传统服务更重要?
2026 年,LLM 应用在生产环境中面临三个传统服务没有的监控难题:
| 问题 | 传统服务 | LLM 应用 |
|---|---|---|
| 成本波动大 | 硬件成本相对固定 | 一次回答可能花 0.001到1,相差千倍 |
| 质量不可预测 | 输出确定性强 | 同样的输入可能输出不同质量的结果 |
| 故障模式复杂 | 通常是 5xx 错误 | 可能是"回答了但不准确"、"幻觉"、"拒绝回答" |
| 依赖链长 | 通常 2-3 层 | LLM → Embedding → 向量库 → Reranker → 缓存,5-6 层 |
LLM 应用的可观测性 = 传统监控(延迟/错误/吞吐)+ 质量监控 + 成本监控 + 行为监控
二、监控体系总览
┌─────────────────────────────────────────────────────────────────┐
│ LLM 可观测性体系 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Metrics │ │ Tracing │ │ Logging │ │
│ │ (Prometheus)│ │ (Jaeger) │ │ (Loki) │ │
│ ├─────────────┤ ├─────────────┤ ├─────────────┤ │
│ │ • QPS │ │ • 完整链路 │ │ • 请求日志 │ │
│ │ • 延迟 P50 │ │ • Span 明细 │ │ • 错误日志 │ │
│ │ • 延迟 P99 │ │ • 依赖分析 │ │ • 审计日志 │ │
│ │ • Token 用量│ │ • 瓶颈定位 │ │ • 成本日志 │ │
│ │ • 成本/小时 │ │ • 异常追踪 │ │ • 质量采样 │ │
│ │ • 缓存命中 │ │ │ │ │ │
│ │ • 模型分布 │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 告警规则 │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 成本突增 │ │ 延迟飙高 │ │ 错误率 │ │ 召回率低 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 可视化面板(Grafana) │ │
│ │ 实时大盘 | 成本看板 | 质量看板 | 链路追踪 | 告警中心 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
三、核心数据模型
// observability/models.go
package observability
import (
"context"
"time"
)
// LLMSpan LLM 调用 Span
type LLMSpan struct {
TraceID string `json:"trace_id"`
SpanID string `json:"span_id"`
ParentSpanID string `json:"parent_span_id"`
Name string `json:"name"` // 操作名称
Kind string `json:"kind"` // internal, client, server
// 时间
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
DurationMs int64 `json:"duration_ms"`
// LLM 特定属性
ModelName string `json:"model_name"`
Provider string `json:"provider"`
RequestType string `json:"request_type"` // chat, embedding, rerank
// Token 用量
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
// 成本
CostUSD float64 `json:"cost_usd"`
// 状态
Status string `json:"status"` // ok, error, timeout
ErrorMessage string `json:"error_message,omitempty"`
// 元数据
UserID string `json:"user_id"`
SessionID string `json:"session_id"`
Tags map[string]string `json:"tags"`
// 缓存
FromCache bool `json:"from_cache"`
CacheHit bool `json:"cache_hit"`
}
// LLMMetric LLM 指标
type LLMMetric struct {
Timestamp time.Time `json:"timestamp"`
// 计数指标
RequestCount int64 `json:"request_count"`
ErrorCount int64 `json:"error_count"`
TimeoutCount int64 `json:"timeout_count"`
// 延迟指标
P50LatencyMs float64 `json:"p50_latency_ms"`
P95LatencyMs float64 `json:"p95_latency_ms"`
P99LatencyMs float64 `json:"p99_latency_ms"`
// Token 指标
TotalPromptTokens int64 `json:"total_prompt_tokens"`
TotalCompletionTokens int64 `json:"total_completion_tokens"`
TotalTokens int64 `json:"total_tokens"`
// 成本指标
TotalCostUSD float64 `json:"total_cost_usd"`
// 缓存指标
CacheHitCount int64 `json:"cache_hit_count"`
CacheMissCount int64 `json:"cache_miss_count"`
CacheHitRate float64 `json:"cache_hit_rate"`
// 模型分布
ModelDistribution map[string]int64 `json:"model_distribution"`
// 重排序指标
RerankGain float64 `json:"rerank_gain"` // 重排序带来的精度提升
}
// AlertRule 告警规则
type AlertRule struct {
Name string `json:"name"`
Description string `json:"description"`
Metric string `json:"metric"`
Condition string `json:"condition"` // gt, lt, eq, change_rate
Threshold float64 `json:"threshold"`
Duration time.Duration `json:"duration"` // 持续多久触发
Severity string `json:"severity"` // critical, warning, info
Channels []string `json:"channels"` // email, slack, wechat
}
四、OpenTelemetry 埋点 SDK
// observability/tracer.go
package observability
import (
"context"
"fmt"
"runtime"
"sync"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// LLMTracer LLM 专用 Tracer
type LLMTracer struct {
tracer trace.Tracer
metrics *MetricsCollector
logger *LogCollector
config TracerConfig
}
type TracerConfig struct {
ServiceName string `json:"service_name"`
Environment string `json:"environment"` // dev, staging, prod
SampleRate float64 `json:"sample_rate"` // 采样率 0.0-1.0
EnableDetailed bool `json:"enable_detailed"` // 是否记录详细属性
CostAlertThreshold float64 `json:"cost_alert_threshold"` // 单次调用成本告警阈值
}
func NewLLMTracer(config TracerConfig) *LLMTracer {
return &LLMTracer{
tracer: otel.Tracer("llm-tracer"),
metrics: NewMetricsCollector(config.ServiceName),
logger: NewLogCollector(config.ServiceName, config.Environment),
config: config,
}
}
// StartLLMSpan 开始一个 LLM 调用 Span
func (t *LLMTracer) StartLLMSpan(ctx context.Context, operation string, attrs ...attribute.KeyValue) (context.Context, *LLMSpan) {
ctx, span := t.tracer.Start(ctx, operation,
trace.WithAttributes(attrs...),
trace.WithSpanKind(trace.SpanKindClient),
)
llmSpan := &LLMSpan{
TraceID: span.SpanContext().TraceID().String(),
SpanID: span.SpanContext().SpanID().String(),
Name: operation,
StartTime: time.Now(),
Tags: make(map[string]string),
}
return ctx, llmSpan
}
// EndLLMSpan 结束一个 LLM 调用 Span
func (t *LLMTracer) EndLLMSpan(span *LLMSpan) {
span.EndTime = time.Now()
span.DurationMs = time.Since(span.StartTime).Milliseconds()
// 记录指标
t.metrics.RecordLLMCall(span)
// 记录日志
t.logger.LogLLMCall(span)
// 检查成本告警
if span.CostUSD > t.config.CostAlertThreshold {
t.logger.Warn("High cost LLM call",
"cost", span.CostUSD,
"model", span.ModelName,
"trace_id", span.TraceID,
)
}
}
// RecordTokenUsage 记录 Token 用量
func (t *LLMTracer) RecordTokenUsage(span *LLMSpan, promptTokens, completionTokens int) {
span.PromptTokens = promptTokens
span.CompletionTokens = completionTokens
span.TotalTokens = promptTokens + completionTokens
// 估算成本(简化版)
rates := map[string][2]float64{
"deepseek-chat": {0.014, 0.028},
"gpt-4o-mini": {0.015, 0.060},
"claude-3-haiku": {0.025, 0.125},
}
if rate, ok := rates[span.ModelName]; ok {
span.CostUSD = (float64(promptTokens)/1000.0)*rate[0] +
(float64(completionTokens)/1000.0)*rate[1]
}
}
// RecordError 记录错误
func (t *LLMTracer) RecordError(span *LLMSpan, err error) {
span.Status = "error"
span.ErrorMessage = err.Error()
}
// shouldSample 判断是否采样
func (t *LLMTracer) shouldSample() bool {
// 生产环境按采样率,开发环境全量
if t.config.Environment == "prod" {
return fastRand() < t.config.SampleRate
}
return true
}
// fastRand 快速随机数(简化版)
func fastRand() float64 {
return float64(time.Now().UnixNano()%10000) / 10000.0
}
// ========== 指标收集器 ==========
// MetricsCollector 指标收集器
type MetricsCollector struct {
serviceName string
counters map[string]*Counter
histograms map[string]*Histogram
gauges map[string]*Gauge
mu sync.RWMutex
}
func NewMetricsCollector(serviceName string) *MetricsCollector {
return &MetricsCollector{
serviceName: serviceName,
counters: make(map[string]*Counter),
histograms: make(map[string]*Histogram),
gauges: make(map[string]*Gauge),
}
}
// RecordLLMCall 记录 LLM 调用指标
func (mc *MetricsCollector) RecordLLMCall(span *LLMSpan) {
mc.mu.Lock()
defer mc.mu.Unlock()
// 请求计数
mc.getCounter("llm.requests.total").Add(1)
mc.getCounter(fmt.Sprintf("llm.requests.model.%s", span.ModelName)).Add(1)
if span.Status == "error" {
mc.getCounter("llm.errors.total").Add(1)
}
// Token 计数
mc.getCounter("llm.tokens.prompt").Add(int64(span.PromptTokens))
mc.getCounter("llm.tokens.completion").Add(int64(span.CompletionTokens))
mc.getCounter("llm.tokens.total").Add(int64(span.TotalTokens))
// 成本
mc.getCounter("llm.cost.usd").Add(int64(span.CostUSD * 1000000)) // 微美元
// 延迟直方图
mc.getHistogram("llm.latency.ms").Observe(float64(span.DurationMs))
// 缓存
if span.FromCache {
mc.getCounter("llm.cache.hit").Add(1)
} else {
mc.getCounter("llm.cache.miss").Add(1)
}
}
func (mc *MetricsCollector) getCounter(name string) *Counter {
if c, ok := mc.counters[name]; ok {
return c
}
c := &Counter{Name: name}
mc.counters[name] = c
return c
}
func (mc *MetricsCollector) getHistogram(name string) *Histogram {
if h, ok := mc.histograms[name]; ok {
return h
}
h := &Histogram{Name: name, Buckets: []float64{100, 300, 500, 1000, 2000, 5000, 10000}}
mc.histograms[name] = h
return h
}
// Export 导出指标(供 Prometheus 拉取)
func (mc *MetricsCollector) Export() string {
mc.mu.RLock()
defer mc.mu.RUnlock()
output := ""
for _, c := range mc.counters {
output += c.Format()
}
for _, h := range mc.histograms {
output += h.Format()
}
return output
}
// Counter 计数器
type Counter struct {
Name string
Value int64
}
func (c *Counter) Add(v int64) {
c.Value += v
}
func (c *Counter) Format() string {
return fmt.Sprintf("# HELP %s LLM metric counter\n# TYPE %s counter\n%s %d\n",
c.Name, c.Name, c.Name, c.Value)
}
// Histogram 直方图
type Histogram struct {
Name string
Buckets []float64
Counts []int64
Sum float64
Count int64
}
func (h *Histogram) Observe(v float64) {
h.Sum += v
h.Count++
for i, b := range h.Buckets {
if v <= b {
if len(h.Counts) <= i {
h.Counts = append(h.Counts, make([]int64, i+1)...)
}
h.Counts[i]++
break
}
}
}
func (h *Histogram) Format() string {
output := fmt.Sprintf("# HELP %s LLM metric histogram\n# TYPE %s histogram\n", h.Name, h.Name)
for i, b := range h.Buckets {
le := "+Inf"
if i < len(h.Buckets) {
le = fmt.Sprintf("%.0f", b)
}
count := int64(0)
if i < len(h.Counts) {
count = h.Counts[i]
}
output += fmt.Sprintf("%s_bucket{le=\"%s\"} %d\n", h.Name, le, count)
}
output += fmt.Sprintf("%s_sum %f\n%s_count %d\n", h.Name, h.Sum, h.Name, h.Count)
return output
}
// Gauge 仪表盘
type Gauge struct {
Name string
Value float64
}
// ========== 日志收集器 ==========
// LogCollector 日志收集器
type LogCollector struct {
serviceName string
environment string
}
func NewLogCollector(serviceName, environment string) *LogCollector {
return &LogCollector{
serviceName: serviceName,
environment: environment,
}
}
func (lc *LogCollector) LogLLMCall(span *LLMSpan) {
// 生产环境:写入 Loki / Elasticsearch
level := "INFO"
if span.Status == "error" {
level = "ERROR"
}
logEntry := fmt.Sprintf("[%s] [%s] model=%s duration=%dms tokens=%d cost=$%.4f status=%s trace=%s",
level,
lc.serviceName,
span.ModelName,
span.DurationMs,
span.TotalTokens,
span.CostUSD,
span.Status,
span.TraceID,
)
fmt.Println(logEntry)
}
func (lc *LogCollector) Warn(msg string, keysAndValues ...interface{}) {
args := ""
for i := 0; i < len(keysAndValues); i += 2 {
if i+1 < len(keysAndValues) {
args += fmt.Sprintf(" %v=%v", keysAndValues[i], keysAndValues[i+1])
}
}
fmt.Printf("[WARN] [%s] %s%s\n", lc.serviceName, msg, args)
}
五、RAG Pipeline 全链路追踪
// observability/rag_tracer.go
package observability
import (
"context"
"fmt"
"time"
)
// RAGTracer RAG 链路追踪器
type RAGTracer struct {
tracer *LLMTracer
}
func NewRAGTracer(config TracerConfig) *RAGTracer {
return &RAGTracer{
tracer: NewLLMTracer(config),
}
}
// TraceRAGPipeline 追踪完整的 RAG 流水线
func (rt *RAGTracer) TraceRAGPipeline(ctx context.Context, query string, pipelineFn func(context.Context) (*RAGTraceResult, error)) (*RAGTraceResult, error) {
ctx, rootSpan := rt.tracer.StartLLMSpan(ctx, "rag.pipeline",
attribute.String("query", truncateStr(query, 100)),
attribute.String("service", rt.tracer.config.ServiceName),
)
rootSpan.Kind = "server"
rootSpan.RequestType = "rag_pipeline"
start := time.Now()
result, err := pipelineFn(ctx)
duration := time.Since(start)
if err != nil {
rt.tracer.RecordError(rootSpan, err)
}
rootSpan.DurationMs = duration.Milliseconds()
if result != nil {
rootSpan.TotalTokens = result.TotalTokens
rootSpan.CostUSD = result.TotalCost
rootSpan.Tags["retrieved_docs"] = fmt.Sprintf("%d", result.RetrievedDocs)
rootSpan.Tags["used_docs"] = fmt.Sprintf("%d", result.UsedDocs)
rootSpan.Tags["cache_hit"] = fmt.Sprintf("%v", result.FromCache)
}
rt.tracer.EndLLMSpan(rootSpan)
return result, err
}
// TraceLLMCall 追踪 LLM 调用
func (rt *RAGTracer) TraceLLMCall(ctx context.Context, model string, callFn func(context.Context) (promptTokens, completionTokens int, err error)) (int, int, error) {
ctx, span := rt.tracer.StartLLMSpan(ctx, fmt.Sprintf("llm.%s", model),
attribute.String("model", model),
)
span.ModelName = model
span.RequestType = "chat"
promptTokens, completionTokens, err := callFn(ctx)
rt.tracer.RecordTokenUsage(span, promptTokens, completionTokens)
if err != nil {
rt.tracer.RecordError(span, err)
}
rt.tracer.EndLLMSpan(span)
return promptTokens, completionTokens, err
}
// TraceEmbedding 追踪 Embedding 调用
func (rt *RAGTracer) TraceEmbedding(ctx context.Context, model string, textLen int, callFn func(context.Context) error) error {
ctx, span := rt.tracer.StartLLMSpan(ctx, fmt.Sprintf("embedding.%s", model),
attribute.String("model", model),
attribute.Int("text_length", textLen),
)
span.ModelName = model
span.RequestType = "embedding"
err := callFn(ctx)
if err != nil {
rt.tracer.RecordError(span, err)
}
rt.tracer.EndLLMSpan(span)
return err
}
// TraceVectorSearch 追踪向量检索
func (rt *RAGTracer) TraceVectorSearch(ctx context.Context, indexName string, topK int, callFn func(context.Context) (int, error)) (int, error) {
ctx, span := rt.tracer.StartLLMSpan(ctx, fmt.Sprintf("vector_search.%s", indexName),
attribute.String("index", indexName),
attribute.Int("top_k", topK),
)
span.RequestType = "vector_search"
resultCount, err := callFn(ctx)
span.Tags["result_count"] = fmt.Sprintf("%d", resultCount)
if err != nil {
rt.tracer.RecordError(span, err)
}
rt.tracer.EndLLMSpan(span)
return resultCount, err
}
// TraceRerank 追踪重排序
func (rt *RAGTracer) TraceRerank(ctx context.Context, candidateCount int, callFn func(context.Context) (selectedCount int, gain float64, err error)) (int, float64, error) {
ctx, span := rt.tracer.StartLLMSpan(ctx, "rerank",
attribute.Int("candidates", candidateCount),
)
span.RequestType = "rerank"
selectedCount, gain, err := callFn(ctx)
span.Tags["selected"] = fmt.Sprintf("%d", selectedCount)
span.Tags["gain"] = fmt.Sprintf("%.2f", gain)
if err != nil {
rt.tracer.RecordError(span, err)
}
rt.tracer.EndLLMSpan(span)
return selectedCount, gain, err
}
// RAGTraceResult RAG 追踪结果
type RAGTraceResult struct {
TotalTokens int
TotalCost float64
RetrievedDocs int
UsedDocs int
FromCache bool
DurationMs int64
}
func truncateStr(s string, maxLen int) string {
runes := []rune(s)
if len(runes) > maxLen {
return string(runes[:maxLen]) + "..."
}
return s
}
六、Prometheus 指标暴露
// observability/exporter.go
package observability
import (
"fmt"
"net/http"
"sync"
)
// MetricsExporter Prometheus 指标导出器
type MetricsExporter struct {
collectors map[string]*MetricsCollector
mu sync.RWMutex
}
var globalExporter *MetricsExporter
var once sync.Once
func GetGlobalExporter() *MetricsExporter {
once.Do(func() {
globalExporter = &MetricsExporter{
collectors: make(map[string]*MetricsCollector),
}
})
return globalExporter
}
// RegisterCollector 注册指标收集器
func (me *MetricsExporter) RegisterCollector(name string, collector *MetricsCollector) {
me.mu.Lock()
defer me.mu.Unlock()
me.collectors[name] = collector
}
// ServeHTTP 实现 http.Handler,供 Prometheus 拉取
func (me *MetricsExporter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
me.mu.RLock()
defer me.mu.RUnlock()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
for name, collector := range me.collectors {
fmt.Fprintf(w, "# HELP %s LLM application metrics\n# TYPE %s gauge\n", name, name)
fmt.Fprint(w, collector.Export())
}
}
// StartMetricsServer 启动指标 HTTP 服务
func StartMetricsServer(addr string) error {
exporter := GetGlobalExporter()
mux := http.NewServeMux()
mux.Handle("/metrics", exporter)
return http.ListenAndServe(addr, mux)
}
七、告警规则
// observability/alerts.go
package observability
import (
"fmt"
"time"
)
// AlertManager 告警管理器
type AlertManager struct {
rules []AlertRule
history map[string][]time.Time // rule name -> trigger times
}
func NewAlertManager() *AlertManager {
return &AlertManager{
rules: DefaultAlertRules(),
history: make(map[string][]time.Time),
}
}
// DefaultAlertRules 默认告警规则
func DefaultAlertRules() []AlertRule {
return []AlertRule{
{
Name: "high_cost_per_request",
Description: "单次 LLM 调用成本超过 $0.05",
Metric: "llm.cost.per_request",
Condition: "gt",
Threshold: 0.05,
Duration: 1 * time.Minute,
Severity: "warning",
Channels: []string{"slack"},
},
{
Name: "high_error_rate",
Description: "LLM 调用错误率超过 5%",
Metric: "llm.error_rate",
Condition: "gt",
Threshold: 0.05,
Duration: 5 * time.Minute,
Severity: "critical",
Channels: []string{"slack", "email"},
},
{
Name: "high_latency_p99",
Description: "P99 延迟超过 10 秒",
Metric: "llm.latency.p99",
Condition: "gt",
Threshold: 10000,
Duration: 5 * time.Minute,
Severity: "warning",
Channels: []string{"slack"},
},
{
Name: "cost_spike",
Description: "小时级成本突增超过 200%",
Metric: "llm.cost.hourly",
Condition: "change_rate",
Threshold: 2.0,
Duration: 10 * time.Minute,
Severity: "critical",
Channels: []string{"slack", "wechat"},
},
{
Name: "low_cache_hit_rate",
Description: "缓存命中率低于 20%",
Metric: "llm.cache.hit_rate",
Condition: "lt",
Threshold: 0.2,
Duration: 30 * time.Minute,
Severity: "info",
Channels: []string{"slack"},
},
{
Name: "zero_recall",
Description: "检索召回率为 0",
Metric: "rag.recall_rate",
Condition: "eq",
Threshold: 0,
Duration: 5 * time.Minute,
Severity: "critical",
Channels: []string{"slack", "email"},
},
{
Name: "budget_exhausted",
Description: "月度预算使用超过 80%",
Metric: "llm.cost.monthly_budget_usage",
Condition: "gt",
Threshold: 0.8,
Duration: 1 * time.Hour,
Severity: "warning",
Channels: []string{"email"},
},
}
}
// Evaluate 评估告警规则
func (am *AlertManager) Evaluate(metrics map[string]float64) []AlertRule {
var triggered []AlertRule
for _, rule := range am.rules {
value, ok := metrics[rule.Metric]
if !ok {
continue
}
if am.evaluateCondition(rule.Condition, value, rule.Threshold) {
triggered = append(triggered, rule)
}
}
return triggered
}
func (am *AlertManager) evaluateCondition(condition string, value, threshold float64) bool {
switch condition {
case "gt":
return value > threshold
case "lt":
return value < threshold
case "eq":
return value == threshold
case "change_rate":
return value > threshold
default:
return false
}
}
// SendAlert 发送告警
func (am *AlertManager) SendAlert(rule AlertRule, currentValue float64) {
message := fmt.Sprintf("[%s] %s\n当前值: %.2f\n阈值: %.2f\n描述: %s",
rule.Severity, rule.Name, currentValue, rule.Threshold, rule.Description)
for _, channel := range rule.Channels {
switch channel {
case "slack":
sendSlackAlert(message)
case "email":
sendEmailAlert(message)
case "wechat":
sendWechatAlert(message)
}
}
}
func sendSlackAlert(message string) {
fmt.Printf("[SLACK] %s\n", message)
}
func sendEmailAlert(message string) {
fmt.Printf("[EMAIL] %s\n", message)
}
func sendWechatAlert(message string) {
fmt.Printf("[WECHAT] %s\n", message)
}
八、Grafana Dashboard 配置
{
"dashboard": {
"title": "LLM Application Monitoring",
"panels": [
{
"title": "Request Volume & Error Rate",
"type": "timeseries",
"targets": [
{
"expr": "rate(llm_requests_total[5m])",
"legendFormat": "Total Requests"
},
{
"expr": "rate(llm_errors_total[5m])",
"legendFormat": "Errors"
}
]
},
{
"title": "Latency (P50/P95/P99)",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.50, sum(rate(llm_latency_ms_bucket[5m])) by (le))",
"legendFormat": "P50"
},
{
"expr": "histogram_quantile(0.95, sum(rate(llm_latency_ms_bucket[5m])) by (le))",
"legendFormat": "P95"
},
{
"expr": "histogram_quantile(0.99, sum(rate(llm_latency_ms_bucket[5m])) by (le))",
"legendFormat": "P99"
}
]
},
{
"title": "Token Consumption",
"type": "timeseries",
"targets": [
{
"expr": "rate(llm_tokens_total[5m])",
"legendFormat": "Total Tokens"
},
{
"expr": "rate(llm_tokens_prompt[5m])",
"legendFormat": "Prompt Tokens"
},
{
"expr": "rate(llm_tokens_completion[5m])",
"legendFormat": "Completion Tokens"
}
]
},
{
"title": "Cost (USD/hour)",
"type": "timeseries",
"targets": [
{
"expr": "rate(llm_cost_usd[1h])",
"legendFormat": "Cost per hour"
}
]
},
{
"title": "Cache Hit Rate",
"type": "timeseries",
"targets": [
{
"expr": "rate(llm_cache_hit[5m]) / (rate(llm_cache_hit[5m]) + rate(llm_cache_miss[5m]))",
"legendFormat": "Hit Rate"
}
]
},
{
"title": "Model Distribution",
"type": "piechart",
"targets": [
{
"expr": "sum by(model) (rate(llm_requests_model[5m]))",
"legendFormat": "{{model}}"
}
]
}
]
}
}
九、集成到 RAG Pipeline
// example/integration.go
package main
import (
"context"
"fmt"
"observability"
"rag"
)
func main() {
// 1. 创建追踪器
tracer := observability.NewRAGTracer(observability.TracerConfig{
ServiceName: "rag-service",
Environment: "prod",
SampleRate: 0.1, // 生产环境 10% 采样
EnableDetailed: true,
CostAlertThreshold: 0.05, // 单次超过 $0.05 告警
})
// 2. 启动指标服务
go func() {
exporter := observability.GetGlobalExporter()
exporter.RegisterCollector("rag", tracer.Tracer().MetricsCollector())
observability.StartMetricsServer(":9090")
}()
// 3. 创建 RAG Pipeline(带追踪)
pipeline := rag.NewRAGPipeline(
&TracedLLM{tracer: tracer},
&TracedEmbedder{tracer: tracer},
&TracedVectorIndex{tracer: tracer},
&TracedBM25Index{tracer: tracer},
nil,
rag.DefaultPipelineConfig(),
)
// 4. 执行带追踪的查询
ctx := context.Background()
result, err := tracer.TraceRAGPipeline(ctx, "如何使用 Go 实现 RAG?", func(ctx context.Context) (*observability.RAGTraceResult, error) {
ragResult, err := pipeline.Run(ctx, "如何使用 Go 实现 RAG?")
if err != nil {
return nil, err
}
return &observability.RAGTraceResult{
TotalTokens: ragResult.Metrics.ContextTokens + estimateResponseTokens(ragResult.Answer),
TotalCost: 0.02, // 实际从 LLM 调用中获取
RetrievedDocs: ragResult.Metrics.RetrievedDocs,
UsedDocs: ragResult.Metrics.UsedDocs,
DurationMs: ragResult.Metrics.TotalDurationMs,
}, nil
})
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Result: %s\n", result)
}
// TracedLLM 带追踪的 LLM 客户端
type TracedLLM struct {
tracer *observability.RAGTracer
}
func (l *TracedLLM) Chat(ctx context.Context, messages []rag.Message) (string, error) {
var response string
_, _, err := l.tracer.TraceLLMCall(ctx, "deepseek-chat", func(ctx context.Context) (int, int, error) {
// 实际的 LLM 调用
response = "模拟回复"
return 100, 50, nil // prompt_tokens, completion_tokens
})
return response, err
}
// TracedEmbedder 带追踪的 Embedder
type TracedEmbedder struct {
tracer *observability.RAGTracer
}
func (e *TracedEmbedder) Embed(ctx context.Context, text string) ([]float32, error) {
var emb []float32
err := e.tracer.TraceEmbedding(ctx, "text-embedding-3", len(text), func(ctx context.Context) error {
emb = make([]float32, 768)
return nil
})
return emb, err
}
// TracedVectorIndex 带追踪的向量索引
type TracedVectorIndex struct {
tracer *observability.RAGTracer
}
func (idx *TracedVectorIndex) Search(ctx context.Context, embedding []float32, topK int, filters map[string]interface{}) ([]rag.Document, error) {
var docs []rag.Document
_, err := idx.tracer.TraceVectorSearch(ctx, "milvus-demo", topK, func(ctx context.Context) (int, error) {
docs = []rag.Document{{ID: "1", Content: "模拟文档", Score: 0.95}}
return len(docs), nil
})
return docs, err
}
// TracedBM25Index 带追踪的 BM25 索引
type TracedBM25Index struct {
tracer *observability.RAGTracer
}
func (idx *TracedBM25Index) Search(ctx context.Context, query string, topK int) ([]rag.Document, error) {
return []rag.Document{}, nil
}
func estimateResponseTokens(answer string) int {
return len([]rune(answer)) / 2
}
十、关键要点
- LLM 应用的三维监控:Metrics + Tracing + Logging --- 三者缺一不可
- 采样率是成本控制的关键 --- 生产环境 10% 采样足够发现问题,全量采样成本过高
- Token 和成本是 LLM 特有的核心指标 --- 传统监控不关心这些,但 LLM 应用必须关注
- 全链路追踪是调试利器 --- 一次用户请求经过 5-6 个组件,没有 Trace 根本找不到瓶颈
- 告警要有分级 --- Critical 立即通知,Warning 白天处理,Info 周报汇总
- 成本告警最重要 --- 一次配置错误可能导致小时级 $1000+ 的损失
- Grafana 面板要分层 --- 运维看实时大盘,管理者看成本看板,开发者看链路追踪
十一、预告
第 10 讲预告:《大模型应用测试:从单元测试到 E2E 评测的完整体系》
你将学到:
- LLM 应用的测试为什么难?确定性 vs 非确定性输出的挑战
- 单元测试:Prompt 模板测试、工具调用测试、输出格式验证
- 集成测试:RAG Pipeline 端到端测试、多 Agent 协作测试
- 评估体系:BLEU/ROUGE/BERTScore、LLM-as-Judge、人工评估
- 回归测试:Diff 检测、语义等价判断、自动化 CI/CD 集成
- Go 实现的测试框架
🧰 开发之余的小工具推荐
处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。