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

  1. Verify the tool name exists at call time: print the registered names (config) and compare with req.getToolName() — look for prefix/truncation differences.
  2. Shorten MCP tool names or customize McpToolNamePrefixGenerator so prefixed names stay within the model/provider's length limits.
  3. Register the missing tool callback on the node that executes calls, keeping registration and execution config in sync.
  4. 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

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


AI-assisted analysis of alibaba/spring-ai-alibaba@f82da0b50f (2026-09-09). Data as JSON: /api/errors/0c7bf3982e7e7502. Report an issue: GitHub.