Building an AI Chatbot in Java With Langchain4j and MongoDB Atlas | Baeldung

Chatbot systems enhance the user experience by providing quick and intelligent responses, making interactions more efficient.

聊天机器人系统通过提供快速智能的响应来增强用户体验,使交互更加高效。

In this tutorial, we’ll walk through the process of building a chatbot using Langchain4j and MongoDB Atlas.

在本教程中,我们将逐步介绍如何使用Langchain4j和MongoDB Atlas构建一个聊天机器人。

LangChain4j is a Java library inspired by LangChain, designed to help build AI-powered applications using LLMs. We use it to develop applications such as chatbots, summarization engines, or intelligent search systems.

LangChain4j 是一个受LangChain启发的Java库,旨在帮助开发者利用大型语言模型(LLM)构建AI驱动的应用程序。我们用它来开发诸如聊天机器人、摘要生成引擎或智能搜索系统等应用。

We’ll use MongoDB Atlas Vector Search to give our chatbot the ability to retrieve relevant information based on meaning, not just keywords. Traditional keyword-based search methods rely on exact word matches, often leading to irrelevant results when users phrase their questions differently or use synonyms.

我们将使用MongoDB Atlas向量搜索技术,使聊天机器人能够根据语义而不仅仅是关键词来检索相关信息。传统的基于关键词的搜索方法依赖于精确的词语匹配,当用户以不同方式表述问题或使用同义词时,往往会导致不相关的结果。

By using vector stores and vector search, our app compares the meaning of the user’s query to the stored content by mapping both into a high-dimensional vector space. This allows the chatbot to understand and respond to complex, natural language questions with greater contextual accuracy, even if the exact words don’t appear in the source content. As a result, we achieve more context-aware results.

通过使用向量存储和向量搜索技术,我们的应用程序将用户查询的含义与存储内容进行对比,方法是把两者映射到一个高维向量空间。这使得聊天机器人能够更准确地理解并回答复杂的自然语言问题,即使原文中没有出现完全相同的词汇。最终,我们实现了更具上下文感知能力的搜索结果。

Our application interacts with the chatbot using HTTP endpoints. It has two flows: a document loading flow and a Chatbot flow.

我们的应用程序通过HTTP端点与聊天机器人交互,包含两个流程:文档加载流程和聊天机器人流程。

For the document loading flow, we’ll take a dataset of articles. Then, we’ll generate vector embeddings using an embedding model. Finally, we’ll save the embeddings alongside our data in MongoDB. These embeddings represent the semantic content of the articles, enabling efficient similarity search.

在文档加载流程中,我们将获取一个文章数据集。然后,使用嵌入模型生成向量嵌入。最后,我们将这些嵌入与数据一起保存到MongoDB中。这些嵌入代表了文章的语义内容,从而实现高效的相似性搜索。

For the chatbot flow, we’ll perform a similarity search in our MongoDB instance based on user input to retrieve the most relevant documents. After this, we’ll use the retrieved articles as context for the LLM prompt and generate the chatbot’s response based on the LLM output.

对于聊天机器人流程,我们将在MongoDB实例中基于用户输入执行相似性搜索,以检索最相关的文档。之后,我们将把检索到的文章作为大语言模型提示的上下文,并根据大语言模型的输出来生成聊天机器人的回复。

Dependencies and Configuration 

依赖项与配置

<dependency> 
    <groupId>org.springframework.boot</groupId>         
    <artifactId>spring-boot-starter-web</artifactId> 
    <version>3.3.2</version> 
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-mongodb-atlas</artifactId>
    <version>1.0.0-beta1</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.0.0-beta1</version>
</dependency>

配置

app.mongodb.url=mongodb://chatbot:password@localhost:27017/admin
app.mongodb.db-name=chatbot_db
app.openai.apiKey=${OPENAI_API_KEY}

Open AI key :https://platform.openai.com/api-keys ,获取。

create a ChatBotConfiguration class

@Configuration
public class ChatBotConfiguration {

    @Value("${app.mongodb.url}")
    private String mongodbUrl;

    @Value("${app.mongodb.db-name}")
    private String databaseName;

    @Value("${app.openai.apiKey}")
    private String apiKey;


    @Bean
    public MongoClient mongoClient() {
        return MongoClients.create(mongodbUrl);
    }

    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(MongoClient mongoClient) {
        String collectionName = "embeddings";
        String indexName = "embedding";
        Long maxResultRatio = 10L;
        CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions();
        Bson filter = null;
        IndexMapping indexMapping = IndexMapping.builder()
          .dimension(TEXT_EMBEDDING_3_SMALL.dimension())
          .metadataFieldNames(new HashSet<>())
          .build();
        Boolean createIndex = true;

        return new MongoDbEmbeddingStore(
          mongoClient,
          databaseName,
          collectionName,
          indexName,
          maxResultRatio,
          createCollectionOptions,
          filter,
          indexMapping,
          createIndex
        );
    }

