LangChain4j 完整学习指南:从入门到企业级应用实战,看完这一篇你就是AI应用开发工程师

4 阅读35分钟

image.png

LangChain4j 完整学习指南:从入门到企业级应用实战

基于真实项目案例,系统讲解 LangChain4j 核心技术与最佳实践


📖 目录


一、LangChain4j 简介

1.1 什么是 LangChain4j?

LangChain4j 是 LangChain 的 Java 版本,是一个用于构建基于大型语言模型(LLM)应用程序的 Java 框架。它提供了一套简洁的 API,帮助开发者快速集成 AI 能力到 Java 应用中。

1.2 核心优势

  • Java 原生支持:完美融入 Java/Spring Boot 生态
  • 声明式编程:通过注解定义 AI 服务,代码简洁优雅
  • 多模型支持:支持 OpenAI、Qwen、Claude 等主流大模型
  • 模块化设计:Memory、Tools、RAG 等组件可灵活组合
  • 企业级特性:支持持久化、流式响应、异步处理

1.3 适用场景

  • 智能客服系统
  • 文档问答助手
  • 数据分析与提取
  • 自动化工作流
  • 知识管理系统

二、技术栈与环境搭建

2.1 核心技术栈

<!-- Spring Boot 3.5.3 + Java 17 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.3</version>
</parent>

<!-- LangChain4j 核心依赖 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.13.0</version>
</dependency>

<!-- Spring Boot Starter -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-spring-boot-starter</artifactId>
    <version>1.13.0-beta23</version>
</dependency>

<!-- 通义千问模型集成 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
    <version>1.13.0-beta23</version>
</dependency>

<!-- 向量数据库 Pinecone -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-pinecone</artifactId>
    <version>1.13.0-beta23</version>
</dependency>

<!-- 嵌入模型 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
    <version>1.13.0-beta23</version>
</dependency>

<!-- MCP 协议支持 -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-mcp</artifactId>
    <version>1.13.0-beta23</version>
</dependency>

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

2.2 配置文件示例

# application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/ai-langchain4j-demo
    username: root
    password: mysqlroot

langchain4j:
  community:
    dashscope:
      chat-model:
        model-name: qwen3.5-plus
        api-key: ${DASHSCOPE_API_KEY}
      streaming-chat-model:
        model-name: qwen3.5-plus
        api-key: ${DASHSCOPE_API_KEY}
      embedding-model:
        model-name: text-embedding-v4
        api-key: ${DASHSCOPE_API_KEY}

pinecone:
  api-key: ${PINECONE_API_KEY}
  cloud: AWS
  region: us-east-1
  name-space: lost-space
  index: my-lost-index

2.3 模型配置类

@ConfigurationProperties(prefix = "langchain4j.community.dashscope.chat-model")
@Data
@Configuration
public class ChatModelConfig {
    private String modelName;
    private String apiKey;

    @Bean
    public ChatModel myQwenChatModel() {
        return QwenChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .build();
    }
}

三、核心技术详解

3.1 AI Services - 声明式 AI 服务

3.1.1 基本概念

AI Services 是 LangChain4j 的核心抽象,通过 Java 接口和注解定义 AI 服务,框架自动处理与大模型的交互。

3.1.2 基础用法
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemory = "chatMemory")
public interface DefaultPromptAssistant {

    @SystemMessage("输出一个int类型数字,{{it}}")
    int extractInt(String text);

    @SystemMessage("输出 userDataType 信息")
    UserDataType extractUserDataType(String text);

    @SystemMessage("{{it}} 是否为好评?")
    boolean isGood(String text);
}

关键注解说明:

  • @AiService:标记接口为 AI 服务

    • wiringMode:装配模式
      • EXPLICIT(显式):需要明确指定 chatModel、chatMemory 等 Bean 名称
      • AUTOMATIC(自动):自动从 Spring 容器中查找匹配的 Bean
    • chatModel:指定聊天模型 Bean 名称(如 "myQwenChatModel")
    • chatMemory / chatMemoryProvider:指定对话记忆组件
    • tools:指定可用的工具 Bean 数组
    • toolProvider:指定工具提供者(如 MCP)
    • contentRetriever:指定 RAG 内容检索器
    • systemMessageProvider:指定动态系统消息提供者
  • @SystemMessage:系统提示词

    • 支持模板变量:{{it}}(单个参数)、{{variable}}(命名参数)
    • 支持外部文件:fromResource = "xxx.txt"
  • @UserMessage:用户消息模板,可与 @SystemMessage 同时使用

  • @V:绑定方法参数到模板变量,如 @V("sessionId") String sessionId

  • @MemoryId:标识会话 ID 参数,用于多会话隔离

重要特性:

  1. 返回类型自动转换:LangChain4j 会自动将 LLM 的文本响应转换为指定的 Java 类型
  2. 结构化输出:通过 @Description 注解指导 LLM 生成符合 POJO 结构的数据
  3. 流式支持:返回 Flux<String> 实现流式响应
  4. 工具调用透明化:声明 tools 后,LLM 会自动决定何时调用哪个工具
3.1.3 项目中的实际应用

数据类型提取测试(DatatypePromptTests):


3.2 Chat Models - 聊天模型集成

3.2.1 模型提供商

LangChain4j 支持多种大模型提供商:

  • 阿里云通义千问(本项目使用)
  • OpenAI GPT 系列
  • Anthropic Claude
  • Google Gemini
  • Ollama 本地模型
3.2.2 通义千问集成(项目配置)

配置类(ChatModelConfig.java):

@ConfigurationProperties(prefix = "langchain4j.community.dashscope.chat-model")
@Data
@Configuration
public class ChatModelConfig {
    private String modelName;  // qwen3.5-plus
    private String apiKey;

    @Bean
    public ChatModel myQwenChatModel() {
        return QwenChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .build();
    }
}

配置文件(application.yml):

langchain4j:
  community:
    dashscope:
      chat-model:
        model-name: qwen3.5-plus
        api-key: sk-xxx
      streaming-chat-model:
        model-name: qwen3.5-plus
        api-key: sk-xxx
      embedding-model:
        model-name: text-embedding-v4
        api-key: sk-xxx

关键技术点:

  1. ChatModel vs StreamingChatModel

    • ChatModel:同步阻塞,等待完整响应
    • StreamingChatModel:异步流式,逐块返回
    • 项目中同时配置两者,根据场景选择使用
  2. 模型选择

    • 项目使用 qwen3.5-plus(通义千问 3.5 Plus)
    • 嵌入模型使用 text-embedding-v4
    • 不同模型有不同的能力和成本,需根据业务需求选择
  3. 配置属性绑定

    • 使用 @ConfigurationProperties 自动绑定 YAML 配置
    • 避免硬编码 API Key,便于环境切换
3.2.3 嵌入模型配置
@Bean
public EmbeddingModel embeddingModel() {
    return QwenEmbeddingModel.builder()
            .modelName("text-embedding-v4")
            .apiKey(apiKey)
            .build();
}

3.3 Prompt Engineering - 提示词工程

3.3.1 提示词模板

LangChain4j 支持多种提示词定义方式:

方式一:注解内联

@SystemMessage("你是一个专业的{{role}},请回答以下问题:{{question}}")
String chat(@V("role") String role, @V("question") String question);

方式二:外部文件

@SystemMessage(fromResource = "intention-prompt.txt")
public interface LostIntentionAiAssistant {
    String chat(@MemoryId String memoryId, @UserMessage String message);
}

提示词文件示例(intention-prompt.txt):

# 角色
你是一位专业的失物招领工作人员,具备高效、负责的工作态度。

## 技能
### 技能 1:判断用户意图
- 仔细分析用户输入内容,精准判断用户的需求
- 对用户的输入意图进行标记,分类如下:
  1. 丢失信息登记
  2. 找到失物登记
  3. 失物查询
  4. 其他

## 限制
- 只回答与失物招领相关的问题
- 所输出的内容需条理清晰、简洁明了
3.3.2 外部文件方式(项目采用)

AI Service 定义

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "mysqlChatMemoryProvider")
public interface MySqlAssistantPromptProvider {
    
    // 从外部文件加载系统提示词
    @SystemMessage(fromResource = "part1-prompt.txt")
    String chat(@MemoryId String memoryId, @UserMessage String userMessage);

    // 同一个接口可以有多个方法,使用不同的提示词
    @SystemMessage(fromResource = "part1-prompt2.txt")
    String chat(@MemoryId String memoryId, @UserMessage String userMessage,
                @V("name") String name, @V("job") String job);
}

