fix: 修复 Tool Search 缓存失效 — deferred 工具不再动态注入 tools 数组

移除 deferred 工具的 "discover then include" 逻辑,让 tools 数组在整个会话中
保持稳定(只有 core tools + ToolSearch + ExecuteExtraTool),避免每次发现新
工具时 tools JSON 变化导致 prompt cache 失效。

同时强化工具优先级引导:core tools 优先直接调用,ToolSearch/ExecuteExtraTool
仅作为发现和调用 deferred 工具的最后手段。当模型搜索已加载的 core tool 时,
ToolSearch 返回明确的拒绝提示。

Co-Authored-By: glm-5.1[1m] <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-09 14:56:22 +08:00
parent 8c157f0767
commit c14b7eadd2
7 changed files with 46 additions and 31 deletions

View File

@@ -184,7 +184,6 @@ import {
type ThinkingConfig,
} from 'src/utils/thinking.js'
import {
extractDiscoveredToolNames,
isDeferredToolsDeltaEnabled,
isToolSearchEnabled,
} from 'src/utils/toolSearch.js'
@@ -1186,28 +1185,24 @@ async function* queryModel(
useToolSearch = false
}
// Filter out ToolSearchTool if tool search is not enabled for this model
// ToolSearchTool returns tool_reference blocks which unsupported models can't handle
// Dynamic tool loading: filter deferred tools that haven't been discovered yet
let filteredTools: Tools
// canDefer is true when the model supports defer_loading.
// Deferred tools that haven't been discovered are filtered out from the API
// request — their schemas are only included after ToolSearch discovers them.
// With defer_loading, we only include discovered tools to save prompt tokens.
if (useToolSearch) {
// Dynamic tool loading: Only include deferred tools that have been discovered
// via tool_reference blocks in the message history. This eliminates the need
// to predeclare all deferred tools upfront and removes limits on tool quantity.
const discoveredToolNames = extractDiscoveredToolNames(messages)
// Never include deferred tools in the API tools array — they are invoked
// via ExecuteExtraTool which looks them up from the global tool registry
// at runtime. Keeping the tools array stable preserves the prompt cache
// across turns (discovered tools no longer bloat the tools JSON).
filteredTools = tools.filter(tool => {
// Always include non-deferred tools
// Always include non-deferred tools (core tools)
if (!deferredToolNames.has(tool.name)) return true
// Always include ToolSearchTool (so it can discover more tools)
if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
// Only include deferred tools that have been discovered
return discoveredToolNames.has(tool.name)
// All other deferred tools are excluded — use ExecuteExtraTool instead
return false
})
} else {
filteredTools = tools.filter(
@@ -1284,11 +1279,8 @@ async function* queryModel(
)
if (useToolSearch) {
const includedDeferredTools = count(filteredTools, t =>
deferredToolNames.has(t.name),
)
logForDebugging(
`Dynamic tool loading: ${includedDeferredTools}/${deferredToolNames.size} deferred tools included`,
`Dynamic tool loading: 0/${deferredToolNames.size} deferred tools in API tools array (all via ExecuteExtraTool)`,
)
}

View File

@@ -53,7 +53,6 @@ import {
import type { SDKAssistantMessageError } from '../../../entrypoints/agentSdkTypes.js'
import {
isToolSearchEnabled,
extractDiscoveredToolNames,
isDeferredToolsDeltaEnabled,
} from '../../../utils/toolSearch.js'
import {
@@ -213,17 +212,18 @@ export async function* queryModelOpenAI(
}
// 5. Filter tools (similar to Anthropic path)
// Never include deferred tools in the API tools array — they are invoked
// via ExecuteExtraTool which looks them up from the global tool registry
// at runtime. Keeping the tools array stable preserves the prompt cache.
let filteredTools = tools
if (useToolSearch && deferredToolNames.size > 0) {
const discoveredToolNames = extractDiscoveredToolNames(messages)
filteredTools = tools.filter(tool => {
// Always include non-deferred tools
if (!deferredToolNames.has(tool.name)) return true
// Always include ToolSearchTool (so it can discover more tools)
if (toolMatchesName(tool, TOOL_SEARCH_TOOL_NAME)) return true
// Only include deferred tools that have been discovered
return discoveredToolNames.has(tool.name)
// All other deferred tools are excluded — use ExecuteExtraTool instead
return false
})
}