
当你的大模型应用从 Demo 走向生产,第一个让你夜不能寐的问题一定是:“它为什么又答错了?” ——没有异常堆栈,没有报错日志,只有一段看似合理却完全跑偏的文本。传统监控手段(日志级别、错误码)在大模型场景下几乎失效,因为 “错误”不再是程序崩溃,而是语义偏差、幻觉、检索遗漏。
本文基于我们团队在金融 QA 机器人上的真实踩坑经历,分享一套面向 LLM 应用的可观测性体系设计方案,涵盖结构化日志、全链路追踪、语义指标监控三大支柱,并提供可落地的代码实践。
传统的 APM(应用性能监控)针对确定性代码设计,你能预判每行代码的输入输出。而 LLM 应用具有三大不确定性:
因此,我们需要一种可解释、可回溯、可量化的观测体系,让每一次请求都像一段可解剖的流水线。
不要用 print(),不要用 logging.info() 随便拼字符串。采用 JSON 格式结构化日志,并强制包含以下字段:
trace_id、span_id(用于关联调用链)session_id、user_idstep(检索、重排、生成、校验)token_usage、latency_msretrieved_docs(只记录文档 ID 和 score)prompt_preview(截断过长)Python 实现:
import json
import logging
from pythonjsonlogger import jsonlogger
class LLMJsonFormatter(jsonlogger.JsonFormatter):
def add_fields(self, log_record, record, message_dict):
super().add_fields(log_record, record, message_dict)
if not log_record.get('timestamp'):
log_record['timestamp'] = datetime.utcnow().isoformat()
# 强制注入全局 trace_id(需从 context 中取)
log_record['trace_id'] = get_current_trace_id() or 'unknown'
logger = logging.getLogger('llm_app')
handler = logging.StreamHandler()
handler.setFormatter(LLMJsonFormatter())
logger.addHandler(handler)
# 使用示例
logger.info(
"retrieval_completed",
extra={
"step": "retrieve",
"query": user_query,
"top_doc_ids": [d.metadata['id'] for d in docs],
"top_scores": [s for _, s in docs_with_scores],
"cost_ms": elapsed
}
)这些 JSON 日志直接灌入 ELK 或 Loki,你可以通过 trace_id 一键聚合所有步骤,快速定位“检索召回太靠后”还是“Prompt 被截断”。
LLM 应用本质是一条流水线:意图分类 → 重写 → 检索 → 重排序 → 上下文压缩 → LLM 生成 → 事实校验。每一步都可能变慢或出错。用 OpenTelemetry 自动埋点并可视化在 Jaeger 中。
部署 OpenTelemetry Collector 并集成 FastAPI:
# docker-compose 部分
services:
app:
image: my-llm-app
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
- OTEL_SERVICE_NAME=llm-qa
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
volumes:
- ./otel-config.yaml:/etc/otel/config.yaml
command: ["--config=/etc/otel/config.yaml"]
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"代码中手动创建子 Span:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@app.post("/ask")
async def ask(query: str):
with tracer.start_as_current_span("full_pipeline") as parent_span:
parent_span.set_attribute("query", query)
with tracer.start_as_current_span("retrieve") as span:
docs = retriever.get_relevant_documents(query)
span.set_attribute("doc_count", len(docs))
with tracer.start_as_current_span("rerank") as span:
top_docs = reranker.rerank(query, docs, top_n=3)
span.set_attribute("top_scores", [d.score for d in top_docs])
with tracer.start_as_current_span("llm_generate") as span:
answer = llm.generate(query, top_docs)
span.set_attribute("token_usage", answer.usage)
return answer在 Jaeger UI 中,你可以清晰看到每一步的耗时占比,发现“检索”阶段突然从 50ms 飙到 800ms,立即排查向量数据库连接池是否耗尽。
业务指标不是 QPS 和错误率,而是 检索命中率、答案忠实度、答案相关性。这些可通过离线评测框架(如 RAGAS)计算,但生产环境需要实时或准实时监控。
我们实现了一个异步评分器,将请求日志采样后发送至评估服务:
# evaluator.py
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_relevancy
def online_evaluate(sample: dict):
# sample 包含 question, answer, contexts, ground_truth (若有)
score = evaluate(
dataset=[sample],
metrics=[faithfulness, answer_relevancy, context_relevancy]
)
# 推送至 Prometheus
for metric_name, value in score.items():
llm_metric.labels(metric=metric_name).set(value)
return scorePrometheus 指标暴露与告警规则:
from prometheus_client import Gauge, generate_latest
llm_metric = Gauge('llm_qa_ragas_score', 'RAGAS evaluation score', ['metric'])
@app.get("/metrics")
def metrics():
return Response(generate_latest(), media_type="text/plain")告警规则:如果 faithfulness 连续 3 个采样周期低于 0.7,触发告警,提示可能需要更新知识库或调整 Prompt。
有了观测数据,我们能精准优化。例如,我们发现高频问题(“年假如何计算”)占请求量的 20%,完全可缓存。
Redis 缓存语义相似结果:
import hashlib
import redis
from sentence_transformers import SentenceTransformer
cache = redis.Redis(host='localhost', decode_responses=True)
encoder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
def get_cached_answer(query):
# 对 query 做语义哈希(如用 embedding 的前 10 维作为 key)
emb = encoder.encode(query)
key = hashlib.md5(emb[:10].tobytes()).hexdigest()
cached = cache.get(f"qa:{key}")
if cached:
logger.info("cache_hit", extra={"query": query})
return json.loads(cached)
return None同时,当 LLM API 连续超时,我们利用观测数据触发降级——回退到只返回检索片段拼接的摘要,避免无响应。
我们这套体系上线后,问题定位时间从平均 2 小时缩短到 15 分钟。典型场景:某天 faithfulness 指标突降,Jaeger 显示 llm_generate 耗时正常但 retrieve 返回的文档平均分变低。进一步查结构化日志发现,某个向量库索引被意外重建,导致 embedding 分布偏移。半小时内回滚索引,指标恢复。
核心经验提炼:
最后,不要试图一次性做全。从结构化日志 + trace_id 关联开始,两周内你就能感受到差异。工具选型上,推荐 OpenTelemetry + Jaeger + Prometheus + Grafana 组合,全部开源且生态成熟。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。