通过Java搭建AI应用项目有两种方式,一种是通过LangChain4j,一种是通过SpringAI,SpringAI必须基于java17+ SpringBoot3.2以上,通过SpringAI可以不必太关注底层,快速搭建ai应用。

-
快速搭建AI聊天机器人:
首先画个前端界面,可以通过ai来构建,把自己想咋搭建写给ai让它生成个提示词再构建效果最好,然后开始搭建后端,后端可以通过本地部署或者调用第三方大模型,本次实例两种方式都有:
本地部署方法: 本次测试采用ollama部署 可参照Ollama 官方文档 下载完之后 执行

然后可以开始搭建AI应用项目了,首先引入依赖 注意: 这个spring-ai 官方更新的很快,有些api会弃用,记得看下官方文档,本次搭建的是1.1.3版本的
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
springAi 可在yaml文件里面配置 调用大模型的端口地址一系列东西
yaml
spring:
application:
name: my-aiProject
ai:
vectorstore:
redis:
enabled: false # 禁用自动配置,使用自定义 Bean
ollama:
base-url: http://localhost:11434
chat:
model: qwen3.5:4b
然后配置 对应的ChatModel 会话记忆以及一些系统默认提示词,这边暂时采用的是内存存储会话记忆,日后可以优化成pgsql redis es之类的去存储会话记忆


然后就可以通过链式编程编写接口
less
@RequestMapping(value = "/chat",produces = "text/html;charset=UTF-8")
public Flux<String> chat(@RequestParam("prompt") String prompt,
@RequestParam("chatId") String chatId,
@RequestParam(value = "files",required = false) List<MultipartFile> files){
chatHistoryRepository.save("chat",chatId);
// 纯文本聊天
if (files==null || files.isEmpty()){
return chatClient.prompt()
.advisors(s->s.param(ChatMemory.CONVERSATION_ID,chatId))
.user(prompt)
.stream()
.content();
}else {
return multiModalChat(prompt,chatId,files);
}
}
测试结果 以及会话记忆测试结果

