Java不能搞大模型应用开发?SpringAI拯救你

1、前言

作为一名后端程序员且最擅长的编程语言的是Java的背景下,一直羡慕python在人工智能领域的发展,目前的大模型应用开发最知名的框架就是langChain,这个框架有多火爆那就不用多说了。虽然笔者也有一定的python语言基础,但是对于现有大量的服务端项目都是java开发的基础下,去引入python的技术栈显然是不太方便的。有句调侃的话说的好,所有Java服务端开发工程师的春天都是Spring给的,于是我把目光转到了SpringAI身上。

其实说实话SpringAI这个项目我很早就有关注,但是当时我理解的比较肤浅以为这个东西就是简化下对接大模型的复杂度,制定统一的接入规范,但是没想到过几个月一看,现在的SpringAI 已经有了完全对标LangChain的能力,下面先看官方文档的几张关键图示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从上面这张图可以看出目前SpringAI已经有了成熟的ETL技术实现,也就是说springAI有专业的套件可以实现将原始的文档经过读取、转换(数据结构化、清理数据、数据分块)、后写入向量数据库。正是由于有这一点支持所以SpringAI也就自然而然的支持RAG技术实现,可以支持让大模型基于已有的知识库回答问题。

再看下面这张图示,这张图示表明SpringAI支持了函数调用能力,也就是说借助SpringAI支持绑定一些函数,在调用大模型之前去调用函数。这个有什么用呢,比如我们注册了一个本地文件内容读取函数,我们可以直接发送这样的问题

D:\工作日志,这个目录下面有多少个文件?pdf和docx文件各自有多少个

这种问题直接使用大模型是无法解决的,但是因为有了函数注册,让这一切都成为可能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果你对本文涉及到的一些大模型相关的术语不太了解,强烈推荐阅读这篇官方文档的解释:docs.spring.io/spring-ai/r…

作为一名后端程序员真的没理由让大模型开发领域python一家独大,也许python在机器学习领域是无可争议的首选,但是在大模型开发领域Java应该也有一战之力,也希望更多的后端开发多了解SpringAI技术,持续推广使用。

本系列博客分为上下两篇,本文先介绍一下SpringAI的“对话”、“对话记忆”、“RAG技术”、“函数调用”四个方面的内容,在下篇的内容里会继续介绍更为复杂的“RAG知识图谱”、“Agent”、“多模态数据处理”三个方面的内容。

2、开发环境准备

按照本文提供的案例进行代码实现之前,你需要确保以下开发环境是满足的

  1. JDK17及以上版本,因为SpringAI只能在SpringBoot3.x以上版本使用,JDK8 只能含泪说再见了
  2. docker运行环境,需要使用Docker安装RedisStack和Neo4J(建立知识图谱使用)
  3. 一个通义千问的APIKEY,本文并不打算去使用SpringAI官方样例中使用的OpenAI大模型,而是使用在国内效果较好的通义千问,通义千问的SDK设计也遵循了OpenAI的设计规范,通义千问基于SpringAI的设计,有了单独的适配套件Spring-AI-Alibaba(github.com/alibaba/spr…)【目前这个项目星数较少,这也说明目前对使用SpringAI做企业级大模型应用开发的人还不够多】

3、SpringAI使用

下面首先需要创建一个标准的SpringBoot项目并在项目的Pom文件中引入下述关键依赖:

