当前位置 : 首页 » 文章分类 :  开发  »  Spring-AI

Spring-AI

Spring AI 使用笔记


Spring-AI 对接 AI 大模型

Spring AI / Overview
https://docs.spring.io/spring-ai/reference/index.html

Spring-AI + WebSocket 对接 ChatGPT 流式 API

Spring AI / AI Models / Chat Models / OpenAI
https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html

1、引入 spring-ai-openai-spring-boot-starter 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

2、配置api key

spring:
  ai:
    openai:
      api-key: sk-xxxx

3、websocket service 中,调用 OpenAiChatModel.stream() 流式api,返回 Flux<ChatResponse>,subscribe 订阅每条响应结果,调用 jakarta.websocket.Session.getBasicRemote().sendText() 发送每条结果到 WebSocket session

@OnMessage
public void onMessage(Session session, String message) {
    log.info("Received sessionId={} message={}", session.getId(), message);
    // WebSocket是由底层的Servlet容器(如Tomcat)直接创建的,而不是由Spring创建的,所以不能直接使用@Autowired注入Spring管理的Bean,必须SpringUtil.getBean()获取
    SpringUtil.getBean(OpenAiChatModel.class)
                .stream(new Prompt(webSocketChat.getMsg()))
                .subscribe(chatResponse -> {
                            try {
                                session.getBasicRemote().sendText(JsonMappers.Normal.toJson(chatResponse.getResults()));
                            } catch (IOException e) {
                                log.error("发送消息出错:{}", e.getMessage(), e);
                            }
                        }
                );
}

4、前端 js 中解析每条内容,拿出其中的文本,拼接到对话框文本中即可。


Spring AI + WebSocket 对接百度云千帆模型

Spring AI AI Models Chat Models QianFan
https://docs.spring.io/spring-ai/reference/api/chat/qianfan-chat.html

1、除了 spring-ai-openai-spring-boot-starter,还需要额外引入 spring-ai-qianfan 依赖,spring-ai-openai-spring-boot-starter 内置了 spring-ai-openai,但没有其他公司的依赖。

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

<!-- spring-ai-百度千帆 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-qianfan</artifactId>
    <version>1.0.0-M6</version>
</dependency>

2、配置 ak、sk 和 模型

spring:
  ai:
    qianfan:
      api-key: xxxxx
      secret-key: xxxxx
      chat:
        options:
          model: ernie-speed-128k

注意: ak 和 sk 需要使用应用接入 https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application/v1 这个页面的
和 Access Key 页面的 ak、sk 还不一样 https://console.bce.baidu.com/iam/#/iam/accesslist

此外,我使用的 1.0.0-M6 版本,目前只支持通过 ak、sk 使用 v1 版的千帆api,即 https://aip.baidubce.com/rpc/2.0/ai_custom 开头的api。
最新的千帆已经推出了完全兼容 OpenAI 标准的 v2 版api,但目前 spring-ai-qianfan 还不支持。

3、代码对接流程和 OpenAI 一样,Spring-AI 封装了底层的差异,把模型换成 QianFanChatModel 即可。


Spring-AI 不同版本 ChatResponse 结构不一样

注意下,对接过程中发现 Spring AI(spring-ai-openai-spring-boot-starter) 不同版本的 ChatResponse 结构不一样,注意 debug 下,判断如何读取文本内容,如何判断流式回答结束:

Spring AI(spring-ai-openai-spring-boot-starter) 0.8.0 版本 的 AI模型 流式回答 ChatResponse:

首字符: [{"output":{"content":"","properties":{"role":"ASSISTANT"},"messageType":"ASSISTANT"},"metadata":{"contentFilterMetadata":null,"finishReason":null}}]
中间:  [{"output":{"content":"问题","properties":{"role":"ASSISTANT"},"messageType":"ASSISTANT"},"metadata":{"contentFilterMetadata":null,"finishReason":null}}]
结束:  [{"output":{"content":null,"properties":{"role":"ASSISTANT"},"messageType":"ASSISTANT"},"metadata":{"contentFilterMetadata":null,"finishReason":"STOP"}}]

文本内容在 output.content 中
可通过 metadata.finishReason == ‘STOP’ 判断结束

Spring AI(spring-ai-openai-spring-boot-starter) 1.0.0-M6 版本 的 AI模型 流式回答 ChatResponse:

首先会把问题给返回: {"systemInfo":false,"sessionId":"1","nickname":"3f477fcb-0c11-4f33-a0ad-cc368201c473","sessionCount":2,"msg":"你能做什么呢?"}
首字符: [{"metadata":{"finishReason":null,"contentFilters":[],"empty":true},"output":{"messageType":"ASSISTANT","metadata":{"messageType":"ASSISTANT","role":"assistant","id":"as-fyfz1i2kd1"},"toolCalls":[],"media":[],"text":"我可以做"}}]
中间:  [{"metadata":{"finishReason":null,"contentFilters":[],"empty":true},"output":{"messageType":"ASSISTANT","metadata":{"messageType":"ASSISTANT","role":"assistant","id":"as-fyfz1i2kd1"},"toolCalls":[],"media":[],"text":"、科学、文化、娱乐、体育等各种主题的问题,并提供详细的答案和"}}]
结束:  [{"metadata":{"finishReason":null,"contentFilters":[],"empty":true},"output":{"messageType":"ASSISTANT","metadata":{"messageType":"ASSISTANT","role":"assistant","id":"as-fyfz1i2kd1"},"toolCalls":[],"media":[],"text":""}}]

文本内容在 output.text 中
需通过 output.text 为空字符串判断结束


langchain4j

LangChain for Java
https://github.com/langchain4j/langchain4j


前端流式输出(打字机效果)

1、一开始直接将每次返回的 choice.delta.content 内容 append 到 div 末尾,可以实现打字机流式输出,但没有 Markdown 渲染。
2、后来使用一个字符串变量累加每次回答的结果,每收到服务端发来的一次数据,都累加后用 marked 进行 Markdown 渲染并替换 div 内容,可实现打字机流式输出 + 实时 Markdown 渲染及代码高亮,但代码高亮效果不太好。

<script type="text/javascript">
  var websocket;
  var chatGptStreaming = false; // true: ChatGpt 流式回答中
  var chatGptFullResp = ""; // ChatGpt 累积完整回答

  //markdown解析,代码高亮设置
  marked.setOptions({
      highlight: function (code, language) {
          // const hljs = require('highlight.js');
          const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
          return hljs.highlight(code, { language: validLanguage }).value;
      },
  });

  if ( 'WebSocket' in window) {
    // 实例化WebSocket对象,指定要连接的服务器地址与端口建立连接
    websocket = new WebSocket("ws://localhost:8001/ws/chat");
    // 连接打开事件
    websocket.onopen = function() {
        console.log("Socket 已打开");
    };

    // 收到消息事件
    websocket.onmessage = function(msg) {
        console.log("收到消息:" + msg.data);
        // ChatGpt 流式回答结束
        if (msg.data === "[DONE]") {
            console.log("ChatGpt 流式回答结束");
            chatGptStreaming = false;
            chatGptFullResp = "";
            return;
        }
        var time = new Date().toLocaleString();
        var resp = JSON.parse(msg.data);
        $("#div-status").css("background-color", "rgb(82, 196, 26)");

        // ChatGpt 流式回答
        if (resp.object == "chat.completion.chunk") {
          if (!chatGptStreaming) {
            chatGptStreaming = true;
          }
          choices = JSON.parse(event.data).choices;
          choices.filter(choice => choice.delta.content).forEach(choice => {
              if (choice.delta.content.indexOf("\n") >= 0) {
                  choice.delta.content = choice.delta.content.replace("\n", "<br>");
              }
              console.log(chatGptFullResp);
              chatGptFullResp += choice.delta.content;
              // $(`#${resp.id}`).append(choice.delta.content); // 未加 Markdown 渲染的版本,每次往 div 末尾 append 添加返回的流式内容
              // 添加 Markdown 渲染的版本,用全局变量累加记录本地的完整回答,每次渲染后替换 div 内容
              $(`#${resp.id}`).html(marked.parse(chatGptFullResp)); 
          });
        } 
        // 始终显示滚动条最底部
        $("#div-msg").scrollTop($("#div-msg").prop("scrollHeight"));
    };
  } else {
      console.log("当前浏览器不支持WebSocket");
  }
</script>

扒一扒 Chatgpt 背后的 web 开发技术(二)
https://zhaozhiming.github.io/2023/04/18/chatgpt-technical-part-two/

使用marked和highlight.js对GPT接口返回的代码块渲染,高亮显示,支持复制,和选择不同的高亮样式
https://juejin.cn/post/7255557296951738429


下一篇 Python-pytest

阅读
评论
1.8k
阅读预计9分钟
创建日期 2025-03-23
修改日期 2025-03-23
类别

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论