1. AI代理可观测性为什么成了绕不开的坎
AI代理和传统后端服务有一个本质区别:它的执行路径不是确定性的。你给它一个输入,它可能调用三次工具、也可能调用十次;可能走检索增强生成(RAG)链路,也可能直接凭模型内部知识作答;同一个问题问两遍,token消耗和响应延迟可能差出一倍。这种不确定性带来的直接后果是——出了问题你根本不知道从哪儿查。
我最早接触AI代理监控是在一个客服问答机器人的项目上。用户反馈“回答变慢了”,但我们翻遍应用日志只看到一条“请求完成,耗时8.2秒”,中间发生了什么完全是黑盒。是模型推理慢?是向量检索慢?还是某个外部工具调用超时重试了三次?没有链路数据,只能靠猜。后来我们接入了OpenTelemetry(简称OTel)做分布式追踪,把代理的每一步——规划、工具调用、模型推理、结果聚合——都打上span,才第一次看清了整条执行链路。
这就是AI代理可观测性要解决的核心问题:让一个非确定性的、多步骤的、涉及外部依赖的执行过程变得可追踪、可度量、可归因。OTel负责采集和标准化遥测数据,OpenLit负责把AI相关的语义约定(比如token数、模型名、prompt内容)自动注入到span里,Elastic负责存储、检索和可视化。三者组合起来,就是一套从采集到分析的完整方案。
这套东西适合谁?如果你正在把AI代理往生产环境推,或者已经被“代理行为不可预测”折磨过,那这套方案值得花时间搭起来。如果你只是本地跑个demo玩玩,那确实用不上,但了解一下思路没坏处。
2. 三个组件各自扮演什么角色
2.1 OTel:遥测数据的“普通话”
OpenTelemetry的核心价值在于标准化。在OTel出现之前,每个监控厂商都有自己的SDK和数据格式,你用了A厂商的APM,想换B厂商就得把所有埋点重写一遍。OTel定义了一套与厂商无关的API、SDK和协议(OTLP),你只需要按它的规范埋点,后端想换谁换谁。
对于AI代理场景,OTel提供三种信号:
- Traces(链路):记录一次请求从入口到出口的完整调用链,每个步骤是一个span,span之间有父子关系。这是排查“哪一步慢了”的核心武器。
- Metrics(指标):聚合性的数值,比如请求总数、平均延迟、token消耗总量。适合做告警和趋势分析。
- Logs(日志):离散的事件记录,适合记录具体的错误信息和调试细节。
三者不是孤立的。OTel的Exemplar机制可以把trace ID关联到metric数据点上,你在看延迟飙升的指标时,能直接跳到对应的慢链路。这个能力在排查AI代理的间歇性性能问题时特别有用。
2.2 OpenLit:给AI代理装上“AI语义”
OTel本身是通用的,它不知道什么是“模型推理”、什么是“token消耗”。OpenLit做的事情就是在OTel的基础上,为AI/LLM场景补充语义约定。它提供了一套自动埋点能力,你只需要初始化OpenLit,它就会自动拦截主流AI框架(LangChain、LlamaIndex、OpenAI SDK等)的调用,生成带有AI专属属性的span。
这些属性包括但不限于:
| 属性名 | 含义 | 排查时的用途 |
|---|---|---|
gen_ai.system | AI系统标识(如openai) | 区分不同模型提供商 |
gen_ai.request.model | 请求的模型名 | 对比不同模型的表现 |
gen_ai.usage.prompt_tokens | 输入token数 | 分析成本构成 |
gen_ai.usage.completion_tokens | 输出token数 | 发现异常长的输出 |
gen_ai.response.finish_reasons | 结束原因 | 判断是否被截断 |
没有这些属性,你看到的只是一个普通的HTTP调用span,根本不知道这次调用消耗了多少token、用的是哪个模型。OpenLit把这些信息补全了,让链路数据真正对AI场景有意义。
2.3 Elastic:存储、检索与可视化
Elastic在这里承担的是可观测性后端的角色。OTel Collector把数据通过OTLP协议发过来,Elastic的APM Server接收后存入Elasticsearch,Kibana提供可视化界面。
选择Elastic的理由有几个:一是它对OTLP的原生支持已经比较成熟,不需要额外的转换层;二是它的查询语言(KQL、ES|QL)足够灵活,能做复杂的聚合分析;三是它的APM界面开箱即用,服务地图、延迟分布、错误率这些视图不需要自己从零搭。
当然Elastic不是唯一选择,Jaeger、Grafana Tempo、Datadog都能接OTel数据。但如果你已经在用Elastic Stack做日志分析,那把它扩展成可观测性后端是最省事的路径。
3. 从零搭建:环境准备与部署实操
3.1 部署Elastic Stack
我推荐用Docker Compose来搭本地环境,比手动安装省心得多。Elastic官方提供了docker-compose.yml模板,但默认配置对可观测性场景不够用,需要做几处调整。
首先创建一个docker-compose.yml:
version: '3.8' services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0 environment: - discovery.type=single-node - xpack.security.enabled=false - ES_JAVA_OPTS=-Xms2g -Xmx2g ports: - "9200:9200" volumes: - es-data:/usr/share/elasticsearch/data kibana: image: docker.elastic.co/kibana/kibana:8.13.0 environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - "5601:5601" depends_on: - elasticsearch apm-server: image: docker.elastic.co/apm/apm-server:8.13.0 command: > apm-server -e -E output.elasticsearch.hosts=["elasticsearch:9200"] -E apm-server.auth.anonymous.enabled=true -E apm-server.rum.enabled=false ports: - "8200:8200" depends_on: - elasticsearch volumes: es-data:几个关键点说明一下。xpack.security.enabled=false是为了本地调试方便,生产环境必须开启。ES_JAVA_OPTS设置堆内存为2GB,低于这个值在数据量上来后容易OOM。APM Server的auth.anonymous.enabled=true允许匿名上报,同样只适合本地环境。
启动后访问http://localhost:5601确认Kibana正常,访问http://localhost:8200看到APM Server的JSON响应就算通了。
注意:Elastic 8.x默认开启安全认证,如果不想关安全,需要生成 enrollment token 并配置证书,步骤会多不少。本地开发建议先关掉,跑通流程后再补安全配置。
3.2 配置OTel Collector
OTel Collector是数据管道的中枢,它接收应用发来的遥测数据,经过处理后转发给Elastic。创建一个otel-collector-config.yaml:
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 1024 memory_limiter: check_interval: 1s limit_mib: 512 exporters: elasticsearch: endpoints: ["http://apm-server:8200"] tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, batch] exporters: [elasticsearch] metrics: receivers: [otlp] processors: [memory_limiter, batch] exporters: [elasticsearch]memory_limiter这个processor建议一定要加。AI代理的span数据量可能很大,特别是prompt内容被完整记录时,没有内存限制Collector可能被撑爆。batch的timeout设为5秒是个折中值,太短会增加网络请求数,太长会导致数据延迟可见。
3.3 应用侧接入OpenLit
Python环境下接入最简单:
pip install openlit opentelemetry-exporter-otlp然后在应用入口处初始化:
import openlit openlit.init( otlp_endpoint="http://localhost:4318", application_name="my-ai-agent", environment="development", capture_message_content=True, )capture_message_content=True会把prompt和completion的完整内容记录到span里。这个选项在调试阶段非常有用,但生产环境要慎重——一是数据量大,二是可能包含敏感信息。建议生产环境关掉,或者只记录摘要。
初始化完成后,OpenLit会自动拦截LangChain、OpenAI SDK等库的调用。你不需要改任何业务代码,原有的代理逻辑照常跑,遥测数据会自动上报。
4. 核心细节:埋点策略与数据采集要点
4.1 手动埋点补充自动埋点的盲区
OpenLit的自动埋点覆盖了主流AI框架的调用,但代理的“规划”和“决策”环节往往是自己写的逻辑,自动埋点覆盖不到。这时候需要手动加span:
from opentelemetry import trace tracer = trace.get_tracer("agent.planner") def plan_task(user_input): with tracer.start_as_current_span("agent.plan") as span: span.set_attribute("agent.input_length", len(user_input)) plan = llm_plan(user_input) span.set_attribute("agent.step_count", len(plan.steps)) span.set_attribute("agent.plan_type", plan.type) return plan手动span的价值在于把代理的决策过程显式化。当用户反馈“代理选错了工具”时,你能直接看到规划阶段的输入和输出,而不是只能看到工具调用的结果。
4.2 Span的粒度控制
粒度太粗,排查时定位不到具体步骤;粒度太细,数据量爆炸且链路图难以阅读。我的经验是遵循“一个可独立失败的步骤一个span”原则。
具体来说:
- 每次模型推理调用 → 一个span
- 每次外部工具调用 → 一个span
- 每次向量检索 → 一个span
- 代理的整体规划 → 一个span(父span)
- 整个请求入口 → 一个span(根span)
不要为每个token生成事件,也不要把整个代理循环塞进一个span。前者数据量不可控,后者失去了追踪的意义。
4.3 采样策略的选择
生产环境不可能记录100%的链路,采样是必须的。OTel支持两种采样:
- 头部采样(Head-based):在链路开始时决定是否采样,实现简单但可能漏掉慢请求。
- 尾部采样(Tail-based):等链路结束后根据条件决定,能保证慢请求和错误请求被采集,但需要Collector缓存完整链路。
对于AI代理场景,我强烈建议用尾部采样。因为AI代理的慢请求往往是间歇性的,头部采样很容易漏掉。在Collector里配置:
processors: tail_sampling: decision_wait: 10s policies: - name: errors type: status_code status_code: {status_codes: [ERROR]} - name: slow-requests type: latency latency: {threshold_ms: 5000} - name: sample-10-percent type: probabilistic probabilistic: {sampling_percentage: 10}这个配置保证所有错误链路和超过5秒的慢链路都被采集,其余链路按10%采样。decision_wait设为10秒是因为AI代理的链路可能很长,需要等足够时间让所有span到达。
提示:尾部采样会显著增加Collector的内存消耗,因为要缓存等待中的链路。如果内存紧张,可以适当降低
decision_wait或减少采样策略。
5. 实操过程:从数据采集到问题定位
5.1 验证数据链路是否通畅
搭好环境后第一步是确认数据能到Elastic。在Kibana里进入Observability → APM,如果看到my-ai-agent这个服务出现,说明链路通了。点进去应该能看到:
- 服务地图:展示服务间的调用关系
- 延迟分布:P50、P95、P99延迟曲线
- 事务列表:每次请求的详细链路
如果服务没出现,按这个顺序排查:应用是否成功初始化OpenLit → OTel Collector是否收到数据(看Collector日志)→ APM Server是否正常接收(看APM Server日志)→ Elasticsearch是否有索引写入。
5.2 用KQL定位慢请求
假设用户反馈“某些问题回答特别慢”,在Kibana的APM界面用KQL过滤:
service.name: "my-ai-agent" and transaction.duration.us > 5000000这会筛出所有超过5秒的请求。点进任意一条,能看到完整的span瀑布图。我实际排查过的一个案例中,瀑布图显示:
- 根span:8.2秒
- 规划span:0.3秒
- 向量检索span:0.5秒
- 模型推理span:7.1秒
- 结果聚合span:0.3秒
问题一目了然——模型推理占了87%的时间。进一步看span属性,发现gen_ai.usage.completion_tokens高达2000,而正常请求只有200左右。原因是某个用户的提问触发了模型的长篇输出,而我们的max_tokens设置得太宽松。
5.3 用ES|QL做聚合分析
Kibana的ES|QL(Elasticsearch Query Language)适合做跨链路的聚合分析。比如统计不同模型的平均token消耗:
FROM traces-apm* | WHERE service.name == "my-ai-agent" | STATS avg_prompt = AVG(gen_ai.usage.prompt_tokens), avg_completion = AVG(gen_ai.usage.completion_tokens) BY gen_ai.request.model这个查询能帮你判断哪个模型的性价比最高。我们当时对比了三个模型,发现其中一个模型虽然单价低,但completion token数平均是其他模型的两倍,实际成本反而更高。没有这个聚合数据,光看单价很容易做出错误决策。
5.4 建立告警规则
可观测性的最终目的是提前发现问题,而不是等用户投诉。在Kibana的Alerting里可以基于APM指标建告警:
| 告警项 | 条件 | 严重级别 |
|---|---|---|
| 高延迟 | P95延迟 > 10秒持续5分钟 | Critical |
| 错误率 | 错误率 > 5%持续3分钟 | Critical |
| Token异常 | 单次请求completion token > 4000 | Warning |
| 工具调用失败 | 工具span错误率 > 10% | Warning |
告警触发后可以对接邮件、Slack或PagerDuty。关键是阈值要结合业务实际调整,不要照搬默认值。我们一开始把延迟阈值设为3秒,结果告警天天响,后来发现AI代理的正常延迟就在2-4秒之间,调到10秒后才变得有意义。
6. 常见问题与排查技巧实录
6.1 数据不上报的排查路径
这是最常见的问题,按以下顺序检查:
- OpenLit是否初始化成功:在应用启动日志里搜索“openlit”,确认没有报错。
- OTLP端点是否可达:用
curl http://localhost:4318/v1/traces测试,返回405说明端点活着。 - Collector日志是否有错误:
docker logs otel-collector看有没有导出失败的信息。 - APM Server是否接收:
docker logs apm-server看有没有publish相关的日志。 - Elasticsearch索引是否存在:
curl http://localhost:9200/_cat/indices?v看有没有traces-apm*索引。
我踩过的一个坑是:OpenLit初始化时otlp_endpoint写成了http://localhost:4317(gRPC端口),但OpenLit默认用HTTP协议,导致数据发不出去。改成4318就好了。gRPC和HTTP的端口别搞混。
6.2 Span丢失与链路断裂
有时候能看到部分span,但链路不完整。常见原因:
- 上下文传播失败:跨进程调用时trace context没有正确传递。检查是否在HTTP header里带了
traceparent。 - 异步任务未关联:Python的asyncio任务如果没正确传递context,span会变成孤儿span。用
trace.use_span()手动关联。 - 采样决策不一致:如果多个服务各自采样,可能出现上游采了下游没采的情况。统一在Collector做尾部采样可以避免。
6.3 数据量过大导致Elasticsearch压力
AI代理的span属性多、内容长,数据量比普通APM大得多。几个优化手段:
- 关闭prompt内容记录:生产环境设
capture_message_content=False。 - 设置索引生命周期(ILM):trace数据保留7天,之后自动删除或归档到冷存储。
- 限制属性长度:在Collector里用
attributesprocessor截断过长的属性值。 - 调整刷新间隔:Elasticsearch默认1秒刷新一次,对trace数据可以调到30秒,减少写入压力。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 服务不出现在APM | OpenLit未初始化或端点错误 | 检查初始化代码和端点端口 |
| 链路只有根span | 自动埋点未生效 | 确认AI框架版本被OpenLit支持 |
| 延迟数据偏高 | 采样包含了大量慢请求 | 检查采样策略是否偏向慢请求 |
| token数为0 | 模型响应未包含usage信息 | 确认模型API返回了usage字段 |
| Kibana查询超时 | 索引过大或查询范围太广 | 缩小时间范围或优化查询条件 |
7. 生产环境部署的几点经验
本地跑通和上生产是两回事。几个我在实际部署中总结的要点:
安全配置不能省。本地关掉的安全认证,生产必须开。APM Server的secret token、Elasticsearch的TLS、Kibana的认证,一个都不能少。我见过因为APM端点暴露导致trace数据被篡改的案例,虽然不常见但后果严重。
资源规划要留余量。Elasticsearch对内存很敏感,trace数据又是写入密集型的。建议至少给Elasticsearch分配4GB堆内存,并且用SSD存储。如果数据量特别大,考虑用Elastic Cloud的Serverless模式,省去运维成本。
采样率要动态调整。业务低峰期可以调高采样率获取更多细节,高峰期调低减少压力。这个可以通过Collector的配置热更新实现,不需要重启服务。
和现有监控体系打通。如果团队已经在用Prometheus做指标监控,可以把OTel的metric数据同时导出到Prometheus和Elastic,避免形成数据孤岛。OTel Collector支持配置多个exporter,一份数据多处消费。
定期review span属性。随着代理逻辑的迭代,span属性可能会变得冗余或缺失。建议每个季度review一次埋点方案,删掉不再使用的属性,补充新业务需要的属性。我们有一次发现某个关键的工具调用参数没有被记录,导致排查问题时缺少关键信息,后来补上才解决。
这套方案我从最初搭建到稳定运行大概花了两周时间,其中大部分时间花在调采样策略和优化Elasticsearch性能上。一旦跑顺了,排查AI代理问题的效率提升是数量级的——以前靠猜,现在靠数据。如果你也在被AI代理的“黑盒”问题困扰,建议尽早把这套可观测性体系搭起来,越早搭收益越大。