Prechádzať zdrojové kódy

Merge branch 'feature/ollama'

wangjinfei 4 týždňov pred
rodič
commit
eddcedaa50

+ 6 - 2
resources/nacos-yaml/webchat-aigc-service-dev.yaml

@@ -17,7 +17,7 @@ spring:
 # 大模型配置,应用于对话机器人
 llm:
   config:
-    # 当前开启使用哪个模型 kimi or deepseek
+    # 当前开启使用哪个模型 kimi or deepseek or ollama
     model: deepseek
     kimi:
       api-key:
@@ -28,8 +28,12 @@ llm:
     liblib:
       accessKey:
       SecretKey:
+    ollama:
+      base-url: http://127.0.0.1:11434
+      # ollama的本地模型
+      model: deepseek-r1:7b
 
 rocketmq:
   name-server: 127.0.0.1:9876
   consumer:
-    group: web_chat
+    group: web_chat

+ 33 - 0
webchat-aigc/src/main/java/com/webchat/aigc/config/OllamaConfig.java

@@ -0,0 +1,33 @@
+package com.webchat.aigc.config;
+
+import com.webchat.aigc.config.properties.OllamaPropertiesConfig;
+import com.webchat.common.util.llm.OllamaClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+/**
+ * @ClassName OllamaConfig
+ * @Description TODO
+ * @Author wangjinfei
+ * @Date 2025/2/18 22:04
+ */
+@Slf4j
+@Component
+public class OllamaConfig {
+    @Autowired
+    private OllamaPropertiesConfig ollamaPropertiesConfig;
+
+    @Bean
+    @ConditionalOnProperty(prefix = "llm.config", name = "model", havingValue = "ollama", matchIfMissing = false)
+    public OllamaClient ollamaClient(){
+        String baseUrl = ollamaPropertiesConfig.getBaseUrl();
+        String model = ollamaPropertiesConfig.getModel();
+
+        OllamaClient ollamaClient = new OllamaClient(baseUrl, model);
+        log.info("------->>>> ollamaJava客户端注入成功,baseUrl:{},model:{}", baseUrl, model);
+        return ollamaClient;
+    }
+}

+ 15 - 0
webchat-aigc/src/main/java/com/webchat/aigc/config/properties/OllamaPropertiesConfig.java

@@ -0,0 +1,15 @@
+package com.webchat.aigc.config.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "llm.config.ollama")
+public class OllamaPropertiesConfig {
+
+    private String baseUrl;
+
+    private String model;
+}

+ 5 - 0
webchat-aigc/src/main/java/com/webchat/aigc/llm/LLMServiceFactory.java

@@ -46,6 +46,11 @@ public class LLMServiceFactory implements InitializingBean, ApplicationContextAw
          * deepseek
          */
         serviceMap.put(LlmModelEnum.DEEPSEEK.getModel(), applicationContext.getBean(DeepSeekAIService.class));
