当你运行单个AI代理时,调试过程非常简单。你只需查看日志,就能了解发生了什么。

但当同时运行五个代理时,每个代理都会执行自己的操作并生成相应的输出,此时再“查看日志”就已经无法有效地解决问题了。

我开发了Claude Forge,这是一个基于Claude Code构建的对抗性多代理编程框架。在典型的运行过程中,系统会生成一个规划器、一个执行者、一个审核者和一个修复工具;它们会互相评估彼此的工作成果,当质量检查失败时则会重新进行迭代。

然而,每当出现问题时,虽然我有时间戳和日志记录,但却无法确定是哪个代理出了故障、实际耗用了多少时间,也不知道数据去了哪里。

Jaeger解决了这些问题。这篇文章介绍了如何使用Docker配置Jaeger v2,如何通过OpenTelemetry将其集成到多代理系统中,以及我在这一过程中学到的经验。

目录

什么是分布式追踪?

分布式追踪能够跟踪一个操作在多个服务中的执行过程。一个“Span”是表示某项工作的一个单元,它包含开始时间、结束时间以及键值属性;这些Span可以嵌套成父子关系结构,每条这样的路径就构成了一条追踪记录。

微服务开发者早已熟悉这种模式:跟踪一个HTTP请求从网关开始,经过认证模块、数据库和缓存系统,最终完成处理。对于多代理AI系统来说,这个原理同样适用——只需追踪一个任务调用从协调器开始,然后依次传递给各个子代理及其对应的操作即可。

OpenTelemetry是分布式追踪领域的标准技术。它提供了用于创建Span的SDK,并允许通过OTLP协议将这些数据传输出去;Jaeger接收这些数据后,会将其呈现为可供查询的时间线界面。

为什么选择Jaeger v2?

Jaeger最初是在Uber项目中开发的,后来于2019年成为了CNCF官方认可的项目。v1版本在2025年12月结束了其支持周期,而目前的v2版本则是基于OpenTelemetry Collector框架构建的。它采用了单一的二进制文件格式,集收集器、查询服务和用户界面于一体;同时,它能够通过4317端口(gRPC协议)和4318端口(HTTP协议)原生地支持OTLP协议,因此在进行本地测试时并不需要单独安装收集器组件。

与v1相比,有一个重要的变化:配置设置的方式从CLI命令行参数和环境变量改为了YAML文件。在v2版本中,旧的-e SPAN_STORAGE_TYPE=badger环境变量会被默默忽略,容器虽然能够正常启动,但会默认使用内存存储方式来保存数据。我在发现这个问题之前,已经丢失了两天的大量追踪数据。下面会详细介绍正确的配置方法。

先决条件

  • Docker已安装并正在运行中。

  • Claude Code已安装。

  • 需要Python 3.8+版本来支持追踪功能的实现。

  • 需要Claude Forge或其他多代理系统来进行性能监控配置。

在Debian系统中安装Docker

如果你已经安装了Docker,就可以跳过这个步骤。macOS和Windows用户可以使用Docker Desktop。在Debian系统中,可以按照以下命令进行安装:

sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/debian \
  \((. /etc/os-release && amp; echo "\)VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

对于Ubuntu用户,需要将linux/debian替换为linux/ubuntu

配置Jaeger v2

基本运行方式

如果只是进行快速测试,且不需要持久化存储数据,可以按照以下命令运行:

docker run -d --name jaeger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/jaeger:2.17.0

端口16686用于访问用户界面,端口4317用于OTLP/gRPC数据传输,端口4318用于OTLP/HTTP通信。一旦删除该容器,所有的追踪数据就会丢失。

使用Badger实现持久化存储

在v2版本中,配置信息是从YAML文件中读取的,而不是从环境变量中获取的。请将以下配置保存到~/.local/share/jaeger/config.yaml文件中:

service:
  extensions: [jaeger_storage, jaeger_query, healthcheckv2]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger_storage_exporter]
extensions:
  healthcheckv2:
    use_v2: true
    http: { endpoint: 0.0.0.0:13133 }
  jaeger_query:
    storage: { traces: main_store }
  jaeger_storage:
    backends:
      main_store:
        badger:
          directories: { keys: /badger/key, values: /badger/data }
          ephemeral: false
          ttl: { spans: 720h }
receivers:
  otlp:
    protocols:
      grpc: { endpoint: 0.0.0.0:4317 }
      http: { endpoint: 0.0.0.0:4318 }
processors:
  batch:
exporters:
  jaeger_storage_exporter:
    trace_storage: main_store

Jaeger容器以UID 10001的身份运行。Docker创建的卷默认属于root用户。如果不先修改权限,容器就会因为出现“`mkdir /badger/key: permission denied`”这样的错误而陷入死循环。

请先创建相应的卷并调整其所有权:

docker volume create jaeger-data

docker run --rm \
  -v jaeger-data:/badger \
  alpine sh -c "mkdir -p /badger/data /badger/key &;& chown -R 10001:10001 /badger"

然后使用已挂载的配置文件来运行Jaeger:

docker run -d --name jaeger \
  --restart unless-stopped \
  -v ~/.local/share/jaeger/config.yaml:/etc/jaeger/config.yaml:ro \
  -v jaeger-data:/badger \
  -p 16686:16686 \
  -p 4317:4317 \
  -p 4318:4318 \
  jaegertracing/jaeger:2.17.0 \
  --config /etc/jaeger/config.yaml

通过运行“`docker restart jaeger`”来验证配置是否生效,确认之前记录的追踪数据仍然存在。访问“`http://localhost:16686`”即可查看用户界面。

配置Claude Forge Tracing

安装Claude Forge

您可以通过Claude Code插件市场来安装它:

/plugin marketplace add hatmanstack/claude-forge
/plugin install forge@claude-forge
/reload-plugins

安装完成后,系统会弹出一个TUI界面,让您确认相关设置。重新加载插件后,所有命令都会以“`forge:`”为前缀(例如“`/forge:pipeline`”)。

您也可以从GitHub克隆该仓库进行安装。

安装追踪钩子

在目标项目目录中运行安装脚本。对于通过插件进行安装的情况:

cd your-project
forge-trace                # 如果您在README文件中设置了别名,就可以使用这个命令;如果没有设置别名,则使用以下命令:
bash "$(find ~/.claude -path '*/forge*' -name install-tracing.sh 2>/dev/null | head -1)"

如果是通过克隆仓库进行安装,则运行:

cd your-project
bash /path/to/claude-forge/bin/install-tracing.sh

该脚本会在“`~/.local/share/claude-forge/venv`”目录下创建一个虚拟环境(优先使用“`uv`”工具,如果无法使用则使用“`python3 -m venv`”),然后安装OpenTelemetry相关包,将追踪钩子文件复制到目标位置,将其配置信息合并到“`.claude/settings.local.json`”文件中,并针对OTLP端点进行自我测试。

如果想跳过配置信息的合并过程,可以使用“`–no-settings`”选项;如果要彻底卸载该插件,可以使用“`–uninstall`”选项。

启用该功能

将以下命令添加到您的shell初始化脚本中,然后重新启动终端:

export CLAUDE_FORGE_TRACING=1

重新启动Claude Code插件,运行“`/pipeline`”命令,然后访问“`http://localhost:16686`”来查看“`claude-forge`”服务的运行状态。

理解Span模型

以下是典型群组运行过程中层级结构的示例:

session: "使用OAuth实现登录表单"        <- 根级Span
├── 子代理:规划器
│   ├── 工具:Write  (Phase-0.md)                  <- 变更操作相关Span(默认启用)
│   ├── 工具:Write  (Phase-1.md)
│   └── 子代理结果:规划器                   <- 执行时长、令牌数量、输出结果
├── 子代理:执行者
│   ├── 工具:Edit   (src/auth.ts)
│   ├── 工具:Bash   (npm test)
│   ├── 工具:Write  (src/oauth.ts)
│   └── 子代理结果:执行者
├── 子代理:审核者
│   └── 子代理结果:审核者
└── session_complete                              <- 整个会话的统计信息

根级Span的名称来源于提示信息的第一行。查找追踪记录时应根据用户请求的内容来定位,而不是通过UUID来识别。

子代理在开始执行时会生成一个“锚定Span”,在任务完成后会生成一个结果Span。结果Span会包含执行时长、令牌数量、输入内容以及输出结果。

三个细节层级

并非所有的内部工具调用都同样重要。“Write”、“Edit”、“MultiEdit”和“Bash”这类工具属于变更操作相关工具:虽然调用次数较少,但能提供关键信息,帮助用户了解实际发生了哪些变化;而“Read”、“Glob”、“Grep”和“WebFetch”这类工具主要用于导航操作:虽然调用次数较多,但大部分情况下提供的信息并不重要。

系统默认会记录所有变更操作相关操作。这种设置被证明是合理的。在之前的系统中,要么看不到子代理内部的任何操作细节,要么每次运行都会生成200多个Span记录。

>

模式 子代理 变更操作(Write/Edit/Bash) 其他内部工具
默认设置
CLAUDE_FORGE_TRACE_INNER=1 是(排除被屏蔽的工具)
CLAUDE_FORGE_TRACE_MUTATIONS=0 否(或按每个内部工具分别记录)

Span属性

session_complete阶段: session.tokens.input, sessiontokens.output, session.tokens.total, session.tokens.turns, session.duration_ms, user.prompt(前2KB内容)。

subagent_result阶段: agent.description, agent.prompt, agent.output, agent.duration_ms, agent.is_error, agent.tokens.input, agenttokens.output

tool:*阶段: tool.name, tool.input, tool.output, tool.duration_ms, tool.is_error

对多代理群组进行监控与分析

挂钩架构

Claude Code具备生命周期挂钩功能,能够在特定事件发生时自动执行脚本。在这里有四种重要的挂钩机制:

  1. UserPromptSubmit(用于创建根级跨度),

  2. PreToolUse(用于启动一个跨度),

  3. PostToolUse(用于完成该跨度并记录结果),

  4. Stop(用于结束整个追踪过程)。每个钩子都会通过标准输入接收JSON数据,并作为子进程运行。

使用OpenTelemetry发送跨度数据

以下是一段简单的Python代码,用于将跨度数据发送到Jaeger中:

from opentelemetry import trace
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": "my-agent-system"})
exporter = OTLPSpanExporter(endpoint="http://localhost:4317", insecure=True)
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("agent-tracer")

with tracer.start_as_current_span("my-agent-task") as span:
    span.set_attribute("agent.name", "planner")
    span.setattribute("agent.tokens.input", 1500)
    span.setAttribute("agenttokens.output", 800)

请访问localhost:16686,选择您的服务,然后点击“查找追踪记录”。

关联前后事件

您需要将每个PreToolUse操作与其对应的PostToolUse操作匹配起来。对于由代理程序执行的工具调用,其数据负载中并不包含tool_use_id字段,因此我使用了工具名称和输入参数来生成唯一的标识符。由于前后操作使用的tool_input参数是相同的,因此这些标识符能够准确对应起来。

import hashlib, json

def correlation_key/tool_name: str, tool_input: dict) -> str:
    content = json.dumps({"tool": tool_name, "input": tool_input}, sort_keys=True)
    return hashlib.sha1(content.encode()).hexdigest()[:16]

