背景
实现一个AI聊天问答功能,其中AI的回复是分段返回的。为了给用户带来更好的交互体验,需要设计一个流式读取AI回复的接口,并在UI上实时显示这些分段内容。具体来说,我们需要实现以下功能:
-
流式接口: 设计一个接口,能够持续从AI模型获取回复内容,并以段落为单位返回。
-
UI流式显示: 在UI界面上,随着新段落内容的获取,实时将这些内容追加到聊天窗口中,营造出AI在持续思考、逐步回答的氛围
流式传输网络设计
APP整体设计大致是
okhttp - retrofit - 自定义网络解析封装层(api) - repository - ViewModel - view
以下关于流式传输的设计,是基于自定义网络解析封装层来实现的
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