RAG+FUNCTION_CALLING
RAG:检索增强生成目前大模型可能存在知识是有截止日期,不懂企业内部业务,不懂私有数据,所以要靠 RAG 先从本地知识库/文档/数据库中进行查询,然后再把资料塞给大模型,让大模型基于真实数据回答
实现RAG+FUNCTION_CALLING方式如下: 首先定义一个 // 这其中的提示词要注意防止提示词注入 然后rag+function_calling 主要通过defaultTools(courseTools)实现
scss
@Bean
public ChatClient customerServiceChatClient(OpenAiChatModel openAiChatModel,CourseTools courseTools) {
return ChatClient.builder(openAiChatModel)
.defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor(), MessageChatMemoryAdvisor.builder(chatMemory()).build())
.defaultTools(courseTools)
.build();
}
这个defaultTools 里面需要传入一个Object 类型的 自定义的Tools Spring给我们提供了很多注解去自定义Tools
java
@Component
@RequiredArgsConstructor
public class CourseTools {
// spring会自动把这些注解的东西打包发给大模型
private final ICourseService courseService;
private final ISchoolService schoolService;
private final ICourseReservationService courseReservationService;
@Tool(description = "根据条件查询课程")
public List<Course> queryCourse(@ToolParam(description = "查询的条件") CourseQuery courseQuery){
if (courseQuery==null){
return courseService.list();
}
QueryChainWrapper<Course> wrapper = courseService.query()
.eq(courseQuery.getType() != null, "type", courseQuery.getType())
.le(courseQuery.getEdu() != null, "edu", courseQuery.getEdu());
if (courseQuery.getSorts()!=null){
courseQuery.getSorts().forEach(sort->{
wrapper.orderBy(true,sort.getAsc(),sort.getField());
});
}
return wrapper.list();
}
@Tool(description = "查询校区")
public List<School> querySchool(){
return schoolService.list();
}
@Tool(description = "创建预约单,返回预约单号")
public Integer createCourseReservation(@ToolParam(description = "课程id") String course,
@ToolParam(description = "校区id") String school,
@ToolParam(description = "预约人姓名") String studentName,
@ToolParam(description = "预约人电话") String contactInfo,
@ToolParam(description = "备注") String remark){
CourseReservation courseReservation = new CourseReservation()
.setCourse(course)
.setSchool(school)
.setStudentName(studentName)
.setContactInfo(contactInfo)
.setRemark(remark);
courseReservationService.save(
courseReservation);
return courseReservation.getId();
}}
}
SpringAI 给我们提供了 @Tool注解 这个@Tool 就是给这个方法标记一下这是一个AI可调用工具,然后这个@ToolParam 是告诉AI每个参数是什么,
通过这种方式,可以让AI集成检索增强,可以搜索本地数据库里面的数据
AI 分析文本,PDF,图片等内容
首先要准备 一个向量数据库可以参照SpringAI官方文档,比如PostgreSQL + pgvector向量库 或者es 或者redis-stack之类的,本项目采用redis-stack
java
@Bean
public RedisVectorStore redisVectorStore(OpenAiEmbeddingModel embeddingModel) {
// 创建 JedisPooled 连接
JedisPooled jedisPooled = new JedisPooled("localhost", 32768);
return RedisVectorStore.builder(jedisPooled, embeddingModel)
.indexName("spring-ai-index")
.prefix("spring-ai-")
.metadataFields(
RedisVectorStore.MetadataField.tag("file_name") // 👈 必须用 TAG 类型,才能用 == 精确匹配
)
.initializeSchema(true)
.build();
}
typescript
private void writeToVectorStore(Resource resource, String chatId) {
// 1.创建PDF的读取器
PagePdfDocumentReader reader = new PagePdfDocumentReader(
resource, // 文件源
PdfDocumentReaderConfig.builder()
.withPageExtractedTextFormatter(ExtractedTextFormatter.defaults())
.withPagesPerDocument(1) // 每1页PDF作为一个Document
.build()
);
// 2.读取PDF文档,拆分为Document
List<Document> documents = reader.read();
// 修复 file_name:RediSearch TAG 字段中 . 是特殊字符,会把值拆开导致精确匹配失败
// 解决方案:写入时将 . 替换为 _
String fileName = resource.getFilename();
String safeFileName = fileName != null ? fileName.replace(".", "_") : "unknown";
for (Document document : documents) {
document.getMetadata().put("chat_id", chatId);
document.getMetadata().put("file_name", safeFileName);
}
// 3.写入向量库
vectorStore.add(documents);
}
他这个上传文件有个bug 之前测半天弄不懂,后面发现不知道是Redis特有还是其他向量库也会有在RedisSerachTAG字段里面.是特殊字符,会导致精确匹配失效,可以把. replace成下划线
typescript
public Flux<String> chat(String prompt,String chatId ){
Resource file = fileService.getFile(chatId);
if (file == null){
return Flux.just("文件不存在");
}
// 保存会话id
chatHistoryRepository.save("pdf",chatId);
// 获取安全的文件名(将 . 替换为 _)
String fileName = file.getFilename();
String safeFileName = fileName != null ? fileName.replace(".", "_") : "unknown";
return pdfChatClient.prompt()
.advisors(s->s.param(ChatMemory.CONVERSATION_ID,chatId))
.advisors(a->a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION,"file_name == '" + safeFileName + "'"))
.user(prompt)
.stream()
.content();
}
然后pdf 解析,或者企业知识库问答
typescript
@RequestMapping(value = "/chat",produces = "text/html;charset=UTF-8")
public Flux<String> chat(String prompt,String chatId ){
Resource file = fileService.getFile(chatId);
if (file == null){
return Flux.just("文件不存在");
}
// 保存会话id
chatHistoryRepository.save("pdf",chatId);
// 获取安全的文件名(将 . 替换为 _)
String fileName = file.getFilename();
String safeFileName = fileName != null ? fileName.replace(".", "_") : "unknown";
return pdfChatClient.prompt()
.advisors(s->s.param(ChatMemory.CONVERSATION_ID,chatId))
.advisors(a->a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION,"file_name == '" + safeFileName + "'"))
.user(prompt)
.stream()
.content();
}
可以通过文件名去库里去,然后通过SpringAI 提供的QuestionAnswerAdvisor 进行向量计算来检索需要的内容
本文讲述了如何快速搭建SpringAI 但是相对LangChain4j,SpringAI对多Agent、复杂编排(如 LangChain 的 Chain/Graph)支持较弱,但是SpringAI对JAVA开发还是很友好的可以快速上手