如果你是一名Java后端工程师,最近打算在公司内部做一个基于大模型的问答系统,大概率会陷入一种尴尬:Python生态的LangChain教程一搜一大把,Java这边却像在等一个“Spring Boot版本的LangChain”。这个等待可以结束了。LangChain4j正是面向Java与JVM生态的LLM应用开发框架,它把模型调用、RAG检索、智能体编排封装成Java程序员熟悉的API,而且能和Spring Boot无缝集成。
很多人以为LangChain4j只是把Python的LangChain“翻译”成Java,真正用起来才发现,它解决的是Java项目接入大模型的工程化问题:统一供应商接入、简化文档检索、提供工具调用机制,同时让你在已有Spring Boot项目里叠加AI能力,而不必推倒重写。
这篇文章会从一个可运行的视角切入,先讲清楚RAG和智能体的核心原理,再通过一个Spring Boot项目演示如何集成LangChain4j,实现带知识库检索和工具调用的问答应用,最后整理常见问题与最佳实践。读完之后,你不仅知道怎么用,还能判断什么时候该用、什么时候不要硬用。
1. 为什么Java开发者应该关注LangChain4j
1.1 Java生态中LLM开发之痛
过去两年,大模型应用开发基本被Python“垄断”。生态工具、示例代码、技术评测绝大多数以Python优先,导致Java团队面临一种尴尬局面:底层业务是Java/Spring Boot,但一到AI环节就不得不拆出一个Python服务,或者用HTTP硬调模型接口,然后自己拼接Prompt、管理上下文、处理文档切分。
这不是不能做,而是很累。凡是写过这类代码的人都知道,真正麻烦的不是调用模型API那一行代码,而是围绕它的一圈工程问题:
- 多模型供应商如何切换?
- 对话多轮历史如何保存和维护?
- 文档PDF、Word、TXT如何切分,如何向量化?
- 如何把检索到的内容塞进Prompt并控制长度?
- 如何让模型调用数据库查询、工单查询等内部接口?
这些问题在RAG和智能体场景中尤其突出。LangChain4j把这些问题抽象成了统一API,并提供与Spring Boot的集成支持,让Java团队在熟悉的工程体系内完成LLM应用开发。
1.2 LangChain4j是什么
LangChain4j是一个面向Java/JVM的LLM开发框架,名字致敬LangChain,但并非简单直译。它保留了LangChain的分层思想:模型接入、Prompt管理、文档处理、向量存储、检索增强、Agent工具调用等,同时深度复用Java生态的成熟组件,比如Spring Boot的自动配置、Jackson处理JSON、SLF4J记录日志。
相比Python LangChain,LangChain4j有几个明显的Java式设计:
- 通过接口抽象
ChatLanguageModel、EmbeddingModel,切换模型只改配置。 - 使用
AiServices创建面向业务的服务对象,开发者不需要直接拼Prompt。 @Tool注解把普通Java方法暴露给模型调用,省去手写Function Calling协议。- 与Spring Boot集成时,可以使用starter依赖,配置文件里声明模型参数即可。
对于已经有Spring Boot资产的团队来说,LangChain4j的学习成本远低于想象。
1.3 这篇文章真正解决的问题
这篇文章的目标不是带你浏览LangChain4j全部特性,而是解决一个具体问题:如何在Spring Boot项目中落地一套“能回答业务问题、能调用业务工具”的AI应用。
你会看到:
- RAG知识库问答如何从零搭起来,包括文档加载、切分、嵌入、检索和生成。
- 结构化知识库与向量知识库的边界在哪里,什么场景该用哪个。
- 智能体如何通过工具调用和对话记忆,把静态问答变成可执行动作。
- 在Spring Boot工程里,如何配置、扩展、排错,以及哪些坑最值得提前避开。
如果你正在做内部知识库、智能客服、Copilot类功能,这篇文章的代码和思路可以直接复用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RAG、知识库与智能体的核心概念
2.1 RAG解决了什么
RAG的全称是Retrieval-Augmented Generation,检索增强生成。它的核心思想很简单:不直接让模型凭记忆回答,而是先从外部知识库中检索出与问题最相关的片段,再把这些片段作为参考上下文交给模型生成答案。
这样做能带来三个直接好处:
- 缓解大模型幻觉,回答有据可依。
- 知识更新无需重新训练模型。
- 敏感业务数据可以保留在企业内部,不暴露给模型预训练。
实际项目中,RAG的瓶颈往往不在模型,而在检索质量。如果切分不合理、嵌入模型不匹配、召回结果不准确,即使模型很强,答案也会跑偏。所以这篇文章会刻意关注检索链路中的工程细节。
2.2 RAG知识库与结构化知识库的区分
很多人会混淆“RAG知识库”和“结构化知识库”。简单说:
- RAG知识库通常指“非结构化文档库”,比如PDF、Word、Markdown,先切分再向量化,通过语义相似度检索。适合“根据技术手册查问题”“根据制度文档做解答”这类语义匹配场景。
- 结构化知识库一般指数据库表、CSV、规则引擎。适合“查订单状态”“计算价格”这类精确查询场景。
两者不是替代关系,而是互补。RAG适合回答“怎么配置数据源”“退货政策是什么”;结构化查询适合回答“订单A的金额是多少”。真实产品里经常是两种模式混合:先用RAG定位问题类型,再调用SQL或API获取精确数据。
2.3 智能体与普通API调用的区别
普通API调用是一次问答:用户输入,模型输出。智能体在此基础上增加了“感知、推理、行动”的循环:模型可以决定调用哪个工具、传什么参数、看到结果后继续推理,直到完成任务。
例如用户问“帮我查一下昨天的销售报表,如果是增长趋势就生成一句总结”。普通API可能只会生成一段话;智能体则可能先查询数据库,拿到数据后判断趋势,再生成回答。LangChain4j通过AiServices配合@Tool方法,实现了这种循环,而且对Java开发者非常友好。
2.4 为什么要在Spring Boot中落地
Spring Boot已经是Java后端的事实标准。把AI能力直接嵌进去,意味着可以复用现有的Bean、事务、权限、监控体系。比如智能体要调用内部订单查询接口时,可以直接注入Service,而不是再起一个Python服务去对接。
LangChain4j官方提供了langchain4j-spring-boot-starter,它会在Spring容器里自动创建ChatLanguageModel、EmbeddingModel等Bean,极大减少样板代码。这也是我推荐Java团队优先用LangChain4j而不是自研封装的原因。
3. 环境准备与Spring Boot项目搭建
3.1 基础环境说明
本文以通用思路演示,具体版本以你实际项目为准。建议准备:
- JDK 17+(LangChain4j建议17,Spring Boot 3.x也要求17及以上)
- Maven 3.8+ 或 Gradle 7.5+
- 一个支持OpenAI接口规范的大模型服务,可以是云端API,也可以是本地部署的模型,只要提供Base URL和API Key即可。
- 一个向量数据库,示例中可以使用内存模式,方便本地跑通,后续再替换为Milvus、PGVector等。
更稳妥的判断是:版本选官方最新稳定版,不推荐使用alpha或beta版本。
3.2 创建Spring Boot项目
你可以通过Spring Initializr网站创建项目,选择Spring Web和Validation依赖即可。如果你在IDE中创建,也可以手动添加依赖。
xml复制<?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.3.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>langchain4j-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>langchain4j-demo</name>
<description>LangChain4j with Spring Boot</description>
<properties>
<java.version>17</java.version>
<langchain4j.version>0.35.0</langchain4j.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
</dependencies>
</project>
上面取了0.35.0作为示例版本,实际使用时请到Maven仓库或官方GitHub确认最新版本。版本冲突是最常见的坑之一,尽量把langchain4j相关模块版本统一。
3.3 配置模型服务
在src/main/resources/application.yml中配置模型参数。这里以OpenAI兼容协议为例,实际上很多本地模型服务也支持这种配置方式。
yaml复制langchain4j:
open-ai:
chat-model:
base-url: https://api.openai.com
api-key: ${OPENAI_API_KEY}
model-name: gpt-3.5-turbo
temperature: 0.7
timeout: 60s
embedding-model:
base-url: https://api.openai.com
api-key: ${OPENAI_API_KEY}
model-name: text-embedding-3-small
注意:如果你使用本地或私有化部署的模型,比如基于vLLM或Ollama暴露的好兼容接口,可以把base-url修改为对应的地址。不要在配置中暴露真实Key,推荐用环境变量方式注入。
3.4 第一个LangChain4j程序:最简单的对话
在Spring Boot中,因为引入了starter,ChatLanguageModel已经被自动配置为Spring Bean。我们可以直接在业务类里注入使用。
java复制// 文件路径:src/main/java/com/example/demo/service/ChatService.java
package com.example.demo.service;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.structured.StructuredPrompt;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatLanguageModel chatLanguageModel;
public ChatService(ChatLanguageModel chatLanguageModel) {
this.chatLanguageModel = chatLanguageModel;
}
public String chat(String message) {
// 最简单的方式:直接发送文本
return chatLanguageModel.chat(message);
}
public String chatWithPrompt(String userMessage) {
// 推荐使用Prompt模板,避免直接拼接字符串
Prompt prompt = Prompt.of("你是公司的技术助手,请用简洁专业的风格回答问题。用户问题是:{0}", userMessage);
return chatLanguageModel.chat(prompt.text());
}
}
为了验证,再写一个Controller:
java复制// 文件路径:src/main/java/com/example/demo/controller/ChatController.java
package com.example.demo.controller;
import com.example.demo.service.ChatService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping("/chat")
public String chat(@RequestParam("message") String message) {
return chatService.chat(message);
}
}
这里我刻意没有写流式API,先跑通最简链路。启动Spring Boot应用后,访问http://localhost:8080/chat?message=你好,应该能看到模型返回的文本。如果这一步失败,说明模型连接配置有问题,后面的RAG和智能体都不需要继续。
4. 搭建RAG知识库:从文档到可检索的问答
4.1 RAG整体流程拆解
RAG落地通常要经过五个步骤:
- 读取文档。
- 将长文档切分成片段。
- 为每个片段生成向量嵌入并存入向量存储。
- 检索时把用户问题转为向量,找出最相似的片段。
- 将片段与问题组装成Prompt,交给大模型生成最终答案。
使用LangChain4j时,前四步都有现成组件。如果是做最小演示,我们甚至可以用内存向量存储,不需要额外安装数据库。
4.2 文档加载与切分
对于企业内部知识库,文档格式很杂,可能是Markdown、TXT,也可能是PDF。LangChain4j提供了DocumentParser和DocumentSplitter来处理这些格式。下面以TXT和Markdown为例:
java复制// 文件路径:src/main/java/com/example/demo/rag/DocumentLoader.java
package com.example.demo.rag;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.RecursiveCharacterDocumentSplitter;
import dev.langchain4j.data.document.DocumentSplitter;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
public class DocumentLoader {
public static void main(String[] args) throws URISyntaxException {
// 读取类路径下的一个TXT文档
URL url = DocumentLoader.class.getResource("/knowledge.txt");
Path path = Paths.get(url.toURI());
Document document = Document.load(path, new TextDocumentParser());
// 切分文档,每段500字,重叠100字,避免语义被截断
DocumentSplitter splitter = new RecursiveCharacterDocumentSplitter(500, 100);
var segments = splitter.split(document);
System.out.println("切分为 " + segments.size() + " 段");
}
}
这里真正容易踩坑的是切分参数。太小的片段会丢失上下文,太大的片段会超出模型输入限制,同时增加向量计算的噪声。一般项目里,我建议先按产品实际情况调:技术文档用300到500字,制度文档可以用更长的段落。
4.3 嵌入与向量存储
LangChain4j的EmbeddingModel负责把文本变成向量,EmbeddingStore负责存储向量并执行相似度搜索。我们选择简单的内存实现,并写入一段测试内容。
java复制// 文件路径:src/main/java/com/example/demo/rag/EmbeddingIndexer.java
package com.example.demo.rag;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitter;
import dev.langchain4j.data.document.splitter.RecursiveCharacterDocumentSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class EmbeddingIndexer {
public static void main(String[] args) throws URISyntaxException {
// 配置:InMemoryEmbeddingStore 可以配合 EmbeddingStore 接口使用
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
URL url = EmbeddingIndexer.class.getResource("/knowledge.txt");
Path path = Paths.get(url.toURI());
Document document = Document.load(path, new TextDocumentParser());
DocumentSplitter splitter = new RecursiveCharacterDocumentSplitter(500, 100);
List<TextSegment> segments = splitter.split(new Document(document.text(), document.metadata()));
// EmbeddingModel 需要从Spring容器中获取,这里作为示意
// 实际项目中,直接注入 EmbeddingModel 和 EmbeddingStore
EmbeddingModel embeddingModel = null; // 注入点
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
embeddingStore.addAll(embeddings, segments);
System.out.println("已索引 " + segments.size() + " 段文本");
}
}
注意:上面的代码不是可直接运行的完整示例,因为EmbeddingModel需要从Spring容器中注入。在Spring Boot项目中,我们会把EmbeddingStore声明为Bean,这样所有地方都能复用。
4.4 使用ContentRetriever实现检索问答
LangChain4j提供了ContentRetriever接口,用于在AiServices中自动执行检索。我们真正需要做的是定义一个“业务助手接口”,然后让LangChain4j自动实现。
java复制// 文件路径:src/main/java/com/example/demo/rag/Assistant.java
package com.example.demo.rag;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.Result;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface Assistant {
@SystemMessage("你是一个企业内部知识助手,只能基于提供的文档内容回答。如果文档中没有相关信息,请明确说明不知道。")
Result<String> answer(@UserMessage String userQuestion);
}
接口定义好之后,我们通过AiServices构建一个实例,并传入ContentRetriever。在Spring Boot中,官方推荐直接使用@AiService注解,让Spring自动管理接口代理。这里演示手动构建的方式,以便看清楚依赖:
java复制// 文件路径:src/main/java/com/example/demo/rag/AssistantFactory.java
package com.example.demo.rag;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreContentRetriever;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AssistantFactory {
private final ChatLanguageModel chatLanguageModel;
private final EmbeddingStore<TextSegment> embeddingStore;
public AssistantFactory(ChatLanguageModel chatLanguageModel,
EmbeddingStore<TextSegment> embeddingStore) {
this.chatLanguageModel = chatLanguageModel;
this.embeddingStore = embeddingStore;
}
@Bean
public Assistant assistant() {
// 构建检索器
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(chatLanguageModel.embeddingModel()) // 示意,需要单独注入
.maxResults(3)
.minScore(0.7)
.build();
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.contentRetriever(retriever)
.build();
}
}
上面的代码有一个细节:EmbeddingStoreContentRetriever里需要传入与文档向量化时一致的EmbeddingModel。在项目里,你应该单独注入EmbeddingModel Bean,保证所有模块使用的是同一个模型。
如果使用langchain4j-spring-boot-starter,可以更简化:直接在Assistant接口上加上@AiService,Spring会为你自动装配ChatLanguageModel和ContentRetriever。你可以通过application.yml配置内容检索器的参数。这里由于篇幅不再展开,但官方文档有明确说明。
4.5 RAG知识库与结构化知识库混合使用
前面我们介绍了RAG知识库和结构化知识库,实际落地时,不要把全部数据都塞进向量库。更好的架构是:先判断用户请求属于“语义查询”还是“精确查询”。精确查询走SQL或API,语义查询走RAG。
比如用户问“报销流程是什么”,走RAG知识库;用户问“我的报销单到哪一步了”,走业务API。LangChain4j的智能体机制可以把这两者统一起来:定义一个@Tool方法去查结构化数据,同时保留ContentRetriever做文档检索。这样模型就能自己决定是用文档回答,还是调用工具获取数据。
5. 智能体实战:让模型调用Spring Bean方法
5.1 为什么需要工具调用
RAG解决了“知识从哪来”的问题,但很多业务场景还需要“动作”。例如:
- 用户问“帮我查下订单状态”,模型无法直接知道订单数据,需要调用订单服务。
- 用户问“明天下午三点提醒我开会”,模型需要把信息写入日程表。
- 用户问“给我写一封请假邮件”,模型需要获取当前用户名、日期,甚至调用邮件接口。
这类场景,单纯靠Prompt是不能稳定完成的。LangChain4j通过“工具调用”机制,让模型在对话过程中自动选择合适的Java方法执行,并把执行结果返回给模型继续推理。
5.2 用@Tool注解暴露业务方法
在Spring Boot中,只需要在方法上添加@Tool注解,方法参数自动作为模型的输入描述。下面演示一个查询订单状态的工具:
java复制// 文件路径:src/main/java/com/example/demo/agent/OrderTool.java
package com.example.demo.agent;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class OrderTool {
// 模拟订单数据
private static final Map<String, String> ORDERS = new ConcurrentHashMap<>();
static {
ORDERS.put("A1001", "已发货,预计明天送达");
ORDERS.put("A1002", "已支付,正在打包");
}
@Tool("根据订单号查询订单状态")
public String getOrderStatus(String orderId) {
String status = ORDERS.get(orderId);
if (status == null) {
return "未找到该订单,请检查订单号";
}
return status;
}
}
关键点在于方法的描述和参数。描述要说明“什么时候使用”和“参数含义”,参数最好使用原始类型或简单对象,这样模型更容易生成正确的参数。如果使用复杂对象,LangChain4j也会通过JSON反序列化支持,但描述要更细致。
5.3 通过AiServices构建智能体
下面定义一个支持工具调用的服务接口。注意和RAG中的Assistant接口类似,但这次不配置ContentRetriever,而是提供工具列表。
java复制// 文件路径:src/main/java/com/example/demo/agent/AgentAssistant.java
package com.example.demo.agent;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface AgentAssistant {
@SystemMessage("你是电商客服助手,可以使用订单查询工具。回答时保持礼貌和专业。")
String chat(@UserMessage String userMessage);
}
然后通过Spring配置注入OrderTool:
java复制// 文件路径:src/main/java/com/example/demo/agent/AgentConfig.java
package com.example.demo.agent;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolProvider;
import dev.langchain4j.service.tool.DefaultToolProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AgentConfig {
private final ChatLanguageModel chatLanguageModel;
private final OrderTool orderTool;
public AgentConfig(ChatLanguageModel chatLanguageModel, OrderTool orderTool) {
this.chatLanguageModel = chatLanguageModel;
this.orderTool = orderTool;
}
@Bean
public AgentAssistant agentAssistant() {
// 方式一:直接传入工具对象数组
return AiServices.builder(AgentAssistant.class)
.chatLanguageModel(chatLanguageModel)
.tools(orderTool)
.build();
// 方式二:如果希望动态决定工具,可以使用ToolProvider
// .toolProvider(new DefaultToolProvider())
}
}
如果你使用Spring Boot starter,并且接口标注了@AiService,LangChain4j会自动扫描当前Spring上下文中的@Tool Bean。推荐先手动构建,这样更容易理解依赖关系。
5.4 增加对话记忆
没有记忆的智能体就像失忆的人,每轮对话都是首次见面。LangChain4j的AiServices支持把ChatMemory传给服务对象,可以按用户维度维护多轮上下文。
java复制// 文件路径:src/main/java/com/example/demo/agent/AgentService.java
package com.example.demo.agent;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.memory.chat. TokenWindowChatMemory;
import org.springframework.stereotype.Service;
@Service
public class AgentService {
private final ChatLanguageModel chatLanguageModel;
private final OrderTool orderTool;
public AgentService(ChatLanguageModel chatLanguageModel, OrderTool orderTool) {
this.chatLanguageModel = chatLanguageModel;
this.orderTool = orderTool;
}
// 实际项目中,每个用户维护自己的ChatMemory,不能共享
public AgentAssistant createAssistant(String userSessionId) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.maxMessages(20) // 保留最近20条消息
.build();
return AiServices.builder(AgentAssistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemory(chatMemory)
.tools(orderTool)
.build();
}
}
这里一个常见错误是把AgentAssistant做成单例,然后所有用户共享同一个ChatMemory。如果这样做,用户A的消息会被用户B看到。正确的做法是:按会话ID维护独立的ChatMemory,或者在Assistant接口方法中传入@MemoryId参数让LangChain4j自动管理。
5.5 平台搭建智能体与代码自建智能体的区别
最近很多人讨论“用Dify、Coze等平台搭智能体”和“用Python/Java手搭智能体”有什么区别。从技术角度看,平台型智能体适合业务人员快速验证,通过界面拖拽完成Prompt编排和工具配置;代码自建则适合深度定制、内部系统集成和精细化控制。
LangChain4j属于后者。它能让你在Java工程内部直接使用模型能力,而不是把智能体放到外部平台后,再通过API与内部系统交互。选择哪种方式,取决于团队对控制力、维护成本、迭代速度的权衡。如果核心流程简单且变化频繁,平台更高效;如果智能体要深度集成复杂业务逻辑,代码自建更可持续。
6. Spring Boot工程化落地:流式输出、监控与配置管理
6.1 流式输出
真实业务场景中,用户等待大模型逐字生成往往不可接受。LangChain4j为ChatLanguageModel提供了stream方法,可以把回答以TokenStream形式输出到SSE或者WebSocket。
java复制// 文件路径:src/main/java/com/example/demo/service/StreamChatService.java
package com.example.demo.service;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class StreamChatService {
private final StreamingChatLanguageModel streamingChatLanguageModel;
private final ExecutorService executorService = Executors.newFixedThreadPool(4);
public StreamChatService(StreamingChatLanguageModel streamingChatLanguageModel) {
this.streamingChatLanguageModel = streamingChatLanguageModel;
}
public SseEmitter chatWithStream(String userMessage) {
SseEmitter emitter = new SseEmitter(60_000L);
executorService.execute(() -> {
// 发起流式请求
streamingChatLanguageModel.chat(ChatRequest.builder()
.userMessage(userMessage)
.build(), new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
try {
emitter.send(SseEmitter.event().data(partialResponse));
} catch (IOException e) {
emitter.completeWithError(e);
}
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
emitter.complete();
}
@Override
public void onError(Throwable error) {
emitter.completeWithError(error);
}
});
});
return emitter;
}
}
这段代码演示了SSE的基本接入,实际上LangChain4j对StreamingChatLanguageModel的返回类型做了多版本适配。使用前建议查看你依赖版本对应的接口方法签名。
6.2 配置管理
模型配置、检索参数、Prompt模板不应该散落在代码里。推荐的做法是:
- 使用
application.yml管理模型Base URL、Key、温度、超时时间。 - 使用
@ConfigurationProperties把RAG参数封装成可配置对象。 - Prompt模板用独立的
prompt.properties或prompt/*.st文件维护,避免在Java代码中拼接长文本。 - API Key一定要走环境变量或配置中心,不要提交到Git仓库。
6.3 日志与监控
引入LangChain4j后,你最少要关注四类日志:
- 模型请求日志:输入、输出、Token消耗、耗时。
- 检索日志:用户问题、命中片段、分数。
- 工具调用日志:模型调用了哪个方法、参数是什么、结果如何。
- 错误日志:超时、限流、JSON解析失败。
LangChain4j支持langchain4j日志模块,你可以在logback.xml中配置dev.langchain4j包的DEBUG级别,观察内部运行过程。尤其建议在开发阶段开启DEBUG,能直观看到Prompt构造和检索结果。
6.4 安全边界
在企业内部落地时,权限控制尤其重要。智能体能够调用工具,意味着模型拥有了执行动作的入口。必须做好以下边界:
- 工具方法内部必须校验当前用户权限,不能只看模型传的参数。
- 对工具方法进行输入校验,限制危险操作,比如删除、批量修改。
- 设置模型调用频率和最大Token数,防止资源浪费。
- 敏感信息不要拼入Prompt,比如数据库密码、内部API Token。
- 如果涉及生产数据库操作,必须走独立账号,并授予最小权限。
7. 常见问题与排查思路
下面汇总我实际见过的高频问题,不一定每个都来源于本文代码,但排查思路很有参考价值。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动失败:找不到ChatLanguageModel Bean | 没有引入starter,或模型配置缺少Base URL/API Key | 查看启动日志,搜索langchain4j相关配置 |
引入langchain4j-spring-boot-starter并正确配置模型参数 |
| 调用模型超时 | 网络延迟或模型服务负载高 | 查看应用日志中的超时异常,检查模型服务状态 | 增大timeout配置;如果使用云端API,检查服务可用性 |
| RAG检索结果为空 | EmbeddingModel和EmbeddingStore不一致;文档没有正确切分 | 打印检索结果,检查向量维度一致性和文本切分数量 | 保证同一项目只使用一个EmbeddingModel;调整切分参数 |
| 模型无法调用@Tool方法 | 工具对象没有注册到AiServices;参数描述不够清晰 | 开启DEBUG日志,观察模型是否生成Function Call | 重新配置tools(orderTool);优化工具描述 |
| 多用户数据串场 | ChatMemory被所有用户共享 |
检查Assistant实例是否单例 |
按@MemoryId隔离,不要共用同一个Agent实例 |
| 向量长度超限 | 文档片段过长 | 查看报错信息和切分片段长度 | 减小DocumentSplitter的chunkSize |
| 依赖版本冲突 | langchain4j不同模块版本不一致 | 执行mvn dependency:tree |
统一所有langchain4j相关依赖版本 |
| 响应不是流式 | 用了同步chat方法或SSE事件格式不对 |
确认Controller是否正确返回SseEmitter |
使用StreamingChatLanguageModel并正确推送事件 |
这些问题的核心规律是:先看日志,再缩小范围。把AI应用拆成“模型服务、检索链路、工具调用”三部分,逐段验证,往往能快速定位。
8. 最佳实践与工程建议
8.1 渐进式引入LangChain4j
不要试图在一周内把所有业务都做成智能体。建议分三步走:
- 先用
ChatLanguageModel替换零散的HTTP调用,让团队熟悉框架API。 - 选择一个非核心知识库做RAG,验证检索质量。
- 业务稳定后,再引入
@Tool,让模型调用内部接口。
每一步都要有可量化的验收标准。比如回答准确率、检索命中率、工具调用成功率。
8.2 RAG多路召回与重排序
如果知识库文档很多,单一向量检索难以保证质量。更稳妥的做法是“多路召回 + 重排序”:
- 向量语义检索召回Top 50。
- 关键词BM25检索召回Top 50。
- 合并去重后用Rerank模型重新打分,只取Top 5。
LangChain4j支持通过ContentRetriever的自定义接口组合多个检索器。从工程视角看,这比调大maxResults更有效。
8.3 Prompt版本管理
Prompt不是一次写好的,需要频繁优化。建议:
- 每条Prompt都有版本号。
- 把Prompt模板放到配置文件或数据库。
- 记录每次修改对回答质量的影响。
如果团队有条件,可以引入简单的评测集,用固定问题列表跑回归,避免“改了A问题又坏了B问题”。
8.4 处理RAG的“上下文噪音”
检索结果并不是越多越好。当命中片段包含大量无关信息时,模型容易被带偏。实践中可以:
- 设置合理的
minScore,过滤低分片段。 - 在Prompt中明确“如果片段与问题无关,请忽略,并说明不知道”。
- 结合元数据过滤,比如按部门、时间、文档类型缩小检索范围。
8.5 关于模型选型
不要只盯着最大的模型。Java后端项目最关心的是稳定性和成本。我建议:
- 简单问答、信息抽取用中等规模模型。
- 复杂推理、工具调用用更强模型。
- 嵌入模型和生成模型不需要来自同一供应商,但要保证输出维度与向量库匹配。
9. 总结与后续学习方向
这篇文章从Java开发者的实际痛点出发,介绍了LangChain4j在Spring Boot中的应用。我们理解了RAG和智能体的核心概念,掌握了文档加载、切分、嵌入、检索的完整链路,也看到了如何通过@Tool注解把普通Java方法变成智能体能力。
如果你正打算在Spring Boot中引入大模型,我建议先用最小的对话示例跑通链路,再逐步加入RAG和工具调用。不要一开始就追求复杂的架构。真正值得花时间研究的不是框架API,而是检索质量、Prompt设计和业务边界的工程化。
接下来你可以继续深入的方向包括:LangChain4j的OutputParser、不同类型向量数据库的集成、流式响应在WebFlux中的优化、评测体系搭建,以及如何把多智能体协作融入现有系统。结合团队业务,挑一个切面做深度验证,会比浏览再多资料都有用。
希望这篇文章能让你少踩几个坑。最好的实践方式,就是打开IDE,创建一个Spring Boot工程,从最简单的ChatLanguageModel开始跑一次。
