news 2026/8/30 12:59:47

AI应用可观测性实战:从Determinism到Instrumentation的工程化之路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI应用可观测性实战:从Determinism到Instrumentation的工程化之路

当一个客服机器人刚上线时,你收到用户投诉:“它明明两天前回答得很好,今天同样的问法,结果完全跑偏了。”于是你尝试复现,但用同样的提示词试了三次,三次答案都不一样。你开始怀疑是模型升级、上下文被污染、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 与 Instrumentation

2.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(resource=resource) # 为了方便本地验证,使用 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}")

这段代码的核心逻辑:

  1. 初始化TracerProvider,设置服务名为ai-demo
  2. 注册ConsoleSpanExporter,所有 span 会打印到终端。
  3. tracer.start_as_current_span("llm.call")创建一个 span。
  4. 在 span 上写入模型名称、temperature、prompt、response、token 用量等属性。
  5. 模拟调用模型,并返回结果。

运行命令:

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(resource=resource) otlp_exporter = OTLPSpanExporter( endpoint="http://localhost:4317", insecure=True, ) provider.add_span_processor(BatchSpanProcessor(otlp_exporter)) trace.set_tracer_provider(provider)

然后准备一个最简 Collector 配置otel-collector-config.yaml

receivers: otlp: protocols: grpc: http: processors: batch: exporters: logging: verbosity: detailed service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [logging]

启动 Collector:

otelcol --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后,按下面顺序判断是否成功:

  1. 程序没有报错,且打印出“模型输出:...”。
  2. 控制台打印出一个包含llm.call的 span 对象。
  3. span 中包含trace_idspan_idllm.request.promptllm.response.textllm.usage.total_tokens等属性。

如果使用 OTLP 导出,还应确认:

  • Collector 启动时没有端口冲突。
  • 脚本执行后,Collector 日志出现对应 span 输出。
  • 如果数据没有到达,先用grpc_health_probecurl检查端口连通性。

最容易出问题的环节通常是:

  • OTel SDK 版本变化导致部分配置项名称不一致。
  • 安装依赖时漏装了opentelemetry-exporter-otlp
  • Collector 配置格式不合法,启动时报错。
  • 防火墙或容器网络阻止了 4317 端口访问。

7. 常见问题与排查方法

问题现象可能原因排查方式解决方案
控制台没有输出 span没有调用with tracer.start_as_current_spanrun_chat内打印日志确认函数是否执行确保进入了 span 的上下文管理块
span 有输出但属性缺失属性名写错或属性值类型不是 string/int查看代码中set_attribute调用统一字段命名,参考官方语义约定
OTLP 数据没有到达 Collectorendpoint 配置错误或网络不通检查 Collector 日志和端口监听改为insecure=True,确认 endpoint 地址
trace_id 全是 0当前没有活跃 span 上下文start_as_current_span内部获取 trace_id确认调用get_current_span的时机
成本数据统计不准只记录了 model 层,没有关联业务层检查 trace 关联是否打通从主入口生成 trace_id,贯穿整个请求
prompt/response 泄露敏感信息埋点采集了原始对话内容检查埋点字段是否包含 PII在采集端做脱敏,只保留必要字段
模型调用超时但无 trace客户端超时时间太短,异常被吞掉在异常捕获处手动创建 spanrecord_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 应用在进入生产环境后,不再是一个无法被调试的黑盒。基础工程永远不性感,但它决定了你能走多远。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/30 12:58:10

集成卷积神经网络提升中风预测准确率的完整实战解析

为什么单个 CNN 模型预测中风总差一点?集成卷积神经网络(Ensemble CNN)完整实战最近不少做医疗 AI 的同学都在讨论一个现象:用卷积神经网络(CNN)做中风预测时,单模型的准确率往往提升到某个阶段…

作者头像 李华
网站建设 2026/8/30 12:57:45

光纤的一些知识总结

关于单模光纤和多模光纤 ‌单模光纤和双模光纤的主要区别在于它们能够传输的光模式数量、传输性能和应用场景。‌ 定义与原理 单模光纤(Single-Mode Fiber, SMF)‌:只允许一种光模式(基模,即LP01模)在其中传…

作者头像 李华
网站建设 2026/8/30 12:54:07

知识图谱+GraphRAG+多智能体:研0研一从入门到实战的完整路线

研0研一最头疼的一件事,就是“方向怎么选”。打开论文库,满屏都是大模型、Agent、知识图谱、RAG,每个词都认识,串在一起却不知道从哪下手。这篇文直接给你一条相对清晰的路线:从知识图谱构建,到 GraphRAG 问…

作者头像 李华
网站建设 2026/8/30 12:51:42

AI QA Agent 实战:用大模型自动测试 Vercel Preview 并反馈到 PR

过去半年,如果你的团队在用前端部署平台,一定见过这个场景:PR 一开,机器人自动评论里附上一个 Preview URL,然后没人点。直到合并上线后,运营拿着截图找过来,才发现移动端按钮被侧边栏挡住了。流…

作者头像 李华
网站建设 2026/8/30 12:51:33

Vale:用自然语言Linter让文档检查像代码审查一样自动化

代码写完后过一遍 lint,这是工程团队的肌肉记忆。可轮到文档、README、API 说明,很多团队又退回人肉检查。Vale 是一个开源的自然语言 Linter,它把写作规范变成可执行的检查流程。我第一次用 Vale 跑自己的博客文章,看到一排告警后…

作者头像 李华
网站建设 2026/8/30 12:50:29

STM32CubeIDE导入项目参数丢失排查与修复指南

项目从同事电脑拷贝过来,在 STM32CubeIDE 里用 Import 导进自己的工作区,编译、下载一切正常,可烧到板子上跑起来之后,行为却和原项目完全不一样——优化生效的代码段突然变慢,某个功能宏控制的分支像消失了一样&#…

作者头像 李华