当你运行单个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具备生命周期挂钩功能,能够在特定事件发生时自动执行脚本。在这里有四种重要的挂钩机制:
-
UserPromptSubmit(用于创建根级跨度),
-
PreToolUse(用于启动一个跨度),
-
PostToolUse(用于完成该跨度并记录结果),
-
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的追踪功能目前位于主分支上。
文件名会被处理以防止路径遍历攻击。_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的追踪功能目前位于主分支上。