spring AI (五) 本地部署模型-llama3

在前面几章，我们使用的都是 ChatGPT 这种在线的大模型，即使各类大模型的 API 价格是越来越低的，但是在大量调用时依然是笔不小的支出，更不要说还有各种限制。而本地部署一些小模型在很多情况下也能提供一些不错的效果，不仅能够降低成本，限制还更少，最重要的是还能够对其进行一些自定义。这对公司及组织来说无疑是更有吸引力的。

本节将介绍使用 Ollama 来安装部署最新的 Llama3 模型，并使用 spring AI 进行调用。

安装 Ollama

Ollama 的安装很简单，在官网 下载对应操作系统的安装包安装即可。安装并启动后，系统托盘会显示图标。

然后打开终端，输入 ollama -v 验证版本

此时 Ollama 已经安装好了，但它仅支持命令行的方式对话。如果想要在界面中使用可以安装一个 webUI（如果只是代码调用，此步可以省略）

windows 系统，如果已经安装了 WSL 和 Docker 那可以使用以下命令运行

(1) 在 CPU 下运行：

Copy
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

(2) 支持 GPU 运行：

Copy
docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama

安装完成通过本地地址：http://127.0.0.1:3000 进行访问

已知问题

在 WSL 中安装此镜像后，无法直接在线下载模型，只能读取本地已安装的模型，但在模型选择完成对话依然失败。不确定是否是 WSL 和 Windows 宿主机的端口映射的问题。
在 Windows 宿主机中安装Docker Desktop再运行此镜像就没有问题，如果想使用 GPU 运行，也许要安装 Conda 环境。

Llama3 模型

要安装模型，可以先在Ollama 官网进行搜索

原生的 llama3 主要是使用英文训练的，虽然可以用 prompt 强制指定它使用中文回答，但也经常被忽略。因此我更推荐使用一些用中文微调的 llama3 model

你可以搜索 llama3-chinese 来找到中文微调的版本

以 wangshenzhi/llama3-8b-chinese-chat-ollama-q8 这个我使用的模型举例

复制此处命令在终端运行即可

在大多数情况下仅推荐安装 8B 的模型，更大的模型最好安装在专用的计算卡上。

Copy
ollama run wangshenzhi/llama3-8b-chinese-chat-ollama-q8

ollama run 表示运行模型，如果模型未下载，则会先进行下载，如果模型已下载就会直接运行。

此时可以直接在终端进行对话

框架调用

根据上一节的学习，我们可以直接复用代码，只需要修改模型的配置即可

Copy
private static OllamaChatClient getClient(){
var ollamaApi = new OllamaApi();

return new OllamaChatClient(ollamaApi).withDefaultOptions(OllamaOptions.create()
.withModel("wangshenzhi/llama3-8b-chinese-chat-ollama-q8")
.withTemperature(0.4f));
}

其中 OllamaApi 默认的 baseUrl 是 http://localhost:11434 如果你的模型不是部署在本地，需要修改这个地址
.withModel("wangshenzhi/llama3-8b-chinese-chat-ollama-q8") 表示指定模型，注意一定要是全名称

其它的不需要更改，直接运行即可

llama3 的已知问题是：1. 模型太小，回复经常错误或产生幻觉、2. 其似乎并不支持 Function Calling

完整代码如下

Copy
/**
* @author lza
* @date 2024/04/22-10:31
**/

@RestController
@RequestMapping("ollama")
@RequiredArgsConstructor
@CrossOrigin
public class OllamaController {

private static final Integer MAX_MESSAGE = 10;

private static Map<String, List<Message>> chatMessage = new ConcurrentHashMap<>();

/**
* 创建OpenAiChatClient
* @return OpenAiChatClient
*/
private static OllamaChatClient getClient(){
var ollamaApi = new OllamaApi();

return new OllamaChatClient(ollamaApi).withDefaultOptions(OllamaOptions.create()
.withModel("wangshenzhi/llama3-8b-chinese-chat-ollama-q8")
.withTemperature(0.4f));
}

/**
* 返回提示词
* @param message 用户输入的消息
* @return Prompt
*/
private List<Message> getMessages(String id, String message) {
String systemPrompt = "{prompt}";
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);

Message userMessage = new UserMessage(message);

Message systemMessage = systemPromptTemplate.createMessage(MapUtil.of("prompt", "you are a helpful AI assistant"));

List<Message> messages = chatMessage.get(id);

// 如果未获取到消息，则创建新的消息并将系统提示和用户输入的消息添加到消息列表中
if (messages == null){
messages = new ArrayList<>();
messages.add(systemMessage);
messages.add(userMessage);
} else {
messages.add(userMessage);
}

return messages;
}

/**
* 初始化函数调用
* @return ChatOptions
*/
private ChatOptions initFunc(){
return OpenAiChatOptions.builder().withFunctionCallbacks(List.of(
FunctionCallbackWrapper.builder(new MockWeatherService()).withName("weather").withDescription("Get the weather in location").build(),
FunctionCallbackWrapper.builder(new WbHotService()).withName("wbHot").withDescription("Get the hot list of Weibo").build(),
FunctionCallbackWrapper.builder(new TodayNews()).withName("todayNews").withDescription("60s watch world news").build(),
FunctionCallbackWrapper.builder(new DailyEnglishFunc()).withName("dailyEnglish").withDescription("A daily inspirational sentence in English").build())).build();
}

/**
* 创建连接
*/
@SneakyThrows
@GetMapping("/init/{message}")
public String init() {
return String.valueOf(UUID.randomUUID());
}

@GetMapping("chat/{id}/{message}")
public SseEmitter chat(@PathVariable String id, @PathVariable String message, HttpServletResponse response) {

response.setHeader("Content-type", "text/html;charset=UTF-8");
response.setCharacterEncoding("UTF-8");

OllamaChatClient client = getClient();
SseEmitter emitter = SseEmitterUtils.connect(id);
List<Message> messages = getMessages(id, message);
System.err.println("chatMessage大小: " + messages.size());
System.err.println("chatMessage: " + chatMessage);

if (messages.size() > MAX_MESSAGE){
SseEmitterUtils.sendMessage(id, "对话次数过多，请稍后重试🤔");
}else {
// 获取模型的输出流
Flux<ChatResponse> stream = client.stream(new Prompt(messages));

// 把流里面的消息使用SSE发送
Mono<String> result = stream
.flatMap(it -> {
StringBuilder sb = new StringBuilder();
String content = it.getResult().getOutput().getContent();
Optional.ofNullable(content).ifPresent(r -> {
SseEmitterUtils.sendMessage(id, content);
sb.append(content);
});
return Mono.just(sb.toString());
})
// 将消息拼接成字符串
.reduce((a, b) -> a + b)
.defaultIfEmpty("");

// 将消息存储到chatMessage中的AssistantMessage
result.subscribe(finalContent -> messages.add(new AssistantMessage(finalContent)));

// 将消息存储到chatMessage中
chatMessage.put(id, messages);

}
return emitter;

}

白嫖

以下是一些免费的 llama3 模型和 API

Groq 马斯克家的，听说用的不是英伟达的卡，而是专用的，速度非常快。
NVIDIA 老黄家的，里面有很多免费的模型以供使用。
cloudflare 大善人 CF 家的，也有很多模型可供部署，有免费额度。

Ollama
本地部署 Llama3 – 8B/70B 大模型！最简单的方法： 支持 CPU /GPU 运行 【3 种方案】
轻松搭建 llama3Web 交互界面 - Ollama + Open WebUI
使用 ollama + AnythingLLM 快速且简单的在本地部署 llama3
Ollama：本地大模型运行指南