LangChain4j实战:Chain、Memory、Tool的Java实现与Spring整合
LangChain4j实战:Chain、Memory、Tool的Java实现与Spring整合
适读人群:Java后端工程师、AI应用开发者 | 阅读时长:约20分钟 | 依赖:LangChain4j 0.36、Spring Boot 3.3
开篇故事
说一个真实的选型故事。去年年初,我们团队同时有两个AI项目:一个是企业内部的知识库问答,另一个是面向外部用户的智能客服Agent。
知识库问答那个,我选了Spring AI,原因是团队都是Spring工程师,学习成本低,快速上线。
智能客服Agent那个,我用了LangChain4j。原因是它的AiService接口设计太优雅了——用Java接口注解的方式定义AI助手的行为,再复杂的工具调用、记忆管理,写出来的代码看起来就像普通的Service层,业务逻辑和AI调用完全解耦。
在那个客服Agent项目里,我需要实现:根据用户问题查询订单系统、查询知识库、如果都没答案就转人工。这种多工具协作的场景,LangChain4j的工具机制处理起来异常流畅。
今天把LangChain4j的核心用法从工程角度完整地整理一遍,重点是Chain、Memory、Tool这三个最重要的模块。
一、核心问题分析
LangChain4j和Spring AI的核心差异在于设计哲学:
Spring AI的思路是"把AI接口Spring化",对Spring工程师最友好,集成Spring生态最简单。
LangChain4j的思路是"用Java接口抽象AI行为",核心是AiService接口声明式定义,更面向工程化、可测试性,Prompt模板管理更系统。
两者不是非此即彼,实际项目可以共存——Spring AI负责基础的ChatClient和VectorStore,LangChain4j负责更复杂的Agent逻辑。
LangChain4j的三大核心能力:
- Chain:将多步AI处理串联成流水线,每步的输出是下步的输入
- Memory:对话历史管理,支持内存/Redis/数据库多种存储
- Tool:Java方法注解为AI工具,LLM可以自动判断何时调用
二、原理深度解析
2.1 LangChain4j架构
2.2 AiService的工作机制
LangChain4j的AiService是通过Java动态代理实现的。当你调用接口方法时,代理会:
- 读取方法上的
@SystemMessage和@UserMessage注解,构建Prompt - 从ChatMemory加载历史对话
- 收集可用的Tool方法,转换为工具规格说明
- 调用LLM,如果LLM决定调用工具,执行工具方法
- 把工具结果回传给LLM继续生成
- 把最终结果保存到ChatMemory
- 返回最终响应
三、完整代码实现
3.1 Maven依赖
<dependencies>
<!-- LangChain4j核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.36.2</version>
</dependency>
<!-- OpenAI集成 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.36.2</version>
</dependency>
<!-- Spring Boot集成 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>0.36.2</version>
</dependency>
<!-- 向量存储:pgvector -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-pgvector</artifactId>
<version>0.36.2</version>
</dependency>
<!-- Redis记忆存储 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-redis</artifactId>
<version>0.36.2</version>
</dependency>
</dependencies>3.2 AiService接口定义(核心特性)
/**
* 智能客服助手接口
* LangChain4j会自动生成实现,注入为Spring Bean
*/
@AiService
public interface CustomerServiceAgent {
/**
* 基础问答,带系统提示
*/
@SystemMessage("""
你是一名专业的客服助手,负责处理用户的订单查询、退换货、投诉等问题。
请保持礼貌、耐心,用简洁的语言回答。
如果无法解决,引导用户联系人工客服:400-xxx-xxxx
""")
String chat(@MemoryId String userId, @UserMessage String userMessage);
/**
* 带会话记忆的专项咨询
*/
@SystemMessage("你是一名售后退款专员,专注处理退款相关问题。")
@UserMessage("用户{{userId}}咨询退款问题:{{issue}}")
String handleRefundInquiry(
@V("userId") String userId,
@V("issue") String issue,
@MemoryId String sessionId);
/**
* 无记忆的一次性任务(分类、摘要等)
*/
@UserMessage("请将以下客服工单分类:{{ticket}}\n分类选项:技术问题、订单问题、退款申请、投诉建议、其他")
TicketCategory classifyTicket(@V("ticket") String ticketContent);
enum TicketCategory {
TECHNICAL, ORDER, REFUND, COMPLAINT, OTHER
}
}3.3 工具(Tool)定义
/**
* 客服工具集——LLM可以自动决定何时调用
*/
@Component
public class CustomerServiceTools {
private final OrderRepository orderRepository;
private final KnowledgeBaseService knowledgeBase;
private final HumanHandoffService humanHandoff;
public CustomerServiceTools(OrderRepository orderRepository,
KnowledgeBaseService knowledgeBase,
HumanHandoffService humanHandoff) {
this.orderRepository = orderRepository;
this.knowledgeBase = knowledgeBase;
this.humanHandoff = humanHandoff;
}
@Tool("根据订单号查询订单状态、物流信息和预计到达时间")
public String queryOrderStatus(@P("订单号,格式为ORD+数字") String orderId) {
return orderRepository.findById(orderId)
.map(order -> String.format(
"订单%s:状态=%s,物流单号=%s,预计%s到达",
orderId, order.getStatus(),
order.getTrackingNumber(),
order.getEstimatedDelivery()))
.orElse("订单" + orderId + "不存在,请确认订单号是否正确");
}
@Tool("查询退款政策和退款进度,支持按订单号或退款申请号查询")
public String queryRefundInfo(@P("订单号或退款申请单号") String id) {
RefundInfo info = orderRepository.findRefundInfo(id);
if (info == null) {
return "未找到相关退款记录。退款政策:自签收之日起7日内支持无理由退货,退款将在1-3个工作日内到账。";
}
return String.format("退款申请%s:状态=%s,金额=%.2f元,预计到账时间=%s",
info.getId(), info.getStatus(), info.getAmount(), info.getEstimatedDate());
}
@Tool("当用户问题无法解决或用户要求转人工时,触发转人工流程")
public String transferToHuman(
@P("用户ID") String userId,
@P("问题摘要") String issueSummary) {
String ticketId = humanHandoff.createTicket(userId, issueSummary);
return String.format(
"已为您创建人工服务工单,工单号:%s。" +
"人工客服将在30分钟内联系您,您也可以拨打400-xxx-xxxx直接咨询。",
ticketId);
}
@Tool("搜索常见问题知识库,回答产品使用、政策等常见问题")
public String searchKnowledge(@P("用户的问题") String question) {
List<String> results = knowledgeBase.search(question, 3);
if (results.isEmpty()) {
return "知识库中暂无相关内容";
}
return "相关内容:\n" + String.join("\n---\n", results);
}
}3.4 对话记忆配置
@Configuration
public class LangChain4jConfig {
/**
* 基于Redis的对话记忆(生产推荐)
*/
@Bean
@ConditionalOnProperty(name = "ai.memory.type", havingValue = "redis")
public ChatMemoryStore redisChatMemoryStore(
@Value("${spring.redis.host}") String redisHost,
@Value("${spring.redis.port}") int redisPort) {
return RedisChatMemoryStore.builder()
.host(redisHost)
.port(redisPort)
.keyPrefix("chat_memory:")
.ttl(Duration.ofHours(24)) // 会话记忆保留24小时
.build();
}
/**
* 内存版对话记忆(开发调试用)
*/
@Bean
@ConditionalOnProperty(name = "ai.memory.type",
havingValue = "in-memory", matchIfMissing = true)
public ChatMemoryStore inMemoryChatMemoryStore() {
return new InMemoryChatMemoryStore();
}
/**
* 配置ChatMemory:保留最近15轮对话
*/
@Bean
public ChatMemoryProvider chatMemoryProvider(ChatMemoryStore store) {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(30) // 15轮=30条消息(用户+助手各一条)
.chatMemoryStore(store)
.build();
}
/**
* 配置客服Agent:集成工具、记忆、RAG
*/
@Bean
public CustomerServiceAgent customerServiceAgent(
ChatLanguageModel chatModel,
ChatMemoryProvider memoryProvider,
CustomerServiceTools tools,
ContentRetriever contentRetriever) {
return AiServices.builder(CustomerServiceAgent.class)
.chatLanguageModel(chatModel)
.chatMemoryProvider(memoryProvider)
.tools(tools)
.contentRetriever(contentRetriever) // 自动RAG
.build();
}
/**
* RAG内容检索器
*/
@Bean
public ContentRetriever contentRetriever(EmbeddingStore<TextSegment> embeddingStore,
EmbeddingModel embeddingModel) {
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.6)
.build();
}
}3.5 Pipeline Chain(多步处理链)
@Service
public class DocumentProcessingChain {
private final ChatLanguageModel chatModel;
public DocumentProcessingChain(ChatLanguageModel chatModel) {
this.chatModel = chatModel;
}
/**
* 文档处理流水线:翻译 -> 摘要 -> 关键词提取
*/
public DocumentAnalysisResult processDocument(String rawDocument) {
// 第一步:如果是英文,先翻译成中文
String translatedDoc = translateIfNeeded(rawDocument);
// 第二步:生成摘要
String summary = generateSummary(translatedDoc);
// 第三步:提取关键词
List<String> keywords = extractKeywords(translatedDoc);
// 第四步:情感分析
String sentiment = analyzeSentiment(translatedDoc);
return new DocumentAnalysisResult(translatedDoc, summary, keywords, sentiment);
}
private String translateIfNeeded(String text) {
// 简单语言检测:超过一半是ASCII且不是数字,认为是英文
long asciiCount = text.chars().filter(c -> c < 128 && Character.isLetter(c)).count();
double asciiRatio = (double) asciiCount / text.length();
if (asciiRatio > 0.5) {
return chatModel.generate(
"请将以下英文文本翻译成中文,保持专业术语准确:\n\n" + text);
}
return text;
}
private String generateSummary(String text) {
return chatModel.generate(
"请用不超过200字总结以下文档的核心内容:\n\n" + text);
}
private List<String> extractKeywords(String text) {
String result = chatModel.generate(
"从以下文本中提取5-10个最重要的关键词,用逗号分隔,不要加序号:\n\n" + text);
return Arrays.asList(result.split("[,,]\\s*"));
}
private String analyzeSentiment(String text) {
return chatModel.generate(
"分析以下文本的情感倾向,只输出:正面/负面/中性\n\n" + text);
}
@Data
@AllArgsConstructor
public static class DocumentAnalysisResult {
private String processedContent;
private String summary;
private List<String> keywords;
private String sentiment;
}
}3.6 Controller层集成
@RestController
@RequestMapping("/api/customer-service")
public class CustomerServiceController {
private final CustomerServiceAgent agent;
public CustomerServiceController(CustomerServiceAgent agent) {
this.agent = agent;
}
@PostMapping("/chat")
public ChatResponse chat(@RequestBody ChatRequest request) {
long start = System.currentTimeMillis();
String response = agent.chat(request.getUserId(), request.getMessage());
return new ChatResponse(response, System.currentTimeMillis() - start);
}
@PostMapping("/classify")
public String classify(@RequestBody String ticketContent) {
return agent.classifyTicket(ticketContent).name();
}
@Data
public static class ChatRequest {
private String userId;
private String message;
}
@Data
@AllArgsConstructor
public static class ChatResponse {
private String content;
private long latencyMs;
}
}四、效果评估与优化
在智能客服项目上线3个月的数据:
| 指标 | LangChain4j实现 | 旧版规则引擎 |
|---|---|---|
| 一次解决率 | 78.3% | 45.2% |
| 平均对话轮次(至解决) | 3.2轮 | 7.8轮 |
| 工具调用准确率(正确选择工具) | 91.5% | - |
| 用户满意度评分 | 4.3/5.0 | 3.1/5.0 |
| 转人工比例 | 21.7% | 54.8% |
工具调用准确率是一个有意思的指标——LLM自动判断什么时候查订单、什么时候查知识库、什么时候转人工,91.5%的准确率远超我的预期(调优前只有78%)。
调优工具调用准确率的关键是优化@Tool注解里的描述文字:描述越精确,LLM越能正确选择工具。比如最初我的queryOrderStatus描述是"查询订单",LLM经常在用户问退款时也调用它。改成"根据订单号查询物流配送状态和预计到达时间"之后,LLM的选择就准确多了。
五、踩坑实录
坑1:@MemoryId必须是String类型
我一开始把@MemoryId对应的参数类型用了Long(用户的数据库ID),LangChain4j没有报错,但记忆存储的key是null,所有用户共用了同一个记忆空间,互相串了对话历史。翻文档才发现@MemoryId只支持String类型。改成userId.toString()传入就解决了。
坑2:工具方法不能有检查型异常
工具方法上如果throws了受检异常,LangChain4j的代理无法生成。我的queryOrderStatus一开始声明了throws DatabaseException,编译没问题,但运行时抛出了IllegalArgumentException,提示工具方法签名不合法。工具方法的异常要在内部catch住,返回错误描述字符串,而不是向外抛出。
坑3:LangChain4j的Spring Boot Starter自动配置有时候会和Spring AI冲突
我在一个项目里同时用了Spring AI和LangChain4j,两个框架都会尝试自动配置OpenAI的Bean,名字相同导致冲突。解决方案是在LangChain4j的Starter配置上加@Primary注解明确优先级,或者把其中一个的自动配置关掉,手动创建Bean。
六、总结
LangChain4j的AiService接口是它最大的亮点——用声明式的Java接口定义AI助手的行为,代码简洁且高度可测试(可以轻松Mock AiService做单元测试)。对于需要复杂工具调用和多轮对话管理的Agent场景,LangChain4j比Spring AI更顺手。
如果你的项目主要是RAG问答,Spring AI足够了;如果你要做带工具调用的AI Agent,LangChain4j的AiService方案值得认真考虑。两者并不互斥,可以根据具体场景选择最合适的工具。
