引言
在智能对话应用中,为了实现实时响应,可以使用流式响应技术,即服务端逐条发送数据流事件,使得客户端可以即时渲染并呈现消息,减少等待时间。
本篇文章的重点是利用rcp模块实现流式响应,相比轮询请求对用户更加友好,也更难一点。
主要技术及介绍
Coze api 返回结构
我使用的是coze平台搭建的智能体的api,其他平台大概差不多: (。
在流式响应中,服务端不会一次性发送所有数据,而是以数据流的形式逐条发送数据给客户端,数据流中包含对话过程中触发的各种事件(event),直至处理完毕或处理中断。处理结束后,服务端会通过 conversation.message.completed 事件返回拼接后完整的模型回复信息。各个事件的说明可参考流式响应事件。
流式响应允许客户端在接收到完整的数据流之前就开始处理数据,例如在对话界面实时展示智能体的回复内容,减少客户端等待模型完整回复的时间。
扣子api返回响应示例:
event:conversation.chat.created
data:{"id":"7436978328604753946","conversation_id":"7436978328604737562","bot_id":"7421457483137105931","created_at":1731556459,"last_error":{"code":0,"msg":""},"status":"created","usage":{"token_count":0,"output_count":0,"input_count":0},"section_id":"7436978328604737562"}
event:conversation.chat.in_progress
data:{"id":"7436978328604753946","conversation_id":"7436978328604737562","bot_id":"7421457483137105931","created_at":1731556459,"last_error":{"code":0,"msg":""},"status":"in_progress","usage":{"token_count":0,"output_count":0,"input_count":0},"section_id":"7436978328604737562"}
event:conversation.message.delta
data:{"id":"7436978350943387685","conversation_id":"7436978328604737562","bot_id":"7421457483137105931","role":"assistant","type":"answer","content":"您好","content_type":"text","chat_id":"7436978328604753946","section_id":"7436978328604737562"}
event:conversation.message.delta
data:{"id":"7436978350943387685","conversation_id":"7436978328604737562","bot_id":"7421457483137105931","role":"assistant","type":"answer","content":"呀","content_type":"text","chat_id":"7436978328604753946","section_id":"7436978328604737562"}
还有更多...
| 参数 | 类型 | 说明 |
|---|---|---|
| event | String | 当前流式返回的数据包事件。详细说明可参考 流式响应事件。 |
| data | Object | 消息内容。其中,chat 事件和 message 事件的格式不同。- chat 事件中,data 为 Chat Object。- message 事件中,data 为 Message Object。 |
服务端按数据流形式逐条发送内容。各事件分别触发,如conversation.chat.created、conversation.message.delta、conversation.message.completed等,客户端根据事件类型逐步处理数据。
鸿蒙 rcp 请求模块
据说和axios更像,比http更好?
OnDataReceive 请求回调
实现的一个重点是创建回调.在回调中实时接收数据处理数据
const customHttpEventsHandler: rcp.HttpEventsHandler = {
onDataReceive: (incomingData: ArrayBuffer) => {
//接收buffer数据流,转换格式,处理...
const tracingConfig: rcp.TracingConfiguration = {
httpEventsHandler: customHttpEventsHandler
}; //这是什么,看不懂,文档就这么写的~
//将回调作为请求对象的参数
const session = rcp.createSession({ requestConfiguration: { tracing: tracingConfig } });
//发送请求
session.fetch(request).then((rep: rcp.Response) => {
//没什么用了
实现流程
1. 请求配置
准备请求头,请求体数据。 不同api要求的参数格式不一样。
const chatRequest: ChatRequest = {
bot_id: '742145748313***你的bot id',
user_id: '114514',
stream: true, //开启流式响应
auto_save_history: true,
additional_messages: [
{
role: 'user',
content: text, //函数传入的参数
content_type: 'text'
}
]
};
const url = "https://api.coze.cn/v3/chat";
const request = new rcp.Request(url, "POST", {
'Authorization': 'Bearer pat_3O7LumMTJwaW*****密钥',
'Content-Type': 'application/json'
}, chatRequest);
2. 创建回调函数,实时处理二进制流数据
通过textDecoder.decodeToString将二进制流转成字符串,再转成对象
因为返回的每份结构还有换行,所以看起来很麻烦(这部分由claude-3.5-sonnet By cursor生成😎)
@State rspData: string = '' //状态变量,不必多说
@State buffer: string = ''; // 用于存储未处理完的数据
const customHttpEventsHandler: rcp.HttpEventsHandler = {
onDataReceive: (incomingData: ArrayBuffer) => {
const textDecoder = util.TextDecoder.create('utf-8');
const chunk = textDecoder.decodeToString(new Uint8Array(incomingData));
// 将新数据追加到buffer
this.buffer += chunk;
// 按行处理数据
const lines = this.buffer.split('\n');
// 保留最后一个可能不完整的行
this.buffer = lines[lines.length - 1];
// 处理完整的行
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (!line) {
continue;
}
console.info('114514收到原始数据:', line);
if (line.startsWith('data:')) {
try {
const jsonStr = line.slice(5); // 移除 'data:' 前缀
if (jsonStr === '[DONE]') {
console.info('114514流式响应完成');
continue;
}
const data: Message = JSON.parse(jsonStr);
// 处理delta消息
if (data.content && data.type === 'answer') {
this.rspData += data.content;
this.chatHistory[this.chatHistory.length - 1] = {
content: this.rspData,
type: 'bot'
} as ChatHistory;
console.info('114514当前累积内容:', this.rspData);
}
} catch (e) {
console.error('114514解析错误:', e);
}
}
}
return incomingData.byteLength;
},
onDataEnd: () => {
// Custom logic for handling data transfer completion
this.rspData = ''
console.info('114514' + "请求结束");
},
};
const tracingConfig: rcp.TracingConfiguration = {
httpEventsHandler: customHttpEventsHandler
};
3. 创建请求对象,发起请求
// Use the configuration in the session creation
const session = rcp.createSession({ requestConfiguration: { tracing: tracingConfig } });
session.fetch(request).then((rep: rcp.Response) => {
console.info('114514' + `Response succeeded: ${rep}`);
}).catch((err: BusinessError) => {
console.error('114514' + `Response err: Code is ${err.code}, message is ${JSON.stringify(err)}`);
});
4. 可扩展内容
会话流中不同内容类型触发不同的事件。例如知识库召回、工具调用(function_call)等事件,可以通过判断type字段处理特定内容,之后有时间写一下~🤪。
演示
简单演示一下,界面由ai生成😿💔
代码
代码仅用于验证流式输出功能实现,界面随便写的😇😇
import { rcp } from '@kit.RemoteCommunicationKit';
import { promptAction, window } from '@kit.ArkUI';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { ChatRequest, Message } from '../../Model/DataModel';
import { util } from '@kit.ArkTS';
@Builder
function Rcp_StreamViewBuilder() {
NavDestination() {
Rcp_StreamView()
}
.hideTitleBar(false)
}
interface ChatHistory {
content: string
type: 'user' | 'bot'
}
@Entry
@Component
export struct Rcp_StreamView {
aboutToAppear(): void {
// window.getLastWindow(getContext())
// .then((win) => {
// win.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE)
// })
}
@State rspData: string = ''
@State buffer: string = ''; // 用于存储未处理完的数据
@State chatHistory: ChatHistory[] = []
@State inputText: string = '' // 新增输入框的内容状态
scroller: Scroller = new Scroller()
StartChat = async (text: string): Promise<void> => {
const chatRequest: ChatRequest = {
bot_id: '742960328318我是蔡徐坤',
user_id: '114514',
stream: true,
auto_save_history: true,
additional_messages: [
{
role: 'user',
content: text,
content_type: 'text'
}
]
};
const url = "https://api.coze.cn/v3/chat";
const request = new rcp.Request(url, "POST", {
'Authorization': 'Bearer pat_3O7LumMTJwaWLX4f我是蔡徐坤',
'Content-Type': 'application/json'
}, chatRequest);
const customHttpEventsHandler: rcp.HttpEventsHandler = {
onDataReceive: (incomingData: ArrayBuffer) => {
const textDecoder = util.TextDecoder.create('utf-8');
const chunk = textDecoder.decodeToString(new Uint8Array(incomingData));
// 将新数据追加到buffer
this.buffer += chunk;
// 按行处理数据
const lines = this.buffer.split('\n');
// 保留最后一个可能不完整的行
this.buffer = lines[lines.length - 1];
// 处理完整的行
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (!line) {
continue;
}
console.info('114514收到原始数据:', line);
if (line.startsWith('data:')) {
try {
const jsonStr = line.slice(5); // 移除 'data:' 前缀
if (jsonStr === '[DONE]') {
console.info('114514流式响应完成');
continue;
}
const data: Message = JSON.parse(jsonStr);
// 处理delta消息
if (data.content && data.type === 'answer') {
this.rspData += data.content;
this.chatHistory[this.chatHistory.length - 1] = {
content: this.rspData,
type: 'bot'
} as ChatHistory;
console.info('114514当前累积内容:', this.rspData);
}
} catch (e) {
console.error('114514解析错误:', e);
}
}
}
return incomingData.byteLength;
},
onDataEnd: () => {
// Custom logic for handling data transfer completion
this.rspData = ''
console.info('114514' + "请求结束");
},
};
const tracingConfig: rcp.TracingConfiguration = {
httpEventsHandler: customHttpEventsHandler
};
// Use the configuration in the session creation
const session = rcp.createSession({ requestConfiguration: { tracing: tracingConfig } });
session.fetch(request).then((rep: rcp.Response) => {
console.info('114514' + `Response succeeded: ${rep}`);
}).catch((err: BusinessError) => {
console.error('114514' + `Response err: Code is ${err.code}, message is ${JSON.stringify(err)}`);
});
}
build() {
Column() {
// 聊天内容区域
Scroll(this.scroller) {
Column() {
// AI回复气泡
ForEach(this.chatHistory, (item: ChatHistory, index) => {
if (item.type == 'bot') {
Row() {
Image($r('app.media.app_icon'))
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 10 })
.syncLoad(true)
Text(item.content)
.fontSize(16)
.backgroundColor('#FFFFFF')
.padding(12)
.borderRadius(12)
.margin({ right: 50 })
}
.width('100%')
.margin({ top: 10, bottom: 10 })
.alignItems(VerticalAlign.Top)
} else {
Row() {
Text(item.content)
.fontSize(16)
.backgroundColor('#ffc2ffb4')
.padding(12)
.borderRadius(12)
.margin({ right: 12 })
Image($r('app.media.app_icon'))
.width(40)
.height(40)
.borderRadius(20)
.margin({ right: 10 })
.syncLoad(true)
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 10, bottom: 10 })
.alignItems(VerticalAlign.Top)
}
})
}
.width('100%')
.padding({ left: 16, right: 16 })
}
.align(Alignment.Top)
.layoutWeight(1)
.width('100%')
.backgroundColor('#F5F5F5')
// 底部输入区域
Row() {
// 输入框
TextInput({ placeholder: '请输入消息...', text: $$this.inputText })
.fontSize(16)
.backgroundColor('#FFFFFF')
.borderRadius(20)
.padding({ left: 16, right: 16 })
.height(40)
.layoutWeight(1)
.onSubmit(async () => {
if (!this.inputText.trim()) {
promptAction.showToast({ message: '请输入内容喵~' })
return
}
// TODO: 更新chatRequest中的content
await this.StartChat(this.inputText)
this.chatHistory.push({ content: this.inputText, type: 'user' })
this.chatHistory.push({ content: '思考中...', type: 'bot' })
this.inputText = '' // 发送后清空输入框
this.scroller.scrollEdge(Edge.Bottom)
})
// 发送按钮
Button('发送', { type: ButtonType.Normal })
.width(80)
.height(40)
.margin({ left: 12 })
.borderRadius(20)
.backgroundColor('#007AFF')
.onClick(async () => {
if (!this.inputText.trim()) {
promptAction.showToast({ message: '请输入内容喵~' })
return
}
// TODO: 更新chatRequest中的content
await this.StartChat(this.inputText)
this.chatHistory.push({ content: this.inputText, type: 'user' })
this.chatHistory.push({ content: '思考中...', type: 'bot' })
this.inputText = '' // 发送后清空输入框
this.scroller.scrollEdge(Edge.Bottom)
})
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
}
.width('100%')
.height('100%')
}
}
// 定义请求的数据结构
export interface AdditionalMessage {
role: 'user' | 'bot';
content: string;
content_type: 'text' | 'image' | 'video';
}
export interface ChatRequest {
bot_id: string;
user_id: string;
stream: boolean;
auto_save_history: boolean;
additional_messages: AdditionalMessage[];
}
export interface Message {
id: string;
conversation_id: string;
bot_id: string;
chat_id: string;
role: 'user' | 'assistant';
content: string;
content_type: 'text' | 'object_string' | 'card';
created_at: number;
updated_at: number;
type: 'question' | 'answer' | 'function_call' | 'tool_output' | 'tool_response' | 'follow_up' | 'verbose';
}