TungDaDev's Blog

Spring AI: Dynamic Tool Calling

Spring ai dynamic tool calling.webp
Published on
/9 mins read/

Khi xây dựng Enterprise AI Agent với hàng chục hoặc hàng trăm công cụ nghiệp vụ, việc nạp tĩnh toàn bộ JSON Schema vào mỗi lượt gọi LLM sẽ làm bùng nổ chi phí token, kéo dài độ trễ và gia tăng tỷ lệ ảo giác chọn nhầm công cụ. Giải pháp nằm ở kiến trúc Dynamic Tool Calling kết hợp Vector Store và Advisor Pattern trong Spring AI.

# bài toán quá tải Context Window và kinh tế học Token

Trong các hệ sinh thái doanh nghiệp kết nối nhiều hệ thống nghiệp vụ (ERP, CRM, Core Banking, Kho hàng, MCP Servers), số lượng hàm công cụ (Tools / Function Calling) có thể dễ dàng chạm mốc từ 50 đến hơn 200 tools:

# công thức chi phí token overhead

Mỗi định nghĩa công cụ gửi tới LLM bao gồm: tên hàm, chuỗi mô tả ngữ nghĩa (description), các thuộc tính tham số (properties), kiểu dữ liệu (types), và danh sách trường bắt buộc (required). Trung bình một schema công cụ tiêu tốn T_schema ≈ 200 - 350 tokens.

Nếu hệ thống có M = 100 công cụ và nhận 500,000 lượt yêu cầu trò chuyện mỗi tháng:

Token dư thừa mỗi request = M × T_schema = 100 × 250 = 25,000 tokens

Tổng token lãng phí hàng tháng = 500,000 × 25,000 = 12.5 Tỷ tokens

Với mức giá mô hình flagship trung bình 2.5 / 1M input tokens, doanh nghiệp phải trả **31,250 mỗi tháng** chỉ để mô tả các công cụ mà phần lớn không bao giờ được dùng đến trong request đó.

# ba hệ quả kỹ thuật nghiêm trọng

  1. Lãng phí chi phí vận hành: Hơn 90% chi phí đầu vào là để truyền tải các schema không liên quan.
  2. Kéo dài Time-to-First-Token (TTFT): LLM phải tính toán self-attention trên một ma trận ngữ cảnh hàng chục nghìn tokens trước khi sinh ra token đầu tiên, làm tăng độ trễ mạng thêm 2 đến 4 giây.
  3. Hiện tượng ảo giác công cụ (Tool Hallucination & Confusion): Khi có nhiều công cụ có chữ ký tương đồng (ví dụ: cancelBooking, cancelOrder, cancelTicket), việc xuất hiện cùng lúc trong prompt khiến mô hình dễ kích hoạt sai công cụ hoặc điền nhầm tham số.

# kiến trúc Dynamic Tool Calling với Spring AI Advisor

Để giải quyết bài toán trên, chúng ta áp dụng mô hình RAG cho Tool Schemas. Thay vì đưa toàn bộ công cụ vào ChatClient, hệ thống chỉ truy vấn và nạp động Top-K (K = 3 - 5) công cụ có độ tương đồng ngữ nghĩa cao nhất với câu hỏi của người dùng.

Trong Spring AI 1.x, cơ chế sạch nhất để can thiệp vào vòng đời của request mà không làm xáo trộn mã nguồn nghiệp vụ là xây dựng một CallAroundAdvisor.


# hiện thực hóa mã nguồn kỹ thuật

# 1. tự động lập chỉ mục các Bean có gắn @Tool

Khi ứng dụng khởi động, hệ thống tự động quét toàn bộ Spring Beans, bóc tách metadata và nạp vào VectorStore:

package com.tungdadev.agent.tools;
 
import org.springframework.ai.document.Document;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
 
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
 
@Component
public class DynamicToolRegistry {
 
    private final ApplicationContext applicationContext;
    private final VectorStore vectorStore;
    private final Map<String, ToolCallback> callbackMap = new ConcurrentHashMap<>();
 
    public DynamicToolRegistry(ApplicationContext applicationContext, VectorStore vectorStore) {
        this.applicationContext = applicationContext;
        this.vectorStore = vectorStore;
    }
 
    @EventListener(ApplicationReadyEvent.class)
    public void indexAllTools() {
        // Tìm toàn bộ beans có chứa phương thức gắn annotation @Tool
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        List<Document> toolDocuments = new ArrayList<>();
 
        for (String beanName : beanNames) {
            Object bean = applicationContext.getBean(beanName);
            ToolCallback[] callbacks = ToolCallbacks.from(bean);
 
            for (ToolCallback callback : callbacks) {
                String toolName = callback.getToolDefinition().name();
                String description = callback.getToolDefinition().description();
 
                callbackMap.put(toolName, callback);
 
                // Lưu nội dung mô tả vào Vector Store để phục vụ tìm kiếm ngữ nghĩa
                Document doc = new Document(
                    description,
                    Map.of(
                        "toolName", toolName,
                        "type", "agent_tool"
                    )
                );
                toolDocuments.add(doc);
            }
        }
 
        if (!toolDocuments.isEmpty()) {
            vectorStore.add(toolDocuments);
        }
    }
 
    public ToolCallback getCallback(String toolName) {
        return callbackMap.get(toolName);
    }
}

# 2. thiết kế CallAroundAdvisor nạp công cụ động

Advisor này đóng vai trò chặn bắt (interceptor) trước khi request tới LLM, thực hiện tìm kiếm tương đồng và tiêm các ToolCallback tương ứng vào cấu hình yêu cầu:

package com.tungdadev.agent.advisor;
 
import com.tungdadev.agent.tools.DynamicToolRegistry;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.document.Document;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.Ordered;
 
import java.util.*;
 
public class DynamicToolSelectionAdvisor implements CallAroundAdvisor {
 
    private final VectorStore vectorStore;
    private final DynamicToolRegistry toolRegistry;
    private final int topK;
    private final double similarityThreshold;
    private final List<ToolCallback> alwaysOnTools;
 
    public DynamicToolSelectionAdvisor(VectorStore vectorStore,
                                       DynamicToolRegistry toolRegistry,
                                       int topK,
                                       double similarityThreshold,
                                       List<ToolCallback> alwaysOnTools) {
        this.vectorStore = vectorStore;
        this.toolRegistry = toolRegistry;
        this.topK = topK;
        this.similarityThreshold = similarityThreshold;
        this.alwaysOnTools = alwaysOnTools != null ? alwaysOnTools : List.of();
    }
 
    @Override
    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
        String userPrompt = advisedRequest.userText();
 
        // 1. Tìm kiếm ngữ nghĩa trong Vector Store
        SearchRequest searchRequest = SearchRequest.query(userPrompt)
                .withTopK(topK)
                .withSimilarityThreshold(similarityThreshold);
 
        List<Document> matchedDocs = vectorStore.similaritySearch(searchRequest);
 
        // 2. Chuyển đổi tên tool thành danh sách ToolCallback thực thi
        Set<ToolCallback> selectedTools = new HashSet<>(alwaysOnTools);
        for (Document doc : matchedDocs) {
            String toolName = (String) doc.getMetadata().get("toolName");
            ToolCallback callback = toolRegistry.getCallback(toolName);
            if (callback != null) {
                selectedTools.add(callback);
            }
        }
 
        // 3. Tái cấu trúc AdvisedRequest với danh sách tools đã được chọn lọc
        AdvisedRequest modifiedRequest = AdvisedRequest.from(advisedRequest)
                .toolCallbacks(selectedTools.toArray(new ToolCallback[0]))
                .build();
 
        // 4. Tiếp tục chuỗi xử lý
        return chain.nextAroundCall(modifiedRequest);
    }
 
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 100;
    }
 
    @Override
    public String getName() {
        return "DynamicToolSelectionAdvisor";
    }
}

# 3. cấu hình ChatClient trong Spring Configuration

Đăng ký Advisor vào ChatClient.Builder để toàn bộ các lời gọi agent đều tự động thừa hưởng cơ chế truy xuất động:

package com.tungdadev.agent.config;
 
import com.tungdadev.agent.advisor.DynamicToolSelectionAdvisor;
import com.tungdadev.agent.tools.DynamicToolRegistry;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.List;
 
@Configuration
public class AgentChatConfiguration {
 
    @Bean
    public ChatClient enterpriseAgentChatClient(ChatClient.Builder builder,
                                                VectorStore vectorStore,
                                                DynamicToolRegistry toolRegistry) {
        return builder
                .defaultAdvisors(
                    new DynamicToolSelectionAdvisor(
                        vectorStore,
                        toolRegistry,
                        3,       // Top-3 tools liên quan nhất
                        0.72,    // Ngưỡng Cosine Similarity tối thiểu
                        List.of() // Always-on tools nếu có
                    )
                )
                .build();
    }
}

# chiến lược kết hợp: Hybrid Tool Retrieval và RBAC

Trong môi trường doanh nghiệp thực tế, chỉ tìm kiếm ngữ nghĩa đơn thuần là chưa đủ. Bạn cần thiết lập hai cơ chế bảo vệ:

  1. Always-On Tools: Một số công cụ mang tính kiểm tra cốt lõi (như ghi nhật ký kiểm toán, kiểm tra quyền truy cập) luôn được đưa vào danh sách alwaysOnTools mà không phụ thuộc vào kết quả vector search.
  2. Kiểm soát quyền truy cập theo vai trò (Role-Based Tool Filtering):
    • Trước khi vector search trả về kết quả cho LLM, hệ thống lọc danh sách công cụ theo quyền của người dùng hiện tại (Authentication / GrantedAuthorities).
    • Nếu nhân viên hỗ trợ cấp 1 hỏi câu lệnh hoàn tiền, công cụ issueRefundTool sẽ tự động bị loại khỏi danh sách ngay ở tầng bộ lọc metadata, ngăn chặn hoàn toàn rủi ro vượt quyền thông qua Prompt Injection.

# đo lường hiệu năng thực tế

Kiểm thử hệ thống trợ lý ảo với 60 công cụ nghiệp vụ doanh nghiệp:

Tiêu chí kỹ thuậtNạp tĩnh toàn bộ (Static)Dynamic Tool RetrievalCải thiện thực tế
Dung lượng Input Tokens trung bình~15,200 tokens~850 tokens📉 Tiết kiệm ~94.4%
Thời gian phản hồi token đầu tiên (TTFT)3.12 giây0.78 giây⚡ Nhanh gấp 4 lần
Tỷ lệ chọn đúng công cụ (Accuracy)82.3%98.1%🎯 Giảm thiểu ảo giác
Giới hạn số lượng công cụ tối đaBị nghẽn ở ~80 toolsMở rộng hàng nghìn tools🚀 Khả năng mở rộng cao

Kiến trúc Dynamic Tool Calling biến các Agent xây dựng trên Spring AI từ những hệ thống nguyên khối cồng kềnh thành những tác tử phần mềm tinh gọn, phản hồi nhanh và tối ưu chi phí hạ tầng ở quy mô lớn.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.