    @Bean
    public EmbeddingModel embeddingModel() {
        return OpenAiEmbeddingModel.builder()
          .apiKey(apiKey)
          .modelName(TEXT_EMBEDDING_3_SMALL)
          .build();
    }
}

We’ve built the EmbeddingModel using the OpenAI text-embedding-3-small model. Of course, we can choose another embedding model that meets our needs. Then, we create a MongoDbEmbeddingStore bean. The store is backed by a MongoDB Atlas collection where embeddings will be saved and indexed for fast semantic retrieval. Next, we set the dimension to the default text-embedding-3-small value. Using the EmbeddingModel, we need to be sure the dimension of the created vector matches the mentioned model.

我们基于OpenAI的text-embedding-3-small模型构建了EmbeddingModel。当然,我们也可以根据需要选择其他嵌入模型。接着,我们创建了一个MongoDbEmbeddingStore组件。该存储后端由MongoDB Atlas集合支持,嵌入向量将被保存并建立索引以实现快速语义检索。随后我们将维度设置为text-embedding-3-small模型的默认值。使用EmbeddingModel时,必须确保生成的向量维度与指定模型相匹配。

Load Documentation Data to Vector Store

加载文档数据到向量存储

app.load-articles=true

We’ll use the MongoDB articles as our ChatBot data. For demonstration purposes, we can manually download the dataset with the articles from Hugging Face. Next, we’ll save this dataset as an articles.json file in the resources folder.  

We want to ingest these articles during application startup by converting them into vector embeddings and storing them in our MongoDB Atlas vector store.

我们将使用MongoDB文章作为ChatBot的数据源。出于演示目的,我们可以手动从Hugging Face下载包含这些文章的数据集。接着,我们会把这个数据集保存为resources文件夹中的articles.json文件。

我们希望通过在应用启动时将这些文章转换为向量嵌入,并将它们存储到MongoDB Atlas向量存储中,来实现对这些文章的摄取。

Now, let’s add the property to the application.properties file. We’ll use it to control whether the data load is needed:

ArticlesRepository

文章仓库

@Component
public class ArticlesRepository {
    private static final Logger log = LoggerFactory.getLogger(ArticlesRepository.class);

    private final EmbeddingStore<TextSegment> embeddingStore;
    private final EmbeddingModel embeddingModel;
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    public ArticlesRepository(@Value("${app.load-articles}") Boolean shouldLoadArticles, 
      EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) throws IOException {
        this.embeddingStore = embeddingStore;
        this.embeddingModel = embeddingModel;
        
        if (shouldLoadArticles) {
            loadArticles();
        }
    }
}

private void loadArticles() throws IOException {
    String resourcePath = "articles.json";
    int maxTokensPerChunk = 8000;
    int overlapTokens = 800;

    List<TextSegment> documents = loadJsonDocuments(resourcePath, maxTokensPerChunk, overlapTokens);

    log.info("Documents to store: " + documents.size());

    for (TextSegment document : documents) {
        Embedding embedding = embeddingModel.embed(document.text()).content();
        embeddingStore.add(embedding, document);
    }

    log.info("Documents are uploaded");
}

Here we use a loadJsonDocuments() method to load the data stored in the resource folder. We create a collection of TextSegment instances. For each TextSegment, we create an embedding and store it in the vector store. We’ll use the maxTokensPerChunk variable to specify the maximum number of tokens that will be present in the chunk of the vector store document. This value should be lower than the model dimension. Also, we use overlapTokens to indicate how many tokens may overlap between the text segments. This helps us preserve the context between segments.

这里我们使用loadJsonDocuments()方法来加载资源文件夹中存储的数据。我们创建一个TextSegment实例集合。对于每个TextSegment,我们创建一个嵌入并将其存储在向量存储中。我们将使用maxTokensPerChunk变量来指定向量存储文档块中存在的最大token数。这个值应该小于模型的维度。此外,我们使用overlapTokens来指示文本段之间可能重叠的token数量。这有助于我们保留段之间的上下文。

loadJsonDocuments() Implementation

private List<TextSegment> loadJsonDocuments(String resourcePath, int maxTokensPerChunk, int overlapTokens) throws IOException {

    InputStream inputStream = ArticlesRepository.class.getClassLoader().getResourceAsStream(resourcePath);

    if (inputStream == null) {
        throw new FileNotFoundException("Resource not found: " + resourcePath);
    }

    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

    int batchSize = 500;
    List<Document> batch = new ArrayList<>();
    List<TextSegment> textSegments = new ArrayList<>();

    String line;
    while ((line = reader.readLine()) != null) {
        JsonNode jsonNode = objectMapper.readTree(line);

        String title = jsonNode.path("title").asText(null);
        String body = jsonNode.path("body").asText(null);
        JsonNode metadataNode = jsonNode.path("metadata");

        if (body != null) {
            addDocumentToBatch(title, body, metadataNode, batch);

            if (batch.size() >= batchSize) {
                textSegments.addAll(splitIntoChunks(batch, maxTokensPerChunk, overlapTokens));
                batch.clear();
            }
        }
    }

    if (!batch.isEmpty()) {
        textSegments.addAll(splitIntoChunks(batch, maxTokensPerChunk, overlapTokens));
    }

    return textSegments;
}

