Ohhnews

分类导航

$ cd ..
foojay原文

使用MongoDB构建AI系统:实现规划模式

#mongodb#人工智能#规划模式#ai系统开发#软件工程

人工智能已迅速从一个小众研究领域演变为影响软件行业几乎每个方面的技术。开发者现在使用 AI 生成代码、审查拉取请求、编写文档,并通过诸如“氛围编程”和规格驱动开发等方法加速工作流。虽然这些应用将 AI 定位为工程工具,但一个新趋势正在浮现:将 AI 直接集成到应用内的业务工作流中。

这种转变给软件架构师和工程师带来了新的挑战。传统应用依赖确定性流程,执行路径由方法、条件、循环和业务规则明确界定。相比之下,机器学习系统可以在执行过程中做出决策、选择行动,并根据上下文和可用信息动态调整。因此,架构师必须设计能够平衡传统软件的可预测性与 AI 驱动推理的灵活性的系统。Planning 模式是连接这两个领域的有效方法。该模式不是让 LLM 一次性解决复杂目标,而是将目标分解为更小、可行的任务,这些任务与确定性代码、外部服务和数据源进行交互。这为自主 AI 应用创建了一条更可靠、更可观测的路径。

本教程介绍 Planning 模式,并演示如何使用 MongoDB 实现它。

在本教程中,您将:

  • 建模一个简单的旅行系统。
  • 使用 Java 建模并与 MongoDB 交互。
  • 探索 MongoDB 如何帮助您实现带 Planning 模式的 AI。 您可以在 GitHub 仓库中找到本教程提供的所有代码:
git@github.com:soujava/mongodb-ai-planning-pattern.git

前置条件

对于本教程,您需要:

  • Java 21
  • Maven
  • 一个 MongoDB 集群

您可以使用以下 Docker 命令启动一个独立的 MongoDB 实例:

docker run --rm -d --name mongodb-instance -p 27017:27017 mongo

Planning 模式使 AI 系统能够将高级目标分解为更小、独立的操作。智能体并非仅依赖内部知识,而是识别所需信息并选择确定性工具,例如数据库或 REST 服务。这种方法通过结合大语言模型的推理能力与传统软件组件的可预测行为,提高了可靠性、可观测性和准确性。

与使用专用规划器智能体或工作流图的架构不同,本实现将语言模型本身用作规划器。在执行过程中,模型反复推理用户的目标,选择最合适的工具,观察返回的数据,并决定在生成最终响应之前是否需要额外的操作。这种“推理—行动—观察”循环提供了一种轻量级但高效的规划架构,在现代智能体系统中被广泛使用。 [LOADING...]

为了说明这种模式,我们将开发一个旅行行程助手。用户可能会问诸如“显示可前往的城市”或“创建葡萄牙的历史行程”之类的问题。AI 智能体将访问一个包含城市和景点的 MongoDB 数据库,该数据库反映了旅行社可提供的服务,而不是仅依赖训练数据。根据用户的目标,智能体将选择合适的工具,检索相关信息,并使用真实的应用数据生成推荐。

实现使用 Jakarta EE 作为应用平台,JSFPrimeFaces 用于用户界面,LangChain4j 用于 AI 集成和工具调用,MongoDB 用于数据持久化。Jakarta DataJakarta NoSQL 通过支持仓库和领域模型,以最少的样板代码简化了数据库访问。这些技术共同创建了一个实用环境,用于探索使用 Planning 模式的 AI 智能体与企业应用的协作。 [LOADING...]

例如,当用户执行任何问题时,应用将遵循以下流程: [LOADING...]

步骤 1:生成项目

首先使用 Jakarta EE 入门工具生成一个新的 Jakarta EE 项目。在此示例中,我们将使用 Glassfish 版本 8.0.3。

选择 Jakarta EE 11 作为版本,Platform 作为配置文件,Java 21 作为 Java 版本。请参考下图中的选项。选择后,点击“Generate Project”下载项目。 [LOADING...]

接下来,添加所需的库。Jakarta Data 简化了 Java 与 MongoDB 的集成,并支持 Java Enterprise 标准。对于 AI 集成,我们将使用带 CDI 的 langchain4j。

MongoDB 的文档模型与 AI 驱动的应用非常契合,这类应用经常交换层次化和半结构化数据。这种灵活性不仅支持带有城市引用的景点,还支持行程、用户偏好、推荐和对话历史。

