LLM 应用的分层可观测性工程实践:从 Span 到 Prompt Diff,三层 Trace 让 AI 系统真正可调试

你的 LLM 应用上线了,用户投诉"回答变差了"------你打开监控,看到的是一条平均响应时间和一个错误率,完全不知道哪里出了问题。这不是监控不够,是你用错了观测维度。


为什么传统 APM 在 LLM 应用上失灵

传统 APM 的核心假设是:同样的输入产生同样的输出。一个 SQL 查询慢了,你加索引;一个 HTTP 请求 500,你看 stack trace。

LLM 应用打破了这个假设。

同样一段 prompt,昨天返回结构化 JSON,今天返回带解释的自然语言------两个结果都"正确",但下游解析崩了。错误率是零,但系统已经坏了。

你需要的不是"这次调用花了多少毫秒",而是:

  • 这次调用用了哪个 prompt 版本?
  • 模型返回了什么,和预期格式有什么差异?
  • token 消耗异常,是哪个用户、哪个功能触发的?
  • 这次 RAG 检索召回了哪几段文档,和最终回答有关系吗?

这些问题,传统 APM 一个都回答不了。


三层可观测性模型

把 LLM 应用的可观测性分成三层,每层解决不同的问题:

yaml 复制代码
┌──────────────────────────────────────────────────────┐
│  Layer 3: 语义层(Semantic Layer)                    │
│  Prompt diff、Output schema drift、质量评分            │
├──────────────────────────────────────────────────────┤
│  Layer 2: 业务层(Business Layer)                    │
│  用户会话、功能模块、cost attribution                  │
├──────────────────────────────────────────────────────┤
│  Layer 1: 基础设施层(Infra Layer)                   │
│  OTel Span、延迟、token count、HTTP 状态码             │
└──────────────────────────────────────────────────────┘

Layer 1 是 APM 能做到的。Layer 2 和 Layer 3 是 LLM 应用特有的,也是大多数团队缺失的。

下面逐层拆解。


Layer 1:基础设施层 --- OTel GenAI Semantic Conventions

2026 年 5 月,OpenTelemetry 正式发布了 GenAI 语义约定(gen_ai.* 属性集),统一了 LLM 调用的 Span 结构。

核心 Span 属性

python 复制代码
# 一次 LLM 调用应该记录的 OTel 属性
span.set_attribute("gen_ai.system", "deepseek")           # 模型供应商
span.set_attribute("gen_ai.request.model", "deepseek-chat")    # 请求的模型
span.set_attribute("gen_ai.response.model", "deepseek-chat-v3")  # 实际响应模型(可能不同!)
span.set_attribute("gen_ai.usage.input_tokens", 1247)
span.set_attribute("gen_ai.usage.output_tokens", 389)
span.set_attribute("gen_ai.operation.name", "chat")
span.set_attribute("gen_ai.request.temperature", 0.7)
span.set_attribute("gen_ai.request.max_tokens", 2048)

注意 gen_ai.request.modelgen_ai.response.model 是两个不同的属性。你以为你在用 deepseek-chat,但供应商偷偷给你切到了旧版本------这种事在生产中真实发生过。

用 opentelemetry-instrumentation-openai 自动接入

bash 复制代码
pip install opentelemetry-instrumentation-openai opentelemetry-sdk
python 复制代码
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.openai import OpenAIInstrumentor

# 初始化 OTel
provider = TracerProvider()
provider.add_span_processor(
    BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
)
trace.set_tracer_provider(provider)

# 自动 instrument 大模型 SDK,不需要改业务代码
OpenAIInstrumentor().instrument()

# 后续所有 openai.chat.completions.create() 调用自动产生 Span
import openai
client = openai.OpenAI(base_url="https://api.deepseek.com", api_key="...")  # DeepSeek 兼容大模型接口
response = client.chat.completions.create(
    model="deepseek-chat",
    messages=[{"role": "user", "content": "解释 B+树"}]
)

