关注

android 简易实现一个ai流式问答——流式接收数据与使用JetPack Compose 流式显示内容

背景

实现一个AI聊天问答功能,其中AI的回复是分段返回的。为了给用户带来更好的交互体验,需要设计一个流式读取AI回复的接口,并在UI上实时显示这些分段内容。具体来说,我们需要实现以下功能:

  • 流式接口: 设计一个接口,能够持续从AI模型获取回复内容,并以段落为单位返回。

  • UI流式显示: 在UI界面上,随着新段落内容的获取,实时将这些内容追加到聊天窗口中,营造出AI在持续思考、逐步回答的氛围

流式传输网络设计

APP整体设计大致是

okhttp - retrofit - 自定义网络解析封装层(api) - repository - ViewModel - view

以下关于流式传输的设计,是基于自定义网络解析封装层来实现的

整体架构设计.drawio.png

OkHttp响应的流式读取

当我们使用OkHttp发送网络请求时,服务器返回的数据通常会以流的形式传输。为了高效地处理这些数据,我们可以采用流式读取的方式,即边接收数据边处理,而不是一次性将所有数据读入内存。

为什么不直接使用string()方法?

OkHttp提供的string()方法会将整个响应体一次性读入内存,然后转换为字符串。对于大数据量的场景,这可能会导致内存溢出。而且,这种方式也不符合流式读取的理念。

// string()方法的源码展示了其一次性读取的特性
fun string(): String = source().use { source ->
    source.readString(charset = source.readBomAsCharset(charset()))
}

流式读取的方法

要实现流式读取,我们可以通过byteStream()方法获取到响应体的输入流,然后逐段读取数据。

读取指定字节数

当我们不需要严格按照行来读取数据时,可以采用这种方式:

// 获取输入流
val inputStream = responseBody.byteStream()
val buffer = ByteArray(1024) // 创建一个缓冲区
var len: Int
​
while (inputStream.read(buffer).also { len = it } != -1) {
    // 将读取到的数据转换为字符串
    val data = String(buffer, 0, len)
    // 处理数据
    // ...
}
一行一行读取

当服务器返回的数据是按行组织的(比如JSON数据),我们可以一行一行地读取:

// 获取输入流
val inputStream = responseBody.byteStream()
// 创建BufferedReader
val reader = BufferedReader(InputStreamReader(inputStream))
​
var line: String?
while (reader.readLine().also { line = it } != null) {
    // 处理每一行数据
    // ...
    // 例如,解析JSON
    val type = object : TypeToken<Response<AnswerStreamResponse>>() {}.type
    // 将JSON字符串解析为响应对象
    val response = Gson().fromJson<Response<AnswerStreamResponse>>(line, type)
}

如何呈现给业务层

对于业务层,我们只需要关注三个关键信息:

  • 请求参数: request 是一个 GetAnswerStream 对象,包含了业务逻辑所需的全部信息,无需考虑其他公共参数。

  • 流数据: 每读取到一段流数据后,会调用我们提供的 streamData 函数进行处理。这个函数接收一个 AnswerStreamResponse 对象作为参数,表示当前读取到的数据。

  • 流结束标志: 当所有流数据读取完毕后,会调用 end 函数通知业务层。业务层可以在这个函数中执行一些收尾工作,比如更新状态或释放资源。

具体的方法代码定义如下所示:

suspend fun getAnswer(
        request: GetAnswerStream,
        streamData: (AnswerStreamResponse) -> Unit,
        end: () -> Unit
)

异常处理机制

为了确保系统稳定运行,设计了如下两方面的异常处理机制:

接口返回异常: 当调用外部接口时,如果返回数据中的状态码 code 不等于 0,表示接口调用数据不符合预期,需要额外抛出业务异常给业务层进行处理。例如:

  • 错误提示: 与云端协商不同的错误码对应的消息,当返回相应的错误码后,向用户弹窗对应的错误码消息内容,告知用户异常的原因。这类异常向上层抛出,由调用方处理

  • 日志记录: 将异常信息记录到日志中,方便后续排查。

代码运行时异常: 在代码执行过程中,可能会出现各种未预期的异常,如空指针异常、数组越界等。我们将使用 try-catch 块对这些异常进行捕获,并采取相应的措施:

  • 错误处理: 根据异常类型进行不同的处理,例如记录日志等。

  • 异常抛出: 对于无法处理的异常,可以向上层抛出,由调用方处理。

Compose流式显示文本

通过流式读取网络请求内容,那我们如何流式显示文本内容呢?

在 Compose 中实现流式显示文本,核心在于如何将网络请求返回的实时数据流映射到 UI 组件上,并实时更新 UI。

定义数据模型

首先定义一个要展示UI数据的实体类,其中包括一些基本信息,以及一个最重要的需要流式展示的数据项,该数据类型需要使用MutableState。

什么是 MutableState?

在 Kotlin 中,MutableState 并不是一个内置的数据类型,而是一种状态管理的模式。它通常与Compose结合使用,用于表示一个可变的状态值,并在该值发生变化时触发 UI 的重新渲染。

核心作用:

  • 表示可变状态: MutableState 代表一个可以被修改的值。

  • 触发 UI 更新: 当 MutableState 的值发生变化时,订阅了该状态的 UI 组件会自动重新渲染。

  • 简化状态管理: 提供了一种方便的方式来管理应用程序的状态。

模型定义如下所示:

data class ChatMessage(
    val sessionId: String? = null,
    val messageId: String? = null,
    val message: String?,
    val timestamp: Long,
    val role: String,
){
  var content: MutableState<String> = mutableStateOf("")
}

设置数据

首先在调用getAnswer方法前新建一个ChatMessage对象,并将该对象加入到要显示的UI列表数据中。

接着在给业务层调用的流式方法中的streamData: (AnswerStreamResponse) -> Unit对 content进行修改。

val newMsg =ChatMessage(message = null, timestamp = timestamp, role = Role.Assistant.value, messageId = messageId, sessionId = sessionId)
updateData(newMsg)
getAnswer(
  request = request,
  streamData = {
    it.messageChunks.forEach { data ->
      newMsg.content.value += data.text
    }
  },
  end = {
    isLoading.value = false
    updateBottomState()
    endLoading()
  }
)

UI显示

在Compose中的文本显示控件是Text,向该控件中传入刚刚创建对象的content属性,就可以在streamData方法内对content进行修改时,这里的文本显示会自动刷新。

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_45782795/article/details/143716705

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--