Langchain4j 充当 AI 编排器,使用单一接口管理 AI 执行,类似于 Spring 或 Jakarta Data 中的仓库。它允许您轻松切换 AI 提供商,就像更改 JDBC 驱动程序一样。虽然数据库返回确定性结果,但 AI 系统生成概率性响应。在此示例中,我们使用 OpenAI 作为提供商,但您可以通过更新依赖项来更改提供商,无需修改代码。

对于用户界面,我们将结合 JSF 和 PrimeFaces 组件。这种方法使我们能够高效地使用现有组件,而不是开发新组件。

为简化执行,我们将包含 Eclipse GlassFish 插件。

以下代码显示了更新后的依赖项。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>expert.os.demos</groupId>
    <artifactId>travel-assistance</artifactId>
    <version>1.0.0</version>
    <packaging>war</packaging>

    <name>travel-assistance</name>
  
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.report.sourceEncoding>UTF-8</project.report.sourceEncoding>
        <maven.compiler.release>21</maven.compiler.release>
        <jakartaee-api.version>11.0.0</jakartaee-api.version>
        <compiler-plugin.version>3.15.0</compiler-plugin.version>
        <war-plugin.version>3.5.1</war-plugin.version>
        <jnosql.version>1.1.14</jnosql.version>
        <langchain4j-cdi.version>1.3.3</langchain4j-cdi.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>dev.langchain4j.cdi</groupId>
            <artifactId>langchain4j-cdi-portable-ext</artifactId>
            <version>${langchain4j-cdi.version}</version>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j.cdi.mp</groupId>
            <artifactId>langchain4j-cdi-config</artifactId>
            <version>${langchain4j-cdi.version}</version>
        </dependency>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>1.16.0</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jnosql.databases</groupId>
            <artifactId>jnosql-mongodb</artifactId>
            <version>${jnosql.version}</version>
        </dependency>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>${jakartaee-api.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.primefaces</groupId>
            <artifactId>primefaces</artifactId>
            <version>15.0.16</version>
            <classifier>jakarta</classifier>
        </dependency>
    </dependencies>

    <build>
        <finalName>travel-assistance</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler-plugin.version}</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>${war-plugin.version}</version>
            </plugin>
            <plugin>
                <groupId>org.glassfish.embedded</groupId>
                <artifactId>embedded-glassfish-maven-plugin</artifactId>
                <version>8.0</version>
                <configuration>
                    <app>target/travel-assistance.war</app>
                    <glassfish.version>8.0.3</glassfish.version>
                    <contextRoot>/</contextRoot>
                    <port>8080</port>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

将以下配置添加到位于 src/main/webapp/WEB-INF/web.xml 文件中,以启用 JSF。

<web-app version="6.1"
        xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd">
   <welcome-file-list>
       <welcome-file>index.xhtml</welcome-file>
   </welcome-file-list>
   <servlet>
       <servlet-name>Faces Servlet</servlet-name>
       <servlet-class>jakarta.faces.webapp.FacesServlet</servlet-class>
       <load-on-startup>1</load-on-startup>
   </servlet>

   <servlet-mapping>
       <servlet-name>Faces Servlet</servlet-name>
       <url-pattern>*.xhtml</url-pattern>
   </servlet-mapping>
</web-app>

在同一目录下,添加 beans.xml 文件以启用 CDI 支持

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
       bean-discovery-mode="annotated">
</beans>

下一步是在属性文件中配置凭据。设置 AI 模型类、OpenAI API 密钥和 MongoDB 连接字符串。您可以使用系统环境变量覆盖这些设置,遵循十二要素应用方法论。理想情况下,这些配置应对软件开发者透明,并且出于安全原因,敏感信息不应硬编码。因此,创建文件:src/main/resources/META-INF/microprofile-config.properties

dev.langchain4j.cdi.plugin.chat-model.class=dev.langchain4j.model.openai.OpenAiChatModel
dev.langchain4j.cdi.plugin.chat-model.config.api-key=${OPENAI_API_KEY}
dev.langchain4j.cdi.plugin.chat-model.config.model-name=gpt-5
jnosql.document.database=travels
jnosql.mongodb.url=mongodb+srv://admin:<db_password>@cluster0.gblhb3d.mongodb.net/?appName=devrel-article-java-jnosql
jnosql.mongodb.application.name=devrel-article-java-jnosql
```## 第二步:生成领域类

设置完成后,下一步是创建实体:`City` 及其景点。注解方式类似于 Jakarta Persistence(前身为 JPA),但每个属性必须使用 `Id` 或 `Column` 注解标记。整体结构保持相似。我们将定义两个实体和一个嵌入类。

第一个类是枚举,用于显示景点类型的选项:

```java
package expert.os.demos.travel.assistance;