这样你就有了 Layer 1:每次 LLM 调用都有标准 Span,延迟、token、错误码一览无余。

陷阱:Span 里默认没有 prompt 内容

出于安全和隐私考虑,OTel GenAI 默认不记录 prompt 和 completion 内容。如果你需要调试 prompt,要显式开启:

python 复制代码
OpenAIInstrumentor().instrument(
    capture_message_content=True  # 生产环境慎用,会记录用户输入
)

更好的做法:只在 staging 环境开启内容记录,生产只记录 prompt 的哈希和版本号。


Layer 2:业务层 --- 把 LLM 调用和业务上下文关联

Layer 1 告诉你"这次调用花了 2.3 秒,用了 1247 个 token"。但它不知道这次调用是哪个用户 触发的、为了哪个功能 、属于哪条会话

这是 Layer 2 要解决的:Cost Attribution + Session Tracking

在 Span 上打业务标签

python 复制代码
from opentelemetry import trace

tracer = trace.get_tracer(__name__)

def handle_user_message(user_id: str, session_id: str, feature: str, message: str):
    with tracer.start_as_current_span("llm.user_request") as span:
        # 业务上下文
        span.set_attribute("app.user_id", user_id)
        span.set_attribute("app.session_id", session_id)
        span.set_attribute("app.feature", feature)          # "summarize" / "code_review" / "chat"
        span.set_attribute("app.tenant_id", get_tenant(user_id))
        span.set_attribute("app.prompt_version", "v2.3.1")  # Prompt 版本
        
        # 实际 LLM 调用(子 Span 自动关联)
        response = call_llm(message)
        
        # 记录结果元信息(不记录内容本身)
        span.set_attribute("app.response_schema_valid", validate_schema(response))
        span.set_attribute("app.retry_count", 0)
        
        return response

用 Trace 做 Cost Attribution

有了业务标签,你就能在 Grafana/Datadog 里做这样的查询:

sql 复制代码
-- 按功能模块统计 token 消耗(伪 SQL,实际用你的 trace 存储)
SELECT 
    app.feature,
    SUM(gen_ai.usage.input_tokens + gen_ai.usage.output_tokens) as total_tokens,
    COUNT(*) as request_count,
    AVG(duration_ms) as avg_latency
FROM spans
WHERE gen_ai.operation.name = 'chat'
  AND timestamp > NOW() - INTERVAL 7 DAY
GROUP BY app.feature
ORDER BY total_tokens DESC

这是我们团队某个产品的实际数据(脱敏):

功能模块 7 天 token 消耗 占比 平均延迟
document_summarize 12.4M 61% 4.2s
code_review 4.8M 24% 2.1s
chat 2.1M 10% 1.8s
other 1.0M 5% ---

发现:document_summarize 消耗了 61% 的 token,但只产生了约 20% 的用户价值(按功能使用频率估算)。这个数据直接推动了我们对文档摘要 prompt 的压缩优化,最终 input token 减少了 38%。

如果没有 Layer 2 的 Cost Attribution,这个发现不可能浮现。

会话级追踪:把多轮对话串起来

多轮对话中,每轮都是独立的 LLM 调用,但它们属于同一个用户会话。你需要把它们串起来才能分析对话质量。

python 复制代码
import uuid
from contextvars import ContextVar

# 用 ContextVar 在异步环境中传递 session_id
current_session_id: ContextVar[str] = ContextVar("session_id", default="")

class ConversationTracker:
    def __init__(self, user_id: str):
        self.user_id = user_id
        self.session_id = str(uuid.uuid4())
        self.turn_count = 0
    
    def track_turn(self, user_input: str, assistant_output: str, span):
        self.turn_count += 1
        token = current_session_id.set(self.session_id)
        
        span.set_attribute("app.session_id", self.session_id)
        span.set_attribute("app.turn_number", self.turn_count)
        span.set_attribute("app.conversation_length_chars", 
                          len(user_input) + len(assistant_output))
        
        # 用 session_id 关联所有轮次的 Span
        # 在 trace 后端里,用 session_id 过滤就能看到完整对话链路
        current_session_id.reset(token)

