LangChain4j:非 Spring 系,AI For Java的另一条路

目录

[一 技术实现:](#一 技术实现:)

[0 依赖管理](#0 依赖管理)

[1 AI代理调用](#1 AI代理调用)

[2 向量模型](#2 向量模型)

[3 向量数据库](#3 向量数据库)

[4 记忆存储](#4 记忆存储)

[5 流式聊天大模型](#5 流式聊天大模型)

[6 Tools工具/Functional Calling](#6 Tools工具/Functional Calling)

[二 调用链路分析](#二 调用链路分析)

[1 请求处理流程](#1 请求处理流程)

[2 AI代理调用流程](#2 AI代理调用流程)

[3 工具调用流程](#3 工具调用流程)

[4 知识库检索调用流程](#4 知识库检索调用流程)

[三 技术积累](#三 技术积累)


一 技术实现:

0 依赖管理

导入OpenAi的依赖项

XML 复制代码
        <!-- 基于open-ai标准的大模型(ChatGPT、DeepSeek) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        </dependency>

        <!-- langchain4j核心starter -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>

完整依赖

XML 复制代码
<?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>com.itysky</groupId>
    <artifactId>java-ai-longchain4j</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.2.6</spring-boot.version>
        <knife4j.version>4.3.0</knife4j.version>
        <langchain4j.version>1.0.0-beta3</langchain4j.version>
        <mybatis-plus.version>3.5.11</mybatis-plus.version>
    </properties>

    <dependencies>
        <!-- 覆盖dashscope-sdk-java版本至2.19.5,支持text()和voice()方法,排除slf4j-simple避免与Logback冲突 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>dashscope-sdk-java</artifactId>
            <version>2.19.5</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- web应用程序核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 基于open-ai标准的大模型(ChatGPT、DeepSeek) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
        </dependency>

        <!-- langchain4j核心starter -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-spring-boot-starter</artifactId>
        </dependency>

        <!-- 接入ollama -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
        </dependency>

        <!-- 接入阿里云百炼平台(通义千问) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
        </dependency>

        <!-- 文档解析PDF -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
        </dependency>

        <!-- 简易RAG -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-easy-rag</artifactId>
        </dependency>

        <!-- PgVector 向量数据库(PostgreSQL + pgvector 扩展) -->
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-pgvector</artifactId>
        </dependency>

        <!-- PostgreSQL JDBC 驱动 -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

        <!-- 流式输出依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
        </dependency>

        <!-- MySQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

        <!-- MongoDB -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>

        <!-- 接口文档 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>${knife4j.version}</version>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 修复Maven编译插件,无弃用参数、无冗余配置 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Boot 依赖管理 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- LangChain4j 核心依赖管理 -->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!-- LangChain4j 社区版依赖管理(通义千问) -->
            <dependency>
                <groupId>dev.langchain4j</groupId>
                <artifactId>langchain4j-community-bom</artifactId>
                <version>${langchain4j.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

1 AI代理调用

1 底层API调用

大模型标准协议需要自己去进行转换代码实现。(命令式编程)

java 复制代码
    @Autowired
    private QwenChatModel qwenChatModel;
    @Test
    public void testChatMemory2() {
        //从String -> UserMessage
        UserMessage userMessage1 = UserMessage.userMessage("我是小明");
        ChatResponse chatResponse1 = qwenChatModel.chat(userMessage1);
        AiMessage aiMessage1 = chatResponse1.aiMessage();
        //从AiMessage -> String
        String text = aiMessage1.text();
        System.out.println(text);
    }

2 AiServices

这种方式不在接口上打注解,而是需要在 @Configuration 类里面手动写一个 @Bean 方法。(编程式声明)

java 复制代码
@Bean
public LegalAssistant legalAssistant(ChatLanguageModel model) {
    // 手动调用 builder 组装
    return AiServices.builder(LegalAssistant.class)
            .chatLanguageModel(model)
            .build();
}

3 @AiService

1 实现一个无状态,单轮对话的AI服务,没有记忆,没有RAG。(注解式声明)

java 复制代码
package com.itsky.java.ai.longchain4j.assistant;

import dev.langchain4j.service.spring.AiService;

import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

/*定义聊天助手接口-指定对应的ChatModel*/
@AiService(wiringMode = EXPLICIT, chatModel = "qwenChatModel")
public interface Assistant {
    String chat(String userMassage);
}
  • @AiService声明式代理服务
  • writingMode:chatModel,chatMemory,tools,RAG组件进行管理
    • AUTOMATIC:自动装配(去找那唯一的一个组件,出现多个会冲突报错)
    • EXPLICIT:显式注入(点名显示调用)
  • chatModel:对话的核心大脑。
  • tools:指定代码当中实现的工具类。
  • retrievalAugmentor:(RAG)检索增强生成,大模型私有知识库,配合EmbeddingModel向量模型+EmbeddingStore向量数据库实现。
  • chatmemoryProbider:聊天记忆组件,一般都是配合MongDB实现。

测试:

java 复制代码
    // 1.1直接注入Assistant对象
    @Autowired
    private Assistant assistant;
    @Test
    public void testChat2() {
    // 2.直接调用注入的Assistant对象
        String answer = assistant.chat("写一首创造性的七言律诗(要求结合时事,以美国兴衰为背景)");
        System.out.println(answer);
    }

2 实际使用

这里配置了持久化记忆,工具调用,私有知识库,流式输出

java 复制代码
package com.itsky.java.ai.longchain4j.assistant;

import dev.langchain4j.service.*;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;

import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;

@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi", // 使用 XiaozhiAgentConfig 中的 MongoDB 持久化记忆提供者
        tools = "appointmentTools", // tools配置
        contentRetriever = "contentRetrieverXiaozhipgsql" // 配置向量存储
)
public interface XiaozhiAgent {
    @SystemMessage(fromResource = "xiaozhi-prompt-template.txt")//提示词
    Flux<String> chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}

2 向量模型

这里使用的是阿里通义千问的text-embedding-v3

java 复制代码
package com.itsky.java.ai.longchain4j.config;

import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author ABC
 */
@Configuration
public class DashscopeEmbeddingConfig {

    @Value("${langchain4j.community.dashscope.embedding-model.api-key}")
    private String apiKey;

    @Value("${langchain4j.community.dashscope.embedding-model.model-name}")
    private String modelName;

    @Bean(name = "dashscopeEmbeddingModel")
    public EmbeddingModel dashscopeEmbeddingModel() {
        return QwenEmbeddingModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .build();
    }
}

3 向量数据库

使用的是pinecone免费版,性能优秀适合实时检索,属于直接就能用。

java 复制代码
package com.itsky.java.ai.longchain4j.config;

import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * PgVector 向量存储配置。
 * 使用 PostgreSQL + pgvector 扩展替代 Pinecone,本地部署,成本低。
 * 启动时自动建表(createTable=true),无需手动执行 DDL。
 * PostgreSQL 侧只需提前执行:CREATE EXTENSION IF NOT EXISTS vector;
 */
@Configuration
public class EmbeddingStoreConfig {

    @Value("${pgvector.host}")
    private String host;

    @Value("${pgvector.port:5432}")
    private int port;

    @Value("${pgvector.database}")
    private String database;

    @Value("${pgvector.user}")
    private String user;

    @Value("${pgvector.password}")
    private String password;

    @Value("${pgvector.table:langchain4j_embeddings}")
    private String table;

    @Autowired
    @Qualifier("dashscopeEmbeddingModel")
    private EmbeddingModel dashscopeEmbeddingModel;

    @Bean(name = "embeddingStore")
    public PgVectorEmbeddingStore embeddingStore() {
        return PgVectorEmbeddingStore.builder()
                .host(host)
                .port(port)
                .database(database)
                .user(user)
                .password(password)
                .table(table)
                // 向量维度:由嵌入模型决定(text-embedding-v3 为 1024 维)
                .dimension(dashscopeEmbeddingModel.dimension())
                // 首次启动自动在 PostgreSQL 中创建向量表,已存在则跳过
                .createTable(true)
                .build();
    }
}
维度 Milvus Elasticsearch PostgreSQL (pgvector) Redis (RediSearch)
主要吃什么硬件? 内存 + CPU + 网络带宽 JVM 内存 + OS Cache + SSD IO 高频 CPU + SSD IO 纯物理大内存
高可用与灾备实现 云原生多活、数据存 S3 自身分片副本机制 (Replicas) 主从流复制 (WAL 流) 哨兵 / 集群切片机制
整体运维难度 🔴 极难 (需 K8s 专家) 🟠 偏难 (需调优 JVM) 🟢 极低 (常规 DBA 即可) 🟡 中等 (需死盯内存监控)
适合的公司基建 大型微服务架构团队 已有 ES 运维专家的团队 绝大多数中小互联网团队 强依赖全量缓存的高频交易/推荐团队

4 记忆存储

java 复制代码
package com.itsky.java.ai.longchain4j.store;

import com.itsky.java.ai.longchain4j.bean.ChatMessages;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;

import java.util.LinkedList;
import java.util.List;

@Component
public class MongoChatMemoryStore implements ChatMemoryStore {
    @Autowired
    private MongoTemplate mongoTemplate;

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
        if (chatMessages == null) return new LinkedList<>();
        return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        Update update = new Update();
        update.set("content", ChatMessageSerializer.messagesToJson(messages));
        //根据query条件能查询出文档,则修改文档;否则新增文档
        mongoTemplate.upsert(query, update, ChatMessages.class);
    }

    @Override
    public void deleteMessages(Object memoryId) {
        Criteria criteria = Criteria.where("memoryId").is(memoryId);
        Query query = new Query(criteria);
        mongoTemplate.remove(query, ChatMessages.class);
    }
}

5 流式聊天大模型

这里并没有单独去进行定义,而是使用SDK(核心逻辑包),结合starter,使用约定大于配置的理念,只需要引入依赖编写对应的yml配置就可以减少一些繁琐的配置。

进行整合

java 复制代码
package com.itsky.java.ai.longchain4j.config;

import com.itsky.java.ai.longchain4j.store.MongoChatMemoryStore;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

@Configuration
public class XiaozhiAgentConfig {

    @Autowired
    private MongoChatMemoryStore mongoChatMemoryStore;

    /**
     * 小智Agent专用的聊天记忆提供者(基于MongoDB持久化)
     * Bean名称为 chatMemoryProviderXiaozhi,供 XiaozhiAgent 的 chatMemoryProvider 使用。
     * 注意:SeparateChatAssistantConfig 中也定义了名为 chatMemoryProvider 的 Bean,
     *       此处使用不同名称以避免冲突。
     */
    @Bean
    ChatMemoryProvider chatMemoryProviderXiaozhi() {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(20)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }

    /**
     * 基于内存向量存储的内容检索器(使用本地知识库文档,InMemory方式)
     */
    @Bean
    ContentRetriever contentRetrieverXiaozhi(
            @Qualifier("dashscopeEmbeddingModel") EmbeddingModel embeddingModel) {
        // 使用 ClassPathResource 获取 resources 目录下的知识库文档路径
        Document document1 = loadClassPathDocument("医院信息.md");
        Document document2 = loadClassPathDocument("科室信息.md");
        Document document3 = loadClassPathDocument("神经内科.md");
        List<Document> documents = Arrays.asList(document1, document2, document3);

        // 使用内存向量存储
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

        // 使用指定的嵌入模型进行文档向量化
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        ingestor.ingest(documents);

        // 从嵌入存储中检索和查询内容相关的信息
        return EmbeddingStoreContentRetriever.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .maxResults(3)
                .minScore(0.6)
                .build();
    }

    /**
     * 基于 Pinecone 向量存储的内容检索器
     */
    @Bean
    ContentRetriever contentRetrieverXiaozhipgsql(
            @Qualifier("embeddingStore") EmbeddingStore<TextSegment> embeddingStore,
            @Qualifier("dashscopeEmbeddingModel") EmbeddingModel embeddingModel) {
        return EmbeddingStoreContentRetriever
                .builder()
                // 设置用于生成嵌入向量的嵌入模型
                .embeddingModel(embeddingModel)
                // 指定要使用的嵌入存储(Pinecone)
                .embeddingStore(embeddingStore)
                // 最多返回 3 条匹配结果
                .maxResults(3)
                // 只有得分大于等于 0.7 的结果才会被返回
                .minScore(0.7)
                .build();
    }

    /**
     * 从 classpath 加载文档(兼容 jar 包运行)
     */
    private Document loadClassPathDocument(String filename) {
        try {
            ClassPathResource resource = new ClassPathResource(filename);
            Path path = resource.getFile().toPath();
            return FileSystemDocumentLoader.loadDocument(path);
        } catch (Exception e) {
            throw new RuntimeException("无法加载知识库文档: " + filename, e);
        }
    }
}

6 Tools工具/Functional Calling

这里编写的Tools,当LLM调用时,会自动的将这些namevlaue值进行注入,后续大模型会去进行检查,判断是否有对应的方法,如果有的话就会去进行调用。结合一定的提示词进行约束条件。

代码展示:实现预约挂号,取消挂号,查询号源的功能。

java 复制代码
package com.itsky.java.ai.longchain4j.tools;

import com.itsky.java.ai.longchain4j.entity.Appointment;
import com.itsky.java.ai.longchain4j.service.AppointmentService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AppointmentTools {

    @Autowired
    private AppointmentService appointmentService;

    /**
     * 预约挂号工具。
     * 调用前请先执行 queryDepartment 查询号源,确认有号后再调用此方法。
     * Appointment 对象的 JSON 字段说明:
     *   - username:     患者姓名(必填)
     *   - id_card:      患者身份证号(必填)
     *   - department:   预约科室名称(必填)
     *   - date:         预约日期,格式 yyyy-MM-dd(必填)
     *   - time:         预约时间段,可选值:上午、下午(必填)
     *   - doctor_name:  预约医生姓名(必填,若用户未指定请从知识库中选择一位合适医生)
     */
    @Tool(name = "预约挂号",
            value = "根据患者信息进行挂号预约。调用此工具前必须先调用 queryDepartment 确认有号," +
                    "并向用户确认全部预约信息后再执行。" +
                    "参数 appointment 为 JSON 对象,字段名必须使用驼峰格式:" +
                    "username(患者姓名)、idCard(身份证号)、department(科室)、" +
                    "date(预约日期 yyyy-MM-dd)、time(上午或下午)、doctorName(医生姓名)。" +
                    "若用户未提供医生姓名,请从向量知识库中选择一位合适的医生填入。")
    public String bookAppointment(
            @P("预约信息,JSON对象,字段名用驼峰格式:username(患者姓名)、idCard(身份证号)、department(科室)、date(日期yyyy-MM-dd)、time(上午或下午)、doctorName(医生姓名)")
            Appointment appointment) {
        // 打印日志,便于调试
        System.out.println("=== 预约挂号工具被调用 ===");
        System.out.println("患者姓名: " + appointment.getUsername());
        System.out.println("身份证号: " + appointment.getIdCard());
        System.out.println("科室: " + appointment.getDepartment());
        System.out.println("日期: " + appointment.getDate());
        System.out.println("时间: " + appointment.getTime());
        System.out.println("医生: " + appointment.getDoctorName());

        // 查找数据库中是否包含对应的预约记录(防重复预约)
        Appointment appointmentDB = appointmentService.getOne(appointment);
        if (appointmentDB == null) {
            // 防止大模型幻觉设置了 id,强制置 null 让数据库自增
            appointment.setId(null);
            boolean saved = appointmentService.save(appointment);
            System.out.println("预约保存结果: " + saved);
            if (saved) {
                return String.format("预约成功!预约详情:患者 %s,科室 %s,日期 %s %s,主治医生 %s。",
                        appointment.getUsername(),
                        appointment.getDepartment(),
                        appointment.getDate(),
                        appointment.getTime(),
                        appointment.getDoctorName());
            } else {
                return "预约失败,数据库写入错误,请稍后重试。";
            }
        }
        return String.format("您在 %s %s %s 已有预约(医生:%s),请勿重复预约。",
                appointmentDB.getDepartment(),
                appointmentDB.getDate(),
                appointmentDB.getTime(),
                appointmentDB.getDoctorName());
    }

    /**
     * 取消预约挂号工具。
     */
    @Tool(name = "取消预约挂号",
            value = "根据患者信息取消已有预约。" +
                    "参数 appointment 为 JSON 对象,字段名使用驼峰格式:" +
                    "username(患者姓名)、idCard(身份证号)、department(科室)、" +
                    "date(预约日期)、time(上午或下午)。")
    public String cancelAppointment(
            @P("取消预约信息,JSON对象,字段名用驼峰格式:username(患者姓名)、idCard(身份证号)、department(科室)、date(日期)、time(上午或下午)")
            Appointment appointment) {
        System.out.println("=== 取消预约工具被调用 ===");
        Appointment appointmentDB = appointmentService.getOne(appointment);
        if (appointmentDB != null) {
            if (appointmentService.removeById(appointmentDB.getId())) {
                return String.format("取消预约成功!已取消 %s %s %s 的预约(医生:%s)。",
                        appointmentDB.getDepartment(),
                        appointmentDB.getDate(),
                        appointmentDB.getTime(),
                        appointmentDB.getDoctorName());
            } else {
                return "取消预约失败,数据库操作错误,请稍后重试。";
            }
        }
        return "未找到对应的预约记录,请核对科室、日期和时间是否正确。";
    }

    /**
     * 查询号源工具。
     */
    @Tool(name = "查询是否有号源",
            value = "根据科室名称、日期、时间段和医生姓名查询是否有可用号源,并将结果告知用户。")
    public boolean queryDepartment(
            @P(value = "科室名称") String name,
            @P(value = "日期,格式 yyyy-MM-dd") String date,
            @P(value = "时间段,可选值:上午、下午") String time,
            @P(value = "医生姓名(可选,不指定则查询该科室所有医生)", required = false) String doctorName
    ) {
        System.out.println("=== 查询号源工具被调用 ===");
        System.out.println("科室名称:" + name);
        System.out.println("日期:" + date);
        System.out.println("时间:" + time);
        System.out.println("医生名称:" + doctorName);
        // TODO 维护医生的排班信息:
        // 如果没有指定医生名字,则根据其他条件查询是否有可以预约的医生(有返回true,否则返回false);
        // 如果指定了医生名字,则判断医生是否有排班(没有排班返回false);
        // 如果有排班,则判断医生排班时间段是否已约满(约满返回false,有空闲时间返回true)。
        return true;
    }
}

二 调用链路分析

1 请求处理流程

-1 前端请求 :用户通过前端发送聊天请求到 /xiaozhi/chat 接口

  • 2 控制器处理 : XiaozhiController 接收请求并调用 AI 代理

  • 3 AI 代理调用 : XiaozhiAgent 使用大模型生成回复

  • 4 流式返回 :使用 WebFlux 实现流式响应,提升用户体验

2 AI代理调用流程

  • 1代理配置 : @AiService 注解配置大模型、工具和内容检索器

  • 2系统提示 :加载 xiaozhi-prompt-template.txt 作为系统提示

  • 3聊天记忆 :使用 MongoChatMemoryStore 存储聊天历史

  • 4工具调用 :根据用户需求调用 AppointmentTools 中的工具,实现增删改查

  • 5内容检索 :从向量存储中检索相关知识

3 工具调用流程

  • 1工具定义 : AppointmentTools 定义预约相关工具

  • 2 工具调用 :大模型根据用户需求调用相应工具

  • 3 服务调用 :工具调用 AppointmentService 提供的业务逻辑

  • 4 数据库操作 : AppointmentServiceImpl 操作 MySQL 数据库

4 知识库检索调用流程

  • 1内容检索器 : contentRetrieverXiaozhiPincone 从向量存储中检索相关内容

  • 2向量模型 :使用阿里通义千问 text-embedding-v3 模型生成查询向量

  • 3向量存储 :在 Pinecone 中搜索相似向量

  • 4结果返回 :将检索到的内容返回给大模型

三 技术积累

1 在DataInitializer.java类当中实现了CommandLineRunner接口

java 复制代码
@Component
public class DataInitializer implements CommandLineRunner {

原理 : CommandLineRunner 是 Spring Boot 提供的一个接口,任何实现了该接口的 Bean 都会在 Spring Boot 应用完全启动后、开始接收请求前 自动执行其 run() 方法。这是执行初始化逻辑的理想位置

@Value值注入:去读取数据,默认是false不去进行初始化

java 复制代码
@Value("${pgvector.init-data:false}")
private boolean initData;

配置文件展示示例:pgvector.init-data 配置项

XML 复制代码
pgvector:
  #  首次部署时设为 true 注入知识库文档,完成后改回 false 避免重复注入
  init-data: false

代码逻辑

java 复制代码
@Override
public void run(String... args) throws Exception {
    if (initData) {
        initKnowledgeBase();  // 执行初始化
    } else {
        System.out.println("PgVector 知识库初始化已跳过(pgvector.init-data=false)");
    }
}

完整代码

java 复制代码
package com.itsky.java.ai.longchain4j.config;

import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.ClassPathDocumentLoader;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
 * 应用启动时初始化 PgVector 向量存储中的知识库数据。
 * 通过配置项 pgvector.init-data 控制是否执行初始化(默认为 false,避免重复注入)。
 */
@Component
public class DataInitializer implements CommandLineRunner {

    @Autowired
    @Qualifier("embeddingStore")
    private EmbeddingStore<TextSegment> embeddingStore;

    @Autowired
    @Qualifier("dashscopeEmbeddingModel")
    private EmbeddingModel embeddingModel;

    /**
     * 是否在启动时向 PgVector 注入知识库文档。
     * 首次部署时设为 true,之后改为 false 避免重复注入。
     */
    @Value("${pgvector.init-data:false}")
    private boolean initData;

    @Override
    public void run(String... args) throws Exception {
        if (initData) {
            initKnowledgeBase();
        } else {
            System.out.println("PgVector 知识库初始化已跳过(pgvector.init-data=false)");
        }
    }

    private void initKnowledgeBase() {
        System.out.println("开始向 PgVector 注入知识库数据...");
        // 读取知识库文档(从 classpath 加载)
        Document document1 = ClassPathDocumentLoader.loadDocument("医院信息.md");
        Document document2 = ClassPathDocumentLoader.loadDocument("科室信息.md");
        Document document3 = ClassPathDocumentLoader.loadDocument("神经内科.md");
        List<Document> documents = Arrays.asList(document1, document2, document3);

        // 使用指定的嵌入模型将文档向量化并存入 PgVector
        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .build();
        ingestor.ingest(documents);
        System.out.println("PgVector 知识库初始化完成,共注入 " + documents.size() + " 个文档");
    }
}
相关推荐
维元码簿2 小时前
系列开篇 | Claude Code 源码架构概览:51万行代码的模块地图
ai·agent·claude code·ai coding
庄小焱2 小时前
【AI模型】——RAG索引构建与优化
人工智能·ai·向量数据库·ai大模型·rag·rag索引·索引构建与优化
呆呆敲代码的小Y2 小时前
从LLM到Agent Skill:AI核心技术全拆解与系统化学习路线
人工智能·ai·llm·agent·优化·skill·mcp
俊哥V2 小时前
每日 AI 研究简报 · 2026-04-18
人工智能·ai
Csvn3 小时前
🌟 LangChain 30 天保姆级教程 · Day 23|Agent 进阶实战!Function Calling + 自动 Tool 注册,打造会“动
python·langchain
Csvn3 小时前
🌟 LangChain 30 天保姆级教程 · Day 22|长文档处理三剑客!MapReduce、Refine、Map-Rerank,让 AI 消化整本手册
python·langchain
DFCED4 小时前
突发!Sora 之父 Bill Peebles 离职:OpenAI 理想主义的又一次落幕
人工智能·大模型·agent·sora
拾薪4 小时前
[SuperPower] Brainingstorm - 流程控制架构分析
网络·人工智能·ai·架构·superpower·brainstorming
胡志辉的博客5 小时前
多智能体协作,不是多开几个 Agent:从中介者模式看 OpenClaw 和 Hermes Agent
人工智能·设计模式·ai·agent·中介者模式·openclaw·herman