
当一个客服机器人刚上线时你收到用户投诉“它明明两天前回答得很好今天同样的问法结果完全跑偏了。”于是你尝试复现但用同样的提示词试了三次三次答案都不一样。你开始怀疑是模型升级、上下文被污染、Embedding 检索到了错误片段还是用户输入被格式化时出了问题。最可怕的是你连“它到底当时看到了什么”都不知道。这是 AI 应用工程化最典型的一道坎模型本身是概率性的而你的系统却在一个又一个环节叠加着不确定性。如果不从工程上把每次调用的上下文、输入、输出、链路和成本完整记录下来AI 应用一旦进入生产环境就几乎等于“睁眼瞎”。Charity Majors 是她长期研究可观测性、分布式系统调试的工程师。她在这个话题上的态度一贯鲜明AI 应用应该被当成分布式系统来观测而不是当成一个黑盒模型来供奉那些枯燥、不性感但必须做的可观测性工程就是科技行业需要吃下的“西兰花”。本文就从确定性、仪器化Instrumentation和 AI 可观测性的关系出发讲清楚为什么 AI 应用比传统软件更需要观测并给出一个可直接上手的最小可观测性示例。1. 这篇文章真正要解决的问题很多人第一次接触大模型时最强烈的感受是“惊喜”到了第二次开始做工程化感受就会变成“失控”。失控来自三个层面输出不可复现同样的问题模型可能给出不同答案而且说不清变化来自采样、模型版本、提示词、上下文、检索结果还是外部工具。链路不可追踪一次 Agent 调用可能涉及大模型、向量数据库、外部 API、工具函数。任何一环出错错误都会被模型“包装”成一段流畅但错误的话。成本与质量很难归因如果每个请求花费 0.02 美元10 万请求就是一笔不小的成本。到底是哪个用户、哪个 prompt、哪类上下文触发了高 token 消耗没有观测数据就无法回答。这些问题之所以棘手根本原因不是“模型不够聪明”而是观测能力没有跟上。传统软件靠日志、指标、链路追踪就能定位问题但 AI 应用多了一层无法用日志简单表达的“语义信息”模型到底读了什么、生成了什么、为什么选了这个结果。要让 AI 应用可调试、可评估、可回滚就必须补齐这层观测。因此这篇文章要解决的核心问题不是“怎么调 prompt”而是如何为 AI 应用建立一套可观测性体系让概率性的模型行为变得可以被分析、被比较、被控制。如果你正在做 AI 应用开发、Agent 平台、大模型集成或者在负责承载 AI 应用的平台基础设施这篇文章适合你。读完你会明白Determinism确定性不是靠“把 temperature 设为 0”就能实现的真正可靠的确定性来自 instrumentation仪器化所建立的数据基础。2. 核心概念Determinism 与 Instrumentation2.1 确定性在传统工程中意味着什么在传统软件开发里确定性是一条默认的工程假设。你写一个函数输入2 2输出就应该永远是4。如果某天输出变成了5这被称为 Bug它通常可以复现、可以在调试器里单步执行、可以通过加日志缩小范围、最终通过代码修复。这种确定性的另一面是可解释性栈、日志、事务 ID、异常堆栈、数据库查询记录这些信息构成了一张因果关系网。出了问题顺着网就能找到源头。在分布式系统中确定性被弱化了一些网络可能超时队列可能乱序节点可能宕机。但我们仍然可以用请求 ID、链路追踪、结构化日志来重建“这次请求的行为轨迹”。换句话说传统分布式系统通过观测重新获得确定性。2.2 仪器化为什么是可观测性的前提Instrumentation中文常翻译为仪器化或插桩指的是在软件中主动埋入数据采集点打印日志、记录指标、生成调用链。它是一切可观测性系统的前提。没有 instrumentation就没有数据没有数据就没有可观测性。这听起来像废话但很多团队在实际操作中顺序是反的先搭监控大屏再讨论告警最后才考虑埋点。结果大屏上只有 CPU、内存这种基础设施指标业务和模型指标全空白。正确的顺序是先埋点再采集再可视化再告警。这也是 Charity Majors 和 Honeycomb 团队反复强调的“telemetry-driven development”把遥测数据当作开发的一部分而不是运维的附属品。2.3 AI 应用改变了什么从确定性到概率性大模型给工程带来的最大变化是把“确定性”从默认假设变成了需要额外设计的目标。传统函数是输入决定输出大模型输出的是概率分布上的采样结果。即使 temperature 设为 0很多模型仍可能因为算子实现、batch 策略、量化方式而出现微小变化。更复杂的是AI 应用的输出不仅依赖模型还依赖系统给它构建的上下文检索到的文档、Agent 上一步的工具返回、多轮对话历史的截断方式。可以把 AI 应用想象成一个极度依赖外部环境的“演员”同一个剧本prompt不同化妆间context、不同对手工具、不同场次模型版本都会导致台词的细微变化。你要想“复现”一场表演必须完整记录现场所有变量。下面的表格可以更直观说明这种差异维度传统软件AI 应用输入结构化、有明确边界自然语言、上下文、向量检索结果输出稳定、可预期概率性、不可完全复现错误形态异常、堆栈、错误码流畅但错误的文本、语义偏差调试方式断点、日志、堆栈需要记录模型输入输出、检索结果、工具调用可复现性高低需要大量上下文与参数才能逼近成本特征相对固定随 token、模型版本、上下文长度波动这张表的结论很简单AI 应用的可观测性不能套用传统软件的“日志 指标”模式需要增加专门针对模型调用和上下文的追踪能力。3. 为什么说可观测性是 AI 时代的“西兰花”很多人用“吃西兰花”比喻那些正确但无聊的事情。健康和好吃常常是矛盾的工程上也一样做新模型评测很刺激搭一个新 Agent 框架很兴奋但给系统加埋点、统一 trace 规范、做上下文脱敏确实没有那么多“成就感”。Charity Majors 在讨论 AI 与可观测性时的核心态度就是强调这个“吃西兰花”的时刻。她并不是说不要研究 AI而是说如果你要把 AI 放进生产系统就必须用生产系统的标准来要求它。生产系统的标准不是“demo 能跑通”而是“故障能定位、变更能评估、成本能归因、行为能解释”。但这个逻辑很容易被误解。一种常见误区是把 prompt 调优当成解决一切质量问题的突破口。于是团队花大量时间改措辞、加 few-shot却不知道线上用户的实际输入分布也不知道是哪一类输入让模型频频出错。没有观测数据prompt 再精妙也是凭感觉。另一种误区是把离线评测集当成“确定性”的来源。评测集能告诉你模型平均分有没有涨但它回答不了“为什么线上这个用户的问题会触发幻觉”“为什么昨天和今天的回答差异这么大”。评测是必要的但它替代不了 trace。更隐蔽的误区是以为接入一个 LLM 监控平台就算“可观测”了。实际上很多 LLM 监控 SaaS 只记录了模型调用本身的输入输出却与业务链路、用户请求、Agent 工具调用完全割裂。你看到模型“某次回答很怪”却不知道这个用户之前的 5 轮对话是什么、检索到了什么、哪一步决策导致模型采用了错误信息。所以AI 时代的可观测性不是“可选项”也不是“上线后再补”而是“成为生产级 AI 应用的基本条件”。它不性感的点在于要处理大量数据格式、字段命名、脱敏策略、链路打通但真正让 AI 系统从“技术 Demo”走向“业务系统”的关键恰恰是这些基础工作。4. AI 应用可观测性到底要观测什么给 AI 应用做可观测性不能只盯“模型返回了什么”。你需要从三个层次分别采集数据。4.1 模型层模型层关注一次 LLM 调用的直接特征模型名称与版本请求参数temperature、max_tokens、top_p 等实际发送给模型的 prompt以及经过处理后拼入的上下文生成结果response 内容、是否被截断用量prompt tokens、completion tokens、总 tokens延迟首 token 延迟、总耗时成本按 token 单价换算后的请求成本是否命中缓存这一层能回答“这个调用花了多少钱、多慢、输入和输出各是什么”。4.2 调用链路层模型调用不是孤立的。在 RAG 或 Agent 应用里它往往是链路中间的一环用户请求 ID / trace ID上游业务上下文用户 ID、会话 ID、来源页面检索阶段query 是什么、检索了哪个向量库、返回了哪些 chunk、相关性得分上下文构造哪些检索结果被拼入 prompt哪部分被截断工具调用Agent 选择了哪个工具、传入了什么参数、工具返回了什么代码异常超时、限流、内容安全过滤、JSON 解析失败这一层能回答“一次端到端请求到底发生了几次模型调用每一步靠什么数据决策”。4.3 业务层模型返回的文本本身不是业务结果业务结果应该是用户在业务体系里定义的成功或失败。用户是否满意点赞、复制、点击、人工介入业务是否达成客服工单是否解决、推荐是否被接受、代码助手生成代码是否被采纳评估分数离线测试或在线打分模型给出的质量分数人工反馈用户举报、纠正、二次修改业务层的作用是给模型层面的观测数据“定性”。下面是一个简化的字段对照表方便你规划自己的数据模型层次示例字段主要用途模型层model、prompt、response、tokens、latency成本、资源、模型行为对比链路层trace_id、retrieval_chunks、tool_name行为归因、故障定位业务层user_feedback、task_success、eval_score业务价值判断只有把这三层数据通过统一的 trace ID 关联起来才算真正建立起了 AI 应用的可观测性。5. 给 AI 应用加上可观测性最小示例下面用一个最小示例演示如何用 OpenTelemetry 为 LLM 调用埋点并把跟踪数据导出到控制台和 OTLP Collector。为了避免版本问题这里不锁定 OpenTelemetry 的具体版本安装时请以官方最新稳定版为准。关键 API 在多个版本中基本一致。5.1 环境准备假设你使用 Python 3.10 以上版本。先创建虚拟环境并安装依赖python -m venv venv source venv/bin/activate pip install opentelemetry-api \ opentelemetry-sdk \ opentelemetry-exporter-otlp如果你只是想快速验证不需要 Collector可以先使用ConsoleSpanExporter把 span 打印到终端。5.2 用 ConsoleSpanExporter 验证手动埋点创建一个文件ai_observability.py# 文件路径ai_observability.py from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor from opentelemetry.sdk.resources import Resource resource Resource.create({service.name: ai-demo}) provider TracerProvider(resourceresource) # 为了方便本地验证使用 ConsoleSpanExporter exporter ConsoleSpanExporter() provider.add_span_processor(SimpleSpanProcessor(exporter)) trace.set_tracer_provider(provider) tracer trace.get_tracer(__name__) def call_llm(prompt: str) - dict: # 这里替换成你真实使用的模型客户端 # 例如 OpenAI、Anthropic、本地模型等 return { response_text: 这是一个模拟的模型输出, usage: { prompt_tokens: 120, completion_tokens: 45, total_tokens: 165, }, model: your-model-name, } def run_chat(prompt: str) - str: # 创建 span代表一次 LLM 调用 with tracer.start_as_current_span(llm.call) as span: span.set_attribute(llm.request.model, your-model-name) span.set_attribute(llm.request.temperature, 0.0) span.set_attribute(llm.request.prompt, prompt) result call_llm(prompt) response_text result[response_text] usage result[usage] span.set_attribute(llm.response.text, response_text) span.set_attribute(llm.usage.prompt_tokens, usage[prompt_tokens]) span.set_attribute(llm.usage.completion_tokens, usage[completion_tokens]) span.set_attribute(llm.usage.total_tokens, usage[total_tokens]) span.set_attribute(llm.cost.estimated_usd, 0.001) return response_text if __name__ __main__: text run_chat(请介绍一下可观测性) print(f模型输出{text})这段代码的核心逻辑初始化TracerProvider设置服务名为ai-demo。注册ConsoleSpanExporter所有 span 会打印到终端。用tracer.start_as_current_span(llm.call)创建一个 span。在 span 上写入模型名称、temperature、prompt、response、token 用量等属性。模拟调用模型并返回结果。运行命令python ai_observability.py预期会看到类似下面的输出{ name: llm.call, context: { trace_id: 0x..., span_id: 0x... }, attributes: { llm.request.model: your-model-name, llm.request.prompt: 请介绍一下可观测性, llm.response.text: 这是一个模拟的模型输出, llm.usage.total_tokens: 165 } }模型输出模型输出这是一个模拟的模型输出如果这一步能输出 span说明基础埋点已经生效。5.3 用 OTLP 导出到 OpenTelemetry Collector控制台输出只适合本地验证生产环境更常使用 OTLP 协议把数据发送给 Collector 或后端平台。修改ai_observability.py把导出器换成OTLPSpanExporter# 文件路径ai_observability_otlp.py from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.resources import Resource from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter resource Resource.create({service.name: ai-demo}) provider TracerProvider(resourceresource) otlp_exporter OTLPSpanExporter( endpointhttp://localhost:4317, insecureTrue, ) provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) trace.set_tracer_provider(provider)然后准备一个最简 Collector 配置otel-collector-config.yamlreceivers: otlp: protocols: grpc: http: processors: batch: exporters: logging: verbosity: detailed service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [logging]启动 Collectorotelcol --config otel-collector-config.yaml再运行修改后的脚本python ai_observability_otlp.py此时 Collector 会收到 trace 并通过 logging exporter 输出验证链路已经打通。5.4 把 trace_id 写入业务日志为了让业务日志与模型调用关联建议在入口处获取当前 trace_id并写入日志字段。示例from opentelemetry import trace def get_current_trace_id() - str: span trace.get_current_span() span_context span.get_span_context() if span_context.is_valid: return format(span_context.trace_id, 032x) return 0 * 32在业务流程中调用trace_id get_current_trace_id() logger.info(chat request received, extra{trace_id: trace_id})这样在查看业务日志时可以通过 trace_id 跳到对应的链路追踪详情实现日志与 trace 的双向关联。6. 运行效果与验证运行python ai_observability.py后按下面顺序判断是否成功程序没有报错且打印出“模型输出...”。控制台打印出一个包含llm.call的 span 对象。span 中包含trace_id、span_id、llm.request.prompt、llm.response.text、llm.usage.total_tokens等属性。如果使用 OTLP 导出还应确认Collector 启动时没有端口冲突。脚本执行后Collector 日志出现对应 span 输出。如果数据没有到达先用grpc_health_probe或curl检查端口连通性。最容易出问题的环节通常是OTel SDK 版本变化导致部分配置项名称不一致。安装依赖时漏装了opentelemetry-exporter-otlp。Collector 配置格式不合法启动时报错。防火墙或容器网络阻止了 4317 端口访问。7. 常见问题与排查方法问题现象可能原因排查方式解决方案控制台没有输出 span没有调用with tracer.start_as_current_span在run_chat内打印日志确认函数是否执行确保进入了 span 的上下文管理块span 有输出但属性缺失属性名写错或属性值类型不是 string/int查看代码中set_attribute调用统一字段命名参考官方语义约定OTLP 数据没有到达 Collectorendpoint 配置错误或网络不通检查 Collector 日志和端口监听改为insecureTrue确认 endpoint 地址trace_id 全是 0当前没有活跃 span 上下文在start_as_current_span内部获取 trace_id确认调用get_current_span的时机成本数据统计不准只记录了 model 层没有关联业务层检查 trace 关联是否打通从主入口生成 trace_id贯穿整个请求prompt/response 泄露敏感信息埋点采集了原始对话内容检查埋点字段是否包含 PII在采集端做脱敏只保留必要字段模型调用超时但无 trace客户端超时时间太短异常被吞掉在异常捕获处手动创建 span用record_exception记录异常并标记状态这些排查思路不复杂但都有同一个前提数据进入统一模型之前必须想清楚字段规范和脱敏边界。否则后面做告警、做分析都会寸步难行。8. 工程最佳实践在不确定性里重建确定性8.1 把可观测性设计在接口层而不是事后补很多团队先把 LLM 调用写进了业务代码等发现问题后才想加埋点结果需要改所有调用点。更推荐的做法是封装一个统一的模型调用入口class LLMClient: def __init__(self, model_name: str, temperature: float 0.0): self.model_name model_name self.temperature temperature def complete(self, prompt: str) - LLMResult: with tracer.start_as_current_span(llm.call) as span: span.set_attribute(llm.request.model, self.model_name) span.set_attribute(llm.request.temperature, self.temperature) # 调用真正的模型 SDK ... span.set_attribute(llm.response.text, result) return result只要所有调用都经过这个类埋点就是全量的。这比到处埋print或中间件更可控。8.2 用评估和数据双向锁定变更模型在变、prompt 在变、上下文策略也在变。每次变更至少要回答两类问题指标上离线评测集的分数有没有回退数据上线上 trace 里真实用户的输入分布、失败模式有没有变化只盯评测集会脱离真实场景只盯 trace 又会缺少全局质量判断。两者必须结合。8.3 成本归因到 trace 维度不要只统计“今天花了多少钱”要把成本关联到具体 trace、用户、场景和模型版本。这样当模型的 token 消耗异常上涨时你能立刻定位到是哪些请求、哪类 prompt 造成的。具体做法在每次 LLM 调用的 span 中加入 estimated_cost_usd、currency、model_revision 等字段。成本数据可以和调用链一起导出避免单独维护一份容易对不上的账单。8.4 安全与合规边界埋点采集的是 AI 应用的“原料”里面可能包含用户提问、系统 prompt、检索文档、模型输出。这些字段都可能包含敏感信息。推荐从设计阶段就把字段分成三类明文可观测模型名、token 数、延迟、错误码。可加密可观测用户 ID、会话 ID做好哈希或加密。尽量不采集原始 prompt 中的身份证号、电话、邮箱等 PII如必须采集先脱敏再入库。尤其要注意不要把 API Key、内部系统 prompt 的完整内容作为 attribute 打到 trace 里。对内部提示词建议只记录版本号或哈希值。8.5 为 Agent 系统设计父子链路Agent 应用的一个用户请求通常会触发多次模型调用和多次工具调用。如果只给每次模型调用建 span你会看到一大堆孤立的调用无法判断决策过程。建议整个 Agent 执行的入口创建一个总 span每次工具调用创建子 span模型调用作为工具内部的子 span。通过父子结构还原“先检索、再决策、再调用工具、最后生成回复”的完整路径。8.6 关注语义约定但不要被字段名绑死OpenTelemetry 社区和厂商都在推进生成式 AI 的 Semantic Conventions包括gen_ai.*前缀等。不过这些规范仍在快速演进不同后端对字段的支持也不一致。更稳妥的策略是在业务代码里定义一个稳定的内部模型再在导出层映射到后端约定的字段。这样即使后端字段调整你也不需要改业务代码。9. 总结与后续学习方向Charity Majors 谈论 AI 与可观测性时本质上是在提醒工程师无论模型多聪明生产系统的可靠性仍然建立在“能够观察到正在发生什么”的基础上。AI 让系统从确定性走向概率性但可观测性可以帮你重新获得掌控不是让模型每一次输出都一样而是让每一次输出都有据可查、可比较、可归因。本文的核心判断是Determinism 不应该是 AI 应用工程化的前提目标Instrumentation 才是那个真正可控的支点。先把模型调用、链路层、业务层的数据采集打通再谈 prompt 优化、Agent 框架和评测体系你会发现很多之前“玄学”的问题其实只是缺少一张完整的数据全景图。下一步建议你先做三件事阅读 OpenTelemetry 官方文档理解 Trace、Span、Attribute、Resource 的基本概念。把文中的最小示例改成你真实使用的模型 SDK跑通一次全链路。为你的核心业务场景定义 10 个左右的观测字段并让团队讨论哪些字段必须脱敏。做这些事不会立刻提升模型能力但会让你的 AI 应用在进入生产环境后不再是一个无法被调试的黑盒。基础工程永远不性感但它决定了你能走多远。