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() + " 个文档");
    }
}
相关推荐
2601_962078032 小时前
百度双重主要上市9月1日生效,“全栈AI”如何打开估值空间?
百度·ai·双重上市·估值空间·港股通
怕浪猫4 小时前
Agent 编排 Agent:DeepSeek Harness 的子代理与工作流系统有多强
agent·工作流引擎·deepseek
CaseyWei6 小时前
Harness 架构 Multi‑Agent(多智能体)完整深度解析
人工智能·ai·架构·harness
eaglewgs6 小时前
关于AI书写测试用例,谈一下我的思考
人工智能·测试开发·ai·测试用例·agent·测试经验·agent测试开发
知了一笑7 小时前
企业的卷味,组织的AI味
人工智能·ai·ai工作流
icsocket8 小时前
国内测试稳定可靠的AI算力芯片测试座厂家
ai
魔术师Grace8 小时前
AI 为什么会越改越坏?5个Tools 拆解编程 Agent
openai·agent·ai编程
王国强20098 小时前
OpenCode 深度使用指南
agent
ovO8 小时前
DeepSeek Harness 源码解读(九):它适合什么场景,应该从哪里扩展
开源·agent