第 07 步 - 函数调用和工具
RAG 模式允许根据您自己的数据将知识传递给 LLM。这是一个非常流行的模式,但并非唯一可以使用的模式。
在此步骤中,我们将介绍另一种赋予 LLM 超能力的方法:函数调用。基本上,我们将允许 LLM 调用您在代码中定义的函数。LLM 将决定何时以及使用什么参数调用函数。当然,请确保不要允许 LLM 调用可能对您的系统有害的函数,并确保净化任何输入数据。
函数调用
函数调用是某些 LLM(GPT、Llama...)提供的一种机制。它允许 LLM 调用您在应用程序中定义的函数。当应用程序将用户消息发送到 LLM 时,它还会发送 LLM 可以调用的函数列表。
然后,LLM 可以决定是否要使用它想要的参数调用其中一个函数。应用程序接收方法调用请求,并使用 LLM 提供的参数执行函数。结果会发送回 LLM,LLM 可以使用该结果继续对话,并计算下一条消息。
在此步骤中,我们将介绍如何在我们的应用程序中实现函数调用。我们将设置一个数据库并创建一个允许 LLM 从数据库检索数据(预订、客户…)的函数。
最终代码可在 step-06
文件夹中找到。但是,我们建议您按照分步指南进行操作,以了解其工作原理以及实现此模式的不同步骤。
几个新的依赖项
开始之前,我们需要安装几个新的依赖项。打开 pom.xml
文件并添加以下依赖项:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
如果您不熟悉 Panache,它是 Hibernate ORM 的一个层,它简化了与数据库的交互。您可以在此处找到有关 Panache 的更多信息。
准备实体
现在我们有了依赖项,我们可以创建几个实体。我们将把预订列表存储在数据库中。每个预订都与一个客户相关联。一个客户可以有多个预订。
创建 dev.langchain4j.quarkus.workshop.Customer
实体类,内容如下
package dev.langchain4j.quarkus.workshop;
import java.util.Optional;
import jakarta.persistence.Entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
public class Customer extends PanacheEntity {
String firstName;
String lastName;
public static Optional<Customer> findByFirstAndLastName(String firstName, String lastName) {
return find("firstName = ?1 and lastName = ?2", firstName, lastName).firstResultOptional();
}
}
然后创建 dev.langchain4j.quarkus.workshop.Booking
实体类,内容如下
package dev.langchain4j.quarkus.workshop;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import java.time.LocalDate;
@Entity
public class Booking extends PanacheEntity {
@ManyToOne
Customer customer;
LocalDate dateFrom;
LocalDate dateTo;
String location;
}
既然我们已经完成了,让我们创建 dev.langchain4j.quarkus.workshop.Exceptions
类,其中包含一组我们将使用的 Exception
package dev.langchain4j.quarkus.workshop;
public class Exceptions {
public static class CustomerNotFoundException extends RuntimeException {
public CustomerNotFoundException(String customerName, String customerSurname) {
super("Customer not found: %s %s".formatted(customerName, customerSurname));
}
}
public static class BookingCannotBeCancelledException extends RuntimeException {
public BookingCannotBeCancelledException(long bookingId) {
super("Booking %d cannot be cancelled - see terms of use".formatted(bookingId));
}
public BookingCannotBeCancelledException(long bookingId, String reason) {
super("Booking %d cannot be cancelled because %s - see terms of use".formatted(bookingId, reason));
}
}
public static class BookingNotFoundException extends RuntimeException {
public BookingNotFoundException(long bookingId) {
super("Booking %d not found".formatted(bookingId));
}
}
}
好了,我们有了实体和异常。让我们向数据库添加一些数据。
创建 src/main/resources/import.sql
文件,内容如下
INSERT INTO customer (id, firstName, lastName) VALUES (1, 'Speedy', 'McWheels');
INSERT INTO customer (id, firstName, lastName) VALUES (2, 'Zoom', 'Thunderfoot');
INSERT INTO customer (id, firstName, lastName) VALUES (3, 'Vroom', 'Lightyear');
INSERT INTO customer (id, firstName, lastName) VALUES (4, 'Turbo', 'Gearshift');
INSERT INTO customer (id, firstName, lastName) VALUES (5, 'Drifty', 'Skiddy');
ALTER SEQUENCE customer_seq RESTART WITH 5;
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (1, 1, '2024-07-10', '2024-07-15', 'Brussels, Belgium');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (2, 1, '2024-08-05', '2024-08-12', 'Los Angeles, California');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (3, 1, '2024-10-01', '2024-10-07', 'Geneva, Switzerland');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (4, 2, '2024-07-20', '2024-07-25', 'Tokyo, Japan');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (5, 2, '2024-11-10', '2024-11-15', 'Brisbane, Australia');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (7, 3, '2024-06-15', '2024-06-20', 'Missoula, Montana');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (8, 3, '2024-10-12', '2024-10-18', 'Singapore');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (9, 3, '2024-12-03', '2024-12-09', 'Capetown, South Africa');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (10, 4, '2024-07-01', '2024-07-06', 'Nuuk, Greenland');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (11, 4, '2024-07-25', '2024-07-30', 'Santiago de Chile');
INSERT INTO booking (id, customer_id, dateFrom, dateTo, location)
VALUES (12, 4, '2024-10-15', '2024-10-22', 'Dubai');
ALTER SEQUENCE booking_seq RESTART WITH 12;
该文件将在应用程序启动时执行,并将一些数据插入到数据库中。没有特定配置,它将仅在开发模式(./mvnw quarkus:dev
)下应用。
定义工具
好了,现在我们有了创建允许 LLM 从数据库检索数据的函数所需的一切。我们将创建一个 BookingRepository
类,其中包含一组用于与数据库交互的函数。
创建 dev.langchain4j.quarkus.workshop.BookingRepository
类,内容如下
package dev.langchain4j.quarkus.workshop;
import static dev.langchain4j.quarkus.workshop.Exceptions.*;
import java.time.LocalDate;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import dev.langchain4j.agent.tool.Tool;
@ApplicationScoped
public class BookingRepository implements PanacheRepository<Booking> {
@Tool("Cancel a booking")
@Transactional
public void cancelBooking(long bookingId, String customerFirstName, String customerLastName) {
var booking = getBookingDetails(bookingId, customerFirstName, customerLastName);
// too late to cancel
if (booking.dateFrom.minusDays(11).isBefore(LocalDate.now())) {
throw new BookingCannotBeCancelledException(bookingId, "booking from date is 11 days before today");
}
// too short to cancel
if (booking.dateTo.minusDays(4).isBefore(booking.dateFrom)) {
throw new BookingCannotBeCancelledException(bookingId, "booking period is less than four days");
}
delete(booking);
}
@Tool("List booking for a customer")
@Transactional
public List<Booking> listBookingsForCustomer(String customerName, String customerSurname) {
var found = Customer.findByFirstAndLastName(customerName, customerSurname);
return found
.map(customer -> list("customer", customer))
.orElseThrow(() -> new CustomerNotFoundException(customerName, customerSurname));
}
@Tool("Get booking details")
@Transactional
public Booking getBookingDetails(long bookingId, String customerFirstName, String customerLastName) {
var found = findByIdOptional(bookingId)
.orElseThrow(() -> new BookingNotFoundException(bookingId));
if (!found.customer.firstName.equals(customerFirstName) || !found.customer.lastName.equals(customerLastName)) {
throw new BookingNotFoundException(bookingId);
}
return found;
}
}
存储库定义了三个方法
cancelBooking
用于取消预订。它会检查预订是否可以取消,然后从数据库中删除它。listBookingsForCustomer
用于列出客户的所有预订。getBookingDetails
用于检索预订的详细信息。
每个方法都用 @Tool
注解。这样我们就可以告诉 LLM 这些方法可以被调用。该注解的可选值可以提供有关该工具的更多信息,以便 LLM 可以选择正确的工具。
为 LLM 提供工具箱
现在让我们修改我们的 AI 服务接口(dev.langchain4j.quarkus.workshop.CustomerSupportAgent
)
package dev.langchain4j.quarkus.workshop;
import jakarta.enterprise.context.SessionScoped;
import dev.langchain4j.service.SystemMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.ToolBox;
@SessionScoped
@RegisterAiService
public interface CustomerSupportAgent {
@SystemMessage("""
You are a customer support agent of a car rental company 'Miles of Smiles'.
You are friendly, polite and concise.
If the question is unrelated to car rental, you should politely redirect the customer to the right department.
Today is {current_date}.
""")
@ToolBox(BookingRepository.class)
String chat(String userMessage);
}
我们已将 @Toolbox
注解添加到 chat
方法。它列出了 LLM 可以调用的工具所在的类。
另请注意,我们在系统提示中添加了一个新的占位符 {current_date}
,以便 LLM 知道当前日期(并可以应用取消策略)。
提示和模板
系统消息和用户消息可以包含占位符。占位符将替换为应用程序提供的值。您可以将参数传递给 AI 服务方法,并将其包含在提示中。它在底层使用 Qute 模板引擎。这是一个强大的功能,可以为您提供高级模板逻辑。
工具和流式响应
目前在使用(阻塞)工具和流式响应时存在一个已知限制。将 AI 服务方法签名中的 Multi<String>
替换为 String
以避免此问题,并相应地更改 dev.langchain4j.quarkus.workshop.CustomerSupportAgentWebSocket
中的调用
测试函数调用
让我们测试函数调用。确保应用程序正在运行(./mvnw quarkus:dev
)。
在浏览器中打开 https://:8080。在聊天机器人中,让我们尝试以下交互
AI: 当然,我很乐意为您提供帮助。我需要更多信息才能继续:1. 您的名字和姓氏,以便查找预订。2. 与您的预订相关的预订 ID。请提供这些详细信息,以便我能为您提供进一步的帮助。
My name is Speedy McWheels. But, I don't remember the booking ID. Can you list all my future bookings?
AI: 这是您即将进行的预订:1. 预订 ID:1 - 从:2025-07-10 - 到:2025-07-12 2. 预订 ID:2 - 从:2025-08-05 - 到:2025-08-12 3. 预订 ID:3 - 从:2025-10-01 - 到:2025-10-07 请告诉我您想取消哪个预订。
AI: 您从 2025-10-01 到 2025-10-07 的预订已成功取消。如果您需要任何进一步的帮助,请随时告诉我!
这是您当前的预订:1. 预订 ID:1 - 从:2025-07-10 - 到:2025-07-12 2. 预订 ID:2 - 从:2025-08-05 - 到:2025-08-12 如果您需要任何进一步的帮助,请告诉我!
好了,现在让我们尝试取消一个无效的预订
AI: 不幸的是,我无法取消您从 2025-07-10 开始的预订,因为预订期少于 4 天,并且不允许对此类预订进行取消。如果您有任何其他问题或需要进一步的帮助,请告诉我!
结论
在此步骤中,我们探讨了如何在我们的应用程序中实现函数调用,从而使我们能够创建代理——LLM 不仅能够进行推理,还能与系统进行动态交互。
在此上下文中,函数只是您应用程序中用 @Tool
注释的函数。函数的实际实现完全可自定义。例如,您可以通过集成远程服务、个性化推荐或其他外部数据源来扩展您的聊天机器人,以获取天气预报工具。此外,您还可以利用更专业的 LLM,将特定查询(如法律或保险相关问题)路由到在这些领域接受过训练的模型。
但是,引入工具和函数调用也会带来新的风险,例如 LLM 行为异常(例如,过度调用函数或使用错误的参数)或易受提示注入攻击。在下一步中,我们将探讨如何通过模型上下文协议进一步扩展这一 Agentic AI 概念。