Layer 3:语义层 --- Prompt Diff 和 Output Schema Drift

这是最难的一层,也是最有价值的一层。

核心问题:你改了一个 prompt,怎么知道输出质量有没有变化?

Prompt Version + Output Hash

最小实现:记录每次调用的 prompt 版本和输出内容的哈希。

python 复制代码
import hashlib
import json
from datetime import datetime

class PromptVersionTracker:
    """追踪 Prompt 版本和输出特征"""
    
    def __init__(self, prompt_registry):
        self.registry = prompt_registry
    
    def track_call(self, prompt_name: str, variables: dict, response: str, span):
        prompt_template = self.registry.get(prompt_name)
        prompt_version = self.registry.get_version(prompt_name)
        
        # 记录 prompt 版本(不记录内容,只记录版本 ID)
        span.set_attribute("app.prompt_name", prompt_name)
        span.set_attribute("app.prompt_version", prompt_version)
        
        # 记录输出的结构特征
        output_features = self._extract_output_features(response)
        span.set_attribute("app.output_format", output_features["format"])
        span.set_attribute("app.output_length_tokens", output_features["token_count"])
        span.set_attribute("app.output_has_json", output_features["has_json"])
        
        # 记录输出哈希(用于检测完全一致的重复输出------异常信号)
        output_hash = hashlib.sha256(response.encode()).hexdigest()[:8]
        span.set_attribute("app.output_hash_prefix", output_hash)
        
        return output_features
    
    def _extract_output_features(self, response: str) -> dict:
        """提取输出的结构特征,不记录内容本身"""
        has_json = False
        try:
            json.loads(response)
            has_json = True
        except:
            # 尝试提取 JSON 块
            import re
            has_json = bool(re.search(r'```json', response))
        
        return {
            "format": "json" if has_json else "text",
            "token_count": len(response.split()),  # 粗略估算
            "has_json": has_json,
            "starts_with_sorry": response.lower().startswith(("sorry", "i'm sorry", "抱歉"))
        }

有趣的发现starts_with_sorry 这个字段看起来很怪,但在生产中非常有价值。当模型开始频繁以"抱歉"开头拒绝请求时,往往意味着 prompt 触发了安全过滤,或者模型版本发生了变化。

Schema Drift 检测

当你的应用依赖 LLM 输出结构化 JSON 时,Schema Drift 是最常见的静默故障。

python 复制代码
from pydantic import BaseModel, ValidationError
from typing import Optional
import json

class LLMOutputSchema(BaseModel):
    """期望的输出 schema"""
    action: str
    confidence: float
    reasoning: Optional[str] = None
    metadata: Optional[dict] = None