+
+        /**
+         * ollama
+         */
+        serviceMap.put(LlmModelEnum.OLLAMA.getModel(), applicationContext.getBean(OllamaService.class));
     }
 
     public static AbstractLLMChatService getLLMService(String model) {

+ 100 - 0
webchat-aigc/src/main/java/com/webchat/aigc/llm/OllamaService.java

@@ -0,0 +1,100 @@
+package com.webchat.aigc.llm;
+
+import com.webchat.common.util.llm.OllamaClient;
+import com.webchat.domain.vo.llm.ChatCompletionMessage;
+import com.webchat.domain.vo.llm.ChatCompletionRequest;
+import com.webchat.domain.vo.llm.ChatCompletionResponse;
+import com.webchat.domain.vo.llm.ChatCompletionStreamChoice;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * @ClassName OllamaService
+ * @Description TODO
+ * @Author wangjinfei
+ * @Date 2025/2/15 13:25
+ */
+@AllArgsConstructor
+@Service
+public class OllamaService extends AbstractLLMChatService{
+    @Nullable
+    private OllamaClient ollamaClient;
+
+    /**
+     * 同步对话
+     * @author wangjinfei
+     * @date 2025/2/18 22:56
+     * @param messageList
+     * @return ChatCompletionResponse
+    */
+    @Override
+    protected ChatCompletionResponse chat(List<ChatCompletionMessage> messageList) throws Exception {
+        final List<ChatCompletionMessage> messages = messageList;
+        return ollamaClient.ChatCompletion(new ChatCompletionRequest(
+                ollamaClient.getModel(),
+                messages,
+                4096,
+                0.3f,
+                1
+        ));
+    }
+
+    /**
+     * 流式对话
+     * @author wangjinfei
+     * @date 2025/2/18 22:57
+     * @param emitter
+     * @param messageList
+     * @return String
+    */
+    @Override
+    protected String chat(SseEmitter emitter, List<ChatCompletionMessage> messageList) throws Exception {
+        // 用于记录流式推理完整结果,返回用于,用于对话消息持久化
+        StringBuilder aiMessage = new StringBuilder();
+
+        final List<ChatCompletionMessage> messages = messageList;
+        try {
+            ollamaClient.ChatCompletionStream(new ChatCompletionRequest(
+                    ollamaClient.getModel(),
+                    messages,
+                    2000,
+                    0.3f,
+                    1
+            )).subscribe(
+                    streamResponse -> {
+                        if (streamResponse.getChoices().isEmpty()) {
+                            return;
+                        }
+                        for (ChatCompletionStreamChoice choice : streamResponse.getChoices()) {
+                            String finishReason = choice.getFinishReason();
+                            if (finishReason != null) {
+                                emitter.send("finished");
+                                continue;
+                            }
+                            String responseContent = choice.getDelta().getContent();
+                            if (responseContent.equals("```") || responseContent.equals("json") ) {
+                                continue;
+                            }
+                            responseContent = responseContent.replaceAll("\n", "<br>");
+                            System.out.println(responseContent);
+                            emitter.send(responseContent);
+                            aiMessage.append(responseContent);
+                        }
+                    },
+                    error -> {
+                        error.printStackTrace();
+                    },
+                    () -> {
+//                        emitter.complete(); // 完成事件流发送
+                    }
+            );
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return aiMessage.toString();
+    }
+}

+ 3 - 1
webchat-common/src/main/java/com/webchat/common/enums/LlmModelEnum.java

@@ -7,7 +7,9 @@ public enum LlmModelEnum {
 
     KIMI("kimi"),
 
-    DEEPSEEK("deepseek");
+    DEEPSEEK("deepseek"),
+
+    OLLAMA("ollama");
 
     private String model;
 

+ 203 - 0
webchat-common/src/main/java/com/webchat/common/util/llm/OllamaClient.java

@@ -0,0 +1,203 @@
+package com.webchat.common.util.llm;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.google.gson.Gson;
+import com.webchat.common.exception.BusinessException;
+import com.webchat.domain.vo.llm.*;
+import com.webchat.domain.vo.ollama.Model;
+import com.webchat.domain.vo.ollama.OllamaChatCompletionResponse;
+import io.reactivex.BackpressureStrategy;
+import io.reactivex.Flowable;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @ClassName OllamaClient
+ * @Description
+ * @Author wangjinfei
+ * @Date 2025/2/14 16:07
+ */
+@Data
+@Slf4j
+public class OllamaClient {
+    // 列出本地可用的模型
+    public static final String API_TAGS = "/api/tags";
+    public static final String CHAT_COMPLETION_SUFFIX = "/api/chat";
+    private String baseUrl;
+
+    private String model;
+
+    public OllamaClient() {
+    }
+
+    public OllamaClient(String baseUrl, String model) {
+        if (StringUtils.isEmpty(baseUrl)) {
+            throw new BusinessException("请输入ollama的baseUrl");
+        }
+        if (baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
+        }
+        this.baseUrl = baseUrl;
+
+        if (modelExist(model) == false) {
+            throw new BusinessException(String.format("模型%s不存在,请检查模型是否存在", model));
+        }
+
+        this.model = model;
+    }
+
+    /**
+     * 判断模型是否存在
+     * @author wangjinfei
+     * @date 2025/2/14 16:46
+     * @param model
+     * @return Boolean
+    */
+    public Boolean modelExist(String model) {
+        if(StringUtils.isEmpty(model)){
+            throw new BusinessException("请输入模型名称");
+        }
+        String url = this.baseUrl + API_TAGS;
+        try  {
+            OkHttpClient client = new OkHttpClient();
+            Request request = new Request.Builder()
+                    .url(url)
+                    .build();
+            Response response = client.newCall(request).execute();
+            if (response.isSuccessful() == false) {
+                log.error("ollama请求{}失败,请检查ollama是否启动", url);
+                throw new BusinessException("ollama请求失败,请检查ollama是否启动");
+            }
+            JSONObject jsonObject = JSONObject.parseObject(response.body().string());
+            List<Model> modelList = JSONArray.parseArray(jsonObject.getString("models"), Model.class);
+            return modelList.stream().anyMatch(m -> model.equals(m.getName()));
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("ollama请求{}失败,请检查ollama是否启动", url, e);
+            throw new BusinessException("ollama请求失败,请检查ollama是否启动");
+        }
+    }
+
+    public ChatCompletionResponse ChatCompletion(ChatCompletionRequest request) throws IOException {
+        request.stream = false;
+        // 设置超时时间 本地ollama api响应很慢
+        OkHttpClient client = new OkHttpClient.Builder()
+                .connectTimeout(2, TimeUnit.MINUTES)
+                .readTimeout(2, TimeUnit.MINUTES)
+                .writeTimeout(2, TimeUnit.MINUTES)
+                .build();
+        okhttp3.MediaType mediaType = okhttp3.MediaType.parse("application/json");
+        System.out.println(new Gson().toJson(request));
+        okhttp3.RequestBody body = okhttp3.RequestBody.create(mediaType, new Gson().toJson(request));
+        okhttp3.Request httpRequest = new okhttp3.Request.Builder()
+                .url(getChatCompletionUrl())
+                .addHeader("Content-Type", "application/json")
+                .post(body)
+                .build();
+        try {
+            okhttp3.Response response = client.newCall(httpRequest).execute();
+            String responseBody = response.body().string();
+            System.out.println("-------------->>>>>>>>>>");
+            System.out.println(responseBody);
+            Gson gson = new Gson();
+            OllamaChatCompletionResponse ollamaResult = gson.fromJson(responseBody, OllamaChatCompletionResponse.class);
+            return ollamaResultToChatCompletionResponse(ollamaResult);
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw e;
+        }
+    }
+
+    public String getChatCompletionUrl() {
+        return baseUrl + CHAT_COMPLETION_SUFFIX;
+    }
+
+    /**
+     * ollama调用结果 封装上游对象
+     * @author wangjinfei
+     * @date 2025/2/19 20:37
+     * @param ollamaResult
+     * @return ChatCompletionResponse
+    */
+    private ChatCompletionResponse ollamaResultToChatCompletionResponse(OllamaChatCompletionResponse ollamaResult){
+        ChatCompletionResponse chatCompletionResponse = new ChatCompletionResponse();
+        chatCompletionResponse.setModel(ollamaResult.getModel());
+        List<ChatCompletionChoice> choices = new ArrayList<>();
+        ChatCompletionChoice choice = new ChatCompletionChoice();
+        ChatCompletionMessage message = new ChatCompletionMessage(ollamaResult.getMessage().getRole(), ollamaResult.getMessage().getContent());
+        choice.setMessage(message);
+        choices.add(choice);
+        chatCompletionResponse.setChoices(choices);
+        return chatCompletionResponse;
+    }
+
+    public Flowable<ChatCompletionStreamResponse> ChatCompletionStream(ChatCompletionRequest request) throws IOException {
+        request.stream = true;
+        // 设置超时时间 本地ollama api响应很慢
+        okhttp3.OkHttpClient client = new OkHttpClient.Builder()
+                .connectTimeout(1, TimeUnit.MINUTES)
+                .readTimeout(1, TimeUnit.MINUTES)
+                .writeTimeout(1, TimeUnit.MINUTES)
+                .build();
+        okhttp3.MediaType mediaType = okhttp3.MediaType.parse("application/json");
+        okhttp3.RequestBody body = okhttp3.RequestBody.create(mediaType, new Gson().toJson(request));
+        okhttp3.Request httpRequest = new okhttp3.Request.Builder()
+                .url(getChatCompletionUrl())
+                .addHeader("Content-Type", "application/json")
+                .post(body)
+                .build();
+        okhttp3.Response response = client.newCall(httpRequest).execute();
+        if (response.code() != 200) {
+            throw new RuntimeException("Failed to start stream: " + response.body().string());
+        }
+
+        Flowable<ChatCompletionStreamResponse> objectFlowable = Flowable.create(emitter -> {
+            okhttp3.ResponseBody responseBody = response.body();
+            String line;
+            while ((line = responseBody.source().readUtf8Line()) != null) {
+                OllamaChatCompletionResponse ollamaResult = JSONObject.parseObject(line, OllamaChatCompletionResponse.class);
+                if(ollamaResult.getDone()){
+                    System.out.println("---------------->>>> 结束");
+                    emitter.onComplete();
+                    return;
+                }
+                line = line.trim();
+                if (line.isEmpty()) {
+                    continue;
+                }
+
+                ChatCompletionStreamResponse chatCompletionStreamResponse = ollamaResultToChatCompletionStreamResponse(ollamaResult);
+                emitter.onNext(chatCompletionStreamResponse);
+            }
+        }, BackpressureStrategy.BUFFER);
+        return objectFlowable;
+    }
+
+    /**
+     * ollama调用结果 封装上游对象
+     * @author wangjinfei
+     * @date 2025/2/19 20:38
+     * @param ollamaResult
+     * @return ChatCompletionStreamResponse
+    */
+    private ChatCompletionStreamResponse ollamaResultToChatCompletionStreamResponse(OllamaChatCompletionResponse ollamaResult){
+        ChatCompletionStreamResponse chatCompletionStreamResponse = new ChatCompletionStreamResponse();
+        chatCompletionStreamResponse.setModel(ollamaResult.getModel());
+        List<ChatCompletionStreamChoice> choices = new ArrayList<>();
+        ChatCompletionStreamChoiceDelta delta = new ChatCompletionStreamChoiceDelta(ollamaResult.getMessage().getContent(), ollamaResult.getMessage().getRole());
+        ChatCompletionStreamChoice choice = new ChatCompletionStreamChoice(0, delta, null, null);
+        choices.add(choice);
+        chatCompletionStreamResponse.setChoices(choices);
+        return chatCompletionStreamResponse;
+    }
+}

+ 8 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/llm/ChatCompletionResponse.java

@@ -35,6 +35,14 @@ public class ChatCompletionResponse {
         return model;
     }
 
+    public void setModel(String model) {
+        this.model = model;
+    }
+
+    public void setChoices(List<ChatCompletionChoice> choices) {
+        this.choices = choices;
+    }
+
     public List<ChatCompletionChoice> getChoices() {
         if (choices == null) {
             return Lists.newArrayList();

+ 7 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/llm/ChatCompletionStreamResponse.java

@@ -31,11 +31,18 @@ public class ChatCompletionStreamResponse {
     public String getModel() {
         return model;
     }
+    public void setModel(String model) {
+        this.model = model;
+    }
 
     public List<ChatCompletionStreamChoice> getChoices() {
         return choices;
     }
 
+    public void setChoices(List<ChatCompletionStreamChoice> choices) {
+        this.choices = choices;
+    }
+
     public void setId(String id) {
         this.id = id;
     }

+ 27 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/ollama/Model.java

@@ -0,0 +1,27 @@
+package com.webchat.domain.vo.ollama;
+
+import lombok.Data;
+
+/**
+ * @ClassName Model
+ * @Description
+ * @Author wangjinfei
+ * @Date 2025/2/14 16:38
+ */
+@Data
+public class Model {
+//    @ApiModelProperty(value = "模型名称")
+    private String name;
+
+//    @ApiModelProperty(value = "最后修改时间")
+    private String modifiedAt;
+
+//    @ApiModelProperty(value = "模型大小")
+    private Long size;
+
+//    @ApiModelProperty(value = "模型的哈希摘要")
+    private String digest;
+
+//    @ApiModelProperty(value = "模型的详细信息")
+    private ModelDetails details;
+}

+ 27 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/ollama/ModelDetails.java

@@ -0,0 +1,27 @@
+package com.webchat.domain.vo.ollama;
+
+import lombok.Data;
+
+/**
+ * @ClassName ModelDetails
+ * @Description
+ * @Author wangjinfei
+ * @Date 2025/2/14 16:41
+ */
+@Data
+public class ModelDetails {
+//    @ApiModelProperty(value = "模型格式")
+    private String format;
+
+//    @ApiModelProperty(value = "模型家族")
+    private String family;
+
+//    @ApiModelProperty(value = "模型家族列表")
+    private String families;
+
+//    @ApiModelProperty(value = "模型的参数大小")
+    private String parameterSize;
+
+//    @ApiModelProperty(value = "量化级别")
+    private String quantizationLevel;
+}

+ 23 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/ollama/OllamaChatCompletionResponse.java

@@ -0,0 +1,23 @@
+package com.webchat.domain.vo.ollama;
+
+import lombok.Data;
+
+/**
+ * @ClassName OllamaChatCompletionResponse
+ * @Description TODO
+ * @Author wangjinfei
+ * @Date 2025/2/15 20:09
+ */
+@Data
+public class OllamaChatCompletionResponse {
+    private String model;
+    private String createdAt;
+    private OllamaMessage message;
+    private Boolean done;
+    private String totalDuration;
+    private String loadDuration;
+    private String promptEvalCount;
+    private String promptEvalDuration;
+    private String evalCount;
+    private String evalDuration;
+}

+ 15 - 0
webchat-domain/src/main/java/com/webchat/domain/vo/ollama/OllamaMessage.java

@@ -0,0 +1,15 @@
+package com.webchat.domain.vo.ollama;
+
+import lombok.Data;
+
+/**
+ * @ClassName OllamaMessage
+ * @Description TODO
+ * @Author wangjinfei
+ * @Date 2025/2/15 20:11
+ */
+@Data
+public class OllamaMessage {
+    private String role;
+    private String content;
+}