十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI应用可观测性实战:用OpenTelemetry实现确定性插桩

AI应用可观测性实战:用OpenTelemetry实现确定性插桩 1. 背景AI 时代最容易被低估的工程问题过去一年AI 应用从“演示级”快速走向“生产级”。不少团队把大模型接入客服、知识库、数据分析等业务场景之后很快发现一个尴尬的事实模型本身的能力已经不是瓶颈真正让人头疼的是这个系统到底在做什么、为什么这么做、做错了怎么定位。如果你也是刚把 LLM 接到业务里的开发者大概率会遇到下面这些场景同一个 prompt今天调用和昨天调用返回的结果不一样用户开始质疑功能有 Bug。线上出现一条异常回答但日志里只有“请求成功、返回 200”完全不知道模型看到了什么上下文。一次请求平均耗时 800ms但偶尔会到 15 秒无法判断是网络抖动、模型排队还是上下文过长导致的计算变慢。模型返回了一段不准确甚至有害的内容却无法复现因为输入里的某些字段已经找不到了。这些问题的共同根源恰恰是本文标题里提到的三个词AI、Determinism确定性、Instrumentation插桩。这个话题最有代表性的讨论来自 Charity Majors。作为可观测性领域的长期实践者她反复在技术分享中强调AI 系统并不会因为“看起来智能”就自动可维护它仍然需要工程师像对待传统分布式系统一样扎扎实实地做插桩、做观测、做验证。她说的“Eating Your Broccoli吃你的蔬菜”本质上是在提醒工程师越是在新技术面前越不能跳过高投入、低刺激但决定长期质量的基础工作。所以这篇文章不打算只讲大模型 API 的调用方式而是想和你一起梳理 AI 应用在生产环境里的可观测性问题并给出一套可以落地的插桩思路和代码示例。文章会覆盖以下内容AI、确定性、插桩这三个概念到底是什么关系为什么 LLM 应用比传统服务更难观测如何用 OpenTelemetry 给一个真实的 AI 问答服务做插桩如何在不确定性的前提下做回归测试和问题排查生产环境里关于成本、日志、数据安全的最佳实践。无论你是刚接触 LLM 应用开发还是已经在做 AI 平台工程都可以在本文里找到可复用的思路。2. 核心概念拆解AI、确定性与插桩要把一个技术问题讲清楚最好先回到概念的起点。很多团队讨论 AI 可观测性时经常各说各话原因就是对基础术语的定义不一致。2.1 确定性传统软件的默认前提在传统软件开发里“确定性”通常是一个默认假设。同一个函数、同样的输入参数在相同环境下执行应该得到相同的结果。单元测试、回归测试、CI/CD 流水线全都建立在这个假设之上。你写一个add(a, b)函数不管调用一万次还是一百万次结果都是a b。正因为有确定性你才能放心地做自动化测试、预发布验证、灰度发布。但 LLM 打破了这个假设。大模型生成文本本身是概率性的同样的 prompt、同样的参数多次调用的结果可能不同。即便把temperature设为 0也只是降低随机性并不能完全消除变化。再加上模型版本更新、服务端滚动发布、上下文窗口变化输出结果更难保持严格一致。这就是为什么“AI 应用回归测试难”会成为普遍问题。你的测试框架不能继续使用assert response 预期结果这种精确断言而必须引入相似度、关键词分布、结构校验等新的验证手段。2.2 插桩让系统“可见”的手段插桩Instrumentation是指在应用程序内部埋入观测代码把关键路径上的状态、时间、错误、上下文记录下来供外部系统采集和分析。传统后端开发中的日志、指标、链路追踪都属于插桩的范畴。一个良好的插桩体系能回答三个问题发生了什么——日志记录具体事件和异常系统状态如何——指标反映延迟、错误率、饱和度一次请求完整经过了哪些环节——链路追踪串联起调用链。到了 AI 应用里插桩的重要性进一步放大。因为模型推理是一个黑盒你无法直接从模型内部拿到“为什么生成这句话”的解释只能通过观察输入、输出、上下文和相关指标来推断系统行为。如果插桩做得到位你就能看到每次请求的完整轨迹用户原始输入提示词组装后的最终形态拼接了哪些外部上下文请求了哪个模型、什么参数返回内容是什么耗时多少、Token 消耗多少是否有内容截断、超时、重试。缺少这些信息AI 应用出问题之后基本只能“靠猜”。2.3 AI 应用可观测性的三大指标维度把传统可观测性的“三支柱”日志、指标、链路映射到 AI 场景每一项都有新的含义。日志层面除了记录业务请求还要记录 prompt 模板、注入的上下文片段、模型返回的原始内容。日志要有结构化字段不能只写一行字符串。指标层面需要关注模型调用延迟、Token 消耗、错误率、重试次数、上下文长度、缓存命中率等。这些指标既能反映用户体验也直接关联成本。链路层面一次 AI 请求往往不是简单的“用户-模型”而是“用户-应用-检索/工具调用-多轮上下文组装-模型调用-后处理-返回”。链路追踪能把每个环节的耗时和状态串起来帮助判断瓶颈出在检索、构造 prompt 还是模型生成。这三个维度分开看都很简单但组合到 AI 场景后团队往往不知道从哪里开始埋点。接下来我们用一个实战案例来演示完整的插桩思路。3. 环境准备与工具选型在动手写代码之前先明确本文使用的技术栈。我会尽量选择通用、开源、社区认可度高的方案。3.1 技术栈说明语言环境Python 3.9Web 框架Flask简单演示用生产环境可换成 FastAPI、Spring Boot 等LLM SDK以 OpenAI Python SDK 为例代码思路可扩展到任意模型供应商可观测性客户端OpenTelemetry PythonExporter默认使用控制台导出方便本地观察也可以替换为 Jaeger、Zipkin、Prometheus 等。3.2 版本注意事项OpenTelemetry 的相关库更新频率较快具体版本号不做固定限制建议在写本文的当前时间点使用各主线库的最新稳定版本。安装命令如下pip install flask pip install openai pip install opentelemetry-api pip install opentelemetry-sdk pip install opentelemetry-exporter-otlp-proto-grpc pip install opentelemetry-instrumentation-flask pip install opentelemetry-instrumentation-requests如果你所在网络环境访问外网不便也可以先用ConsoleSpanExporter在本地查看输出不依赖任何远端采集系统。3.3 示例项目结构为了便于理解我们用一个最小但完整的项目作为演示ai-observability-demo/ ├── app.py # Flask 应用入口 ├── llm_service.py # 模拟/真实模型调用封装 ├── prompt_builder.py # 提示词组装逻辑 ├── instrumentation.py # OTel 初始化与自定义埋点 └── requirements.txt # 依赖清单这个项目会实现一个简单的“AI 知识问答接口”用户传入问题服务先拼接预设的知识库上下文再调用 LLM 生成回答。整个过程会埋入链路追踪、结构化日志和指标记录。4. 实战给 AI 问答服务做插桩接下来我们进入本文的核心环节。先写一个没有插桩的“原始版本”再逐步加上观测能力。这样你能更清楚地看到插桩发生在哪些位置。4.1 原始版本的 AI 问答服务先看一个最简化实现# llm_service.py import openai import os openai.api_key os.environ.get(OPENAI_API_KEY) def generate_answer(question: str, context: str, model: str gpt-3.5-turbo) - str: system_prompt 你是一个严谨的客服助手请结合给定资料回答问题。 user_content f资料{context}\n\n问题{question} response openai.chat.completions.create( modelmodel, messages[ {role: system, content: system_prompt}, {role: user, content: user_content} ], temperature0, ) return response.choices[0].message.content单看这段代码功能上完全能跑。但从可观测性角度看它几乎是一个“黑盒”调用模型花了多长时间给模型传了什么上下文上下文有多大消耗了多少 Token返回结果是什么如果调用失败异常发生在哪一步这些信息全部丢失。一旦线上出现问题你无法通过日志定位原因。4.2 初始化 OpenTelemetry接下来我们创建插桩模块。先初始化 TracerProvider并导出到控制台# instrumentation.py from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter resource Resource.create(attributes{service.name: ai-knowledge-service}) provider TracerProvider(resourceresource) console_exporter ConsoleSpanExporter() provider.add_span_processor(BatchSpanProcessor(console_exporter)) trace.set_tracer_provider(provider) def get_tracer(name: str): return trace.get_tracer(name)这里做了两件事给服务定义了一个全局资源属性service.name后续所有 trace 都会带上这个标签方便在链路系统里区分服务把 Span 导出方式设为控制台本地运行时可以直接看到链路信息。在生产环境里你可以把ConsoleSpanExporter换成 OTLP exporter把数据发送到 Jaeger、Tempo、Honeycomb 或自建 Collector。4.3 对模型调用进行埋点接下来改造llm_service.py在模型调用前后记录关键信息# llm_service.py import time import openai import os from opentelemetry import trace from instrumentation import get_tracer openai.api_key os.environ.get(OPENAI_API_KEY) tracer get_tracer(llm-service) def generate_answer(question: str, context: str, model: str gpt-3.5-turbo) - str: start_time time.monotonic() system_prompt 你是一个严谨的客服助手请结合给定资料回答问题。 user_content f资料{context}\n\n问题{question} messages [ {role: system, content: system_prompt}, {role: user, content: user_content} ] with tracer.start_as_current_span(llm.call) as span: span.set_attribute(llm.model, model) span.set_attribute(llm.request.messages_count, len(messages)) span.set_attribute(llm.prompt.system, system_prompt) span.set_attribute(llm.prompt.user, user_content) span.set_attribute(llm.request.temperature, 0) try: response openai.chat.completions.create( modelmodel, messagesmessages, temperature0, ) result response.choices[0].message.content usage getattr(response, usage, None) if usage: span.set_attribute(llm.token.prompt, usage.prompt_tokens) span.set_attribute(llm.token.completion, usage.completion_tokens) span.set_attribute(llm.token.total, usage.total_tokens) span.set_attribute(llm.response, result) latency_ms (time.monotonic() - start_time) * 1000 span.set_attribute(llm.latency_ms, latency_ms) span.set_attribute(llm.success, True) return result except Exception as e: latency_ms (time.monotonic() - start_time) * 1000 span.set_attribute(llm.latency_ms, latency_ms) span.set_attribute(llm.success, False) span.set_attribute(llm.error, str(e)) span.record_exception(e) raise这个版本的改进点非常明确我们把模型调用的关键数据全部记录到 Span 属性里。以后追踪一条链路时可以直观看到使用了哪个模型送给模型的 system prompt 和 user prompt 分别是什么提交了多少条消息消耗了多少 Token耗时多少是否成功如果失败异常信息是什么。4.4 对 Web 请求层进行埋点模型层只是链路的一部分。用户请求进入 Flask、组装提示词、调用模型、返回结果整个过程都应该被追踪。# app.py import uuid from flask import Flask, request, jsonify from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry import trace from instrumentation import get_tracer from llm_service import generate_answer from prompt_builder import build_context app Flask(__name__) FlaskInstrumentor().instrument_app(app) tracer get_tracer(web-service) app.route(/ask, methods[POST]) def ask(): data request.get_json() question data.get(question, ) user_id data.get(user_id, unknown) # 生成本次请求的 trace_id方便日志关联 request_id str(uuid.uuid4()) with tracer.start_as_current_span(ask.endpoint) as span: span.set_attribute(app.request_id, request_id) span.set_attribute(app.question, question) span.set_attribute(app.user_id, user_id) # 组装上下文 context build_context(question) span.set_attribute(app.context_length, len(context)) answer generate_answer(question, context) span.set_attribute(app.answer, answer) return jsonify({ request_id: request_id, answer: answer }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这里的链路由两层 Span 组成外层ask.endpoint代表一次完整的 HTTP 请求处理内层llm.call代表模型调用这一段子过程。两层 Span 之间通过 OpenTelemetry 的上下文传播机制自动关联。你可以在链路系统里看到“哪个请求 - 调用了多少次模型 - 每次耗时如何”的完整关系。4.5 运行与验证本地运行时先设置环境变量export OPENAI_API_KEYyour_api_key_here python app.py然后另开终端发送测试请求curl -X POST http://localhost:5000/ask \ -H Content-Type: application/json \ -d {question: 什么是可观测性, user_id: test_user}服务端控制台会输出类似下面的 Span 信息简化展示Span #0 Name: llm.call Attributes: llm.model - gpt-3.5-turbo llm.request.messages_count - 2 llm.prompt.system - ... llm.prompt.user - ... llm.token.total - 425 llm.latency_ms - 1234.56 llm.success - True同时还会有一条外层 HTTP 请求的 Span包含app.question、app.context_length、app.answer等业务字段。到这里一个最基础的 AI 应用插桩已经完成。5. 确定性问题与回归测试实践插桩解决了“看见系统”的问题但还有一个更深的坑需要处理AI 输出的不确定性导致我们没法用常规方式做质量验证。5.1 为什么传统断言不适用假设你为问答接口写的测试是def test_ask(): response client.post(/ask, json{question: 什么是可观测性}) assert response.json()[answer] 可观测性是用来观察系统内部状态的能力。这个测试大概率会失败。因为即使temperature0模型也可能换一种表达方式比如“可观测性是指通过外部输出来了解系统内部运行状态的手段”。语义一致但字符串不相等。更麻烦的是模型还可能因为上下文拼接顺序变化、远端模型版本更新而产生表达风格差异。所以面向 AI 应用的测试必须先承认“结果有随机性”再想办法设计更稳健的断言。5.2 面向 AI 的回归测试策略常见的做法有以下几种。第一语义相似度断言。使用文本向量模型把模型回答和期望答案嵌入到向量空间计算余弦相似度设定一个合理阈值。相似度大于 0.85 就认为通过。from sentence_transformers import SentenceTransformer from sklearn.metrics.pairwise import cosine_similarity model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) def similarity(text1: str, text2: str) - float: vec1 model.encode([text1]) vec2 model.encode([text2]) return cosine_similarity(vec1, vec2)[0][0] def test_answer_semantic(): actual 可观测性是指通过系统输出来了解内部状态的能力 expected 可观测性帮助我们持续理解系统在做什么 assert similarity(actual, expected) 0.85第二关键词与结构断言。如果回答应该包含某些关键实体或遵循特定格式可以直接校验结果中是否出现这些关键词。def test_answer_contains_key_info(): answer client.post(/ask, json{question: 解释一下 Kubernetes}) assert 容器 in answer assert any(word in answer for word in [编排, 调度, 集群])第三黄金样例回归集。挑选几十条覆盖核心场景的测试问题固定一份期望输出或期望摘要。每次模型或提示词变更后运行整套样例人工检查 diff确保没有明显劣化。5.3 利用插桩数据回放线上问题插桩不仅能服务实时观测还有一个经常被忽略的价值回放调试。当线上出现一次错误回答时你从链路系统里可以找到这次请求的完整字段包括 prompt、上下文、模型参数、返回结果。你可以把这些数据保存下来作为回归测试的输入。实践中建议在链路数据里增加一个request_id并在业务日志里同样打印这个 ID。这样出现问题时可以快速在日志系统和链路系统之间切换定位环节再决定是修代码、改 prompt 还是换模型。6. 常见问题与排查思路在做 AI 应用插桩和可观测性改造时团队经常会遇到下面这些高频问题。问题现象常见原因解决思路日志里没有 trace 信息TracerProvider 没有正确初始化或 Span 上下文没有传入异步任务确认在应用启动时初始化 OTel并检查异步线程是否手动传递了 Context模型调用耗时很高但无法定位是网络还是生成没有在 SDK 层面记录网络连接时间和首 Token 时间使用支持细粒度指标的 SDK或在封装层记录start_time到first_token_time的分段耗时Token 消耗突然上涨上下文拼接逻辑变更或引入了无上限的对话历史检查 prompt 组装逻辑给上下文增加长度截断与摘要策略测试用例频繁失败直接使用字符串相等断言改用语义相似度或结构校验断言并建立人工抽查流程线上出现敏感数据泄漏风险prompt 或日志中记录了大量用户原始数据在插桩时做字段脱敏只保留必要的调试字段多次调用结果差异巨大temperature设置过高或模型版本被远端更新确认线上参数配置并对模型版本做固定快照策略排查 AI 应用问题建议按“链路 - 日志 - 指标 - 复现”的顺序推进通过request_id找到完整链路看链路里每个环节的耗时和状态在日志里查看 prompt 和上下文从指标面板看整体错误率和 Token 消耗趋势用链路里存下的原始输入去复现问题必要时再测一次模型行为是否稳定。7. 最佳实践与工程建议插桩和可观测性不是一次性工作而是一套需要持续维护的工程体系。下面这些建议来自我处理线上 AI 服务的实际经验供你参考。7.1 尽快给 prompt 和上下文建立版本管理prompt 不是普通的字符串配置它直接影响输出质量。建议把 prompt 模板纳入版本管理像管理代码一样管理 prompt 的变更记录。每次修改 prompt都应该配套一组回归测试样例。7.2 插桩字段要遵守最小权限原则记录 prompt 和上下文时要注意防止隐私数据泄漏。不要盲目把整个用户输入都塞进日志。建议做一个字段级脱敏工具比如手机号、身份证号、邮箱地址等自动替换为掩码。如果业务确实需要完整记录至少要设置访问权限和审计日志。7.3 成本指标和性能指标同等重要LLM 应用的成本不像传统基础设施那么直观。一次复杂调用可能消耗几万 Token累积起来成本惊人。建议把 Token 消耗按用户、按接口、按模型维度聚合生成日报或周报。一旦发现某个接口的 Token 消耗异常增长及时排查上下文是否失控。7.4 不要忘记采样策略高并发 AI 服务的日志量会非常大把所有请求全量落盘既不经济也不利于快速定位问题。建议对核心接口做全量采样对一般接口按 10% 或更低的比率采样。出错请求可以设置条件采样保证异常一定有记录。7.5 把人工审查纳入发布流程模型应用和传统发布不同自动化测试无法覆盖所有语义边界。建议每次上线模型版本或 prompt 变更时至少安排一次人工抽查。把线上实际问题的失败案例沉淀为新的回归样例持续扩充测试集。7.6 建立问题复盘机制AI 应用的问题通常不是单一代码 Bug而是多个因素叠加比如长尾输入 上下文遗漏 模型输出偏差。建议定期做一次线上问题复盘把链路数据、日志、修改记录放到一起复盘找到系统性改进点。8. 结语回到 Charity Majors 的标题AI、Determinism、Instrumentation、Eating Your Broccoli。这四个词放在一起其实是在说一件很朴素的事AI 再神奇也改变不了工程的基本规律——不可见的系统必然无法维护不验证的改动必然带来风险。额外补充一个实际经验跳过了插桩和回归测试的低级修改短期看起来“快”但后期会持续补偿成本——这就像不吃蔬菜省下的时间最后都要以更贵的药费还回去。希望这篇文章能帮你建立一套可落地的 AI 应用可观测性方案。如果你想系统掌握这部分能力建议从下面几条路径继续深挖。熟悉 OpenTelemetry 的全链路追踪模型包括 Span、Trace、Context Propagation实践 LLM 回归测试框架比如语义相似度断言和黄金样例集学习成本分析和 Token 优化策略这是 AI 应用规模化必经的一步如果有条件把链路数据接入可视化平台训练团队所有人“先看链路再猜原因”的排障习惯。AI 应用的可观测性才刚刚开始被重视。越早把基础工作做扎实你越能在模型快速迭代的浪潮里稳住生产环境。如果在实践过程中遇到问题欢迎留言交流。
返回列表