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_msexternal_service_status_code。当这两个字段异常时,问题不在 Agent 框架而在外部依赖。

五、总结

Agent 的调用链不是单次推理,是多步骤、多类型调用(LLM + 工具 + 逻辑)的串联。可视化这条链路不是为了好看,是为了在延迟飙升时立刻定位瓶颈步骤。

三个关键实践:

  1. step_type 分类:LLM 调用、工具调用、内部逻辑的延迟特征不同,不分类就无法诊断。
  2. Token 耗用按步骤拆分:不同步骤可能用不同模型,拆分后才能找到成本优化方向。
  3. 重试和循环必须标注:反思机制的循环会让调用链变长,标注 retry_countoriginal_step_id 才能理解实际逻辑。

基础设施不需要漂亮话。调用链可视化的价值不是图表有多漂亮,是延迟从 3 秒涨到 10 秒时,你能在 10 秒内找到瓶颈在哪一步。找不到,就只能逐步骤手动排查,每排查一次消耗 30 分钟。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