Agentic AI with Quarkus - 第二部分

本系列博客文章的第一部分简要介绍了 Agentic AI 并讨论了工作流模式。本文将探讨另一种模式:Agent。两者之间的主要区别在于,工作流模式是通过编程定义的,而 Agent 更为灵活,可以处理更广泛的任务。使用 Agent,LLM 会协调步骤序列,而不是由外部程序进行协调,从而达到更高的自主性和灵活性。

Agent

Agent 与工作流模式的不同之处在于,控制流完全委托给 LLM,而不是通过编程实现。要成功实现 Agent,LLM 必须能够进行推理并访问一组工具(工具箱)。LLM 会协调步骤序列,并决定调用哪些工具以及使用哪些参数。从外部来看,调用 Agent 可以被视为调用一个函数,该函数会机会性地调用工具来完成确定的子任务。

Agent 的工具箱可以包含

  • 外部服务(如 HTTP 端点)

  • 其他 LLM / Agent

  • 提供数据存储中数据的方法

  • 应用程序本身提供的方法

Agents can invoke tools
图 1. Agent 可以调用工具

在 Quarkus 中,Agent 由带 @RegisterAiService 注解的接口表示。它们被称为AI 服务。在这方面,它们与工作流模式没有区别。主要区别在于,Agent 接口的方法都带有 @ToolBox 注解,用于声明 LLM 可以使用的工具来完成任务。

@RegisterAiService(modelName = "my-model")  // <-- The model used by this agent must be able to reason and decide which tools to call
@SystemMessage("...")
public interface RestaurantAgent {

    @UserMessage("...")
    @ToolBox({BookingService.class, WeatherService.class}) // <-- The tools that the LLM can use
    String handleRequest(String request);
}

或者,@RegisterAiService 注解可以在其 tools 参数中接收一组工具。

让我们通过一些 Agent 的示例来更好地理解它们的工作原理和能实现的目标。

天气预报 Agent

Agentic AI 的第一个示例实现了以下天气预报 Agent。该 Agent 接收用户提示,并必须用不超过三行的方式回答天气问题。为了实现这一目标,该 Agent 的工具箱包含:

  1. 一个专门用于从用户提示中提取城市名称的 AI 服务 - 这可以是另一个Agent

  2. 一个返回给定城市地理坐标的 Web 服务 - 这是一个远程调用;

  3. 第二个 Web 服务,提供给定纬度和经度的天气预报 - 另一个远程调用。

Weather agent architecture
图 2. 天气 Agent 架构

我们不指明这些工具何时以及如何使用,我们只是将它们添加到工具箱中。LLM 会决定何时调用它们以及使用什么参数。

@RegisterAiService(modelName = "tool-use")
public interface WeatherForecastAgent {

    @SystemMessage("""
        You are a meteorologist, and you need to answer questions asked by the user about weather using at most 3 lines.

        The weather information is a JSON object and has the following fields:

        maxTemperature is the maximum temperature of the day in Celsius degrees
        minTemperature is the minimum temperature of the day in Celsius degrees
        precipitation is the amount of water in mm
        windSpeed is the speed of wind in kilometers per hour
        weather is the overall weather.
    """)
    @ToolBox({CityExtractorAgent.class, WeatherForecastService.class, GeoCodingService.class})
    String chat(String query);
}

让我们看看这个 Agent 使用的工具是如何定义的

1. 另一个 AI 服务,专门用于从用户提示中提取城市名称(因此也演示了如何轻松地将一个Agent配置为另一个 AI 服务/Agent 的工具)。

@ApplicationScoped
@RegisterAiService(chatMemoryProviderSupplier = RegisterAiService.NoChatMemoryProviderSupplier.class)
public interface CityExtractorAgent {

    @UserMessage("""
        You are given one question and you have to extract city name from it
        Only reply the city name if it exists or reply 'unknown_city' if there is no city name in question

        Here is the question: {question}
        """)
    @Tool("Extracts the city from a question") // <-- The tool description, the LLM can use it to decide when to call this tool
    String extractCity(String question); // <-- The method signature, the LLM use it to know how to call this tool
}

2. 一个Web 服务,返回给定城市的地理坐标。它是一个简单的 Quarkus REST 客户端接口,意味着 Quarkus 会自动生成实际的实现。它可以与容错、指标和其他 Quarkus 功能结合使用。

@RegisterRestClient(configKey = "geocoding")
@Path("/v1")
public interface GeoCodingService {

    @GET
    @Path("/search")
    @ClientQueryParam(name = "count", value = "1") // Limit the number of results to 1 (HTTP query parameter)
    @Tool("Finds the latitude and longitude of a given city")
    GeoResults findCity(@RestQuery String name);
}

3. 另一个Web 服务,提供给定纬度和经度的天气预报。

@RegisterRestClient(configKey = "openmeteo")
@Path("/v1")
public interface WeatherForecastService {