关键技术点:

  1. 静态 vs 动态提示词

    • @SystemMessage(fromResource = "xxx.txt"):静态提示词,编译时确定
    • 注意:LangChain4j 目前主要通过注解和外部文件定义提示词
    • 如需动态提示词,可通过方法参数 @V 注入变量实现
  2. 提示词文件位置

    • 放在 src/main/resources/ 目录下
    • 项目中的提示词文件:
      • intention-prompt.txt:意图识别
      • lostreg-prompt.txt:失物登记
      • lostproperty-prompt.txt:拾取登记
      • lostproperty-query-prompt.txt:失物查询
      • part1-prompt.txtpart1-prompt2.txt:示例提示词
  3. 同一接口多方法多提示词

    • 一个 AI Service 接口可以定义多个方法
    • 每个方法可以使用不同的 @SystemMessage
    • 适合不同场景使用不同的提示词策略
  4. 提示词版本管理

    • 提示词作为资源文件,可纳入版本控制
    • 便于 A/B 测试和优化迭代

3.4 Structured Output - 结构化输出

3.4.1 POJO 映射

通过 @Description 注解指导大模型生成结构化数据:

@Data
public class UserDataType {
    @Description("用户姓名")
    private String name;
    
    @Description("用户年龄")
    private int age;
    
    @Description("用户职业")
    private String job;
}
3.4.2 复杂业务对象
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class LostRegOutput {
    @Description("用户姓名")
    private String username;

    @Description("用户手机号")
    private String phone;

    @Description("失物名称")
    private String lostItem;

    @Description("失物特征,多个特征用顿号、号隔开")
    private String lostItemFeature;

    @Description("是否完成登记")
    private Boolean isCompleted;

    @Description("大模型对用户的输出")
    private String output;

    @Description("数据库记录id")
    private Integer id;
}

关键技术点:

  1. @Description 的作用

    • 告诉 LLM 每个字段的含义和期望格式
    • 影响 LLM 生成 JSON 的结构和内容
    • 描述越详细,输出越准确
  2. @JsonIgnoreProperties(ignoreUnknown = true)

    • 容错机制:忽略 LLM 生成的多余字段
    • LLM 可能生成不在 POJO 中定义的字段,不加此注解会报错
    • 生产环境必备,提高系统稳定性
  3. 字段类型选择

    • 使用包装类型(IntegerBoolean)而非基本类型(intboolean
    • 允许字段为 null,LLM 未提取时不会报错
    • isCompleted 用于判断是否收集完所有信息
  4. 混合字段设计

    • 业务字段:usernamephonelostItem
    • 控制字段:isCompletedid
    • 输出字段:output(LLM 给用户的回复)
    • 一次调用同时完成信息提取和对话回复
3.4.3 项目中的实际应用

意图识别输出(IntentionOutput.java):

@JsonIgnoreProperties(ignoreUnknown = true)
@Data
public class IntentionOutput {
    @Description("意图分析,1-代表丢失信息登记,2-找到失物登记,3-失物查询,4-其他")
    private Integer intention;

    @Description("大模型输出结果")
    private String output;

    @Description("会话ID")
    private String sessionId;
}

失物登记输出(LostRegOutput.java):


3.5 Memory - 对话记忆管理

3.5.1 为什么需要 Memory?

大模型本身是无状态的,每次请求都是独立的。Memory 机制让 AI 能够记住上下文,实现连贯的多轮对话。

3.5.2 项目中的三种 Memory 实现

1. 单会话内存存储(MemoryChatConfig.java):

@Bean
public ChatMemory chatMemory() {
    return MessageWindowChatMemory.builder()
            .maxMessages(10)
            .build();
}

特点

  • 全局共享,无会话隔离
  • 适合单用户场景或测试
  • 重启后数据丢失

2. 多会话内存存储

@Bean
public ChatMemoryProvider chatMemoryProvider() {
    return (memoryId) -> MessageWindowChatMemory.builder()
            .id(memoryId)
            .chatMemoryStore(new InMemoryChatMemoryStore())
            .maxMessages(10)
            .build();
}

特点

  • 通过 memoryId 隔离不同会话
  • 每个会话独立的记忆窗口
  • 适合多用户并发场景
  • 重启后数据丢失

3. MySQL 持久化存储(生产环境采用):

@Component
public class MySqlChatMemoryStore implements ChatMemoryStore {
    
    @Resource
    private MemoryMessageMapper memoryMessageMapper;

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        QueryWrapper<MemoryMessage> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("uid", memoryId);
        MemoryMessage memoryMessage = memoryMessageMapper.selectOne(queryWrapper);
        if (memoryMessage != null) {
            return ChatMessageDeserializer.messagesFromJson(memoryMessage.getMessage());
        } else {
            return new ArrayList<>();
        }
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        MemoryMessage memoryMessage = memoryMessageMapper.selectOne(
            new QueryWrapper<MemoryMessage>().eq("uid", memoryId)
        );
        if (memoryMessage != null) {
            memoryMessage.setMessage(ChatMessageSerializer.messagesToJson(messages));
            memoryMessage.setUpdateTime(new Date());
            memoryMessageMapper.updateById(memoryMessage);
        } else {
            memoryMessage = new MemoryMessage();
            memoryMessage.setUid(String.valueOf(memoryId));
            memoryMessage.setMessage(ChatMessageSerializer.messagesToJson(messages));
            memoryMessage.setCreateTime(new Date());
            memoryMessage.setUpdateTime(new Date());
            memoryMessageMapper.insert(memoryMessage);
        }
    }

    @Override
    public void deleteMessages(Object memoryId) {
        memoryMessageMapper.delete(new QueryWrapper<MemoryMessage>().eq("uid", memoryId));
    }
}

3. 配置持久化 Memory Provider

@Configuration
public class ChatMemoryConfig {

    @Bean("lostChatMemoryProvider")
    public ChatMemoryProvider lostChatMemoryProvider(MySqlChatMemoryStore store) {
        return (id) -> MessageWindowChatMemory.builder()
                .maxMessages(28)
                .id(id)
                .chatMemoryStore(store)
                .build();
    }
}

关键技术点:

  1. ChatMemory vs ChatMemoryProvider

    • ChatMemory:单个记忆实例,无会话隔离
    • ChatMemoryProvider:工厂模式,根据 memoryId 创建/获取记忆实例
    • 多用户场景必须使用 ChatMemoryProvider
  2. MessageWindowChatMemory

    • 窗口机制:只保留最近 N 条消息
    • 防止上下文过长:避免超出 LLM 的 Token 限制
    • 成本控制:减少每次请求的 Token 数量
    • 项目中设置为 10-28 条,根据业务复杂度调整
  3. 序列化机制

    • ChatMessageSerializer.messagesToJson():将消息列表转为 JSON
    • ChatMessageDeserializer.messagesFromJson():从 JSON 恢复消息列表
    • 支持所有消息类型(SystemMessage、UserMessage、AiMessage)
  4. 性能考虑

    • 每次对话都会读写数据库
    • 高频场景可考虑 Redis 缓存
    • 定期清理过期会话(如 7 天前的记录)
3.5.3 项目中的测试用例

多会话隔离测试(MemoryTests.java - testChatMemory3):

@Test
void testChatMemory3() {
    // 会话 1
    String chat = memoryAssistantProvider.chat("1", "你好,我的名字叫alison");
    System.out.println(chat);
    String chat2 = memoryAssistantProvider.chat("1", "我的名字叫什么");
    System.out.println(chat2); // 输出: 你的名字是 alison
    
    // 会话 2(完全隔离)
    String chat3 = memoryAssistantProvider.chat("2", "我的名字叫什么");
    System.out.println(chat3); // 输出: 我不知道你的名字
    
    String chat4 = memoryAssistantProvider.chat("2", "我的名字叫alison2");
    System.out.println(chat4);
    String chat5 = memoryAssistantProvider.chat("2", "我的名字叫什么");
    System.out.println(chat5); // 输出: 你的名字是 alison2
}

MySQL 持久化测试(MemoryTests.java - testChatMemory4):

@Test
void testChatMemory4() {
    // 使用 MySQL 存储,重启后数据不丢失
    String chat = mySqlMemoryAssistantProvider.chat("1", "你好,我的名字叫alison");
    System.out.println(chat);
    String chat2 = mySqlMemoryAssistantProvider.chat("1", "我的名字叫什么");
    System.out.println(chat2); // 输出: 你的名字是 alison
}

3.6 Tools & Function Calling - 工具调用

3.6.1 什么是 Function Calling?

