AI Function Calling in Production: Complete Guide from Concept to Deployment

后端开发AI agents in production

AI Function Calling in Production: Complete Guide for 2026

Function Calling has become infrastructure-level capability for LLM applications. But there's a huge gap between "running a demo" and "running in production." This guide covers the full chain from protocol understanding to production deployment.

The essence of Function Calling is not "letting AI call functions" — it's making LLMs output structured JSON instead of free text. Whether the function actually executes is entirely up to your code.


The Real Nature: Structured Output, Not Execution

User Query → LLM Reasoning → Output JSON (function name + args) → Your code decides → Return result to LLM → Final answer

Key insight: The LLM never directly executes any function. It only "suggests" which function to call and what arguments to pass. Your application code is the real executor.

This means:

  1. Safety: You can refuse dangerous operations
  2. Control: You can modify/validate arguments before execution
  3. Observability: Every call has complete audit logs
  4. Testability: LLM output is pure JSON, easy to assert

Protocol Comparison: OpenAI / Anthropic / Google

OpenAI Format

{
    "tools": [{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"},
                    "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
                },
                "required": ["city"]
            }
        }
    }]
}

Key Differences

Dimension OpenAI Anthropic Google Gemini
Schema field parameters input_schema parameters
Type format JSON Schema JSON Schema Gemini Schema
Parallel calls
Force call tool_choice: "required" tool_choice: {type, name} function_calling_config

Spring AI + Function Calling in Practice

Auto Function Registration

@Configuration
public class ToolConfig {

    @Bean
    @Description("Get current weather for a city including temperature and humidity")
    public Function<WeatherRequest, WeatherResponse> getWeather() {
        return request -> weatherService.getCurrentWeather(request.city(), request.unit());
    }

    @Bean
    @Description("Query user's recent orders")
    public Function<OrderQueryRequest, OrderQueryResponse> queryOrders() {
        return request -> orderService.queryRecentOrders(request.userId(), request.limit());
    }
}

Spring AI magic: Define a Function<Req, Resp> bean with @Description, and the framework auto-registers it as an LLM tool, generates JSON Schema, and handles the call loop.

Chat Service

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("You are a smart customer service assistant.")
            .defaultFunctions("getWeather", "queryOrders", "createTicket")
            .build();
    }

    public String chat(String userMessage) {
        return chatClient.prompt()
            .user(userMessage)
            .call()
            .content();
    }
}

LangChain4j Tool Annotation-Driven Development

@Tool("Search knowledge base for relevant documents")
public List<DocumentResult> searchKnowledge(
    @P("Search query") String query,
    @P("Number of results, default 5") int topK
) {
    return vectorStore.similaritySearch(query, topK);
}

@Tool("Execute read-only SQL query")
public QueryResult executeQuery(@P("SQL query, SELECT only") String sql) {
    if (!sql.trim().toUpperCase().startsWith("SELECT")) {
        throw new IllegalArgumentException("Only SELECT queries allowed");
    }
    return databaseService.executeQuery(sql);
}

Parallel Tool Calls: Scheduling Strategies

Strategy Use Case Pros Cons
Sequential Strict dependencies Simple, reliable Slow
Fully Parallel No dependencies Fastest May violate business rules
DAG Scheduling Mixed dependencies Performance + correctness Complex implementation
Rate-Limited Parallel API rate limits Avoids throttling Needs config
public List<ToolResult> executeParallel(List<ToolCall> calls) {
    var dag = buildDependencyGraph(calls);
    List<ToolResult> results = new ArrayList<>();
    for (List<ToolCall> layer : dag.getLayers()) {
        List<CompletableFuture<ToolResult>> futures = layer.stream()
            .map(call -> CompletableFuture.supplyAsync(() -> executeWithSafety(call)))
            .toList();
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        futures.forEach(f -> results.add(f.join()));
    }
    return results;
}

Error Handling & Retry: Graceful Degradation

public class ResilientToolExecutor {

