feat: 新增多大模型管理功能,重构生成服务
1. 新增大模型配置模块,支持百炼、豆包、腾讯混元三种提供商 2. 重构推荐服务和意图服务,改为调用生成服务的Embedding和生成接口 3. 新增大模型管理前端页面和路由 4. 新增数据库迁移脚本和模型相关的Mapper、Service、Controller 5. 替换旧的硬编码LLM配置为动态可配置模式
This commit is contained in:
parent
60b6370cd1
commit
b3e2afa9fe
Binary file not shown.
@ -1,11 +1,13 @@
|
||||
package com.artedu.generation;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* LLM生成服务启动类
|
||||
*/
|
||||
@MapperScan("com.artedu.generation.mapper")
|
||||
@SpringBootApplication(scanBasePackages = {"com.artedu.generation", "com.artedu.common"})
|
||||
public class GenerationServiceApplication {
|
||||
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
package com.artedu.generation.client;
|
||||
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 百炼(阿里Dashscope)客户端
|
||||
*/
|
||||
@Slf4j
|
||||
public class BailianClient implements LLMClient {
|
||||
|
||||
private LlmModel model;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public BailianClient(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt, Integer maxTokens) {
|
||||
return generate(null, prompt, maxTokens);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("百炼模型配置不完整");
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = model.getApiUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + model.getApiKey());
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model.getModelCode());
|
||||
|
||||
Map<String, Object> input = new HashMap<>();
|
||||
if (system != null && !system.isEmpty()) {
|
||||
input.put("messages", new Object[]{
|
||||
Map.of("role", "system", "content", system),
|
||||
Map.of("role", "user", "content", prompt)
|
||||
});
|
||||
} else {
|
||||
input.put("prompt", prompt);
|
||||
}
|
||||
requestBody.put("input", input);
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("result_format", "text");
|
||||
if (maxTokens != null) {
|
||||
parameters.put("max_tokens", maxTokens);
|
||||
}
|
||||
requestBody.put("parameters", parameters);
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
if (response == null) {
|
||||
log.error("百炼API返回空响应");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> output = (Map<String, Object>) response.get("output");
|
||||
if (output == null) {
|
||||
log.error("百炼API返回错误: {}", response.get("message"));
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = (String) output.get("text");
|
||||
log.debug("百炼生成结果, tokens={}",
|
||||
((Map<String, Object>) response.get("usage")).get("total_tokens"));
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
log.error("百炼API调用失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LlmModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModel(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Double> embed(String text) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("百炼模型配置不完整,无法获取embedding");
|
||||
return List.of();
|
||||
}
|
||||
|
||||
String url = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings";
|
||||
String embeddingModel = model.getConfigJson() != null && model.getConfigJson().contains("embeddingModel")
|
||||
? extractEmbeddingModel(model.getConfigJson())
|
||||
: "text-embedding-v1";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + model.getApiKey());
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", embeddingModel);
|
||||
requestBody.put("input", text);
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
if (response == null) {
|
||||
log.error("百炼Embedding API返回空响应");
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Map<String, Object>> data = (List<Map<String, Object>>) response.get("data");
|
||||
if (data == null || data.isEmpty()) {
|
||||
log.error("百炼Embedding API返回错误: {}", response.get("error"));
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Double> embedding = (List<Double>) data.get(0).get("embedding");
|
||||
log.debug("百炼Embedding成功, dimensions={}", embedding != null ? embedding.size() : 0);
|
||||
return embedding != null ? embedding : List.of();
|
||||
} catch (Exception e) {
|
||||
log.error("百炼Embedding API调用失败: {}", e.getMessage(), e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private String extractEmbeddingModel(String configJson) {
|
||||
try {
|
||||
Map<String, Object> config = JsonUtils.fromJsonMap(configJson);
|
||||
return (String) config.getOrDefault("embeddingModel", "text-embedding-v1");
|
||||
} catch (Exception e) {
|
||||
return "text-embedding-v1";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package com.artedu.generation.client;
|
||||
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 豆包(Volcengine Ark)客户端
|
||||
*/
|
||||
@Slf4j
|
||||
public class DoubaoClient implements LLMClient {
|
||||
|
||||
private LlmModel model;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public DoubaoClient(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt, Integer maxTokens) {
|
||||
return generate(null, prompt, maxTokens);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("豆包模型配置不完整");
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = model.getApiUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + model.getApiKey());
|
||||
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
if (system != null && !system.isEmpty()) {
|
||||
messages.add(Map.of("role", "system", "content", system));
|
||||
}
|
||||
messages.add(Map.of("role", "user", "content", prompt));
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model.getModelCode());
|
||||
requestBody.put("messages", messages);
|
||||
if (maxTokens != null) {
|
||||
requestBody.put("max_tokens", maxTokens);
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
if (response == null) {
|
||||
log.error("豆包API返回空响应");
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
|
||||
if (choices == null || choices.isEmpty()) {
|
||||
log.error("豆包API返回错误: {}", response.get("error"));
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
|
||||
String text = (String) message.get("content");
|
||||
log.debug("豆包生成成功");
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
log.error("豆包API调用失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LlmModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModel(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Double> embed(String text) {
|
||||
log.warn("豆包客户端暂不支持embedding,返回空向量");
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,106 @@
|
||||
package com.artedu.generation.client;
|
||||
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 腾讯混元客户端
|
||||
*/
|
||||
@Slf4j
|
||||
public class HunyuanClient implements LLMClient {
|
||||
|
||||
private LlmModel model;
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public HunyuanClient(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String prompt, Integer maxTokens) {
|
||||
return generate(null, prompt, maxTokens);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
if (model == null || model.getApiKey() == null || model.getApiKey().isEmpty()) {
|
||||
log.error("混元模型配置不完整");
|
||||
return null;
|
||||
}
|
||||
|
||||
String url = model.getApiUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = "https://hunyuan.tencentcloudapi.com/";
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + model.getApiKey());
|
||||
|
||||
List<Map<String, String>> messages = new ArrayList<>();
|
||||
if (system != null && !system.isEmpty()) {
|
||||
messages.add(Map.of("Role", "system", "Content", system));
|
||||
}
|
||||
messages.add(Map.of("Role", "user", "Content", prompt));
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("Model", model.getModelCode());
|
||||
requestBody.put("Messages", messages);
|
||||
if (maxTokens != null) {
|
||||
requestBody.put("MaxTokens", maxTokens);
|
||||
}
|
||||
|
||||
try {
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
|
||||
|
||||
if (response == null) {
|
||||
log.error("混元API返回空响应");
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> choices = (Map<String, Object>) response.get("Choices");
|
||||
if (choices == null) {
|
||||
log.error("混元API返回错误: {}", response.get("Error"));
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> messageList = (List<Map<String, Object>>) choices.get("Message");
|
||||
if (messageList == null || messageList.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = (String) messageList.get(0).get("Content");
|
||||
log.debug("混元生成成功");
|
||||
return text;
|
||||
} catch (Exception e) {
|
||||
log.error("混元API调用失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LlmModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModel(LlmModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Double> embed(String text) {
|
||||
log.warn("混元客户端暂不支持embedding,返回空向量");
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package com.artedu.generation.client;
|
||||
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
|
||||
/**
|
||||
* 大模型客户端接口
|
||||
*/
|
||||
public interface LLMClient {
|
||||
|
||||
/**
|
||||
* 文本生成
|
||||
*
|
||||
* @param prompt 提示词
|
||||
* @param maxTokens 最大token数
|
||||
* @return 生成的文本
|
||||
*/
|
||||
String generate(String prompt, Integer maxTokens);
|
||||
|
||||
/**
|
||||
* 文本生成(支持system消息)
|
||||
*
|
||||
* @param system 系统提示词
|
||||
* @param prompt 用户提示词
|
||||
* @param maxTokens 最大token数
|
||||
* @return 生成的文本
|
||||
*/
|
||||
String generate(String system, String prompt, Integer maxTokens);
|
||||
|
||||
/**
|
||||
* 获取当前使用的模型配置
|
||||
*/
|
||||
LlmModel getModel();
|
||||
|
||||
/**
|
||||
* 更新模型配置
|
||||
*/
|
||||
void setModel(LlmModel model);
|
||||
|
||||
/**
|
||||
* 获取文本 Embedding 向量
|
||||
*
|
||||
* @param text 输入文本
|
||||
* @return 向量列表,如果该模型不支持 embedding 则返回空列表
|
||||
*/
|
||||
java.util.List<Double> embed(String text);
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package com.artedu.generation.client;
|
||||
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 大模型客户端工厂
|
||||
*/
|
||||
@Slf4j
|
||||
public class LLMClientFactory {
|
||||
|
||||
private static final ConcurrentHashMap<Long, LLMClient> CLIENT_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 根据模型配置创建客户端
|
||||
*/
|
||||
public static LLMClient createClient(LlmModel model) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
LLMClient cached = CLIENT_CACHE.get(model.getId());
|
||||
if (cached != null && cached.getModel().getUpdatedAt().equals(model.getUpdatedAt())) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
LLMClient client;
|
||||
String provider = model.getProvider();
|
||||
if (provider == null) {
|
||||
provider = "bailian";
|
||||
}
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case "doubao":
|
||||
case "volcengine":
|
||||
client = new DoubaoClient(model);
|
||||
break;
|
||||
case "hunyuan":
|
||||
case "tencent":
|
||||
client = new HunyuanClient(model);
|
||||
break;
|
||||
case "bailian":
|
||||
case "dashscope":
|
||||
case "aliyun":
|
||||
default:
|
||||
client = new BailianClient(model);
|
||||
break;
|
||||
}
|
||||
|
||||
CLIENT_CACHE.put(model.getId(), client);
|
||||
log.info("创建LLM客户端: provider={}, model={}", provider, model.getModelName());
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
public static void clearCache() {
|
||||
CLIENT_CACHE.clear();
|
||||
log.info("LLM客户端缓存已清除");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定模型的缓存
|
||||
*/
|
||||
public static void clearCache(Long modelId) {
|
||||
CLIENT_CACHE.remove(modelId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class EmbedRequest {
|
||||
@NotBlank(message = "text不能为空")
|
||||
private String text;
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class GenerateRequest {
|
||||
/** 系统提示词(可选) */
|
||||
private String system;
|
||||
|
||||
/** 用户提示词(必填) */
|
||||
@NotBlank(message = "prompt不能为空")
|
||||
private String prompt;
|
||||
|
||||
/** 最大token数(可选,默认500) */
|
||||
private Integer maxTokens = 500;
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.service.LlmModelService;
|
||||
import com.artedu.generation.service.PromptEngine;
|
||||
import com.artedu.generation.service.QianwenClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -10,6 +12,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@ -24,6 +27,9 @@ public class GenerationController {
|
||||
@Autowired
|
||||
private PromptEngine promptEngine;
|
||||
|
||||
@Autowired
|
||||
private LlmModelService llmModelService;
|
||||
|
||||
@PostMapping("/utterance")
|
||||
public Result<Map<String, Object>> generateUtterance(
|
||||
@NotBlank(message = "message不能为空") @RequestParam("message") String message,
|
||||
@ -54,4 +60,45 @@ public class GenerationController {
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用文本生成接口(供其他服务调用)
|
||||
*/
|
||||
@PostMapping("/generate")
|
||||
public Result<Map<String, Object>> generate(
|
||||
@RequestBody @Validated GenerateRequest request) {
|
||||
|
||||
String generated = qianwenClient.generate(request.getSystem(), request.getPrompt(), request.getMaxTokens());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("content", generated);
|
||||
result.put("model", qianwenClient.getCurrentModel() != null ? qianwenClient.getCurrentModel().getModelName() : "default");
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 Embedding 接口(供其他服务调用)
|
||||
*/
|
||||
@PostMapping("/embed")
|
||||
public Result<Map<String, Object>> embed(
|
||||
@RequestBody @Validated EmbedRequest request) {
|
||||
|
||||
LLMClient client = llmModelService.getDefaultClient();
|
||||
if (client == null) {
|
||||
return Result.fail("没有可用的默认模型");
|
||||
}
|
||||
|
||||
List<Double> embedding = client.embed(request.getText());
|
||||
if (embedding == null || embedding.isEmpty()) {
|
||||
return Result.fail("Embedding 生成失败,当前模型可能不支持 embedding");
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("embedding", embedding);
|
||||
result.put("dimensions", embedding.size());
|
||||
result.put("model", client.getModel().getModelName());
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,154 @@
|
||||
package com.artedu.generation.controller;
|
||||
|
||||
import com.artedu.common.result.PageResult;
|
||||
import com.artedu.common.result.Result;
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.client.LLMClientFactory;
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import com.artedu.generation.service.LlmModelService;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 大模型配置管理接口
|
||||
* 仅管理员可操作
|
||||
*/
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/generation/llm-models")
|
||||
public class LlmModelController {
|
||||
|
||||
@Autowired
|
||||
private LlmModelService llmModelService;
|
||||
|
||||
/**
|
||||
* 获取默认模型
|
||||
*/
|
||||
@GetMapping("/default")
|
||||
public Result<LlmModel> getDefault() {
|
||||
return Result.success(llmModelService.getDefaultModel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有启用的模型(下拉选择用)
|
||||
*/
|
||||
@GetMapping("/active")
|
||||
public Result<List<LlmModel>> listActive() {
|
||||
return Result.success(llmModelService.listActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询模型列表
|
||||
*/
|
||||
@GetMapping
|
||||
public Result<PageResult<LlmModel>> list(
|
||||
@RequestParam(defaultValue = "1") @Min(1) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) int size) {
|
||||
Page<LlmModel> result = llmModelService.list(page, size);
|
||||
return Result.success(PageResult.of(
|
||||
(int) result.getCurrent(),
|
||||
(int) result.getSize(),
|
||||
result.getTotal(),
|
||||
result.getRecords()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public Result<LlmModel> getById(@PathVariable Long id) {
|
||||
return Result.success(llmModelService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模型
|
||||
*/
|
||||
@PostMapping
|
||||
public Result<String> add(@RequestBody @Validated LlmModel model) {
|
||||
llmModelService.add(model);
|
||||
return Result.success("添加成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模型
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public Result<String> update(@PathVariable Long id, @RequestBody @Validated LlmModel model) {
|
||||
model.setId(id);
|
||||
llmModelService.update(model);
|
||||
return Result.success("更新成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<String> delete(@PathVariable Long id) {
|
||||
llmModelService.delete(id);
|
||||
return Result.success("删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*/
|
||||
@PutMapping("/{id}/default")
|
||||
public Result<String> setDefault(@PathVariable Long id) {
|
||||
llmModelService.setDefault(id);
|
||||
return Result.success("已设置为默认模型");
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换启用状态
|
||||
*/
|
||||
@PutMapping("/{id}/toggle")
|
||||
public Result<String> toggleActive(@PathVariable Long id) {
|
||||
llmModelService.toggleActive(id);
|
||||
return Result.success("状态切换成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试模型连接
|
||||
*/
|
||||
@PostMapping("/{id}/test")
|
||||
public Result<Map<String, Object>> testConnection(@PathVariable Long id) {
|
||||
LlmModel model = llmModelService.getById(id);
|
||||
if (model == null) {
|
||||
return Result.fail("模型不存在");
|
||||
}
|
||||
|
||||
LLMClient client = LLMClientFactory.createClient(model);
|
||||
if (client == null) {
|
||||
return Result.fail("无法创建客户端,请检查提供商配置");
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
String result = client.generate("请回复一个字:OK", 10);
|
||||
if (result == null || result.isEmpty()) {
|
||||
return Result.fail("模型连接失败:返回内容为空");
|
||||
}
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("provider", model.getProvider());
|
||||
data.put("modelName", model.getModelName());
|
||||
data.put("elapsedMs", System.currentTimeMillis() - startTime);
|
||||
data.put("response", result.trim());
|
||||
|
||||
return Result.success(data);
|
||||
} catch (Exception e) {
|
||||
log.error("模型连接测试失败: id={}, provider={}", id, model.getProvider(), e);
|
||||
return Result.fail("模型连接失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package com.artedu.generation.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 大模型配置实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("llm_models")
|
||||
public class LlmModel {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 提供商: bailian(百炼), doubao(豆包), hunyuan(混元) */
|
||||
private String provider;
|
||||
|
||||
/** 模型显示名称 */
|
||||
private String modelName;
|
||||
|
||||
/** 模型代码(调用API时使用) */
|
||||
private String modelCode;
|
||||
|
||||
/** API密钥 */
|
||||
private String apiKey;
|
||||
|
||||
/** API基础地址 */
|
||||
private String apiUrl;
|
||||
|
||||
/** 是否启用: 1启用 0禁用 */
|
||||
private Integer isActive;
|
||||
|
||||
/** 是否为默认模型: 1是 0否 */
|
||||
private Integer isDefault;
|
||||
|
||||
/** 额外配置JSON */
|
||||
private String configJson;
|
||||
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.artedu.generation.mapper;
|
||||
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* 大模型配置Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface LlmModelMapper extends BaseMapper<LlmModel> {
|
||||
|
||||
/**
|
||||
* 获取默认启用的模型
|
||||
*/
|
||||
@Select("SELECT * FROM llm_models WHERE is_active = 1 AND is_default = 1 LIMIT 1")
|
||||
LlmModel selectDefault();
|
||||
|
||||
/**
|
||||
* 取消所有默认模型
|
||||
*/
|
||||
@Update("UPDATE llm_models SET is_default = 0 WHERE is_default = 1")
|
||||
int clearDefault();
|
||||
}
|
||||
@ -0,0 +1,147 @@
|
||||
package com.artedu.generation.service;
|
||||
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.client.LLMClientFactory;
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import com.artedu.generation.mapper.LlmModelMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 大模型配置服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class LlmModelService {
|
||||
|
||||
@Autowired
|
||||
private LlmModelMapper llmModelMapper;
|
||||
|
||||
/**
|
||||
* 获取默认模型
|
||||
*/
|
||||
public LlmModel getDefaultModel() {
|
||||
LlmModel model = llmModelMapper.selectDefault();
|
||||
if (model == null) {
|
||||
// 如果没有默认模型,返回第一个启用的模型
|
||||
LambdaQueryWrapper<LlmModel> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LlmModel::getIsActive, 1).orderByAsc(LlmModel::getId).last("LIMIT 1");
|
||||
model = llmModelMapper.selectOne(wrapper);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取模型
|
||||
*/
|
||||
public LlmModel getById(Long id) {
|
||||
return llmModelMapper.selectById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认模型的客户端
|
||||
*/
|
||||
public LLMClient getDefaultClient() {
|
||||
LlmModel model = getDefaultModel();
|
||||
if (model == null) {
|
||||
log.error("没有可用的默认大模型配置");
|
||||
return null;
|
||||
}
|
||||
return LLMClientFactory.createClient(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型ID获取客户端
|
||||
*/
|
||||
public LLMClient getClientById(Long modelId) {
|
||||
LlmModel model = llmModelMapper.selectById(modelId);
|
||||
if (model == null || model.getIsActive() != 1) {
|
||||
log.error("模型不存在或未启用: id={}", modelId);
|
||||
return null;
|
||||
}
|
||||
return LLMClientFactory.createClient(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询模型列表
|
||||
*/
|
||||
public Page<LlmModel> list(int page, int size) {
|
||||
Page<LlmModel> pageParam = new Page<>(page, size);
|
||||
LambdaQueryWrapper<LlmModel> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.orderByDesc(LlmModel::getCreatedAt);
|
||||
return llmModelMapper.selectPage(pageParam, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有启用的模型
|
||||
*/
|
||||
public List<LlmModel> listActive() {
|
||||
LambdaQueryWrapper<LlmModel> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(LlmModel::getIsActive, 1).orderByDesc(LlmModel::getIsDefault);
|
||||
return llmModelMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增模型
|
||||
*/
|
||||
@Transactional
|
||||
public void add(LlmModel model) {
|
||||
if (model.getIsDefault() != null && model.getIsDefault() == 1) {
|
||||
llmModelMapper.clearDefault();
|
||||
}
|
||||
llmModelMapper.insert(model);
|
||||
LLMClientFactory.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新模型
|
||||
*/
|
||||
@Transactional
|
||||
public void update(LlmModel model) {
|
||||
if (model.getIsDefault() != null && model.getIsDefault() == 1) {
|
||||
llmModelMapper.clearDefault();
|
||||
}
|
||||
llmModelMapper.updateById(model);
|
||||
LLMClientFactory.clearCache(model.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模型
|
||||
*/
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
llmModelMapper.deleteById(id);
|
||||
LLMClientFactory.clearCache(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*/
|
||||
@Transactional
|
||||
public void setDefault(Long id) {
|
||||
llmModelMapper.clearDefault();
|
||||
LlmModel model = new LlmModel();
|
||||
model.setId(id);
|
||||
model.setIsDefault(1);
|
||||
llmModelMapper.updateById(model);
|
||||
LLMClientFactory.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模型启用状态
|
||||
*/
|
||||
@Transactional
|
||||
public void toggleActive(Long id) {
|
||||
LlmModel model = llmModelMapper.selectById(id);
|
||||
if (model == null) return;
|
||||
model.setIsActive(model.getIsActive() == 1 ? 0 : 1);
|
||||
llmModelMapper.updateById(model);
|
||||
LLMClientFactory.clearCache(id);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
package com.artedu.generation.service;
|
||||
|
||||
import com.artedu.common.util.JsonUtils;
|
||||
import com.artedu.generation.client.LLMClient;
|
||||
import com.artedu.generation.entity.LlmModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
@ -14,21 +16,79 @@ import java.util.Map;
|
||||
@Service
|
||||
public class QianwenClient {
|
||||
|
||||
@Value("${llm.qianwen.api-key:}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${llm.qianwen.model:qwen-turbo}")
|
||||
private String model;
|
||||
@Autowired
|
||||
private LlmModelService llmModelService;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
|
||||
|
||||
/**
|
||||
* 文本生成(使用配置的默认模型)
|
||||
*/
|
||||
public String generate(String prompt, Integer maxTokens) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 优先使用数据库配置的模型
|
||||
LLMClient client = llmModelService.getDefaultClient();
|
||||
if (client != null) {
|
||||
String result = client.generate(prompt, maxTokens);
|
||||
if (result != null) {
|
||||
log.info("LLM生成完成(多模型), provider={}, model={}, elapsed={}ms",
|
||||
client.getModel().getProvider(),
|
||||
client.getModel().getModelName(),
|
||||
System.currentTimeMillis() - startTime);
|
||||
return result;
|
||||
}
|
||||
log.warn("多模型客户端生成失败,回退到默认百炼配置");
|
||||
}
|
||||
|
||||
// 回退到默认百炼配置(兼容旧逻辑)
|
||||
return generateWithDefault(prompt, maxTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认模型生成(支持system消息)
|
||||
*/
|
||||
public String generate(String system, String prompt, Integer maxTokens) {
|
||||
LLMClient client = llmModelService.getDefaultClient();
|
||||
if (client != null) {
|
||||
String result = client.generate(system, prompt, maxTokens);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// fallback:忽略system,使用旧方法
|
||||
return generate(prompt, maxTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定模型生成
|
||||
*/
|
||||
public String generateWithModel(Long modelId, String prompt, Integer maxTokens) {
|
||||
LLMClient client = llmModelService.getClientById(modelId);
|
||||
if (client == null) {
|
||||
log.error("指定模型不存在或未启用: id={}", modelId);
|
||||
return generate(prompt, maxTokens);
|
||||
}
|
||||
return client.generate(prompt, maxTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前默认模型信息
|
||||
*/
|
||||
public LlmModel getCurrentModel() {
|
||||
return llmModelService.getDefaultModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧的百炼直接调用(作为fallback)
|
||||
*/
|
||||
private String generateWithDefault(String prompt, Integer maxTokens) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model);
|
||||
requestBody.put("model", "qwen-turbo");
|
||||
|
||||
Map<String, Object> input = new HashMap<>();
|
||||
input.put("prompt", prompt);
|
||||
@ -43,7 +103,7 @@ public class QianwenClient {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
headers.set("Authorization", "Bearer sk-39ceb8d746014b349109e76f893acb18");
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class);
|
||||
@ -51,7 +111,7 @@ public class QianwenClient {
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
|
||||
String text = extractText(result);
|
||||
log.info("LLM生成完成, elapsed={}ms, tokens={}",
|
||||
log.info("LLM生成完成(默认回退), elapsed={}ms, tokens={}",
|
||||
System.currentTimeMillis() - startTime,
|
||||
extractTokens(result));
|
||||
return text;
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
-- 大模型配置表
|
||||
CREATE TABLE IF NOT EXISTS llm_models (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
provider VARCHAR(32) NOT NULL COMMENT '提供商: bailian(百炼), doubao(豆包), hunyuan(混元)',
|
||||
model_name VARCHAR(64) NOT NULL COMMENT '模型显示名称',
|
||||
model_code VARCHAR(64) NOT NULL COMMENT '模型代码(调用API时使用)',
|
||||
api_key VARCHAR(255) COMMENT 'API密钥',
|
||||
api_url VARCHAR(255) COMMENT 'API基础地址',
|
||||
is_active TINYINT DEFAULT 1 COMMENT '是否启用: 1启用 0禁用',
|
||||
is_default TINYINT DEFAULT 0 COMMENT '是否为默认模型: 1是 0否',
|
||||
config_json TEXT COMMENT '额外配置JSON',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_provider_model (provider, model_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表';
|
||||
|
||||
-- 插入默认模型(百炼)
|
||||
INSERT INTO llm_models (provider, model_name, model_code, api_key, api_url, is_active, is_default, config_json) VALUES
|
||||
('bailian', '通义千问-Turbo', 'qwen-turbo', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 1, '{"maxTokens": 1500, "temperature": 0.7}'),
|
||||
('bailian', '通义千问-Plus', 'qwen-plus', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 0, '{"maxTokens": 2000, "temperature": 0.7}'),
|
||||
('doubao', '豆包-Pro', 'doubao-pro-32k', '', 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', 1, 0, '{"maxTokens": 1500, "temperature": 0.7}'),
|
||||
('hunyuan', '混元-Standard', 'hunyuan-standard', '', 'https://hunyuan.tencentcloudapi.com/', 0, 0, '{"maxTokens": 1500, "temperature": 0.7}')
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
-- 大模型配置表
|
||||
CREATE TABLE IF NOT EXISTS llm_models (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
provider VARCHAR(32) NOT NULL COMMENT '提供商: bailian(百炼), doubao(豆包), hunyuan(混元)',
|
||||
model_name VARCHAR(64) NOT NULL COMMENT '模型显示名称',
|
||||
model_code VARCHAR(64) NOT NULL COMMENT '模型代码(调用API时使用)',
|
||||
api_key VARCHAR(255) COMMENT 'API密钥',
|
||||
api_url VARCHAR(255) COMMENT 'API基础地址',
|
||||
is_active TINYINT DEFAULT 1 COMMENT '是否启用: 1启用 0禁用',
|
||||
is_default TINYINT DEFAULT 0 COMMENT '是否为默认模型: 1是 0否',
|
||||
config_json TEXT COMMENT '额外配置JSON',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_provider_model (provider, model_code)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表';
|
||||
|
||||
-- 插入默认模型(百炼)
|
||||
INSERT INTO llm_models (provider, model_name, model_code, api_key, api_url, is_active, is_default, config_json) VALUES
|
||||
('bailian', '通义千问-Turbo', 'qwen-turbo', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 1, '{"maxTokens": 1500, "temperature": 0.7}'),
|
||||
('bailian', '通义千问-Plus', 'qwen-plus', 'sk-39ceb8d746014b349109e76f893acb18', 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, 0, '{"maxTokens": 2000, "temperature": 0.7}'),
|
||||
('doubao', '豆包-Pro', 'doubao-pro-32k', '', 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', 1, 0, '{"maxTokens": 1500, "temperature": 0.7}'),
|
||||
('hunyuan', '混元-Standard', 'hunyuan-standard', '', 'https://hunyuan.tencentcloudapi.com/', 0, 0, '{"maxTokens": 1500, "temperature": 0.7}')
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP;
|
||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,15 @@
|
||||
com\artedu\generation\controller\GenerationController.class
|
||||
com\artedu\generation\GenerationServiceApplication.class
|
||||
com\artedu\generation\service\QianwenClient.class
|
||||
com\artedu\generation\controller\GenerateRequest.class
|
||||
com\artedu\generation\service\LlmModelService.class
|
||||
com\artedu\generation\client\DoubaoClient.class
|
||||
com\artedu\generation\client\LLMClientFactory.class
|
||||
com\artedu\generation\controller\LlmModelController.class
|
||||
com\artedu\generation\mapper\LlmModelMapper.class
|
||||
com\artedu\generation\entity\LlmModel.class
|
||||
com\artedu\generation\controller\GenerationController.class
|
||||
com\artedu\generation\service\PromptEngine.class
|
||||
com\artedu\generation\client\BailianClient.class
|
||||
com\artedu\generation\client\LLMClient.class
|
||||
com\artedu\generation\client\HunyuanClient.class
|
||||
com\artedu\generation\GenerationServiceApplication.class
|
||||
com\artedu\generation\controller\EmbedRequest.class
|
||||
|
||||
@ -1,4 +1,15 @@
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\BailianClient.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\DoubaoClient.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\HunyuanClient.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\LLMClient.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\client\LLMClientFactory.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\EmbedRequest.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerateRequest.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\GenerationController.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\controller\LlmModelController.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\entity\LlmModel.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\GenerationServiceApplication.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\mapper\LlmModelMapper.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\LlmModelService.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\PromptEngine.java
|
||||
D:\www\agent_9art\backend\generation-service\src\main\java\com\artedu\generation\service\QianwenClient.java
|
||||
|
||||
@ -15,14 +15,10 @@ import java.util.*;
|
||||
@Service
|
||||
public class LLMIntentClient {
|
||||
|
||||
@Value("${llm.qianwen.api-key:}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${llm.qianwen.model:qwen-turbo}")
|
||||
private String model;
|
||||
@Value("${generation.service.url:http://generation-service:8086}")
|
||||
private String generationServiceUrl;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
|
||||
|
||||
public IntentResult recognize(String message, String contextSummary, List<IntentCategory> categories) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
@ -31,30 +27,27 @@ public class LLMIntentClient {
|
||||
String prompt = buildPrompt(message, contextSummary, categories);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model);
|
||||
|
||||
Map<String, Object> input = new HashMap<>();
|
||||
input.put("prompt", prompt);
|
||||
requestBody.put("input", input);
|
||||
|
||||
Map<String, Object> parameters = new HashMap<>();
|
||||
parameters.put("result_format", "text");
|
||||
requestBody.put("parameters", parameters);
|
||||
requestBody.put("system", "你是一个专业的教育培训机构客服意图识别助手。请严格按要求的JSON格式输出结果。");
|
||||
requestBody.put("prompt", prompt);
|
||||
requestBody.put("maxTokens", 500);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(API_URL, request, String.class);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(
|
||||
generationServiceUrl + "/api/v1/generation/generate", request, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
|
||||
String text = extractText(result);
|
||||
return parseIntentResult(text, categories);
|
||||
if (result != null && result.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
String text = (String) data.get("content");
|
||||
return parseIntentResult(text, categories);
|
||||
}
|
||||
}
|
||||
|
||||
log.error("LLM意图识别失败: status={}", response.getStatusCode());
|
||||
log.error("LLM意图识别失败: status={}, body={}", response.getStatusCode(), response.getBody());
|
||||
return createDefaultResult();
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -65,7 +58,7 @@ public class LLMIntentClient {
|
||||
|
||||
private String buildPrompt(String message, String contextSummary, List<IntentCategory> categories) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("你是一个专业的教育培训机构客服意图识别助手。请根据学员的咨询消息,判断其意图。\n\n");
|
||||
sb.append("请根据学员的咨询消息,判断其意图。\n\n");
|
||||
sb.append("可选意图列表:\n");
|
||||
for (IntentCategory cat : categories) {
|
||||
sb.append("- ").append(cat.getCode()).append(": ").append(cat.getName())
|
||||
@ -77,15 +70,6 @@ public class LLMIntentClient {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String extractText(Map<String, Object> result) {
|
||||
if (result == null) return "";
|
||||
Map<String, Object> output = (Map<String, Object>) result.get("output");
|
||||
if (output != null) {
|
||||
return (String) output.get("text");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private IntentResult parseIntentResult(String text, List<IntentCategory> categories) {
|
||||
IntentResult result = new IntentResult();
|
||||
try {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -13,14 +13,10 @@ import java.util.*;
|
||||
@Service
|
||||
public class EmbeddingClient {
|
||||
|
||||
@Value("${llm.qianwen.api-key:}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${llm.qianwen.embedding-model:text-embedding-v1}")
|
||||
private String embeddingModel;
|
||||
@Value("${generation.service.url:http://generation-service:8086}")
|
||||
private String generationServiceUrl;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final String EMBEDDING_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/embeddings";
|
||||
|
||||
/**
|
||||
* 获取文本的 Embedding 向量
|
||||
@ -31,19 +27,22 @@ public class EmbeddingClient {
|
||||
}
|
||||
try {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", embeddingModel);
|
||||
requestBody.put("input", text);
|
||||
requestBody.put("text", text);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(EMBEDDING_URL, request, String.class);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(
|
||||
generationServiceUrl + "/api/v1/generation/embed", request, String.class);
|
||||
|
||||
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||
Map<String, Object> result = JsonUtils.fromJsonMap(response.getBody());
|
||||
return extractEmbedding(result);
|
||||
if (result != null && result.get("data") != null) {
|
||||
Map<String, Object> data = (Map<String, Object>) result.get("data");
|
||||
List<Double> embedding = (List<Double>) data.get("embedding");
|
||||
return embedding != null ? embedding : List.of();
|
||||
}
|
||||
}
|
||||
log.error("Embedding API 调用失败: status={}, body={}", response.getStatusCode(), response.getBody());
|
||||
return List.of();
|
||||
@ -66,19 +65,4 @@ public class EmbeddingClient {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Double> extractEmbedding(Map<String, Object> result) {
|
||||
if (result == null) return List.of();
|
||||
try {
|
||||
List<Map<String, Object>> data = (List<Map<String, Object>>) result.get("data");
|
||||
if (data != null && !data.isEmpty()) {
|
||||
List<Double> embedding = (List<Double>) data.get(0).get("embedding");
|
||||
return embedding != null ? embedding : List.of();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析 Embedding 失败", e);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -12,6 +12,7 @@ import CustomerSessionList from './pages/CustomerSessionList'
|
||||
import SensitiveWordList from './pages/SensitiveWordList'
|
||||
import RiskEventList from './pages/RiskEventList'
|
||||
import MessageSearch from './pages/MessageSearch'
|
||||
import LlmModelList from './pages/LlmModelList'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -31,6 +32,7 @@ function App() {
|
||||
<Route path="risk/sensitive-words" element={<SensitiveWordList />} />
|
||||
<Route path="risk/events" element={<RiskEventList />} />
|
||||
<Route path="risk/message-search" element={<MessageSearch />} />
|
||||
<Route path="llm-models" element={<LlmModelList />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Layout as AntLayout, Menu, Button, message, Badge } from 'antd'
|
||||
import { DashboardOutlined, CommentOutlined, BookOutlined, TeamOutlined, UserOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
||||
import { DashboardOutlined, CommentOutlined, BookOutlined, TeamOutlined, UserOutlined, SafetyCertificateOutlined, RobotOutlined } from '@ant-design/icons'
|
||||
import { connectRiskEventStream, disconnectRiskEventStream, requestNotificationPermission, showDesktopNotification } from '../utils/notification'
|
||||
|
||||
const { Header, Sider, Content } = AntLayout
|
||||
@ -76,6 +76,7 @@ export default function Layout() {
|
||||
],
|
||||
},
|
||||
{ key: '/admins', icon: <TeamOutlined />, label: '管理员账号' },
|
||||
{ key: '/llm-models', icon: <RobotOutlined />, label: '大模型管理' },
|
||||
]
|
||||
|
||||
const handleLogout = () => {
|
||||
|
||||
354
frontend/admin/src/pages/LlmModelList.tsx
Normal file
354
frontend/admin/src/pages/LlmModelList.tsx
Normal file
@ -0,0 +1,354 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Table, Card, Button, Modal, Form, Input, Switch, Tag, message, Popconfirm,
|
||||
Space, Select
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, EditOutlined, DeleteOutlined, CheckCircleOutlined,
|
||||
ThunderboltOutlined, ApiOutlined
|
||||
} from '@ant-design/icons'
|
||||
import request from '../utils/request'
|
||||
|
||||
interface LlmModel {
|
||||
id: number
|
||||
provider: string
|
||||
modelName: string
|
||||
modelCode: string
|
||||
apiKey: string
|
||||
apiUrl: string
|
||||
isActive: number
|
||||
isDefault: number
|
||||
configJson: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const providerMap: Record<string, { label: string; color: string }> = {
|
||||
bailian: { label: '百炼(阿里)', color: 'orange' },
|
||||
doubao: { label: '豆包(字节)', color: 'blue' },
|
||||
hunyuan: { label: '混元(腾讯)', color: 'green' },
|
||||
}
|
||||
|
||||
export default function LlmModelList() {
|
||||
const [models, setModels] = useState<LlmModel[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingId, setEditingId] = useState<number | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const loadModels = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res: any = await request.get('/v1/generation/llm-models')
|
||||
setModels(res.data.list || [])
|
||||
} catch (e) {
|
||||
message.error('加载模型列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadModels()
|
||||
}, [])
|
||||
|
||||
const defaultApiUrls: Record<string, string> = {
|
||||
bailian: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
|
||||
doubao: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
|
||||
hunyuan: 'https://hunyuan.tencentcloudapi.com/',
|
||||
}
|
||||
|
||||
const handleProviderChange = (provider: string) => {
|
||||
const currentUrl = form.getFieldValue('apiUrl')
|
||||
// 如果当前URL为空或是默认URL之一,则自动切换
|
||||
const isDefaultUrl = Object.values(defaultApiUrls).includes(currentUrl)
|
||||
if (!currentUrl || isDefaultUrl) {
|
||||
form.setFieldsValue({ apiUrl: defaultApiUrls[provider] || '' })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingId(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
provider: 'bailian',
|
||||
isActive: 1,
|
||||
isDefault: 0,
|
||||
apiUrl: defaultApiUrls['bailian'],
|
||||
configJson: '{"maxTokens": 1500, "temperature": 0.7}',
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (record: LlmModel) => {
|
||||
setEditingId(record.id)
|
||||
form.setFieldsValue({
|
||||
...record,
|
||||
isActive: record.isActive === 1,
|
||||
isDefault: record.isDefault === 1,
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleSave = async (values: any) => {
|
||||
try {
|
||||
const payload = {
|
||||
...values,
|
||||
isActive: values.isActive ? 1 : 0,
|
||||
isDefault: values.isDefault ? 1 : 0,
|
||||
}
|
||||
|
||||
if (editingId) {
|
||||
await request.put(`/v1/generation/llm-models/${editingId}`, payload)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await request.post('/v1/generation/llm-models', payload)
|
||||
message.success('添加成功')
|
||||
}
|
||||
setModalVisible(false)
|
||||
loadModels()
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.message || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await request.delete(`/v1/generation/llm-models/${id}`)
|
||||
message.success('删除成功')
|
||||
loadModels()
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetDefault = async (id: number) => {
|
||||
try {
|
||||
await request.put(`/v1/generation/llm-models/${id}/default`)
|
||||
message.success('已设置为默认模型')
|
||||
loadModels()
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.message || '设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleActive = async (id: number) => {
|
||||
try {
|
||||
await request.put(`/v1/generation/llm-models/${id}/toggle`)
|
||||
message.success('状态切换成功')
|
||||
loadModels()
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.message || '切换失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTest = async (record: LlmModel) => {
|
||||
try {
|
||||
const res: any = await request.post(`/v1/generation/llm-models/${record.id}/test`)
|
||||
const data = res.data
|
||||
Modal.success({
|
||||
title: '连接测试成功',
|
||||
content: (
|
||||
<div>
|
||||
<p><strong>提供商:</strong>{providerMap[record.provider]?.label || record.provider}</p>
|
||||
<p><strong>模型:</strong>{data.modelName}</p>
|
||||
<p><strong>响应时间:</strong>{data.elapsedMs}ms</p>
|
||||
<p><strong>模型回复:</strong>{data.response}</p>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
} catch (e: any) {
|
||||
message.error(e.response?.data?.message || '连接测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
title: '提供商',
|
||||
dataIndex: 'provider',
|
||||
width: 120,
|
||||
render: (p: string) => (
|
||||
<Tag color={providerMap[p]?.color || 'default'}>
|
||||
{providerMap[p]?.label || p}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '模型名称',
|
||||
dataIndex: 'modelName',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '模型代码',
|
||||
dataIndex: 'modelCode',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: 'API地址',
|
||||
dataIndex: 'apiUrl',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '默认',
|
||||
dataIndex: 'isDefault',
|
||||
width: 80,
|
||||
render: (v: number) =>
|
||||
v === 1 ? <Tag color="gold" icon={<CheckCircleOutlined />}>默认</Tag> : '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'isActive',
|
||||
width: 100,
|
||||
render: (v: number, record: LlmModel) => (
|
||||
<Switch
|
||||
checked={v === 1}
|
||||
checkedChildren="启用"
|
||||
unCheckedChildren="禁用"
|
||||
onChange={() => handleToggleActive(record.id)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_: any, record: LlmModel) => (
|
||||
<Space size="small">
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ApiOutlined />}
|
||||
onClick={() => handleTest(record)}
|
||||
>
|
||||
测试
|
||||
</Button>
|
||||
{record.isDefault !== 1 && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<ThunderboltOutlined />}
|
||||
onClick={() => handleSetDefault(record.id)}
|
||||
>
|
||||
设为默认
|
||||
</Button>
|
||||
)}
|
||||
<Popconfirm
|
||||
title="确定删除此模型?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>大模型管理</h2>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<span style={{ color: '#666' }}>
|
||||
管理系统使用的大语言模型,可切换百炼、豆包、混元等不同提供商。
|
||||
</span>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
添加模型
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={models}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingId ? '编辑模型' : '添加模型'}
|
||||
open={modalVisible}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
onOk={() => form.submit()}
|
||||
width={600}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSave}
|
||||
>
|
||||
<Form.Item
|
||||
name="provider"
|
||||
label="提供商"
|
||||
rules={[{ required: true, message: '请选择提供商' }]}
|
||||
>
|
||||
<Select placeholder="选择提供商" onChange={handleProviderChange}>
|
||||
<Select.Option value="bailian">百炼(阿里)</Select.Option>
|
||||
<Select.Option value="doubao">豆包(字节)</Select.Option>
|
||||
<Select.Option value="hunyuan">混元(腾讯)</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="modelName"
|
||||
label="模型显示名称"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}
|
||||
>
|
||||
<Input placeholder="如:通义千问-Turbo" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="modelCode"
|
||||
label="模型代码"
|
||||
rules={[{ required: true, message: '请输入模型代码' }]}
|
||||
>
|
||||
<Input placeholder="如:qwen-turbo" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="apiKey"
|
||||
label="API密钥"
|
||||
rules={[{ required: true, message: '请输入API密钥' }]}
|
||||
>
|
||||
<Input.Password placeholder="sk-..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="apiUrl"
|
||||
label="API地址"
|
||||
rules={[{ required: true, message: '请输入API地址' }]}
|
||||
>
|
||||
<Input placeholder="https://..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="configJson"
|
||||
label="额外配置(JSON)"
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder='{"maxTokens": 1500, "temperature": 0.7}'
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="启用状态" name="isActive" valuePropName="checked">
|
||||
<Switch checkedChildren="启用" unCheckedChildren="禁用" defaultChecked />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="设为默认" name="isDefault" valuePropName="checked">
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user