Here, we parse the JSON file and iterate over each item. Then, we add the article title, body, and metadata as a document to the batch. Once the batch reaches 500 entries (or at the end), we process it using splitIntoChunks() to break the content into manageable text segments.  The method returns a complete list of TextSegment objects, ready for embedding and storage.

Let’s implement the addDocumentToBatch() method:

private void addDocumentToBatch(String title, String body, JsonNode metadataNode, List<Document> batch) {
    String text = (title != null ? title + "\n\n" + body : body);

    Metadata metadata = new Metadata();
    if (metadataNode != null && metadataNode.isObject()) {
        Iterator<String> fieldNames = metadataNode.fieldNames();
        while (fieldNames.hasNext()) {
            String fieldName = fieldNames.next();
            metadata.put(fieldName, metadataNode.path(fieldName).asText());
        }
    }

    Document document = Document.from(text, metadata);
    batch.add(document);
}

The article’s title and body are concatenated into a single text block. If we have metadata present, we parse the fields and add them to a Metadata object. The combined text and metadata are wrapped in a Document object, which we add to the current batch.

splitIntoChunks() Implementation and Getting the Upload Results

splitIntoChunks() 的实现及获取上传结果

Once we’ve assembled our articles as Document objects, with both content and metadata, the next step is to split them into smaller, token-aware chunks that are compatible with our embedding model’s limits.  Finally, let’s see what splitIntoChunks() looks like:

private List<TextSegment> splitIntoChunks(List<Document> documents, int maxTokensPerChunk, int overlapTokens) {
    OpenAiTokenizer tokenizer = new OpenAiTokenizer(OpenAiEmbeddingModelName.TEXT_EMBEDDING_3_SMALL);

    DocumentSplitter splitter = DocumentSplitters.recursive(
            maxTokensPerChunk,
            overlapTokens,
            tokenizer
    );

    List<TextSegment> allSegments = new ArrayList<>();
    for (Document document : documents) {
        List<TextSegment> segments = splitter.split(document);
        allSegments.addAll(segments);
    }

    return allSegments;
}

This process is important because most embedding models have token limits. This means only a certain amount of data can be embedded into vectors at once. Chunking allows us to obey these limits, while the overlap helps us maintain continuity between segments. This is especially important for paragraph-based content.

We’ve used only a part of the entire dataset for our demo purposes. Uploading the entire dataset may take some time and require more credits.

这一过程很重要,因为大多数嵌入模型都存在令牌限制。这意味着每次只能将一定量的数据嵌入为向量。通过分块处理,我们既能遵守这些限制,同时重叠部分有助于保持段落之间的连续性——这对于基于段落的内容尤为重要。

出于演示目的,我们仅使用了整个数据集的一部分。上传完整数据集可能需要较长时间并消耗更多积分。

Chatbot API
Now, let’s implement the Chatbot API flow (our chatbot interface). Here, we’ll create a few beans that retrieve the documents from the vector store and communicate with the LLM, creating context-aware responses. Finally, we’ll build the Chatbot API and verify how it works.

现在,让我们实现聊天机器人API流程(我们的聊天机器人界面)。在此过程中,我们将创建几个bean来从向量存储中检索文档并与大语言模型通信,从而生成上下文感知的响应。最后,我们将构建聊天机器人API并验证其运行效果。

ArticleBasedAssistant Implementation
We’ll start by creating the ContentRetriever bean, to perform a vector search in MongoDB Atlas using the user’s input:

我们将首先创建ContentRetriever bean,用于根据用户输入在MongoDB Atlas中执行向量搜索:

@Bean
public ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore, EmbeddingModel embeddingModel) {
    return EmbeddingStoreContentRetriever.builder()
      .embeddingStore(embeddingStore)
      .embeddingModel(embeddingModel)
      .maxResults(10)
      .minScore(0.8)
      .build();
}


This retriever uses the embedding model to encode the user’s query and compares it against stored article embeddings. Also, we specified the maximum number of result items to be returned and the score, which will control how strictly the response must match our request.

该检索器使用嵌入模型对用户查询进行编码,并将其与存储的文章嵌入进行比较。此外,我们还指定了返回结果项的最大数量和匹配分数,这将控制响应与我们的请求匹配的严格程度。