    public ToolResult executeWithResilience(ToolCall call, String userId) {
        // Layer 1: Circuit breaker check
        if (circuitBreakers.isOpen(call.name())) {
            return ToolResult.fallback(call.name(), "Service temporarily unavailable");
        }

        // Layer 2: Parameter validation
        ValidationResult validation = registry.validate(call.name(), call.arguments());
        if (!validation.isValid()) {
            return ToolResult.error(call.name(), "Validation failed: " + validation.getErrors());
        }

        // Layer 3: Permission check
        if (!registry.hasPermission(call.name(), userId)) {
            return ToolResult.error(call.name(), "Permission denied");
        }

        // Layer 4: Retry execution
        try {
            return retryPolicy.execute(() -> registry.execute(call.name(), call.arguments(), userId));
        } catch (RetryExhaustedException e) {
            circuitBreakers.recordFailure(call.name());
            return ToolResult.fallback(call.name(), getFallbackMessage(call.name()));
        }
    }
}

Security: Permission Control & Sandbox Execution

Permission Model

ToolPermissionManager.builder()
    .role("USER", Set.of("getWeather", "queryOrders", "searchKnowledge"))
    .role("VIP", Set.of("getWeather", "queryOrders", "searchKnowledge", "createTicket", "processRefund"))
    .role("ADMIN", Set.of("*"))
    .dangerousOperations(Set.of("processRefund", "deleteAccount"))
    .build();

Input Sanitization

public class ToolInputSanitizer {
    public SanitizedArgs sanitize(String toolName, Map<String, Object> args) {
        // Prevent SQL injection, path traversal, command injection
        // Enforce length limits
        // Validate types
    }
}

Production Case: Multi-Turn Customer Service Orchestration

User: "My order from yesterday hasn't shipped. I want a refund if it doesn't ship today."

Turn 1: LLM → queryOrders(userId, "yesterday")
Turn 2: LLM analyzes status → processRefund(orderId)
Turn 3: LLM → createTicket(userId, "Refund request", ...)
Turn 4: LLM → Generate final response
@Service
public class SmartCustomerService {

    public CustomerServiceResponse handle(String userMessage, String userId, String sessionId) {
        var response = chatClient.prompt()
            .system(buildSystemPrompt(userId))
            .messages(conversationStore.getHistory(sessionId))
            .user(userMessage)
            .functions("queryOrders", "processRefund", "createTicket", "sendNotification")
            .advisors(
                new LoggingAdvisor(),
                new RateLimitAdvisor(),
                new SafetyCheckAdvisor(),
                new MetricsAdvisor()
            )
            .call();

        conversationStore.append(sessionId, userMessage, response.content());
        return new CustomerServiceResponse(response.content(), extractToolUsage(response));
    }
}

Debugging: Function Call Tracing

Call Chain Visualization

┌──────────────────────────────────────────────────┐
│ Session: sess_abc123  User: user_456             │
├──────────────────────────────────────────────────┤
│ Turn 1: User → "Order not shipped, want refund"  │
│   ├─ LLM → queryOrders(userId, days=1)          │
│   │   └─ Result: [Order#1234 Not Shipped]  120ms│
│   ├─ LLM → processRefund(orderId="1234")        │
│   │   └─ Result: Refund success  850ms          │
│   └─ LLM → "Refund processed for order 1234..." │
│                                                  │
│ Total: 2 tool calls, 970ms tool time             │
│ Tokens: 450 input + 280 output = 730 total      │
└──────────────────────────────────────────────────┘

Production Checklist

Item Priority Description
Precise descriptions ⭐⭐⭐ LLM relies entirely on descriptions
Parameter validation ⭐⭐⭐ Never trust LLM output
Permission control ⭐⭐⭐ Role-based function access
Circuit breaker ⭐⭐ Graceful degradation
Turn limit ⭐⭐ Prevent infinite loops (5-10 turns)
Timeout control ⭐⭐ Per-function timeout
Log tracing ⭐⭐ Full traceId chain
Cost monitoring Token and API call costs

Most important principle: Function Calling is a "suggestion" not an "instruction." Always validate, authorize, and degrade before execution — never trust LLM output.

Try these browser-local tools — no sign-up required →

#Function Calling#AI Agent#LLM#工具调用#Spring AI