找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4970

积分

0

好友

688

主题
发表于 2 小时前 | 查看: 1| 回复: 0

多 Agent 协作听起来是个很美的概念:让多个 AI 分工合作,效率翻倍。但一旦动手实践,你会发现坑多得让人崩溃:

  • 子 Agent 在干什么?父 Agent 要不要去“偷看”一眼?
  • 某个 Agent 任务还没完成,用户来问进度了,父 Agent 该怎么回答?
  • Agent 列表一有变动,整个 Prompt Cache 就失效,每次都要重新花钱计算?
  • 子 Agent 的工具权限该怎么管理?给多了危险,给少了又没法干活?

在 Claude Code 的 AgentTool 模块里,上述问题都被一套完整的工程化方案解决了。

为什么这套方案值得深挖?

因为它不只是一个简单的“调用 API 再返回结果”的工具,而是一个多 Agent 运行时:它内置了 Fork 和 Fresh 两种 Agent 模式、包含了防幻觉的 Prompt 设计、有针对 Prompt Cache 的专门优化、实现了细粒度的工具权限沙箱,甚至还支持后台异步执行与主线程竞速的调度逻辑。

架构图解

设计思路:两种 Agent,两种世界观

Claude Code 的 Agent Tool 本质上实现了一个轻量级 Agent 运行时。其核心设计思想是:协调者(Parent Agent)专注决策,执行者(Sub Agent)专注行动,两者通过严格定义的消息协议通信,互不干扰彼此的上下文

其中,Fork Agent 和 Fresh Agent 代表了两种截然不同的“哲学”:

  • Fork:我是你的“分身”,我继承你的所有记忆和上下文,去干那些你不想让自己的主要上下文被污染的活。
  • Fresh:我是个“新人”,你得给我讲清楚所有背景,我从零开始执行任务。

Claude Code Agent Tool 任务执行流程图

整个系统的设计建立在三大支柱之上:

  1. Prompt Cache 保护:Agent 列表可以动态变化,但通过 Attachment 消息机制将工具描述部分保持静态,从而避免 Cache 失效。
  2. 防幻觉协议:通过 “Don’t Peek”(别偷看)和 “Don’t Race”(别抢跑)的 Prompt 设计,从根源上杜绝父 Agent 编造或臆测子 Agent 的执行结果。
  3. 工具权限沙箱:每个子 Agent 都独立计算其可用的工具池,父 Agent 的权限不会自动“泄漏”给子 Agent,确保了安全边界。

源码亮点

亮点一:Cache 命中率优化,把动态内容“搬”出工具描述

位置src/tools/AgentTool/prompt.ts

源码

/**
 * Whether the agent list should be injected as an attachment message instead
 * of embedded in the tool description. When true, getPrompt() returns a static
 * description and attachments.ts emits an agent_listing_delta attachment.
 *
 * The dynamic agent list was ~10.2% of fleet cache_creation tokens: MCP async
 * connect, /reload-plugins, or permission-mode changes mutate the list →
 * description changes → full tool-schema cache bust.
 */
export function shouldInjectAgentListInMessages(): boolean {
  if (isEnvTruthy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES)) return true
  if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_AGENT_LIST_IN_MESSAGES)) return false
  return getFeatureValue_CACHED_MAY_BE_STALE('tengu_agent_list_attach', false)
}

// 在 getPrompt() 内部的关键逻辑:
const listViaAttachment = shouldInjectAgentListInMessages()

const agentListSection = listViaAttachment
  ? `Available agent types are listed in <system-reminder> messages in the conversation.`
  : `Available agent types and the tools they have access to:
${effectiveAgents.map(agent => formatAgentLine(agent)).join('\n')}`

解读

直觉上,我们很容易会把可用的 Agent 列表直接塞进工具描述(Prompt)里,这样模型就知道有哪些“帮手”可以调用了,简单直接。

但问题在于:每次 Agent 列表发生变动(例如加载了新的 MCP 插件、用户权限变更、执行了 /reload-plugins 命令),工具描述就变了,这会导致整个工具 Schema 的 Prompt Cache 彻底失效。 根据 Anthropic 的内部数据,这个动态的 Agent 列表占了整个 Fleet 约 10.2%cache_creation tokens,每次失效都意味着要重新花钱计算。

Claude Code 的解法非常巧妙:将 Agent 列表从静态的工具描述中剥离出来,通过单独的 system-reminder 附件消息动态注入到对话流中。这样一来,工具描述本身变成了一个永远不变的静态字符串,Cache 命中率得以大幅提升。动态内容走了另一条“通道”,不再污染工具描述这个影响 Cache Key 的“热路径”。

启示
在设计 LLM 应用的 Prompt 时,应有意识地区分静态稳定部分动态变化部分,并尝试将动态内容从会影响 Cache Key 的核心 Prompt 位置剥离出去。这对于任何需要精细控制 Token 成本和生产环境稳定性的应用来说,都是一个极具价值的工程模式。关于更多 AI Agent 的最佳实践,可以在 云栈社区 的人工智能板块找到深入讨论。

亮点二:Fork Agent 的 Cache 魔法,用“占位符”换“共享 Cache”

位置src/tools/AgentTool/forkSubagent.ts

源码

