首页 > 教程攻略 > ai教程 >Java 开发者的 AI 利器:LangChain4j 从入门到企业实战

Java 开发者的 AI 利器:LangChain4j 从入门到企业实战

来源:互联网 时间:2026-06-13 07:23:07

前言

2023年,大语言模型(LLM)席卷全球。Python生态凭借LangChain框架迅速占领了AI应用开发的高地。而作为企业级开发主力军的Ja va开发者,却面临一个尴尬的问题:如何在Ja va项目中集成LLM能力?

Ja va 开发者的 AI 利器:LangChain4j 从入门到企业实战

答案就是LangChain4j——Ja va生态的LLM应用开发框架。

本文将从企业面临的四个真实问题出发,带你从零掌握LangChain4j,并实现四个完整的企业级应用场景。


一、企业面临的真实问题

在AI时代,企业面临以下挑战:

1.1 知识管理困境

员工每天花2.5小时搜索信息

企业文档散落在各个系统

新员工培训周期长

重复回答相同问题

问题:如何让AI理解企业内部文档,成为知识助手?

1.2 客服成本高企

人工客服成本高

7×24小时难以保障

回答质量参差不齐

高峰期响应慢

问题:如何构建智能客服,自动处理80%的常见问题?

1.3 非结构化数据处理

合同、发片、报告堆积如山

人工提取信息效率低

容易出错

无法批量处理

问题:如何自动解析文档,提取关键信息?

1.4 数据查询门槛

业务人员想看数据

不会写SQL

依赖开发人员

需求响应慢

问题:如何让业务人员用自然语言查询数据库?

LangChain4j就是解决这些问题的利器。


二、LangChain4j是什么

2.1 定义

LangChain4j是一个Ja va框架,用于简化LLM(大语言模型)应用开发。它是Python LangChain的Ja va实现,但并非简单移植,而是充分利用Ja va生态的优势。

先初步看一下它和Python版LangChain的对照:

┌─────────────────────────────────────────────────────────┐
│ LLM 应用开发框架 │
├────────────────────────┬────────────────────────────────┤
│ Python LangChain │ LangChain4j (Ja va) │
├────────────────────────┼────────────────────────────────┤
│✓ 生态成熟 │✓ 企业级稳定性 │
│✓ 社区活跃 │✓ Spring Boot 集成 │
│✓ 快速原型 │✓ 类型安全 │
│✗ 企业应用部署复杂 │✓ 成熟的工程实践 │
│✗ 运行时性能 │✓ 与现有Ja va系统无缝集成 │
└────────────────────────┴────────────────────────────────┘

2.2 核心特性

1. 统一的LLM接口

这套框架的核心设计理念是:一套代码,支持多种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 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提供商

提供商模型示例特点
OpenAIGPT-4, GPT-3.5最强大,成本较高
Azure OpenAIGPT-4, GPT-3.5企业级,合规性好
OllamaLlama2, Mistral本地部署,免费
Hugging Face各种开源模型丰富的模型库
GoogleGemini多模态支持
AnthropicClaude长上下文
国产模型通义千问、文心一言中文优化

2.4 与Spring Boot的完美集成



dev.langchain4j
langchain4j-open-ai-spring-boot-starter
0.25.0

# 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创建项目,或手动添加依赖。一个典型的企业级项目依赖如下:


xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ma ven.apache.org/POM/4.0.0 https://ma ven.apache.org/xsd/ma ven-4.0.0.xsd">

4.0.0


org.springframework.boot
spring-boot-starter-parent
3.2.0

com.example
langchain4j-demo
0.0.1-SNAPSHOT
langchain4j-demo
LangChain4j企业级实战示例


17
0.25.0




org.springframework.boot
spring-boot-starter-web



dev.langchain4j
langchain4j
${langchain4j.version}



dev.langchain4j
langchain4j-spring-boot-starter
${langchain4j.version}



dev.langchain4j
langchain4j-open-ai-spring-boot-starter
${langchain4j.version}



dev.langchain4j
langchain4j-embeddings-all-minilm-l6-v2
${langchain4j.version}



dev.langchain4j
langchain4j-document-parser-apache-tika
${langchain4j.version}



com.h2database
h2
runtime


org.springframework.boot
spring-boot-starter-data-jpa



org.projectlombok
lombok
true




