flutter+dart+dio对接ai聊天接口,并处理stream流式响应数据,实现ai逐字回复效果

导语:本文以对接阿里百炼平台的模型为例,展示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); // 响应流

由示例可知,要开启流式响应,仅需要设置responseTypeResponseType.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');
        // 如果解析失败,可以尝试其他方式处理数据
        // 例如,检查是否有其他非标准前缀,并进行相应的处理
      }
    }
  }
}

最终结果输出

你好
你好!
你好!很高兴
你好!很高兴能
你好!很高兴能为你服务。有什么
你好!很高兴能为你服务。有什么可以帮助你的吗?
你好!很高兴能为你服务。有什么可以帮助你的吗?
实验目的: 1. 掌握Flutter应用程序开发的基本流程和技巧。 2. 熟悉FlutterDart语言的基本语法和应用。 3. 了解Flutter的UI组件和布局,以及它们在应用程序中的实际应用。 4. 学习如何使用FlutterDart来调用摄像头和文件系统API,实现拍照功能。 5. 学习如何使用FlutterDart处理图像数据,以及如何在应用程序中显示和保存图像。 实验内容: 本次实验的主要内容是使用FlutterDart语言开发一个拍照应用程序,程序的主要功能包括: 1. 调用摄像头API,实现拍照功能。 2. 显示拍摄的照片,并支持手势缩放和旋转。 3. 保存拍摄的照片到本地文件系统中。 以下是实验的具体步骤: 1. 创建一个新的Flutter项目,使用Flutter提供的UI组件和布局来实现应用程序的UI设计。 2. 在应用程序中添加一个按钮,用于触发拍照功能。 3. 在应用程序中调用摄像头API,实现拍照功能,并将拍摄的照片数据保存到内存中。 4. 在应用程序中显示拍摄的照片,并支持手势缩放和旋转。您可以使用Flutter提供的GestureDetector和Transform组件来实现这些功能。 5. 在应用程序中将拍摄的照片保存到本地文件系统中。您可以使用Flutter提供的dart:io库来实现这个功能。 在完成上述步骤后,您可以在模拟器或实际设备上运行应用程序,并测试它的功能。如果需要,您还可以对应用程序进行进一步的优化和改进。 总之,本次实验将帮助您掌握FlutterDart语言的基本知识和技巧,以及如何使用它们来开发实际应用程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值