在 Claude、Claude Code 或自研 Agent 中,一次模型请求通常包含三部分:
await client.messages.create({
system,
tools,
messages,
});对应关系是:
system
全局行为规则、工具选择原则、安全约束
tools
当前暴露给模型的工具名称、描述和参数 Schema
messages
用户输入、项目上下文、对话历史、tool_use、tool_resultAnthropic API 收到 tools 后,会根据工具定义、工具配置以及开发者传入的 System Prompt,构造模型实际使用的特殊工具上下文。因此,工具定义虽然在 API 层是顶层 tools 字段,但在模型推理层属于高优先级的输入上下文,而不是普通的 user message。
这篇文章回答一个工程问题:Agent 运行中需要动态改变工具能力时,应该改 tools、tool_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_choice 和 tools 定义的缓存影响不同。
修改工具定义
例如:
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 Prompt | System 之后的缓存需要重建 |
tool_choice | Tools 和 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 同样支持按工具配置 enabled 和 defer_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。