Khái niệm cơ bản về Function Calling
Function Calling là một giao thức cho phép các mô hình ngôn ngữ lớn (LLM) tương tác với các hệ thống bên ngoài. Thay vì chỉ trả về văn bản, LLM có thể quyết định gọi một hàm cụ thể với các tham số có cấu trúc. Spring AI giúp đơn giản hóa quá trình này bằng cách sử dụng Reflection và Annotation để tự động hóa việc đăng ký hàm, tuần tự hóa dữ liệu và xử lý phản hồi.
Quy trình thực thi trong Spring AI
Luồng dữ liệu diễn ra theo các bước sau:
- Người dùng gửi yêu cầu thông qua ChatClient.
- LLM phân tích ngữ cảnh và xác định xem có cần sử dụng công cụ bên ngoài hay không.
- Nếu cần, LLM trả về một yêu cầu gọi hàm (Function Call Request) dưới dạng JSON.
- Spring AI ánh xạ yêu cầu này vào một phương thức Java cụ thể và thực thi nó.
- Kết quả của hàm được gửi ngược lại cho LLM để tổng hợp câu trả lời cuối cùng cho người dùng.
Triển khai hàm tùy chỉnh với @Bean
Cách phổ biến nhất để đăng ký hàm trong Spring AI là sử dụng @Bean kết hợp với interface Function. Spring AI sẽ tự động tạo JSON Schema dựa trên các Record hoặc Class được sử dụng làm đầu vào.
@Configuration
public class LogisticsConfig {
@Autowired
private TrackingService trackingService;
@Bean
@Description("Tra cứu thông tin vận chuyển dựa trên mã đơn hàng")
public Function<ShipmentRequest, ShipmentStatus> trackShipment() {
return request -> trackingService.getDetail(request.trackingCode());
}
@Bean
@Description("Thay đổi địa chỉ nhận hàng cho đơn hàng chưa xử lý")
public Function<AddressUpdateRequest, UpdateResult> updateShippingAddress() {
return request -> trackingService.changeAddress(request.orderId(), request.newAddress());
}
}
/**
* Các Record định nghĩa cấu trúc dữ liệu
*/
public record ShipmentRequest(
@JsonPropertyDescription("Mã vận đơn, định dạng thường bắt đầu bằng VN...") String trackingCode
) {}
public record ShipmentStatus(
String code,
String currentPosition,
String estimatedDelivery,
String status
) {}
public record AddressUpdateRequest(
@JsonPropertyDescription("ID duy nhất của đơn hàng") String orderId,
@JsonPropertyDescription("Địa chỉ đầy đủ mới của khách hàng") String newAddress
) {}
public record UpdateResult(
boolean isSuccess,
String message
) {}
Quản lý hàm nâng cao với FunctionCallback
Để kiểm soát chi tiết hơn hoặc đăng ký hàm một cách linh hoạt trong quá trình chạy (runtime), FunctionCallback là một lựa chọn tối ưu.
@Service
public class SupportAssistant {
private final ChatClient chatClient;
public String handleCustomerQuery(String prompt) {
// Xây dựng callback thủ công
var stockCheckTool = FunctionCallback.builder()
.function("checkStock", (StockQuery q) -> "Số lượng còn lại: 15")
.description("Kiểm tra số lượng tồn kho của sản phẩm")
.inputType(StockQuery.class)
.build();
return chatClient.prompt()
.user(prompt)
.functionCallbacks(stockCheckTool)
.functions("trackShipment") // Đăng ký hàm từ Bean context
.call()
.content();
}
}
public record StockQuery(String productName) {}
Đăng ký hàm động theo ngữ cảnh (Dynamic Registration)
Trong các hệ thống phức tạp, bạn có thể muốn LLM chỉ truy cập vào một nhóm hàm nhất định tùy thuộc vào loại người dùng hoặc phạm vi nghiệp vụ.
public String processRequestByDomain(String message, String scope) {
FunctionCallbackResolver customResolver = switch (scope) {
case "FINANCE" -> financeToolResolver();
case "HR" -> hrToolResolver();
default -> FunctionCallbackResolver.empty();
};
return chatClient.prompt()
.user(message)
.functionCallbacks(customResolver)
.call()
.content();
}
private FunctionCallbackResolver financeToolResolver() {
List<FunctionCallback> tools = List.of(
FunctionCallback.builder()
.function("getExchangeRate", req -> "25400 VND/USD")
.description("Lấy tỷ giá hối đoái mới nhất")
.build()
);
return FunctionCallbackResolver.from(tools);
}
Xử lý bất đồng bộ và giám sát luồng
Đối với các tác vụ tốn thời gian, chúng ta có thể sử dụng cơ chế xử lý bất đồng bộ để tránh làm nghẽn luồng chính của LLM.
@Bean
@Description("Khởi tạo tiến trình xuất báo cáo dữ liệu lớn")
public Function<ReportCriteria, JobTicket> generateLargeReport() {
return criteria -> {
String ticketId = "JOB-" + System.currentTimeMillis();
CompletableFuture.runAsync(() -> {
// Giả lập xử lý nặng
System.out.println("Đang xử lý báo cáo cho: " + criteria.module());
});
return new JobTicket(ticketId, "PENDING", "Báo cáo đang được khởi tạo.");
};
}
public record ReportCriteria(String module, String dateRange) {}
public record JobTicket(String id, String status, String note) {}
Cơ chế an toàn cho Function Calling
Việc gọi các API bên ngoài tiềm ẩn rủi ro về độ trễ và lỗi hệ thống. Một SafeExecutor có thể được triển khai để bao bọc các lời gọi hàm.
@Component
public class ExecutionGuard {
public Object callWithRetry(Supplier<Object> action, int retryCount) {
int current = 0;
while (current < retryCount) {
try {
return action.get();
} catch (Exception e) {
current++;
if (current >= retryCount) {
return Map.of("error", "Thực thi thất bại sau nhiều lần thử", "reason", e.getMessage());
}
}
}
return null;
}
public Object callWithFallback(Supplier<Object> action, Object fallbackValue) {
try {
return action.get();
} catch (Exception e) {
return fallbackValue;
}
}
}
Tùy biến JSON Schema qua Annotation
Để LLM hiểu rõ hơn về các tham số, việc cung cấp mô tả chi tiết là rất quan trọng. Spring AI hỗ trợ các annotation của thư viện Jackson để làm phong phú Schema.
public record InventoryAction(
@JsonProperty(required = true)
@JsonPropertyDescription("Mã SKU của sản phẩm")
String sku,
@JsonPropertyDescription("Hành động: INCREASE hoặc DECREASE")
String operationType,
@JsonPropertyDescription("Số lượng thay đổi")
Integer amount
) {}
@Component
public class WarehouseService {
@Description("Cập nhật số lượng kho bãi cho một mã SKU cụ thể")
public String updateStock(InventoryAction action) {
// Thực hiện logic cập nhật
return "Cập nhật thành công SKU: " + action.sku();
}
}