xml 体验AI代码助手 代码解读复制代码<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>${spring-ai.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter</artifactId>
        <version>${spring-ai-alibaba.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>

这里SpringAI的版本和SpringAIAlibaba的版本要匹配,笔者使用的版本为

xml 体验AI代码助手 代码解读复制代码<spring-ai.version>1.0.0-M3</spring-ai.version>
<spring-ai-alibaba.version>1.0.0-M3.2</spring-ai-alibaba.version>

3.1 模型对接

先需要在application.yaml文件中添加下述内容

yaml 体验AI代码助手 代码解读复制代码spring:
  application:
    name: springboot-rocket-ai-demo
  ai:
    # 阿里通义千问
    dash-scope:
      api-key: 写上你自己的ApiKey
      chat:
        options:
          model: qwen-max
      embedding:
        options:
          model: text-embedding-v2

然后我们编写一个Controller开始最基础的模型对接。

java 体验AI代码助手 代码解读复制代码@RequestMapping("/qwen")
@RestController
@AllArgsConstructor
public class QwenChatController {

    private final ChatModel chatModel;

     /**
     * 同步问答,直接输出结果
     *
     * @param prompt 用户提问
     */ 
    @GetMapping("chat")
    public String chat(@RequestParam String prompt) {
        ChatClient chatClient = ChatClient.create(chatModel);
        return chatClient.prompt()
        .user(prompt)
        .call()
        .content();
    }


    /**
     * 流式问答 (搭配前端可以实现打字机输出效果)
     *
     * @param prompt 用户提问
     * @return  流式响应
     */
    @GetMapping(value = "chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<String>> chatStream(@RequestParam String prompt) {
        return ChatClient.create(chatModel)
        .prompt()
        // 输入多条消息,消息可以分角色
        .messages(new SystemMessage("你是一个Spring专家帮助用户回答JavaSpring框架方面的问题"),
                  new UserMessage(prompt))
        // 流式返回
        .stream()
        // 构造SSE(ServerSendEvent)格式返回结果
        .chatResponse()
        .map(chatResponse -> {
            return ServerSentEvent.builder(
                JSON.toJSONString(chatResponse)
            )
            .event("message").build();
        });
    }
}

执行效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里的ChatModel 会使用SpringBoot自动装配,打断点就可以得知为通义千问的实现,同样的如果在路径和依赖里面注入诸如“千帆”“智谱”的maven依赖并进行正确的配置,则也能转为对应大模型的实现,所以SpringAI实际上也提供了一种接入规范,不然每家大模型都搞一个接入规则,对接多个大模型的复杂度就会成倍上升。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3.2 对话记忆

在上一部分的模型接入过程中,可以发现对话是没有记忆的,也就是说大模型记不得上次你问了些什么问题,实现对话记忆的关键类就是ChatMemory接口,这个接口有个最简单的内置实现InMemoryChatMemory。下面我们继续改造代码为我们与大模型的对话增加记忆能力

java 体验AI代码助手 代码解读复制代码private final ChatModel chatModel;

// 内存记忆 这种方式是不适合分布式环境的
private final ChatMemory chatMemory = new InMemoryChatMemory();

// .......

@GetMapping(value = "chat/stream/memory", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatStreamWithMemory(@RequestParam String prompt,
                                                           @RequestParam String sessionId) {
    var messageChatMemoryAdvisor = new MessageChatMemoryAdvisor(chatMemory, sessionId, 10);
    return ChatClient.create(chatModel).prompt()
            .user(prompt)
            .advisors(messageChatMemoryAdvisor)
            .stream()
            .content()
            .map(chatResponse -> ServerSentEvent.builder(chatResponse)
                    .event("message")
                    .build());
}

MessageChatMemoryAdvisor这个熟悉SpringBoot的都应该感到比较熟悉,实际上就是一种拦截请求,会在正式发起大模型请求时进行拦截,追加会话id对应的历史会话消息。

这个构造方法接收三个参数分别是chatMemory 、会话id、以及需要从会话记忆中发送几条历史消息给大模型。

java 体验AI代码助手 代码解读复制代码public MessageChatMemoryAdvisor(ChatMemory chatMemory, String defaultConversationId, int chatHistoryWindowSize) {
    this(chatMemory, defaultConversationId, chatHistoryWindowSize, Advisor.DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER);
}

显然内存记忆,在分布式的场景下就会失效,所以正常情况下应该是我们自己去实现ChatMemory,把数据存储到数据库里面,根据会话id来读取历史会话内容。需要注意是大模型都有上下文限制,如果携带过多的上下文信息其实反而会影响大模型最终的回答效果,在实际开发中可以只提取3-5条上下文,这样既能节省token还能保证大模型的回答效果。

3.3 RAG初探

首先执行这个环节时,需要安装redisStack作为向量数据库,可以参考官方文档:docs.spring.io/spring-ai/r… 进行安装

为什么要用向量数据库呢?因为我们要想让大模型回答基于我们提供的材料的响应结果,我们需要告诉大模型背景,大模型在回答你的问题的时候它需要评估材料和问答的关联性,评估关联性,实际上就是计算两个向量的夹角,所以使用的是向量数据库来存储。

redisStack实际上是redis的一种增强版,也包含了redis的全部功能。

首先我们需要把我们需要上传的资料转到向量数据库中,使用的是Spring的ETL套件,如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

核心代码如下:可以发现这里的代码步骤基本和上图是一一对应的

java 体验AI代码助手 代码解读复制代码@PostMapping("embedding")
public String embedding(@RequestParam MultipartFile file) {
    // 从IO流中读取文件
    TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(new InputStreamResource(file.getInputStream()));
    List<Document> documentList = tikaDocumentReader.read();
    // 将文本内容划分成更小的块
    List<Document> splitDocuments = new TokenTextSplitter().apply(documentList);
    // 存入向量数据库,这个过程会自动调用embeddingModel,将文本变成向量再存入。
    redisVectorStore.add(splitDocuments);
    return "success";
}

启动之后RedisStack会有一个管理地址(默认端口号8001),我们可以通过管理地址查看文档有没有正确的存储到向量数据库中。我这里实际上传了我个人的一份文档,,可以发现文档被切割成了三个部分,存储到了向量数据库中

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

存储到了向量数据库之后,我们需要让大模型基于我们向量数据库里面的内容进行问题回答,怎么实现呢?

java 体验AI代码助手 代码解读复制代码@GetMapping(value = "chat/stream/vector", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatStreamWithVector(@RequestParam String prompt) {
    // 1. 定义提示词模板,question_answer_context会被替换成向量数据库中查询到的文档。
    String promptWithContext = """
            以下是用户提问的上下文信息
            ---------------------
            {question_answer_context}
            ---------------------
            给定的上下文和提供的历史信息,而不是事先的知识,回复用户的意见。如果答案不在上下文中,告诉用户你不能回答这个问题。
            """;
    return ChatClient.create(chatModel).prompt()
            .user(prompt)
            // 2. QuestionAnswerAdvisor会在运行时替换模板中的占位符`question_answer_context`,替换成向量数据库中查询到的文档。此时的query=用户的提问+替换完的提示词模板;
            .advisors(new QuestionAnswerAdvisor(redisVectorStore, SearchRequest.defaults(), promptWithContext))
            .stream()
            // 3. query发送给大模型得到答案
            .content()
            .map(chatResponse -> ServerSentEvent.builder(chatResponse)
                    .event("message")
                    .build());
}

可以发现和上一节我们解决对话记忆一样也是有一个Advisor来解决这个问题,这个QuestionAnswerAdvisor构造方法定义如下

java 体验AI代码助手 代码解读复制代码public QuestionAnswerAdvisor(VectorStore vectorStore, SearchRequest searchRequest, String userTextAdvise) {
    this(vectorStore, searchRequest, userTextAdvise, true);
}

可能有的读者会对这里的question_answer_context占位符有点疑问,可能会问换成其他的占位符可不可以,事实上是不可以的,简单的看下QuestionAnswerAdvisor的实现即可发现在源码org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor#before位置处,代码写死的占位符的名称。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实际演示效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以发现大模型肯定是不认识笔者的,但是由于我上传了我个人的自我介绍,大模型正确回答了问题,(这里因为设置的是流式响应所以文字是一段段输出的)

使用RAG技术构建知识图谱则比这个要更复杂,需要使用图形数据库管理系统例如Neo4j ,这一部分将在后续的文章中专门讲解。

3.4 函数调用

考虑一种场景我需要让大模型,读取我本地目录的一个文件,然后基于这个文件回答问题,显然大模型不具备读取本地文件的能力,除了我们上面说的把文档通过ETL技术放到向量数据库的方式以外有没有其他办法解决这个问题呢?实际方案是有的使用的也就是SpringAI的FunctionCall(docs.spring.io/spring-ai/r…

首先我们定义一个函数:

java 体验AI代码助手 代码解读复制代码@Description("文档解析函数")
@Component
@Slf4j
public class DocumentContentAnalyzerFunction implements Function<DocumentContentAnalyzerFunction.Request,
     DocumentContentAnalyzerFunction.Response> {


    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonClassDescription("文件解析函数请求")
    public static class Request {
        @JsonProperty(required = true, value = "path")
        @JsonPropertyDescription(value = "需要解析的文档路径")
        String path;
    }

    @JsonClassDescription("根据指定路径读取出来的文件内容")
    public record Response(String result) {
    }

    @SneakyThrows
    @Override
    public Response apply(Request request) {
        // ai解析用户的提问得到path参数,使用tika读取本地文件获取内容。把读取到的内容再返回给ai作为上下文去回答用户的问题。
        TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(new FileSystemResource(request.path));
        return new Response(tikaDocumentReader.read().get(0).getContent());
    }
}

可以发现这里加了很多的注解,加这些注解的内容就是为了提供一种信息告诉大模型,如何调用,什么时候调用。

github.com/alibaba/spr… SpringAI alibaba中也定义了很多函数实现,例如这里的天气服务函数。可以供参考

定义函数之后只需要在代码中写上函数在Spring容器中的Bean名称即可

java 体验AI代码助手 代码解读复制代码@GetMapping(value = "chat/function")
public String chatStreamWithFunction(@RequestParam String prompt) {
    return ChatClient.create(chatModel).prompt()
            .functions("documentContentAnalyzerFunction")
            .user(prompt)
            .call()
            .content();
}

可以看下具体的调用效果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

可以发现大模型准确回答了我本地一个文档的字符总数,这实际上是调用我们的函数来实现文档内容读取的

4、总结

通过上文的介绍相信各位读者对SpringAI目前具备的一些基本能力有了一定了解,由于篇幅有限可能涉及到的技术细节不够深入,如果对文章中的内容有疑问,可以在评论区交流。

实际上SpringAI还有一些其他功能,比如多模态数据的处理能力,模型评估,模型链路监测这部分后续可能会继续写一些相关的博客介绍其内容。

        .call()
        .content();

}

可以看下具体的调用效果

[外链图片转存中…(img-KsmnY5Y6-1747898842315)]

可以发现大模型准确回答了我本地一个文档的字符总数,这实际上是调用我们的函数来实现文档内容读取的

4、总结

通过上文的介绍相信各位读者对SpringAI目前具备的一些基本能力有了一定了解,由于篇幅有限可能涉及到的技术细节不够深入,如果对文章中的内容有疑问,可以在评论区交流。

实际上SpringAI还有一些其他功能,比如多模态数据的处理能力,模型评估,模型链路监测这部分后续可能会继续写一些相关的博客介绍其内容。

或许现在的langchain还是大模型应用开发的首选 但是例如智谱、通义千问、千帆等大模型都在陆陆续续的发布基于SpringAI框架设计下的starter,相信后面使用SpringAI开发大模型应用应该也会成为一种主流选择。

在这里插入图片描述

大模型岗位需求

大模型时代,企业对人才的需求变了,AIGC相关岗位人才难求,薪资持续走高,AI运营薪资平均值约18457元,AI工程师薪资平均值约37336元,大模型算法薪资平均值约39607元。
在这里插入图片描述

掌握大模型技术你还能拥有更多可能性

• 成为一名全栈大模型工程师,包括Prompt,LangChain,LoRA等技术开发、运营、产品等方向全栈工程;

• 能够拥有模型二次训练和微调能力,带领大家完成智能对话、文生图等热门应用;

• 薪资上浮10%-20%,覆盖更多高薪岗位,这是一个高需求、高待遇的热门方向和领域;

• 更优质的项目可以为未来创新创业提供基石。

可能大家都想学习AI大模型技术,也想通过这项技能真正达到升职加薪,就业或是副业的目的,但是不知道该如何开始学习,因为网上的资料太多太杂乱了,如果不能系统的学习就相当于是白学。为了让大家少走弯路,少碰壁,这里我直接把全套AI技术和大模型入门资料、操作变现玩法都打包整理好,希望能够真正帮助到大家。

读者福利:如果大家对大模型感兴趣,这套大模型学习资料一定对你有用

零基础入门AI大模型

今天贴心为大家准备好了一系列AI大模型资源,包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

有需要的小伙伴,可以点击下方链接免费领取【保证100%免费

点击领取 《AI大模型&人工智能&入门进阶学习资源包》*

内容包括:项目实战、面试招聘、源码解析、学习路线。

img

imgimgimgimg
如果大家想领取完整的学习路线及大模型学习资料包,可以扫下方二维码获取
在这里插入图片描述
👉2.大模型配套视频👈

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,每个章节都是当前板块的精华浓缩。(篇幅有限,仅展示部分)

img

大模型教程

👉3.大模型经典学习电子书👈

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。(篇幅有限,仅展示部分,公众号内领取)

img

电子书

👉4.大模型面试题&答案👈

截至目前大模型已经超过200个,在大模型纵横的时代,不仅大模型技术越来越卷,就连大模型相关的岗位和面试也开始越来越卷了。为了让大家更容易上车大模型算法赛道,我总结了大模型常考的面试题。(篇幅有限,仅展示部分,公众号内领取)

img

大模型面试

**因篇幅有限,仅展示部分资料,**有需要的小伙伴,可以点击下方链接免费领取【保证100%免费

点击领取 《AI大模型&人工智能&入门进阶学习资源包》

**或扫描下方二维码领取 **

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员一粟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值