
LangChain4j 完整学习指南:从入门到企业级应用实战
基于真实项目案例,系统讲解 LangChain4j 核心技术与最佳实践
📖 目录
- [一、LangChain4j 简介](#一、LangChain4j 简介 "#%E4%B8%80langchain4j-%E7%AE%80%E4%BB%8B")
- 二、技术栈与环境搭建
- 三、核心技术详解
- [3.1 AI Services - 声明式 AI 服务](#3.1 AI Services - 声明式 AI 服务 "#31-ai-services---%E5%A3%B0%E6%98%8E%E5%BC%8F-ai-%E6%9C%8D%E5%8A%A1")
- [3.2 Chat Models - 聊天模型集成](#3.2 Chat Models - 聊天模型集成 "#32-chat-models---%E8%81%8A%E5%A4%A9%E6%A8%A1%E5%9E%8B%E9%9B%86%E6%88%90")
- [3.3 Prompt Engineering - 提示词工程](#3.3 Prompt Engineering - 提示词工程 "#33-prompt-engineering---%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B")
- [3.4 Structured Output - 结构化输出](#3.4 Structured Output - 结构化输出 "#34-structured-output---%E7%BB%93%E6%9E%84%E5%8C%96%E8%BE%93%E5%87%BA")
- [3.5 Memory - 对话记忆管理](#3.5 Memory - 对话记忆管理 "#35-memory---%E5%AF%B9%E8%AF%9D%E8%AE%B0%E5%BF%86%E7%AE%A1%E7%90%86")
- [3.6 Tools & Function Calling - 工具调用](#3.6 Tools & Function Calling - 工具调用 "#36-tools--function-calling---%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8")
- [3.7 RAG - 检索增强生成](#3.7 RAG - 检索增强生成 "#37-rag---%E6%A3%80%E7%B4%A2%E5%A2%9E%E5%BC%BA%E7%94%9F%E6%88%90")
- [3.8 MCP - Model Context Protocol](#3.8 MCP - Model Context Protocol "#38-mcp---model-context-protocol")
- [3.9 Streaming - 流式响应](#3.9 Streaming - 流式响应 "#39-streaming---%E6%B5%81%E5%BC%8F%E5%93%8D%E5%BA%94")
- 四、企业级实战:失物招领系统
- [4.1 系统架构设计](#4.1 系统架构设计 "#41-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1")
- [4.2 意图识别模块](#4.2 意图识别模块 "#42-%E6%84%8F%E5%9B%BE%E8%AF%86%E5%88%AB%E6%A8%A1%E5%9D%97")
- [4.3 多轮对话与信息收集](#4.3 多轮对话与信息收集 "#43-%E5%A4%9A%E8%BD%AE%E5%AF%B9%E8%AF%9D%E4%B8%8E%E4%BF%A1%E6%81%AF%E6%94%B6%E9%9B%86")
- [4.4 RAG 智能查询](#4.4 RAG 智能查询 "#44-rag-%E6%99%BA%E8%83%BD%E6%9F%A5%E8%AF%A2")
- [4.5 AOP 切面记录](#4.5 AOP 切面记录 "#45-aop-%E5%88%87%E9%9D%A2%E8%AE%B0%E5%BD%95")
- 五、关键技术点深度解析
- [5.1 @AiService 装配模式](#5.1 @AiService 装配模式 "#51-aiservice-%E8%A3%85%E9%85%8D%E6%A8%A1%E5%BC%8F")
- [5.2 会话隔离机制](#5.2 会话隔离机制 "#52-%E4%BC%9A%E8%AF%9D%E9%9A%94%E7%A6%BB%E6%9C%BA%E5%88%B6")
- [5.3 工具调用原理](#5.3 工具调用原理 "#53-%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%E5%8E%9F%E7%90%86")
- [5.4 RAG 工作流程](#5.4 RAG 工作流程 "#54-rag-%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B")
- [5.5 向量相似度检索](#5.5 向量相似度检索 "#55-%E5%90%91%E9%87%8F%E7%9B%B8%E4%BC%BC%E5%BA%A6%E6%A3%80%E7%B4%A2")
- 六、总结与展望
一、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 核心技术栈
xml
<!-- 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 配置文件示例
yaml
# 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 模型配置类
java
@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 基础用法
java
@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:指定动态系统消息提供者
- wiringMode :装配模式
-
@SystemMessage:系统提示词- 支持模板变量:
{{it}}(单个参数)、{{variable}}(命名参数) - 支持外部文件:
fromResource = "xxx.txt"
- 支持模板变量:
-
@UserMessage:用户消息模板,可与@SystemMessage同时使用 -
@V:绑定方法参数到模板变量,如@V("sessionId") String sessionId -
@MemoryId:标识会话 ID 参数,用于多会话隔离
重要特性:
- 返回类型自动转换:LangChain4j 会自动将 LLM 的文本响应转换为指定的 Java 类型
- 结构化输出 :通过
@Description注解指导 LLM 生成符合 POJO 结构的数据 - 流式支持 :返回
Flux<String>实现流式响应 - 工具调用透明化:声明 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):
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):
yaml
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
关键技术点:
-
ChatModel vs StreamingChatModel:
ChatModel:同步阻塞,等待完整响应StreamingChatModel:异步流式,逐块返回- 项目中同时配置两者,根据场景选择使用
-
模型选择:
- 项目使用
qwen3.5-plus(通义千问 3.5 Plus) - 嵌入模型使用
text-embedding-v4 - 不同模型有不同的能力和成本,需根据业务需求选择
- 项目使用
-
配置属性绑定:
- 使用
@ConfigurationProperties自动绑定 YAML 配置 - 避免硬编码 API Key,便于环境切换
- 使用
3.2.3 嵌入模型配置
java
@Bean
public EmbeddingModel embeddingModel() {
return QwenEmbeddingModel.builder()
.modelName("text-embedding-v4")
.apiKey(apiKey)
.build();
}
3.3 Prompt Engineering - 提示词工程
3.3.1 提示词模板
LangChain4j 支持多种提示词定义方式:
方式一:注解内联
java
@SystemMessage("你是一个专业的{{role}},请回答以下问题:{{question}}")
String chat(@V("role") String role, @V("question") String question);
方式二:外部文件
java
@SystemMessage(fromResource = "intention-prompt.txt")
public interface LostIntentionAiAssistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
}
提示词文件示例(intention-prompt.txt):
markdown
# 角色
你是一位专业的失物招领工作人员,具备高效、负责的工作态度。
## 技能
### 技能 1:判断用户意图
- 仔细分析用户输入内容,精准判断用户的需求
- 对用户的输入意图进行标记,分类如下:
1. 丢失信息登记
2. 找到失物登记
3. 失物查询
4. 其他
## 限制
- 只回答与失物招领相关的问题
- 所输出的内容需条理清晰、简洁明了
3.3.2 外部文件方式(项目采用)
AI Service 定义:
java
@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);
}
关键技术点:
-
静态 vs 动态提示词:
@SystemMessage(fromResource = "xxx.txt"):静态提示词,编译时确定- 注意:LangChain4j 目前主要通过注解和外部文件定义提示词
- 如需动态提示词,可通过方法参数
@V注入变量实现
-
提示词文件位置:
- 放在
src/main/resources/目录下 - 项目中的提示词文件:
intention-prompt.txt:意图识别lostreg-prompt.txt:失物登记lostproperty-prompt.txt:拾取登记lostproperty-query-prompt.txt:失物查询part1-prompt.txt、part1-prompt2.txt:示例提示词
- 放在
-
同一接口多方法多提示词:
- 一个 AI Service 接口可以定义多个方法
- 每个方法可以使用不同的
@SystemMessage - 适合不同场景使用不同的提示词策略
-
提示词版本管理:
- 提示词作为资源文件,可纳入版本控制
- 便于 A/B 测试和优化迭代
3.4 Structured Output - 结构化输出
3.4.1 POJO 映射
通过 @Description 注解指导大模型生成结构化数据:
java
@Data
public class UserDataType {
@Description("用户姓名")
private String name;
@Description("用户年龄")
private int age;
@Description("用户职业")
private String job;
}
3.4.2 复杂业务对象
java
@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;
}
关键技术点:
-
@Description 的作用:
- 告诉 LLM 每个字段的含义和期望格式
- 影响 LLM 生成 JSON 的结构和内容
- 描述越详细,输出越准确
-
@JsonIgnoreProperties(ignoreUnknown = true):
- 容错机制:忽略 LLM 生成的多余字段
- LLM 可能生成不在 POJO 中定义的字段,不加此注解会报错
- 生产环境必备,提高系统稳定性
-
字段类型选择:
- 使用包装类型(
Integer、Boolean)而非基本类型(int、boolean) - 允许字段为 null,LLM 未提取时不会报错
isCompleted用于判断是否收集完所有信息
- 使用包装类型(
-
混合字段设计:
- 业务字段:
username、phone、lostItem等 - 控制字段:
isCompleted、id - 输出字段:
output(LLM 给用户的回复) - 一次调用同时完成信息提取和对话回复
- 业务字段:
3.4.3 项目中的实际应用
意图识别输出(IntentionOutput.java):
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):
java
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.maxMessages(10)
.build();
}
特点:
- 全局共享,无会话隔离
- 适合单用户场景或测试
- 重启后数据丢失
2. 多会话内存存储:
java
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return (memoryId) -> MessageWindowChatMemory.builder()
.id(memoryId)
.chatMemoryStore(new InMemoryChatMemoryStore())
.maxMessages(10)
.build();
}
特点:
- 通过
memoryId隔离不同会话 - 每个会话独立的记忆窗口
- 适合多用户并发场景
- 重启后数据丢失
3. MySQL 持久化存储(生产环境采用):
java
@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
java
@Configuration
public class ChatMemoryConfig {
@Bean("lostChatMemoryProvider")
public ChatMemoryProvider lostChatMemoryProvider(MySqlChatMemoryStore store) {
return (id) -> MessageWindowChatMemory.builder()
.maxMessages(28)
.id(id)
.chatMemoryStore(store)
.build();
}
}
关键技术点:
-
ChatMemory vs ChatMemoryProvider:
ChatMemory:单个记忆实例,无会话隔离ChatMemoryProvider:工厂模式,根据memoryId创建/获取记忆实例- 多用户场景必须使用
ChatMemoryProvider
-
MessageWindowChatMemory:
- 窗口机制:只保留最近 N 条消息
- 防止上下文过长:避免超出 LLM 的 Token 限制
- 成本控制:减少每次请求的 Token 数量
- 项目中设置为 10-28 条,根据业务复杂度调整
-
序列化机制:
ChatMessageSerializer.messagesToJson():将消息列表转为 JSONChatMessageDeserializer.messagesFromJson():从 JSON 恢复消息列表- 支持所有消息类型(SystemMessage、UserMessage、AiMessage)
-
性能考虑:
- 每次对话都会读写数据库
- 高频场景可考虑 Redis 缓存
- 定期清理过期会话(如 7 天前的记录)
3.5.3 项目中的测试用例
多会话隔离测试(MemoryTests.java - testChatMemory3):
java
@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):
java
@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 允许大模型在需要时调用外部函数,实现与外部系统的交互。
工作流程:
javascript
用户 → 应用程序 → LLM 大模型 → Function/Tool → 应用程序 → LLM 大模型 → 用户
3.6.2 项目中的工具实现
1. 计算器工具(CalculatorTools.java):
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. 查询工具
java
@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 查询工具
java
@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
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
chatMemoryProvider = "chatMemoryProvider",
tools = {"calculatorTools"})
public interface ToolsAssistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
}
关键技术点:
-
@Tool 注解参数:
name:工具名称(建议纯英文,避免 LLM 匹配失败)value:工具描述,非常重要!LLM 根据描述决定何时调用- 描述要清晰说明工具的用途、适用场景、参数含义
-
@P 注解:
value:参数描述,帮助 LLM 理解参数含义required:是否必需参数- 描述越准确,LLM 传参越正确
-
@ToolMemoryId:
- 接收当前会话的
memoryId - 可用于日志追踪、权限校验、会话隔离等
- 不是所有工具都需要,按需使用
- 接收当前会话的
-
工具返回值:
- 支持基本类型、POJO、List 等
- 返回值会被序列化为 JSON 传给 LLM
- LLM 会根据返回值继续推理或生成最终回答
-
工具组合使用:
java@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 多工具组合
java
@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 工作流程:
markdown
1. 文档加载 → 2. 文档分割 → 3. 文本向量化 → 4. 存入向量数据库
↓
用户提问 → 5. 问题向量化 → 6. 相似度检索 → 7. 返回相关片段 → 8. LLM 生成答案
3.7.2 项目中的 RAG 配置
RAG 配置类(RagConfig.java):
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();
}
}
关键技术点:
-
EmbeddingModel(嵌入模型):
- 将文本转换为向量(数值数组)
- 项目使用
text-embedding-v4,维度为 1024 - 语义相似的文本,向量距离更近
- 嵌入模型的质量直接影响检索效果
-
EmbeddingStore(向量存储):
- 存储文本片段及其对应的向量
- 项目使用 Pinecone(云托管向量数据库)
- 其他可选:Chroma、Milvus、Elasticsearch 等
nameSpace:命名空间,用于逻辑隔离(如同一索引存不同类型数据)
-
ContentRetriever(内容检索器):
- 封装了"向量化 → 检索 → 返回"的完整流程
minScore:相似度阈值(0-1),过滤低质量结果maxResults:返回最相关的 N 条结果- 被 AI Service 自动注入,在提示词中通过
{{contents}}使用
-
Pinecone 配置(application.yml):
yamlpinecone: 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 集成
java
@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 业务场景:失物查询
java
@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 配置
java
@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 工具
java
@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
java
@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
java
@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 层暴露流式接口
java
@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 前端消费流式数据
javascript
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 流式与非流式对比
java
// 非流式:等待完整响应
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 技术架构
scss
┌─────────────┐
│ 前端界面 │
└──────┬──────┘
│ HTTP/WebSocket
↓
┌─────────────────────────────────┐
│ LostController (REST API) │
└──────┬──────────────────────────┘
│
↓
┌─────────────────────────────────┐
│ AiChatService (业务编排) │
│ ┌──────────────────────────┐ │
│ │ 意图识别 → 路由分发 │ │
│ └──────────────────────────┘ │
└──────┬──────────────────────────┘
│
├──────────────┬──────────────┐
↓ ↓ ↓
┌────────────┐ ┌──────────┐ ┌──────────┐
│失物登记 AI │ │拾取登记 AI│ │失物查询 AI│
│ (Tools) │ │ (Tools) │ │ (RAG) │
└──────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
↓ ↓ ↓
┌─────────────────────────────────────┐
│ MySQL (业务数据 + 对话历史) │
│ Pinecone (向量数据库) │
└─────────────────────────────────────┘
4.1.3 核心组件
java
@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 意图分类
系统识别四种用户意图:
- 丢失信息登记(intention=1):用户丢失物品,需要登记
- 找到失物登记(intention=2):用户捡到物品,需要登记
- 失物查询(intention=3):用户想查询是否有匹配的失物
- 其他(intention=4):无关话题或其他需求
4.2.2 意图识别 AI Service
java
@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 意图提示词设计
txt
# 角色
你是一位专业的失物招领工作人员,具备高效、负责的工作态度。
## 技能
### 技能 1:判断用户意图
- 每次对话都需要通过读取用户历史对话内容工具获取用户对话历史
- 仔细分析用户输入内容,精准判断用户需求
- 对用户的输入意图进行标记,分类如下:
1. 丢失信息登记
2. 找到失物登记
3. 失物查询
4. 其他
# 历史说明(严格遵循)
- 历史记录字段 role: role=0 为用户消息;role=1 为AI消息
- 判定时,优先使用"当前用户消息 + 最近的若干条用户历史消息(role=0)"
# 意图标签与触发规则(强约束)
- 1 丢失信息登记:
- 典型触发词:丢了、不见了、掉了、遗失、找不到、帮我登记
- 2 找到失物登记:
- 典型触发词:我捡到/拾到/捡起/找到了一件...
- 强优先规则:若最近的用户消息中出现"拾得类"表达,直接判定 intention=2
- 3 失物查询:
- 典型触发词:查询/查一下/有没有...记录/有没有人捡到
- 4 其他:不属于以上三类的请求
## 限制
- 只回答与失物招领相关的问题
- 所输出的内容需条理清晰、简洁明了
4.2.4 意图路由
java
@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
java
@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 登记提示词设计
txt
# 角色
你是一位专业且尽责的失物登记专员,始终秉持耐心、细致的态度。
## 技能
### 技能 1:登记失物信息
- 每次对话都需要通过读取用户历史对话内容工具获取用户对话历史
- 如果是补充丢失信息,要求用户告诉姓名和手机号码
- 如果用户没有传手机号,不得随意自动生成手机号,必须是用户输入的手机号
- 若用户已经输入了手机号,调用根据手机号查询丢失登记信息工具
- 若能查询到数据,将其与用户已输入的物品信息进行匹配
- 最多返回一条已经匹配的数据,并填充id字段
- 当用户提供失物相关信息时,从中提取物品信息和相关特征
- 主动询问用户的姓名和手机号,并准确记录
- 详细引导用户清晰描述失物的特征,包括颜色、材质、尺寸、特殊标识等
- 根据用户输入情况,准确判断是否完成登记,并标记"true"或"false"
- 若判断为"false",清晰告知用户还缺少哪些信息
- 若用户表示没有更多信息或特征,也可以完成登记
## 限制
- 仅处理用户与丢失物品登记紧密相关的内容
- 所输出的内容需严格按照要求的格式进行组织
- 要充分结合用户的历史对话记录,自然地承上启下
- 禁止机械重复话术,禁止重复或者相似的回复
- 如果判断完成登记,必须要准确告诉用户已经完成登记
4.3.4 对话示例
diff
用户:我丢了一个钱包
AI:您好!请问您的姓名和手机号是多少?另外,能详细描述一下钱包的特征吗?比如颜色、品牌、里面有什么物品等。
用户:我叫张三,电话15655524410,黑色的LV钱包
AI:好的,张三先生。您丢失的是一个黑色的LV钱包。请问钱包里还有什么物品吗?比如现金、银行卡等。
用户:里面有400块钱和三张中国银行卡
AI:明白了。让我确认一下信息:
- 姓名:张三
- 手机号:15655524410
- 失物名称:钱包
- 失物特征:黑色、LV品牌、内有400元现金和三张中国银行卡
请问还有需要补充的信息吗?如果没有,我将为您完成登记。
用户:没有了
AI:好的,已完成登记!我们会尽快帮您寻找。如果有人拾取到您的钱包,我们会第一时间联系您。
4.3.5 工具:查询历史记录
java
@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 查询流程
scss
用户提问 → 意图识别(查询) → 向量化 → 相似度检索 → 返回匹配结果 → LLM 生成回答
4.4.2 查询 AI Service
java
@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 查询工具
java
@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 查询提示词
txt
# 角色
你是一位专业的失物查询助手。
## 技能
- 使用 queryLostItem 工具查询数据库中是否有匹配的失物
- 根据查询结果,友好地告知用户
- 如果有匹配项,提供联系方式
- 如果没有匹配项,建议用户留下联系方式,待有匹配时通知
## 限制
- 仅回答与失物查询相关的内容
- 语气要温和、专业
4.4.5 查询示例
css
用户:有没有人捡到一个黑色的LV钱包?
AI:[调用 queryLostItem 工具]
[检索到 2 条相似记录]
我找到了 2 条可能匹配的记录:
1. 拾取时间:2024-01-15
物品描述:黑色钱包,内有身份证和银行卡
联系人:李四 138xxxx1234
2. 拾取时间:2024-01-18
物品描述:黑色皮质钱包,LV品牌
联系人:王五 139xxxx5678
请您核对是否是您的钱包,并联系相关人员确认。
4.5 AOP 切面记录
4.5.1 自定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ChatFlow {
}
4.5.2 AOP 切面实现
java
@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 使用切面
java
@Service
public class AiChatService {
@ChatFlow // 自动记录对话历史
public String chatSse(String userId, String message) {
// 业务逻辑...
}
}
4.5.4 历史管理
java
@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(显式模式)- 项目采用:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
chatMemoryProvider = "lostChatMemoryProvider",
tools = {"lostRegTools", "queryLostItemTools"})
public interface LostPropertyAssistant {
// ...
}
优点:
- 明确依赖:清楚指定使用哪个 Bean,避免歧义
- 灵活配置:可以为不同 AI Service 配置不同的模型、记忆策略
- 便于测试:可以注入 Mock Bean 进行单元测试
- 项目实践:所有 AI Service 都使用 EXPLICIT 模式
AUTOMATIC(自动模式):
java
@AiService // 自动从 Spring 容器查找 ChatModel、ChatMemory 等
public interface AutoAssistant {
String chat(String message);
}
适用场景:
- 简单应用,只有一个 ChatModel
- 快速原型开发
- 不需要特殊配置的场景
5.1.2 常见配置组合
1. 基础对话:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel")
2. 带记忆的对话:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
chatMemoryProvider = "lostChatMemoryProvider")
3. 带工具的对话:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
tools = {"calculatorTools"})
4. RAG 增强对话:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
contentRetriever = "contentRetriever")
5. MCP 远程工具:
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
toolProvider = "mcpToolProvider")
6. 完整配置(失物招领系统):
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
streamingChatModel = "qwenStreamingChatModel",
chatMemoryProvider = "lostChatMemoryProvider",
tools = {"lostRegChatHistoryTools", "lostPropertyTools",
"lostRegTools", "queryLostItemTools"})
5.2 会话隔离机制
5.2.1 MemoryId 的作用
问题:多用户同时使用时,如何保证对话不混乱?
解决方案 :通过 @MemoryId 实现会话隔离
java
@AiService(wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "myQwenChatModel",
chatMemoryProvider = "lostChatMemoryProvider")
public interface LostIntentionAiAssistant {
// memoryId 用于标识不同会话
String chat(@MemoryId String memoryId, @UserMessage String message);
}
工作流程:
ini
用户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 工厂模式
java
@Bean("lostChatMemoryProvider")
public ChatMemoryProvider lostChatMemoryProvider(MySqlChatMemoryStore store) {
return (id) -> MessageWindowChatMemory.builder()
.maxMessages(28)
.id(id) // 关键:每个 id 对应独立的 ChatMemory
.chatMemoryStore(store)
.build();
}
内部逻辑:
java
// 伪代码
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 加载历史
更新:每次对话后保存最新历史到数据库
删除:
java
@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 工作流程
json
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 工具描述的重要性
好的描述:
java
@Tool(value = "根据手机号获取用户登记丢失物品信息",
name = "toolGetLostRegListByPhone")
public List<LostReg> toolGetLostRegListByPhone(
@P("用户手机号") String phone
)
LLM 理解:
- 当用户提到"查询"、"手机号"、"登记"时调用
- 参数 phone 需要传入手机号格式
差的描述:
java
@Tool(value = "查询数据", name = "query")
public List<LostReg> query(String param)
问题:
- LLM 不知道何时调用
- 不知道 param 应该传什么
- 可能导致工具滥用或不调用
5.3.3 工具调用调试
开启日志:
yaml
logging:
level:
dev.langchain4j: DEBUG
日志输出示例:
css
DEBUG - Tool execution request: toolGetLostRegListByPhone({"phone":"13800138000"})
INFO - 执行工具:toolGetLostRegListByPhone, 参数:13800138000
DEBUG - Tool execution result: [{"id":1,"username":"张三","lostItem":"钱包"}]
常见问题排查:
-
工具未被调用:
- 检查
@Tool的value描述是否清晰 - 检查 AI Service 是否正确注册了 tools
- 查看 LLM 是否理解了用户需求
- 检查
-
参数传递错误:
- 检查
@P注解的描述是否准确 - 检查参数类型是否匹配
- 查看日志中的实际传参
- 检查
-
工具执行失败:
- 检查工具代码是否有异常
- 检查依赖的服务是否正常
- 查看完整堆栈信息
5.4 RAG 工作流程
5.4.1 完整的 RAG 流程
scss
【入库阶段】
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:向量1
↓
8. 构建提示词:
System: 你是淘宝客服
Context: 淘宝发货时间:通常24小时内发货
User: 多久可以发货
↓
9. LLM 生成回答:"我们通常在下单后24小时内发货"
↓
10. 返回给用户
5.4.2 向量相似度计算
余弦相似度公式:
css
cos(θ) = (A · B) / (||A|| × ||B||)
其中:
- A · B 是向量点积
- ||A|| 是向量 A 的模
- 结果范围:[-1, 1],越接近 1 越相似
示例:
css
向量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 集成
配置:
java
@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}}
请根据以上信息回答用户问题。
运行时:
sql
1. 用户提问:"多久发货"
↓
2. ContentRetriever 自动检索相关片段
↓
3. 替换 {{contents}} 为检索结果
↓
4. 发送给 LLM:
System: 你是一个淘宝客服助手。
相关商品信息:
- 淘宝发货时间:通常24小时内发货
User: 多久发货
↓
5. LLM 生成回答
5.5 向量相似度检索
5.5.1 EmbeddingSearchRequest 参数详解
java
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 检索结果处理
java
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);
}
实际输出示例:
css
相似度: 0.87
内容: {"id":1,"lostItem":"钱包","lostItemFeature":"黑色、LV品牌","phone":"13800138000"}
相似度: 0.72
内容: {"id":2,"lostItem":"手机","lostItemFeature":"黑色、iPhone13","phone":"13900139000"}
5.5.3 性能优化建议
1. 批量入库:
java
// ✅ 推荐:批量操作
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. 分页处理大数据量:
java
int batchSize = 100;
for (int i = 0; i < totalRecords; i += batchSize) {
List<LostProperty> batch = lostPropertyMapper.selectPage(...);
// 处理批次...
}
3. 增量更新:
java
// 只更新变化的记录
@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. 缓存嵌入结果:
java
@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 架构设计要点
- 分层架构:Controller → Service → AI Service → Tools/RAG
- 意图驱动:先识别意图,再路由到对应处理器
- 多轮对话:利用 Memory 实现上下文感知
- 工具增强:通过 Tools 扩展 AI 能力边界
- 知识增强:通过 RAG 接入私有知识库
- 可观测性:AOP 切面记录对话历史
6.3 未来发展方向
- Agent 化:构建更智能的自主 Agent
- 多模态:支持图片、音频等多模态输入
- 工作流引擎:可视化编排 AI 工作流
- 评估体系:建立 AI 输出质量评估机制
- 微调模型:针对特定领域微调专属模型
6.4 学习资源
- 官方文档 :docs.langchain4j.dev/
- GitHub :github.com/langchain4j...
- 示例项目 :github.com/langchain4j...
- 社区论坛 :discord.gg/langchain4j
6.5 结语
LangChain4j 为 Java 开发者提供了一套强大的 AI 应用开发工具。通过本项目的实践,我们不仅掌握了 LangChain4j 的核心技术,还构建了一个完整的企业级应用。
希望这份学习指南能帮助你快速上手 LangChain4j,构建属于自己的 AI 应用!
后端项目源码 :gitee.com/byterun/ai-...
前端项目源码 :gitee.com/byterun/ai-...
作者 :byteRun
日期:2026-04-23
附录:完整代码结构
bash
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 # 工具调用测试
祝学习愉快!🎉