LLM 调用链可视化:一次 Agent 调用到底走了几步
LLM 调用链可视化:一次 Agent 调用到底走了几步
一、背景与问题
LLM Agent 的调用链路比单次推理复杂得多。一个典型的 Agent 请求不是"用户提问→模型回答",而是"用户提问→意图识别→工具选择→工具调用→结果整合→二次推理→最终回答"。每一步都是一次独立的 LLM 调用,中间还穿插外部 API 调用(搜索、数据库查询等)。
这种多步骤链路带来的运维问题是:当 Agent 响应时间从 3 秒涨到 10 秒,你不知道是哪个步骤慢了。是意图识别的推理慢了?是工具调用(外部搜索 API)慢了?是结果整合的二次推理慢了?还是某个步骤触发了错误重试导致多跑了一轮?
调用链可视化解决的就是这个问题:把 Agent 的多步骤调用展开成一条可见的链路,每一步的耗时、输入输出摘要、调用类型(LLM 调用 vs 外部 API 调用 vs 内部逻辑)一目了然。这不是锦上添花的可视化需求,是 Agent 服务运维的基本要求——你必须知道一个请求到底走了几步,才能知道该优化哪一步。
二、调用链数据模型
flowchart TD
A[用户请求: 查询北京天气] --> B[Step1: 意图识别 LLM]
B --> C[Step2: 工具选择 LLM]
C --> D[Step3: 天气API调用]
D --> E[Step4: 结果整合 LLM]
E --> F[Step5: 最终回答生成 LLM]
F --> G[响应: 北京今天晴 25°C]
H[Step2-alt: 知识库检索] --> I[Step4-alt: 结合检索结果整合]
style B fill:#7b1fa2,color:#fff
style C fill:#8e24aa,color:#fff
style D fill:#9c27b0,color:#fff
style E fill:#ab47bc,color:#fff
style F fill:#ba68c8
上图展示了一条典型 Agent 调用链。5 个步骤中有 4 次 LLM 调用和 1 次外部 API 调用。关键数据模型:
| 字段 | 类型 | 说明 |
|---|---|---|
| trace_id | string | 请求唯一标识 |
| step_id | int | 步骤序号(1, 2, 3...) |
| step_type | enum | llm_call / tool_call / logic |
| model_name | string | LLM 调用时的模型名 |
| input_tokens | int | 输入 Token 数 |
| output_tokens | int | 输出 Token 数 |
| latency_ms | int | 本步骤耗时 |
| status | enum | success / error / retry |
| parent_step_id | int | 父步骤(并行分支时用) |
step_type 是核心分类维度。LLM 调用和工具调用的性能特征完全不同——LLM 调用延迟由模型和 GPU 决定,工具调用延迟由外部 API 决定。如果不区分类型,"平均步骤延迟 500ms" 这个数字没有任何诊断价值。
三、实现方案
3.1 Agent 框架层的 Span 嵌套
以 LangChain/LangGraph 为例,每个 Agent 步骤天然对应一个 Span。关键是正确嵌套父子关系:
from opentelemetry import trace
tracer = trace.get_tracer("agent-service")
async def run_agent(user_query: str):
with tracer.start_as_current_span("agent.request") as root_span:
root_span.set_attribute("user_query_length", len(user_query))
# Step1: 意图识别
with tracer.start_as_current_span("agent.intent识别") as s1:
intent_result = await llm_call(
model="gpt-4o-mini",
prompt=f"识别意图: {user_query}"
)
s1.set_attribute("step_type", "llm_call")
s1.set_attribute("model_name", "gpt-4o-mini")
s1.set_attribute("input_tokens", intent_result.usage.input_tokens)
s1.set_attribute("output_tokens", intent_result.usage.output_tokens)
# Step2: 工具选择
with tracer.start_as_current_span("agent.tool_select") as s2:
tool_name = await llm_call(
model="gpt-4o-mini",
prompt=f"选择工具: intent={intent_result}"
)
s2.set_attribute("step_type", "llm_call")
# Step3: 工具调用
with tracer.start_as_current_span("agent.tool_call") as s3:
tool_result = await call_weather_api(city="北京")
s3.set_attribute("step_type", "tool_call")
s3.set_attribute("tool_name", "weather_api")
s3.set_attribute("external_latency_ms", tool_result.latency)
# Step4: 结果整合
with tracer.start_as_current_span("agent.result整合") as s4:
final = await llm_call(
model="gpt-4o",
prompt=f"整合结果: query={user_query}, data={tool_result}"
)
s4.set_attribute("step_type", "llm_call")
s4.set_attribute("model_name", "gpt-4o")
return final.content
每个 start_as_current_span 自动嵌套在父 Span 下。OpenTelemetry SDK 会自动传递 trace-id 和计算 span-id 的父子关系。在 Trace Viewer 中,这5个 Span 会以树状结构展示,一目了然。
3.2 Token 耗用聚合
Agent 调用链中的 LLM 步骤可能使用不同模型(意图识别用小模型,最终回答用大模型)。Token 耗用需要按步骤拆分,而非只看总量:
# 每个步骤的 Token 耗用
sum(inference_token_total{direction="output"}) by (step_type, model_name)
# 整条链路的 Token 总耗用
sum(inference_token_total{trace_id="trace-abc123"})
这个拆分直接影响成本优化。如果 Step1 和 Step2 用 gpt-4o-mini 各消耗 50 token,而 Step4 用 gpt-4o 消耗 500 token,优化方向很明确:Step4 的模型选择和 prompt 设计是成本大头,Step1/Step2 的优化空间很小。
3.3 重试和循环的可视化
Agent 调用链不是线性流程,可能包含循环(反思机制:结果不满意时重新推理)。循环在 Trace 中表现为同一类型的 Span 重复出现:
flowchart LR
A[Step1: 意图识别] --> B[Step2: 推理]
B --> C[Step3: 反思评估]
C --> D{质量达标?}
D -->|否| B2[Step4: 二次推理 retry]
D -->|是| E[Step5: 输出]
style D fill:#ff6f00
style B2 fill:#d32f2f,color:#fff
重试步骤必须标注 retry_count 属性。在 Span 上设置:
s_retry = tracer.start_span("agent.inference_retry")
s_retry.set_attribute("retry_count", 2)
s_retry.set_attribute("original_step_id", 2)
这样在 Trace Viewer 中可以区分"这是第几次重试"和"原始步骤是哪个"。没有这个标注,一条包含 3 次重试的链路看起来像是 6 个独立步骤,无法理解实际逻辑。
四、生产实践与踩坑
Span 数量控制。一个复杂 Agent 调用可能包含 10+ 个步骤,加上每个步骤内部的子 Span(LLM API 的 HTTP 调用、工具 API 的 HTTP 调用),单次请求可能产生 30+ 个 Span。这个量级 OTel Collector 可以处理,但要注意采样策略——Agent 调用的 QPS 通常不高(几十到几百),建议全量采集。
Token 统计口径。不同 LLM 提供商的 Token 计数方法不同(OpenAI 的 tokenizer 和 Anthropic 的 tokenizer 不兼容)。在 Span attribute 中记录 token_provider 字段(如 "openai_bpe"),避免跨提供商的 Token 数汇总出现歧义。如果只做成本估算,用美元金额(cost_usd)而非 Token 数作为统一口径——金额的计算在 Agent 框架层完成,不受 tokenizer 差异影响。
超长调用链的展示。包含反思循环的 Agent 调用链可能长达 20+ 步骤,在 Trace Viewer 中线性展示不直观。更好的方式是按步骤类型分组:先展示 LLM 调用的总耗时和总 Token 数,再展开每一步的细节。Grafana 的 Trace View 支持折叠子 Span,利用这个功能让长链路可读。
和传统微服务追踪的区别。传统微服务的追踪是一层调用一层(A→B→C),Span 深度通常 3-5 层。Agent 的追踪是同一进程内的多步骤串联,Span 深度可能 10+ 层。OTel SDK 默认的 Span 限制是 1000,Agent 调用不会触碰这个限制,但要确保自定义的 Span limit 配置不会被误改。
外部工具调用的超时追踪。工具调用(搜索 API、数据库查询)的超时是 Agent 延迟飙升的常见原因。在工具调用 Span 上必须记录 external_service_timeout_ms 和 external_service_status_code。当这两个字段异常时,问题不在 Agent 框架而在外部依赖。
五、总结
Agent 的调用链不是单次推理,是多步骤、多类型调用(LLM + 工具 + 逻辑)的串联。可视化这条链路不是为了好看,是为了在延迟飙升时立刻定位瓶颈步骤。
三个关键实践:
- 按
step_type分类:LLM 调用、工具调用、内部逻辑的延迟特征不同,不分类就无法诊断。 - Token 耗用按步骤拆分:不同步骤可能用不同模型,拆分后才能找到成本优化方向。
- 重试和循环必须标注:反思机制的循环会让调用链变长,标注
retry_count和original_step_id才能理解实际逻辑。
基础设施不需要漂亮话。调用链可视化的价值不是图表有多漂亮,是延迟从 3 秒涨到 10 秒时,你能在 10 秒内找到瓶颈在哪一步。找不到,就只能逐步骤手动排查,每排查一次消耗 30 分钟。
更多推荐



所有评论(0)