    @GET
    @Path("/forecast")
    @ClientQueryParam(name = "forecast_days", value = "7")
    @ClientQueryParam(name = "daily", value = {
            "temperature_2m_max",
            "temperature_2m_min",
            "precipitation_sum",
            "wind_speed_10m_max",
            "weather_code"
    })
    @Tool("Forecasts the weather for the given latitude and longitude")
    WeatherForecast forecast(@RestQuery double latitude, @RestQuery double longitude);
}

可以调用公开此 Agentic 天气服务的HTTP 端点

curl https://:8080/weather/city/Rome

响应将是这样的

The weather in Rome today will have a maximum temperature of 14.3°C, minimum temperature of 2.0°C.
No precipitation expected, and the wind speed will be up to 5.6 km/h.
The overall weather condition is expected to be cloudy.

本质上,这种控制流与提示链工作流(在上一篇文章中介绍)非常相似,其中用户输入被按顺序转换为步骤(在本例中,从提示到提示中包含的城市名称,再到该城市的地理坐标,再到这些坐标的天气预报)。显著的区别在于,LLM 直接协调步骤序列,而不是由外部程序进行协调。

Quarkus 自动提供的可观测性(在 GitHub 项目中,可观测性默认是禁用的,但可以通过 -Dobservability 标志开启)允许直观地追踪 Agent 为执行其任务而完成的任务序列。

Tracing sequential execution of the prompt chaining pattern
图 3. 追踪天气 Agent 执行

更通用的 AI Agent

在前面的示例中,Agent 可以访问非常具体的工具。可以提供更通用的工具来帮助 Agent 执行更广泛的任务。通常,Web 搜索工具对于信息检索任务很有用。这就是第二个示例的目的。它通过允许 LLM 在线搜索其原始训练集之外的信息来扩展 Agent 的能力。

通常,这些场景需要更大的模型,因此本示例已配置为使用 qwen2.5-14b 和更长的超时时间,以使其有机会完成其任务。

quarkus.langchain4j.ollama.big-model.chat-model.model-id=qwen2.5:14b
quarkus.langchain4j.ollama.big-model.chat-model.temperature=0
quarkus.langchain4j.ollama.big-model.timeout=600s

这个示例中的智能 Agent可以通过将模型名称传递给 @RegisterAiService 注解来配置使用这个更大的模型。

@RegisterAiService(modelName = "big-model")
public interface IntelligentAgent {

    @SystemMessage("""
        You are a chatbot, and you need to answer questions asked by the user.
        Perform a web search for information that you don't know and use the result to answer to the initial user's question.
    """)
    @ToolBox({WebSearchService.class}) // <-- the web search tool
    String chat(String question);
}

工具可以在 DuckDuckGo 上执行 Web 搜索,并以纯文本格式返回结果。

@ApplicationScoped
public class WebSearchService {

    @Tool("Perform a web search to retrieve information online")
    String webSearch(String q) throws IOException {
        String webUrl = "https://html.duckduckgo.com/html/?q=" + q;
        return Jsoup.connect(webUrl).get().text();
    }
}

可以使用更高级的搜索引擎或 API,例如Tavily

AI 服务使用此工具来检索它不知道的所有信息,并将这些数据整理在一起,以回答通用的用户问题。

例如,请考虑以下问题:一只豹子全速跑过艺术桥需要多少秒? 使用这个HTTP 端点,它将使用以下方式执行:

curl https://:8080/ask/how%20many%20seconds%20would%20it%20take%20for%20a%20leopard%20at%20full%20speed%20to%20run%20through%20Pont%20des%20Arts

为了回答这个问题,Agent 调用 Web 搜索工具两次:一次是为了查找艺术桥的长度,一次是为了检索豹子的速度。

An agent using an external web search tool
图 4. Agent 使用外部 Web 搜索工具

然后,Agent 将这些信息组合起来,生成类似以下的输出:

The length of Pont des Arts is approximately 155 meters. A leopard can run at speeds up to about 58 kilometers per hour (36 miles per hour). To calculate how many seconds it would take for a leopard running at full speed to cross the bridge, we need to convert its speed into meters per second and then divide the length of the bridge by this speed.

1 kilometer = 1000 meters
58 kilometers/hour = 58 * 1000 / 3600 ≈ 16.11 meters/second

Now, we can calculate the time it would take for a leopard to run through Pont des Arts:

Time (seconds) = Distance (meters) / Speed (m/s)
= 155 / 16.11
≈ 9.62 seconds

So, it would take approximately 9.62 seconds for a leopard running at full speed to run through Pont des Arts.

这个示例说明了 Agent 如何使用工具来检索数据。虽然我们这里使用了搜索引擎,但您也可以轻松实现一个查询数据库或其他服务的工具来检索所需信息。您可以查看此示例,了解如何使用 Quarkus Panache 存储库实现查询数据库的工具。

