AI

Agent 动态工具管理:tool_choice、Runtime 权限与 Prompt Cache

tool_choice 不是权限系统。Agent 工具管理的推荐分层是:核心工具保持稳定,选择方式用 tool_choice,权限变化交给 Runtime,大量低频工具用 Tool Search。

在 Claude、Claude Code 或自研 Agent 中,一次模型请求通常包含三部分:

await client.messages.create({
  system,
  tools,
  messages,
});

对应关系是:

system
  全局行为规则、工具选择原则、安全约束

tools
  当前暴露给模型的工具名称、描述和参数 Schema

messages
  用户输入、项目上下文、对话历史、tool_use、tool_result

Anthropic API 收到 tools 后,会根据工具定义、工具配置以及开发者传入的 System Prompt,构造模型实际使用的特殊工具上下文。因此,工具定义虽然在 API 层是顶层 tools 字段,但在模型推理层属于高优先级的输入上下文,而不是普通的 user message。

这篇文章回答一个工程问题:Agent 运行中需要动态改变工具能力时,应该改 toolstool_choice 还是 Runtime 权限?以及这些选择如何影响 Prompt Cache。

一、tool_choice 是什么

tool_choice 用来控制:

模型在本轮请求中是否必须使用工具,以及必须使用哪个工具。

它不负责定义工具,也不负责执行权限校验。

tools
  决定模型“有哪些工具可以选择”

tool_choice
  决定模型“这一轮应该如何选择工具”

Runtime Permission
  决定某次调用“最终是否允许执行”

1. auto

tool_choice: {
  type: 'auto',
}

含义:

模型可以调用工具
也可以直接输出自然语言
由模型自行判断

当请求提供了 tools,但没有明确设置 tool_choice 时,默认通常是 auto

适合普通 Agent Loop:

const response = await client.messages.create({
  model,
  system,
  tools,
  tool_choice: { type: 'auto' },
  messages,
});

例如用户说:

读取 package.json,看看项目使用了什么框架。

模型可以选择:

tool_use: Read(...)

用户只是问:

什么是 React Fiber?

模型也可以不调用工具,直接回答。

2. any

tool_choice: {
  type: 'any',
}

含义:

这一轮必须调用至少一个工具
但具体调用哪个工具由模型选择

例如:

tools: {
  searchDocs,
  searchCode,
  readFile,
},

tool_choice: {
  type: 'any',
}

此时模型不能仅输出自然语言,必须在提供的工具中选择至少一个。

适合:

  • 必须检索后才能回答;
  • 必须经过外部数据验证;
  • 必须生成结构化工具调用;
  • 不允许模型仅凭已有知识作答。

需要注意,使用 any 强制工具调用时,API 会以特殊方式引导 assistant 直接产生 tool_use,模型通常不会先输出一段自然语言说明。

3. tool

tool_choice: {
  type: 'tool',
  name: 'readFile',
}

含义:

这一轮必须调用指定工具
不能改用其他工具
也不能直接输出普通答案

示例:

const response = await client.messages.create({
  model,
  tools: {
    readFile,
    grep,
    bash,
  },

  tool_choice: {
    type: 'tool',
    name: 'readFile',
  },

  messages: [
    {
      role: 'user',
      content: '读取 package.json',
    },
  ],
});

模型被要求产生类似:

{
  type: 'tool_use',
  name: 'readFile',
  input: {
    file_path: 'package.json',
  },
}

适合:

  • 强制进行某个确定步骤;
  • 使用工具代替 Structured Output;
  • 固定工作流中的特定阶段;
  • 强制模型把参数整理成某个 Schema;
  • 需要确保某个外部系统被调用。

例如让模型通过一个 submitResult 工具返回结构化结果:

tools: {
  submitResult: tool({
    inputSchema: z.object({
      category: z.string(),
      confidence: z.number(),
      reason: z.string(),
    }),
  }),
},

tool_choice: {
  type: 'tool',
  name: 'submitResult',
},

这里工具甚至不一定真的访问外部系统,它也可以仅作为结构化输出通道。

4. none

tool_choice: {
  type: 'none',
}

含义:

即使请求里提供了 tools
本轮也禁止模型调用任何工具

适合:

  • 只需要总结现有上下文;
  • 当前阶段只允许推理;
  • 工作流中的纯文本生成阶段;
  • 临时禁止所有工具;
  • 避免模型在已经拥有足够信息时继续调用工具。

例如:

const response = await client.messages.create({
  model,
  system,
  tools: allTools,

  tool_choice: {
    type: 'none',
  },

  messages,
});

工具定义仍然存在,但本轮不能产生新的工具调用。

二、tool_choice 不等于工具权限

这是设计中最重要的区分。

假设当前注册了:

tools: {
  readFile,
  editFile,
  bash,
}

以下三件事完全不同。

1. 工具是否对模型可见

tools 决定:

tools: {
  readFile,
  bash,
}

这里没有 editFile,因此模型这一轮看不到 editFile

2. 模型是否必须调用工具

tool_choice 决定:

tool_choice: {
  type: 'any',
}

这只表示必须调用某个工具,并不意味着所有工具都安全,也不意味着调用一定被执行。

3. 工具是否允许真正执行

由 Agent Runtime 的权限层决定:

async function executeToolCall(call: ToolCall, context: AgentContext) {
  const decision = await checkPermission(call, context);

  if (decision.behavior === 'deny') {
    return {
      success: false,
      error: decision.reason,
    };
  }

  if (decision.behavior === 'ask') {
    return requestUserApproval(call);
  }

  return toolExecutors[call.toolName](call.input);
}

正确架构是:

模型提出调用

Runtime 校验工具名和参数

权限检查

用户确认(可选)

真正执行

返回 tool_result

不能因为 System Prompt 写着:

不要执行危险操作。

就认为危险工具已经安全。

Prompt 是软约束,Runtime 才是硬约束。

三、tool_choice 与动态 enable / disable 的区别

场景一:本轮禁止所有工具

使用:

tool_choice: {
  type: 'none',
}

不需要删除 tools

场景二:本轮必须使用某个工具

使用:

tool_choice: {
  type: 'tool',
  name: 'searchDocs',
}

不需要改变工具定义。

场景三:某个工具临时不允许执行

不建议每轮都从 tools 中删除它。

更适合在 Runtime 权限层处理:

if (
  context.mode === 'readOnly' &&
  ['editFile', 'writeFile'].includes(call.toolName)
) {
  return {
    success: false,
    error: '当前处于只读模式,不能修改文件。',
  };
}

这样可以维持稳定的工具定义和 Prompt Cache。

场景四:模型根本不应该知道工具存在

这时才从 tools 中删除:

const tools =
  user.role === 'admin'
    ? adminTools
    : publicTools;

适合:

  • 不同租户严格隔离;
  • 不同 Agent 角色拥有完全不同能力;
  • 工具名称本身可能暴露敏感能力;
  • 某个工具在整段会话中都不应该出现;
  • 服务端根本没有连接对应系统。

四、Prompt Cache 的工作方式

Anthropic Prompt Cache 本质上是前缀缓存。

可以把模型输入的逻辑顺序理解为:

tools

system

messages

工具定义、System Prompt 和历史消息并不是三个完全独立的缓存文件,而是同一个连续输入前缀。

例如:

请求 1:

[Read][Grep][System][Message A]

下一轮:

请求 2:

[Read][Grep][System][Message A][Message B]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       可以复用缓存

但如果修改工具:

请求 3:

[Read][Grep][Edit][System][Message A][Message B]

开头的输入已经改变,旧前缀不能直接复用。

原因不是 Anthropic 刻意限制动态工具,而是 Transformer 后面 token 的中间状态依赖前面的所有 token:

System 的模型状态
=
f(Tools, System)

Messages 的模型状态
=
f(Tools, System, Messages)

当 Tools 变化时,即使 System Prompt 文本完全相同,它的模型内部状态也不再相同。

因此:

修改 tool definitions

工具前缀发生变化

旧的后续前缀不能继续复用

Anthropic 的缓存文档要求被缓存的前缀在多次调用间保持一致,同时 tool_choice、thinking 配置等请求参数也应保持一致。

五、tool_choice 对缓存的影响

tool_choicetools 定义的缓存影响不同。

修改工具定义

例如:

tools:
  Read
  Grep
+ Edit

通常意味着工具上下文发生变化,可能导致整个旧前缀不能命中。

变化包括:

新增工具
删除工具
修改工具名称
修改 description
修改 input_schema
修改 strict
修改工具顺序
修改其他会进入模型上下文的工具配置

只修改 tool_choice

例如:

- tool_choice: { type: 'auto' }
+ tool_choice: { type: 'none' }

Anthropic 官方当前说明是:

工具定义缓存仍可复用
System Prompt 缓存仍可复用
Messages 缓存块需要重新处理

也就是说,tool_choice 变化不会像修改工具定义那样破坏最前面的 tools 和 system 缓存,但会使消息层缓存失效。

可以简化为:

变化内容主要缓存影响
工具名称、描述或 Schema工具前缀变化,后续旧前缀通常无法复用
System PromptSystem 之后的缓存需要重建
tool_choiceTools 和 System 可保留,Messages 需重新处理
只追加新消息旧消息前缀可以继续命中
修改较早历史消息从修改位置之后重新处理

因此,临时禁止所有工具时:

tool_choice: { type: 'none' }

通常比删除整个 tools 数组更利于保留前部缓存。

六、为什么 Agent 不应该频繁增删核心工具

Agent 运行过程中确实经常发生模式变化:

分析模式
只读模式
执行模式
用户尚未授权
用户已经授权
进入数据库阶段
进入代码修改阶段

但“模式变化”并不一定意味着工具定义必须变化。

例如核心工具始终注册:

const CORE_TOOLS = {
  readFile,
  glob,
  grep,
  editFile,
  bash,
};

只读模式:

const permissionPolicy = {
  readFile: 'allow',
  glob: 'allow',
  grep: 'allow',
  editFile: 'deny',
  bash: 'restricted',
};

执行模式:

const permissionPolicy = {
  readFile: 'allow',
  glob: 'allow',
  grep: 'allow',
  editFile: 'ask',
  bash: 'ask',
};

模型看到的工具集合不变,但执行权限发生变化:

稳定工具定义

更高的缓存命中率

动态权限策略

运行时安全控制

这比每轮都这样更稳定:

// 第一轮
tools: {
  readFile,
  grep,
}

// 第二轮
tools: {
  readFile,
  grep,
  editFile,
}

// 第三轮
tools: {
  readFile,
  grep,
  editFile,
  bash,
}

七、推荐的动态工具管理方案

方案总览

核心高频工具
    → 始终注册

工具调用倾向
    → tool_choice

临时权限变化
    → Runtime Permission

大量低频工具
    → Tool Search / defer_loading

完全不同的能力边界
    → 不同稳定 Tool Profile

第一层:稳定核心工具

始终注册高频工具:

const CORE_TOOLS = {
  readFile,
  glob,
  grep,
  editFile,
  bash,
} satisfies ToolSet;

要求:

工具名称固定
工具顺序固定
description 固定
inputSchema 固定
不要在每轮动态生成 description

例如避免:

description: `当前时间是 ${new Date().toISOString()},执行 Bash 命令`

因为这样每轮工具定义都不同。

改成稳定描述:

description: 'Execute a shell command in the current workspace.'

动态状态放在 conversation 或 Runtime Context 中。

第二层:使用 tool_choice 控制本轮选择方式

普通 Agent Loop:

tool_choice: {
  type: 'auto',
}

强制检索:

tool_choice: {
  type: 'any',
}

强制固定工具:

tool_choice: {
  type: 'tool',
  name: 'searchKnowledgeBase',
}

禁止所有工具:

tool_choice: {
  type: 'none',
}

不要把 tool_choice 当作权限系统:

tool_choice 控制模型输出
Runtime 控制真实执行

第三层:Runtime 权限层

type PermissionBehavior = 'allow' | 'ask' | 'deny';

interface PermissionDecision {
  behavior: PermissionBehavior;
  reason?: string;
  updatedInput?: unknown;
}

async function checkPermission(
  call: ToolCall,
  context: AgentContext,
): Promise<PermissionDecision> {
  if (
    context.mode === 'readOnly' &&
    ['editFile', 'writeFile'].includes(call.toolName)
  ) {
    return {
      behavior: 'deny',
      reason: '当前处于只读模式。',
    };
  }

  if (call.toolName === 'bash') {
    return {
      behavior: 'ask',
      reason: 'Shell 命令可能产生副作用。',
    };
  }

  return {
    behavior: 'allow',
  };
}

执行入口:

async function executeToolSafely(
  call: ToolCall,
  context: AgentContext,
): Promise<unknown> {
  const parsedInput = validateToolInput(call);

  const decision = await checkPermission(
    {
      ...call,
      input: parsedInput,
    },
    context,
  );

  if (decision.behavior === 'deny') {
    return {
      success: false,
      error: decision.reason ?? 'Permission denied',
    };
  }

  if (decision.behavior === 'ask') {
    const approved = await requestApproval(call, decision.reason);

    if (!approved) {
      return {
        success: false,
        error: 'User rejected this tool call.',
      };
    }
  }

  return executeTool(
    call.toolName,
    decision.updatedInput ?? parsedInput,
  );
}

第四层:大量低频工具按需加载

当工具数量达到几十或几百个时,不建议全部固定塞入初始 tools

问题包括:

工具定义占用大量 token
模型选择工具更困难
相似工具容易混淆
首次请求成本增加
Prompt Cache 写入成本上升

Anthropic 提供 defer_loading 配合 Tool Search:延迟工具的完整描述不会初始发送给模型,而是需要时再通过工具搜索加载。MCP toolset 同样支持按工具配置 enableddefer_loading

架构:

初始稳定工具
├── Read
├── Grep
├── Bash
└── ToolSearch

    搜索匹配工具

    加载完整定义

    调用目标工具

