AI Agent监控:两个环境变量打通可观测性链路
先说我遇到的实际场景吧。一个基于 Microsoft Foundry 搭建的 AI agent 项目本地怎么调都正常一旦放到平台环境里跑用户报障就只能翻 stdout。可 agent 这东西和普通 Web 服务最大的不同是它的每一步都可能动态变化今天先调用搜索工具再查数据库明天可能先调代码解释器再决定要不要继续。没有链路级的 trace你看到的日志就是一堆没有 operation ID 关联的碎片压根拼不出“这次回答为什么失败”。后来我仔细梳理了 Foundry 上 AI agent 的可观测性接入路径发现一个很省事的方案——两个环境变量不依赖额外采集器就能让 trace、metric、log 顺着同一条链路进到 Application Insights。这篇文章就把我的接入过程和踩坑记录完整写出来。这篇文章适合谁看你正在做 agent 开发或者你负责 agent 的测试、交付、线上排障再或者你只是被“agent 到底怎么监控”这件事折磨过那这篇应该能帮你省不少时间。我不打算只贴结论而是把“那两个环境变量为什么能顶掉采集器”的底层逻辑一并讲清楚方便你以后换平台、换部署方式时也能自己判断该怎么做。1. Agent 排障难在哪动态链路让“日志对齐”彻底失效1.1 一个典型的 agent 故障现场假设你有一个客服 agent用户问“帮我查一下上个月订单里金额最高的三笔分别是什么时候发货的”。Agent 先规划出步骤然后调用订单查询工具拿到结果后发现还需要调用物流查询工具中间可能要读一段说明文档来判断字段映射。整个过程在 Application Insights 里应该是一整条 trace串起多个 LLM 调用和工具调用。故障往往发生在两个工具调用的边界里。比如第二个工具返回了一个空数组agent 没有立刻报错而是选择换一种说法再调一次甚至直接按“查无数据”回答用户。传统日志里你只能看到这句“查无数据”但看不到它是在哪个分支下做出的决定、之前那步拿到了什么上下文、token 消耗了多少。对普通服务来说错误通常伴随异常栈对 agent 来说大量失败是“没有抛异常但结果不对”的逻辑失败这是两类完全不同的排查问题。这种场景下我们能依赖的不是“多打几行日志”而是要能重放这个 agent 的一次完整决策轨迹从用户输入开始经过 planning、每次 LLM generation、每次工具调用及返回值到最终输出。每一个环节的耗时、token 成本、错误码和关键属性都要能串到一个 trace ID 下。1.2 Agent 链路与传统服务链路的差异我以前排障普通 Web 服务时拿到一个 request ID 基本就能定位问题请求进了网关经过鉴权中间件调了业务服务 A 和 BB 查了数据库然后返回。链路的拓扑基本是固定的父子 span 关系一眼就能看清。AI agent 完全不同。它的调用拓扑是动态生成出来的同样的用户问题上一轮走三步这一轮可能走七步还可能并行触发多个工具。还有个麻烦的地方模型调工具时如果没有严格按结构输出框架可能会自动做一次修复重试日志里看不到任何 error level 的痕迹只是多了一次调用。你在日志里找半天都找不到根因是因为它根本没有以“异常”的形式存在。我用一张对比表来说明差异方便后面理解为什么 agent 的 trace 格外重要。对比维度普通 Web 服务AI Agent 应用调用拓扑相对固定动态规划每次都可能不同错误表现异常栈、错误码常表现为“低质量结果”或策略变化成本指标请求数、响应时间token 消耗、工具次数、模型调用链日志关联request ID 基本够用需要一个完整、有父子关系的 trace依赖关系服务间调用相对清晰涉及 LLM 外部 API、内部工具、RAG 检索、数据库等多类依赖也就是说对 agent 来说trace 不只是辅助工具它几乎是唯一能把一次决策过程完整还原出来的手段。如果只依赖日志你会发现每个 microservice 都打了日志但缺了“上下文”这个维度信息根本连不起来。1.3 可观测性真正要解决的问题跟一次请求、查一个指标、看一段日志做 agent 可观测性业界常讲 telemetry 三支柱trace、metric、log。但到了 agent 场景有一点需要重新强调trace 应该成为中心log 和 metric 都要围绕 trace 去组织。trace 回答“这次调用经历了什么”哪个 agent、哪个 thread、哪个 run。metric 回答“系统的健康度和成本如何”LLM 调用延迟、成功/失败率、token 消耗、工具调用次数。log 回答“每一个节点的具体细节是什么”比如某个工具返回的原始 payload 片段、异常详情。如果没有 trace IDmetric 就只能展示全局平均值log 之间也没有关联。传统做法里要在应用侧集成一套比较重的 agent/collector 采集器。现在 Foundry 给出的方案干脆得多应用内部直接通过两个环境变量完成连接少了中间那一层采集进程链路数据就能直接到监控后端。这听起来好像少了东西实际是把自己的 SDK 层做厚了。2. Foundry 上“两个环境变量”背后的链路设计2.1 为什么很多团队会先想到装采集器在聊“不需要采集器”之前先得说清楚传统方案里采集器到底负责什么。OpenTelemetry 生态里应用内的 SDK 负责产生 span、metric 和 log但默认并不会直连后端。采集器collector作为一个独立进程接收应用通过 OTLP 协议上报的遥测数据然后做加工、过滤、脱敏再转发给后端的监控系统。它的好处是灵活比如同时把数据发到多个后端或者统一做采样。代价也很明显你要多维护一个进程多写一份配置多处理一个网络依赖点。如果你的应用本来就跑在 Kubernetes 里可能还要升级 agent 和 collector 的版本监控链路上任何一环出问题业务侧反倒先报警。对一个用 Foundry 托管 agent 的团队来说这种额外的运维成本就很没必要。尤其当数据唯一去向就是 Application Insights 时中间多一层 collector 纯属增加故障面。2.2 第一个环境变量数据该往哪里送先说直连方案里最核心的环境变量连接字符串。在 Azure 生态里它通常长得像这样APPLICATIONINSIGHTS_CONNECTION_STRINGInstrumentationKeyxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx;IngestionEndpointhttps://xxx.applicationinsights.azure.com/;LiveEndpointhttps://xxx.livediagnostics.monitor.azure.com/它跟早期的 instrumentation key 不一样不是只告诉后端“我是哪个资源”而是把数据要发往的区域 endpoint 也一并带上了。这样 SDK 启动后不需要额外配置区域地址直接就能确定 ingestion endpoint 和数据来源标识。为什么要设计成连接字符串而不是单独一个 key因为 agent 应用可能部署在全球不同区域。如果只传一个 keySDK 还得自己猜该连哪个 endpoint猜错就会导致数据跨区域绕路延迟升高不说还有可能被网络策略拦掉。连接字符串把这些信息全都编码进去路由就明确多了。我实际验证过使用包含正确 IngestionEndpoint 的连接字符串数据上报的延迟明显更低而且基本不会出现“配置了但后端收不到”的问题。2.3 第二个环境变量让 SDK 自动初始化到位接下来是第二个环境变量。很多初次接触的人会被它的名字误导以为又要装什么 agent 组件了其实它的作用是让应用内的 SDK 以“自动埋点”或“免代码接入”的模式完成初始化而不是在宿主机上再拉一个进程。以常见配置为例它的启用开关信息会通过类似下面这样的环境变量传递给 SDKAPPLICATIONINSIGHTS_ENABLE_AGENTtrue不要被名字里的 Agent 吓到。这里的 Agent 指的是 Azure 监控体系里的自动插桩能力不是独立运行的 collector。设成 true 之后SDK 启动时会自动加载一批 instrumentation 库把 HTTP 调用、数据库访问、消息队列等依赖调用都接进来同时也为 AI 相关的 SDK 预留了 span 生成的钩子。当然不同语言发行版的变量命名会有一些差异。比如 .NET 里有些配置可能通过 appsettings 配置节做Python 里则读取环境变量。我这里以环境变量为例是想说明一种通用机制第一个变量解决“数据去哪”第二个变量解决“怎么免配置启动”。你可以理解为第一个是地址第二个是总开关。真正干活时应用代码里通常还需要一行调用例如 Python 下使用 Azure Monitor OpenTelemetry Distro 时的样子from azure.monitor.opentelemetry import configure_azure_monitor configure_azure_monitor()这行代码执行时SDK 会自动去读刚刚提到的连接字符串和启用开关然后完成 TracerProvider、MeterProvider 和 LoggerProvider 的初始化。后面你在业务代码里用 OpenTelemetry API 创建 span 时数据就会自动走到 Application Insights不需要在每个模块里手动配置 exporter。2.4 关键认知不是“不需要可观测链路”而是“exporter 内嵌了”有人会问那少了 collector数据怎么到后端答案是 exporter 直接内嵌到了 SDK 里由 Azure Monitor OpenTelemetry Distro 在启动时帮应用创建好了 OTLP exporter。这样链路变成应用进程内的 SDK / instrumentation ↓ 直接通过 OTLP/HTTPS 上报 Application Insights ingestion endpoint从表面看这确实省掉了一个采集器进程但并不是说不需要可观测链路了。你还是要有 trace、metric、log还是要有后端存储和查询只是原来 collector 承担的那一部分职责被移到了官方 SDK 里。因为 Foundry 和 Application Insights 同属一套云生态网络、身份、数据面天然打通不再需要 collector 做中转。对部署在 Foundry 里的 agent 来说还有一个很实际的收益少一个附属进程就少一个排查点。Kubernetes 里每加一个 sidecar 容器都要考虑内存限制、启动依赖、升级节奏。采用直连方案后你可以部署得更轻。不过要不要完全去掉 collector 并不是绝对的我后面专门有一节讲什么情况下还是得加回来。3. 上手步骤我建议按“四步走”接入3.1 前置准备三个资源接入之前先确认环境里是否有这几样东西一个 Microsoft Foundry 项目里面已经创建了要观测的 agent 应用。一个 Application Insights 资源通常会和 Log Analytics Workspace 绑定。一个可以给运行环境注入环境变量的位置例如容器的环境变量配置、平台应用设置或者部署脚本。我遇到过不少同事把顺序搞反代码都改完了才发现 Application Insights 还没建。其实这个一分钟就能搞定直接在 Azure Portal 里创建 Application Insights 资源创建完复制它的连接字符串备用。同时在 Azure AI Foundry 的项目里找到目标 agent 的部署配置确认它的运行环境支持读取环境变量。这一点很重要如果你的 agent 跑在某个托管 endpoint 后面你得搞清楚平台控制台里哪个入口是“部署时环境变量”而不是“构建时环境变量”。构建时注入的变量在运行进程里不一定读得到。3.2 代码侧最小改造我的建议是不要写一堆和框架强绑定的埋点代码先把最小链路跑通。核心逻辑就是这样首先安装依赖。以 Python 为例通常需要pip install azure-monitor-opentelemetry然后在程序入口执行一次配置# main.py 或 app.py 最早的启动位置 from azure.monitor.opentelemetry import configure_azure_monitor configure_azure_monitor()如果环境变量已配置这行代码就能让 SDK 读取并建立上报通道。如果你的 agent 主循环是自研的可以在关键步骤手动加上 spanfrom opentelemetry import trace tracer trace.get_tracer(foundry.agent) def run_agent(user_input): with tracer.start_as_current_span(agent.run) as span: span.set_attribute(agent.name, customer-support) span.set_attribute(thread.id, get_current_thread_id()) messages build_initial_messages(user_input) response call_llm(messages) for tool_call in get_tool_calls(response): with tracer.start_as_current_span(ftool.{tool_call.name}) as tool_span: tool_span.set_attribute(tool.input, trim(tool_call.input)) result execute_tool(tool_call) tool_span.set_attribute(tool.success, result.success) span.set_attribute(response.id, get_response_id(response)) return response这段代码不是让你原样照抄而是展示一个思路可以用 OTel API 自由地给 Agent 的编排层加上 span不用等框架自动埋点补全。官方 instrumentation 能覆盖最常见的 HTTP 客户端和部分 AI SDK但 agent 编排逻辑里那些“决定调用哪个工具”“工具返回后如何处理”的步骤往往是你自己埋点才能得到最准确的信息。需要注意的是configure_azure_monitor()必须在创建任何 span 之前调用一次并且只需要调用一次。如果多次调用可能产生重复的 exporter 初始化甚至导致数据重复上报。3.3 运行环境与部署配置代码改完后把两个环境变量放进运行环境。我通常会先在本地用 .env 或 export 验证一遍export APPLICATIONINSIGHTS_CONNECTION_STRINGInstrumentationKey...;IngestionEndpoint...;LiveEndpoint... export APPLICATIONINSIGHTS_ENABLE_AGENTtrue python main.py确认本地能看到数据之后再上 Foundry 平台配置。这时需要注意一个细节不同平台对“环境变量”的配置入口叫法不同但一定选 runtime 或 deployment 级别的设置保证启动 agent 进程的用户能读到。如果你是用容器部署不要把连接字符串写死在镜像里否则换环境就要重新构建镜像。正确做法是做成运行时 env 注入镜像里放一份空值占位平台部署时再填充。这样做也方便在测试、预发、生产三套环境之间切换不同 Application Insights 资源。我在代码仓库里会放一个.env.example但会把真实连接字符串放到密钥管理服务或 CI/CD 的 secret 里。连接字符串跟数据库密码是一个等级的敏感信息泄露了别人就能往你的监控资源里灌数据干扰告警严重情况下还会产生额外成本。3.4 端到端做一次验证配置完成后不要直接看仪表盘先主动制造一次可以预期结果的操作。比如向 agent 提一个必然会触发两次工具调用的问题等它执行完去 Application Insights 的 Transaction Search 里查最近一小时的记录。如果能直接按 trace ID 搜到一整条调用链就说明链路通了。如果想用 KQL 查询可以类似这样union requests, dependencies, traces | where timestamp ago(1h) | where operation_Id 你的traceId | project timestamp, itemType, operation_Id, id, name, resultCode, duration | order by timestamp asc常见的困惑是这里没有数据。先不用着急改代码大概率是环境变量没生效或时间范围不对我下一节会把排查顺序完整列出来。如果能看到 span但发现一些关键的 agent 动作没有成为独立 span那就说明对应代码片段还没有被手动埋点覆盖回到 3.2 步补上。自动 instrumentation 只是保底核心编排步骤还是建议显式加 span后面排查时你会感谢当时的自己。4. 实测之后最容易翻车的几个怪问题4.1 设了变量但一点数据都没有先按这四个方向查我见过最多的场景就是“明明配了两个环境变量Application Insights 里就是没有数据”。按照下面的顺序排查通常能很快定位。环境变量真的注入到运行进程了吗在 agent 启动脚本里加一行print(os.getenv(APPLICATIONINSIGHTS_CONNECTION_STRING))或者直接进容器执行env | grep APPLICATIONINSIGHTS。如果输出为空就不用看后面了先去平台配置里找正确入口。configure_azure_monitor()到底有没有执行成功我见过有人把调用放在了if __name__ __main__块里但 agent 逻辑被模块 import 时就会触发 span 创建结果 exporter 还没初始化早期 span 全丢了。解决方式是把配置调用放到进程最开始的逻辑里尽可能早。时间范围和 Region 对不对Application Insights 控制台默认可能显示“过去 30 分钟”而你的测试发生在 40 分钟前当然看不到。Region 更坑如果你连接字符串里的 ingestion endpoint 是 East US但你的查询页面打开的是上海区域资源那两边根本不是同一个资源。网络出口有没有被防火墙或代理挡住有些公司网络环境不允许应用直连云端的 ingestion endpoint需要在出口侧放行对应域名。查看官方文档确认 endpoint 域名后让网络管理员加白名单或者走代理。这一步在本地开发时经常遇到但在 Foundry 托管环境里一般不会有问题因为平台本身已经打通了与 Azure Monitor 的连通性。这四个方向解决了大约 80% 的“没有数据”问题。还有一种情况是数据有延迟Application Insights 的链路数据通常延迟在一两分钟内不至于完全没有。4.2 trace 有了但日志与 request 串不起来trace 通了之后下一步最常见的问题是日志系统里的记录没有 operation ID导致 trace 和 log 对不上。这个问题的本质在于OpenTelemetry 的 trace context 需要显式地注入到日志记录器中否则日志文本只是普通字符串和当前正在执行的 span 没有关联。不同语言的日志 handler 配置方式不同但思路都一样让日志格式里带上trace_id和span_id字段这样日志后端才能按 operation ID 把日志挂到对应链路上。我实际项目里比较推荐的兜底方案是不要只依赖日志框架的自动关联而是在关键步骤里直接把结论写进 span event。例如with tracer.start_as_current_span(tool.order_query) as span: result query_orders(user_id) span.add_event(order_query.result, {order_count: len(result)})这样即使日志系统没把 operation ID 关联起来你在 trace 的 span 详情里也能看到关键返回信息。在 agent 场景里工具返回值往往是排障时最重要的线索放进 span event 比放普通日志可靠得多。4.3 Prompt、Tool 内容不要盲目全量采另一个容易翻车的点是安全合规。AI agent 的 trace 天然会记录很多上下文包括用户输入、工具输入、模型输出。如果这些内容包含个人信息或企业敏感数据直接全量上报到监控后端会有很高的泄露风险。很多 instrumentation 库默认不会采集完整 prompt只采集元数据。但我看有些团队为了让排障方便把 capture content 开关打开了结果就是把用户在公司系统里输入的问题原文全部发到了日志后端。你可以认为 Application Insights 足够安全但“足够安全”不等于“应该采集”尤其是企业内部数据合规很严格时这属于给自己埋雷。建议做法默认只采集 span 名称和关键元数据不采集 prompt 原文。如果确需采集先做脱敏比如把身份证号、邮箱、手机号等字段替换成占位符。通过采样配置控制总量例如采用 parent-based sampler对成功且重复度高的请求降低采样率只保留下错误或有代表性的请求。采样器配置看起来是后话但对成本影响很大。agent 的一次完整执行可能产生几十个 spanLLM 调用多时数据量增长很快。不控制采样率月底账单会很难看。4.4 一台资源跑多个 agent把所有调用混在一起如果你在一个 Foundry 部署里同时跑了多个 agent比如一个做客服一个做数据分析结果它们共享同一个 Application Insights那你会看到所有 span 混在一起。从 trace 本身能看出链路结构但很难一眼区分到底是哪个 agent 产生的。解决办法有两个。最简单的做法是设置cloud_RoleName在 SDK 配置或环境变量里给不同 agent 起不同名字这样 Application Insights 的 Application map 和查询都能按角色区分。更细致一点的做法是在 span 里打统一的属性例如agent.name、agent.version这样查询和告警时都能按这个维度过滤。我自己的习惯是每个 agent 应用一个独立连接字符串除非量很小或者纯测试环境。虽然会增加一点资源管理成本但隔离更彻底权限控制也清晰不会出现 A agent 的排障请求把 B agent 的数据也搜出来降低误判概率。5. 什么时候我应该老老实实把 Collector 加回来5.1 并不是所有 agent 部署都适合零采集器直连讲了这么多直连方案的优点但如果你的架构里出现了下面任意一种情况直连方案就不一定是最优解了监控数据需要同时发送到两个以上后端比如既要发给 Application Insights又要发给自建的 Prometheus 或日志平台。安全策略要求所有出站流量都经过统一的代理或网关不允许应用直接访问外部 endpoint。你需要在数据进入后端之前做统一脱敏、丢弃、分流而且不想在每个应用里重复实现一套逻辑。你除了 agent 之外还有周边组件比如数据库中间件、消息队列、网关它们都无法配置 OTLP exporter只能靠 collector 统一接收和转发。如果你中了其中一条那额外部署一个采集器就不是“增加复杂度”而是“合理架构”。它把可观测性管道从业务应用中剥离出来独立治理。数据到了 collector 之后你可以先用 processor 做脱敏再按照后端要求改 header 或分批导出整套逻辑集中在一个地方维护比改一堆应用代码要好得多。5.2 使用采集器的最优点把“可观测性管道”从“应用”中剥离直连方案里exporter 配置是和 SDK 版本绑定的升级 SDK 或更换 exporter 版本都要重新发一次应用。这在小规模项目里没问题但在中大型团队里可能变成负担。采集器介入后应用只需要把 OTLP 数据发到本地的某个固定端口后续发到哪个后端、做什么加工都由 collector 负责。这样应用团队不需要知道监控后端的具体 endpoint也不需要关心后端迁移时怎么办只要配置不变代码就不用改。相当于路由层变厚了而应用层变薄了。下表是我做选型时常用的判断依据需求直连方案Collector 方案单一目标后端且为 Application Insights推荐最简稍显冗余多个目标后端 / 需要 fanout不推荐推荐统一出口代理 / 合规要求可能受限推荐做集中出口按内容脱敏、过滤需在每个应用配集中配置收益更高环境无法额外部署进程必选不适用应用量少、团队小推荐优先跑通可后置多团队多应用统一治理需要较强的规范约束可以集中管理选择的关键在于你是把“可观测性”当成每个应用应该自带的属性还是当成一条独立的基础设施管道。如果你的公司已有统一的监控基础设施那不应该让 agent 项目搞特殊化直接接入采集器更合理如果你的项目是独立业务线没有强约束那用直连方案起步最快。5.3 我现在的默认选择对我个人而言新项目默认会先走“两个环境变量直连”的方案先把 trace、metric、log 打通快速看到 agent 跑得怎么样。直连方案配置成本最低几乎不增加运维负担对单个业务团队的排障足够用。等业务量上来或者发现跨团队协作需要统一管理遥测数据出口时再考虑在链路中间加 collector。我不会一开始就把 collector 加进去因为很多 agent 项目还处在快速试错阶段这时候多一个组件只会让试错成本变高。观测这一层应该服务你已经明确的业务目标而不是为了“完整”堆复杂架构。最后说一个我每次排障都会提醒自己的点不要把 trace 想成事后追责工具要在开发阶段就习惯通过 trace 看 agent 行为。在我维护的项目里本地跑一遍之后打开 Application Insights 看整条链路已经成了标准动作。这条习惯带来的直接收益是很多问题我在提交代码之前就发现了而不是等测试环境报错再回去翻日志。