Function Calling 允许大模型在需要时调用外部函数,实现与外部系统的交互。

工作流程:

用户 → 应用程序 → LLM 大模型 → Function/Tool → 应用程序 → LLM 大模型 → 用户
3.6.2 项目中的工具实现

1. 计算器工具(CalculatorTools.java):

@Component("calculatorTools")
public class CalculatorTools {

    @Tool(name = "sum", value = "计算两个数字的和")
    double sum(
        @ToolMemoryId String memoryId,
        @P(value = "加数1", required = true) double a,
        @P(value = "加数2", required = true) double b
    ) {
        System.out.println(memoryId + ",加法运算:" + a + " + " + b + "=" + (a + b));
        return a + b;
    }
}

2. 失物查询工具(LostRegTools.java):

关键注解:

  • @Tool:标记方法为工具
    • name:工具名称
    • value:工具描述(帮助大模型理解何时调用)
  • @P:参数描述
    • value:参数说明
    • required:是否必需
  • @ToolMemoryId:接收会话 ID
3.6.3 业务工具示例

1. 查询工具

@Slf4j
@Component("lostRegTools")
public class LostRegTools {
    
    @Autowired
    private LostRegMapper lostRegMapper;

    @Tool(value = "根据手机号获取用户登记丢失物品信息", 
          name = "toolGetLostRegListByPhone")
    public List<LostReg> toolGetLostRegListByPhone(
        @P("用户手机号") String phone
    ) {
        QueryWrapper<LostReg> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("phone", phone);
        return lostRegMapper.selectList(queryWrapper);
    }
}

2. RAG 查询工具

@Slf4j
@Component("queryLostItemTools")
public class QueryLostItemTools {
    
    @Autowired
    private ContentRetriever contentRetriever;

    @Tool(value = "根据物品名称和特征查询物品登记信息", 
          name = "queryLostItem")
    public List<String> queryLostItem(
        @P("物品名称和特征") String lostItemFeature
    ) {
        log.info("查询物品:{}", lostItemFeature);
        List<Content> retrieve = contentRetriever.retrieve(
            Query.from(lostItemFeature)
        );
        return retrieve.stream()
            .map(r -> r.textSegment().text())
            .toList();
    }
}
3.6.3 使用工具的 AI Service
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "chatMemoryProvider",
        tools = {"calculatorTools"})
public interface ToolsAssistant {
    String chat(@MemoryId String memoryId, @UserMessage String message);
}

关键技术点:

  1. @Tool 注解参数

    • name:工具名称(建议纯英文,避免 LLM 匹配失败)
    • value:工具描述,非常重要!LLM 根据描述决定何时调用
    • 描述要清晰说明工具的用途、适用场景、参数含义
  2. @P 注解

    • value:参数描述,帮助 LLM 理解参数含义
    • required:是否必需参数
    • 描述越准确,LLM 传参越正确
  3. @ToolMemoryId

    • 接收当前会话的 memoryId
    • 可用于日志追踪、权限校验、会话隔离等
    • 不是所有工具都需要,按需使用
  4. 工具返回值

    • 支持基本类型、POJO、List 等
    • 返回值会被序列化为 JSON 传给 LLM
    • LLM 会根据返回值继续推理或生成最终回答
  5. 工具组合使用

    @AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
            chatModel = "myQwenChatModel",
            chatMemoryProvider = "lostChatMemoryProvider",
            tools = {"lostRegChatHistoryTools", "lostPropertyTools",
                     "lostRegTools", "queryLostItemTools"})
    public interface LostPropertyAssistant {
        // ...
    }
    
    • 一个 AI Service 可以注册多个工具
    • LLM 会根据上下文自动选择合适的工具
    • 工具过多可能影响性能,建议控制在 10 个以内
3.6.4 项目中的测试用例

工具调用测试(ToolsTests.java):

3.6.6 多工具组合
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "lostChatMemoryProvider",
        tools = {
            "lostRegChatHistoryTools",
            "lostPropertyTools",
            "lostRegTools",
            "queryLostItemTools"
        })
public interface LostPropertyAssistant {
    // ...
}

3.7 RAG - 检索增强生成

3.7.1 什么是 RAG?

RAG(Retrieval-Augmented Generation)检索增强生成,通过结合向量数据库和 LLM,让 AI 能够基于私有知识库回答问题。

RAG 工作流程:

1. 文档加载 → 2. 文档分割 → 3. 文本向量化 → 4. 存入向量数据库
                                                        ↓
用户提问 → 5. 问题向量化 → 6. 相似度检索 → 7. 返回相关片段 → 8. LLM 生成答案
3.7.2 项目中的 RAG 配置

RAG 配置类(RagConfig.java):

@ConfigurationProperties(prefix = "langchain4j.community.dashscope.embedding-model")
@Data
@Configuration
public class RagConfig {
    private String modelName;  // text-embedding-v4
    private String apiKey;

    @Autowired
    private PineconeProperties pineconeProperties;

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

    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(EmbeddingModel embeddingModel) {
        return PineconeEmbeddingStore.builder()
                .apiKey(pineconeProperties.getApiKey())
                .index(pineconeProperties.getIndex())  // my-lost-index
                .nameSpace(pineconeProperties.getNameSpace())  // lost-space
                .createIndex(
                    PineconeServerlessIndexConfig.builder()
                            .cloud(pineconeProperties.getCloud())  // AWS
                            .region(pineconeProperties.getRegion())  // us-east-1
                            .dimension(embeddingModel.dimension())  // 向量维度
                            .build()
                )
                .build();
    }

    @Bean
    public ContentRetriever contentRetriever(
            EmbeddingModel embeddingModel,
            EmbeddingStore<TextSegment> embeddingStore
    ) {
        return EmbeddingStoreContentRetriever.builder()
                .embeddingModel(embeddingModel)
                .embeddingStore(embeddingStore)
                .minScore(0.5)     // 最小相似度阈值
                .maxResults(1)     // 返回结果数量
                .build();
    }
}

关键技术点:

  1. EmbeddingModel(嵌入模型)

    • 将文本转换为向量(数值数组)
    • 项目使用 text-embedding-v4,维度为 1024
    • 语义相似的文本,向量距离更近
    • 嵌入模型的质量直接影响检索效果
  2. EmbeddingStore(向量存储)

    • 存储文本片段及其对应的向量
    • 项目使用 Pinecone(云托管向量数据库)
    • 其他可选:Chroma、Milvus、Elasticsearch 等
    • nameSpace:命名空间,用于逻辑隔离(如同一索引存不同类型数据)
  3. ContentRetriever(内容检索器)

    • 封装了"向量化 → 检索 → 返回"的完整流程
    • minScore:相似度阈值(0-1),过滤低质量结果
    • maxResults:返回最相关的 N 条结果
    • 被 AI Service 自动注入,在提示词中通过 {{contents}} 使用
  4. Pinecone 配置(application.yml):

    pinecone:
      api-key: pcsk_xxx
      cloud: AWS
      region: us-east-1
      name-space: lost-space
      index: my-lost-index
    
3.7.3 项目中的文档处理

自定义分割器(MyAnswerSplit.java):

3.7.4 项目中的入库实践

手动入库流程(RagTests.java - testStoreTaobaoRag):

3.7.5 项目中的检索实践

基础检索(RagTests.java - testEmbeddingStoreSearch):

3.7.6 RAG 与 AI Service 集成
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        contentRetriever = "contentRetriever")  // 注入内容检索器
public interface TaobaoAssistant {
    
    @SystemMessage(fromResource = "part1-prompt.txt")
    String search(@MemoryId String memoryId, @UserMessage String message);
}

提示词中使用检索内容:

你是一个淘宝客服助手。

相关商品信息:
{{contents}}

请根据以上信息回答用户问题。
3.7.7 业务场景:失物查询
@Service
public class AiChatService {
    
    @Autowired
    private EmbeddingModel embeddingModel;
    
    @Autowired
    private EmbeddingStore<TextSegment> embeddingStore;

    /**
     * 批量索引失物信息
     */
    public String embeddingIndex() {
        // 1. 从数据库查询所有失物
        List<LostProperty> lostProperties = lostPropertyMapper.selectList(null);
        
        // 2. 转换为 JSON 字符串
        List<String> propertyList = lostProperties.stream()
                .map(JSONUtil::toJsonStr)
                .toList();
        
        // 3. 创建文本片段
        List<TextSegment> textSegments = propertyList.stream()
                .map(TextSegment::from)
                .toList();
        
        // 4. 向量化
        Response<List<Embedding>> embeddedAll = embeddingModel.embedAll(textSegments);
        
        // 5. 存入向量数据库
        embeddingStore.addAll(embeddedAll.content(), textSegments);
        
        return "ok";
    }

