快速搭建SpringAi项目 集成智能问答,RAG,FUINCTION_CALLING等功能

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

  1. 快速搭建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开发还是很友好的可以快速上手

相关推荐
Cho1yon2 小时前
【AI Agent 第十期:Claude Code 完全配置指南:三系统一步到位,AI编程助手轻松上手】
人工智能·ai编程
笨蛋不要掉眼泪2 小时前
Java并发编程 :深入剖析LinkedBlockingQueue
java·开发语言·网络·并发
AI闲聊的椰汁2 小时前
RAG技术深度解析:核心原理+全链路调优+主流开源框架选型
ai编程
未若君雅裁2 小时前
算法复杂度与数据结构:Java 集合篇的第一块基石
java·数据结构·算法
致Great2 小时前
Claude Code 上线 Dynamic Workflows:一句话调度 1000 个子智能体并行干活
java·linux·服务器
一个做软件开发的牛马2 小时前
Java 常用类:String不可变、新时间API与包装类陷阱
java·后端
yurenpai(27届找实习中)2 小时前
redis_点评(25.附件店铺—把数据库里的店铺按【类型分组】,批量导入Redis 的 GEO 地理位置结构)
java·redis·缓存
云烟成雨TD3 小时前
Spring AI Alibaba 1.x 系列【66】Graph 长期记忆
java·人工智能·spring
Javatutouhouduan3 小时前
Java面试大厂真题汇总!
java·java面试·java面试题·后端开发·java编程·java架构师·java八股文