SpringBoot也能玩本地大模型?快来集成Ollama试试吧

在这个 AI 日新月异的时代,AIGC(AI生成内容)已迅速席卷全球,甚至掀起了一场技术革命。然而,当我们谈论这些炫酷的大模型时,你是否思考过它们背后的秘密?是什么让这些开源模型如此强大?它们是如何被训练出来的,又如何能够在我们本地运行?更重要的是,这场技术浪潮已经涌来,我们要如何在这股洪流中找到自己的方向,不被时代所抛下?所以作者决定出一系列的文章来和大家一起探索一下AIGC的世界,专栏就叫《重生之我要学AIGC》,欢迎大家订阅!!!谢谢大家。

在这里插入图片描述

在上一篇文章中:如果我们需要在本地运行大模型,我们应该怎么做?Ollama入门指南 我们认识了Ollama,知道了这是一款部署在本地的可以运行大模型的载体。

在之前我们也在SpringBoot项目中调用过阿里的通义灵码:国外的Spring出AI了?阿里:没关系,我会出手 但是,阿里的通义是阿里部署在云端的,我们只需要填写token调用他即可,很多同学说,不够深,我想要更深入一点,更深去了解AIGC本身,而之前我们说过的Ollama就是将大模型部署在本地的一个工具,而且Ollama也提供了RestApi的方式给我们调用,那么我们自然而然的就可以使用SpringBoot去集成Ollama实现全套部署一个自己的AIGC的应用。

Ollama有提供这样的RestApi

curl http://localhost:11434/api/chat -d '{
  "model": "llama3.2",
  "messages": [
    { "role": "user", "content": "why is the sky blue?" }
  ]
}'

但是就在作者想徒手造轮子,自己写一套SpringBoot的demo出来的时候(甚至SpringBootStarter),作者发现,Spring官方居然早就做了这件事!!!

所以,我们这次直接在巨人的肩膀上造车就行啦。

第一步,引入jar包依赖

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

因为这个jar包Spring只发布了在快照仓库,所以我们要配置maven仓库,那么完整的pom文件如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.masiyi</groupId>
    <artifactId>spring-ai-ollama</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-ai-ollama</name>
    <description>spring-ai-ollama</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>23</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


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

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
</project>

第二步,使用OllamaChatModel

大家翻一翻源码就知道,在我们引入的jar包中,Spring有写了一个 OllamaAutoConfiguration类,而我们再去看一下,里面定义了一个 OllamaChatModel类,而这个类就是Spring封装好的一些调用Ollama的api的一些方法的工具类。

所以我们直接在controller里面依赖他

    @Autowired
    private OllamaChatModel ollamaChatModel;

第三步,编写测试方法

@GetMapping("/ollama")
public Object ollamaCall(@RequestParam(value = "msg") String msg){
    return ollamaChatModel.call(new Prompt(msg));

}

我们直接调用在一个controller类里面写一个最简单的方法调用他看一下

第四步,编写配置文件

我们在我们的properties配置文件中配置好我们要调用的模型:

spring.ai.ollama.chat.model=llama3.2

如果看过之前作者写的那篇Ollama文章的话就知道,这个参数的作用就是我们调用RestApi时候Body体Json内容里面指定的模型 "model": "llama3.2"

第五步,启动,测试

@RestController
@RequestMapping("/ai")
public class OllamaController {
    @Autowired
    private OllamaChatModel ollamaChatModel;


    @GetMapping("/ollama")
    public Object ollamaCall(@RequestParam(value = "msg") String msg){
        return ollamaChatModel.call(new Prompt(msg));

    }
 }

我启动之后调用一下我们本地的接口试一下

可以看到,除了时间久了一点完全没有问题

image-20241027194327636

吐字功能

上面的是一个最基础的demo,那么我们之前说的,吐字是怎么来的呢?没看过之前作者写的吐字的那篇文章的可以移步去补习一下: 在ChatGPT中,吐字那么酷炫的效果到底是怎么实现的?

没错,你能想到的OllamaChatModel也想到了,我们根据我们之前学过的吐字加上 OllamaChatModel里面封装好的方法,我们就得到了这样的一个方法:

   @PostMapping(value = "/ollama-stream", produces = "text/event-stream; charset=utf-8")
    public Flux<ServerSentEvent> ollamaStream(@RequestParam(value = "msg") String msg){
        return ollamaChatModel.stream(new Prompt(msg)).map(
                message -> ServerSentEvent.builder().id("111").event("message").data(message.getResult().getOutput().getContent()).build()
        );

    }

我们调用它看看效果怎么样

Apifox_c9hqXZQDVi