    /**
     * 语义搜索失物
     */
    public List<String> embeddingQuery(String message) {
        EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
                .queryEmbedding(embeddingModel.embed(message).content())
                .minScore(0.7)
                .maxResults(2)
                .build();
        
        EmbeddingSearchResult<TextSegment> search = embeddingStore.search(request);
        
        return search.matches().stream()
                .map(match -> match.embedded().text())
                .toList();
    }
}

3.8 MCP - Model Context Protocol

3.8.1 什么是 MCP?

MCP(Model Context Protocol)是一种标准化协议,允许 AI 模型安全地访问外部工具和数据源。通过 MCP,大模型可以调用远程服务,如搜索引擎、数据库、API 等。

3.8.2 MCP 配置
@Configuration
public class McpConfig {

    @Value("${bigmodel.api-key}")
    private String apiKey;

    @Bean("mcpToolProvider")
    public McpToolProvider mcpToolProvider() {
        // 1. 创建传输层(HTTP SSE)
        McpTransport transport = new HttpMcpTransport.Builder()
                .sseUrl("https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization=" + apiKey)
                .logRequests(true)
                .logResponses(true)
                .build();
        
        // 2. 创建 MCP 客户端
        McpClient mcpClient = new DefaultMcpClient.Builder()
                .key("myMcpClient")
                .transport(transport)
                .build();
        
        // 3. 创建工具提供者
        return McpToolProvider.builder()
                .mcpClients(mcpClient)
                .build();
    }
}
3.8.3 使用 MCP 工具
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemory = "chatMemory",
        toolProvider = "mcpToolProvider")  // 注入 MCP 工具提供者
public interface McpAssistant {

    String chat(@UserMessage String message);

    Flux<String> chatStream(@UserMessage String message);
}
3.8.4 测试 MCP
@Test
void testMcp() {
    String chat = mcpAssistant.chat("Java 行情如何?");
    System.out.println(chat);
    // MCP 会自动调用网络搜索工具,获取最新的 Java 行业信息
}
3.8.5 MCP vs 本地 Tools
特性MCP本地 Tools
部署位置远程服务本地代码
扩展性高(无需修改代码)低(需重新编译)
安全性需要认证授权完全可控
延迟较高(网络开销)较低
适用场景第三方服务集成内部业务逻辑

3.9 Streaming - 流式响应

3.9.1 为什么需要流式响应?
  • 提升用户体验:用户可以看到实时生成的内容
  • 降低感知延迟:无需等待完整响应
  • 节省资源:可以提前终止不需要的响应
3.9.2 流式 AI Service
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "lostChatMemoryProvider")
public interface LostIntentionAiAssistant {

    // 普通响应
    String chat(@MemoryId String memoryId, @UserMessage String message);
    
    // 流式响应
    Flux<String> chatStream(@MemoryId String memoryId, @UserMessage String message);
}
3.9.3 Controller 层暴露流式接口
@RestController
@RequestMapping("/lost")
public class LostController {
    
    @Autowired
    private LostIntentionAiAssistant assistant;

    @GetMapping(value = "/chatStream", 
                produces = "text/event-stream;charset=utf-8")
    public Flux<String> chatStream(
            @RequestParam("userId") String userId,
            @RequestParam("message") String message
    ) {
        return assistant.chatStream(userId, message);
    }
}
3.9.4 前端消费流式数据
const eventSource = new EventSource('/api/lost/chatStream?userId=123&message=你好');

eventSource.onmessage = function(event) {
    console.log('收到数据:', event.data);
    // 追加到页面
    document.getElementById('response').innerHTML += event.data;
};

eventSource.onerror = function(error) {
    console.error('Error:', error);
    eventSource.close();
};
3.9.5 流式与非流式对比
// 非流式:等待完整响应
String response = assistant.chat("123", "写一首诗");
System.out.println(response); // 一次性输出整首诗

// 流式:逐字输出
assistant.chatStream("123", "写一首诗")
    .doOnNext(chunk -> System.out.print(chunk)) // 逐块打印
    .blockLast(); // 等待完成

四、企业级实战:失物招领系统

4.1 系统架构设计

4.1.1 业务需求

构建一个智能失物招领系统,支持:

  • 丢失物品登记
  • 拾取物品登记
  • 失物智能查询
  • 多轮对话引导
  • 历史记录管理
4.1.2 技术架构
┌─────────────┐
│   前端界面   │
└──────┬──────┘
       │ HTTP/WebSocket
       ↓
┌─────────────────────────────────┐
│      LostController (REST API)  │
└──────┬──────────────────────────┘
       │
       ↓
┌─────────────────────────────────┐
│     AiChatService (业务编排)     │
│  ┌──────────────────────────┐   │
│  │  意图识别 → 路由分发      │   │
│  └──────────────────────────┘   │
└──────┬──────────────────────────┘
       │
       ├──────────────┬──────────────┐
       ↓              ↓              ↓
┌────────────┐ ┌──────────┐ ┌──────────┐
│失物登记 AI  │ │拾取登记 AI│ │失物查询 AI│
│  (Tools)   │ │ (Tools)  │ │  (RAG)   │
└──────┬─────┘ └────┬─────┘ └────┬─────┘
       │             │            │
       ↓             ↓            ↓
┌─────────────────────────────────────┐
│   MySQL (业务数据 + 对话历史)        │
│   Pinecone (向量数据库)              │
└─────────────────────────────────────┘
4.1.3 核心组件
@RestController
@RequestMapping("/lost")
@RequiredArgsConstructor
public class LostController {
    private final LostIntentionAiAssistant lostIntentionAiAssistant;
    private final AiChatService aiChatService;
    private final LostChatMemoryService lostChatMemoryService;
    private final LostChatPropertyService lostChatPropertyService;

    // 普通聊天
    @GetMapping("/chat")
    public String chat(@RequestParam("userId") String userId,
                       @RequestParam("message") String message) {
        return lostIntentionAiAssistant.chat(userId, message);
    }

    // 流式聊天
    @GetMapping(value = "/chatStream", 
                produces = "text/event-stream;charset=utf-8")
    public Flux<String> chatStream(@RequestParam("userId") String userId,
                                   @RequestParam("message") String message) {
        return lostIntentionAiAssistant.chatStream(userId, message);
    }

    // 获取会话历史
    @GetMapping("/getChatHistory")
    public R<IPage<LostRegChatHistory>> getChatHistoryPagelist(
            @RequestParam("sessionId") String sessionId,
            PageParam pageParam
    ) {
        return R.success(
            lostChatMemoryService.getChatHistoryPagelist(sessionId, pageParam)
        );
    }

    // 清空会话历史
    @DeleteMapping("/clearChatHistory")
    public void clearChatHistory(
            @RequestParam("sessionId") String sessionId,
            @RequestParam(value = "historyId", required = false) String historyId
    ) {
        lostChatMemoryService.clearChatHistory(sessionId, historyId);
    }

    // 向量索引
    @GetMapping("/embeddingIndex")
    public String embeddingIndex() {
        return aiChatService.embeddingIndex();
    }

    // 向量查询
    @GetMapping("/embeddingQuery")
    public List<String> embeddingQuery(@RequestParam("message") String message) {
        return aiChatService.embeddingQuery(message);
    }
}

4.2 意图识别模块

4.2.1 意图分类

系统识别四种用户意图:

  1. 丢失信息登记(intention=1):用户丢失物品,需要登记
  2. 找到失物登记(intention=2):用户捡到物品,需要登记
  3. 失物查询(intention=3):用户想查询是否有匹配的失物
  4. 其他(intention=4):无关话题或其他需求
4.2.2 意图识别 AI Service
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "lostChatMemoryProvider")
@SystemMessage(fromResource = "intention-prompt.txt")
public interface LostIntentionAiAssistant {

    String chat(@MemoryId String memoryId, @UserMessage String message);
    
    Flux<String> chatStream(@MemoryId String memoryId, @UserMessage String message);

    @UserMessage("当前sessionId:{{sessionId}};用户消息:{{message}}")
    IntentionOutput intention(
        @V("sessionId") @MemoryId String sessionId, 
        @V("message") String message
    );
}
4.2.3 意图提示词设计
# 角色
你是一位专业的失物招领工作人员,具备高效、负责的工作态度。