public enum AttractionType {
    HISTORICAL,
    NATURE,
    MUSEUM,
    ARCHITECTURE,
    FOOD,
    RELIGIOUS
}

City 类是表示城市的实体。值得注意的是,该方法与 Jakarta Persistence 类似,Entity 注解将类标记为可持久化。IdColumn 注解指定哪些属性是可持久化的,以及它们是标识符还是标准字段。

$ java
package expert.os.demos.travel.assistance;

import jakarta.nosql.Column;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;

import java.util.UUID;

@Entity
public class City {

    @Id
    private UUID id;

    @Column
    private String name;

    @Column
    private String country;

    @Column
    private String description;

    City() {
    }

    public City(UUID id, String name, String country, String description) {
        this.id = id;
        this.name = name;
        this.country = country;
        this.description = description;
    }

    public UUID getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getCountry() {
        return country;
    }

    public String getDescription() {
        return description;
    }
}

CityReference 作为一种可嵌入类型,由同名注解定义。由于它可以在 DDD 中被定义为值类型,我们将它定义为一个具有两个属性的不可变类。该信息将作为对城市的引用,分组在 Attraction 内部。

$ java
package expert.os.demos.travel.assistance;

import jakarta.nosql.Column;
import jakarta.nosql.Embeddable;

import java.util.UUID;

@Embeddable(Embeddable.EmbeddableType.GROUPING)
public record CityReference(@Column UUID id, @Column String name) {
}

Attraction 使用与 City 相同的结构。主要区别在于属性之外,还包含一个嵌入式分组,作为 Attraction 内部的子文档。

$ java
package expert.os.demos.travel.assistance;

import jakarta.nosql.Column;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;

import java.util.UUID;

@Entity
public class Attraction {

    @Id
    private UUID id;

    @Column
    private CityReference city;

    @Column
    private String name;

    @Column
    private AttractionType type;

    @Column
    private String description;

    Attraction() {
    }

     public Attraction(UUID id, CityReference city, String name, AttractionType type, String description) {
        this.id = id;
        this.city = city;
        this.name = name;
        this.type = type;
        this.description = description;
    }

    public UUID getId() {
        return id;
    }

    public CityReference getCity() {
        return city;
    }

    public String getName() {
        return name;
    }

    public AttractionType getType() {
        return type;
    }

    public String getDescription() {
        return description;
    }
}

实体完成后,下一步是建立 Java 和 MongoDB 类之间的连接。我们将使用 Jakarta Data 和 Jakarta NoSQL,以利用 Jakarta EE 中基于接口的能力。

$ java
package expert.os.demos.travel.assistance;

import jakarta.data.repository.BasicRepository;
import jakarta.data.repository.Repository;

import java.util.List;
import java.util.UUID;

@Repository
public interface CityRepository extends BasicRepository<City, UUID> {

    List<City> findByCountry(String country);
}

Jakarta Data 提供了多种探索数据能力的方式。在我们的场景中,我们使用 BasicRepository 定义了仓库,它提供了多种数据库操作。你可以通过方法名(如 findByCountry)按特定字段查询数据。此外,还可以使用 Query 注解访问 Jakarta Queries 的功能。

$ java
package expert.os.demos.travel.assistance;

import jakarta.data.repository.BasicRepository;
import jakarta.data.repository.Param;
import jakarta.data.repository.Query;
import jakarta.data.repository.Repository;

import java.util.List;
import java.util.UUID;

@Repository
public interface AttractionRepository extends BasicRepository<Attraction, UUID> {

    @Query("WHERE city.name = :name")
    List<Attraction> findByCityName(@Param("name") String name);

    @Query("WHERE city.name = :name AND type = :type")
    List<Attraction> findByCityNameAndType(@Param("name") String city, @Param("type") AttractionType type);
}

Java 与数据层之间的集成已完成,得益于 Jakarta EE 平台,整个过程非常直接。现在,我们将通过实现三个服务来添加功能:景点服务、城市服务以及一个设置服务,用于用初始数据填充数据库。虽然你后续可能会考虑添加用于数据输入的用户界面表单,但本教程不涵盖此功能。

服务类将充当协调者并管理仓库。在下面的示例中,CityService 将处理与 CityRepository 相关的操作。