/tmp/claude-forge-tracing//\ ├── _root.json # 跟踪记录ID,根级跨度上下文信息 ├── _session_start_ns.json # 用于计算持续时间的时间戳 ├── subagent_.json # 每个子代理的跨度上下文信息 └── tool_.json # 每种工具的跨度上下文信息

文件名会被处理以防止路径遍历攻击。_safe_name()函数会删除文件名中所有非[A-Za-z0-9._-]字符的部分,最终保留一个SHA1哈希值作为文件名。

无阻塞地刷新数据

try:
    provider.force_flush(timeout_millis=1000)
except Exception:
    pass  # 确保不会阻塞整个系统

我首先尝试使用2000毫秒的配置,发现系统的运行速度较慢;而在使用100毫秒的配置时,冷启动的TLS连接会出现数据丢失的情况,而1000毫秒的配置则可以正常工作。即便Jaeger服务器出现故障,系统依然能够继续运行。

在Jaeger用户界面中查看追踪数据

打开http://localhost:16686,从服务下拉列表中选择claude-forge,然后点击“查找追踪记录”。

追踪记录的搜索功能可以根据操作名称、标签以及时间范围来进行筛选。由于会话记录的名称是根据用户输入的内容来确定的,因此输入“login form”就可以查找到所有与登录相关的操作记录。