## 技能
### 技能 1:判断用户意图
- 每次对话都需要通过读取用户历史对话内容工具获取用户对话历史
- 仔细分析用户输入内容,精准判断用户需求
- 对用户的输入意图进行标记,分类如下:
  1. 丢失信息登记
  2. 找到失物登记
  3. 失物查询
  4. 其他

# 历史说明(严格遵循)
- 历史记录字段 role: role=0 为用户消息;role=1 为AI消息
- 判定时,优先使用"当前用户消息 + 最近的若干条用户历史消息(role=0)"

# 意图标签与触发规则(强约束)
- 1 丢失信息登记:
   - 典型触发词:丢了、不见了、掉了、遗失、找不到、帮我登记
- 2 找到失物登记:
   - 典型触发词:我捡到/拾到/捡起/找到了一件...
   - 强优先规则:若最近的用户消息中出现"拾得类"表达,直接判定 intention=2
- 3 失物查询:
   - 典型触发词:查询/查一下/有没有...记录/有没有人捡到
- 4 其他:不属于以上三类的请求

## 限制
- 只回答与失物招领相关的问题
- 所输出的内容需条理清晰、简洁明了
4.2.4 意图路由
@Slf4j
@Service
@RequiredArgsConstructor
public class AiChatService {
    
    private final LostIntentionAiAssistant lostIntentionAiAssistant;
    private final LostRegAiAssistant lostRegAiAssistant;
    private final LostPropertyAssistant lostPropertyAssistant;
    private final LostRegMapper lostRegMapper;
    private final LostPropertyMapper lostPropertyMapper;

    @ChatFlow  // AOP 切面:自动记录对话历史
    public String chatSse(String userId, String message) {
        // 1. 意图识别
        IntentionOutput intentionOutput = lostIntentionAiAssistant.intention(userId, message);
        log.info("用户意图:{}", JSONUtil.toJsonStr(intentionOutput));
        
        Integer intention = intentionOutput.getIntention();
        
        // 2. 根据意图路由
        return switch (intention) {
            case 1 -> this.regLost(userId, message, intention);      // 失物登记
            case 2 -> this.findRegLostProperty(userId, message);     // 拾取登记
            case 3 -> this.findLostProperty(userId, message);        // 失物查询
            case 4 -> intentionOutput.getOutput();                   // 其他
            default -> intentionOutput.getOutput();
        };
    }

    private String regLost(String userId, String message, int intention) {
        LostRegOutput output = lostRegAiAssistant.lostReg(userId, message);
        log.info("失物登记结果:{}", JSONUtil.toJsonStr(output));
        
        // 如果登记完成,保存到数据库
        if (1 == intention && output.getIsCompleted()) {
            LostReg lostReg = new LostReg();
            BeanUtil.copyProperties(output, lostReg);
            lostRegMapper.insertOrUpdate(lostReg);
        }
        
        return output.getOutput();
    }

    private String findRegLostProperty(String userId, String message) {
        LostPropertyOutput output = lostPropertyAssistant.regProperty(userId, message);
        log.info("拾取登记结果:{}", JSONUtil.toJsonStr(output));
        
        if (output.isCompleted()) {
            LostProperty lostProperty = new LostProperty();
            BeanUtil.copyProperties(output, lostProperty);
            lostPropertyMapper.insertOrUpdate(lostProperty);
        }
        
        return output.getOutput();
    }

    private String findLostProperty(String userId, String message) {
        StringBuilder sb = new StringBuilder();
        lostPropertyAssistant.queryLostProperty(userId, message)
            .doOnNext(sb::append)
            .blockLast();
        
        log.info("失物查询结果:{}", sb.toString());
        return sb.toString();
    }
}

4.3 多轮对话与信息收集

4.3.1 失物登记流程

目标:通过多轮对话,引导用户提供完整的失物信息

所需字段

  • 用户姓名
  • 用户手机号
  • 失物名称
  • 失物特征(颜色、材质、尺寸、特殊标识等)
4.3.2 失物登记 AI Service
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "lostChatMemoryProvider",
        tools = {"lostRegChatHistoryTools", "lostRegTools"})
@SystemMessage(fromResource = "lostreg-prompt.txt")
public interface LostRegAiAssistant {

    @UserMessage("当前sessionId:{{sessionId}};用户消息:{{message}}")
    LostRegOutput lostReg(
        @V("sessionId") @MemoryId String sessionId, 
        @V("message") String message
    );
}
4.3.3 登记提示词设计
# 角色
你是一位专业且尽责的失物登记专员,始终秉持耐心、细致的态度。

## 技能
### 技能 1:登记失物信息
- 每次对话都需要通过读取用户历史对话内容工具获取用户对话历史
- 如果是补充丢失信息,要求用户告诉姓名和手机号码
- 如果用户没有传手机号,不得随意自动生成手机号,必须是用户输入的手机号

- 若用户已经输入了手机号,调用根据手机号查询丢失登记信息工具
- 若能查询到数据,将其与用户已输入的物品信息进行匹配
- 最多返回一条已经匹配的数据,并填充id字段

- 当用户提供失物相关信息时,从中提取物品信息和相关特征
- 主动询问用户的姓名和手机号,并准确记录
- 详细引导用户清晰描述失物的特征,包括颜色、材质、尺寸、特殊标识等

- 根据用户输入情况,准确判断是否完成登记,并标记"true"或"false"
- 若判断为"false",清晰告知用户还缺少哪些信息
- 若用户表示没有更多信息或特征,也可以完成登记

## 限制
- 仅处理用户与丢失物品登记紧密相关的内容
- 所输出的内容需严格按照要求的格式进行组织
- 要充分结合用户的历史对话记录,自然地承上启下
- 禁止机械重复话术,禁止重复或者相似的回复
- 如果判断完成登记,必须要准确告诉用户已经完成登记
4.3.4 对话示例
用户:我丢了一个钱包
AI:您好!请问您的姓名和手机号是多少?另外,能详细描述一下钱包的特征吗?比如颜色、品牌、里面有什么物品等。

用户:我叫张三,电话15655524410,黑色的LV钱包
AI:好的,张三先生。您丢失的是一个黑色的LV钱包。请问钱包里还有什么物品吗?比如现金、银行卡等。

用户:里面有400块钱和三张中国银行卡
AI:明白了。让我确认一下信息:
- 姓名:张三
- 手机号:15655524410
- 失物名称:钱包
- 失物特征:黑色、LV品牌、内有400元现金和三张中国银行卡

请问还有需要补充的信息吗?如果没有,我将为您完成登记。

用户:没有了
AI:好的,已完成登记!我们会尽快帮您寻找。如果有人拾取到您的钱包,我们会第一时间联系您。
4.3.5 工具:查询历史记录
@Component("lostRegChatHistoryTools")
public class LostRegChatHistoryTools {
    
    @Autowired
    private LostRegChatHistoryMapper chatHistoryMapper;

    @Tool(value = "获取用户历史对话记录", name = "toolGetRegChatHistory")
    public List<LostRegChatHistory> toolGetRegChatHistory(
        @P("会话ID") String sessionId
    ) {
        QueryWrapper<LostRegChatHistory> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("session_id", sessionId)
                   .orderByDesc("create_time")
                   .last("LIMIT 10");
        return chatHistoryMapper.selectList(queryWrapper);
    }
}

4.4 RAG 智能查询

4.4.1 查询流程
用户提问 → 意图识别(查询) → 向量化 → 相似度检索 → 返回匹配结果 → LLM 生成回答
4.4.2 查询 AI Service
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = ChatConfig.QWEN_STREAM_BEAN,
        chatMemoryProvider = "lostChatMemoryProvider",
        tools = {"lostRegChatHistoryTools", "lostPropertyTools",
                 "lostRegTools", "queryLostItemTools"})
public interface LostPropertyAssistant {

    @SystemMessage(fromResource = "lostproperty-query-prompt.txt")
    @UserMessage("当前sessionId:{{sessionId}};用户消息:{{message}}")
    Flux<String> queryLostProperty(
        @V("sessionId") @MemoryId String sessionId, 
        @V("message") String message
    );
}
4.4.3 RAG 查询工具
@Slf4j
@Component("queryLostItemTools")
public class QueryLostItemTools {
    
    @Autowired
    private ContentRetriever contentRetriever;

