SpringAI+Ollama三部曲之二:细说开发

发布于:2024-05-20 ⋅ 阅读:(90) ⋅ 点赞:(0)

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

Spring AI实战全系列链接

  1. Spring AI实战之一:快速体验(OpenAI)
  2. SpringAI+Ollama三部曲之一:极速体验
  3. SpringAI+Ollama三部曲之二:细说开发

本篇概览

  • 本文是《SpringAI+Ollama三部曲》系列的第二篇,前文咱们通过简单的操作完成了以下系统的部署,并体验了基于大模型的聊天功能,想必您已迫不及待的想知道具体的实现细节,本篇就是为爱学习的您准备的,一起来了解完整的开发过程吧
    在这里插入图片描述
  • 今天的开发过程如下图所示
    在这里插入图片描述

源码下载(觉得作者啰嗦的,直接在这里下载)

  • 如果您只想快速浏览完整源码,可以在GitHub下载代码直接运行,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议
  • 这个git项目中有多个文件夹,本篇的源码在leader-tutorials文件夹下,如下图红色箭头所示:
    在这里插入图片描述
  • tutorials目录下有多个项目,整个《SpringAI实战》系列的源码在springai-tutorials,这是个maven工程,里面有多个子工程,今天的实战就是子工程ollama-chat

Java开发(新建工程)

  • 《Spring AI实战之一:快速体验(OpenAI)》一文中创建了一个名为springai-tutorials的maven父工程,用来管理所有SpringAI有关的源码,今天要写的代码也放在这里面统一管理
  • 新建名为ollama-chat的maven工程,这是springai-tutorials的子工程,pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springai-tutorials</artifactId>
        <groupId>com.bolingcavalry</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>ollama-chat</artifactId>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>
		<dependency>
    		<groupId>io.projectreactor</groupId>
    		<artifactId>reactor-core</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>io.projectreactor</groupId>
			<artifactId>reactor-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

    <build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>
  • 从上述pom.xml可见,对接ollama的关键是spring-ai-ollama-spring-boot-starter库的使用,另外为了达到不间断输出内容到前端的效果,还依赖了webflux和reactor,以及渲染网页所需的thymeleaf库
  • 既然是子工程,那么就要父工程的pom.xml中配置好,这个别漏了
    在这里插入图片描述

Java开发(配置文件)

  • 前文中,咱们在部署的时候创建了application.properties文件,这是为了方便配置,您也可以选择不额外配置,直接把配置文件放在传统的src/main/resources目录中
# ollama的通信地址,如果当前应用和ollama通过docker-compose打包部署,host就可以直接写ollama的容器名
spring.ai.ollama.base-url=http://ollama:11434
# 指定大模型,这里用的是通义千问1.8b
spring.ai.ollama.chat.options.model=qwen:1.8b
# 值越小回答越严谨,值越大回答越有创造性
spring.ai.ollama.chat.options.temperature=0.7
# 响应式web服务
spring.main.web-application-type=reactive

Java编码(后端)

  • 接下来是本篇的核心:后端代码,功能是收到前端发来的问题,通过SpringAI调用ollama,将大模型的响应返回给前端
  1. 首先是启动类Application.java,平凡无奇
package com.bolingcavalry.ollamachat;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  1. 然后是RestClient的配置类,没有的话可能会启动失败
package com.bolingcavalry.ollamachat;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {
    @Bean
    public RestClient.Builder restClientBuilder() {
        return RestClient.builder();
    }
}
  1. 接下来是最核心的代码,响应前端请求的controller类,尽管重要,但是代码还是很简单而且充满了套路,依赖注入client bean,从controller入参得到前端的请求参数,调用ollama client的API就完成提问,收到的返回值不是ollama的响应,而是对流的封装bean,这里用到的就是springboot的webflux,将流对象返回给前端
package com.bolingcavalry.ollamachat.controller;

import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.ai.chat.messages.UserMessage;

import reactor.core.publisher.Flux;

@RestController
public class ChatController {

    private final OllamaChatClient chatClient;


    public ChatController(OllamaChatClient chatClient) {
        // 依赖注入ollama的客户端类
        this.chatClient = chatClient;
    }

    @GetMapping(value = "/ai/streamresp", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
	public Flux<String> streamResp(@RequestParam(value = "message", defaultValue = "Hello!") String message) throws InterruptedException {
        // 提示词类,包裹了用户发来的问题
        Prompt prompt = new Prompt(new UserMessage(message));
        // 调用ollama的客户端类的API,将问题发到ollama,并获取流对象,Ollama响应的数据就会通过这个流对象持续输出
        Flux<ChatResponse> chatResp = chatClient.stream(prompt);
        // ollama客户端返回的数据包含了多个内容,例如system,assistant等,需要做一次变换,取大模型的回答内容返回给前端
        return chatResp.map(chatObj -> chatObj.getResult().getOutput().getContent());
    }
}
  • 至此,后端代码就写完了,没错就只有这么点

前端代码

