
1. 多智能体调用 LLM 时为什么链路追踪总是断在中间做多智能体Multi-Agent项目的人大概率都遇到过这种场景本地跑一个 Planner Executor Reviewer 的三段式 Agent日志里只看到「Planner 完成」「Executor 完成」但中间某一步 LLM 调用超时、返回空、或者工具参数拼错你根本不知道是哪一层出的问题。传统 APM 能告诉你 HTTP 200、延迟 800ms但它回答不了「这次调用里 prompt 注入了什么 context」「模型为什么选了那个工具」「第几轮开始 context 膨胀」。这就是 AI Agent 可观测性要解决的核心问题把一次智能体运行拆成可回溯的调用链让每一次 LLM 请求、每一次工具执行、每一次上下文拼接都有迹可循。适合谁适合正在本地开发或测试环境里调试多智能体流程的工程师尤其是用 Python/Node 写 Agent、又不想一上来就搭一整套重型监控栈的人。我试过在三个 Agent 项目里分别用裸日志、OpenTelemetry、以及统一 Key 通道 埋点的方式做追踪最后发现最省事的路径是先把所有 LLM 调用收敛到一个统一的 API 通道再在这个通道上做埋点。原因很直接——多智能体项目里最容易失控的不是代码逻辑而是 Key 分散在多个 Agent、多个环境变量、多个 SDK 配置里导致你连「这次请求到底走了哪个模型」都说不清。TaoToken 在这里的角色就是统一 Key/API 通道所有 Agent 的 LLM 请求都指向同一个 base_url用同一套 Key链路追踪的入口就唯一了。下面我会给出可复制的config.toml和settings.json骨架把 TaoToken 作为统一通道接入监控埋点然后走三步验证发起一次 Agent 调用、查看请求日志、确认异常可回溯。全程面向本地开发与测试环境不涉及生产库直连。2. 前置准备把 TaoToken 作为统一 Key 通道接进来在动手写埋点之前先把通道统一。多智能体项目常见的坑是Planner 用一份 KeyExecutor 用另一份Reviewer 又读环境变量结果 trace 里三个 Agent 的请求散落在不同 provider 下根本串不起来。统一到 TaoToken 之后所有 Agent 共享一个 base_url 和一套 Keytrace_id 才能跨 Agent 传递。你需要先拿到 Key。访问 API Keys 管理页创建https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建后你会得到形如sk-xxxx的 Key。注意两点一是本地开发建议单独建一个测试用 Key方便按 Key 维度过滤日志二是不要把 Key 硬编码进config.toml提交到仓库用环境变量注入。TaoToken 的 API 入口是https://taotoken.net/api兼容 OpenAI 风格的/v1/chat/completions所以现有用 openai SDK 的 Agent 代码基本不用改只改base_url和api_key即可。这一点对可观测性很关键你不需要为每个 Agent 写不同的适配层埋点可以统一加在 SDK 客户端初始化处。如果你还没决定用哪个模型跑 Agent可以先去模型对话页试一下不同模型在多轮工具调用下的表现https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite对于长期跑编码类 Agent 的场景Coding Plan 会更划算后面第 6 节会提https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite3. 可复制配置config.toml 与 settings.json 骨架这一节给两份可直接抄的配置。config.toml用于 Python 侧 Agent读取通道、模型、埋点开关settings.json用于 Node 侧或需要 JSON 配置的工具链。两份配置里的base_url都指向 TaoTokenapi_key从环境变量读。先看config.toml# config.toml —— 多智能体统一通道与埋点配置 [llm] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不写死 default_model gpt-4o-mini timeout_seconds 60 max_retries 2 [observability] enabled true trace_exporter console # 本地开发先用 console接 Jaeger 改 otlp service_name multi-agent-local log_prompt_hash true # 只记 prompt 哈希不落原文 log_token_usage true log_latency true sample_rate 1.0 # 本地全采样 [agents] planner_model gpt-4o-mini executor_model gpt-4o-mini reviewer_model gpt-4o-mini max_turns 6再看settings.json给 Node 侧或统一读取 JSON 的埋点脚本用{ llm: { provider: taotoken, baseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY, defaultModel: gpt-4o-mini, timeoutMs: 60000 }, observability: { enabled: true, exporter: console, serviceName: multi-agent-local, logPromptHash: true, logTokenUsage: true, logLatency: true, sampleRate: 1.0 }, agents: { planner: { model: gpt-4o-mini, maxTurns: 3 }, executor: { model: gpt-4o-mini, maxTurns: 6 }, reviewer: { model: gpt-4o-mini, maxTurns: 2 } } }两份配置的字段是对齐的方便你在 Python 和 Node 混合的 Agent 项目里共用同一套语义。几个参数说明一下trace_exporter本地先用console把 span 打到终端确认链路通了再换成otlp发到 Jaeger 或 Tempolog_prompt_hash打开后只记录 prompt 的哈希值避免把长文本和潜在敏感内容写进日志sample_rate本地设 1.0 全采样生产再降。环境变量这样注入export TAOTOKEN_API_KEYsk-你的测试Key然后写一个最小的埋点初始化脚本把配置读进来并初始化 tracer# observability.py import os import hashlib import time import tomllib from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ConsoleSpanExporter, ) from opentelemetry.sdk.resources import Resource def load_config(path: str config.toml) - dict: with open(path, rb) as f: return tomllib.load(f) def init_tracer(cfg: dict): resource Resource.create({ service.name: cfg[observability][service_name], }) provider TracerProvider(resourceresource) if cfg[observability][trace_exporter] console: provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter())) trace.set_tracer_provider(provider) return trace.get_tracer(multi-agent) def prompt_hash(text: str) - str: return hashlib.sha256(text.encode(utf-8)).hexdigest()[:16]这段代码做了三件事读配置、按配置初始化 exporter、提供一个 prompt 哈希函数。prompt_hash是后面日志里关联「同一次请求」的关键因为你不落原文只落哈希靠哈希去重和回溯。4. 三步验证发起调用、看日志、确认异常可回溯配置就绪后用三步验证链路是否真的打通。这三步是递进的第一步确认请求能发出去第二步确认埋点有输出第三步确认异常能被定位。4.1 第一步发起一次带 trace 的 Agent 调用写一个最小 Agent把 TaoToken 作为通道并在每次 LLM 调用外层包一个 span# agent_demo.py import os import time from openai import OpenAI from observability import load_config, init_tracer, prompt_hash cfg load_config() tracer init_tracer(cfg) client OpenAI( base_urlcfg[llm][base_url], api_keyos.environ[cfg[llm][api_key_env]], ) def call_llm(messages, model, turn): with tracer.start_as_current_span(fllm.call.turn_{turn}) as span: span.set_attribute(llm.model, model) span.set_attribute(llm.messages_count, len(messages)) span.set_attribute(llm.prompt_hash, prompt_hash(str(messages))) start time.monotonic() resp client.chat.completions.create( modelmodel, messagesmessages, temperature0.7, ) elapsed time.monotonic() - start usage resp.usage span.set_attribute(llm.usage.prompt_tokens, usage.prompt_tokens) span.set_attribute(llm.usage.completion_tokens, usage.completion_tokens) span.set_attribute(llm.latency_ms, round(elapsed * 1000, 2)) span.set_attribute(llm.finish_reason, resp.choices[0].finish_reason) return resp.choices[0].message.content def run_agent(user_input: str): with tracer.start_as_current_span(agent.run) as root: root.set_attribute(agent.input_hash, prompt_hash(user_input)) messages [ {role: system, content: 你是一个数据分析助手先规划再执行。}, {role: user, content: user_input}, ] final None for turn in range(1, cfg[agents][max_turns] 1): content call_llm(messages, cfg[agents][planner_model], turn) messages.append({role: assistant, content: content}) if [DONE] in content: final content break messages.append({role: user, content: 继续执行下一步。}) root.set_attribute(agent.total_turns, turn) return final if __name__ __main__: result run_agent(帮我规划一个三步的数据清洗流程) print(result)运行python agent_demo.py你会看到 ConsoleSpanExporter 把每个 span 打到终端包含llm.model、llm.usage.prompt_tokens、llm.latency_ms等属性。这一步成功意味着请求走了 TaoToken 通道埋点也生效了。4.2 第二步查看请求日志确认字段齐全把 span 输出重定向到文件方便过滤python agent_demo.py 21 | tee agent_trace.log然后检查关键字段是否都在grep -E llm.model|llm.usage|llm.latency_ms|llm.prompt_hash agent_trace.log你应该能看到类似这样的输出字段名以实际 exporter 为准llm.model: gpt-4o-mini llm.usage.prompt_tokens: 128 llm.usage.completion_tokens: 64 llm.latency_ms: 842.31 llm.prompt_hash: a1b2c3d4e5f6a7b8如果llm.usage缺失说明响应里没有 usage 字段检查是不是用了流式但没开stream_options如果llm.latency_ms异常大先看是不是网络问题再看 prompt 是不是太长。这一步的核心是确认「每次 LLM 调用都有独立的 span 和 token 记录」而不是只有一个笼统的 agent.run。4.3 第三步制造一次异常确认可回溯可观测性的价值在异常时才体现。故意把模型名改成一个不存在的值或者把 Key 换成错的再跑一次# 临时改 config.toml 里 default_model gpt-not-exist运行后你会看到 span 上出现 error 状态并且llm.finish_reason或异常信息被记录。此时用prompt_hash去日志里反查grep a1b2c3d4e5f6a7b8 agent_trace.log能定位到具体是哪一轮、哪个 Agent、哪次调用出的问题。这就是「异常可回溯」不是靠翻全量日志而是靠 trace_id prompt_hash 精确定位。如果你把 exporter 换成 OTLP 发到 Jaeger这一步就是在 Jaeger UI 里按 trace_id 搜索效果更直观。5. 本篇常见错排查5.1 报错401 Unauthorized或invalid api key最常见的原因是环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有值以及config.toml里的api_key_env名字是否和实际导出的变量名一致。另一个原因是 Key 复制时带了空格或换行重新从 API Keys 页面复制一次。注意本地测试 Key 和正式 Key 不要混用否则日志里按 Key 过滤会乱。5.2 span 打出来了但 token 用量全是 0通常是响应对象里没有usage字段。如果你用了流式调用需要在请求里加stream_options{include_usage: True}否则最后一个 chunk 才带 usage而你可能提前 break 了。非流式调用一般都有 usage如果没有检查是不是中间层做了转发丢字段。5.3 trace 里多个 Agent 的 span 串不起来根因是每个 Agent 各自初始化了 TracerProvider导致 trace_id 不共享。正确做法是全局只初始化一次 provider所有 Agent 从同一个 tracer 取 span。如果你是多进程部署需要把 trace context 通过消息头传递本地开发阶段可以先单进程跑通。5.4config.toml读取报tomllib不存在tomllib是 Python 3.11 才进标准库的。如果你用 3.10 或更早装tomli并改成import tomli as tomllib。或者干脆把配置换成settings.json用json.load读兼容性更好。5.5 日志里 prompt 原文泄露如果你不小心把log_prompt_hash关了又直接记了messages长 prompt 会写进日志。回到配置把log_prompt_hash true打开并且代码里只记哈希不记原文。需要看原文时用 trace_id 去专门的调试存储查不要混在常规日志里。6. 把统一通道用在长期编码 Agent 上本地验证跑通后如果你要把这套多智能体流程长期用于编码类任务比如自动改代码、跑测试、生成 PR 描述单次调用成本会累积得很快。这时候可以考虑 Coding Plan它面向长期编码场景配合统一 Key 通道能让 trace 和成本统计都收敛到一处https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入文档在这里里面有各语言 SDK 的 base_url 配置示例照着改就能把现有 Agent 迁过来https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你用的是 Claude Code 这类工具做 Agent 开发Anthropic 兼容通道的配置也在文档里https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite最后提醒一句可观测性不是等出问题才补的。我踩过的坑是早期为了快Agent 里直接print日志结果多轮调用一多终端刷屏根本找不到哪次是哪次。后来把 trace 下沉到 turn 级别、prompt 只记哈希、token 和延迟都进 span排查效率才上来。你可以先从config.toml的 console exporter 跑通三步验证再逐步换成 OTLP 接 Jaeger链路就稳了。