    @Tool(value = "根据物品名称和特征查询物品登记信息", 
          name = "queryLostItem")
    public List<String> queryLostItem(
        @P("物品名称和特征") String lostItemFeature
    ) {
        log.info("根据物品名称和特征查询物品登记信息,{}", lostItemFeature);
        
        // 向量检索
        List<Content> retrieve = contentRetriever.retrieve(
            Query.from(lostItemFeature)
        );
        
        List<String> list = retrieve.stream()
            .map(r -> r.textSegment().text())
            .toList();
        
        log.info("查询结果:{}", JSONUtil.toJsonStr(list));
        return list;
    }
}
4.4.4 查询提示词
# 角色
你是一位专业的失物查询助手。

## 技能
- 使用 queryLostItem 工具查询数据库中是否有匹配的失物
- 根据查询结果,友好地告知用户
- 如果有匹配项,提供联系方式
- 如果没有匹配项,建议用户留下联系方式,待有匹配时通知

## 限制
- 仅回答与失物查询相关的内容
- 语气要温和、专业
4.4.5 查询示例
用户:有没有人捡到一个黑色的LV钱包?
AI:[调用 queryLostItem 工具]
     [检索到 2 条相似记录]
     
     我找到了 2 条可能匹配的记录:
     
     1. 拾取时间:2024-01-15
        物品描述:黑色钱包,内有身份证和银行卡
        联系人:李四 138xxxx1234
        
     2. 拾取时间:2024-01-18
        物品描述:黑色皮质钱包,LV品牌
        联系人:王五 139xxxx5678
     
     请您核对是否是您的钱包,并联系相关人员确认。

4.5 AOP 切面记录

4.5.1 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChatFlow {
}
4.5.2 AOP 切面实现
@Slf4j
@Component
@Aspect
public class ChatFlowAop {
    
    @Autowired
    private LostRegChatHistoryMapper lostRegChatHistoryMapper;

    private final String userRole = "0"; // 用户消息
    private final String aiRole = "1";   // AI消息

    @Pointcut("@annotation(com.byterun.ailangchain4jdemo.part2.lostsystem.aop.ChatFlow)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        String sessionId = (String) args[0];
        String message = (String) args[1];
        
        log.info("sessionId:{}, message:{}", sessionId, message);
        
        // 1. 保存用户消息
        this.saveChatMemory(sessionId, message, userRole);
        
        // 2. 执行目标方法
        Object result = joinPoint.proceed();
        
        // 3. 保存 AI 回复
        this.saveChatMemory(sessionId, (String) result, aiRole);
        
        return result;
    }

    private void saveChatMemory(String sessionId, String message, String role) {
        LostRegChatHistory history = new LostRegChatHistory();
        history.setSessionId(sessionId);
        history.setRole(role);
        history.setContent(message);
        lostRegChatHistoryMapper.insert(history);
    }
}
4.5.3 使用切面
@Service
public class AiChatService {
    
    @ChatFlow  // 自动记录对话历史
    public String chatSse(String userId, String message) {
        // 业务逻辑...
    }
}
4.5.4 历史管理
@Service
public class LostChatMemoryService {
    
    @Autowired
    private LostRegChatHistoryMapper chatHistoryMapper;

    /**
     * 分页查询会话历史
     */
    public IPage<LostRegChatHistory> getChatHistoryPagelist(
            String sessionId, PageParam pageParam
    ) {
        Page<LostRegChatHistory> page = new Page<>(
            pageParam.getPageNum(), 
            pageParam.getPageSize()
        );
        
        QueryWrapper<LostRegChatHistory> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("session_id", sessionId)
                   .orderByAsc("create_time");
        
        return chatHistoryMapper.selectPage(page, queryWrapper);
    }

    /**
     * 清空会话历史
     */
    public void clearChatHistory(String sessionId, String historyId) {
        if (StrUtil.isNotBlank(historyId)) {
            // 删除单条记录
            chatHistoryMapper.deleteById(historyId);
        } else {
            // 删除整个会话
            QueryWrapper<LostRegChatHistory> queryWrapper = new QueryWrapper<>();
            queryWrapper.eq("session_id", sessionId);
            chatHistoryMapper.delete(queryWrapper);
        }
    }
}

五、关键技术点深度解析

5.1 @AiService 装配模式

5.1.1 EXPLICIT vs AUTOMATIC

EXPLICIT(显式模式)- 项目采用

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "lostChatMemoryProvider",
        tools = {"lostRegTools", "queryLostItemTools"})
public interface LostPropertyAssistant {
    // ...
}

优点

  • 明确依赖:清楚指定使用哪个 Bean,避免歧义
  • 灵活配置:可以为不同 AI Service 配置不同的模型、记忆策略
  • 便于测试:可以注入 Mock Bean 进行单元测试
  • 项目实践:所有 AI Service 都使用 EXPLICIT 模式

AUTOMATIC(自动模式)

@AiService  // 自动从 Spring 容器查找 ChatModel、ChatMemory 等
public interface AutoAssistant {
    String chat(String message);
}

适用场景

  • 简单应用,只有一个 ChatModel
  • 快速原型开发
  • 不需要特殊配置的场景
5.1.2 常见配置组合

1. 基础对话

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel")

2. 带记忆的对话

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "lostChatMemoryProvider")

3. 带工具的对话

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        tools = {"calculatorTools"})

4. RAG 增强对话

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        contentRetriever = "contentRetriever")

5. MCP 远程工具

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        toolProvider = "mcpToolProvider")

6. 完整配置(失物招领系统)

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "lostChatMemoryProvider",
        tools = {"lostRegChatHistoryTools", "lostPropertyTools",
                 "lostRegTools", "queryLostItemTools"})

5.2 会话隔离机制

5.2.1 MemoryId 的作用

问题:多用户同时使用时,如何保证对话不混乱?

解决方案:通过 @MemoryId 实现会话隔离

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        chatMemoryProvider = "lostChatMemoryProvider")
public interface LostIntentionAiAssistant {
    
    // memoryId 用于标识不同会话
    String chat(@MemoryId String memoryId, @UserMessage String message);
}

工作流程

用户A (userId=1) → chat("1", "我丢了钱包") → Memory[id=1] 存储
用户B (userId=2) → chat("2", "我捡到手机") → Memory[id=2] 存储
用户A (userId=1) → chat("1", "叫什么名字") → 从 Memory[id=1] 读取 → 记得是钱包
5.2.2 ChatMemoryProvider 工厂模式
@Bean("lostChatMemoryProvider")
public ChatMemoryProvider lostChatMemoryProvider(MySqlChatMemoryStore store) {
    return (id) -> MessageWindowChatMemory.builder()
            .maxMessages(28)
            .id(id)  // 关键:每个 id 对应独立的 ChatMemory
            .chatMemoryStore(store)
            .build();
}

内部逻辑

// 伪代码
Map<Object, ChatMemory> memoryCache = new ConcurrentHashMap<>();

public ChatMemory get(Object memoryId) {
    return memoryCache.computeIfAbsent(memoryId, id -> {
        return MessageWindowChatMemory.builder()
                .id(id)
                .chatMemoryStore(store)
                .maxMessages(28)
                .build();
    });
}

关键点

  • 首次调用某个 memoryId 时创建新的 ChatMemory
  • 后续调用同一个 memoryId 时复用已有实例
  • 线程安全(ConcurrentHashMap)
5.2.3 会话生命周期管理

创建:首次使用该 memoryId 时自动创建

读取:每次对话从 MySqlChatMemoryStore 加载历史

更新:每次对话后保存最新历史到数据库

删除

@DeleteMapping("/clearChatHistory")
public void clearChatHistory(
        @RequestParam("sessionId") String sessionId,
        @RequestParam(value = "historyId", required = false) String historyId
) {
    lostChatMemoryService.clearChatHistory(sessionId, historyId);
}

最佳实践

  • 使用用户 ID 或会话 ID 作为 memoryId
  • 定期清理过期会话(如 30 天未活跃)
  • 监控内存使用,避免内存泄漏

5.3 工具调用原理

5.3.1 Function Calling 工作流程
1. 用户提问:"帮我查一下手机号 13800138000 的失物登记"2. LLM 分析:需要查询数据库 → 决定调用工具
                ↓
3. LLM 返回工具调用指令:
   {
     "tool": "toolGetLostRegListByPhone",
     "arguments": {"phone": "13800138000"}
   }4. LangChain4j 框架执行工具:
   lostRegTools.toolGetLostRegListByPhone("13800138000")
                ↓
5. 工具返回结果:
   [{"id": 1, "username": "张三", "lostItem": "钱包"}]6. 结果返回给 LLM
                ↓
7. LLM 生成最终回答:
   "找到 1 条记录:张三登记丢失了一个钱包"8. 返回给用户
5.3.2 工具描述的重要性

好的描述

