导语:本文以对接阿里百炼平台的模型为例,展示flutter+dart如何通过dio调用ai接口,进行流式chat聊天功能。
然后其实我是为了凑字数,所以通篇都是些废话,直接跳到最下面看demo示例代码就行了。
参考文档
模型平台官方对接文档:如何通过OpenAI接口调用通义千问模型-阿里云帮助中心_(Model Studio)-阿里云帮助中心 (aliyun.com)
dio官方文档:dio/dio/README-ZH.md at main · cfug/dio
步骤
请求条件
平台介绍:阿里云的大模型服务平台百炼是一站式的大模型开发及应用构建平台。不论是开发者还是业务人员,都能深入参与大模型应用的设计和构建。您可以通过简单的“拖拉拽”操作,在5分钟内开发出一款大模型应用,或在几小时内训练出一个专属模型,从而将更多精力专注于应用创新。 总而言之,就是这里有ai的api接口可以调用,价格便宜点,而且是国内的,不用魔法。
根据官方的请求示例,可知调用请求的基本条件为:
- 接口调用方式:
POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- 请求头:Authorization:你的api-key、Content-Type:请求数据类型
- 请求体:
model
:模型,messages
:聊天列表 ->role
:角色、content
:内容,stream
:是否要开启stream流式响应
curl --location 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' \
--header "Authorization: Bearer $DASHSCOPE_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"model": "qwen-plus",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "你是谁?"
}
],
"stream":true
}'
其中api-key的申请教程为:如何获取API-KEY_大模型服务平台百炼(Model Studio)-阿里云帮助中心
构建dio实例
dio介绍:dio 是一个强大的 HTTP 网络请求库,支持全局配置、Restful API、FormData、拦截器、 请求取消、Cookie 管理、文件上传/下载、超时、自定义适配器、转换器等。
主要就是初始化设置一下连接超时时间、响应超时时间、api-key以及响应数据类型这些条件
final dio = Dio(BaseOptions(
// 请求连接超时时间
connectTimeout: const Duration(seconds: 10),
// 因为是流式响应,同时ai的回复响应速度也较久,固响应超时时间设置长些,此处设置为180秒
receiveTimeout: const Duration(seconds: 180),
// 设置请求头,其中的api-key要设置为你自己的
headers: {
'Authorization': 'Bearer 这里写你的api-key',
},
// 设置请求头
contentType: 'application/json; charset=utf-8',
// 设置响应类型为流式响应
responseType: ResponseType.stream,
));
构建流式响应的请求
根据官方的文档示例,dio本身就已经是支持stream流式响应的了,一下是官方的示例:
final rs = await dio.get(
url,
options: Options(responseType: ResponseType.stream), // 设置接收类型为 `stream`
);
print(rs.data.stream); // 响应流
由示例可知,要开启流式响应,仅需要设置responseType
为ResponseType.stream
即可,这点我在构建dio实例的时候就已经添加上去了,可以翻上去看看上一个代码块的倒数第二行。
当然,如果你不想在初始化时就设置流式响应,也可以在请求方法中设置,可以参考下面代码中的注释部分。
以下,我们直接构建请求方法并发送请求:
// 发起post请求
var result = await dio.post(
// 请求的url地址,这里使用的是阿里云的H5对接请求api地址
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
data: {
// 请求的模型名称,这些都可以在阿里百练的文档中找到
'model': 'qwen-plus',
// 请求的参数
'messages': [
{
'role': 'user',
'content': '你好',
}
],
// 是否流式响应
'stream': true,
},
/*开启流式响应
options: Options(
responseType: ResponseType.stream,
)
*/
);
stream流式响应处理
根据官方的示例,stream流响应在请求返回值的data内,即result.data.stream
就是我们需要的stream流对象。
我们通过循环遍历该对象就可以逐次取出响应的数据流了。
同时,通过查阅官方文档,可以得知api的响应例子为:
data: {"choices":[{"delta":{"content":"","role":"assistant"},"index":0,"logprobs":null,"finish_reason":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"finish_reason":null,"delta":{"content":"我是"},"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"delta":{"content":"来自"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"delta":{"content":"阿里"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"delta":{"content":"云的大规模语言模型"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"delta":{"content":",我叫通义千问。"},"finish_reason":null,"index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: {"choices":[{"delta":{"content":""},"finish_reason":"stop","index":0,"logprobs":null}],"object":"chat.completion.chunk","usage":null,"created":1715931028,"system_fingerprint":null,"model":"qwen-plus","id":"chatcmpl-3bb05cf5cd819fbca5f0b8d67a025022"}
data: [DONE]
分析例子,可以得知,我们需要对steam流中的每一条进行去掉头部data:
才可以得到真正的json对象,同时还要判断finish_reason
的值以及结果[DONE]
来得知请求响应的结束时间(此时就可以做些保存数据库之类的操作了)。
以下为示例代码
// 用于每个阶段的对话结果
final StringBuffer buffer = StringBuffer();
// 处理流式响应
await for (var data in result.data.stream) {
// 将字节数据解码为字符串
final bytes = data as List<int>;
final decodedData = utf8.decode(bytes);
// 移除 JSON 数据前的额外字符
// 这里因为qwen模型的响应数据每次都以data:开头,后面跟着一个json字符串,所以需要先移除data:
List<String> jsonData = decodedData.split('data: ');
// 移除空字符串
jsonData = jsonData.where((element) => element.isNotEmpty).toList();
// 遍历每个阶段
for (var element in jsonData) {
// 判断是否结束,如果结束则直接返回
if (element == '[DONE]') {
break;
}
try {
// 解析 JSON 数据
final json = jsonDecode(element);
// 获取当前阶段的对话结果,根据qwen模型的对接文档,这里的content就是当前阶段的对话结果,finish_reason表示当前阶段是否结束
final content = json['choices'][0]['delta']['content'] as String;
final finishReason = json['choices'][0]['finish_reason'] ?? '';
if (content.isNotEmpty) {
// 将每次的 content 添加到缓冲区中
buffer.write(content);
// 输出对话结果
print(buffer.toString());
}
if (finishReason == 'stop') {
// 如果 finish_reason 为 stop,则输出完整的对话完成结果
print(buffer.toString());
break;
}
} catch (e) {
print('Error parsing JSON: $e');
// 如果解析失败,可以尝试其他方式处理数据
// 例如,检查是否有其他非标准前缀,并进行相应的处理
}
}
}
最终demo代码:
import 'package:dio/dio.dart';
import 'dart:convert';
void main() async {
// 创建Dio实例
final dio = Dio(BaseOptions(
// 请求连接超时时间
connectTimeout: const Duration(seconds: 10),
// 因为是流式响应,同时ai的回复响应速度也较久,固响应超时时间设置长些,此处设置为180秒
receiveTimeout: const Duration(seconds: 180),
// 设置请求头,其中的api-key要设置为你自己的
headers: {
'Authorization': 'Bearer 这里写你的api-key',
},
// 设置请求头
contentType: 'application/json; charset=utf-8',
// 设置响应类型为流式响应
responseType: ResponseType.stream,
));
// 发起post请求
var asStream = await dio.post(
// 请求的url地址,这里使用的是阿里云的H5对接请求api地址
'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
data: {
// 请求的模型名称,这些都可以在阿里百练的文档中找到
'model': 'qwen-plus',
// 请求的参数
'messages': [
{
'role': 'user',
'content': '你好',
}
],
// 是否流式响应
'stream': true,
},
);
// 处理流式响应
await processStreamResponse(asStream.data.stream);
}
/// 处理流式响应
Future<void> processStreamResponse(Stream stream) async {
// 用于每个阶段的对话结果
final StringBuffer buffer = StringBuffer();
// 处理流式响应
await for (var data in stream) {
// 将字节数据解码为字符串
final bytes = data as List<int>;
final decodedData = utf8.decode(bytes);
// 移除 JSON 数据前的额外字符
// 这里因为qwen模型的响应数据每次都以data:开头,后面跟着一个json字符串,所以需要先移除data:
List<String> jsonData = decodedData.split('data: ');
// 移除空字符串
jsonData = jsonData.where((element) => element.isNotEmpty).toList();
// 遍历每个阶段
for (var element in jsonData) {
// 判断是否结束,如果结束则直接返回
if (element == '[DONE]') {
break;
}
try {
// 解析 JSON 数据
final json = jsonDecode(element);
// 获取当前阶段的对话结果,根据qwen模型的对接文档,这里的content就是当前阶段的对话结果,finish_reason表示当前阶段是否结束
final content = json['choices'][0]['delta']['content'] as String;
final finishReason = json['choices'][0]['finish_reason'] ?? '';
if (content.isNotEmpty) {
// 将每次的 content 添加到缓冲区中
buffer.write(content);
// 输出对话结果
print(buffer.toString());
}
if (finishReason == 'stop') {
// 如果 finish_reason 为 stop,则输出完整的对话完成结果
print(buffer.toString());
break;
}
} catch (e) {
print('Error parsing JSON: $e');
// 如果解析失败,可以尝试其他方式处理数据
// 例如,检查是否有其他非标准前缀,并进行相应的处理
}
}
}
}
最终结果输出
你好
你好!
你好!很高兴
你好!很高兴能
你好!很高兴能为你服务。有什么
你好!很高兴能为你服务。有什么可以帮助你的吗?
你好!很高兴能为你服务。有什么可以帮助你的吗?