资讯中心

[全链路监控] 复杂Agent系统的调试噩梦?智能体来了(西南总部)基于OpenTelemetry构建AI agent指挥官与AI调度官的可观测性平台

📅 2026/9/30 5:43:33
[全链路监控] 复杂Agent系统的调试噩梦?智能体来了(西南总部)基于OpenTelemetry构建AI agent指挥官与AI调度官的可观测性平台
1. 多 Agent 协作下为什么日志越看越糊涂如果你正在做 AI agent 编排大概率遇到过这种场景用户问了一句“帮我查下上周的订单异常”AI agent 指挥官先拆任务再让 AI 调度官去调工具中间穿插 RAG 检索、外部 API、数据库查询最后汇总回答。整个过程跑完你在日志里只看到几十条零散的 LLM 输入输出根本还原不出“它到底怎么想的”。这就是多 Agent 协作下最典型的调试困境链路断裂、指标缺失。传统微服务里一个 TraceID 能串起一次 HTTP 请求的完整生命周期但 Agent 系统不一样。同一个 Prompt指挥官这次走路径 A下次走路径 B一个任务可能包含 50 次 LLM 交互加 20 次工具调用跨度长达几分钟更麻烦的是你根本不知道哪个步骤烧掉了最多 Token哪次调度官的 SQL 查询导致了死循环。OpenTelemetry 在这里的价值就体现出来了。它是一套厂商中立的可观测性标准能把 Agent 的“思维链”映射成“分布式追踪”。AI agent 指挥官的每一次思考是一个 SpanAI 调度官的每一次工具执行是它的子 Span跨进程、跨语言都能通过 W3C Trace Context 串起来。这篇就按可跟做的节奏把 Collector 配置、Span 埋点骨架和验证动作完整走一遍帮你定位调度延迟和工具调用异常。2. 前置准备TaoToken 接入与可观测性底座在开始埋点之前先把模型调用这条链路跑通。我试过用 TaoToken 作为统一的模型接入层它兼容 OpenAI 风格的接口AI agent 指挥官和调度官都可以通过同一个 API 端点调用不同模型省去多套 Key 管理的麻烦。你需要先拿到 API Key。访问 https://taotoken.net/api-keys 创建一个然后到 https://taotoken.net/doc 确认当前支持的模型列表和参数格式。如果你更习惯在网页里直接验证模型行为可以先用 https://taotoken.net/chat 做一轮对话测试确认 Key 和模型都正常。对于长期跑编码类 Agent 的场景比如让指挥官持续做代码生成和重构可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan 它更适合高频、长会话的 Agent 工作负载。接入文档统一在 https://taotoken.net/doc 查看API 基址是 https://taotoken.net/api 注意这个地址不带任何查询参数。环境变量建议这样设置后面所有代码都从这里读export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export OTEL_EXPORTER_OTLP_ENDPOINThttp://localhost:4317 export OTEL_SERVICE_NAMEagent-commander可观测性底座这边我选的是 OpenTelemetry Collector Jaeger 的组合。Collector 负责接收、处理和导出遥测数据Jaeger 负责展示 Trace。用 Docker Compose 起一套最省事version: 3.8 services: otel-collector: image: otel/opentelemetry-collector-contrib:0.96.0 command: [--config/etc/otel/config.yaml] volumes: - ./otel-config.yaml:/etc/otel/config.yaml ports: - 4317:4317 - 4318:4318 - 8889:8889 jaeger: image: jaegertracing/all-in-one:1.55 ports: - 16686:16686 - 4317 environment: - COLLECTOR_OTLP_ENABLEDtrueCollector 的配置文件otel-config.yaml是整条链路的核心它决定了 Span 怎么进来、怎么处理、怎么出去receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 processors: batch: timeout: 5s send_batch_size: 512 memory_limiter: check_interval: 1s limit_mib: 512 attributes: actions: - key: deployment.environment value: dev action: upsert exporters: otlp: endpoint: jaeger:4317 tls: insecure: true prometheus: endpoint: 0.0.0.0:8889 service: pipelines: traces: receivers: [otlp] processors: [memory_limiter, attributes, batch] exporters: [otlp] metrics: receivers: [otlp] processors: [batch] exporters: [prometheus]这里有几个参数值得说明。batch处理器的send_batch_size设为 512是因为 Agent 场景下 Span 产生速度比普通微服务快得多批量发送能减少网络开销。memory_limiter的limit_mib设 512 是防止 Collector 在 Span 洪峰时把内存吃满。attributes处理器统一注入环境标签方便后面按环境过滤。3. 可复制配置指挥官与调度官的 Span 埋点骨架3.1 语义映射把 Agent 活动翻译成 Span在写代码之前先明确映射关系。AI agent 指挥官的“思考”对应一个 INTERNAL SpanAI 调度官的工具执行对应一个 SERVER 或 CLIENT SpanRAG 检索对应一个 CLIENT Span。关键属性这样设计Agent 概念OTel Span 类型关键属性用户请求Root Spansession.id, user.id指挥官思考INTERNALllm.model, llm.prompt_tokens, llm.completion_tokens调度官路由INTERNALtool.name, tool.params外部 API 调用CLIENThttp.url, http.status_codeRAG 检索CLIENTvector_db.score, retrieved.chunks3.2 指挥官侧Python 埋点骨架指挥官通常跑在 Python 环境里用装饰器包住“思考”函数是最省事的做法。先装依赖pip install opentelemetry-api opentelemetry-sdk \ opentelemetry-exporter-otlp-proto-grpc \ opentelemetry-instrumentation-requests然后写埋点骨架from opentelemetry import trace from opentelemetry.trace import Status, StatusCode from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.resources import Resource resource Resource.create({service.name: agent-commander}) provider TracerProvider(resourceresource) provider.add_span_processor( BatchSpanProcessor(OTLPSpanExporter(endpointhttp://localhost:4317, insecureTrue)) ) trace.set_tracer_provider(provider) tracer trace.get_tracer(agent.commander) def trace_thought(func): def wrapper(*args, **kwargs): with tracer.start_as_current_span( namefCommander.{func.__name__}, kindtrace.SpanKind.INTERNAL ) as span: try: if prompt in kwargs: span.set_attribute(llm.input, kwargs[prompt][:500]) result func(*args, **kwargs) span.set_attribute(llm.output, str(result)[:500]) if hasattr(result, usage): span.set_attribute(llm.prompt_tokens, result.usage.prompt_tokens) span.set_attribute(llm.completion_tokens, result.usage.completion_tokens) span.set_status(Status(StatusCode.OK)) return result except Exception as e: span.record_exception(e) span.set_status(Status(StatusCode.ERROR, str(e))) raise return wrapper注意llm.input和llm.output都做了截断因为 Span 属性有大小限制完整 Prompt 应该放到日志或对象存储里Span 里只留摘要。3.3 跨进程传播Python 到 Go 的 Trace 传递指挥官决定调工具时会通过 HTTP 请求调度官。这一步必须把 Trace Context 注入到 Header 里否则链路就断了。Python 端注入from opentelemetry.propagate import inject import requests headers {} inject(headers) resp requests.post( http://dispatcher-service/execute, headersheaders, json{tool: query_orders, params: {date: last_week}} )Go 端提取并继续package main import ( net/http go.opentelemetry.io/otel go.opentelemetry.io/otel/attribute go.opentelemetry.io/otel/propagation go.opentelemetry.io/otel/trace ) func HandleToolExecution(w http.ResponseWriter, r *http.Request) { propagator : otel.GetTextMapPropagator() ctx : propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) tracer : otel.Tracer(agent.dispatcher) ctx, span : tracer.Start(ctx, Dispatcher.ExecuteTool, trace.WithSpanKind(trace.SpanKindServer)) defer span.End() toolName : r.URL.Query().Get(tool) span.SetAttributes(attribute.String(tool.name, toolName)) result : performAction(toolName) span.SetAttributes(attribute.String(tool.result, result)) }这样无论 Agent 的架构拆得多细Python 的“脑”和 Go 的“手”在 Jaeger 里看到的永远是一棵完整的调用树。4. 验证请求确认链路真的串起来了配置写完后必须做一轮端到端验证。先启动 Collector 和 Jaeger然后跑一个最小 Agent 任务python -c from commander import think_next_step result think_next_step(prompt查询上周订单异常, context[]) print(result) 打开 Jaeger UIhttp://localhost:16686选择agent-commander服务应该能看到一条完整的 Trace。点进去检查三件事第一Root Span 下面是否挂着Commander.think_next_step子 Span且llm.prompt_tokens和llm.completion_tokens都有值。第二如果触发了工具调用Dispatcher.ExecuteTool是否作为子 Span 出现tool.name是否正确。第三整条 Trace 的 Duration 是否合理有没有某个 Span 异常长。如果用的是 TaoToken 的模型对话接口做验证可以先用 https://taotoken.net/chat 手动发一轮请求确认模型返回正常再跑埋点代码。这样能把“模型问题”和“埋点问题”分开排查。验证通过后可以再加一条基于 Span 的告警规则。比如在 Collector 里加一个filter处理器或者直接在 Jaeger 里配置同一个 trace_id 下连续出现 3 个相同tool.name且参数相同的 Span就触发死循环告警。这个规则对 AI 调度官特别有用因为调度官最容易陷入“查 A 不存在再查 A”的循环。5. 本篇常见错排查Span 没有出现在 Jaeger 里。先检查 Collector 日志有没有报错再确认OTEL_EXPORTER_OTLP_ENDPOINT指向的是 Collector 的 4317 端口而不是 Jaeger 的。如果 Collector 和 Jaeger 都在 Docker 里注意容器间通信用服务名而不是 localhost。Trace 断成两截。九成是跨进程传播没做对。检查 Python 端inject(headers)之后headers 里是否有traceparent字段Go 端是否用了propagation.HeaderCarrier提取。如果中间经过了消息队列需要手动把 Context 序列化到消息头里。Token 统计不准。llm.prompt_tokens和llm.completion_tokens依赖模型返回的 usage 字段。如果用的是流式响应usage 可能在最后一个 chunk 才返回需要在流结束后再 set_attribute而不是在函数返回时。Collector 内存暴涨。Agent 场景下 Span 产生速度极快如果batch的send_batch_size太小会导致频繁发送如果太大会积压内存。建议从 512 开始调同时开启memory_limiter。另外Span 属性里的长文本一定要截断否则单个 Span 可能几 MB。调度延迟定位不到。如果发现整体 Duration 很长但每个 Span 都不长大概率是 Span 之间的间隙。这时候需要在指挥官和调度官之间加一个显式的“等待 Span”或者检查是否有同步阻塞调用没被埋点覆盖。6. 接入与排障入口如果你在接入过程中遇到 Key 或模型调用问题直接到 https://taotoken.net/api-keys 检查 Key 状态接入文档在 https://taotoken.net/doc 有完整的参数说明和错误码对照。验证模型行为用 https://taotoken.net/chat 最快长期跑编码类 Agent 任务可以看 https://taotoken.net/coding-plan 。API 基址统一用 https://taotoken.net/api 不要加多余参数。可观测性这件事越早引入越省事。等 Agent 上线后再补埋点就像房子装修完才想起来布电线能补但很痛苦。先把 Collector 和 Jaeger 跑起来再把指挥官的思考 Span 和调度官的工具 Span 接上后面定位调度延迟和工具调用异常就有据可查了。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案