可以看到的是,完全符合我们吐字的要求!!!

上下文功能

大家要知道的是之所以gpt能够理解我们之前说过的话,核心之一的就是 上下文。那么作者之前也给大家演示了如何在RestApi中携带上下文:

image-20241027180030961

接口:

    @PostMapping("/chat")
    public String ollamaChat(@RequestBody List<AigcMessage> aigcMessages){
        ArrayList<Message> messages = new ArrayList<>();
        for (AigcMessage aigcMessage : aigcMessages) {
            if ("user".equals(aigcMessage.role())) {
                messages.add(new UserMessage(aigcMessage.content()));
            } else if ("assistant".equals(aigcMessage.role())) {
                messages.add(new AssistantMessage(aigcMessage.content()));
            }
        }
        return ollamaChatModel.call(new Prompt(messages)).getResult().getOutput().getContent();
    }

这地方我们用 AigcMessage类去包装前端发送的用户和gpt的角色和内容:

package com.masiyi.springaiollama.controller.dto;

public record AigcMessage(String role,
                          String content) {

}

这个时候我们的接口就可以这么传参了:通过assistant(gpt回答)和user的role去区分角色,然后传给大模型,这就是我们的上下文。

image-20241031194112367

既然后端已经支持了,那么前端就需要每次把我们的回答和系统的回答再发送过来就行了。

上下文吐字

加上我们之前的吐字能力和上下文的能力,我们直接把他结合起来就成这样了:

@PostMapping(value = "/chat-stream", produces = "text/event-stream; charset=utf-8")
    public Flux<ServerSentEvent> ollamaStream(@RequestBody List<AigcMessage> aigcMessages) {
        ArrayList<Message> messages = new ArrayList<>();
        for (AigcMessage aigcMessage : aigcMessages) {
            if ("user".equals(aigcMessage.role())) {
                messages.add(new UserMessage(aigcMessage.content()));
            } else if ("assistant".equals(aigcMessage.role())) {
                messages.add(new AssistantMessage(aigcMessage.content()));
            }
        }

        return ollamaChatModel.stream(new Prompt(messages)).map(
                message -> {
                    String content = message.getResult().getOutput().getContent()
                            .replace("\n", "\\n") // 保留换行符
                            .replace(" ", "&nbsp;"); // 保留空格
                    return ServerSentEvent.builder()
                            .id("222")
                            .event("message")
                            .data(content)
                            .build();
                }
        );
    }

我们来看一下效果:

image-20241031194511378

写个前端

写个前端来更直观得看到效果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Interface with Streaming & Context</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: auto;
            padding: 10px;
        }
        #chatBox {
            border: 1px solid #ccc;
            padding: 10px;
            height: 400px;
            overflow-y: scroll;
            margin-bottom: 10px;
        }
        .message {
            padding: 5px;
            border-radius: 5px;
            margin-bottom: 5px;
            white-space: pre-wrap; /* 保留换行 */
        }
        .user {
            background-color: #e0f7fa;
            text-align: right;
        }
        .assistant {
            background-color: #e8f5e9;
        }
        #userInput {
            width: 100%;
            padding: 10px;
            font-size: 16px;
        }
    </style>
</head>
<body>

<h1>Chat Interface with Streaming & Context</h1>

<div id="chatBox"></div>
<input type="text" id="userInput" placeholder="Type a message..." onkeydown="if(event.key === 'Enter') sendMessage()">

<script>
    let conversationHistory = [];

    function displayMessage(role, content) {
        const chatBox = document.getElementById('chatBox');
        const messageDiv = document.createElement('div');
        messageDiv.className = `message ${role}`;

        const formattedContent = content.replace(/\\n/g, '<br>'); // 解析 \n 为 <br>

        messageDiv.innerHTML = formattedContent; // 使用 innerHTML 显示换行格式内容

        chatBox.appendChild(messageDiv);
        chatBox.scrollTop = chatBox.scrollHeight;
        return messageDiv;
    }

    function sendMessage() {
        const userInput = document.getElementById('userInput');
        const messageContent = userInput.value.trim();

        if (!messageContent) return;

        displayMessage('user', messageContent);
        userInput.value = '';

        conversationHistory.push({ role: 'user', content: messageContent });

        startSSE(conversationHistory);
    }

    function startSSE(messages) {
        const url = 'http://localhost:8098/ai/chat-stream';

        fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'text/event-stream'
            },
            body: JSON.stringify(messages)
        }).then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            let buffer = '';
            let assistantMessageDiv = displayMessage('assistant', ''); // 空元素用于后续更新
            let accumulatedData = '';

            return new ReadableStream({
                start(controller) {
                    function read() {
                        reader.read().then(({ done, value }) => {
                            if (done) {
                                controller.close();
                                conversationHistory.push({ role: 'assistant', content: accumulatedData });
                                return;
                            }

                            buffer += decoder.decode(value, { stream: true });
                            const eventBlocks = buffer.split("\n\n");
                            buffer = eventBlocks.pop();

                            eventBlocks.forEach(eventBlock => {
                                const lines = eventBlock.split("\n");
                                let eventData = { data: '' };

                                lines.forEach(line => {
                                    if (line.startsWith("data:")) {
                                        eventData.data += line.substring(5);
                                    }
                                });

                                if (eventData.data) {
                                    accumulatedData += eventData.data.replace(/\\n/g, '\n');
                                    assistantMessageDiv.innerHTML = accumulatedData;
                                    document.getElementById('chatBox').scrollTop = document.getElementById('chatBox').scrollHeight;
                                }
                            });

                            read();
                        });
                    }

                    read();
                }
            });
        }).catch(error => {
            console.error('Fetch error:', error);
            displayMessage('assistant', 'Error: Unable to fetch response');
        });
    }
