qwen-code Workflow 级 Trace Span 缺口分析:用双 ALS 父级解析构建 Agent 执行轨迹树
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文以 qwen-code 的设计文档《Workflow 级 Span 粒度不足分析》为主线,剖析一个 AI coding agent 在 OpenTelemetry 接入中"有 tracing 主干、却没有 workflow 阶段边界"的典型困境:审批等待、hook、subagent 等阶段如何编码成 trace 树中的独立节点。读完本文,你将掌握基于 AsyncLocalStorage(ALS)的 span 父子挂载模型、五种 workflow span 缺口及其修复方案,并能对照当前仓库源码验证这些建议的实际落地方式。
1. 背景:从"有 tracing 主干"到"编码 workflow 阶段边界"
该分析文档基于 2026-05-13 对 qwen-code origin/main 的复核。当时项目已具备 tracing 基础设施,各组件分布如下:
| 组件 | 位置 | 说明 |
|---|---|---|
| Span 类型定义 | session-tracing.ts | interaction、llm_request、tool、tool.execution |
| Tracer 工具 | tracer.ts | session root context、withSpan、startSpanWithContext |
| 交互入口 | client.ts | 顶层交互显式启动interactionspan |
| 生命周期管理 | — | AsyncLocalStorage + WeakRef + TTL cleanup |
当时的 runtime 中稳定接入的主要是两类 generic span:
api.generateContent/api.generateContentStreamtool.<toolName>
文档的核心结论是:已进入"有 tracing 主干"阶段,但尚未把 agent workflow 的阶段边界完整编码进 trace 树。作为对照,文档引用了外部项目 claude-code 在src/utils/telemetry/sessionTracing.ts中已实现的六类 span:interaction、llm_request、tool、tool.blocked_on_user、tool.execution、hook(此引用来自原文档的外部对比,非本仓库文件)。
2. 五大缺口:workflow 阶段在哪里"隐形"
| 缺失 span / 机制 | 影响 |
|---|---|
permission_wait/blocked_on_userspan | 无法区分审批等待 vs 工具执行耗时 |
hookspan | hook 耗时被折叠进 tool span,定位边界不清 |
subagentroot span | subagent 内部 llm/tool 调用无法形成 trace 子树 |
tool.execution真实接线 | helper 已定义但主链路未调用 |
| 稳定的 parent-child wiring | spans 多为 session root 下的 sibling 而非层级树 |
2.1 用户审批等待不在 trace 中
工具调用等待审批时,状态迁移路径为awaiting_approval→scheduled→ 执行。"等待用户确认"只是状态迁移,不是 trace 节点,trace 上看不到审批等待耗时;工具慢时无法区分是"卡在等用户"还是"工具本身执行慢"。
2.2 Hook 有事件记录但没有独立 span
Pre/Post hook 执行后产出HookCallEvent,走logHookCall()记录日志,但不建立独立 OTel span。后果是:hook 变慢时表现为外层 tool span 变慢;hook 失败时表现为"tool 失败";trace 无法回答"时间花在 hook 还是 tool.execution 上"。
2.3 Subagent 是 log/metric 而非 trace subtree
subagent 启动/完成时记录SubagentExecutionEvent(事件名定义见 constants.ts)并进入 log/metric,但没有形成显式 span 子树。能统计"哪个 subagent 跑过",但不能顺着 trace 看"这个 subagent 触发了哪些 llm/tool 调用";并发 subagent 场景下因果链不清。
2.4 tool.execution helper 已定义但未接入主链路
复核时session-tracing.ts中已有startToolExecutionSpan()/endToolExecutionSpan(),但非测试代码中未见调用点。当时的实际 trace 树与理想 trace 树对比如下:
实际:
session-root interaction api.generateContent tool.Bash subagent_execution (log/metric) hook_call (event/QwenLogger)理想:
interaction llm_request tool tool.blocked_on_user hook(pre) tool.execution hook(post) subagent interaction llm_request tool2.5 Parent-child wiring 不够稳定
interaction span 已存在,但很多运行中的 spans 挂在 session root 下作为 sibling,而不是 interaction 的子节点。调用树偏平、节点间因果关系不直观,从一个用户轮次追到内部 llm/tool/hook/subagent 的体验不连续。在 Jaeger / Tempo / ARMS 等后端上,这样的树比层级清晰的实现更难读。
3. 根因剖析:两套断裂的 span 创建路径
这是文档指出的当前最关键的架构问题:
| 层 | 文件 | 用法 | parent 解析 |
|---|---|---|---|
| session-tracing 层 | session-tracing.ts | startInteractionSpan/startLLMRequestSpan/startToolSpan/startToolExecutionSpan | 显式从interactionContextALS 取 parent |
| tracer 层 | tracer.ts | withSpan/startSpanWithContext | 从context.active()取 parent,fallback 到 session root |
runtime 实际调用情况(复核时点):
startInteractionSpan→已接入(client.ts),写入interactionContextALS;startLLMRequestSpan/endLLMRequestSpan→未接入,runtime 用的是withSpan('api.generateContent', ...)(在loggingContentGenerator.ts);startToolSpan/endToolSpan→未接入,runtime 用的是withSpan('tool.${name}', ...)(在coreToolScheduler.ts);startToolExecutionSpan/endToolExecutionSpan→未接入。
从源码看,withSpan的父级解析函数getParentContext()只返回context.active()(tracer.ts),它完全不读取interactionContextALS;找不到活跃 span 时回退到 session root context。因此 interaction span 与 LLM/tool spans 变成了 session root 下的平级 sibling,而不是 parent-child 树:
session-root ├── interaction (来自 session-tracing, 写入了 interactionContext ALS) ├── api.generateContent (来自 withSpan, 不读 interactionContext → 挂到 session root) ├── tool.Bash (来自 withSpan, 同上) └── tool.Read (来自 withSpan, 同上)而参照实现 claude-code 中只有一套 span 创建路径(sessionTracing.ts),所有 span 都走同一套 ALS → OTel context 转换逻辑,所以树是完整的。
4. 参照模型:claude-code 的双 ALS span 管理
文档对 claude-code 源码做了深度对比,其 tracing 架构可概括为:
interactionContext (ALS) toolContext (ALS) │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ │ interaction span │ │ tool span │ │ (session root) │ │ (child of intxn) │ └─────────────────────┘ └─────────────────────┘ ▲ parent of ▲ parent of │ │ ┌───────┴───────┐ ┌──────────┼──────────┐ │ │ │ │ │ llm_request tool blocked execution hook _on_user核心机制:
| 机制 | 实现 |
|---|---|
| 双 ALS | interactionContext存当前 interaction span;toolContext存当前 tool span |
| parent 解析 | 每种 span 类型硬编码从哪个 ALS 取 parent:llm_request/tool取interactionContext;blocked_on_user/execution/hook取toolContext;hook有 fallback 到interactionContext |
| 生命周期 | enterWith 注入 → span 运行 → enterWith(undefined) 清除 |
| 查找 span | 非 ALS 存储的 span(如 blocked_on_user)通过activeSpansMap 按span.type反查 |
| 内存管理 | ALS 持有的 span 用 WeakRef;非 ALS 持有的 span 用 strongRef 防 GC;TTL 30min 自动清理 |
tool span 完整生命周期(toolExecution.ts):
startToolSpan(name, attrs) // → toolContext.enterWith(spanCtx) startToolBlockedOnUserSpan() // → parent = toolContext.getStore() [permission resolution / user prompt] endToolBlockedOnUserSpan(decision, source) startToolExecutionSpan() // → parent = toolContext.getStore() [tool.call()] endToolExecutionSpan({ success }) endToolSpan(result) // → toolContext.enterWith(undefined)hook span(hooks.ts):
startHookSpan(event, name, count, defs) // → parent = toolContext ?? interactionContext [parallel hook execution] endHookSpan(span, { success, blocking, ... })5. 逐项复用方案
5.1 双 ALS + 显式 parent 解析:核心修复
| 维度 | claude-code | qwen-code(复核时) |
|---|---|---|
| ALS 数量 | 2(interactionContext+toolContext) | 1(interactionContext,无toolContext) |
| parent 解析 | 每种 span 类型显式指定从哪个 ALS 取 parent | withSpan统一走context.active() |
| context 注入 | trace.setSpan(otelContext.active(), parentCtx.span) | withSpan内部由startActiveSpan隐式注入 |
qwen-code 的session-tracing.ts当时已经实现了与 claude-code几乎相同的 parent 解析模式:
// qwen-code session-tracing.ts (已有但未用) export function startLLMRequestSpan(model, promptId): Span { const parentCtx = interactionContext.getStore(); const ctx = parentCtx ? trace.setSpan(otelContext.active(), parentCtx.span) : otelContext.active(); // ... }核心修复路径:废弃 runtime 中的withSpan('api.*')/withSpan('tool.*')调用,改为调用 session-tracing 的 typed helpers。不需要重写 session-tracing 层——它的 API 已经就绪。需要新增的只有:增加toolContextALS(仿 claude-code);增加blocked_on_user和hookspan 类型及 helper 函数。
5.2 tool.blocked_on_user:适配审批流差异
| 维度 | claude-code | qwen-code |
|---|---|---|
| 审批位置 | 在toolExecution.ts内,tool span 内部 | 在coreToolScheduler._schedule()内,tool span 之前 |
| 审批模式 | 同步等待resolveHookPermissionDecision() | 状态机驱动:validating→awaiting_approval→scheduled→executing |
| span 覆盖范围 | tool span 包含 blocked + execution | tool span(withSpan)只包含 execution(从executeSingleToolCall开始) |
关键差异:qwen-code 的executeSingleToolCall入口检查toolCall.status !== 'scheduled'才继续——调用到这里时审批已经完成,tool span 的withSpan包不住审批等待。文档给出两种适配方案:
方案 A — 前移 tool span 起点(推荐):将startToolSpan调用从executeSingleToolCall移到_schedule中审批检查之前,使 tool span 覆盖完整生命周期。在进入awaiting_approval状态时startToolBlockedOnUserSpan,在审批完成(scheduled)时endToolBlockedOnUserSpan:
_schedule(): startToolSpan(name) // ← 新增 startToolBlockedOnUserSpan() // ← 新增,进入 awaiting_approval 时 [状态机等待] endToolBlockedOnUserSpan(decision) // ← 新增,进入 scheduled 时 executeSingleToolCall(): startToolExecutionSpan() // ← 接入已有 helper [hook + execute] endToolExecutionSpan() endToolSpan() // ← 需要在 finally 中方案 B — 保持 tool span 位置不变,单独追踪审批:在_schedule中独立创建approval_waitspan(不作为 tool 的 child),挂到 interaction 下。好处是改动更小,坏处是与参照模型不一致、trace 树可读性差。
建议采用方案 A,原因:与参照实现的 trace 树结构一致;trace 上一个 tool 节点就能看到"等了多久 + 执行了多久";状态机驱动的特性只影响 span start/end 的触发时机,不影响 parent-child 建模。
5.3 hook span:可直接复用
| 维度 | claude-code | qwen-code |
|---|---|---|
| hook 执行入口 | executeHooks()inhooks.ts | firePreToolUseHook/firePostToolUseHookviahookEventHandler.ts |
| 现有记录方式 | OTel span + Perfetto span | HookCallEvent→QwenLogger(无 OTel) |
| parent | toolContext ?? interactionContext | — |
复用方案:在session-tracing.ts新增startHookSpan/endHookSpan(parent =toolContext ?? interactionContext);在coreToolScheduler.ts的executeSingleToolCall中 pre/post hook 调用前后分别 start/end hook span;保留现有logHookCall事件记录(两套并行,不互斥)。改动量低,不影响现有 hook 逻辑。
5.4 tool.execution:已有 helper,只需接线
startToolExecutionSpan()/endToolExecutionSpan()已经完整实现,只需在executeSingleToolCall中调用:
// coreToolScheduler.ts executeSingleToolCall 内部 const toolSpan = startToolSpan(toolName, attrs); // ... hook pre ... const execSpan = startToolExecutionSpan(toolSpan); try { // ... invocation.execute() ... endToolExecutionSpan(execSpan, { success: true }); } catch (e) { endToolExecutionSpan(execSpan, { success: false, error: e.message }); } // ... hook post ... endToolSpan(toolSpan);风格差异说明:qwen-code 的startToolExecutionSpan原设计接收显式parentToolSpan参数,而参照实现从toolContextALS 隐式获取。引入toolContextALS 后可统一为隐式获取。
5.5 subagent trace tree:不建议直接复用
| 维度 | claude-code | qwen-code |
|---|---|---|
| OTel trace 传播 | 无— subagent 的 interaction 是新 root | 无— subagent 无显式 trace 传播 |
| 身份关联 | Perfetto metadata(agent process/thread)+teammateContextStorageALS | subagentNameContextALS +SubagentExecutionEvent |
| 并发隔离 | OTel ALS 有泄漏风险(enterWith是进程级,并发 subagent 会互覆盖) | 同样的风险 |
claude-code 在 subagent OTel tracing 上自己也没解决好:interactionContext.enterWith()是进程级的,并发 subagent 会覆盖彼此的 ALS 值;真正的 agent 层级树只存在于 Perfetto(一个 feature-flagged 的内部系统),不在 OTel 中。因此建议:短期沿用现有的subagentNameContext+ 事件日志方案;中期在 subagent 启动时创建一个subagentspan(parent = 当前 toolContext),并用context.with()而非enterWith()来隔离并发 subagent 的 OTel context。这是需要独立设计的工作项,不建议直接照搬。
5.6 LLM request span:路径明确
复核时点在loggingContentGenerator.ts中用withSpan('api.generateContent', ...)和startSpanWithContext('api.generateContentStream', ...),改为调用startLLMRequestSpan/endLLMRequestSpan(session-tracing 层已有实现)即可。streaming 场景需注意:startLLMRequestSpan返回Span对象,需要手动传入endLLMRequestSpan(span, metadata)终结——这与startSpanWithContext的手动管理模式兼容。
5.7 复用总结与实施顺序
| 改造项 | 可复用程度 | 改动量 | 优先级 |
|---|---|---|---|
统一 span 创建路径(废弃 runtimewithSpan,用 session-tracing helpers) | 核心修复— 解决 parent-child 断裂 | 中(约 5 个调用点) | P0 |
新增toolContextALS | 直接照搬参照模式 | 低(session-tracing.ts 内部) | P0 |
| tool.blocked_on_user span | 方案 A 需适配状态机 | 中(_schedule+executeSingleToolCall协调) | P1 |
| tool.execution 接线 | helper 已有,只需调用 | 低(executeSingleToolCall内 3 行) | P1 |
| hook span | 新增 helper + 调用点 | 低 | P1 |
| LLM request span 切换 | 替换 withSpan 为 typed helper | 低(2 个调用点) | P1 |
| subagent trace tree | 不建议直接复用— 需独立设计 | 高 | P2 |
Phase 1 — 修复 trace 树结构 (P0) ├── 1a. session-tracing.ts 新增 toolContext ALS + blocked_on_user / hook span helpers ├── 1b. loggingContentGenerator.ts: withSpan → startLLMRequestSpan/endLLMRequestSpan └── 1c. coreToolScheduler.ts: withSpan → startToolSpan/endToolSpan Phase 2 — 补齐 workflow span (P1) ├── 2a. coreToolScheduler._schedule: blocked_on_user span 接入 ├── 2b. coreToolScheduler.executeSingleToolCall: tool.execution span 接入 └── 2c. hook pre/post 调用处: hook span 接入 Phase 3 — Subagent trace tree (P2) ├── 3a. 设计 context.with() 隔离方案(替代 enterWith) ├── 3b. subagent 启动时创建 subagent root span └── 3c. 并发 subagent 场景验证6. 当前仓库源码验证:缺口如何被逐一修复
设计文档是 2026-05 的快照,而当前仓库源码显示上述修复项已基本落地(源码注释中引用了 issue #3731 的 Phase 2/3 等推进标记)。本节以当前源码为证据,说明每项建议的实际实现形态。
6.1 Span 词汇表:七类 workflow span 全部成为一等公民
constants.ts 定义了完整的 span 名常量:
| 常量 | span 名 | 语义 |
|---|---|---|
SPAN_INTERACTION | qwen-code.interaction | 一次用户轮次(trace root) |
SPAN_LLM_REQUEST | qwen-code.llm_request | 单次 LLM 请求 |
SPAN_TOOL | qwen-code.tool | 工具调用完整生命周期(含审批等待) |
SPAN_TOOL_EXECUTION | qwen-code.tool.execution | 工具实际执行阶段 |
SPAN_TOOL_BLOCKED_ON_USER | qwen-code.tool.blocked_on_user | awaiting_approval等待用户的时间 |
SPAN_HOOK | qwen-code.hook | 单个 hook 触发点 |
SPAN_SUBAGENT | qwen-code.subagent | 单次 subagent 调用 |
同时 constants.ts 维护了tool.failure_kind词汇表(cancelled、pre_hook_blocked、invocation_guard_denied、timeout、plan_mode_blocked等),注释明确要求"写入点与文档不能漂移"——即 span 语义在 coreToolScheduler、session-tracing 与文档三方共享同一常量源。
6.2 双 ALS(实为三 ALS)与 parent 优先级链
session-tracing.ts 中现有三个 AsyncLocalStorage:
const interactionContext = new AsyncLocalStorage<SpanContext | undefined>(); const toolContext = new AsyncLocalStorage<SpanContext | undefined>(); // 注释:子 span 创建时优先读取 subagentContext, // 否则前台 subagent 的子 span 会被 re-parent 回外层 interaction const subagentContext = new AsyncLocalStorage<SpanContext | undefined>();startLLMRequestSpanWithContext与startToolSpan的 parent 解析采用统一优先级链(session-tracing.ts):
const parentCtx = subagentContext.getStore() ?? toolContext.getStore() ?? interactionParentCtx; const ctx = resolveGenAiParentContext(parentCtx);这正是文档 5.1 节"双 ALS + 显式 parent 解析"建议的实现,并额外增加了subagentContext一层,解决了"subagent 内部 LLM span 逃逸回外层 interaction"的问题。值得注意的是resolveGenAiParentContext的防御逻辑(session-tracing.ts):当没有任何 ALS 属主时强制返回ROOT_CONTEXT,防止错误 prompt 的 span 被错误地挂到活跃 interaction 之下。
6.3 方案 A 落地:tool span 前移到 validating 阶段
文档推荐的方案 A 在 coreToolScheduler.ts 中按注释原文实现:
// Open the tool span as soon as the call is validated. This covers // validating → awaiting_approval → executing in one span (#3731 // Phase 2). Every cancel/error path below — and the existing // success path in executeSingleToolCall — must call // finalizeToolSpan(callId, ...) to avoid leaking spans. const toolSpan = startToolSpan(canonicalName, { 'tool.call_id': reqInfo.callId, ... }, ...);即 tool span 从validating状态就打开,一个 span 覆盖validating → awaiting_approval → executing完整生命周期;span 句柄存入this.toolSpansMap 以便跨状态机阶段终结。审批等待阶段则显式挂 blocked_on_user 子 span(coreToolScheduler.ts):
this.setStatusInternal(callId, 'awaiting_approval', confirmationDetails); // blocked_on_user span as a child of the tool span const blockedSpan = startToolBlockedOnUserSpan(toolSpan, { tool_name: canonicalName, call_id: callId, });startToolBlockedOnUserSpan的父级通过显式toolSpan参数解析(session-tracing.ts),注释明确说明原因:该 span 启动于工具主体进入runInToolSpanContext之前,此时toolContext.getStore()为空;同时显式传 span 对象也规避了参照实现中"按 type 反查最后一个 span"在并发下的竞态问题。endToolBlockedOnUserSpan记录decision(proceed_once/proceed_always/cancel/aborted/auto_approved/error,见 session-tracing.ts)与source(cli/ide/hook/auto/system)两个规范属性,且 span 状态保持 UNSET——"等用户"既非 OK 也非 ERROR,决策属性才是规范信号。
6.4 tool.execution 与 hook span 接线
tool.execution:coreToolScheduler.ts 与 #L5305 两处startToolExecutionSpan({ toolName, callId })对应文档 5.4 节"只需 3 行接线"的预测。helper 内部从toolContext.getStore()取父级(session-tracing.ts),在runInToolSpanContext外调用时会打 warning 并回退到活跃 OTel span。- hook span:coreToolScheduler.ts 中
startHookSpan(opts)与endHookSpan(hookSpan, endMeta)成对出现。当前HookEvent类型比文档复核时更丰富,覆盖PreToolUse/PostToolUse/PostToolUseFailure/PostToolBatch(session-tracing.ts);startHookSpan的 parent 优先级为toolContext → subagentContext → interactionContext(session-tracing.ts),在 subagent 内部、tool 之外触发的 hook 也能正确挂到 subagent 下。 - 并发安全:
runInToolSpanContext(session-tracing.ts)刻意用toolContext.run()+otelContext.with()而非enterWith(),把上下文作用域限定在单个异步调用树内——这直接回应了文档 5.5 节对"进程级enterWith在并发下互相覆盖"的担忧。
6.5 LLM request span 切换到 typed helper
loggingContentGenerator.ts 与非流式/流式路径(#L563)均已改为startLLMRequestSpanWithContext/endLLMRequestSpan,测试文件 loggingContentGenerator.test.ts 对 token 计数、缓存命中、重试上下文(attempt/requestSetupMs/retryTotalDelayMs)、流空闲超时等终结路径均有断言。endLLMRequestSpan除写入gen_ai.usage.*、ttft_ms、finish_reason等属性外,还派生sampling_ms与output_tokens_per_second,并按 Phase 4c 记录分阶段直方图(session-tracing.ts)。
6.6 Subagent trace tree:Phase 3 的独立设计
文档 5.5 节建议"独立设计、用context.with()隔离并发",当前实现(源码注释标记#3731 Phase 3)给出了具体答案:
startSubagentSpan(session-tracing.ts)区分foreground/fork/background三种调用形态:foreground作为当前活跃 span(通常 AGENT tool span)的子节点继承 traceId;fork/background则创建linked-root span——root: true强制新 traceId,同时用 OTelLink指向发起方 span,注释引用了 OTel 规范对"长耗时异步操作"使用 Link 的建议,理由正是 fire-and-forget subagent 运行数分钟到数小时,若挂在父 trace 下会超出多个后端的 trace 容量上限。runInSubagentSpanContext(session-tracing.ts)是并发隔离的关键:它用subagentContext.run()+toolContext.run(undefined, ...)+otelContext.with()组合包裹 subagent 主体,注释明确说明会主动清空toolContext——否则 subagent 主体内、首个内部 tool 调用之前触发的 hook(如 SubagentStart)会错误地挂到外层 AGENT tool span 上。全程没有任何enterWith,与文档"用context.with()替代enterWith()"的建议一致。- 记忆管理上,
fork/background这类可能运行数小时的调用获得 4 小时长 TTL(LONG_TTL_SUBAGENT_KINDS,session-tracing.ts),其余 span 默认 30 分钟(为tool.blocked_on_user的用户思考时间选取)。注释坦陈一个已知限制:长 TTL 只作用于 subagent span 本身,其内部子 span 仍用 30 分钟默认值,长时间后台 agent 的 trace 可能出现前段子 span 被清扫的"空洞",留作后续工作项。
6.7 内存与生命周期管理
文档"现状"表中提到的 "AsyncLocalStorage + WeakRef + TTL cleanup" 在当前源码中完整可见:activeSpans(WeakRef 表)与strongSpans(防 GC 的强引用表)双表管理(session-tracing.ts);sweepStaleSpans每 60 秒巡检一次,对被 TTL 清扫的 span 打上qwen-code.span.ttl_expired哨兵属性并按类型补规范属性(如 blocked_on_user 补decision: 'aborted'、subagent 补terminate_reason: 'ttl_swept'),使后端能把"被安全网回收"与"主动结束但未设状态"区分开(session-tracing.ts)。所有 span 的文本属性经truncateSpanError截断(默认 1024 字符、防孤立代理项、剥离 ANSI、脱敏 URL 凭据,session-tracing.ts),避免超大字段导致后端丢弃整个 span。
6.8 修复后的 trace 树形态
从当前源码结构看,文档"理想 trace 树"已达成并略有演化:每个 interaction 现在是独立的 trace root(startInteractionSpan显式传入ROOT_CONTEXT,session-tracing.ts;旧的 session root 机制已标记@deprecated,见 tracer.ts),跨轮次关联改由session.idspan 属性完成。这样单条 trace 保持有界、可在 ARMS / Jaeger 中正常渲染:
qwen-code.interaction (trace root) qwen-code.llm_request qwen-code.tool (Bash) qwen-code.tool.blocked_on_user (decision, source, duration_ms) qwen-code.hook (PreToolUse) qwen-code.tool.execution qwen-code.hook (PostToolUse) qwen-code.tool (AGENT) qwen-code.subagent (foreground: 子节点 / fork|background: linked root) qwen-code.llm_request qwen-code.tool qwen-code.tool.execution与文档理想的差异在于:subagent 下不再嵌套一层新的interaction,而是 subagent span 直接承载内部的 llm_request / tool / hook 子树——这与startSubagentSpan的设计注释("Hosts the LLM/tool/hook subtree emitted by the subagent")一致,语义上等价且更简洁。
7. 小结:这份设计文档的价值
《Workflow 级 Span 粒度不足分析》展示了一种可复用的排障方法论:
- 先盘点再补缺口——用"组件 × 位置 × 说明"表格固化现状基线,再列出"缺失项 × 影响"对照表,让每个缺口都有可感知的排障代价(如"无法区分审批等待 vs 工具执行耗时");
- 定位到架构级根因——问题不在个别 helper 缺失,而在"两套断裂的 span 创建路径"导致 parent 解析分叉;
- 对照外部实现做逐项复用评估——对每项机制标注"可复用程度 / 改动量 / 优先级",并诚实标注"双方都不完整、不建议照搬"的部分(subagent tree),给出独立设计路线;
- 修复方案可对照源码验收——从当前仓库看,typed helper 统一创建路径、
toolContextALS、方案 A 的前移 tool span、hook span 接线、context.with()化的 subagent 隔离均已落地,文档中的 P0/P1/P2 路线与源码注释中的 Phase 标记一一对应。
对于任何构建 agent 式产品的团队,这套"span 词汇表 + 显式 parent 解析 + 按阶段建模"的思路都能直接借鉴:trace 的价值不在于 span 数量,而在于能否回答"这轮慢在等用户、hook,还是 tool 真执行"这类排障问题。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考