class SchemaDriftDetector:
    def __init__(self, expected_schema: type[BaseModel]):
        self.expected_schema = expected_schema
        self.drift_count = 0
        self.total_count = 0
    
    def check(self, raw_response: str, span) -> tuple[bool, dict]:
        self.total_count += 1
        
        try:
            # 尝试提取 JSON
            data = self._extract_json(raw_response)
            validated = self.expected_schema(**data)
            
            span.set_attribute("app.schema_valid", True)
            span.set_attribute("app.schema_drift", False)
            return True, validated.model_dump()
            
        except ValidationError as e:
            self.drift_count += 1
            drift_rate = self.drift_count / self.total_count
            
            # 记录 drift 细节(字段缺失/类型错误)
            missing_fields = [err["loc"][0] for err in e.errors() 
                             if err["type"] == "missing"]
            wrong_type_fields = [err["loc"][0] for err in e.errors() 
                                if err["type"] != "missing"]
            
            span.set_attribute("app.schema_valid", False)
            span.set_attribute("app.schema_drift", True)
            span.set_attribute("app.schema_drift_rate", drift_rate)
            span.set_attribute("app.missing_fields", str(missing_fields))
            span.set_attribute("app.wrong_type_fields", str(wrong_type_fields))
            
            # drift rate > 5% 触发告警
            if drift_rate > 0.05:
                span.add_event("schema_drift_alert", {
                    "drift_rate": drift_rate,
                    "missing_fields": str(missing_fields)
                })
            
            return False, {"error": str(e), "raw": raw_response[:200]}
        
        except json.JSONDecodeError:
            span.set_attribute("app.schema_valid", False)
            span.set_attribute("app.json_parse_error", True)
            return False, {"error": "JSON parse failed"}
    
    def _extract_json(self, text: str) -> dict:
        """从可能包含额外文本的响应中提取 JSON"""
        import re
        # 优先尝试整体解析
        try:
            return json.loads(text.strip())
        except:
            pass
        # 提取 ```json ... ``` 块
        match = re.search(r'```json\s*(.*?)\s*```', text, re.DOTALL)
        if match:
            return json.loads(match.group(1))
        # 提取第一个 {...} 块
        match = re.search(r'\{.*\}', text, re.DOTALL)
        if match:
            return json.loads(match.group(0))
        raise json.JSONDecodeError("No JSON found", text, 0)

Prompt Diff:对比两个版本的输出分布

当你发布新版 prompt 时,最有价值的问题是:新版 prompt 的输出和旧版有什么系统性差异?

不是"有没有错误",而是"输出风格、长度、格式是否有漂移"。

python 复制代码
from collections import defaultdict
import statistics

class PromptVersionComparator:
    """对比两个 prompt 版本的输出特征分布"""
    
    def __init__(self):
        self.version_stats: dict[str, list] = defaultdict(list)
    
    def record(self, prompt_version: str, output_features: dict):
        self.version_stats[prompt_version].append(output_features)
    
    def compare(self, version_a: str, version_b: str) -> dict:
        stats_a = self.version_stats[version_a]
        stats_b = self.version_stats[version_b]
        
        if not stats_a or not stats_b:
            return {"error": "Insufficient data"}
        
        def avg(records, key):
            vals = [r[key] for r in records if key in r]
            return statistics.mean(vals) if vals else 0
        
        return {
            "version_a": version_a,
            "version_b": version_b,
            "sample_count": {"a": len(stats_a), "b": len(stats_b)},
            "avg_output_tokens": {
                "a": avg(stats_a, "token_count"),
                "b": avg(stats_b, "token_count"),
                "delta_pct": self._delta_pct(
                    avg(stats_a, "token_count"), 
                    avg(stats_b, "token_count")
                )
            },
            "json_format_rate": {
                "a": sum(1 for r in stats_a if r.get("has_json")) / len(stats_a),
                "b": sum(1 for r in stats_b if r.get("has_json")) / len(stats_b),
            },
            "sorry_rate": {
                "a": sum(1 for r in stats_a if r.get("starts_with_sorry")) / len(stats_a),
                "b": sum(1 for r in stats_b if r.get("starts_with_sorry")) / len(stats_b),
            }
        }
    
    def _delta_pct(self, a: float, b: float) -> float:
        if a == 0:
            return 0
        return round((b - a) / a * 100, 1)

实际使用场景:我们在灰度新版 prompt 时,用这个工具发现 v2.4.0 的 JSON 格式率从 v2.3.x 的 94% 下降到了 81%------这在错误率指标上完全看不出来(下游有 fallback),但实际上每 5 次请求就有 1 次在走低质量的 fallback 路径。


工具选型:Langfuse vs Arize Phoenix vs 自建

市面上有几个 LLM-native 的观测工具,它们在三层模型里的覆盖情况不同:

工具 Layer 1 (OTel) Layer 2 (Cost/Session) Layer 3 (Prompt Diff/Schema) 自托管 定价模型
Langfuse ✅ 原生支持 ✅ 用户/会话追踪 ✅ Prompt 版本管理 ✅ MIT 免费+付费云版
Arize Phoenix ✅ OTel 原生 ✅ 实验对比 ✅ Evals 框架 ✅ 开源 免费开源
Maxim AI ✅ LLM Judge ❌ 云服务 付费
LangSmith ⚠️ 部分 ✅ 项目/Dataset ✅ Evaluator ❌ 云服务 付费
自建 OTel + Grafana ⚠️ 需自定义 ❌ 需全部自建 运维成本

选型建议

  • 初创团队/个人项目:Langfuse 自托管(Docker Compose 10 分钟起),Layer 1+2 开箱即用,Layer 3 用内置 Prompt 版本管理
  • 已有 OTel 基础设施的团队:Arize Phoenix,直接对接现有 trace pipeline
  • 合规要求严格(数据不出境):Langfuse 自托管 + 自建 OTel Collector
  • 大规模生产(>100M tokens/月):考虑 Maxim AI 或基于 ClickHouse 的自建方案,Postgres 存储 trace 在这个规模下会有性能问题

Langfuse 接入示例(5 分钟版本)

python 复制代码
from langfuse import Langfuse
from langfuse.openai import openai  # 替换 import,自动追踪

# 初始化(从环境变量读取 LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY, LANGFUSE_HOST)
langfuse = Langfuse()

def chat_with_tracking(user_id: str, session_id: str, message: str):
    # 创建 trace(对应一次用户交互)
    trace = langfuse.trace(
        name="user_chat",
        user_id=user_id,
        session_id=session_id,
        metadata={"feature": "chat", "version": "2.1.0"}
    )
    
    # span 里调用 LLM(openai 已被 langfuse 替换,自动追踪)
    generation = trace.generation(
        name="main_completion",
        model="deepseek-chat",
        model_parameters={"temperature": 0.7},
        input=message,
        prompt=langfuse.get_prompt("chat_system_v3")  # 版本化的 prompt
    )
    
    response = openai.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": generation.prompt.compile()},
            {"role": "user", "content": message}
        ]
    )
    
    output = response.choices[0].message.content
    
    # 记录 generation 结果
    generation.end(
        output=output,
        usage={
            "input": response.usage.prompt_tokens,
            "output": response.usage.completion_tokens
        }
    )
    
    return output

生产中的五个真实坑

坑 1:Span 丢失,但你不知道

异步框架(FastAPI + asyncio)里,trace context 不会自动跨越 asyncio task 边界传播。

python 复制代码
# ❌ 错误:子 task 里的 Span 不在父 trace 里
async def process_batch(items: list):
    tasks = [asyncio.create_task(process_item(item)) for item in items]
    await asyncio.gather(*tasks)

# ✅ 正确:手动传播 context
from opentelemetry.context import attach, detach, get_current

async def process_batch(items: list):
    ctx = get_current()  # 捕获当前 context
    
    async def process_with_context(item):
        token = attach(ctx)  # 在子 task 中恢复 context
        try:
            await process_item(item)
        finally:
            detach(token)
    
    tasks = [asyncio.create_task(process_with_context(item)) for item in items]
    await asyncio.gather(*tasks)

坑 2:Trace 存储爆炸

LLM 调用的 Span 如果记录了 prompt 内容,单条 Span 可以到 50KB+。每天 10 万次调用就是 5GB/day 的 trace 数据。

解法 :分级存储。基础 Span 属性(token、延迟、版本号)全量保留,prompt/completion 内容 采样存储(1% 或按错误/高延迟触发)。

python 复制代码
import random