</script>

</body>
</html>

我们直接来看一下效果!!

msedge_yITFrPaMCJ

简直完美!!!

我们可以看到我们的接口是没问题的

image-20241031194939724

至此,我们的SpringBoot集成Ollama,成功把本地模型通过java后端暴露出来了,意味着我们只要有机器,就可以对外暴露我们的接口,而且可以无限次调用!!!那么还有一些更进阶的操作,欢迎关注博主和这篇专栏,让我们一起探索AIGC的世界吧!!

那么这篇博客对应的项目地址博主也开源了,欢迎大家去玩玩

https://gitee.com/wangfugui-ma/spring-boot-ollama.git

在这里插入图片描述

### LlamaIndex 多模态 RAG 实现 LlamaIndex 支持多种数据类型的接入与处理,这使得它成为构建多模态检索增强生成(RAG)系统的理想选择[^1]。为了实现这一目标,LlamaIndex 结合了不同种类的数据连接器、索引机制以及强大的查询引擎。 #### 数据连接器支持多样化输入源 对于多模态数据的支持始于数据收集阶段。LlamaIndex 的数据连接器可以从多个异构资源中提取信息,包括但不限于APIs、PDF文档、SQL数据库等。这意味着无论是文本还是多媒体文件中的内容都可以被纳入到后续的分析流程之中。 #### 统一化的中间表示形式 一旦获取到了原始资料之后,下一步就是创建统一而高效的内部表达方式——即所谓的“中间表示”。这种转换不仅简化了下游任务的操作难度,同时也提高了整个系统的性能表现。尤其当面对复杂场景下的混合型数据集时,良好的设计尤为关键。 #### 查询引擎助力跨媒体理解能力 借助于内置的强大搜索引擎组件,用户可以通过自然语言提问的形式轻松获得所需答案;而对于更复杂的交互需求,则提供了专门定制版聊天机器人服务作为补充选项之一。更重要的是,在这里实现了真正的语义级关联匹配逻辑,从而让计算机具备了一定程度上的‘认知’功能去理解和回应人类意图背后所蕴含的意义所在。 #### 应用实例展示 考虑到实际应用场景的需求多样性,下面给出一段Python代码示例来说明如何利用LlamaIndex搭建一个多模态RAG系统: ```python from llama_index import GPTSimpleVectorIndex, SimpleDirectoryReader, LLMPredictor, PromptHelper, ServiceContext from langchain.llms.base import BaseLLM import os def create_multi_modal_rag_system(): documents = SimpleDirectoryReader(input_dir='./data').load_data() llm_predictor = LLMPredictor(llm=BaseLLM()) # 假设已经定义好了具体的大型预训练模型 service_context = ServiceContext.from_defaults( chunk_size_limit=None, prompt_helper=PromptHelper(max_input_size=-1), llm_predictor=llm_predictor ) index = GPTSimpleVectorIndex(documents, service_context=service_context) query_engine = index.as_query_engine(similarity_top_k=2) response = query_engine.query("请描述一下图片里的人物表情特征") print(response) ``` 此段脚本展示了从加载本地目录下各类格式文件开始直到最终完成一次基于相似度排序后的top-k条目返回全过程。值得注意的是,“query”方法接收字符串参数代表使用者想要询问的内容,而在后台则会自动调用相应的解析模块并结合先前准备好的知识库来进行推理计算得出结论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

掉头发的王富贵

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

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

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

打赏作者

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

抵扣说明:

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

余额充值