add zhipu

This commit is contained in:
Chuck1sn
2025-05-22 15:57:06 +08:00
parent 82fe9d97df
commit b8cd5e7485
11 changed files with 110 additions and 61 deletions

View File

@@ -59,9 +59,10 @@ dependencies {
implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion") implementation("org.flywaydb:flyway-database-postgresql:$flywayVersion")
implementation("com.github.ben-manes.caffeine:caffeine:3.2.0") implementation("com.github.ben-manes.caffeine:caffeine:3.2.0")
implementation("org.springframework.boot:spring-boot-starter-quartz") implementation("org.springframework.boot:spring-boot-starter-quartz")
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0")
implementation("io.projectreactor:reactor-core:3.7.6")
implementation("dev.langchain4j:langchain4j:1.0.0") implementation("dev.langchain4j:langchain4j:1.0.0")
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0")
implementation("dev.langchain4j:langchain4j-community-zhipu-ai:1.0.1-beta6")
implementation("io.projectreactor:reactor-core:3.7.6")
testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion") testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
testImplementation("org.testcontainers:postgresql:$testcontainersVersion") testImplementation("org.testcontainers:postgresql:$testcontainersVersion")
testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion") testImplementation("org.testcontainers:testcontainers-bom:$testcontainersVersion")

View File

@@ -5,6 +5,6 @@ import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.memory.ChatMemoryAccess; import dev.langchain4j.service.memory.ChatMemoryAccess;
public interface DeepSeekChatAssistant extends ChatMemoryAccess { public interface AiChatAssistant extends ChatMemoryAccess {
TokenStream chat(@MemoryId String memoryId, @UserMessage String userMessage); TokenStream chat(@MemoryId String memoryId, @UserMessage String userMessage);
} }

View File

@@ -1,5 +1,6 @@
package com.zl.mjga.config.ai; package com.zl.mjga.config.ai;
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import dev.langchain4j.service.AiServices; import dev.langchain4j.service.AiServices;
@@ -12,6 +13,18 @@ import org.springframework.context.annotation.Configuration;
public class ChatModelConfig { public class ChatModelConfig {
private final DeepSeekConfiguration deepSeekConfiguration; private final DeepSeekConfiguration deepSeekConfiguration;
private final ZhiPuConfiguration zhiPuConfiguration;
private final PromptConfiguration promptConfiguration;
@Bean
public ZhipuAiStreamingChatModel zhipuChatModel() {
return ZhipuAiStreamingChatModel.builder()
.model(zhiPuConfiguration.getModelName())
.apiKey(zhiPuConfiguration.getApiKey())
.logRequests(true)
.logResponses(true)
.build();
}
@Bean @Bean
public OpenAiStreamingChatModel deepSeekChatModel() { public OpenAiStreamingChatModel deepSeekChatModel() {
@@ -23,10 +36,19 @@ public class ChatModelConfig {
} }
@Bean @Bean
public DeepSeekChatAssistant deepSeekChatAssistant(OpenAiStreamingChatModel deepSeekChatModel) { public AiChatAssistant deepSeekChatAssistant(OpenAiStreamingChatModel deepSeekChatModel) {
return AiServices.builder(DeepSeekChatAssistant.class) return AiServices.builder(AiChatAssistant.class)
.streamingChatModel(deepSeekChatModel) .streamingChatModel(deepSeekChatModel)
.systemMessageProvider(chatMemoryId -> deepSeekConfiguration.getPrompt().getSystem()) .systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem())
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build();
}
@Bean
public AiChatAssistant zhiPuChatAssistant(ZhipuAiStreamingChatModel zhipuChatModel) {
return AiServices.builder(AiChatAssistant.class)
.streamingChatModel(zhipuChatModel)
.systemMessageProvider(chatMemoryId -> promptConfiguration.getSystem())
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build(); .build();
} }

View File

@@ -1,13 +1,7 @@
package com.zl.mjga.config.ai; package com.zl.mjga.config.ai;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Data @Data
@@ -15,22 +9,7 @@ import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "deep-seek") @ConfigurationProperties(prefix = "deep-seek")
public class DeepSeekConfiguration { public class DeepSeekConfiguration {
@jakarta.annotation.Resource private ResourceLoader resourceLoader;
private String baseUrl; private String baseUrl;
private String apiKey; private String apiKey;
private Prompt prompt;
private String modelName; private String modelName;
@Data
public static class Prompt {
private String system;
}
@PostConstruct
public void init() throws IOException {
Resource resource = resourceLoader.getResource("classpath:prompt.txt");
prompt = new Prompt();
prompt.setSystem(Files.readString(Paths.get(resource.getURI())));
}
} }

View File

@@ -0,0 +1,24 @@
package com.zl.mjga.config.ai;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import lombok.Data;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;
@Data
@Component
public class PromptConfiguration {
@jakarta.annotation.Resource private ResourceLoader resourceLoader;
private String system;
@PostConstruct
public void init() throws IOException {
Resource resource = resourceLoader.getResource("classpath:prompt.txt");
system = Files.readString(Paths.get(resource.getURI()));
}
}

View File

@@ -0,0 +1,15 @@
package com.zl.mjga.config.ai;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "zhipu")
public class ZhiPuConfiguration {
private String baseUrl;
private String apiKey;
private String modelName;
}

View File

@@ -1,6 +1,6 @@
package com.zl.mjga.controller; package com.zl.mjga.controller;
import com.zl.mjga.service.DeepSeekAiService; import com.zl.mjga.service.AiChatService;
import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.TokenStream;
import java.security.Principal; import java.security.Principal;
import java.time.Duration; import java.time.Duration;
@@ -17,12 +17,12 @@ import reactor.core.publisher.Sinks;
@Slf4j @Slf4j
public class AiController { public class AiController {
private final DeepSeekAiService deepSeekAiService; private final AiChatService aiChatService;
@PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chat(Principal principal, @RequestBody String userMessage) { public Flux<String> chat(Principal principal, @RequestBody String userMessage) {
Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer(); Sinks.Many<String> sink = Sinks.many().unicast().onBackpressureBuffer();
TokenStream chat = deepSeekAiService.chat(principal.getName(), userMessage); TokenStream chat = aiChatService.chatWithZhiPu(principal.getName(), userMessage);
chat.onPartialResponse(text -> sink.tryEmitNext(text.replace(" ", "").replace("\t", ""))) chat.onPartialResponse(text -> sink.tryEmitNext(text.replace(" ", "").replace("\t", "")))
.onCompleteResponse( .onCompleteResponse(
r -> { r -> {

View File

@@ -0,0 +1,24 @@
package com.zl.mjga.service;
import com.zl.mjga.config.ai.AiChatAssistant;
import dev.langchain4j.service.TokenStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class AiChatService {
private final AiChatAssistant deepSeekChatAssistant;
private final AiChatAssistant zhiPuChatAssistant;
public TokenStream chatWithDeepSeek(String sessionIdentifier, String userMessage) {
return deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
}
public TokenStream chatWithZhiPu(String sessionIdentifier, String userMessage) {
return zhiPuChatAssistant.chat(sessionIdentifier, userMessage);
}
}

View File

@@ -1,19 +0,0 @@
package com.zl.mjga.service;
import com.zl.mjga.config.ai.DeepSeekChatAssistant;
import dev.langchain4j.service.TokenStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class DeepSeekAiService {
private final DeepSeekChatAssistant deepSeekChatAssistant;
public TokenStream chat(String sessionIdentifier, String userMessage) {
return deepSeekChatAssistant.chat(sessionIdentifier, userMessage);
}
}

View File

@@ -1,4 +1,8 @@
deep-seek: deep-seek:
base-url: "https://api.deepseek.com" base-url: "https://api.deepseek.com"
api-key: "sk-3633b0cd40884b27aa8402a1c5dc029d" api-key: "sk-3633b0cd40884b27aa8402a1c5dc029d"
model-name: "deepseek-chat" model-name: "deepseek-chat"
zhipu:
base-url: "https://open.bigmodel.cn/"
api-key: ""
model-name: "glm-4-flash"

View File

@@ -80,16 +80,15 @@ marked.setOptions({
const renderMarkdown = (content: string) => { const renderMarkdown = (content: string) => {
if (!content) return ''; if (!content) return '';
// 替换所有空白占位符(包括前后端约定的特殊字符)
const restoredContent = content const restoredContent = content
.replace(/␣/g, ' ') // 普通空格 .replace(/␣/g, ' ')
.replace(/⇥/g, '\t') // 制表符 .replace(/⇥/g, '\t')
.replace(/␤/g, '\n'); // 如果后端也处理了换行符 .replace(/␤/g, '\n');
// 处理Markdown中的代码块缩进
const processedContent = restoredContent const processedContent = restoredContent
.replace(/^(\s*)(`{3,})/gm, '$1$2') // 保留代码块前的空格 .replace(/^(\s*)(`{3,})/gm, '$1$2')
.replace(/(\s+)`/g, '$1`'); // 保留代码内联前的空格 .replace(/(\s+)`/g, '$1`');
const rawHtml = marked(processedContent); const rawHtml = marked(processedContent);
return DOMPurify.sanitize(rawHtml as string); return DOMPurify.sanitize(rawHtml as string);
@@ -104,10 +103,10 @@ const chatElements = computed(() => {
}); });
}); });
watch(messages, (newVal) => { // watch(messages, (newVal) => {
console.log('原始消息:', newVal[newVal.length - 1]); // console.log('原始消息:', newVal[newVal.length - 1]);
console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1])); // console.log('处理后HTML:', renderMarkdown(newVal[newVal.length - 1]));
}, { deep: true }); // }, { deep: true });
watch( watch(
chatElements, chatElements,