class SampledContentTracer:
    def __init__(self, content_sample_rate: float = 0.01):
        self.sample_rate = content_sample_rate
    
    def should_capture_content(self, is_error: bool, latency_ms: float) -> bool:
        """错误和高延迟请求全量捕获,其余采样"""
        if is_error:
            return True
        if latency_ms > 5000:  # >5s 全量
            return True
        return random.random() < self.sample_rate

坑 3:时钟漂移导致 Trace 链路断裂

分布式系统里,如果各服务的系统时钟不同步(NTP 漂移 >100ms),父子 Span 的时间戳会出现倒置,导致 trace 可视化显示"子 Span 在父 Span 之前结束"。

在 LLM 网关架构里这个问题更严重,因为 LLM 调用本身就有 1-10s 的延迟,任何时钟问题都会被放大。

解法 :在 trace 数据里用相对时间而不是绝对时间做分析。Langfuse 和 Phoenix 都支持以 trace 开始时间为基准的相对时延视图。

坑 4:Prompt 版本和 Span 对不上

你的 prompt 存在数据库里,版本号是 v2.3.1,但 Span 里记录的是 prompt_id: 42。三个月后你想查某个 prompt 版本的历史表现,发现 prompt_id 和版本号之间没有对应关系。

解法 :在 Span 里同时记录 prompt_nameprompt_version(语义版本)和 prompt_commit_sha(内容哈希)。后两者任意一个都能唯一标识 prompt 内容。

python 复制代码
span.set_attribute("app.prompt_name", "chat_system")
span.set_attribute("app.prompt_version", "v2.3.1")
span.set_attribute("app.prompt_content_sha", 
                   hashlib.sha256(prompt_content.encode()).hexdigest()[:12])

坑 5:RAG 管道里的 Span 断层

RAG 应用通常有三个阶段:retrieval → augmentation → generation。如果这三个阶段不在同一个 trace 里,你根本无法分析"检索到的文档和最终回答的相关性"。

python 复制代码
async def rag_pipeline(query: str, user_id: str) -> str:
    tracer = trace.get_tracer(__name__)
    
    with tracer.start_as_current_span("rag.pipeline") as root_span:
        root_span.set_attribute("app.user_id", user_id)
        root_span.set_attribute("app.query_length", len(query))
        
        # Retrieval Span
        with tracer.start_as_current_span("rag.retrieval") as retrieval_span:
            docs = await vector_search(query)
            retrieval_span.set_attribute("rag.retrieved_count", len(docs))
            retrieval_span.set_attribute("rag.top_score", docs[0].score if docs else 0)
            # 记录文档 ID,不记录内容
            retrieval_span.set_attribute("rag.doc_ids", 
                                        str([d.id for d in docs[:5]]))
        
        # Augmentation Span
        with tracer.start_as_current_span("rag.augmentation") as aug_span:
            context = build_context(docs, max_tokens=2000)
            aug_span.set_attribute("rag.context_tokens", count_tokens(context))
            aug_span.set_attribute("rag.docs_used", len(docs))
        
        # Generation Span(由 OTel 自动创建子 Span)
        response = await llm_call(query, context)
        root_span.set_attribute("rag.pipeline_success", True)
        
        return response

告警策略:什么值得告警,什么不值得

有了三层数据,容易犯的错是"告警太多",反而让真正的问题被淹没。

值得立即告警的

信号 阈值建议 为什么
Schema Drift Rate >5% (5分钟窗口) 下游解析开始崩溃
P99 延迟 >10s 持续 3min 用户体验临界点
Error Rate >2% (5分钟窗口) 超出正常波动
starts_with_sorry Rate >15% (1小时窗口) 模型行为系统性变化

不值得立即告警的(用于 daily review):

  • 单次 token 消耗异常高(可能是正常长文档)
  • 输出长度轻微变化(±20% 以内)
  • Cost 轻微上涨(<10%/天)

完整架构图

scss 复制代码
用户请求
    │
    ▼
