一、前言
通义
,由通义千问更名而来,是阿里云推出的语言模型 ,于2023年9月13日正式向公众开放。 属于(AI Generated Content,AIGC
)领域, 是一个MaaS(模型即服务)的底座。为多模态大模型(Multimodal Models)。
通义意为“通情,达义”
,具备全副AI能力,致力于成为人们的工作、学习、生活助手。 功能包括多轮对话、文案创作、逻辑推理、多模态理解、多语言支持,能够跟人类进行多轮的交互,也融入了多模态的知识理解,且有文案创作能力,能够续写小说,编写邮件等。
虽然通义千问已经开源,但是连最基础的 Qwen2-0.5B ,也需要起码16GB的显存以上才能流程云运行(本人4060 8GB显存的显卡跑起来,等待一个问答结果,需要好几分钟),所以,私有化部署大模型所需要承受的硬件代价,不是一般的玩家所能承受的。此路不通,只得另寻他法了,这里所指的他法
,就是购买通义千问的Token接口服务,价格也相当实惠:
其他信息本文不再赘述,请移步 通义千问官网 进行查看和购买。
二、成果展示
最终的效果如下所示:
三、接口正文
3.1、maven依赖
通义千问maven依赖主要有一下两个:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-ai</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.15.2</version>
</dependency>
其中,dashscope-sdk-java
的版本需要根据你的jdk版本调整。
3.2、接口
- interface
public interface IChatGPTService {
void chat(ChatMessageRequest content, Principal principal) throws NoApiKeyException, InputRequiredException;
}
- impl
/**
* 历史对话记录; sessionId---> 历史记录
*/
private static final ConcurrentHashMap<String, List<Message>> history = new ConcurrentHashMap<>();
@Override
public void chat(ChatMessageRequest msg, Principal principal) throws NoApiKeyException, InputRequiredException {
String sessionId = msg.getSessionId();
//用户发送的消息入库
CompletableFuture.runAsync(() -> {
saveMsg(msg.getContent(), sessionId, Role.USER, getLocalDate());
});
Message message = Message.builder().role(Role.USER.getValue()).content(msg.getContent()).build();
// 创建QwenParam对象,设置参数
GenerationParam param = GenerationParam.builder()
.model(module) // 模型版本 qwen-max
.messages(getHistory(sessionId)) // 消息内容,如果需要启用多伦连续对话的话,就把用户历史消息以及GPT回复的消息一起放进去
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
.topP(0.8)
.enableSearch(true)
.apiKey(apiKey) // 你的apiKey,需要到阿里云百炼官网申请
.incrementalOutput(true)
.build();
// 调用生成接口,获取Flowable对象
Flux<GenerationResult> result = Flux.from(gen.streamCall(param));
StringBuffer builder = new StringBuffer();
DateTime finalLocalTime = getLocalDate();
Flux.from(result)
// 控制发送频率
.delayElements(Duration.ofMillis(200)).doOnNext(res -> {
String output = res.getOutput().getChoices().get(0).getMessage().getContent();
if (output == null || "".equals(output)) {
return;
}
// 将生成的消息通过websocket发送给前端,websocket内容将在下篇文章介绍
sendMsg(output, sessionId, principal);
builder.append(output);
}).doFinally(signalType -> {
//消息发送结束,告诉前端
sendMsg("!$$---END---$$!", sessionId, principal);
//消息入库
CompletableFuture.runAsync(() -> {
saveMsg(builder.toString(), sessionId, Role.ASSISTANT, finalLocalTime);
buildHistory(sessionId,
Message.builder().role(Role.ASSISTANT.getValue()).content(builder.toString()));
});
}).onErrorResume(str -> {
if (str instanceof ApiException) {
ApiException exception = (ApiException) str;
log.error("接口调用出现错误:{}", exception.getMessage());
}
sendMsg("GPT接口调用出现错误,该功能暂时无法使用,敬请期待.", sessionId, principal);
return Mono.empty();
}).subscribeOn(Schedulers.boundedElastic()) // 在弹性线程池中执行
.subscribe();
}
/**
* 每日凌晨自动清理历史对话缓存,防止缓存过大
*/
@Scheduled(cron = "0 59 23 * * ?")
private void autoCleanHistory() {
history.clear();
}
/**
* 构建历史消息
*/
private void buildHistory(String sessionId, MessageBuilder<?, ?> message) {
List<Message> historyMessages = history.computeIfAbsent(sessionId, k -> {
List<ChatMessageVO> list = sessionService.getById(sessionId).getMessages();
List<Message> getMsgList = new ArrayList<>();
if (list.isEmpty()) return getMsgList;
MessageBuilder<?, ?> msg = Message.builder();
//只取后面60条,历史消息太多,一是过快消耗token,二是压力太大
list.subList(Math.max(0, list.size() - 60), list.size()).forEach(item -> {
if (!"".equals(item.getContent())) {
msg.content(item.getContent()).role(item.getRole()).build();
getMsgList.add(msg.build());
}
});
return getMsgList;
});
// 添加消息到列表
historyMessages.add(message.build());
history.remove(sessionId);
history.put(sessionId, historyMessages);
}
private List<Message> getHistory(String sessionId) {
List<Message> list = history.get(sessionId);
if (list == null || list.isEmpty()) {
return new ArrayList<>();
}
list.removeIf(item -> ("".equals(item.getContent())));
List<Message> hist = list.subList(Math.max(0, list.size() - 80), list.size());
history.remove(sessionId);
history.put(sessionId, hist);
return hist;
}
- 接口调用
@Controller
@AllArgsConstructor
@Slf4j
@CrossOrigin
public class WebSocketController {
private final IChatGPTService service;
private final SocketServiceImpl socketService;
// 前端通过websocket发送消息给GPT,调用相关接口生成内容
@MessageMapping("/chat/send")
public void chat(@Payload ChatMessageRequest message, Principal principal) throws NoApiKeyException,
InputRequiredException {
service.chat(message, principal);
}
}
其中涉及到的一些Java实体如下:
@Data
@Accessors(chain = true)
@RequiredArgsConstructor
public class ChatMessageRequest extends BaseEntity {
private String content;
private String role;
private String sessionId;
}
//上文提到的getLocalDate函数内容
public static DateTime getLocalDate() {
ZoneId zoneId = ZoneId.of("Asia/Shanghai");
ZonedDateTime now = ZonedDateTime.now(zoneId);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(zoneId);
String formattedDate = now.format(formatter);
try {
ZonedDateTime parsedDate = ZonedDateTime.parse(formattedDate, formatter);
return new DateTime(parsedDate.toInstant().toEpochMilli());
}
catch (DateTimeParseException e) {
e.printStackTrace();
return cn.hutool.core.date.DateUtil.parse(cn.hutool.core.date.DateUtil.now());
}
}
通义千问主要的接口调用如上所述。在中篇,我将介绍《通义千问AI落地》前端实现。