Java 开发者的 AI 利器:LangChain4j 从入门到企业实战
当 ChatGPT 横空出世,Python 开发者狂欢时,Java 开发者只能望洋兴叹?不,LangChain4j 来了!
前言
2023 年,大语言模型(LLM)席卷全球。Python 生态凭借 LangChain 框架迅速占领了 AI 应用开发的高地。而作为企业级开发主力军的 Java 开发者,却面临一个尴尬的问题:
如何在 Java 项目中集成 LLM 能力?
答案就是 LangChain4j —— Java 生态的 LLM 应用开发框架。
本文将从企业面临的四个真实问题出发,带你从零掌握 LangChain4j,并实现四个完整的企业级应用场景。
一、企业面临的真实问题
在 AI 时代,企业面临以下挑战:
1.1 知识管理困境
员工每天花 2.5 小时搜索信息 ↓ 企业文档散落在各个系统 ↓ 新员工培训周期长 ↓ 重复回答相同问题
问题:如何让 AI 理解企业内部文档,成为知识助手?
1.2 客服成本高企
人工客服成本高 ↓ 7×24 小时难以保障 ↓ 回答质量参差不齐 ↓ 高峰期响应慢
问题:如何构建智能客服,自动处理 80% 的常见问题?
1.3 非结构化数据处理
合同、发票、报告堆积如山 ↓ 人工提取信息效率低 ↓ 容易出错 ↓ 无法批量处理
问题:如何自动解析文档,提取关键信息?
1.4 数据查询门槛
业务人员想看数据 ↓ 不会写 SQL ↓ 依赖开发人员 ↓ 需求响应慢
问题:如何让业务人员用自然语言查询数据库?
LangChain4j 就是解决这些问题的利器!
二、LangChain4j 是什么
2.1 定义
LangChain4j 是一个 Java 框架,用于简化 LLM(大语言模型)应用开发。它是 Python LangChain 的 Java 实现,但并非简单移植,而是充分利用 Java 生态的优势。
┌─────────────────────────────────────────────────────────┐ │ LLM 应用开发框架 │ ├────────────────────────┬────────────────────────────────┤ │ Python LangChain │ LangChain4j (Java) │ ├────────────────────────┼────────────────────────────────┤ │ ✓ 生态成熟 │ ✓ 企业级稳定性 │ │ ✓ 社区活跃 │ ✓ Spring Boot 集成 │ │ ✓ 快速原型 │ ✓ 类型安全 │ │ ✗ 企业应用部署复杂 │ ✓ 成熟的工程实践 │ │ ✗ 运行时性能 │ ✓ 与现有 Java 系统无缝集成 │ └────────────────────────┴────────────────────────────────┘
2.2 核心特性
1. 统一的 LLM 接口
// 一套代码,支持多种 LLM
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey("your-api-key")
.modelName("gpt-4")
.build();
// 切换到其他模型,只需改一行
// ChatLanguageModel model = OllamaChatModel.builder()
// .modelName("llama2")
// .build();
2. RAG(检索增强生成)支持
// 让 LLM 能够访问企业私有数据 EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); // 文档加载 → 向量化 → 存储 → 检索
3. 对话记忆管理
// 自动管理多轮对话上下文 ChatMemory chatMessageWindow = MessageWindowChatMemory.withMaxMessages(10);
4. 工具调用(Function Calling)
// 让 LLM 调用外部工具
@Tool("获取当前天气")
public String getWeather(@P("城市名称") String city) {
return weatherService.getWeather(city);
}
5. AI Services(声明式 AI 接口)
// 像定义 Feign 客户端一样定义 AI 接口
@AiService
public interface Assistant {
@SystemMessage("你是一个 helpful 的助手")
String chat(@MemoryId String sessionId, @UserMessage String message);
}
2.3 支持的 LLM 提供商
| 提供商 | 模型示例 | 特点 |
|---|---|---|
| OpenAI | GPT-4, GPT-3.5 | 最强大,成本较高 |
| Azure OpenAI | GPT-4, GPT-3.5 | 企业级,合规性好 |
| Ollama | Llama2, Mistral | 本地部署,免费 |
| Hugging Face | 各种开源模型 | 丰富的模型库 |
| Gemini | 多模态支持 | |
| Anthropic | Claude | 长上下文 |
| 国产模型 | 通义千问、文心一言 | 中文优化 |
2.4 与 Spring Boot 的完美集成
<!-- LangChain4j Spring Boot Starter --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> <version>0.25.0</version> </dependency>
# application.yml 配置
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY}
model-name: gpt-4
temperature: 0.7
max-tokens: 2000
三、环境搭建
3.1 创建项目
使用 Spring Initializr 创建项目,或手动添加依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>langchain4j-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>langchain4j-demo</name>
<description>LangChain4j 企业级实战示例</description>
<properties>
<java.version>17</java.version>
<langchain4j.version>0.25.0</langchain4j.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- LangChain4j Core -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- LangChain4j Spring Boot Starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- OpenAI 集成 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 本地嵌入模型(用于 RAG) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 文档解析器 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-tika</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 数据库(用于示例) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 配置文件
# application.yml
server:
port: 8080
spring:
application:
name: langchain4j-demo
datasource:
url: jdbc:h2:mem:demo
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
# LangChain4j 配置
langchain4j:
open-ai:
chat-model:
api-key: ${OPENAI_API_KEY:sk-xxx}
model-name: gpt-3.5-turbo
temperature: 0.7
max-tokens: 2000
log-requests: true
log-responses: true
# 自定义配置
app:
knowledge-base:
document-path: classpath:documents/
chunk-size: 500
chunk-overlap: 50
3.3 项目结构
src/main/java/com/example/langchain4j/ ├── Langchain4jDemoApplication.java ├── config/ │ └── LangChain4jConfig.java ├── controller/ │ ├── KnowledgeController.java │ ├── ChatbotController.java │ ├── DocumentController.java │ └── DataQueryController.java ├── service/ │ ├── KnowledgeService.java │ ├── ChatbotService.java │ ├── DocumentService.java │ └── DataQueryService.java ├── model/ │ ├── ChatMessage.java │ └── QueryResult.java ├── agent/ │ ├── Assistant.java │ └── tools/ │ ├── WeatherTool.java │ └── DatabaseTool.java └── rag/ ├── DocumentLoader.java └── Retriever.java src/main/resources/ ├── application.yml └── documents/ ├── product-manual.pdf ├── faq.txt └── company-policy.docx
四、问题 1:如何让 AI 理解企业文档?
4.1 痛点分析
大语言模型(如 GPT-4)的知识截止到训练时间点,而且 不知道企业内部的私有数据:
用户问:我们公司的请假流程是什么? LLM 答:一般来说,请假流程是...(胡说八道)
解决方案:RAG(检索增强生成)
┌─────────────────────────────────────────────────────────┐ │ RAG 工作流程 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 用户提问 │ │ ↓ │ │ 问题向量化 │ │ ↓ │ │ 向量检索(找到相关文档片段) │ │ ↓ │ │ 将文档片段 + 问题 → 发送给 LLM │ │ ↓ │ │ LLM 基于文档内容生成准确回答 │ │ │ └─────────────────────────────────────────────────────────┘
4.2 RAG 实现
1. 定义 AI 服务接口
@AiService
public interface KnowledgeAssistant {
@SystemMessage("""
你是一个企业知识库助手。请根据提供的上下文信息回答问题。
如果上下文中没有相关信息,请明确告知用户,不要编造答案。
回答要简洁、准确、专业。
""")
String answer(@MemoryId String sessionId, @V("question") String question);
}
2. 配置 RAG 组件
@Configuration
public class RagConfig {
@Value("${app.knowledge-base.document-path}")
private Resource documentPath;
@Value("${app.knowledge-base.chunk-size:500}")
private int chunkSize;
@Value("${app.knowledge-base.chunk-overlap:50}")
private int chunkOverlap;
@Bean
public EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}
@Bean
public EmbeddingStore<TextSegment> embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
@Bean
public DocumentParser documentParser() {
return new ApacheTikaDocumentParser();
}
@Bean
public EmbeddingStoreIngestor embeddingStoreIngestor(
EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(chunkSize, chunkOverlap))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
}
@Bean
public ContentRetriever contentRetriever(
EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.7)
.build();
}
}
3. 文档加载服务
@Service
@Slf4j
public class KnowledgeService {
private final EmbeddingStoreIngestor ingestor;
private final DocumentParser documentParser;
private final ResourceLoader resourceLoader;
@Value("${app.knowledge-base.document-path}")
private String documentPath;
public KnowledgeService(EmbeddingStoreIngestor ingestor,
DocumentParser documentParser,
ResourceLoader resourceLoader) {
this.ingestor = ingestor;
this.documentParser = documentParser;
this.resourceLoader = resourceLoader;
}
/**
* 加载单个文档到知识库
*/
public void loadDocument(String filePath) throws IOException {
log.info("加载文档: {}", filePath);
Resource resource = resourceLoader.getResource(filePath);
Document document = documentParser.parse(resource.getInputStream());
// 设置文档元数据
document.metadata().put("source", filePath);
document.metadata().put("loadTime", LocalDateTime.now().toString());
ingestor.ingest(document);
log.info("文档加载完成: {}", filePath);
}
/**
* 批量加载目录下所有文档
*/
public int loadAllDocuments() throws IOException {
Resource directory = resourceLoader.getResource(documentPath);
File dir = directory.getFile();
if (!dir.exists() || !dir.isDirectory()) {
throw new IllegalArgumentException("文档目录不存在: " + documentPath);
}
int count = 0;
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (isSupportedFile(file.getName())) {
loadDocument("file:" + file.getAbsolutePath());
count++;
}
}
}
log.info("共加载 {} 个文档", count);
return count;
}
private boolean isSupportedFile(String filename) {
return filename.endsWith(".pdf") ||
filename.endsWith(".docx") ||
filename.endsWith(".txt") ||
filename.endsWith(".md");
}
}
4. 知识问答控制器
@RestController
@RequestMapping("/api/knowledge")
@RequiredArgsConstructor
public class KnowledgeController {
private final KnowledgeAssistant assistant;
private final KnowledgeService knowledgeService;
/**
* 知识库问答
*/
@PostMapping("/ask")
public ResponseEntity<Map<String, String>> ask(
@RequestParam String sessionId,
@RequestParam String question) {
String answer = assistant.answer(sessionId, question);
return ResponseEntity.ok(Map.of(
"question", question,
"answer", answer,
"sessionId", sessionId
));
}
/**
* 加载文档到知识库
*/
@PostMapping("/documents/load")
public ResponseEntity<Map<String, Object>> loadDocuments() {
try {
int count = knowledgeService.loadAllDocuments();
return ResponseEntity.ok(Map.of(
"success", true,
"message", "成功加载 " + count + " 个文档"
));
} catch (IOException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "加载失败: " + e.getMessage()
));
}
}
/**
* 加载单个文档
*/
@PostMapping("/documents/load/{filename}")
public ResponseEntity<Map<String, Object>> loadDocument(
@PathVariable String filename) {
try {
String filePath = "classpath:documents/" + filename;
knowledgeService.loadDocument(filePath);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "文档加载成功: " + filename
));
} catch (IOException e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "加载失败: " + e.getMessage()
));
}
}
}
5. 测试 RAG
# 1. 先加载文档
curl -X POST http://localhost:8080/api/knowledge/documents/load
# 2. 提问
curl -X POST "http://localhost:8080/api/knowledge/ask?sessionId=user1&question=公司的年假政策是什么"
# 返回:
# {
# "question": "公司的年假政策是什么",
# "answer": "根据公司规定,员工入职满一年后享有5天年假,每增加一年工龄增加1天,最多15天...",
# "sessionId": "user1"
# }
4.3 高级 RAG:混合检索
@Configuration
public class AdvancedRagConfig {
@Bean
public ContentRetriever hybridRetriever(
EmbeddingModel embeddingModel,
EmbeddingStore<TextSegment> embeddingStore) {
// 向量检索
EmbeddingStoreContentRetriever embeddingRetriever =
EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.6)
.build();
// 关键词检索(需要额外实现)
// ContentRetriever keywordRetriever = ...;
// 混合检索
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.5)
.build();
}
}
五、问题 2:如何构建智能客服?
5.1 痛点分析
传统客服面临的问题:
-
重复回答相同问题
-
无法 7×24 小时服务
-
多轮对话上下文丢失
-
无法调用外部系统
解决方案:Memory + Tools + Agent
5.2 智能客服实现
1. 定义客服 AI 服务
@AiService
public interface CustomerServiceAgent {
@SystemMessage("""
你是「小智」,一个专业的客服助手。
你的职责:
1. 回答用户关于产品、服务、订单的常见问题
2. 帮助用户查询订单状态、物流信息
3. 处理简单的售后问题
4. 对于复杂问题,建议用户联系人工客服
规则:
- 回答要友好、专业、简洁
- 如果不确定答案,不要编造,建议用户联系人工客服
- 可以调用工具查询订单、天气等实时信息
- 记住用户的对话历史,提供连贯的服务
人工客服热线:400-123-4567
""")
String chat(@MemoryId String sessionId, @UserMessage String message);
}
2. 客服工具定义
@Component
@Slf4j
public class CustomerServiceTools {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductRepository productRepository;
@Tool("查询订单状态。参数:订单号")
public String queryOrderStatus(@P("订单号") String orderNumber) {
log.info("查询订单状态: {}", orderNumber);
return orderRepository.findByOrderNumber(orderNumber)
.map(order -> String.format(
"订单号:%s\n状态:%s\n下单时间:%s\n预计送达:%s",
order.getOrderNumber(),
getStatusName(order.getStatus()),
order.getCreateTime(),
order.getEstimatedDelivery()
))
.orElse("未找到订单号为 " + orderNumber + " 的订单,请检查订单号是否正确");
}
@Tool("查询产品信息。参数:产品名称或关键词")
public String queryProduct(@P("产品名称或关键词") String keyword) {
log.info("查询产品信息: {}", keyword);
List<Product> products = productRepository.findByNameContaining(keyword);
if (products.isEmpty()) {
return "未找到相关产品";
}
StringBuilder sb = new StringBuilder("为您找到以下产品:\n");
for (Product product : products) {
sb.append(String.format("- %s:¥%.2f,库存 %d 件\n",
product.getName(),
product.getPrice(),
product.getStock()));
}
return sb.toString();
}
@Tool("查询物流信息。参数:订单号")
public String queryLogistics(@P("订单号") String orderNumber) {
log.info("查询物流信息: {}", orderNumber);
// 模拟物流查询
return String.format(
"订单 %s 的物流信息:\n" +
"当前位置:北京转运中心\n" +
"最新状态:已发出,预计明天送达\n" +
"快递单号:SF1234567890",
orderNumber
);
}
@Tool("创建工单。参数:问题描述、用户ID")
public String createTicket(@P("问题描述") String description,
@P("用户ID") String userId) {
log.info("创建工单: userId={}, description={}", userId, description);
String ticketId = "TK" + System.currentTimeMillis();
// 保存工单到数据库...
return String.format(
"工单已创建!\n工单号:%s\n问题:%s\n我们会尽快处理,预计24小时内回复。",
ticketId,
description
);
}
private String getStatusName(OrderStatus status) {
return switch (status) {
case PENDING_PAYMENT -> "待付款";
case PAID -> "已付款";
case SHIPPED -> "已发货";
case DELIVERED -> "已送达";
case COMPLETED -> "已完成";
case CANCELLED -> "已取消";
};
}
}
3. 实体类定义
@Entity
@Table(name = "orders")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String orderNumber;
private Long userId;
private String productName;
private Integer quantity;
private BigDecimal totalAmount;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private LocalDateTime createTime;
private String estimatedDelivery;
}
public enum OrderStatus {
PENDING_PAYMENT, PAID, SHIPPED, DELIVERED, COMPLETED, CANCELLED
}
@Entity
@Table(name = "products")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stock;
}
4. 客服控制器
@RestController
@RequestMapping("/api/chatbot")
@RequiredArgsConstructor
public class ChatbotController {
private final CustomerServiceAgent agent;
/**
* 与客服机器人对话
*/
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
String answer = agent.chat(request.getSessionId(), request.getMessage());
return ResponseEntity.ok(new ChatResponse(
request.getSessionId(),
request.getMessage(),
answer
));
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class ChatRequest {
private String sessionId;
private String message;
}
@Data
@AllArgsConstructor
class ChatResponse {
private String sessionId;
private String question;
private String answer;
}
5. 测试智能客服
# 查询订单
curl -X POST http://localhost:8080/api/chatbot/chat \
-H "Content-Type: application/json" \
-d '{"sessionId": "user1", "message": "帮我查一下订单 ORD20240115001 的状态"}'
# 返回:
# {
# "sessionId": "user1",
# "question": "帮我查一下订单 ORD20240115001 的状态",
# "answer": "您的订单 ORD20240115001 当前状态为:已发货\n下单时间:2024-01-15\n预计送达:2024-01-18"
# }
# 查询产品
curl -X POST http://localhost:8080/api/chatbot/chat \
-H "Content-Type: application/json" \
-d '{"sessionId": "user1", "message": "你们有蓝牙耳机吗?"}'
# 多轮对话(上下文记忆)
curl -X POST http://localhost:8080/api/chatbot/chat \
-H "Content-Type: application/json" \
-d '{"sessionId": "user1", "message": "刚才那个订单的物流信息呢?"}'
5.3 对话记忆配置
@Configuration
public class MemoryConfig {
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return sessionId -> MessageWindowChatMemory.builder()
.id(sessionId)
.maxMessages(20) // 保留最近20条消息
.build();
}
}
六、问题 3:如何处理非结构化数据?
6.1 痛点分析
企业中存在大量非结构化数据:
-
PDF 合同
-
Word 报告
-
扫描件图片
-
网页内容
解决方案:Document Loaders + Parsers + AI 提取
6.2 文档处理服务
1. 定义文档处理接口
@Data
@AllArgsConstructor
public class DocumentInfo {
private String filename;
private String content;
private String summary;
private Map<String, String> extractedFields;
private LocalDateTime processedTime;
}
public interface DocumentProcessor {
DocumentInfo process(String filePath) throws IOException;
DocumentInfo processWithExtraction(String filePath,
Map<String, String> extractionRules) throws IOException;
}
2. 实现文档处理服务
@Service
@Slf4j
public class DocumentService implements DocumentProcessor {
private final DocumentParser documentParser;
private final ChatLanguageModel chatModel;
private final ResourceLoader resourceLoader;
public DocumentService(DocumentParser documentParser,
ChatLanguageModel chatModel,
ResourceLoader resourceLoader) {
this.documentParser = documentParser;
this.chatModel = chatModel;
this.resourceLoader = resourceLoader;
}
@Override
public DocumentInfo process(String filePath) throws IOException {
log.info("处理文档: {}", filePath);
// 1. 解析文档
Resource resource = resourceLoader.getResource(filePath);
Document document = documentParser.parse(resource.getInputStream());
String content = document.text();
// 2. 生成摘要
String summary = generateSummary(content);
return new DocumentInfo(
getFilename(filePath),
content,
summary,
new HashMap<>(),
LocalDateTime.now()
);
}
@Override
public DocumentInfo processWithExtraction(String filePath,
Map<String, String> extractionRules) throws IOException {
log.info("处理文档并提取信息: {}", filePath);
// 1. 解析文档
Resource resource = resourceLoader.getResource(filePath);
Document document = documentParser.parse(resource.getInputStream());
String content = document.text();
// 2. 生成摘要
String summary = generateSummary(content);
// 3. 提取结构化信息
Map<String, String> extractedFields = extractFields(content, extractionRules);
return new DocumentInfo(
getFilename(filePath),
content,
summary,
extractedFields,
LocalDateTime.now()
);
}
/**
* 使用 AI 生成文档摘要
*/
private String generateSummary(String content) {
// 截取前3000个字符(避免超出 token 限制)
String truncatedContent = content.length() > 3000
? content.substring(0, 3000) + "..."
: content;
String prompt = String.format("""
请为以下文档生成一个简洁的摘要(不超过200字):
%s
""", truncatedContent);
return chatModel.generate(prompt);
}
/**
* 使用 AI 提取结构化信息
*/
private Map<String, String> extractFields(String content,
Map<String, String> extractionRules) {
StringBuilder rulesText = new StringBuilder();
extractionRules.forEach((field, description) ->
rulesText.append(String.format("- %s:%s\n", field, description))
);
String prompt = String.format("""
请从以下文档中提取指定信息,以 JSON 格式返回:
提取规则:
%s
文档内容:
%s
请返回纯 JSON,不要添加其他说明。
""", rulesText, content.substring(0, Math.min(content.length(), 3000)));
String response = chatModel.generate(prompt);
// 解析 JSON
try {
return new ObjectMapper().readValue(response, new TypeReference<>() {});
} catch (Exception e) {
log.warn("解析提取结果失败", e);
return new HashMap<>();
}
}
private String getFilename(String filePath) {
return filePath.substring(filePath.lastIndexOf("/") + 1);
}
}
3. 文档控制器
@RestController
@RequestMapping("/api/documents")
@RequiredArgsConstructor
public class DocumentController {
private final DocumentService documentService;
/**
* 处理文档并生成摘要
*/
@PostMapping("/process")
public ResponseEntity<DocumentInfo> processDocument(
@RequestParam String filePath) {
try {
DocumentInfo info = documentService.process(filePath);
return ResponseEntity.ok(info);
} catch (IOException e) {
return ResponseEntity.badRequest().build();
}
}
/**
* 处理文档并提取信息(如发票、合同)
*/
@PostMapping("/extract")
public ResponseEntity<DocumentInfo> extractFromDocument(
@RequestParam String filePath,
@RequestBody Map<String, String> extractionRules) {
try {
DocumentInfo info = documentService.processWithExtraction(filePath, extractionRules);
return ResponseEntity.ok(info);
} catch (IOException e) {
return ResponseEntity.badRequest().build();
}
}
}
4. 测试文档处理
# 处理 PDF 文档
curl -X POST "http://localhost:8080/api/documents/process?filePath=classpath:documents/contract.pdf"
# 提取发票信息
curl -X POST "http://localhost:8080/api/documents/extract?filePath=classpath:documents/invoice.pdf" \
-H "Content-Type: application/json" \
-d '{
"发票号码": "发票的唯一编号",
"开票日期": "发票开具的日期",
"金额": "发票总金额",
"购买方": "购买方名称",
"销售方": "销售方名称"
}'
6.3 批量处理流水线
@Service
@Slf4j
public class DocumentPipeline {
private final DocumentService documentService;
private final KnowledgeService knowledgeService;
/**
* 批量处理文档并导入知识库
*/
public PipelineResult processAndIndex(String directoryPath) throws IOException {
PipelineResult result = new PipelineResult();
Resource directory = new ClassPathResource(directoryPath);
File dir = directory.getFile();
File[] files = dir.listFiles();
if (files == null) return result;
for (File file : files) {
try {
// 1. 处理文档
DocumentInfo info = documentService.process("file:" + file.getAbsolutePath());
result.addProcessed(info.getFilename());
// 2. 导入知识库
knowledgeService.loadDocument("file:" + file.getAbsolutePath());
result.addIndexed(info.getFilename());
log.info("文档处理完成: {}", file.getName());
} catch (Exception e) {
result.addFailed(file.getName(), e.getMessage());
log.error("文档处理失败: {}", file.getName(), e);
}
}
return result;
}
}
@Data
class PipelineResult {
private List<String> processed = new ArrayList<>();
private List<String> indexed = new ArrayList<>();
private Map<String, String> failed = new HashMap<>();
public void addProcessed(String filename) { processed.add(filename); }
public void addIndexed(String filename) { indexed.add(filename); }
public void addFailed(String filename, String error) { failed.put(filename, error); }
}
七、问题 4:如何用自然语言查数据库?
7.1 痛点分析
业务人员想看数据,但不会写 SQL:
业务人员:我想看上个月销售额最高的10个产品 开发人员:好的,我帮你写个SQL... (等待3天)
解决方案:Text-to-SQL Agent
7.2 SQL Agent 实现
1. 定义数据查询 AI 服务
@AiService
public interface DataQueryAgent {
@SystemMessage("""
你是一个数据分析助手。你可以帮助用户用自然语言查询数据库。
数据库表结构:
- orders:订单表(id, order_number, user_id, product_id, quantity, amount, status, create_time)
- products:产品表(id, name, category, price, stock)
- users:用户表(id, username, email, register_time)
规则:
1. 将用户的自然语言问题转换为 SQL 查询
2. 执行 SQL 并获取结果
3. 将结果转换为易读的自然语言回答
4. 如果用户的查询可能导致性能问题(如全表扫描),请优化 SQL
5. 对于敏感数据,请提醒用户权限限制
你可以使用以下工具:
- executeSql:执行 SQL 查询
- getTableSchema:获取表结构
""")
String query(@MemoryId String sessionId, @UserMessage String question);
}
2. 数据库工具
@Component
@Slf4j
public class DatabaseTools {
@Autowired
private JdbcTemplate jdbcTemplate;
@Tool("执行 SQL 查询并返回结果")
public String executeSql(@P("SQL 查询语句") String sql) {
log.info("执行 SQL: {}", sql);
// 安全检查
if (isDangerousSql(sql)) {
return "错误:不允许执行危险的 SQL 语句(如 DROP, DELETE, UPDATE, INSERT)";
}
try {
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
if (results.isEmpty()) {
return "查询结果为空";
}
// 格式化结果
StringBuilder sb = new StringBuilder();
sb.append("查询返回 ").append(results.size()).append(" 条记录:\n\n");
// 表头
Set<String> columns = results.get(0).keySet();
sb.append(String.join(" | ", columns)).append("\n");
sb.append("-".repeat(columns.size() * 15)).append("\n");
// 数据行(最多显示50行)
int displayCount = Math.min(results.size(), 50);
for (int i = 0; i < displayCount; i++) {
Map<String, Object> row = results.get(i);
List<String> values = columns.stream()
.map(col -> String.valueOf(row.get(col)))
.toList();
sb.append(String.join(" | ", values)).append("\n");
}
if (results.size() > 50) {
sb.append("... 还有 ").append(results.size() - 50).append(" 条记录");
}
return sb.toString();
} catch (Exception e) {
log.error("SQL 执行失败", e);
return "SQL 执行失败: " + e.getMessage();
}
}
@Tool("获取指定表的结构信息")
public String getTableSchema(@P("表名") String tableName) {
log.info("获取表结构: {}", tableName);
try {
List<Map<String, Object>> columns = jdbcTemplate.queryForList(
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_COMMENT " +
"FROM INFORMATION_SCHEMA.COLUMNS " +
"WHERE TABLE_NAME = ?",
tableName.toUpperCase()
);
if (columns.isEmpty()) {
return "表 " + tableName + " 不存在";
}
StringBuilder sb = new StringBuilder();
sb.append("表 ").append(tableName).append(" 的结构:\n\n");
for (Map<String, Object> col : columns) {
sb.append(String.format("- %s (%s): %s %s\n",
col.get("COLUMN_NAME"),
col.get("DATA_TYPE"),
"YES".equals(col.get("IS_NULLABLE")) ? "可空" : "非空",
col.get("COLUMN_COMMENT") != null ? col.get("COLUMN_COMMENT") : ""
));
}
return sb.toString();
} catch (Exception e) {
return "获取表结构失败: " + e.getMessage();
}
}
private boolean isDangerousSql(String sql) {
String upperSql = sql.toUpperCase().trim();
return upperSql.startsWith("DROP") ||
upperSql.startsWith("DELETE") ||
upperSql.startsWith("UPDATE") ||
upperSql.startsWith("INSERT") ||
upperSql.startsWith("ALTER") ||
upperSql.contains("TRUNCATE");
}
}
3. 数据查询控制器
@RestController
@RequestMapping("/api/data-query")
@RequiredArgsConstructor
public class DataQueryController {
private final DataQueryAgent agent;
/**
* 自然语言查询数据
*/
@PostMapping("/query")
public ResponseEntity<QueryResponse> query(@RequestBody QueryRequest request) {
String answer = agent.query(request.getSessionId(), request.getQuestion());
return ResponseEntity.ok(new QueryResponse(
request.getSessionId(),
request.getQuestion(),
answer
));
}
}
@Data
class QueryRequest {
private String sessionId;
private String question;
}
@Data
@AllArgsConstructor
class QueryResponse {
private String sessionId;
private String question;
private String answer;
}
4. 测试自然语言查询
# 查询销售数据
curl -X POST http://localhost:8080/api/data-query/query \
-H "Content-Type: application/json" \
-d '{"sessionId": "analyst1", "question": "上个月销售额最高的10个产品是什么?"}'
# 返回:
# {
# "sessionId": "analyst1",
# "question": "上个月销售额最高的10个产品是什么?",
# "answer": "上个月销售额最高的10个产品如下:\n1. iPhone 15 Pro - ¥1,280,000\n2. MacBook Pro - ¥980,000\n..."
# }
# 复杂查询
curl -X POST http://localhost:8080/api/data-query/query \
-H "Content-Type: application/json" \
-d '{"sessionId": "analyst1", "question": "对比最近3个月各品类的销售趋势"}'
7.3 查询优化与安全
@Component
public class QueryOptimizer {
/**
* 优化 SQL 查询
*/
public String optimizeQuery(String naturalLanguage, String generatedSql) {
// 1. 检查是否有必要索引
if (generatedSql.contains("WHERE") && !generatedSql.contains("INDEX")) {
// 提示添加索引
}
// 2. 限制返回行数
if (!generatedSql.toUpperCase().contains("LIMIT")) {
generatedSql += " LIMIT 1000";
}
// 3. 检查敏感字段
if (containsSensitiveField(generatedSql)) {
// 脱敏处理
}
return generatedSql;
}
private boolean containsSensitiveField(String sql) {
String lowerSql = sql.toLowerCase();
return lowerSql.contains("password") ||
lowerSql.contains("phone") ||
lowerSql.contains("id_card");
}
}
八、企业级最佳实践
8.1 提示词工程(Prompt Engineering)
结构化提示词模板:
@Component
public class PromptTemplates {
public static final String KNOWLEDGE_QA = """
## 角色
你是企业知识库助手,专注于回答员工关于公司政策、流程、产品的问题。
## 规则
1. 仅基于提供的上下文信息回答问题
2. 如果上下文中没有相关信息,明确告知用户
3. 不要编造或推测信息
4. 回答要简洁、准确、专业
5. 如果问题模糊,可以要求用户澄清
## 输出格式
- 使用清晰的段落结构
- 重要信息用加粗标注
- 必要时使用列表或表格
## 上下文
{context}
## 用户问题
{question}
## 请回答
""";
public static final String SQL_GENERATION = """
## 任务
将用户的自然语言问题转换为 SQL 查询语句。
## 数据库表结构
{schema}
## 规则
1. 只生成 SELECT 查询
2. 使用标准 SQL 语法
3. 添加适当的 LIMIT 限制
4. 避免 SELECT *
5. 对敏感字段进行脱敏
## 用户问题
{question}
## 请生成 SQL
""";
}
8.2 输出格式化
@Component
public class OutputParsers {
/**
* 解析 JSON 输出
*/
public <T> T parseJson(String content, Class<T> clazz) {
try {
// 提取 JSON 部分
String json = extractJson(content);
return new ObjectMapper().readValue(json, clazz);
} catch (Exception e) {
throw new RuntimeException("解析输出失败", e);
}
}
/**
* 从内容中提取 JSON
*/
private String extractJson(String content) {
// 尝试找到 JSON 块
int start = content.indexOf("{");
int end = content.lastIndexOf("}");
if (start != -1 && end != -1) {
return content.substring(start, end + 1);
}
// 尝试 JSON 数组
start = content.indexOf("[");
end = content.lastIndexOf("]");
if (start != -1 && end != -1) {
return content.substring(start, end + 1);
}
throw new RuntimeException("未找到有效的 JSON");
}
}
8.3 错误处理与重试
@Configuration
public class RetryConfig {
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
// 重试策略
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(3);
// 退避策略
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2);
backOffPolicy.setMaxInterval(10000);
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOffPolicy);
return template;
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class ResilientAiService {
private final ChatLanguageModel chatModel;
private final RetryTemplate retryTemplate;
public String generateWithRetry(String prompt) {
return retryTemplate.execute(context -> {
log.info("调用 LLM,尝试次数: {}", context.getRetryCount() + 1);
return chatModel.generate(prompt);
}, context -> {
log.error("LLM 调用失败,已重试 {} 次", context.getRetryCount());
return "抱歉,AI 服务暂时不可用,请稍后再试。";
});
}
}
8.4 成本控制
@Component
@Slf4j
public class TokenManager {
// Token 使用统计
private final Map<String, AtomicInteger> dailyUsage = new ConcurrentHashMap<>();
@Value("${app.token.daily-limit:100000}")
private int dailyLimit;
/**
* 检查是否超出每日限额
*/
public boolean checkLimit(String userId) {
AtomicInteger usage = dailyUsage.computeIfAbsent(userId, k -> new AtomicInteger(0));
return usage.get() < dailyLimit;
}
/**
* 记录 Token 使用
*/
public void recordUsage(String userId, int tokens) {
AtomicInteger usage = dailyUsage.computeIfAbsent(userId, k -> new AtomicInteger(0));
int total = usage.addAndGet(tokens);
log.info("用户 {} 使用 {} tokens,今日累计: {}", userId, tokens, total);
}
/**
* 优化 Prompt 以减少 Token 使用
*/
public String optimizePrompt(String prompt) {
// 1. 移除多余空白
prompt = prompt.replaceAll("\\s+", " ").trim();
// 2. 截断过长的上下文
if (prompt.length() > 8000) {
prompt = prompt.substring(0, 8000) + "\n...(内容已截断)";
}
return prompt;
}
}
九、生产部署与监控
9.1 部署架构
┌─────────────────────────────────────────────────────────┐ │ Nginx (负载均衡) │ └────────────────────────┬────────────────────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ App-1 │ │ App-2 │ │ App-3 │ │ (Pod) │ │ (Pod) │ │ (Pod) │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ └────────────────┼────────────────┘ │ ┌────────────────┼────────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Redis │ │ MySQL │ │ Milvus │ │ (缓存) │ │ (数据库)│ │ (向量库)│ └─────────┘ └─────────┘ └─────────┘
9.2 Kubernetes 部署配置
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: langchain4j-app spec: replicas: 3 selector: matchLabels: app: langchain4j-app template: metadata: labels: app: langchain4j-app spec: containers: - name: app image: your-registry/langchain4j-app:latest ports: - containerPort: 8080 env: - name: OPENAI_API_KEY valueFrom: secretKeyRef: name: langchain4j-secrets key: openai-api-key - name: SPRING_PROFILES_ACTIVE value: "prod" resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1000m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 60 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 5
9.3 监控指标
@Component
@Slf4j
public class MetricsCollector {
private final MeterRegistry meterRegistry;
public MetricsCollector(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
/**
* 记录 LLM 调用指标
*/
public void recordLlmCall(String model, long durationMs, int tokens, boolean success) {
// 调用次数
meterRegistry.counter("llm.calls.total",
"model", model,
"success", String.valueOf(success)
).increment();
// 调用耗时
meterRegistry.timer("llm.calls.duration",
"model", model
).record(Duration.ofMillis(durationMs));
// Token 使用
meterRegistry.counter("llm.tokens.total",
"model", model
).increment(tokens);
log.debug("LLM 调用: model={}, duration={}ms, tokens={}, success={}",
model, durationMs, tokens, success);
}
/**
* 记录 RAG 检索指标
*/
public void recordRagRetrieval(int retrievedDocs, long durationMs) {
meterRegistry.timer("rag.retrieval.duration")
.record(Duration.ofMillis(durationMs));
meterRegistry.gauge("rag.retrieval.docs", retrievedDocs);
}
}
9.4 性能优化建议
| 优化点 | 措施 | 效果 |
|---|---|---|
| 响应延迟 | 使用流式输出(Streaming) | 首字响应时间降低 80% |
| 成本控制 | Prompt 缓存 + 压缩 | Token 成本降低 30% |
| 并发处理 | 异步调用 + 线程池 | 吞吐量提升 5 倍 |
| 检索速度 | 向量索引优化 | 检索延迟 < 100ms |
| 可用性 | 多 LLM 供应商切换 | 可用性 99.9% |
9.5 流式输出实现
@RestController
@RequestMapping("/api/stream")
@RequiredArgsConstructor
public class StreamController {
private final ChatLanguageModel chatModel;
/**
* 流式对话接口
*/
@GetMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String message) {
return Flux.create(sink -> {
// 使用流式模型
StreamingChatLanguageModel streamingModel = OpenAiStreamingChatModel.builder()
.apiKey("your-api-key")
.modelName("gpt-3.5-turbo")
.build();
streamingModel.chat(message, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
sink.next(partialResponse);
}
@Override
public void onCompleteResponse(ChatResponse response) {
sink.complete();
}
@Override
public void onError(Throwable error) {
sink.error(error);
}
});
});
}
}
十、总结与展望
10.1 核心要点回顾
┌─────────────────────────────────────────────────────────┐ │ LangChain4j 核心能力 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 1. 统一的 LLM 接口 │ │ └── 一套代码支持多种大模型 │ │ │ │ 2. RAG(检索增强生成) │ │ └── 让 AI 理解企业私有数据 │ │ │ │ 3. 对话记忆 │ │ └── 多轮对话上下文管理 │ │ │ │ 4. 工具调用(Function Calling) │ │ └── 让 AI 调用外部系统 │ │ │ │ 5. AI Services(声明式接口) │ │ └── 像写 Feign 客户端一样写 AI 接口 │ │ │ └─────────────────────────────────────────────────────────┘
10.2 适用场景
| 场景 | 推荐方案 | 复杂度 |
|---|---|---|
| 简单问答 | Chat Model + Prompt | ⭐ |
| 知识库问答 | RAG | ⭐⭐ |
| 智能客服 | RAG + Memory + Tools | ⭐⭐⭐ |
| 文档处理 | Document Loaders + AI | ⭐⭐⭐ |
| 数据分析 | Text-to-SQL Agent | ⭐⭐⭐⭐ |
| 复杂工作流 | Multi-Agent | ⭐⭐⭐⭐⭐ |
10.3 学习路线建议
第一阶段:基础入门 ├── 理解 LLM 基本概念 ├── LangChain4j Hello World └── 调用 OpenAI API 第二阶段:核心功能 ├── Prompt Engineering ├── 输出解析 └── 对话记忆 第三阶段:RAG 实战 ├── 文档加载与解析 ├── 向量数据库使用 └── 检索策略优化 第四阶段:Agent 开发 ├── 工具调用 ├── 多轮对话 └── 多 Agent 协作 第五阶段:企业级应用 ├── 性能优化 ├── 成本控制 └── 生产部署
10.4 未来展望
-
多模态支持:图像、音频、视频处理
-
更强大的 Agent:自主规划、多步推理
-
本地化部署:更多国产模型支持
-
低代码平台:可视化 AI 应用构建
-
标准化:AI 应用开发的最佳实践
参考资源
-
Spring AI - Spring 官方 AI 框架
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题也欢迎在评论区留言讨论。
附录:常见问题解答
Q1: LangChain4j 和 Spring AI 有什么区别?
| 特性 | LangChain4j | Spring AI |
|---|---|---|
| 成熟度 | 较成熟,社区活跃 | 较新,发展中 |
| 功能完整度 | 功能丰富 | 核心功能完整 |
| Spring 集成 | 良好 | 原生支持 |
| 学习曲线 | 中等 | 较低 |
| 推荐场景 | 复杂 AI 应用 | Spring 生态项目 |
Q2: 如何选择 LLM 提供商?
-
OpenAI:功能最强,成本较高,适合原型验证
-
Azure OpenAI:企业级,合规性好,适合生产环境
-
Ollama(本地):免费,数据不出境,适合敏感数据
-
国产模型:中文优化,成本低,适合中文场景
Q3: 如何降低 LLM 调用成本?
-
使用更小的模型(GPT-3.5 vs GPT-4)
-
优化 Prompt,减少 Token 使用
-
实现缓存机制
-
批量处理请求
-
使用本地模型
Q4: RAG 的准确率如何提升?
-
优化文档分块策略
-
使用更好的嵌入模型
-
实现混合检索(向量 + 关键词)
-
添加重排序(Reranking)
-
持续优化 Prompt
全文完
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)