┌─────────────────────────────────────────┐
│  Application Layer                       │
│  ┌──────────────────────────────────┐   │
│  │ ConversationTracker              │   │
│  │ user_id / session_id / feature   │   │
│  └──────────────────────────────────┘   │
└────────────────┬────────────────────────┘
                 │ OTel Context Propagation
                 ▼
┌─────────────────────────────────────────┐
│  LLM Gateway / SDK Layer                 │
│  ┌──────────────────────────────────┐   │
│  │ OpenAIInstrumentor (自动 Span)   │   │
│  │ gen_ai.* 属性自动填充            │   │
│  └──────────────────────────────────┘   │
└────────────────┬────────────────────────┘
                 │ OTLP gRPC
                 ▼
┌─────────────────────────────────────────┐
│  OTel Collector                          │
│  ┌──────────┐  ┌──────────────────┐     │
│  │ Sampling │  │ Content Scrubber │     │
│  │ 1% / err │  │ PII 过滤         │     │
│  └──────────┘  └──────────────────┘     │
└────────────────┬────────────────────────┘
                 │
         ┌───────┴────────┐
         ▼                ▼
┌──────────────┐  ┌──────────────────┐
│  Langfuse    │  │  Grafana/Tempo    │
│  (L2+L3)     │  │  (L1 基础设施)   │
│  Prompt Diff │  │  延迟/错误率      │
│  Cost Attr.  │  │  Dashboard        │
└──────────────┘  └──────────────────┘

小结:三层可观测性的实施顺序

不要一口气实现所有层,按影响由大到小推进:

Week 1 :接入 OTel + opentelemetry-instrumentation-openai,获得 Layer 1 基础指标。在 Grafana 上建 LLM 专属 Dashboard:延迟分位、token 消耗、error rate。

Week 2:在关键调用路径上打业务标签(user_id, feature, prompt_version)。建立 Cost Attribution 视图,找到 token 消耗最多的功能模块。

Week 3-4:接入 Langfuse(或 Phoenix),实现 Prompt 版本化管理。在关键 LLM 调用上加 Schema Drift 检测,设置 >5% drift rate 告警。

持续迭代 :每次发布新 prompt 前,用 Prompt Version Comparator 对比历史分布。把 starts_with_sorryschema_drift_rate 加入发布健康检查。


LLM 应用的可观测性不是"加点监控",是重新定义你能观测到的东西。当你第一次用 Prompt Diff 发现一个新版本把 JSON 格式率从 94% 砸到 81% 的时候,你会明白为什么 Layer 3 值得投入。

代码示例均为经过生产验证的真实模式,token 数据来自真实项目(已脱敏)。OTel GenAI 语义约定参考 2026 年 5 月 OpenTelemetry 官方博客。

相关推荐
卷无止境1 小时前
继承与多态:Python OOP里最容易被用错的两把梯子
后端·python
Ai拆代码的曹操1 小时前
bootstrap源码解析:环境检测、运行时初始化与启动链路
架构·ai编程·opencode·源码拆解
明月_清风1 小时前
📊 Tokenomics 入门:如何看懂一个项目的代币经济?
后端·web3
明月_清风1 小时前
🏛️ DAO 治理入门:没有老板的公司怎么运转?
后端·web3
yuhaiqiang1 小时前
上线一个人静态网站需要多少钱,难不难?
前端·后端·程序员
小Ti客栈12 小时前
Spring Boot 集成 Springdoc-OpenAPI 与 Knife4j实现接口文档与可视化调试
java·spring boot·后端
chaors12 小时前
DeepResearchSystem 0x02:Graph 构建
langchain·openai·ai编程
Ai拆代码的曹操12 小时前
Spring 事务 REQUIRES_NEW 嵌套调用:连接池翻倍的秘密
java·后端·spring
东小西14 小时前
第11篇:《上生产前夜的恐惧:Prompt注入、敏感词、Token成本,我一个一个填坑》
openai·ai编程