Next, let’s create a ChatLanguageModel bean, that will generate responses based on the retrieved content:

接下来,让我们创建一个ChatLanguageModel bean,它将根据检索到的内容生成响应:

​​​​​​​
@Bean
public ChatLanguageModel chatModel() {
    return OpenAiChatModel.builder()
      .apiKey(apiKey)
      .modelName("gpt-4o-mini")
      .build();
}


In this bean, we’ve used the gpt-4o-mini model, but we can always choose another model that better matches our requirements.

在这个项目中,我们使用了gpt-4o-mini模型,但我们随时可以选择另一个更符合需求的模型。

Now, we’ll create an ArticleBasedAssistant interface. Here, we’ll define the answer() method, which takes a text request and returns a text response:

现在,我们将创建一个基于文章的助手接口。在这里,我们将定义answer()方法,该方法接收一个文本请求并返回一个文本响应:

public interface ArticleBasedAssistant {
    String answer(String question);
}


LangChain4j dynamically implements this interface by combining the configured language model and content retriever. Next, let’s create a bean for our assistant interface:

LangChain4j 通过组合配置的语言模型和内容检索器动态实现此接口。接下来,我们为助手接口创建一个bean:

@Bean
public ArticleBasedAssistant articleBasedAssistant(ChatLanguageModel chatModel, ContentRetriever contentRetriever) {
    return AiServices.builder(ArticleBasedAssistant.class)
      .chatLanguageModel(chatModel)
      .contentRetriever(contentRetriever)
      .build();
}


This setup means we can now call assistant.answer(“…”), and under the hood, the query is embedded, and relevant documents are fetched from the vector store. These documents are used as context in the LLM prompt, and a natural language answer is generated and returned.

这样的设置意味着我们现在可以调用 assistant.answer("..."),在底层,查询会被嵌入,并从向量存储中获取相关文档。这些文档将作为大语言模型提示的上下文,最终生成并返回自然语言答案。

ChatBotController Implementation and Test the Results

@RestController
public class ChatBotController {
    private final ArticleBasedAssistant assistant;

    @Autowired
    public ChatBotController(ArticleBasedAssistant assistant) {
        this.assistant = assistant;
    }

    @GetMapping("/chat-bot")
    public String answer(@RequestParam("question") String question) {
        return assistant.answer(question);
    }
}

Here, we’ve implemented the chatbot endpoint, integrating it with the ArticleBasedAssistant. This endpoint accepts a user query via the question request parameter, delegates it to the ArticleBasedAssistant, and returns the generated response as plain text.

在此,我们实现了聊天机器人端点,并将其与ArticleBasedAssistant集成。该端点通过question请求参数接收用户查询,将其委托给ArticleBasedAssistant处理,并以纯文本形式返回生成的响应。

Let’s call our chatbot API and see what it responds with:

@AutoConfigureMockMvc
@SpringBootTest(classes = {ChatBotConfiguration.class, ArticlesRepository.class, ChatBotController.class})
class ChatBotLiveTest {

    Logger log = LoggerFactory.getLogger(ChatBotLiveTest.class);

    @Autowired
    private MockMvc mockMvc;

    @Test
    void givenChatBotApi_whenCallingGetEndpointWithQuestion_thenExpectedAnswersIsPresent() throws Exception {
        String chatResponse = mockMvc
          .perform(get("/chat-bot")
            .param("question", "Steps to implement Spring boot app and MongoDB"))
          .andReturn()
          .getResponse()
          .getContentAsString();

        log.info(chatResponse);
        Assertions.assertTrue(chatResponse.contains("Step 1"));
    }
}

In our test, we called the chatbot endpoint and asked it to provide us with the steps to create a Spring Boot application with MongoDB integration. After this, we retrieved and logged the expected result. The complete response is also visible in the logs:

在我们的测试中,我们调用了聊天机器人端点,要求它提供创建集成MongoDB的Spring Boot应用程序的步骤。之后,我们获取并记录了预期结果。完整的响应在日志中也可以看到:

To implement a MongoDB Spring Boot Java Book Tracker application, follow these steps. This guide will help you set up a simple CRUD application to manage books, where you can add, edit, and delete book records stored in a MongoDB database.

### Step 1: Set Up Your Environment

1. **Install Java Development Kit (JDK)**:
   Make sure you have JDK (Java Development Kit) installed on your machine. You can download it from the [Oracle website](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html) or use OpenJDK.

2. **Install MongoDB**:
   Download and install MongoDB from the [MongoDB official website](https://www.mongodb.com/try/download/community). Follow the installation instructions specific to your operating system.

//shortened

参考:

Building an AI Chatbot in Java With Langchain4j and MongoDB Atlas | Baeldung

LangChain4J 大语言模型,Spring Boot 应用-CSDN博客

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