$ java
package expert.os.demos.travel.assistance;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;

@ApplicationScoped
public class CityService {


    private final CityRepository cityRepository;

    @Inject
    public CityService(CityRepository cityRepository) {
        this.cityRepository = cityRepository;
    }

     CityService() {
        this.cityRepository = null;
    }

    public List<City> findByCountry(String country) {
        return cityRepository.findByCountry(country);
    }

    public List<City> findAll() {
        return cityRepository.findAll().toList();
    }

    public City save(City city) {
        this.cityRepository.save(city);
        return city;
    }
}

AttractionService 将与 CityService 类似,并管理所有与景点相关的操作。

$ java
package expert.os.demos.travel.assistance;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;
import java.util.UUID;

@ApplicationScoped
public class AttractionService {


    private final AttractionRepository attractionRepository;

    @Inject
    public AttractionService(AttractionRepository attractionRepository) {
        this.attractionRepository = attractionRepository;
    }

        AttractionService() {
            this.attractionRepository = null;
        }

    public List<Attraction> findByCity(String name) {
        return attractionRepository.findByCityName(name);
    }

    public Attraction save(Attraction attraction) {
        return attractionRepository.save(attraction);
    }

    public List<Attraction> findByType(String city, AttractionType type) {
        return attractionRepository.findByCityNameAndType(city, type);
    }
}

数据加载器为我们的旅行社生成信息。在真实场景中,这可以通过表单或集成第三方服务(例如 REST API)来实现。在我们的示例中,为简单起见,数据加载器会检查数据库是否为空,然后基于三个城市及其各自的景点创建数据。

$ java
package expert.os.demos.travel.assistance;


import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.UUID;
import java.util.logging.Logger;

@ApplicationScoped
public class DataLoader {

    private static final Logger LOGGER = Logger.getLogger(DataLoader.class.getName());
    @Inject
    private CityService cityService;

    @Inject
    private AttractionService attractionService;

    @PostConstruct
    public void load() {

        LOGGER.info("Loading sample travel data");

        if (!cityService.findAll().isEmpty()) {
            LOGGER.info("Sample travel data already loaded");
            return;
        }

        City lisbon = cityService.save(new City(
                UUID.randomUUID(),
                "Lisbon",
                "Portugal",
                "Portugal's capital city."
        ));

        City porto = cityService.save(new City(
                UUID.randomUUID(),
                "Porto",
                "Portugal",
                "Historic city in northern Portugal."
        ));

        City paris = cityService.save(new City(
                UUID.randomUUID(),
                "Paris",
                "France",
                "Capital of France."
        ));

        City rome = cityService.save(new City(
                UUID.randomUUID(),
                "Rome",
                "Italy",
                "The Eternal City."
        ));

        loadAttractions(lisbon, porto, paris, rome);
        LOGGER.info("Sample travel data loaded successfully");

    }

    private void loadAttractions(
            City lisbon,
            City porto,
            City paris,
            City rome) {

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(lisbon.getId(), lisbon.getName()),
                "Belém Tower",
                AttractionType.HISTORICAL,
                "UNESCO World Heritage Site."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(lisbon.getId(), lisbon.getName()),
                "Jerónimos Monastery",
                AttractionType.RELIGIOUS,
                "Manueline monastery."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(lisbon.getId(), lisbon.getName()),
                "Alfama",
                AttractionType.ARCHITECTURE,
                "Historic neighborhood."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(porto.getId(), porto.getName()),
                "Ribeira",
                AttractionType.ARCHITECTURE,
                "Riverside district."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(porto.getId(), porto.getName()),
                "Livraria Lello",
                AttractionType.MUSEUM,
                "Historic bookstore."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(porto.getId(), porto.getName()),
                "Port Wine Cellars",
                AttractionType.FOOD,
                "Wine tasting experience."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(paris.getId(), paris.getName()),
                "Eiffel Tower",
                AttractionType.ARCHITECTURE,
                "Paris landmark."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(paris.getId(), paris.getName()),
                "Louvre Museum",
                AttractionType.MUSEUM,
                "World-famous museum."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(paris.getId(), paris.getName()),
                "Notre-Dame Cathedral",
                AttractionType.RELIGIOUS,
                "Gothic cathedral."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(rome.getId(), rome.getName()),
                "Colosseum",
                AttractionType.HISTORICAL,
                "Ancient amphitheater."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(rome.getId(), rome.getName()),
                "Roman Forum",
                AttractionType.HISTORICAL,
                "Center of ancient Rome."
        ));

        attractionService.save(new Attraction(
                UUID.randomUUID(),
                new CityReference(rome.getId(), rome.getName()),
                "Vatican Museums",
                AttractionType.MUSEUM,
                "Art and history collections."
        ));
    }
}
```## 第三步:定义 MongoDB 集成上的 AI
所有服务就绪后,下一步是通过工具将这些服务暴露出来,使用 [Tool 注解](https://docs.langchain4j.dev/tutorials/tools/) 定义语言模型可调用的函数。工具类应尽可能为这些服务提供丰富的上下文。Langchain4j 会自动处理响应,响应可能以 JSON 形式返回并传递给语言模型。

工具类的目的是避免在服务内部直接使用 `Tool` 注解。这些工具仅用于将服务暴露给 AI 系统。`Tool` 注解指定了输入和输出细节,lang4chain 按约定自动处理,例如将返回值转换为 JSON。`AttractionTools` 将使用 `Tool` 注解来暴露和描述服务的可用性。`ApplicationScoped` 注解定义了类的生命周期,确保它随应用程序一直存在。

```java
package expert.os.demos.travel.assistance.ai;