Agent 和对话式 AI

AI Agent 的灵活性在不旨在完成单个请求而需要与用户进行更长时间的对话以实现其目标的服务的应用中会变得更加重要。例如,Agent 可以作为聊天机器人,使其能够并行处理多个用户,每个用户都有独立的对话。这需要管理每次对话的状态,通常称为记忆(与 LLM 已交换的消息集)。

一个餐厅预订系统的聊天机器人,旨在与客户聊天并收集他们的数据和要求,是这种模式的一个有趣的实际应用。

@RegisterAiService(modelName = "tool-use")
@SystemMessage("""
        You are an AI dealing with the booking for a restaurant.
        Do not invent the customer name or party size, but explicitly ask for them if not provided.
        If the user specifies a preference (indoor/outdoor), you should book the table with the preference. However, please check the weather forecast before booking the table.
        """)
@SessionScoped
public interface RestaurantAgent {

    @UserMessage("""
            You receive request from customer and need to book their table in the restaurant.
            Please be polite and try to handle the user request.

            Before booking the table, makes sure to have valid date for the reservation, and that the user explicitly provided his name and party size.
            If the booking is successful just notify the user.

            Today is: {current_date}.
            Request: {request}
            """)
    @ToolBox({BookingService.class, WeatherService.class})
    String handleRequest(String request);
}

请注意,用户消息不仅传达了客户的请求,还包括了当前日期。这使得 LLM 能够理解相对日期,例如“明天”或“三天后”,而这些是人类常用的。最初,我们将当前日期包含在系统消息中,但这常常导致 LLM 忘记日期并臆造使用不同的日期。经验证明,将其移至用户消息效果更好,可能是因为它不仅传递一次,而是在每次聊天记录中的每条消息中都传递。

当 Agent 完成信息收集过程时,聊天机器人会使用一个访问现有预订数据库的工具来检查是否有可用的桌子满足客户的需求,并在有可用时进行预订。

@ApplicationScoped
public class BookingService {

    private final int capacity;

    public BookingService(@ConfigProperty(name = "restaurant.capacity") int capacity) {
        this.capacity = capacity;
    }

    public boolean hasCapacity(LocalDate date, int partySize) {
        int sum = Booking.find("date", date).list().stream().map(b -> (Booking) b)
                         .mapToInt(b -> b.partySize)
                         .sum();
        return sum + partySize <= capacity;
    }

    @Transactional
    @Tool("Books a table for a given name, date (passed as day of the month, month and year), party size and preference (indoor/outdoor). If the restaurant is full, an exception is thrown. If preference is not specified, `UNSET` is used.")
    public String book(String name, int day, int month, int year, int partySize, Booking.Preference preference) {
        var date = LocalDate.of(year, month, day);
        if (hasCapacity(date, partySize)) {
            Booking booking = new Booking();
            booking.date = date;
            booking.name = name;
            booking.partySize = partySize;
            if (preference == null) {
                preference = Booking.Preference.UNSET;
            }
            booking.preference = preference;
            booking.persist();
            String result = String.format("%s successfully booked a %s table for %d persons on %s", name, preference, partySize, date);
            Log.info(result);
            return result;
        }
        return "The restaurant is full for that day";
    }
}

为了帮助客户决定是否在外面用餐,Agent 还可以重用前面示例中实现的天气预报服务作为第二个工具,并将餐厅的地理坐标传递给它。

restaurant.location.latitude=45
restaurant.location.longitude=5

聊天机器人的最终架构设计如下:

The restaurant chatbot agent
图 5. 餐厅聊天机器人 Agent

一旦客户提供了所有必要详细信息,聊天机器人就会确认预订并显示预订摘要。最终的预订随后会存储在数据库中。可以通过访问 URL 来试用:

典型的用户交互示例可能如下所示:

An example of interaction with the restaurant chatbot agent
图 6. 与餐厅聊天机器人 Agent 交互的示例

结论和后续步骤

本系列博客文章演示了如何使用 Agentic 模式通过 Quarkus 及其 LangChain4j 扩展来实现注入 AI 的应用程序。

我们在上一篇文章中涵盖了工作流模式,在本文中涵盖了Agent。这两种模式都基于相同的基本原则,但在控制流的管理方式上有所不同。工作流模式更适合可以通过编程轻松定义任务,而 Agent 更灵活,可以处理更广泛的任务。

尽管如此,本系列中讨论的示例可以通过将在未来工作中引入的其他技术进行改进和进一步推广,例如:

  • 跨 LLM 调用的内存管理

  • 长期运行进程的状态管理

  • 改进的可观测性

  • 动态工具和工具发现

  • MCP 协议的关系以及如何使用 MCP 客户端和服务器实现 Agentic 架构

  • 在 Agentic 架构的背景下,如何以工作流模式和 Agent 的方式重新审视 RAG 模式?