alibaba/spring-ai-alibaba · warning
LLM may have adapted the tool name '{}', especially if the n
Error message
LLM may have adapted the tool name '{}', especially if the name was truncated due to length limits. If this is the case, you can customize the prefixing and processing logic using McpToolNamePrefixGenerator What it means
When handling a model tool call, AgentToolNode.executeToolCallWithInterceptors resolves the requested tool name against configured callbacks. If no callback matches, it logs the POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING — the LLM may have altered, truncated, or hallucinated the tool name (common with long names that get prefixed, e.g. by MCP's McpToolNamePrefixGenerator) — and returns an error ToolCallResponse ('Tool not available: <name>') back to the model instead of throwing.
Source
Thrown at spring-ai-alibaba-agent-framework/src/main/java/com/alibaba/cloud/ai/graph/agent/node/AgentToolNode.java:546
* @return the tool call response
*/
private ToolCallResponse executeToolCallWithInterceptors(AssistantMessage.ToolCall toolCall, OverAllState state,
RunnableConfig config, Map<String, Object> extraStateFromToolCall, boolean inParallelExecution,
Map<Integer, DefaultCancellationToken> cancellationTokens, int toolIndex) {
// Create ToolCallRequest
ToolCallRequest request = ToolCallRequest.builder()
.toolCall(toolCall)
.context(config.metadata().orElse(new HashMap<>()))
.executionContext(new ToolCallExecutionContext(config, state))
.build();
// Create base handler that actually executes the tool
ToolCallHandler baseHandler = req -> {
ToolCallback toolCallback = resolve(req.getToolName(), config);
if (toolCallback == null) {
logger.warn(POSSIBLE_LLM_TOOL_NAME_CHANGE_WARNING, req.getToolName());
return ToolCallResponse.builder()
.content("Tool not available: " + req.getToolName())
.toolName(req.getToolName())
.toolCallId(req.getToolCallId())
.status("error")
.metadata(Map.of("error", true, "unresolvedToolName", req.getToolName()))
.build();
}
if (enableActingLog) {
logger.info("[ThreadId {}] Agent {} acting, executing tool {}.",
config.threadId().orElse(THREAD_ID_DEFAULT), agentName, req.getToolName());
}
Map<String, Object> toolContextMap = new HashMap<>(toolContext);
toolContextMap.putAll(req.getContext());
// Handle tools that need state injection:View on GitHub (pinned to f82da0b50f)
Solutions
- Verify the tool name exists at call time: print the registered names (config) and compare with req.getToolName() — look for prefix/truncation differences.
- Shorten MCP tool names or customize McpToolNamePrefixGenerator so prefixed names stay within the model/provider's length limits.
- Register the missing tool callback on the node that executes calls, keeping registration and execution config in sync.
- Optionally add fuzzy name resolution (case-insensitive/starts-with match) in your resolve path for robustness.
Example fix
// before
ToolCallback resolve(String name, Config c) { return byExactName(c, name); }
// after
ToolCallback resolve(String name, Config c) {
ToolCallback t = byExactName(c, name);
if (t == null) t = byCaseInsensitiveOrPrefix(c, name); // handle LLM name drift
return t;
} Defensive patterns
Strategy: validation
Validate before calling
static void verifyRegistered(Collection<String> requested, Collection<String> registered) {
List<String> missing = requested.stream().filter(n -> !registered.contains(n)).toList();
if (!missing.isEmpty()) throw new IllegalStateException("Unregistered tools: " + missing);
} Type guard
static ToolCallback resolveTolerant(String name, List<ToolCallback> tools) {
return tools.stream().filter(t -> t.getToolDefinition().name().equals(name)).findFirst()
.or(() -> tools.stream().filter(t -> t.getToolDefinition().name().equalsIgnoreCase(name)).findFirst())
.or(() -> tools.stream().filter(t -> t.getToolDefinition().name().endsWith(name)).findFirst())
.orElse(null);
} Prevention
- Keep tool registration and execution config in sync (same node/source)
- Keep MCP tool names short; customize McpToolNamePrefixGenerator when prefixes cause truncation
- Log registered tool names at startup and diff against model-declared tools
- Treat the returned 'error' ToolCallResponse as a signal to re-list tools for the model
When it happens
Trigger: executeToolCallWithInterceptors' base handler calls resolve(req.getToolName(), config), which returns null because the model emitted a tool name not registered on the node — due to prefix mismatch, name truncation, case differences, or the tool never being registered.
Common situations: 1) MCP tools whose names exceed length limits and get truncated/prefixed inconsistently between registration and call. 2) Registering tools on one AgentLlmNode but not wiring them into AgentToolNode's config. 3) LLM hallucinating a similar tool name under a large tool set. 4) Renaming a tool without restarting/rebuilding the agent so the model's cached description uses the old name.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- MCP call fail: {e.getMessage()}
- CreateMCPServerError
- MCPServerNotFound
- UpdateMCPServerError
- DeleteMCPServerError
AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09).
Data as JSON: /api/errors/0c7bf3982e7e7502.
Report an issue: GitHub.