时间线视图是我使用频率最高的界面。每个追踪记录都表示为一条水平条形图,这些条形图会根据父子关系进行嵌套显示。例如,我可以清楚地看到规划阶段耗时12秒、执行阶段耗时45秒、审核阶段耗时8秒。点击任意一条条形图,就可以查看相关的数据信息,如令牌数量、输入内容、输出结果以及错误状态等。

通过对比不同操作记录,可以很容易地了解为什么某个操作能够成功完成,而另一个操作则会失败。

实际运行中总结的经验教训

每个追踪团队只应生成一条追踪记录,而不是每个子代理都生成一条:在我最初的设计中,每次发生“停止”事件时,根节点的追踪记录都会被清除,因此每个子代理都会重新开始生成新的追踪记录。后来我将“停止”操作改为仅用于标记时间戳,同时保留原有的根节点追踪记录。

应使用描述性名称而非类型名称:所有子代理都将自己的类型报告为general-purpose,但实际上它们的具体功能是由描述字段来确定的。

为了准确分配令牌信息,需要为每个代理分别记录操作日志:Claude Code会将子代理的操作日志保存在~/.claude/projects/<project>/<session>>/subagents/agent-*.jsonl文件中,而这些日志可以通过agent-*.meta.json文件进行匹配。

应明确解析布尔类型的环境变量:在Python中,bool("0")表示True,因此在使用时需要特别注意这一点。也可以使用允许列表来指定有效的值,例如:{"1", "true", "yes", "on"}

环境变量参考

变量名称 作用
CLAUDE_FORGE_TRACING=1 此选项用于启用主代理的追踪功能。如果不设置此选项,相关钩子操作将不会执行任何操作。
CLAUDE_FORGE_TRACEMutation=0 禁用默认的变更记录功能(包括写入、编辑和Bash命令等操作)。默认值为启用状态。
CLAUDE_FORGE_TRACE_INNER=1 将所有内部工具调用都作为子追踪记录进行捕获。默认值为禁用状态。
CLAUDE_FORGE_TRACE_TOOL_BLOCKLIST 当启用内部追踪功能时,可以指定哪些工具应该被跳过。默认值为Read,Glob,Grep,TodoWrite,NotebookRead
CLAUDE_FORGEHOOK_DEBUG=1 启用对原始钩子数据的调试日志记录功能。默认值为禁用状态。
CLAUDE_FORGE_HOOK_DEBUG_LOG 允许自定义调试日志文件的保存路径。默认值为~/.cache/claude-forge/hook.log
OTEL_EXPORTER_OTLP_ENDPOINT OTLP/gRPC端点的地址。默认值为http://localhost:4317

总结

如果无法了解整个流程的运行情况,你在使用代币以及浪费时间方面就会效率低下。每次执行多智能体协作任务时都会产生实际成本;当某个智能体出现故障并需要重新尝试,或者当审核人员拒绝那些已经接近成功的成果时,你都在为这种盲目性付出代价。

通过追踪功能,你可以清晰地了解问题的根源——哪些地方存在故障,哪些智能体在无谓地消耗代币。如果使用更完善的规划工具,原本需要45秒的测试过程或许只需10秒就能完成。但如果不进行详细分析,你是永远无法知道这一点的。

尽早建立可观测性机制吧。Jaeger和OpenTelemetry使得这种功能的搭建成本非常低廉。一旦你能清楚地看到问题出在哪里,就可以及时加以解决。

Claude Forge的追踪功能目前位于主分支上。

Comments are closed.