适合:

MCP 工具
GitHub / Jira / Slack
企业内部 API
数据库工具
设计平台工具
大量插件

第五层:稳定 Tool Profile

当不同模式确实需要不同工具可见性时,不要每轮任意组合,建议定义有限的稳定 Profile。

const TOOL_PROFILES = {
  readonly: {
    readFile,
    glob,
    grep,
  },

  coding: {
    readFile,
    glob,
    grep,
    editFile,
    bash,
  },

  research: {
    readFile,
    webSearch,
    webFetch,
  },
} satisfies Record<string, ToolSet>;

这样缓存模式从无限组合变成有限集合:

readonly profile
    → 一套稳定缓存

coding profile
    → 一套稳定缓存

research profile
    → 一套稳定缓存

切换 Profile 时会产生新的缓存前缀,但以后再次使用相同 Profile,仍有机会复用对应缓存。

八、推荐的 Agent Loop

async function runAgent(context: AgentContext): Promise<string> {
  const tools = resolveStableToolProfile(context);
  const messages = [...context.messages];

  while (true) {
    const toolChoice = resolveToolChoice(context);

    const response = await client.messages.create({
      model: context.model,
      max_tokens: 4096,

      system: buildStableSystemPrompt(context),

      tools: Object.values(tools),

      tool_choice: toolChoice,

      messages,
    });

    messages.push({
      role: 'assistant',
      content: response.content,
    });

    const toolCalls = response.content.filter(
      block => block.type === 'tool_use',
    );

    if (toolCalls.length === 0) {
      return collectText(response.content);
    }

    const toolResults = await Promise.all(
      toolCalls.map(async call => {
        try {
          const result = await executeToolSafely(call, context);

          return {
            type: 'tool_result' as const,
            tool_use_id: call.id,
            content: JSON.stringify(result),
          };
        } catch (error) {
          return {
            type: 'tool_result' as const,
            tool_use_id: call.id,
            is_error: true,
            content:
              error instanceof Error
                ? error.message
                : 'Unknown tool execution error',
          };
        }
      }),
    );

    messages.push({
      role: 'user',
      content: toolResults,
    });
  }
}

九、工具变化的决策表

需求推荐做法
普通情况下允许模型自行决定tool_choice: auto
本轮必须调用某个工具tool_choice: tool
本轮必须调用任意工具tool_choice: any
本轮禁止所有工具tool_choice: none
某个工具临时无权限Runtime 拒绝执行
用户授权后允许写入修改 Runtime 权限,不一定修改 tools
模型完全不应知道某工具从 tools 移除
有几种固定工作模式使用稳定 Tool Profile
MCP 工具非常多Tool Search + defer_loading
工具 Schema 改版更新 tools,接受缓存重建
只想影响工具选择倾向优先用 Prompt 或 tool_choice
涉及安全和副作用必须由 Runtime 强制控制

十、最终分层模型

┌────────────────────────────────────────┐
│ System Prompt                          │
│                                        │
│ 身份、行为规则、工具偏好、安全原则     │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ Tool Definitions                       │
│                                        │
│ 名称、description、input_schema        │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ tool_choice                            │
│                                        │
│ auto / any / tool / none               │
│ 控制本轮模型如何选择工具               │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ Model Output                           │
│                                        │
│ 普通文本或 tool_use                    │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ Runtime Permission                     │
│                                        │
│ 参数校验、allow / ask / deny           │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ Tool Executor                          │
│                                        │
│ 真正读取、修改、执行、访问外部系统     │
└────────────────────────────────────────┘

┌────────────────────────────────────────┐
│ Conversation                           │
│                                        │
│ tool_use + tool_result 进入 messages   │
└────────────────────────────────────────┘

十一、结论

tool_choice 不是工具注册表,也不是权限系统,而是:

当前这一轮,模型是否必须使用工具,以及必须使用哪个工具的选择策略。

完整的工程设计应当是:

tools
    定义模型能看到哪些工具

tool_choice
    控制模型本轮如何选择工具

System Prompt
    提供工具使用原则和行为偏好

Runtime Permission
    决定工具调用是否真正允许

Tool Executor
    负责执行工具

messages
    保存 tool_use 和 tool_result 历史

对于动态工具和缓存,推荐原则是:

核心工具保持稳定
临时禁用由 Runtime 控制
本轮禁用全部工具使用 tool_choice: none
工具选择方式使用 tool_choice
不同能力边界使用稳定 Tool Profile
大量低频工具使用 Tool Search
只有需要对模型隐藏工具时才真正修改 tools

一句话概括:

能力变化改 tools,选择方式改 tool_choice,权限变化改 Runtime,使用策略改 System Prompt。

Back to Blog

Related Posts

View All Posts »