@Tool(value = "根据手机号获取用户登记丢失物品信息", 
      name = "toolGetLostRegListByPhone")
public List<LostReg> toolGetLostRegListByPhone(
    @P("用户手机号") String phone
)

LLM 理解

  • 当用户提到"查询"、"手机号"、"登记"时调用
  • 参数 phone 需要传入手机号格式

差的描述

@Tool(value = "查询数据", name = "query")
public List<LostReg> query(String param)

问题

  • LLM 不知道何时调用
  • 不知道 param 应该传什么
  • 可能导致工具滥用或不调用
5.3.3 工具调用调试

开启日志

logging:
  level:
    dev.langchain4j: DEBUG

日志输出示例

DEBUG - Tool execution request: toolGetLostRegListByPhone({"phone":"13800138000"})
INFO  - 执行工具:toolGetLostRegListByPhone, 参数:13800138000
DEBUG - Tool execution result: [{"id":1,"username":"张三","lostItem":"钱包"}]

常见问题排查

  1. 工具未被调用

    • 检查 @Toolvalue 描述是否清晰
    • 检查 AI Service 是否正确注册了 tools
    • 查看 LLM 是否理解了用户需求
  2. 参数传递错误

    • 检查 @P 注解的描述是否准确
    • 检查参数类型是否匹配
    • 查看日志中的实际传参
  3. 工具执行失败

    • 检查工具代码是否有异常
    • 检查依赖的服务是否正常
    • 查看完整堆栈信息

5.4 RAG 工作流程

5.4.1 完整的 RAG 流程
【入库阶段】
1. 文档加载:ClassPathDocumentLoader.loadDocument("rag-taobao.md")
                ↓
2. 文档分割:DocumentByParagraphSplitter.split(document)
   → [片段1, 片段2, 片段3, ...]3. 文本向量化:embeddingModel.embedAll(textSegments)
   → [向量1, 向量2, 向量3, ...]4. 存入向量库:embeddingStore.addAll(embeddings, textSegments)
   Pinecone 数据库:
   - 向量1 ↔ "淘宝发货时间:通常24小时内发货"
   - 向量2 ↔ "淘宝退货政策:7天无理由退货"
   - ...

【检索阶段】
5. 用户提问:"多久可以发货"
                ↓
6. 问题向量化:embeddingModel.embed("多久可以发货")
   → 查询向量
                ↓
7. 相似度检索:embeddingStore.search(queryVector)
   → 计算余弦相似度
   → 排序:向量1 (0.92), 向量2 (0.45), ...
   → 过滤:minScore=0.5
   → 返回 Top-1:向量18. 构建提示词:
   System: 你是淘宝客服
   Context: 淘宝发货时间:通常24小时内发货
   User: 多久可以发货
                ↓
9. LLM 生成回答:"我们通常在下单后24小时内发货"10. 返回给用户
5.4.2 向量相似度计算

余弦相似度公式

cos(θ) = (A · B) / (||A|| × ||B||)

其中:
- A · B 是向量点积
- ||A|| 是向量 A 的模
- 结果范围:[-1, 1],越接近 1 越相似

示例

向量A("我喜欢语文"):[0.1, 0.5, -0.3, ..., 0.2]
向量B("我最喜欢的科目是什么"):[0.15, 0.48, -0.28, ..., 0.22]

相似度 = 0.87 (高度相关)

向量C("今天天气不错"):[-0.4, 0.1, 0.6, ..., -0.3]
相似度(A, C) = 0.12 (不相关)

项目中阈值选择

  • RAG 检索:minScore=0.5(宽松,保证召回)
  • 业务查询:minScore=0.7(严格,提高准确率)
5.4.3 ContentRetriever 与 AI Service 集成

配置

@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
        chatModel = "myQwenChatModel",
        contentRetriever = "contentRetriever")
public interface TaobaoAssistant {
    @SystemMessage(fromResource = "part1-prompt.txt")
    String search(@MemoryId String memoryId, @UserMessage String message);
}

提示词模板(part1-prompt.txt):

你是一个淘宝客服助手。

相关商品信息:
{{contents}}

请根据以上信息回答用户问题。

运行时

1. 用户提问:"多久发货"
        ↓
2. ContentRetriever 自动检索相关片段
        ↓
3. 替换 {{contents}} 为检索结果
        ↓
4. 发送给 LLM:
   System: 你是一个淘宝客服助手。
   相关商品信息:
   - 淘宝发货时间:通常24小时内发货
   
   User: 多久发货
        ↓
5. LLM 生成回答

5.5 向量相似度检索

5.5.1 EmbeddingSearchRequest 参数详解
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
        .queryEmbedding(embeddingModel.embed(message).content())
        .minScore(0.7)      // 最小相似度阈值
        .maxResults(2)      // 最大返回数量
        .build();

minScore(最小相似度)

效果适用场景
0.0-0.3非常宽松,返回大量结果探索性搜索
0.3-0.5宽松,可能有噪声RAG 知识库
0.5-0.7适中,平衡召回和准确通用场景
0.7-0.9严格,高准确率精确匹配
0.9-1.0非常严格,可能无结果去重、完全匹配

项目实践

  • RagConfig:minScore=0.5(RAG 检索,保证召回)
  • AiChatService:minScore=0.7(业务查询,提高准确)

maxResults(最大返回数)

  • 太小(1-2):可能遗漏相关信息
  • 适中(3-5):平衡上下文长度和相关性
  • 太大(>10):增加 LLM Token 消耗,可能引入噪声

项目实践

  • RAG 检索:maxResults=1(单一答案场景)
  • 业务查询:maxResults=2(提供多个选项)
5.5.2 检索结果处理
EmbeddingSearchResult<TextSegment> search = embeddingStore.search(request);

// 遍历所有匹配结果
for (EmbeddingMatch<TextSegment> match : search.matches()) {
    double score = match.score();              // 相似度得分
    String text = match.embedded().text();     // 原始文本
    Embedding embedding = match.embedding();   // 向量数据
    
    System.out.println("相似度: " + score);
    System.out.println("内容: " + text);
}

实际输出示例

相似度: 0.87
内容: {"id":1,"lostItem":"钱包","lostItemFeature":"黑色、LV品牌","phone":"13800138000"}

相似度: 0.72
内容: {"id":2,"lostItem":"手机","lostItemFeature":"黑色、iPhone13","phone":"13900139000"}
5.5.3 性能优化建议

1. 批量入库

// ✅ 推荐:批量操作
List<TextSegment> segments = ...;
Response<List<Embedding>> embeddings = embeddingModel.embedAll(segments);
embeddingStore.addAll(embeddings.content(), segments);

// ❌ 不推荐:逐个操作
for (TextSegment segment : segments) {
    Embedding embedding = embeddingModel.embed(segment).content();
    embeddingStore.add(embedding, segment);
}

2. 分页处理大数据量

int batchSize = 100;
for (int i = 0; i < totalRecords; i += batchSize) {
    List<LostProperty> batch = lostPropertyMapper.selectPage(...);
    // 处理批次...
}

3. 增量更新

// 只更新变化的记录
@EventListener
public void handleLostPropertyUpdated(LostPropertyUpdatedEvent event) {
    // 1. 删除旧向量
    embeddingStore.remove(event.getId());
    
    // 2. 添加新向量
    TextSegment segment = TextSegment.from(JSONUtil.toJsonStr(event.getData()));
    Embedding embedding = embeddingModel.embed(segment).content();
    embeddingStore.add(embedding, segment);
}

4. 缓存嵌入结果

@Cacheable(value = "embeddings", key = "#text")
public Embedding getCachedEmbedding(String text) {
    return embeddingModel.embed(text).content();
}

六、总结与展望

6.1 核心技术回顾

通过本项目的实践,我们系统地学习了 LangChain4j 的核心技术:

技术应用场景关键组件
AI Services声明式 AI 编程@AiService, @SystemMessage
Chat Models大模型集成QwenChatModel, ChatModel
Prompt Engineering提示词管理外部文件, 动态提示词
Structured Output数据提取@Description, POJO 映射
Memory对话记忆ChatMemoryStore, MessageWindowChatMemory
Tools函数调用@Tool, ToolProvider
RAG知识增强EmbeddingStore, ContentRetriever
MCP远程工具McpToolProvider, HttpMcpTransport
Streaming流式响应Flux<String>, StreamingChatModel

