AI应用可观测性实战:从确定性测试到Instrumentation全解析
好的我会严格遵循所有要求和约束为您撰写一篇可直接发布到CSDN的技术博文。以下是正文内容。Charity Majors 谈 AI、确定性与可观测性为什么“吃下蔬菜”才是 AI 工程的关键如果你正在做 LLM 应用开发或者正在把 AI 能力接入自己的系统最近肯定绕不开一个问题AI 应用上线之后怎么排查问题传统后端出问题看一眼日志堆栈、查一下链路追踪几分钟就能定位。但换成 AI 应用之后这套经验突然失灵了。你面对的是一个“每次生成结果都可能不同”的黑盒。请求没有崩溃接口返回了 200但用户的反馈是“回答质量变差了”“偶尔会胡说八道”“同一句话问两次结果不一样”。这类问题靠监控 CPU、内存、接口延时根本发现不了因为它们不是“宕机”级别的问题而是“质量漂移”和“行为非确定性”带来的隐性故障。最近读了 Charity Majors 关于 AI、确定性Determinism与可观测性Instrumentation的一些分享她有一个非常直接的观点AI 系统确实带来了新的挑战但很多人试图跳过那些“基础但枯燥”的工程步骤想用更炫的方案解决 AI 问题这条路是走不通的。她把这些基础工程工作称为“吃你的蔬菜”Eating Your Broccoli——不好吃但必须吃。这篇文章我想围绕她的核心判断结合 AI 应用工程化的实际场景聊清楚三个问题为什么 AI 应用让传统监控手段失效确定性与非确定性在 AI 工程里到底意味着什么可观测性如何在 AI 应用里真正落地文章会包含可操作的代码示例、排查思路和工程建议。如果你正在把 AI Agent、RAG 或大模型 API 集成到生产环境这篇文章应该能帮你少踩一些坑。1. 这篇文章真正要解决的问题先给一个明确判断AI 应用开发最大的挑战不是“模型能力不够”而是“工程化能力跟不上”。很多团队从 Demo 到生产只差一步但就是这一步迈不过去。原因不是模型选型不对而是出了问题根本不知道去哪查。传统软件工程里系统的行为是可预测的。输入相同输出必然相同出了问题可以通过复现来排查。LLM 应用恰恰相反它的核心组件是概率模型同样的 prompt 在不同时间、不同参数下可能产生完全不同的输出。这种不确定性让“复现 bug”这个最基本的排查手段失效了。更麻烦的是AI 应用不是只有模型推理这一层。一个典型的 RAG 应用牵扯到文档解析与分块向量化与召回Prompt 模板与上下文组装模型推理参数temperature、top_p 等输出解析与后处理外部工具调用Agent 场景每一层都可能出错而且错误是逐层放大的。文档分块切错了召回质量就差上下文塞得太满模型就抓不住重点输出解析正则写错了前端拿到的就是空壳。这些问题在开发和测试环境里很难触发因为测试集太小数据太干净。Charity Majors 的观点很直接与其把希望寄托在“更好的模型”或“更聪明的框架”上不如先把最基础的工程能力补齐——测试、日志、追踪、指标、数据质量。这些工作看着枯燥但决定了你的 AI 应用能不能长期稳定运行。这篇文章适合的人群很明确正在把 LLM 能力接入生产系统的后端工程师已经在做 AI Agent 或 RAG 应用但被线上问题困扰的开发者负责 AI 应用稳定性、质量和用户体验的技术负责人。读完你会理解为什么“给 AI 应用加监控”这件事的逻辑跟传统监控不一样也会拿到一套可以直接用的思路和代码片段。2. AI 应用的可观测性为什么这么难确定性系统与非确定性系统要讲清楚可观测性先得从“确定性”说起。这是整个议题的核心也是 AI 工程和传统工程最本质的分水岭。2.1 确定性系统你习惯的那套调试方式传统后端系统比如一个典型的 Web 服务本质上是确定性系统给定同样的输入参数经过相同版本的代码逻辑在相同的环境下运行输出结果几乎一定是相同的。所以当用户报障时你可以在测试环境复现可以加日志、加断点一步步缩小范围。这种“复现-定位-修复-验证”的模式是大部分工程师的看家本领。传统监控体系也是围绕确定性系统设计的你关注 QPS、错误率、响应时间因为系统的行为曲线是可预测的异常意味着“偏离了基线”报警阈值可以预先设定因为你知道什么是“正常”。这套体系在确定性系统里非常有效。2.2 非确定性系统AI 引入了什么变量到了 LLM 应用情况变了。模型本身是概率系统它的输出天然带有随机性。即便 temperature 设成 0模型推理过程中的采样策略、上下文窗口中的微小变化也可能导致输出不同。但这还不是最麻烦的。真正让可观测性变难的是AI 应用的不确定性不只是模型层带来的而是整个链路叠加出来的层级不确定性来源传统系统是否有数据层向量库召回结果随文档更新变化无检索层相似度排序受 embedding 影响无Prompt 层模板微调、上下文长度变化无模型层采样随机性、模型版本更新无工具层Agent 调用外部 API 返回不稳定少数后处理层输出解析对格式敏感少数一层的不确定性可能还可以接受但当多层叠加时问题就变得极其复杂。你很难判断一次“回答质量下降”到底是哪一层出的问题。Charity Majors 提到的核心问题在这里非确定性系统不是不需要可观测性而是比确定性系统更需要可观测性只不过要观测的东西变了。在确定性系统里你要观测的是“系统是否偏离了预期行为”。在非确定性系统里你要观测的是“系统在不同输入、不同上下文下的行为分布”。前者看曲线后者看分布。2.3 一个比喻导航系统打个比方。传统后端系统就像一条固定轨道的地铁线路固定时刻表固定出问题只需要查是哪一站、哪一趟车。LLM 应用更像地图 App 的实时导航它会根据实时路况不断调整路线到达时间是一个预估区间。你不能问“为什么它这次走了这条路、上次走了那条路”——每次路况都不一样。但你可以观测的是导航是否始终在朝着目的地前进途中是否有绕路用户重新规划路线的频率是不是变高了这就是 AI 可观测性的核心思路不追求“精确复现”而是理解“行为分布”和“质量趋势”。3. 可观测性不等于监控Instrumentation 到底是什么很多团队说“我们要给 AI 应用上可观测性”实际做的是加了几张 Dashboard看 QPS、错误率和延迟。这不是可观测性这只是在原来监控面板上加了几个新指标。3.1 监控Monitoring与可观测性Observability的区别根据 Charity Majors 一贯强调的分类两者有一个经典对比监控你知道可能出现什么问题提前设定阈值问题发生时报警。它的前提是“已知的未知”。可观测性你不知道会出现什么问题但通过系统暴露的丰富数据你可以在事后探索、定位找出当初没预料到的“未知的未知”。AI 应用里的大多数严重问题恰恰属于“未知的未知”。比如Prompt 模板里一个标点符号的变化导致大范围回答质量下降向量库更新后某些类别的问题召回率骤降模型供应商更新了底层模型版本虽然 API 没变但风格大变用户以某种特定方式提问时Agent 会陷入死循环调用工具。这些问题不可能通过预设阈值发现。你必须让系统产生足够丰富、结构良好、可探索的数据才能在事后大海捞针。3.2 Instrumentation让你获得探索数据的能力“Instrumentation”这个词在中文里常被翻译为“埋点”“插桩”或“检测”但在可观测性语境下它更准确的含义是让系统把内部状态和行为以可查询的形式暴露出来。对于 AI 应用Instrumentation 不是简单地在日志里加几行 print而是要做到记录每次请求的完整上下文用户输入、检索到的文档、最终进入模型的 Prompt、模型输出、后处理结果。保留高基数属性用户 ID、会话 ID、模型版本、Prompt 版本、向量库版本、检索文档 ID 等。这些属性每个值都不同无法预聚合但正是后续排查的关键索引。建立请求链路一次用户请求可能内部调用了多次模型推理、多次向量检索、多次工具调用必须把这些步骤串联起来。输出结构化事件不是“字符串拼接日志”而是带字段的 JSON 结构方便后续分析。从工程实践来看这意味着两件事一是你的应用代码需要做好结构化日志和追踪二是你需要一个能存储和查询高基数数据的后端系统。3.3 AI 可观测性要回答的四类问题把 Instrumentation 做好了你应该能回答以下四类问题问题类型具体示例需要的数据发生了什么哪些请求回答质量差日志、质量评分为什么发生是不是检索召回失败上下文、检索结果、模型输出影响范围多大是某个用户、某个 prompt 版本还是所有流量高基数属性趋势如何变化模型升级后回答风格是否漂移时间序列、属性分布这四类问题的回答难度依次递增但它们都依赖同一件事从第一行代码开始就做好 Instrumentation。4. AI 工程里的“确定性测试”让系统至少在某些层面可预测有些人可能会问既然 AI 系统是非确定性的那“测试”还有意义吗测试的基本假设不就是“输入相同输出可预期”吗Charity Majors 在讨论中反复强调一个观点AI 系统确实非确定但你不能因此放弃确定性测试。恰恰相反你要把能确定的东西全部确定下来把不确定性限制在尽可能小的范围内。这就引出了“确定性测试”的实践思路。4.1 哪些层可以做成确定性测试AI 应用虽然整体是非确定性的但链路里有很多环节其实可以做成确定性测试第一层纯逻辑层。文档解析、文本分块、JSON 解析、后处理逻辑、工具调用的参数组装这些都是纯代码逻辑输入输出完全可预期。这一层没有任何借口不测试。第二层输出约束层。你可以通过 prompt 约束模型输出格式比如 JSON并在代码里做严格校验。虽然模型输出本身非确定但你可以断言“输出必须是合法 JSON”“必含字段必须存在”。第三层行为分布层。对于模型输出本身不追求精确断言而是断言“多次运行的行为分布符合预期”。比如在一个分类任务里10 次运行中至少 8 次返回同一个类别。现实中很多团队跳过了前两层直接对模型输出做模糊断言甚至完全不做测试。结果就是改了一个分块逻辑不确定坏了什么改了一个 Prompt 模板不确定影响了哪些场景。4.2 用测试用例锁定“可预测部分”这里的关键原则是把能锁死的东西全部锁死把不能锁死的东西用分布去描述。举个例子。在做 RAG 应用时向量检索结果的质量可以用“黄金数据集”来做确定性回归测试。你准备一组标准的 query-document 配对每次调整 embedding 模型、分块策略或向量库配置时都跑一遍这组数据看召回率是否下降。这个过程是确定性的同样的 query、同样的向量库、同样的检索参数召回结果必然相同。有了这层保障你才敢放心去调整那些真正非确定的部分——模型参数、Prompt 设计。4.3 用“行为测试”覆盖非确定性部分对于真正非确定的模型输出可以利用 LLM 作为评测器LLM-as-a-judge来做行为测试。这部分测试不断言精确输出而是断言“输出质量是否达标”。例如你可以设置一个测试输入一组标准问题让模型回答然后让另一个 LLM 打分或者用规则检查断言分数超过阈值。这种测试虽然每次结果可能略有浮动但它能捕捉到严重的回归——比如 Prompt 改坏了、上下文拼接错误、召回内容不相关。把这两类测试结合起来黄金数据集召回测试 → 覆盖检索层纯逻辑单元测试 → 覆盖解析、后处理层LLM-as-a-judge 行为测试 → 覆盖模型生成层输出 schema 校验 → 覆盖接口层。这样测试的覆盖范围就远大于只测模型输出的做法。5. 完整示例LLM 应用的可观测性 Instrumentation 实践理论讲了不少接下来进入实操。下面我用一个典型的 RAG 应用简化版来演示如何从代码层面做好 AI 应用的可观测性。5.1 场景说明假设我们构建一个“内部知识库问答助手”用户提问后系统先检索向量库中的相关文档片段再拼接 Prompt 调用 LLM最后返回答案。这是最常见的 RAG 应用形态。我们的目标是每次请求都产出结构化的、包含完整上下文的追踪数据方便事后排查“为什么这个回答质量差”。5.2 代码示例结构化事件记录首先实现一个简单的结构化日志模块。这里使用 Python 的logging JSON 格式化生产环境可以替换为 OpenTelemetry 或其他 tracing 框架。# 文件路径app/observability.py import json import logging import time import uuid from contextvars import ContextVar request_id_var: ContextVar[str] ContextVar(request_id, defaultunknown) class JsonFormatter(logging.Formatter): 将日志记录格式化为 JSON便于后续采集与分析。 def format(self, record: logging.LogRecord) - str: log_entry { timestamp: time.strftime(%Y-%m-%dT%H:%M:%S%z, time.localtime(record.created)), level: record.levelname, logger: record.name, request_id: request_id_var.get(), message: record.getMessage(), } # 允许通过 extra 字段注入自定义属性 if hasattr(record, props): log_entry.update(record.props) return json.dumps(log_entry, ensure_asciiFalse) def setup_logging(): handler logging.StreamHandler() handler.setFormatter(JsonFormatter()) root_logger logging.getLogger() root_logger.handlers [handler] root_logger.setLevel(logging.INFO) def log_event(logger: logging.Logger, event: str, **props): 打印结构化事件自动附带 request_id。 logger.info(event, extra{props: props})核心思路是所有日志自动携带request_id事件的细节通过props字典以字段形式记录而不是拼在 message 字符串里。这样后续就可以在日志系统里按request_id快速聚合某个请求的全部事件。5.3 代码示例RAG 链路的完整埋点接下来是 RAG 的核心流程。每次请求我们会记录检索阶段和生成阶段的完整上下文。# 文件路径app/rag_service.py import logging import uuid import time from app.observability import log_event, request_id_var logger logging.getLogger(__name__) class MockVectorStore: 简化的向量库客户端用于演示。 def search(self, query: str, top_k: int 3): # 实际项目中替换为真实的向量检索调用 return [ {doc_id: doc-001, score: 0.91, content: 可观测性是指系统内部状态的可探索能力。}, {doc_id: doc-002, score: 0.85, content: 现代可观测性基于高基数数据。}, {doc_id: doc-003, score: 0.72, content: 传统监控与可观测性有本质区别。}, ][:top_k] class MockLLM: 简化的 LLM 客户端用于演示。 def generate(self, prompt: str, temperature: float 0.2): # 实际项目中替换为真实的大模型调用 return 可观测性关注系统内部状态而监控关注已知问题。 class RagService: def __init__(self): self.vector_store MockVectorStore() self.llm MockLLM() def answer(self, user_query: str, trace_context: dict | None None): request_id uuid.uuid4().hex request_id_var.set(request_id) start_time time.time() log_event(logger, request_start, request_idrequest_id, user_queryuser_query, trace_contexttrace_context) # 阶段一检索 retrieval_start time.time() docs self.vector_store.search(user_query, top_k3) retrieval_latency_ms (time.time() - retrieval_start) * 1000 log_event(logger, retrieval_completed, request_idrequest_id, retrieval_latency_msretrieval_latency_ms, doc_ids[d[doc_id] for d in docs], scores[d[score] for d in docs]) # 阶段二组装 Prompt context \n\n.join([d[content] for d in docs]) prompt f基于以下资料回答问题\n\n{context}\n\n问题{user_query} log_event(logger, prompt_assembled, request_idrequest_id, prompt_template_versionv1.2, context_char_countlen(context), context_doc_countlen(docs)) # 阶段三调用模型 llm_start time.time() answer_text self.llm.generate(prompt, temperature0.2) llm_latency_ms (time.time() - llm_start) * 1000 log_event(logger, llm_completed, request_idrequest_id, llm_latency_msllm_latency_ms, # 注意完整记录 prompt 和 answer 会有数据成本 # 实际生产环境请评估内容安全与隐私合规要求。 answer_lengthlen(answer_text)) # 阶段四输出清洗 cleaned_answer answer_text.strip() total_latency_ms (time.time() - start_time) * 1000 log_event(logger, request_completed, request_idrequest_id, total_latency_mstotal_latency_ms) return {request_id: request_id, answer: cleaned_answer}这段代码演示了几个关键实践按阶段记录事件request_start、retrieval_completed、prompt_assembled、llm_completed、request_completed事件命名清晰后续可以在日志系统里直接按事件类型查询。每个事件携带高基数属性request_id、doc_ids、scores、prompt_template_version、latency_ms。这些字段每个请求都不同传统监控体系不会去聚合它们但正是在排查问题时最有价值的索引。不记录敏感内容注释里明确提示完整 Prompt 和 answer 的记录可能涉及隐私和内容安全。生产环境应根据合规要求决定是否记录或者截断后记录。5.4 代码示例运行时追踪上下文传递上面的示例用ContextVar传递request_id这只适用于单线程异步场景。如果应用中有子任务并行执行或者调用下游服务需要引入真正的链路追踪。下面给出一个更通用的 OpenTelemetry 式封装思路不需要完整引入框架但足以说明追踪上下文如何在调用链中传递。# 文件路径app/tracing.py import contextlib import uuid import json from dataclasses import dataclass, field from typing import Optional dataclass class Span: trace_id: str span_id: str parent_span_id: Optional[str] name: str attributes: dict field(default_factorydict) start_time: float 0.0 end_time: float 0.0 def to_dict(self): return { trace_id: self.trace_id, span_id: self.span_id, parent_span_id: self.parent_span_id, name: self.name, attributes: self.attributes, duration_ms: (self.end_time - self.start_time) * 1000, } class Tracer: 极简的基于 span 的追踪器用于演示 span 概念。 生产环境建议使用 OpenTelemetry SDK 并接入 Collector。 def __init__(self): self.spans: list[Span] [] contextlib.contextmanager def start_span(self, name: str, trace_id: Optional[str] None, parent_span_id: Optional[str] None, **attributes): tid trace_id or uuid.uuid4().hex sid uuid.uuid4().hex span Span(trace_idtid, span_idsid, parent_span_idparent_span_id, namename, attributesattributes) span.start_time __import__(time).time() try: yield span finally: span.end_time __import__(time).time() self.spans.append(span) print(json.dumps(span.to_dict(), ensure_asciiFalse))这个简化版 Tracer 的意图不是让你在生产环境自己实现 tracing而是帮你理解 span 的核心概念trace_id串联整条链路parent_span_id表达嵌套关系attributes记录关键字段。生产环境直接用 OpenTelemetry 就好。5.5 如何运行与验证在实际的 Demo 项目里你可以像下面这样调用# 文件路径app/main.py import logging from app.observability import setup_logging from app.rag_service import RagService setup_logging() service RagService() result service.answer(什么是可观测性, trace_context{user_id: u-12345}) print(最终回答:, result[answer])运行后标准输出会按时间顺序输出 JSON 格式的事件每个事件都带同一个request_id。接着在日志系统里执行一次按request_id的搜索就能看到这个请求从开始到结束的完整链路。验证成功的标准很简单五个阶段的事件是否都出现了同一个request_id是否贯穿所有事件每个事件是否包含足够的高基数属性doc_ids、scores、latency 等把输出交给一个不懂代码的人他是否能根据日志复述这个请求经历了哪些阶段。如果以上都满足说明你的 Instrumentation 已经达到可以排查问题的级别。6. 运行效果与可观测性数据的使用方式6.1 一次请求的日志输出示例上面的代码运行后日志输出大致如下具体字段值根据运行环境会不同{timestamp: 2025-01-12T10:24:310800, level: INFO, logger: app.rag_service, request_id: 3f2a1b9c..., message: request_start, user_query: 什么是可观测性, trace_context: {user_id: u-12345}} {timestamp: 2025-01-12T10:24:310800, level: INFO, logger: app.rag_service, request_id: 3f2a1b9c..., message: retrieval_completed, retrieval_latency_ms: 12.3, doc_ids: [doc-001, doc-002, doc-003], scores: [0.91, 0.85, 0.72]} {timestamp: 2025-01-12T10:24:310800, level: INFO, logger: app.rag_service, request_id: 3f2a1b9c..., message: prompt_assembled, prompt_template_version: v1.2, context_char_count: 156, context_doc_count: 3} {timestamp: 2025-01-12T10:24:310800, level: INFO, logger: app.rag_service, request_id: 3f2a1b9c..., message: llm_completed, llm_latency_ms: 850.2, answer_length: 36} {timestamp: 2025-01-12T10:24:310800, level: INFO, logger: app.rag_service, request_id: 3f2a1b9c..., message: request_completed, total_latency_ms: 870.1}6.2 如何判断 Instrumentation 是否成功有几个评估维度第一能否回答“发生了什么”。给定一个用户反馈“我昨天问的问题回答质量很差”你能通过用户 ID 和大致时间范围找到那次请求并看到当时的检索文档列表和 Prompt 模板版本吗如果能说明全景式记录做到了。第二能否回答“为什么发生”。找到那次请求后你能不能看出是检索召回了不相关的文档还是 Prompt 模板版本过旧还是模型输出解析失败这里要求链路事件足够完整。第三能否回答“影响范围多大”。你能否按prompt_template_version聚合发现 v1.2 版本覆盖的流量中回答长度普遍偏短或者按doc_ids聚合发现某个文档被频繁召回但回答质量评分很低如果这三个问题都能回答那就说明你的可观测性体系已经真正工作了。如果答不出来问题通常在两个方面一是记录的数据不够结构化二是没有选对高基数属性。6.3 一次失败排查的完整路径假设有用户反馈“有个问题回答得完全不对。”接下来你可以这样排查从日志系统按user_query或用户 ID 模糊搜索找到request_id按request_id拉出全部事件查看retrieval_completed事件中的doc_ids和scores确认召回结果是否相关查看prompt_assembled事件中的prompt_template_version和context_char_count确认 Prompt 是否异常查看llm_completed事件中的answer_length对比历史同类请求的分布判断输出是否偏离。每一步都在回答一个具体问题这就是可观测性数据在排查中的真实用法。7. AI 应用可观测性落地中的常见问题与排查思路这里整理一些实际项目中经常遇到的问题比理论更具体也更贴近一线开发者的日常。7.1 常见问题表格问题现象可能原因排查方式解决方案日志里有事件但缺少 request_idContextVar 没有正确传递或异步任务丢上下文检查异步入口是否 reset 了 ContextVar用框架提供的上下文传递机制或在消息头显式传递 trace_id检索阶段耗时很高但回答质量差召回了大量低分文档或 embedding 与 query 不匹配查看scores分布检查向量库配置调整 top_k、优化分块策略、升级 embedding 模型回答长度突然变短Prompt 模板或模型版本变更按prompt_template_version聚合回答长度回滚到正常版本或在灰度环境验证后再全量输出不是合法 JSON模型输出被截断或格式漂移查看llm_completed的原文和解析错误日志增加重试机制、使用结构化输出约束、增加格式校验同一类问题的回答质量持续下降向量库中文档陈旧或索引损坏检查向量库变更记录和文档更新时间定期重建索引、监控文档新鲜度日志数据量太大成本高无差别记录全部 Prompt 和回答查看存储成本占比分析哪些字段使用率低分层采样核心链路全量记录次要链路按比例采样7.2 深入分析为什么“日志有但查不到”这是最让人头疼的情况。团队确实加了日志但排查时发现日志系统里按request_id一搜只能搜到部分事件链路是断的。常见原因有两个。第一个是异步任务没有传递 request_id。Python 里用了threading或asyncio但没有把 ContextVar 的值传到子任务。修复方式是在创建子任务时显式传递或者在消息队列的消息头里带上 trace_id。第二个是日志采集端有采样策略。很多日志收集器默认会对高吞吐日志做采样比如只采集 10%。这在实际排障时是灾难因为你需要的是一个完整请求的链路而不是随机抽样后的碎片。建议对包含request_id的事件关闭采样策略或至少保留 100% 请求的元数据事件。7.3 深入分析如何设计高基数属性高基数属性high cardinality attributes是 Charity Majors 讲可观测性时反复提及的概念。它的意思是“每个请求都不同”的字段比如user_id、request_id、doc_id、prompt_version。这类属性不适合放进传统监控系统做聚合。传统监控的度量数据通常是预聚合的比如按分钟求平均值高基数属性会让预聚合瞬间爆炸。正因为如此传统监控工具在应对 AI 应用时显得力不从心。设计高基数属性的核心原则是在写入日志的这一刻把将来想查询的任何维度都提前放进去。比如当你怀疑“某个文档导致回答质量变差”时就必须在retrieval_completed事件里记录doc_ids列表。如果当时没记事后再想查就永远查不到了。日志记录无法回到过去补充。7.4 成本控制的平衡策略完整记录 AI 应用的链路数据确实有成本尤其是 LLM API 调用量大时日志量会非常可观。这里给出几条可行策略元数据全量内容按需request_id、doc_ids、latency、版本号等元数据全部记录完整 Prompt 和输出内容只在需要调试时开启。分层采样普通请求按 10% 采样但涉及用户明确投诉、质量评分低于阈值的请求全量记录。按阶段区分检索阶段事件体积小全量记录LLM 输出阶段体积大只记录截断摘要。存储分级热数据存储在查询快的引擎里存 7 天冷数据转入低成本存储做长期归档。成本控制的目的不是省那点存储费而是让你在需要数据时能查到数据同时不因为成本而被迫关闭可观测性。8. 最佳实践与工程建议把“吃蔬菜”变成流程最后这部分我想从更宏观的工程层面总结建议。Charity Majors 那句 “eating your broccoli” 的真正含义是AI 工程没有捷径该做的脏活累活一件都不能少。8.1 建立“从生成到上线”的确定性回归机制不要等 AI 应用上线后再考虑测试。从第一行代码开始就应该意识到这个系统的行为分布随时可能变化。推荐流程是每次修改代码先跑纯逻辑的单元测试和黄金数据集检索测试每次修改 Prompt 模板跑一遍行为测试LLM-as-a-judge并对比上一版本的评分分布每次升级模型版本或 embedding 模型先跑完整的回归套件灰度观察一段时间再全量。这套流程不复杂但需要有人持续执行。真正的难点不是工具而是团队愿不愿意在看起来“没意思”的测试上花时间。8.2 用“质量评分”统一语言团队协作时最大的问题是没有统一的质量标准。后端说“接口正常”算法说“模型效果还行”测试说“好像没啥大问题”——这种模糊沟通在 AI 工程里就是灾难。建议引入一个简单的质量评分机制。每次请求完成后用一个轻量规则或 LLM judge对回答质量打分随其他事件一起记录。评分维度可以包括相关性、完整性、格式正确性。这样团队就有了一组可量化的数据回答质量的平均分、低分请求的占比、质量评分在哪个 Prompt 版本上显著下降。没有评分你只能靠用户投诉被动发现问题有了评分你可以在用户感知之前主动干预。8.3 从“链路追踪”走向“行为分析”链路追踪告诉你“一次请求走了哪些步骤”但对于 AI 应用来说这还不够。你还需要回答“一组相似请求的行为分布是什么样的”。比如你可以按用户意图query 聚类结果分组观察每个分组的回答平均分随时间的走势也可以按检索文档分组找出哪些文档经常被召回但回答质量反而不高。这种“行为分析”是 AI 可观测性的高阶形态也是真正把数据变成决策的地方。从实践路径来看先做好链路追踪再逐步积累行为分析能力是更稳妥的顺序。8.4 生产环境的安全与合规提醒可观测性数据本身也可能引入风险。记录用户 query 和模型输出时要充分考虑用户 query 可能包含个人隐私信息模型输出可能包含非预期的有害内容检索到的内部文档如果记录进日志可能造成敏感信息泄露。建议在日志链路中做脱敏处理或在合规要求下限制内容日志的保留时长。安全边界与可观测性能力需要同时设计不能等到出了问题再补。8.5 团队协作与失败预案AI 应用失败的可能性远高于传统系统。模型供应商宕机、API 限流、输出质量漂移这些都可能发生。团队需要提前约定失败预案模型调用失败时是否有降级方案比如切换到小模型或本地模型质量评分连续下跌时谁负责决策是否回滚向量库索引损坏时恢复流程是什么这些预案在平时看起来“用不上”但一旦发生故障它们就是团队最后的防线。建议把这类预案纳入例行的故障演练。9. 总结确定性留给系统不确定性留给探索回顾整篇文章我想再次强调 Charity Majors 分享中最核心的一个判断AI 系统确实引入了前所未有的复杂性和非确定性但这不代表你可以跳过那些成熟的工程实践。恰恰相反你需要把更多精力投入到基础工程中才能让 AI 应用在生产环境里稳定运行。从实践角度看有三个动作最值得立刻去做第一审视你的应用是否具备完整的 Instrumentation。没有结构化的、带完整上下文的日志AI 应用出问题时就是盲人摸象。第二把能确定性的部分全部确定下来。通过单元测试、黄金数据集、schema 校验和回归测试把可预测的环节锁定住让不确定性只发生在模型生成层。第三用质量评分建立统一的反馈语言。让系统能够主动告诉你“质量在变差”而不是被动等待用户投诉。“吃蔬菜”确实不如“吃甜点”愉快。但真正能把 AI 应用做到生产级稳定运行的团队一定是那些愿意在测试、可观测性、数据质量和失败预案上花功夫的团队。模型能力的差距可以用选型缩短工程能力的差距却只能靠一步一步补齐。