1. LangChain4j实战:RAG(检索增强生成)入门指南
作为一名长期奋战在AI应用开发一线的工程师,我深知如何让大语言模型(LLM)更好地服务于特定业务场景的重要性。今天要分享的RAG(检索增强生成)技术,正是解决这个问题的利器。本文将带您从零开始,用Java生态的LangChain4j框架实现最简单的RAG方案。
1.1 什么是RAG技术?
RAG全称Retrieval-Augmented Generation,即检索增强生成。它的核心思想很简单:当用户提问时,先从您提供的专属资料库中检索相关内容,再将这些信息作为上下文注入到提示词中,最后才交给LLM生成回答。
这种技术特别适合以下场景:
- 回答基于您内部文档的问题(如产品手册、技术文档)
- 处理LLM训练数据中不存在的最新信息
- 需要确保回答内容与您提供的资料保持一致的场景
1.2 为什么选择LangChain4j?
LangChain4j是Java生态中领先的LLM应用开发框架,相比Python版本,它具有:
- 更符合Java开发者习惯的API设计
- 与Spring生态无缝集成
- 对RAG提供了开箱即用的支持
- 内存管理更高效,适合企业级应用
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目搭建
2.1 开发环境要求
- JDK 17或更高版本
- Maven 3.6+
- Spring Boot 3.1+
- 一个可用的LLM API密钥(本文以阿里云通义千问为例)
2.2 项目初始化
首先创建一个标准的Spring Boot项目,在pom.xml中添加关键依赖:
xml复制<dependencies>
<!-- LangChain4j核心库 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.25.0</version>
</dependency>
<!-- 通义千问支持 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope</artifactId>
<version>0.25.0</version>
</dependency>
<!-- Easy RAG支持 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>0.25.0</version>
</dependency>
<!-- Spring Boot基础依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.3 配置文件设置
在application.properties中配置LLM连接参数:
properties复制# 通义千问API配置
langchain4j.open-ai.chat-model.api-key=您的API-KEY
langchain4j.open-ai.chat-model.model-name=qwen-max
langchain4j.open-ai.chat-model.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
# RAG文档目录
rag.file.path=/path/to/your/documents
3. 实现Easy RAG核心逻辑
3.1 文档准备与处理
RAG需要本地文档作为知识库。建议准备:
- 纯文本格式(.txt)
- 结构清晰的Markdown文件
- 从维基百科等来源导出的XML/JSON数据
文档处理的关键步骤:
- 文档加载:将原始文件读入内存
- 文档分割:按语义切分成适当大小的段落
- 向量化:将文本转换为数值向量
- 存储:将向量存入向量数据库
提示:对于中文文档,建议每个文本段落在200-500字之间,太短会丢失上下文,太长则影响检索精度。
3.2 核心代码实现
创建配置类LangChain4jConfig.java:
java复制@Configuration
public class LangChain4jConfig {
private static final Logger logger = LoggerFactory.getLogger(LangChain4jConfig.class);
@Value("${rag.file.path}")
private String ragFilePath;
@Bean
public OpenAiChatModel chatModel(
@Value("${langchain4j.open-ai.chat-model.api-key}") String apiKey,
@Value("${langchain4j.open-ai.chat-model.model-name}") String modelName,
@Value("${langchain4j.open-ai.chat-model.base-url}") String baseUrl) {
return OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName(modelName)
.baseUrl(baseUrl)
.build();
}
@Bean
public Assistant assistant(OpenAiChatModel chatModel) {
ContentRetriever contentRetriever = createContentRetriever(ragFilePath);
return AiServices.builder(Assistant.class)
.chatModel(chatModel)
.contentRetriever(contentRetriever)
.build();
}
private ContentRetriever createContentRetriever(String path) {
// 使用内存存储向量(生产环境建议使用专业向量数据库)
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
// 加载并处理文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments(path);
logger.info("Loaded {} documents from {}", documents.size(), path);
// 创建文档处理器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(embeddingStore)
.build();
// 处理每个文档
documents.forEach(ingestor::ingest);
return EmbeddingStoreContentRetriever.from(embeddingStore);
}
}
3.3 定义AI服务接口
创建Assistant.java接口:
java复制public interface Assistant {
/**
* 使用RAG增强的回答
* @param question 用户问题
* @return 基于知识库的回答
*/
String answerWithRag(String question);
}
4. 构建REST API接口
4.1 控制器实现
创建QwenController.java:
java复制@RestController
@RequestMapping("/api/assistant")
public class AssistantController {
private final Assistant assistant;
public AssistantController(Assistant assistant) {
this.assistant = assistant;
}
@PostMapping("/ask")
public ResponseEntity<Answer> askQuestion(@RequestBody QuestionRequest request) {
String answer = assistant.answerWithRag(request.getQuestion());
return ResponseEntity.ok(new Answer(answer));
}
// 请求响应DTO
@Data
@AllArgsConstructor
static class QuestionRequest {
private String question;
}
@Data
@AllArgsConstructor
static class Answer {
private String answer;
}
}
4.2 测试API
使用curl测试接口:
bash复制curl -X POST http://localhost:8080/api/assistant/ask \
-H "Content-Type: application/json" \
-d '{"question":"请用100字介绍完颜陈和尚"}'
预期响应:
json复制{
"answer": "完颜陈和尚(本名彝,字良佐),金末名将,丰州人(今内蒙古呼和浩特东),为萧王完颜秉德后裔。通晓《孝经》《左传》,擅写牛毛细字,有儒将之风。曾于大昌原之役等战役中屡败蒙古军,展现卓越军事才能。"
}
5. 核心原理深度解析
5.1 RAG工作流程
-
索引阶段:
- 文档加载:从文件系统读取原始文档
- 文本分割:使用递归字符分割器将文档分成段落
- 向量化:使用嵌入模型(如text-embedding-3-small)将文本转换为向量
- 存储:将向量和原始文本存入向量数据库
-
检索阶段:
- 问题向量化:将用户问题转换为向量
- 相似度搜索:在向量数据库中查找最相关的文本段落
- 上下文构建:将检索到的文本作为上下文注入提示词
- 生成回答:LLM基于上下文生成最终回答
5.2 LangChain4j的关键组件
- DocumentLoader:负责从各种来源加载文档
- TextSplitter:将长文档分割成适合处理的段落
- EmbeddingModel:将文本转换为向量表示
- EmbeddingStore:存储和检索向量数据
- ContentRetriever:协调检索过程的组件
6. 性能优化与生产建议
6.1 索引优化
- 批量处理:对于大量文档,实现分批加载和处理
- 并行处理:利用多线程加速向量化过程
- 增量更新:只处理新增或修改的文档
优化后的文档处理代码示例:
java复制private ContentRetriever createOptimizedContentRetriever(String path) {
InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
// 配置文档处理器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel())
.documentSplitter(new DocumentByParagraphSplitter(500, 50))
.build();
// 并行处理文档
List<Path> documentPaths = listDocumentPaths(path);
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
executor.invokeAll(documentPaths.stream()
.map(p -> (Callable<Void>) () -> {
Document doc = DocumentLoader.load(p);
ingestor.ingest(doc);
return null;
})
.collect(Collectors.toList()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return EmbeddingStoreContentRetriever.from(store);
}
6.2 检索优化
- 多路召回:结合关键词检索和向量检索
- 结果重排序:对初步检索结果进行二次排序
- 查询扩展:对原始问题进行语义扩展
7. 常见问题与解决方案
7.1 文档处理问题
问题1:文档加载速度慢
- 解决方案:实现文档预加载和缓存机制
问题2:中文分割效果不佳
- 解决方案:使用专门的中文文本分割器
java复制public class ChineseTextSplitter implements TextSplitter {
@Override
public List<TextSegment> split(String text) {
// 实现基于中文标点和语义的分割逻辑
}
}
7.2 检索质量问题
问题1:检索到无关内容
- 解决方案:调整相似度阈值和top-k参数
java复制ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(model)
.maxResults(3) // 只返回最相关的3个结果
.minScore(0.7) // 相似度阈值
.build();
问题2:上下文过长导致API错误
- 解决方案:限制上下文长度
java复制ContentRetriever retriever = new MaxLengthContentRetriever(
EmbeddingStoreContentRetriever.from(store),
3000 // 最大上下文token数
);
8. 进阶方向与扩展思考
8.1 从Easy RAG到生产级RAG
当您需要更高级的功能时,可以考虑:
- 使用专业的向量数据库(如Milvus、Pinecone)
- 实现混合检索策略(关键词+向量+语义)
- 加入查询理解和重写模块
- 实现结果的后处理和过滤
8.2 监控与评估
建立RAG系统的评估体系:
- 检索召回率监控
- 生成结果的人工评估
- 端到端响应时间监控
- 用户反馈收集机制
在实际项目中,我发现RAG系统的效果很大程度上取决于文档质量和预处理流程。经过多次迭代,我们总结出几个关键点:
- 文档预处理比想象中重要 - 去除无关内容、统一格式、补充元数据等步骤能显著提升检索质量
- 文本分割策略需要根据内容类型调整 - 技术文档和百科全书的理想段落长度可能完全不同
- 定期更新知识库至关重要 - 我们建立了自动化管道,每周同步最新文档并重建索引
对于Java开发者来说,LangChain4j提供了足够灵活的API来实现这些优化。相比直接调用LLM API,RAG方案虽然增加了复杂度,但带来的准确性和可控性提升是值得的。