import dev.langchain4j.agent.tool.Tool;
import expert.os.demos.travel.assistance.Attraction;
import expert.os.demos.travel.assistance.AttractionService;
import expert.os.demos.travel.assistance.AttractionType;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;
import java.util.logging.Logger;

@ApplicationScoped
public class AttractionTools {

    private static final Logger LOGGER = Logger.getLogger(AttractionTools.class.getName());
    @Inject
    private AttractionService service;

    @Tool("""
            Find attractions available in a city.
            Use this tool when you need to discover places to visit in a destination.
            """)
    public List<Attraction> attractionsByCity(String city) {

        LOGGER.info(() -> "[TOOL] attractionsByCity(city=%s)".formatted(city));

        List<Attraction> attractions = service.findByCity(city);

        LOGGER.info(() -> "[TOOL] attractionsByCity returned %d attraction(s)".formatted(attractions.size()));

        return attractions;
    }

    @Tool("""
            Find attractions by category in a city.
            Categories include HISTORICAL, NATURE, MUSEUM, ARCHITECTURE, FOOD, and RELIGIOUS.
            Use this tool when the traveler has specific interests or preferences.
            """)
    public List<Attraction> attractionsByType(
            String city,
            AttractionType type) {

        LOGGER.info(() -> "[TOOL] attractionsByType(city=%s, type=%s)".formatted(city, type));

        List<Attraction> attractions =
                service.findByType(city, type);

        LOGGER.info(() -> "[TOOL] attractionsByType returned %d attraction(s)".formatted(attractions.size()));

        return attractions;
    }

    @Tool("""
            List all available attraction categories.
            Use this tool when you need to discover which attraction types can be used to build an itinerary.
            """)
    public AttractionType[] attractionTypes() {

        LOGGER.info("[TOOL] attractionTypes()");

        AttractionType[] values = AttractionType.values();

        LOGGER.info(() -> "[TOOL] attractionTypes returned %d type(s)".formatted(values.length));

        return values;
    }
}

CityTools 的结构和功能相似,但它暴露和描述的是可供 AI 使用的服务。这样我们就能识别出 AI 可以调用的城市服务。

$ java
package expert.os.demos.travel.assistance.ai;

import dev.langchain4j.agent.tool.Tool;
import expert.os.demos.travel.assistance.City;
import expert.os.demos.travel.assistance.CityService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;
import java.util.logging.Logger;

@ApplicationScoped
public class CityTools {

    private static final Logger LOGGER = Logger.getLogger(CityTools.class.getName());

    @Inject
    private CityService service;


    @Tool("""
            Find cities available in a country.
            Use this tool when you need to discover destinations before creating a travel itinerary.
            """)
    public List<City> citiesByCountry(String country) {

        LOGGER.info(() -> "[TOOL] citiesByCountry country=%s"
                .formatted(country));

        List<City> cities = service.findByCountry(country);

        LOGGER.info(() -> "[TOOL] citiesByCountry resultCount=%d cities=%s"
                .formatted(
                        cities.size(),
                        cities.stream()
                                .map(City::getName)
                                .toList()
                ));

        return cities;
    }