6.2 架构设计要点

  1. 分层架构:Controller → Service → AI Service → Tools/RAG
  2. 意图驱动:先识别意图,再路由到对应处理器
  3. 多轮对话:利用 Memory 实现上下文感知
  4. 工具增强:通过 Tools 扩展 AI 能力边界
  5. 知识增强:通过 RAG 接入私有知识库
  6. 可观测性:AOP 切面记录对话历史

6.3 未来发展方向

  1. Agent 化:构建更智能的自主 Agent
  2. 多模态:支持图片、音频等多模态输入
  3. 工作流引擎:可视化编排 AI 工作流
  4. 评估体系:建立 AI 输出质量评估机制
  5. 微调模型:针对特定领域微调专属模型

6.4 学习资源

6.5 结语

LangChain4j 为 Java 开发者提供了一套强大的 AI 应用开发工具。通过本项目的实践,我们不仅掌握了 LangChain4j 的核心技术,还构建了一个完整的企业级应用。

希望这份学习指南能帮助你快速上手 LangChain4j,构建属于自己的 AI 应用!


后端项目源码gitee.com/byterun/ai-…

前端项目源码gitee.com/byterun/ai-…

作者:byteRun
日期:2026-04-23


附录:完整代码结构

ai-langchain4j-demo/
├── pom.xml                          # Maven 依赖配置
├── README.md                        # 项目文档
│
├── src/main/
│   ├── java/com/byterun/ailangchain4jdemo/
│   │   ├── AiLangchain4jDemoApplication.java   # 启动类
│   │   │
│   │   ├── common/                               # 通用组件
│   │   │   ├── PageParam.java                    # 分页参数
│   │   │   └── R.java                            # 统一返回结果
│   │   │
│   │   ├── config/                               # 配置类
│   │   │   ├── ChatModelConfig.java              # 聊天模型配置
│   │   │   ├── MyMetaObjectHandler.java          # MyBatis 元对象处理器
│   │   │   └── RagConfig.java                    # RAG 配置(向量库、检索器)
│   │   │
│   │   ├── mapper/                               # 全局 Mapper
│   │   │   └── MemoryMessageMapper.java          # 记忆消息 Mapper
│   │   │
│   │   ├── model/                                # 全局数据模型
│   │   │   └── MemoryMessage.java                # 记忆消息实体
│   │   │
│   │   ├── part1/                                # Part 1: LangChain4j 基础示例
│   │   │   ├── datatype/                         # 数据类型示例
│   │   │   │   ├── DefaultPromptAssistant.java   # 默认 Prompt 助手
│   │   │   │   └── UserDataType.java             # 用户数据类型
│   │   │   │
│   │   │   ├── memory/                           # 记忆管理示例
│   │   │   │   ├── MemoryAssistant.java          # 记忆助手接口
│   │   │   │   ├── MemoryAssistant2.java         # 记忆助手接口 2
│   │   │   │   ├── MemoryAssistantProvider.java  # 记忆提供者
│   │   │   │   ├── MemoryChatConfig.java         # 记忆配置
│   │   │   │   ├── MySqlChatMemoryStore.java     # MySQL 记忆存储 ⭐
│   │   │   │   └── MySqlMemoryAssistantProvider.java # MySQL 记忆提供者
│   │   │   │
│   │   │   ├── prompt/                           # Prompt 工程
│   │   │   │   ├── MemoryPromptAssistant.java    # 记忆增强 Prompt
│   │   │   │   └── MySqlAssistantPromptProvider.java # MySQL Prompt 提供者
│   │   │   │
│   │   │   ├── rag/                              # RAG 示例
│   │   │   │   ├── MyAnswerSplit.java            # 自定义答案分割器
│   │   │   │   └── PineconeProperties.java       # Pinecone 配置属性
│   │   │   │
│   │   │   └── tools/                            # 工具调用示例
│   │   │   │   ├── CalculatorTools.java          # 计算器工具
│   │   │   │   └── ToolsAssistant.java           # 工具助手
│   │   │   │
│   │   │   └── mcp/                              # 工具调用示例
│   │   │       ├── McpConfig.java                # mcp配置
│   │   │       └── McpAssistant.java             # mcp助手
│   │   │
│   │   └── part2/                                # Part 2: 实际应用场景
│   │       │
│   │       ├── lostsystem/                       # 失物招领系统 ⭐核心模块
│   │       │   ├── LostController.java           # REST API 控制器
│   │       │   ├── aop/                          # 切面处理
│   │       │   │   ├── ChatFlow.java             # 聊天流程注解
│   │       │   │   └── ChatFlowAop.java          # 聊天流程切面
│   │       │   ├── assistant/                    # AI 助手接口
│   │       │   │   ├── LostIntentionAiAssistant.java  # 意图识别助手
│   │       │   │   ├── LostRegAiAssistant.java      # 失物登记助手
│   │       │   │   └── LostPropertyAssistant.java   # 失物查询助手
│   │       │   ├── bean/                         # 数据传输对象
│   │       │   │   └── ChatHistory.java          # 聊天历史 Bean
│   │       │   ├── config/                       # 模块配置
│   │       │   │   ├── ChatConfig.java           # 聊天配置
│   │       │   │   └── ChatMemoryConfig.java     # 记忆配置
│   │       │   ├── mapper/                       # 数据访问层
│   │       │   │   ├── LostPropertyMapper.java   # 失物信息 Mapper
│   │       │   │   ├── LostRegMapper.java        # 失物登记 Mapper
│   │       │   │   └── LostRegChatHistoryMapper.java # 聊天历史 Mapper
│   │       │   ├── model/                        # 实体类
│   │       │   │   ├── BaseModel.java            # 基础模型
│   │       │   │   ├── LostProperty.java         # 失物信息实体
│   │       │   │   ├── LostReg.java              # 失物登记实体
│   │       │   │   └── LostRegChatHistory.java   # 聊天历史实体
│   │       │   ├── output/                       # AI 输出模型
│   │       │   │   ├── IntentionOutput.java      # 意图识别输出
│   │       │   │   ├── LostPropertyOutput.java   # 失物查询输出
│   │       │   │   └── LostRegOutput.java        # 失物登记输出
│   │       │   ├── service/                      # 服务层
│   │       │   │   ├── AiChatService.java        # AI 聊天服务 ⭐
│   │       │   │   ├── LostChatMemoryService.java   # 聊天记忆服务
│   │       │   │   └── LostChatPropertyService.java # 失物查询服务
│   │       │   └── tools/                        # AI 工具类
│   │       │       ├── LostPropertyTools.java    # 失物查询工具
│   │       │       ├── LostRegChatHistoryTools.java # 聊天历史工具
│   │       │       ├── LostRegTools.java         # 失物登记工具
│   │       │       └── QueryLostItemTools.java   # 查询失物工具
│   │       │
│   │       ├── legal/                            # 法律问答系统
│   │       │   ├── LegalByPromptAssistant.java   # 法律问答助手
│   │       │   └── LegalParam.java               # 法律参数
│   │       │
│   │       ├── product/                          # 商品查询系统
│   │       │   ├── ProductByToolsAssistant.java  # 商品查询助手
│   │       │   ├── ProductInfo.java              # 商品信息
│   │       │   └── ProductTools.java             # 商品工具
│   │       │
│   │       └── taobao/                           # 淘宝客服示例
│   │           ├── EmsDateCalculatorTools.java   # EMS 日期计算工具
│   │           └── TaobaoAssistant.java          # 淘宝客服助手
│   │
│   └── resources/
│       ├── static/
│       │   └── index.html                        # 前端静态页面
│       ├── application.yml                       # 应用配置
│       ├── intention-prompt.txt                  # 意图识别提示词
│       ├── lostreg-prompt.txt                    # 失物登记提示词
│       ├── lostproperty-prompt.txt               # 失物查询提示词
│       ├── lostproperty-query-prompt.txt         # 失物查询(带 RAG)提示词
│       ├── part1-prompt.txt                      # Part1 提示词模板
│       ├── part1-prompt2.txt                     # Part1 提示词模板 2
│       ├── rag-chinese-answer.md                 # RAG 中文问答示例文档
│       └── rag-taobao.md                         # RAG 淘宝客服示例文档
│
└── src/test/java/com/byterun/ailangchain4jdemo/  # 单元测试
    ├── DatatypePromptTests.java                  # 数据类型测试
    ├── LegalPromptTests.java                     # 法律问答测试
    ├── MemoryTests.java                          # 记忆管理测试
    ├── McpTests.java                             # MCP 测试
    ├── PromptTests.java                          # Prompt 工程测试
    ├── RagTests.java                             # RAG 测试
    └── ToolsTests.java                           # 工具调用测试

祝学习愉快!🎉