1、ChatGPT的热度
ChatGPT是由美国人工智能实验室OpenAI开发的一个对话AI模型,于2022年11月正式推出。自推出以来,ChatGPT因其出色的文本生成和对话交互能力而在全球范围内迅速走红。上线短短两个月,ChatGPT已获得1亿月度活跃用户,成为历史上增长最快的面向消费者的应用。
ChatGPT的爆火在业界掀起了惊涛骇浪,其用户增长速度刷新了消费级应用程序的记录。不少和ChatGPT“聊过天”的网友纷纷感叹,“只有你想不到,没有ChatGPT办不成的”。在一位工程师的诱导下,ChatGPT竟写出了毁灭人类的计划书,这进一步引发了人们对其潜在危险性的担忧。
ChatGPT的火热也带动了资本市场相关上市公司股票的普涨,包括AIGC、芯片算力、光模块等板块的普遍上涨。同时,国内互联网公司接连宣布类似ChatGPT的项目存在,如百度的类ChatGPT项目“文心一言”、阿里的“通义千问”。
2、前言准备
在国内也有许多的GPT平台,要使用的步骤都是一样的,先开通服务,再申请Key。
在使用的过程中,需要与流式编程搭配使用才能得到最好的效果,所以了解和掌握流式编程也是很重要的一步。
2.1、开通服务
(1)登录“阿里云”官网。
(2)搜索“通义千问”
(3)开通服务
确认开通
开通成功
2.2、reactor流式响应
Spring流式编程是一种基于流的处理方式,它将数据流作为主要处理对象。
功能:
- 数据处理:Spring流式编程能够处理大量的数据,并将数据转换成所需的形式或结构。
- 异步处理:Spring流式编程支持异步处理,能够并行处理多个数据流,提高系统的吞吐量和响应速度。
- 实时性:由于Spring流式编程支持异步处理和并行处理,因此它能够实现实时数据处理。
好处:
- 提高性能:由于Spring流式编程采用异步处理和并行处理,因此它能够提高系统的性能和响应速度。
- 简化开发:Spring流式编程提供了丰富的API和工具,简化了流式处理应用程序的开发过程。
- 易于维护:由于Spring流式编程采用声明式编程风格,代码结构清晰简洁,易于维护和调试。
- 灵活性强:Spring流式编程具有很强的灵活性,能够处理各种不同形式和结构的数据。
特点:
- 流式处理:Spring流式编程将数据看作流来处理,可以同时处理多个数据流。
- 事件驱动:Spring流式编程采用事件驱动的架构,能够快速响应用户输入和系统事件。
- 异步处理:Spring流式编程支持异步处理,能够并行处理多个数据流,提高系统的吞吐量和响应速度。
- 声明式编程:Spring流式编程采用声明式编程风格,通过简单的注解和XML配置来简化开发过程。
Flux 和 Mono 是 Reactor 中两个最基本的类型,是 Spring WebFlux 核心概念,表示 Reactor 中的数据流。
Flux和Mono本质上也是两个Publisher。
2.2.1、Flux流式对象
Flux 是 Project Reactor 中用于表示非确定性、0 到多个元素的类型。也就是说,Flux 可以是空的,也可以有一个或多个元素。它是响应式编程中的”热”流,类似于传统的迭代器,但更加强大和灵活。你可以把它想象成从一个数据源不断地流出的数据,可以监听这个数据流,当有新的数据出现时,会收到通知。
静态创建 Flux 的方法常见的包括 just()、range()、interval() 以及各种以 from- 为前缀的方法组等。
(1)combineLatest方法
用于组合多个 Flux(反应式流)的值,当这些流中的任何一个发出新的值时,它就会发射一个新的组合值。
public static void main(String[] args) {
// 创建三个 Flux
FluxString> flux1 = Flux.just("Hello");
FluxString> flux2 = Flux.just("World");
FluxString> flux3 = Flux.just("!");
// 使用 combineLatest 组合这三个 Flux
FluxString> combined = Operators.combineLatest(flux1, flux2, flux3, (s1, s2, s3) -> s1 + s2 + s3);
// 输出结果:HelloWorld!
combined.subscribe(System.out::println);
}
(2)concat类型方法
Flux对象的一个操作符,用于按顺序连接两个或多个Flux流,以便它们可以像单个流一样被消费。这意味着第一个Flux流的所有元素都被发射后,第二个Flux流的元素才会开始发射,依此类推,直到所有的Flux流都被完全消费。
public static void main(String[] args) {
FluxInteger> flux1 = Flux.just(1, 2, 3);
FluxInteger> flux2 = Flux.just(4, 5, 6);
// 使用concat按顺序连接flux1和flux2
FluxInteger> concatenatedFlux = Flux.concat(flux1, flux2);
// 订阅并打印结果
concatenatedFlux.subscribe(System.out::println);
// 输出将是:1, 2, 3, 4, 5, 6
}
(3)create方法
这个方法允许你创建一个新的 Flux,并允许你直接控制其发射的元素。
public static void main(String[] args) {
FluxObject> objectFlux = Flux.create(c -> {
for (int i = 0; i 5; i++) c.next(i); // 添加元素
c.complete(); // 添加完成
});
// 01234
objectFlux.subscribe(System.out::println);
}
(4)push方法
用于将元素推入到Flux中。与传统的Flux.next方法不同,Flux.push方法允许更低级别的控制和优化。
Flux.push方法的使用需要具备一定的反应式编程经验和技能,因为它涉及到低级别的并发控制和线程安全问题。在大多数情况下,使用Flux.next方法已经足够满足需求,而Flux.push方法更适合于需要更精细控制或优化性能的场景。
public static void main(String[] args) {
FluxObject> push = Flux.push(emitter -> {
for (int i = 0; i 5; i++) emitter.next(i); // 添加元素
emitter.complete(); // 添加完成
});
// 01234
push.subscribe(System.out::println);
}
(5)defer方法
Flux.defer()方法在Reactor中是用来延迟创建Flux的。这个方法返回一个新的Flux,这个Flux在订阅发生时才开始创建并执行原始的Flux。
public static void main(String[] args) {
FluxString> flux = Flux.defer(() -> Flux.just("create and executor"));
// 此时才会真的创建并执行:01234
flux.subscribe(System.out::println);
}
(6)empty方法
创建一个空的Flux对象。
(7)error方法
Flux.error()方法在Reactor中是用来创建一个在订阅后立即发射一个错误的Flux的。这个方法接收一个Throwable参数,这个参数表示错误。当订阅这个Flux时,它会立即发射这个错误给订阅者。
public FluxString> getFlux() {
return Flux.just("Request")
.flatMap(request -> {
if (request.equals("Invalid")) {
return Flux.error(new IllegalArgumentException("Invalid request"));
} else {
return Flux.just("Response");
}
});
}
(8)from类型方法
Flux.from()方法是一个将其他数据源转换为Flux流的方法。它可以将各种数据源(如集合、迭代器、异步数据源等)转换为Flux对象,以便在反应式编程中使用。
public static void main(String[] args) {
Integer[] array = new Integer[]{1,2,3,4,5};
// from、fromArray、fromStream、fromIterator
FluxInteger> flux = Flux.fromArray(array);
flux.subscribe(System.out::println);
}
(9)just方法
用于创建一个包含指定元素的Flux。这个方法可以指定序列中包含的所有元素,并且创建出来的Flux序列在发布这些元素之后会自动结束。
FluxString> flux = Flux.just("Hello", "World", "!");
(10)其他常用方法
方法名称 | 描述 |
---|---|
Flux.merge | 用于合并多个Flux流 |
Flux.range | 用于生成指定范围内整数序列的Flux |
Flux.using | 用于在Flux的生命周期内使用一个外部资源 |
Flux.collect | 用于将Flux中的元素收集到某种容器或数据结构中 |
Flux.distinct | 用于从Flux中过滤掉重复的元素 |
Flux.doOnEach | 用于在Flux中的每个元素上执行特定的操作 ,这些操作将在每个元素上单独执行,并且不会影响Flux流的其他操作。 |
Flux.filter | 用于对Flux中的元素进行过滤 |
Flux.flatMap | 用于将Flux中的每个元素进行一对多的转换。它可以将每个元素映射成一个新的Flux,然后将所有这些Flux合并成一个单一的Flux。 |
Flux.groupBy | 用于将Flux中的元素按照指定的键进行分组 |
2.2.2、Mono流式对象
Mono 是 Project Reactor 中用于表示 0 或 1 个元素的类型。也就是说,Mono 可以是空的,也可以有一个元素。它是响应式编程中的”冷”流,它可能不会产生任何数据,或者在某些情况下可能会产生大量的数据。你可以把它想象成从数据源获取一个数据,然后你可以在任何时候获取这个数据。
Flux对象有的方法Mono也基本都有。
2.3、前端EventSource
EventSource是一种在HTML5中用于实现服务器推送事件的技术。它允许服务器发送事件流(Server-Sent Events)到客户端,而无需客户端主动向服务器发送请求。
EventSource提供了一种简单的方式来接收服务器端发送的事件数据。它通过建立长连接,在服务器有新的数据时,会自动将数据推送给客户端。与传统的轮询方式相比,EventSource使用了长连接,可以节省带宽和资源,同时提供更好的实时性。
在HTML中,使用EventSource可以通过创建一个EventSource对象来实现。该对象可以指定服务器的URL,然后通过监听不同的事件来接收服务器发送的数据。例如,当服务器发送一个名为”message”的事件时,客户端可以监听该事件并执行相应的操作。
new EventSource(url, ?EventSourceInitDict);
// url:需要监听的地址
// EventSourceInitDict:携带的参数
3、接入通义千问
通义千问是阿里云推出的一个超大规模的语言模型,具有多轮对话、文案创作、逻辑推理、多模态理解、多语言支持等多种功能。它能够跟人类进行多轮的交互,也融入了多模态的知识理解,且有文案创作能力,能够续写小说、编写邮件等。通义千问在2023年4月7日开始邀请测试,4月11日在2023阿里云峰会上揭晓。4月18日,钉钉正式接入阿里巴巴“通义千问”大模型。2023年9月13日,阿里云宣布通义千问大模型已首批通过备案,并正式向公众开放。通义千问APP在各大手机应用市场正式上线,所有人都可以通过APP直接体验最新模型能力。此外,通义千问在2023年12月22日成为首个“大模型标准符合性评测”中首批通过评测的四款国产大模型之一,在通用性、智能性等维度均达到国家相关标准要求。
3.1、后端
后端使用SpringBoot + Reactor实现。
3.1.1、导入依赖
dependency>
groupId>org.springframework.bootgroupId>
artifactId>spring-boot-starter-webartifactId>
dependency>
dependency>
groupId>io.projectreactorgroupId>
artifactId>reactor-coreartifactId>
dependency>
dependency>
groupId>com.alibabagroupId>
artifactId>dashscope-sdk-javaartifactId>
version>2.10.1version>
dependency>
3.1.2、配置
(1)在application.yaml文件中编写API-KEY。
server:
port: 8081
ai-api-key: YOUR KEY
(2)注入Generation对象
用户可以通过与Generation对象进行交互,获得自然、流畅、准确的回答或任务完成结果,从而更加高效地与机器进行交互。这种交互方式能够减少用户对传统搜索引擎或问答系统的依赖,提高信息获取和任务完成的效率。同时,Generation对象也可以用于实现自然语言生成、对话生成、文本摘要、文本改写等多种应用场景。
@Configuration
public class AiConfig {
@Bean
public Generation generation(){
return new Generation();
}
}
3.1.3、编写接口
@RestController
@RequestMapping(value = "/ai")
public class TestAi {
@Value("${ai-api-key}")
private String appKey;
@Resource
private Generation generation;
@PostMapping(value = "/send", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public FluxServerSentEventString>> aiTalk(@RequestBody String question, HttpServletResponse response)
throws NoApiKeyException, InputRequiredException {
Message message = Message.builder()
.role(Role.USER.getValue())
.content(question).build();
QwenParam qwenParam = QwenParam.builder()
.model(Generation.Models.QWEN_PLUS)
.messages(Collections.singletonList(message))
.topP(0.8)
.resultFormat(QwenParam.ResultFormat.MESSAGE)
.enableSearch(true)
.apiKey(appKey)
.incrementalOutput(true)
.build();
FlowableGenerationResult> result = generation.streamCall(qwenParam);
return Flux.from(result)
.map(m -> {
// GenerationResult对象中输出流(GenerationOutput)的choices是一个列表,存放着生成的数据。
String content = m.getOutput().getChoices().get(0).getMessage().getContent();
return ServerSentEvent.String>builder().data(content).build();
})
.publishOn(Schedulers.boundedElastic())
.doOnError(e -> {
MapString, Object> map = new HashMap>(){{
put("code", "400");
put("message", "出现了异常,请稍后重试");
}};
try {
response.getOutputStream().print(JSONObject.toJSONString(map));
} catch (IOException ex) {
throw new RuntimeException(ex);
}
});
}
}
(1)Message对象
用户与模型的对话历史。list中的每个元素形式为{“role”:角色, “content”: 内容}。
role可以选值:
public enum Role {
USER("user"),
ASSISTANT("assistant"),
BOT("bot"),
SYSTEM("system"),
ATTACHMENT("attachment");
private final String value;
private Role(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
role 方法是用于设置消息的角色(或类型)的方法。这个方法允许您为消息指定一个特定的角色,以便在处理消息时可以对其进行分类或特殊处理。
(2)Model对象
指定用于对话的通义千问模型名。
public static class Models {
/** @deprecated */
@Deprecated
public static final String QWEN_V1 = "qwen-v1";
public static final String QWEN_TURBO = "qwen-turbo";
public static final String BAILIAN_V1 = "bailian-v1";
public static final String DOLLY_12B_V2 = "dolly-12b-v2";
/** @deprecated */
@Deprecated
public static final String QWEN_PLUS_V1 = "qwen-plus-v1";
public static final String QWEN_PLUS = "qwen-plus";
public static final String QWEN_MAX = "qwen-max";
public Models() {
}
}
(3)topP/topK方法
topP:生成过程中核采样方法概率阈值,例如,取值为0.8时,仅保留概率加起来大于等于0.8的最可能token的最小集合作为候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的确定性越高。
topK:生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。取值越大,生成的随机性越高;取值越小,生成的确定性越高。默认不传递该参数,取值为None或当top_k大于100时,表示不启用top_k策略,此时,仅有top_p策略生效。
(4)enableSearch方法
模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。取值如下:
- True:启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
- False(默认):关闭互联网搜索。
(5)incrementalOutput方法
控制流式输出模式,即后面内容会包含已经输出的内容;设置为True,将开启增量输出模式,后面输出不会包含已经输出的内容,您需要自行拼接整体输出。默认是false;
False:
I
I like
i like apple
True:
I
like
apple
该参数只能与stream输出模式配合使用。
更多参数描述请浏览阿里官方文档:https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.nextDoc.24ba12b0zyzTIv
3.2、前端
前端使用的是目前市面上流行的框架-Vue。
3.2.1、安装EventSource
EventSource是HTML5内置的一个对象,但是EventSource只支持Get请求,在很多情况下Get请求并不能满足要求,所以我们需要安装支持Post请求的EventSource。
npm install @microsoft/fetch-event-source
使用
import { fetchEventSource } from '@microsoft/fetch-event-source';
export default{
data(){
return{
show: false,
list:[],
}
},
mounted(){
fetchEventSource('http://localhost:8000/user/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"question": "java是什么?"}),
onmessage(event) {
// 接收数据
console.log(event);
},
onclose(){
// 数据传输完毕后就会关闭流
}
})
}
}
3.2.2、安装Markdown
为啥要安装Markdown咧?因为在AI生成的数据中,会有一些特殊的语法需要文本编辑器才能解析,所以就用Markdown才能更好的展示。
npm install markdown-it --save
div v-html="markdown.render(item.answer)" class="answer_message">div>
import MarkdownIt from 'markdown-it'
export default{
data(){
return{
markdown: new MarkdownIt(),
}
},
}
Markdown-it官网:https://markdown-it.docschina.org/
3.2.3、实现事件监听
search(){
if(this.query.trim().length == 0){
showNotify({ type: 'warning', message: '消息内容不能为空' }); return;
}
this.historyList.push({question: this.query, answer: ''});
this.query = "";
let thiz = this;
let length = this.historyList.length;
fetchEventSource(this.$api.CHAT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({question: thiz.historyList[length - 1].question}),
onmessage(event) {
//在此处的this不是外部的this,而是方法的调用者的this,所以需要在外部定义一个变量指向this
thiz.historyList[length - 1].answer += event.data;
},
onclose() {
let temp = thiz.historyList[thiz.historyList.length - 1];
let body = {
sessionId: thiz.$route.params.sessionId,
question: temp.question,
answer: temp.answer
}
thiz.$http.post(thiz.$api.SYNC_MESSAGE, body).then(result => {
console.log(result);
})
}
})
}
3.3、效果
省略了CSS样式。
4、总结
SpringBoot接入通义千问的实践过程,是一个富有挑战和收获的技术之旅。首先,我们需要理解通义千问的API接口和数据格式,这涉及到对其功能和数据模型的深入了解。在接入过程中,我们主要使用了SpringBoot提供的RestTemplate或WebClient进行API请求,通过JSON数据格式进行数据交互。
在这个过程中,我们面临的主要挑战是网络延迟和数据同步的问题。为了解决这些问题,我们采用了异步处理和缓存策略,优化了API请求的频率,提升了数据获取的效率。
从这次实践中,我们深刻体会到技术发展的快速和多变。未来,我们将继续关注通义千问的新特性和API变化,不断优化我们的接入方案,提升系统的稳定性和效率。同时,我们也会将这种技术应用于更多的业务场景,推动业务的智能化发展。