    @Tool("""
            List all available cities.
            Use this tool when you need to explore destinations without any country restriction.
            """)
    public List<City> cities() {

        List<City> cities = service.findAll();
        LOGGER.info(() -> "[TOOL] cities resultCount=%d cities=%s"
                .formatted(
                        cities.size(),
                        cities.stream()
                                .map(City::getName)
                                .toList()
                ));

        return cities;
    }
}

工具集就绪后,下一步是注册所有充当 Java 与 AI 桥梁的 TravelService 组件。这个过程类似于 Jakarta Data 和 Spring Data 的做法,配置基于接口和少量注解即可完成。

我们使用 RegisterAIService 注解将此接口标记为 AI 服务,类似于 Jakarta Data 中的 Repository 注解。同时我们指定了可用工具。该接口中的唯一方法使用 SystemMessage 注解定义提示命令,描述了 JSF 要渲染的 HTML 结构。

旅行服务提供了一个与 AI 通信的接口。通过使用注解,我们指定该接口将由 Langchain 实现,并且其工具将通过 RegisterAIService 注解注册。最后我们定义了一个方法操作,其中提示内容详细写在 SystemMessage 注解中。

$ java
package expert.os.demos.travel.assistance.ai;

import dev.langchain4j.cdi.spi.RegisterAIService;
import dev.langchain4j.service.SystemMessage;
import jakarta.enterprise.context.ApplicationScoped;

@RegisterAIService(
        tools = {
                CityTools.class,
                AttractionTools.class
        }
)
@ApplicationScoped
public interface TravelService {

    @SystemMessage("""
            You are a travel assistant powered by a travel database.
            
            Rules:
            
            - Always use the available tools before answering.
            - Never ask follow-up questions.
            - Never ask for clarification.
            - Never request additional information.
            - Use only cities and attractions returned by the tools.
            - Never invent cities or attractions.
            - Keep responses short and direct.
            - When creating itineraries, select destinations from the available data and generate the itinerary immediately.
            - If information is unavailable, say so briefly.
            Return only valid HTML.
            
            Example:
            
            <h2>Historical Tour in Portugal</h2>
            
            <h3>Cities</h3>
            <ul>
              <li>Lisbon</li>
              <li>Porto</li>
            </ul>
            
            <h3>Attractions</h3>
            <ul>
              <li>Belém Tower</li>
              <li>Jerónimos Monastery</li>
            </ul>
            
            <p>Perfect for travelers interested in Portuguese history.</p>
            """)
    String chat(String userMessage);
}

第四步:通过 UI 展示结果

所有服务和工具就位后,下一步是呈现它们并允许用户交互。我们将使用 Jakarta Faces,它简化了开发,适合没有丰富前端经验的开发者。TravelBean 类将以 HTML 形式展示信息。通过其属性与 Jakarta Expression Language 结合,我们将暴露 getter 和 setter,用于在网页上作为输入和输出。这里我们还将作用域定义为 View

$ java
package expert.os.demos.travel.assistance.web;


import expert.os.demos.travel.assistance.DataLoader;
import expert.os.demos.travel.assistance.ai.TravelService;
import jakarta.annotation.PostConstruct;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import java.io.Serializable;

@Named
@ViewScoped
public class TravelBean implements Serializable {

    @Inject
    private TravelService travelService;

    @Inject
    private DataLoader dataLoader;

    private String userMessage;

    private String answer;

    @PostConstruct
    public void init() {
        SSLBypass.disableSslVerification();
        dataLoader.load();
    }

    public void send() {
        if (userMessage == null || userMessage.isBlank()) {
            return;
        }
        answer = travelService.chat(userMessage);
    }

    public String getUserMessage() {
        return userMessage;
    }

    public void setUserMessage(String userMessage) {
        this.userMessage = userMessage;
    }

    public String getAnswer() {
        return answer;
    }

    public void availableCities() {
        this.userMessage = "Show me available cities to travel";
    }

    public void historicalTour() {
        this.userMessage = "Create a historical itinerary in Portugal";
    }

    public void museumWeekend() {
        this.userMessage = "Create a museum-focused trip in Europe";
    }

    public void foodAndCulture() {
        this.userMessage = "Create a food and culture itinerary";
    }

}

如果你在没有证书的本地环境中运行示例,则需要使用 SSL 绕过。请注意,这种方法不建议用于生产环境。

$ java
package expert.os.demos.travel.assistance.web;