org.springframework.boot
spring-boot-ma ven-plugin



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/ja va/com/example/langchain4j/
├── Langchain4jDemoApplication.ja va
├── config/
│ └── LangChain4jConfig.ja va
├── controller/
│ ├── KnowledgeController.ja va
│ ├── ChatbotController.ja va
│ ├── DocumentController.ja va
│ └── DataQueryController.ja va
├── service/
│ ├── KnowledgeService.ja va
│ ├── ChatbotService.ja va
│ ├── DocumentService.ja va
│ └── DataQueryService.ja va
├── model/
│ ├── ChatMessage.ja va
│ └── QueryResult.ja va
├── agent/
│ ├── Assistant.ja va
│ └── tools/
│ ├── WeatherTool.ja va
│ └── DatabaseTool.ja va
└── rag/
├── DocumentLoader.ja va
└── Retriever.ja va

src/main/resources/
├── application.yml
└── documents/
├── product-manual.pdf
├── faq.txt
└── company-policy.docx


四、问题1:如何让AI理解企业文档?

4.1 痛点分析

大语言模型(如GPT-4)的知识截止到训练时间点,而且不知道企业内部的私有数据。

用户问:我们公司的请假流程是什么?
LLM答:一般来说,请假流程是...(胡说八道)

解决方案:RAG(检索增强生成)

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 embeddingStore() {
return new InMemoryEmbeddingStore<>();
}

@Bean
public DocumentParser documentParser() {
return new ApacheTikaDocumentParser();
}

@Bean
public EmbeddingStoreIngestor embeddingStoreIngestor(
EmbeddingModel embeddingModel,
EmbeddingStore embeddingStore) {

return EmbeddingStoreIngestor.builder()
.documentSplitter(DocumentSplitters.recursive(chunkSize, chunkOverlap))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
}

@Bean
public ContentRetriever contentRetriever(
EmbeddingModel embeddingModel,
EmbeddingStore 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> 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> 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> 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 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 products = productRepository.findByNameContaining(keyword);

if (products.isEmpty()) {
return "未找到相关产品";
}

StringBuilder sb = new StringBuilder("为您找到以下产品:");
for (Product product : products) {
sb.append(String.format("- %s:¥%.2f,库存 %d 件",
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 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 extractedFields;
private LocalDateTime processedTime;
}

public interface DocumentProcessor {
DocumentInfo process(String filePath) throws IOException;
DocumentInfo processWithExtraction(String filePath, Map 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 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 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 extractFields(String content,
Map 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 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 extractFromDocument(
@RequestParam String filePath,
@RequestBody Map 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 processed = new ArrayList<>();
private List indexed = new ArrayList<>();
private Map 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> results = jdbcTemplate.queryForList(sql);

if (results.isEmpty()) {
return "查询结果为空";
}

// 格式化结果
StringBuilder sb = new StringBuilder();
sb.append("查询返回 ").append(results.size()).append(" 条记录:\n");

// 表头
Set 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 row = results.get(i);
List 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> 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");

for (Map 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 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..."
# }

# 复杂查询
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 parseJson(String content, Class 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 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) + "...(内容已截断)";
}

return prompt;
}
}


九、生产部署与监控

9.1 部署架构

在生产环境中,一个典型的部署架构如下。前端由Nginx做负载均衡,后端服务可以水平扩展,配合Redis做缓存、MySQL做业务库、Milvus等向量数据库存储知识库。

┌─────────────────────────────────────────────────────────┐
│ 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 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 未来展望

  1. 多模态支持:图像、音频、视频处理
  2. 更强大的Agent:自主规划、多步推理
  3. 本地化部署:更多国产模型支持
  4. 低代码平台:可视化AI应用构建
  5. 标准化:AI应用开发的最佳实践

参考资源

  • LangChain4j官方文档
  • LangChain4j GitHub
  • Spring AI - Spring官方AI框架
  • OpenAI API文档

附录:常见问题解答

Q1: LangChain4j和Spring AI有什么区别?

特性LangChain4jSpring AI
成熟度较成熟,社区活跃较新,发展中
功能完整度功能丰富核心功能完整
Spring集成良好原生支持
学习曲线中等较低
推荐场景复杂AI应用Spring生态项目

Q2: 如何选择LLM提供商?

  • OpenAI:功能最强,成本较高,适合原型验证
  • Azure OpenAI:企业级,合规性好,适合生产环境
  • Ollama(本地):免费,数据不出境,适合敏感数据
  • 国产模型:中文优化,成本低,适合中文场景

Q3: 如何降低LLM调用成本?

  1. 使用更小的模型(GPT-3.5 vs GPT-4)
  2. 优化Prompt,减少Token使

相关下载