// Fork Agent 的关键:用一个占位符工具调用替换父 Agent 的实际调用
// 这让子 Agent 可以重用父 Agent 的 Prompt Cache
const forkMessages = parentMessages.map(msg => {
  if (msg.role === 'assistant') {
    return {
      ...msg,
      content: msg.content.map(block => {
        if (block.type === 'tool_use') {
          // Replace actual tool results with placeholder to preserve cache
          return {
            ...block,
            input: { command: FORK_PLACEHOLDER_TEXT },
          }
        }
        return block
      }),
    }
  }
  return msg
})

const FORK_PLACEHOLDER_TEXT = 'Fork started — processing in background'

解读

Fork Agent 需要继承父 Agent 的完整消息历史,这听起来很合理。但这里存在一个隐蔽的问题:父 Agent 的消息历史中可能包含具体的工具调用及其结果(例如某次 Bash 命令的详细输出),这些内容每次执行都可能不同。如果原样传递给子 Agent,就会导致子 Agent 无法命中父 Agent 已经建立好的 Prompt Cache

Claude Code 的解决方案是:在创建 Fork 子 Agent 时,遍历父 Agent 的消息历史,将其中的所有 tool_use 块的 input 字段,统一替换为同一个占位符文本 ‘Fork started — processing in background’。这样,无论父 Agent 之前实际执行过什么,Fork 出来的所有子 Agent 所“看到”的父级消息结构都是一模一样的,从而实现了 Prompt Cache 的共享。

值得注意的是,子 Agent 完成任务后,是通过 user-role 类型的消息来通知父 Agent,而不是 tool_result。这是因为 user-role 消息是对话的自然组成部分,模型能更好地理解“这是一个来自其他参与者的背景情况说明”。

启示
在多 Agent 系统中,上下文传递和缓存设计是两个需要独立、审慎考虑的问题。在传递上下文时,必须留意哪些内容会成为计算 Cache Key 的一部分。必要时,可以通过引入标准化“占位符”的方式来“稳定”消息结构,从而换取更高的缓存复用率。

亮点三:三层权限沙箱,精确控制工具访问

位置src/tools/AgentTool/index.ts

源码

// 子 Agent 的工具池计算逻辑(简化版)
function computeSubAgentTools(
  parentTools: Tool[],
  agentDef: AgentDefinition,
  permissionMode: PermissionMode,
): Tool[] {
  // Layer 1: 从父工具池出发
  let tools = parentTools

  // Layer 2: 应用 Agent 定义中的工具过滤
  if (agentDef.tools !== undefined) {
    tools = tools.filter(t => agentDef.tools!.includes(t.name))
  }

  // Layer 3: 根据权限模式决定 bubble vs sandbox
  if (permissionMode === 'bubble') {
    // Fork 模式:权限向上冒泡到父 Agent
    return tools
  } else {
    // Fresh 模式:独立权限沙箱,不继承父 Agent 权限
    return tools.map(t => withRestrictedPermissions(t))
  }
}

解读

权限管理堪称多 Agent 系统中最容易滋生安全漏洞的环节。Claude Code 采用了一个清晰的三层过滤模型来计算子 Agent 最终可用的工具列表:

  1. 从父工具池出发:这是第一道安全闸。子 Agent 能接触到的工具,首先不可能超出其父 Agent 自身已被授权的工具范围。
  2. Agent 定义过滤:每个 Agent 类型都可以在其定义中明确声明自己需要哪些工具(遵循最小权限原则)。系统会据此对父工具池进行筛选。
  3. 权限模式决定冒泡行为:这是最精妙的一层。
    • Fork 模式下,权限确认可以“冒泡”(bubble)到父 Agent。即,如果父 Agent 已经获得了执行某个工具的权限,那么其 Fork 出来的子 Agent 可以直接使用该工具,无需再次向用户申请权限。
    • Fresh 模式下,子 Agent 拥有独立的权限沙箱。即使父 Agent 有权限,Fresh 子 Agent 要使用工具时,仍需单独向用户申请权限(代码中的 withRestrictedPermissions 即表示此状态)。

这三层过滤的顺序至关重要:必须先基于父级权限和 Agent 定义缩小工具范围,最后再根据协作模式决定如何处理剩余工具的权限确认逻辑。如果顺序颠倒,可能导致最终的计算结果与预期不符。

启示
构建多 Agent 系统时,权限模型绝不应该是一个事后补丁。必须从设计伊始就明确:子 Agent 的权限来源是什么?是继承、过滤还是独立计算?“最小权限”原则在 Agent 系统中比在传统软件中更为重要,因为 AI Agent 的行为路径更难被完全预测,严格的权限沙箱是防止意外或恶意操作的关键防线。这种对安全性和工程细节的重视,正是优秀 源码分析 所追求的目标。

希望通过对 Claude Code 中 AgentTool 的这番剖析,能为你设计自己的多智能体协作系统带来一些切实可行的启发。如果你对这类工程实现细节感兴趣,欢迎在 云栈社区 继续交流探讨。




上一篇:神经网络逆向工程:破解Jane Street的MD5哈希算法谜题
下一篇:xia_tan:一款支持多漏洞自动化扫描的BurpSuite扩展,提升渗透测试效率
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-9 06:49 , Processed in 0.593036 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表