import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public final class SSLBypass {

    public static void disableSslVerification() {

        try {

            TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {

                        @Override
                        public void checkClientTrusted(
                                X509Certificate[] chain,
                                String authType) {
                        }

                        @Override
                        public void checkServerTrusted(
                                X509Certificate[] chain,
                                String authType) {
                        }

                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[0];
                        }
                    }
            };

            SSLContext sslContext = SSLContext.getInstance("TLS");

            sslContext.init(
                    null,
                    trustAllCerts,
                    new SecureRandom()
            );

            SSLContext.setDefault(sslContext);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private SSLBypass() {
    }
}

注意:请勿在生产环境中使用此类,仅用于本地测试。

XHTML 页面将以 HTML 形式渲染信息。借助 Jakarta Faces,前端开发效率更高,因为组件通过简单的 Java 方法管理大部分操作。我们将 index.html 文件放在 src/main/webapp/index.html 中,并删除已有的 index.html

$ markup
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="jakarta.faces.html"
      xmlns:f="jakarta.faces.core"
      xmlns:p="primefaces">

<h:head>
    <title>Travel Planner AI</title>
    <h:outputStylesheet library="css" name="travel.css"/>
</h:head>

<h:body>

    <div class="hero">
        <div class="hero-title">
            ✈️ Travel Planner AI
        </div>

        <div class="hero-subtitle">
            Explore cities, attractions, and build personalized travel itineraries
            using AI, MongoDB, Jakarta Data, and the Planning Pattern.
        </div>
    </div>

    <div class="main-container">

        <h:form>

            <p:card styleClass="chat-card">

                <f:facet name="title">
                    AI Travel Assistant
                </f:facet>

                <f:facet name="subtitle">
                    Ask anything about destinations, attractions, or travel plans
                </f:facet>

                <p:inputTextarea
                        id="prompt"
                        value="#{travelBean.userMessage}"
                        rows="6"
                        autoResize="false"
                        styleClass="prompt-box"
                        placeholder="Example: Create a two-day historical itinerary in Portugal"/>

                <p:spacer height="15"/>

                <p:commandButton
                        value="Generate Itinerary"
                        icon="pi pi-send"
                        action="#{travelBean.send}"
                        update="answer"
                        styleClass="ui-button-success"/>

            </p:card>

            <div class="examples">

                <p:panel header="Suggested Prompts">

                    <div class="grid">

                        <div class="col-12 md:col-3">
                            <p:card styleClass="example-card">

                                <p:commandButton
                                        value="🌍 Available Cities"
                                        action="#{travelBean.availableCities}"
                                        update="prompt"
                                        process="@this"
                                        styleClass="ui-button-flat example-button"/>

                            </p:card>
                        </div>

                        <div class="col-12 md:col-3">
                            <p:card styleClass="example-card">

                                <p:commandButton
                                        value="🏛️ Historical Tour"
                                        action="#{travelBean.historicalTour}"
                                        update="prompt"
                                        process="@this"
                                        styleClass="ui-button-flat example-button"/>

                            </p:card>
                        </div>

                        <div class="col-12 md:col-3">
                            <p:card styleClass="example-card">

                                <p:commandButton
                                        value="🎨 Museum Weekend"
                                        action="#{travelBean.museumWeekend}"
                                        update="prompt"
                                        process="@this"
                                        styleClass="ui-button-flat example-button"/>

                            </p:card>
                        </div>

                        <div class="col-12 md:col-3">
                            <p:card styleClass="example-card">

                                <p:commandButton
                                        value="🍷 Food &amp; Culture"
                                        action="#{travelBean.foodAndCulture}"
                                        update="prompt"
                                        process="@this"
                                        styleClass="ui-button-flat example-button"/>

                            </p:card>
                        </div>

                    </div>

                </p:panel>

            </div>

            <h:panelGroup
                    id="answer"
                    layout="block"
                    styleClass="answer-container">

                <div class="answer-header">
                    <i class="pi pi-sparkles"></i>
                    <span>AI Travel Recommendation</span>
                </div>

                <div class="answer-content">

                    <h:panelGroup rendered="#{empty travelBean.answer}">
                        <div class="empty-answer">
                            Ask a question or select one of the suggested prompts to generate an itinerary.
                        </div>
                    </h:panelGroup>

                    <h:panelGroup rendered="#{not empty travelBean.answer}">
                        <h:outputText
                                value="#{travelBean.answer}"
                                escape="false"/>
                    </h:panelGroup>

                </div>

            </h:panelGroup>

        </h:form>

        <div class="footer">
            Powered by Jakarta EE, PrimeFaces, MongoDB, Jakarta NoSQL, Jakarta Data and AI Planning
        </div>

    </div>