  • 前文的体验是在网页上进行的,这就说明今天的开发也少不了前端代码,接下来就一起开发前端代码吧
  • 实话实说,欣宸的前端水平离及格还差很远,所以这里把基本功能跑通已经算超水平发挥了,代码和效果都很差劲,希望您能海涵…
  • src/main/resources/templates目录下新建index.html文件,内容如下,其实也很简单,就是绑定按钮的事件,然后把收到的内容展示在指定div
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Reactive Flux Display</title>
    <script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
    <script th:src="@{/js/custom.js}"></script>
</head>
<body>
    <div>
        <input type="text" id="inputField" placeholder="Enter something...">
        <button id="sendButton">Submit</button>
        <div id="displayArea"></div>
    </div>
    <script>
        // 自定义的JavaScript代码将放在这里
        document.getElementById('sendButton').addEventListener('click', function() {
    const inputField = document.getElementById('inputField');
    const displayArea = document.getElementById('displayArea');
    
    const input = inputField.value;
    if (input) {
        displayArea.textContent = ""
        const url = `/ai/streamresp?message=${encodeURIComponent(input)}`;
        
        fetch(url)
            .then(response => {
                if (response.ok) return response.body.pipeThrough(new TextDecoderStream()).pipeTo(new WritableStream({
                    write(chunk) {
                        displayArea.textContent += chunk.replace(/data:/g, "").trim();
                    }
                }));
            })
            .catch(error => console.error('Error:', error));
    }
});
    </script>
</body>
</html>
  • 现在代码都写完了,这个java应用也能正常运行起来了,但是要达到前文的效果,还需要将工程制作成docker镜像,接下来就是制作过程

制作docker镜像(编写Dockerfile)

  • 要制作docker镜像就要编写制作镜像的脚本,在pom.xml文件所在目录创建名为Dockerfile的文件,内容如下
# 使用Spring Boot官方镜像作为基础镜像
FROM openjdk:17-jdk-slim

# 设置环境变量
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \
    JHIPSTER_SLEEP=0 \
    JAVA_OPTS=""

# 复制项目jar文件到Docker镜像中
COPY target/*.jar /app.jar

# 运行应用
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar --spring.config.location=file:/app/application.properties"]
  • 可见制作镜像的步骤也很简单,就是常规的复制jar文件,并且启动命令中指定了properties文件的位置

制作docker镜像(构建镜像)

  • 首先要编译java工程生成jar文件,在父工程的pom.xml所在目录执行以下命令即可完成编译构建
mvn clean compile package -U -DskipTests
  • 编译成功后,在ollama-chat/target目录下生成了ollama-chat-1.0-SNAPSHOT.jar文件
    在这里插入图片描述
  • 进入Dockerfile文件所在目录,执行以下命令即可完成docker镜像制作(注意最后有个点号,不要漏掉了),注意bolingcavalry/ollam-tutorial:0.0.1-SNAPSHOT 是我为这个镜像起的名字,bolingcavalry是我的docker镜像账号,这样写符合docker的规范,稍后可以推送到hub.docker.com的镜像仓库中,这样算是在网上公开了,大家都可以使用
docker build -t bolingcavalry/ollam-tutorial:0.0.1-SNAPSHOT .
  • docker images命令查看本地镜像,可见已经生成了新镜像
    在这里插入图片描述

上传镜像(可选)

  • 如果您希望自己的镜像保存在公共仓库,以便更多人用到,可以先用docker login命令登录后,再执行以下命令推送本地镜像,注意镜像名的前缀要换成自己的docker账号
docker push bolingcavalry/ollam-tutorial:0.0.1-SNAPSHOT

docker-compose配置

  • 最后就是docker-compose.yml了,这个在前文已经制作过一次,再来回顾一下,可见一共三个容器:ollama、webui、java应用,前两个直接使用官方镜像即可,最后一个是咱们刚才做出来的
version: '3.8'
services:
  ollama:
    image: ollama/ollama:latest
    ports:
      - 11434:11434
    volumes:
      - /home/will/data/ollama:/root/.ollama
    container_name: ollama
    pull_policy: if_not_present
    tty: true
    restart: always
    networks:
      - ollama-docker

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    pull_policy: if_not_present
    volumes:
      - /home/will/data/webui:/app/backend/data
    depends_on:
      - ollama
    ports:
      - 13000:8080
    environment:
      - 'OLLAMA_BASE_URL=http://ollama:11434'
      - 'WEBUI_SECRET_KEY=123456'
      - 'HF_ENDPOINT=https://hf-mirror.com'
    extra_hosts:
      - host.docker.internal:host-gateway
    restart: unless-stopped
    networks:
      - ollama-docker

  java-app:
    image: bolingcavalry/ollam-tutorial:0.0.1-SNAPSHOT
    volumes:
      - /home/will/temp/202405/15/application.properties:/app/application.properties
    container_name: java-app
    pull_policy: if_not_present
    depends_on:
      - ollama
    ports:
      - 18080:8080
    restart: always
    networks:
      - ollama-docker

networks:
  ollama-docker:
    external: false
  • 至此,整个开发过程算是完成了,接下来按照前文的步骤部署和启动,就是使用大模型的能力了
  • 通过本文可见,基于SpringAI的封装后,使用大模型的对话能力是非常简单的事情,当然了,ollama并非如此简单,SpringAI对ollama的能力封装也不可能只有这么一个API调用,那么接下来的文章中,咱们会继续深入,通过SpringAI了解ollama的更多能力

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

网站公告

今日签到

点亮在社区的每一天
去签到