</h:body>
</html>

最后是 CSS 美化,我们在 src/main/webapp/resources/css/travel.css 中创建:

$ style
body {
    margin: 0;
    background: #f5f7fa;
    font-family: Inter, Arial, sans-serif;
}

.hero {
    text-align: center;
    padding: 40px;
    background: linear-gradient(135deg, #0f172a, #1e3a8a);
    color: white;
}

.hero-title {
    font-size: 2.5rem;
    font-weight: bold;
    margin-bottom: 10px;
}

.hero-subtitle {
    opacity: 0.9;
    font-size: 1.1rem;
}

.main-container {
    max-width: 1100px;
    margin: 30px auto;
    padding: 0 20px;
}

.chat-card {
    border-radius: 20px !important;
}

.prompt-box {
    width: 100%;
}

.example-button {
    width: 100%;
    height: 4rem;
    font-weight: 600;
}

.answer-container {
    margin-top: 25px;
    padding: 25px;
    border-radius: 16px;
    background: white;
    min-height: 150px;
    box-shadow: 0 4px 20px rgba(0,0,0,.08);
    line-height: 1.7;
}

.examples {
    margin-top: 25px;
}

.example-card {
    text-align: center;
    cursor: pointer;
    transition: .2s;
}

.example-card:hover {
    transform: translateY(-4px);
}

.footer {
    text-align: center;
    margin-top: 40px;
    color: #64748b;
    font-size: .9rem;
}

.answer-container {
    margin-top: 25px;
    background: white;
    border-radius: 16px;
    box-shadow: 0 4px 20px rgba(0,0,0,.08);
    overflow: hidden;
}

.answer-header {
    background: #f8fafc;
    border-bottom: 1px solid #e2e8f0;
    padding: 16px 24px;
    font-size: 1rem;
    font-weight: 600;
    color: #0f172a;

    display: flex;
    align-items: center;
    gap: .75rem;
}

.answer-content {
    padding: 24px;
    min-height: 180px;
    line-height: 1.8;
    color: #334155;
}

.empty-answer {
    color: #94a3b8;
    text-align: center;
    padding: 40px;
    font-style: italic;
}

第五步:运行应用程序

要完成整个过程,请打包应用程序并使用内嵌的 GlassFish 插件启动:

mvn clean install && mvn embedded-glassfish:run

然后,在浏览器中打开应用程序:

http://localhost:8080/
```## 结论

本教程介绍了规划模式(Planning Pattern)这一将人工智能集成到企业应用中的实用方法。该模式不要求语言模型一步解决复杂问题,而是将目标拆解为可由确定性工具执行的更小操作。通过让AI推理目标的同时,将数据访问和业务操作委托给传统软件组件,这种方法提升了可靠性、可透明度和可维护性。随着AI越来越深入地嵌入到业务工作流中,规划之类的模式提供了一种结构化方法,在自主性和控制力之间取得平衡。

为了演示这些概念,我们使用 Jakarta EE、LangChain4j、MongoDB、Jakarta Data 和 Jakarta NoSQL 开发了一个旅行行程助手。我们建模了一个简单的领域,将数据存储在 MongoDB 中,通过工具公开应用能力,并使 AI 代理能够基于真实数据(而非仅训练数据)生成推荐。这个示例展示了 AI 代理如何与企业系统协作,将动态推理与现代软件架构所需的可预测性和治理能力结合在一起。

准备好探索 MongoDB Atlas 的优势了吗?立即[试用 MongoDB Atlas](https://www.mongodb.com/lp/cloud/atlas/try4-reg?utm_campaign=devrel&utm_source=third-party-content&utm_medium=cta&utm_content=data_driven_test_dev&utm_term=otavio.santana) 开始吧。

[访问本教程使用的源代码](https://github.com/soujava/mongodb-ai-planning-pattern)

有任何问题?欢迎来到 [MongoDB 社区论坛](https://www.mongodb.com/community/forums/?utm_campaign=devrel&utm_source=third-party-content&utm_medium=cta&utm_content=data_driven_test_dev&utm_term=otavio.santana) 与我们交流。

**参考资料**:

* [源代码](https://github.com/soujava/mongodb-ai-planning-pattern)

本文最初发表于 [foojay](https://foojay.io),文章标题为 [《使用 MongoDB 构建 AI 系统:实现规划模式》](https://foojay.io/today/building-ai-systems-with-mongodb-implementing-the-planning-pattern/)。