基于 IM UIKit 实现与 AI 数字人聊天
更新时间: 2026/07/10 15:00:10
网易云信在即时通讯 IM 中实现了 AI 数字人聊天功能。本文介绍如何该功能的相关场景、效果以及实现方式。
本文采用 网易云信即时通讯 UIKit(NIM UIKit) 实现,内容适用的开发平台或框架如下所示:
flowchart TD
classDef default fill:#337EFF,stroke:#337EFF,stroke-width:0px,color:#FFFFFF;
A("Android (Java)")
B("iOS (Swift)")
C("Web (JavaScript)")
D("Flutter (Dart)")
E("鸿蒙 (JavaScript)")
功能介绍
AI 聊包含了三种场景,用来覆盖常见的即时通讯聊天形式,分别是:
-
场景一:单独与 AI 数字人聊天(简称 AI 单聊)
用户可以直接与 AI 数字人发起一对一的聊天会话。无论是寻求信息查询、情感陪伴、知识分享还是娱乐互动、实现角色扮演/拟人沟通、AI 客服等场景,AI 数字人都能迅速响应,提供个性化的反馈和服务。
-
场景二:双人聊中 @AI 数字人引出会话(简称 AI 聊)
当两个用户正在进行一对一聊天时,任意一方可以通过艾特(@)AI 数字人的方式,邀请其参与对话。AI 数字人会根据当前聊天的上下文,结合提问回答有用的信息,促进更深层次的交流。
AI 聊 是网易云信即时通讯 IM 的创新功能,终端用户可以在 IM 单聊场景里,直接艾特(@)AI 数字人,快速参与到好友互动中,无需拉群或加好友,以第三人称提供 AI 辅助和聊天互动。
-
场景三:群聊中 @AI 数字人(简称 AI 群聊)
在群聊环境中,AI 数字人同样可以被召唤加入。通过艾特(@)操作,AI 数字人能够理解群聊的主题和氛围,为群组成员提供实时的智能建议、解答疑问或是活跃气氛,成为群聊中的智慧助手,提升整体的沟通效率和娱乐性。用户可以将 AI 数字人拉入群中,进行互动,当然您也可以使用 AI 聊 功能替代。
效果展示
按照 AI 聊的三种场景,预期可实现的效果如下所示:
AI 数字人配置
在实现层面,AI 数字人被视为一种特殊的用户,在网易云信控制台上 添加了 AI 数字人 后,您需要调用服务端 /im/v2/users/:{account_id} 接口,将 AI 数字人的账号 ID(account_id)传入,并在 extension 字段中加入 AI 聊数字人的信息扩展,如下所示,即可被 IM UIKit 识别为 AI 聊数字人。
创建的 AI 数字人有对应的账号 ID,但是该 ID 仅用于识别,无法进行登录。
-
aichat:1 表示该 AI 数字人功能定位为 AI 聊数字人,可以进行聊天或者置顶到会话列表。 -
welcomeText:用户首次进入 AI 数字人聊天页面,AI 数字人发送的欢迎消息。 -
pinDefault:表示该 AI 数字人是否置顶到会话列表中。1 表示置顶,0 表示不置顶。默认为不置顶。 -
ai_stream:表示 AI 数字人是否为流式输出方式。1 表示流式输出,0 表示非流式输出。默认为非流式输出。 -
ai_stream_status:表示 AI 数字人输出状态。1 表示占位,2 表示停止输出,3 表示停止输出并更新 AI 消息,4 表示输出完成,5 表示服务器终止。JSON{ "aiChat": 1, //是否为 AI 聊数字人 "welcomeText": "欢迎使用 AI 聊数字人", //欢迎语 "pinDefault": 1, //是否默认置顶 "ai_stream": 1, //是否为流式输出方式 "ai_stream_status": 4 //输出状态 }
实现流程
客户端整体实现流程如下图所示:
sequenceDiagram
autonumber
IM UIKit ->> IM UIKit: 通过 IM UIKit 拼接参数
IM UIKit ->> IM SDK: 调用 sendMessage 接口
IM SDK ->> IM UIKit: 注册 onReceiveMessage 回调
IM UIKit ->> IM UIKit: 更新客户端 UI
一:通过 IM UIKit 拼接参数
场景 1:与 AI 数字人单聊
单聊时,上下文取值范围为最新的 30 条消息,且:
- 消息类型只能是文本消息、换行消息、回复消息。
- 第一条消息必须是真实用户发送的消息,而非 AI 数字人回复。
- 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Javaprotected List<V2NIMAIModelCallMessage> getAIMessage() {
//上下文 List<V2NIMAIModelCallMessage> aiMessages = new ArrayList<>();
if (AIUserManager.getAIUserById(accountId) != null) {
int size = Math.min(chatView.getMessageList().size(), AI_MESSAGE_SIZE);
// 第一条消息不能是 AI 数字人消息
// 标记是否已经设置过第一条消息 boolean firstSet = false;
for (int i = size; i > 0; i--) {
int index = chatView.getMessageList().size() - i;
ChatMessageBean chatMessageBean = chatView.getMessageList().get(index);
boolean isFromAIUser =
AIUserManager.isAIUser(chatMessageBean.getMessageData().getMessage().getSenderId());
//1 如果第一条是 AI 数字人消息,则不再添加
//2 如果消息已经撤回,则不再添加
//3 如果消息没有服务器 ID,说明不是发出去的消息,则不再添加 if ((!firstSet && isFromAIUser)
|| chatMessageBean.isRevoked()
|| TextUtils.isEmpty(
chatMessageBean.getMessageData().getMessage().getMessageServerId())) {
continue;
}
firstSet = true;
if (!TextUtils.isEmpty(
MessageHelper.getAIContentMsg(chatMessageBean.getMessageData().getMessage()))) {
aiMessages.add(
new V2NIMAIModelCallMessage(
isFromAIUser
? V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_ASSISTANT : V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_USER,
MessageHelper.getAIContentMsg(chatMessageBean.getMessageData().getMessage()),
V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT.getValue()));
}
}
}
return aiMessages;
}
Swift// 基础参数
let params = chatRepo.getSendMessageParams()
// aiUserAccid 为 AI 数字人账号
if let aiAccid = aiUserAccid {
// 从最新的 30 条消息中取上下文
let messageModels = messages.suffix(30)
// 上下文只能为文本消息、换行消息、回复消息
let aiMessageModels = messageModels.filter { $0.type == .text || $0.type == .richText || $0.type == .reply }
var firstUserMessage = false // 是否找到第一条用户发的消息
var aiMessages = [V2NIMAIModelCallMessage]()
for (i, model) in aiMessageModels.enumerated() {
var isUserMessage = false
if model.message?.aiConfig == nil || !((model.message?.aiConfig?.aiStatus != .MESSAGE_AI_STATUS_RESPONSE) == true) {
firstUserMessage = true
isUserMessage = true
}
// 找到第一条用户发送的消息
if !firstUserMessage {
continue
}
let aiMessage = V2NIMAIModelCallMessage()
aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
if isUserMessage {
// 用户发送的消息上下文 role 为 USER
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER
} else {
// AI 数字人响应的消息上下文 role 为 ASSISTANT
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_ASSISTANT
}
if model.type == .text || model.type == .reply {
// 文本消息、回复消息直接使用消息 text 作为上下文
let text = model.message?.text ?? ""
aiMessage.msg = text
} else if model.type == .richText, let m = model as? MessageRichTextModel {
// 换行消息拼接标题和内容作为上下文
let text = (m.titleText ?? "") + (m.message?.text ?? "")
aiMessage.msg = text
}
aiMessages.append(aiMessage)
}
let aiConfig = V2NIMMessageAIConfigParams()
aiConfig.accountId = aiAccid
// 请求大模型的内容
let content = V2NIMAIModelCallContent()
content.msg = text ?? ""
content.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
// 文本消息
if message.messageType == .MESSAGE_TYPE_TEXT, let text = message.text {
aiConfig.content = content
aiConfig.messages = aiMessages
}
// 换行消息
if message.messageType == .MESSAGE_TYPE_CUSTOM,
let type = NECustomUtils.typeOfCustomMessage(message.attachment),
type == customRichTextType {
let title = NECustomUtils.titleOfRichText(message.attachment)
let body = NECustomUtils.bodyOfRichText(message.attachment)
let text = (title ?? "") + (body ?? "")
aiConfig.content = content
aiConfig.messages = aiMessages
}
params.aiConfig = aiConfig
}
JavaScriptprivate _getAIConfig(
msg: V2NIMMessageForUI
): V2NIMMessageAIConfigParams | undefined {
const {
serverExtension,
conversationId,
receiverId,
messageType,
text = '',
} = msg
let serverExt
try {
serverExt = JSON.parse(serverExtension || '{}')
} catch (error) {
serverExt = {}
}
// @ 消息
const yxAitMsg = serverExt.yxAitMsg || {}
// 回复消息
const replyMsg = this.replyMsgs.get(conversationId)
const { relation } = this.rootStore.uiStore.getRelation(receiverId)
const myAccountId = this.rootStore.userStore.myUserInfo.accountId
let aiConfig: V2NIMMessageAIConfigParams | undefined = void 0
if (relation === 'ai') {
if (messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
// 取最近的 30 条文本会话
let _msgs = (this.msgs.get(conversationId) || [])
.slice(-AI_MESSAGE_LIMIT)
.filter(
(item) =>
item.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
)
// 找到第一条自己发的消息,从此时开始作为真正的上下文
const myIndex = _msgs.findIndex((item) => item.senderId === myAccountId)
_msgs = myIndex === -1 ? [] : _msgs.slice(myIndex)
aiConfig = {
accountId: receiverId,
content: { msg: text, type: 0 },
// 取最后 30 条消息作为上下文
messages: _msgs.map((item) => {
const role =
item.senderId === myAccountId
? ('user' as V2NIMAIModelRoleType)
: ('assistant' as V2NIMAIModelRoleType)
return {
role,
msg: item.text || '',
type: 0,
}
}),
}
} else {
aiConfig = {
accountId: receiverId,
}
}
}
// 只保留 @ AI 数字人的 yxAitMsg
const newYxAitMsg = {}
Object.keys(yxAitMsg).forEach((account) => {
if (this.rootStore.aiUserStore.aiUsers.has(account)) {
newYxAitMsg[account] = yxAitMsg[account]
}
})
// 找到最早的 @ AI 数字人
const aiAtAccount = this._findMinStart(newYxAitMsg)
const aiAtMember = this.rootStore.aiUserStore.aiUsers.get(aiAtAccount || '')
// 表示 @ AI 数字人
if (aiAtMember) {
aiConfig = {
accountId: aiAtMember.accountId,
content: { msg: text, type: 0 },
}
}
// 表示此时有回复消息
if (replyMsg && aiConfig) {
// 只有回复的是文本消息需要带上下文,其他类型消息不用带上下文
if (
replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
aiConfig.messages = [
{
role: 'user' as V2NIMAIModelRoleType,
msg: replyMsg.text || '',
type: 0,
},
]
} else {
aiConfig.messages = void 0
}
}
return aiConfig
}
Dartfinal aiMessageSize= 30;
List<NIMAIModelCallMessage?>? getAIMessages() {
int size = min(_messageList.length, aiMessageSize);
List<NIMAIModelCallMessage?>? aiMessages;
if (isAIUser() && _messageList.isNotEmpty) {
aiMessages = <NIMAIModelCallMessage?>[];
//第一条消息不能是数字人消息
// 标记是否已经设置过第一条消息
bool firstSet = false;
for (int index = 0; index < size; index++) {
final message = _messageList[index];
bool isFromAI =
AIUserManager.instance.isAIUser(message.nimMessage.senderId);
//1 如果第一条是数字人消息,则不再添加
//2 如果消息已经撤回,则不再添加
//3 如果消息没有服务器ID,说明不是发出去的消息,则不再添加
//4 如果没有消息内容,则不再添加
if ((!firstSet && !isFromAI) ||
message.isRevoke ||
message.nimMessage.messageServerId?.isNotEmpty != true ||
ChatMessageHelper.getAIContentMsg(message.nimMessage)?.isNotEmpty !=
true) {
continue;
}
firstSet = true;
aiMessages.add(NIMAIModelCallMessage(
type: 0,
msg: ChatMessageHelper.getAIContentMsg(message.nimMessage),
role: isFromAI
? NIMAIModelRoleType.assistant
: NIMAIModelRoleType.user));
}
}
return aiMessages;
}
JavaScriptimport {
V2NIMAIModelCallMessage,
V2NIMAIModelRoleType,
V2NIMMessage,
V2NIMMessageAIStatus,
V2NIMMessageType,
V2NIMMessageAIConfigParams,
V2NIMAIModelCallContent,
V2NIMSendMessageParams
} from '@nimsdk/base'
import {
NEAIUserCache,
IMKitConfigCenter,
getMultiLineTitle,
getMultiLineBody,
isMultiLineMessage
} from '@nimkit/chatkit'
import { NIMMessageInfo } from '../model/NIMMessageInfo'
const AI_CONTEXT_LIMIT: number = 30
// 判断消息是否可作为 AI 上下文(文本消息、换行消息)
function isTextLikeMessageForAI(message: V2NIMMessage): boolean {
if (message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
return true
}
if (message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
return isMultiLineMessage(message)
}
return false
}
// 获取消息内容(文本消息用 text,换行消息拼接标题+内容)
function getAIMessageContent(message: V2NIMMessage): string {
if (message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
return message.text ?? ''
}
if (message.messageType === V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM) {
const title = getMultiLineTitle(message)
const body = getMultiLineBody(message)
if (title.length > 0 && body.length > 0) {
return `${title}${body}`
}
return title || body
}
return ''
}
// 构建 AI 聊天的上下文消息列表(最近 30 条,第一条必须是用户消息)
function buildAIContextMessages(messages: V2NIMMessage[]): V2NIMAIModelCallMessage[] {
const result: V2NIMAIModelCallMessage[] = []
const startIndex = Math.max(messages.length - AI_CONTEXT_LIMIT, 0)
for (let index = messages.length - 1; index >= startIndex; index--) {
const message = messages[index]
// 只取文本消息和换行消息作为上下文
if (!isTextLikeMessageForAI(message)) {
continue
}
const content = getAIMessageContent(message).trim()
if (content.length === 0) {
continue
}
// AI 回复的消息 role 为 ASSISTANT,用户发送的 role 为 USER
const isAIResponse = message.aiConfig?.aiStatus === V2NIMMessageAIStatus.V2NIM_MESSAGE_AI_STATUS_RESPONSE
result.unshift({
role: isAIResponse ? V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_ASSISTANT :
V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_USER,
msg: content,
type: 0
})
}
// 确保第一条消息是用户消息,移除开头的 AI 回复
while (result.length > 0 && result[0].role === V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_ASSISTANT) {
result.shift()
}
return result
}
// 拼接 AI 数字人聊天参数
function buildAIConfigForSingleChat(
message: V2NIMMessage,
aiAccountId: string,
allMessages: V2NIMMessage[],
content?: string
): V2NIMMessageAIConfigParams {
const aiConfig: V2NIMMessageAIConfigParams = {
accountId: aiAccountId,
aiStream: IMKitConfigCenter.enableAIStream
}
// 设置请求大模型的内容
const aiContent = content ?? getAIMessageContent(message)
if (aiContent.length > 0) {
aiConfig.content = {
msg: aiContent,
type: 0
}
}
// 取最近 30 条消息作为上下文
const contextMessages = buildAIContextMessages(allMessages)
if (contextMessages.length > 0) {
aiConfig.messages = contextMessages
}
return aiConfig
}
// 发送消息时附加 aiConfig
function sendMessageWithAIConfig(
message: V2NIMMessage,
conversationId: string,
aiAccountId: string,
allMessages: V2NIMMessage[]
): void {
const aiConfig = buildAIConfigForSingleChat(message, aiAccountId, allMessages)
const params: V2NIMSendMessageParams = {
aiConfig: aiConfig
}
ChatRepo.sendMessage(message, conversationId, params)
}
场景 2:单聊中 @AI 数字人
单聊中艾特(@)数字人要求只能在真实用户一对一单聊中 @ 数字人,且按照用户场景可分为:
- 直接 @ 数字人,此时不需要传入上下文。
- 在回复消息中 @ 数字人,此时取被回复消息作为上下文,且:
- 目前,被回复消息类型只能为文本消息、换行消息。
- 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Javaif (aiUser != null && replyMsg != null && !TextUtils.isEmpty(MessageHelper.getAIContentMsg(replyMsg))) {
aiMessage =
new V2NIMAIModelCallMessage(
V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_USER,
MessageHelper.getAIContentMsg(replyMsg),
0);
}
Swift// 基础参数
let params = chatRepo.getSendMessageParams()
// aiUserAccid 为数字人账号
if let aiAccid = aiUserAccid {
let aiConfig = V2NIMMessageAIConfigParams()
aiConfig.accountId = aiAccid
// 请求大模型的内容
let content = V2NIMAIModelCallContent()
content.msg = text ?? ""
content.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
// replyMessage 为被回复的消息,且消息类型为文本消息
if replyMessage.messageType == .MESSAGE_TYPE_TEXT {
let aiMessage = V2NIMAIModelCallMessage()
aiMessage.msg = replyMessage.text ?? ""
aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER
aiConfig.messages = [aiMessage]
}
// replyMessage 为被回复的消息,且消息类型为换行消息
if replyMessage.messageType == .MESSAGE_TYPE_CUSTOM,
let type = NECustomUtils.typeOfCustomMessage(replyMessage.attachment),
type == customRichTextType {
let title = NECustomUtils.titleOfRichText(replyMessage.attachment)
let body = NECustomUtils.bodyOfRichText(replyMessage.attachment)
let text = (title ?? "") + (body ?? "")
let aiMessage = V2NIMAIModelCallMessage()
aiMessage.msg = text
aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER
aiConfig.messages = [aiMessage]
}
aiConfig.content = content
params.aiConfig = aiConfig
}
JavaScriptprivate _getAIConfig(
msg: V2NIMMessageForUI
): V2NIMMessageAIConfigParams | undefined {
const {
serverExtension,
conversationId,
receiverId,
messageType,
text = '',
} = msg
let serverExt
try {
serverExt = JSON.parse(serverExtension || '{}')
} catch (error) {
serverExt = {}
}
// @ 消息
const yxAitMsg = serverExt.yxAitMsg || {}
// 回复消息
const replyMsg = this.replyMsgs.get(conversationId)
const { relation } = this.rootStore.uiStore.getRelation(receiverId)
const myAccountId = this.rootStore.userStore.myUserInfo.accountId
let aiConfig: V2NIMMessageAIConfigParams | undefined = void 0
if (relation === 'ai') {
if (messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
// 取最近的 30 条文本会话
let _msgs = (this.msgs.get(conversationId) || [])
.slice(-AI_MESSAGE_LIMIT)
.filter(
(item) =>
item.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
)
// 找到第一条自己发的消息,从此时开始作为真正的上下文
const myIndex = _msgs.findIndex((item) => item.senderId === myAccountId)
_msgs = myIndex === -1 ? [] : _msgs.slice(myIndex)
aiConfig = {
accountId: receiverId,
content: { msg: text, type: 0 },
// 取最后 30 条消息作为上下文
messages: _msgs.map((item) => {
const role =
item.senderId === myAccountId
? ('user' as V2NIMAIModelRoleType)
: ('assistant' as V2NIMAIModelRoleType)
return {
role,
msg: item.text || '',
type: 0,
}
}),
}
} else {
aiConfig = {
accountId: receiverId,
}
}
}
// 只保留 @ 数字人的 yxAitMsg
const newYxAitMsg = {}
Object.keys(yxAitMsg).forEach((account) => {
if (this.rootStore.aiUserStore.aiUsers.has(account)) {
newYxAitMsg[account] = yxAitMsg[account]
}
})
// 找到最早的 @ 数字人
const aiAtAccount = this._findMinStart(newYxAitMsg)
const aiAtMember = this.rootStore.aiUserStore.aiUsers.get(aiAtAccount || '')
// 表示 @ 数字人
if (aiAtMember) {
aiConfig = {
accountId: aiAtMember.accountId,
content: { msg: text, type: 0 },
}
}
// 表示此时有回复消息
if (replyMsg && aiConfig) {
// 只有回复的是文本消息需要带上下文,其他类型消息不用带上下文
if (
replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
aiConfig.messages = [
{
role: 'user' as V2NIMAIModelRoleType,
msg: replyMsg.text || '',
type: 0,
},
]
} else {
aiConfig.messages = void 0
}
}
return aiConfig
}
Dart//根据回复消息设置上下文
List<NIMAIModelCallMessage>? aiMessages;
if (aiAgent != null && replyMsg != null) {
final textMsg = ChatMessageHelper.getAIContentMsg(replyMsg);
if (textMsg != null) {
aiMessages = [
NIMAIModelCallMessage(
type: 0, msg: textMsg, role: NIMAIModelRoleType.user)
];
}
}
JavaScript// 回复消息中 @ AI 数字人,取被回复消息作为上下文
function buildAIReplyContextMessage(replyMessage: V2NIMMessage): V2NIMAIModelCallMessage | undefined {
if (!isTextLikeMessageForAI(replyMessage)) {
return undefined
}
const content = getAIMessageContent(replyMessage).trim()
if (content.length === 0) {
return undefined
}
return {
role: V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_USER,
msg: content,
type: 0
}
}
// 回复 @ AI 数字人时构建 aiConfig
function buildAIConfigForReply(
message: V2NIMMessage,
aiAccountId: string,
replyMessage: V2NIMMessage
): V2NIMMessageAIConfigParams {
const aiConfig: V2NIMMessageAIConfigParams = {
accountId: aiAccountId,
aiStream: IMKitConfigCenter.enableAIStream
}
const aiContent = getAIMessageContent(message)
if (aiContent.length > 0) {
aiConfig.content = {
msg: aiContent,
type: 0
}
}
// 以被回复消息作为上下文
const replyContext = buildAIReplyContextMessage(replyMessage)
if (replyContext) {
aiConfig.messages = [replyContext]
}
return aiConfig
}
// 直接 @ AI 数字人(不带上下文),只设置 accountId 和 content
function buildAIConfigForDirectAt(
message: V2NIMMessage,
aiAccountId: string
): V2NIMMessageAIConfigParams {
const aiConfig: V2NIMMessageAIConfigParams = {
accountId: aiAccountId,
aiStream: IMKitConfigCenter.enableAIStream
}
const aiContent = getAIMessageContent(message)
if (aiContent.length > 0) {
aiConfig.content = {
msg: aiContent,
type: 0
}
}
// 直接 @ 不需要传入上下文
return aiConfig
}
场景 3:群聊中 @AI 数字人
在群聊场景下提及或召唤数字人(即 @ 数字人操作)的实现逻辑,与在单一聊天对话中执行相同动作时的机制保持一致。详情请参考 单聊中 @ AI 数字人。
二:发送消息
发送消息的接口采用 sendMessage,详情请参考《即时通讯 IM》消息收发。
发送消息时,您需要在发送接口的 V2NIMSendMessageParams 参数中填入 aiConfig,以此识别为调用大模型(LLMs)的请求。
Javaif (aiAgent != null) {
aiConfigParams = new V2NIMMessageAIConfigParams(aiAgent.getAccountId());
if (!TextUtils.isEmpty(MessageHelper.getAIContentMsg(message))) {
V2NIMAIModelCallContent content =
new V2NIMAIModelCallContent(MessageHelper.getAIContentMsg(message), 0);
aiConfigParams.setContent(content);
}
}
//AI 消息上下文设置
if (aiConfigParams != null && aiMessage != null && !aiMessage.isEmpty()) {
aiConfigParams.setMessages(aiMessage);
}
if (aiConfigParams != null) {
paramsBuilder.withAIConfig(aiConfigParams);
}
ChatRepo.sendMessage(
message,
conversationId,
paramsBuilder.build(),
new ProgressFetchCallback<>(){
//...
};
Swift// params 为消息发送相关参数,类型为 V2NIMSendMessageParams
ChatRepo.shared.sendMessage(message: message,
conversationId: conversationId,
params: params) { result, error, pro in
completion(result?.message ?? message, error, pro)
}
JavaScriptprivate _getAIConfig(
msg: V2NIMMessageForUI
): V2NIMMessageAIConfigParams | undefined {
const {
serverExtension,
conversationId,
receiverId,
messageType,
text = '',
} = msg
let serverExt
try {
serverExt = JSON.parse(serverExtension || '{}')
} catch (error) {
serverExt = {}
}
// @ 消息
const yxAitMsg = serverExt.yxAitMsg || {}
// 回复消息
const replyMsg = this.replyMsgs.get(conversationId)
const { relation } = this.rootStore.uiStore.getRelation(receiverId)
const myAccountId = this.rootStore.userStore.myUserInfo.accountId
let aiConfig: V2NIMMessageAIConfigParams | undefined = void 0
if (relation === 'ai') {
if (messageType === V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT) {
// 取最近的 30 条文本会话
let _msgs = (this.msgs.get(conversationId) || [])
.slice(-AI_MESSAGE_LIMIT)
.filter(
(item) =>
item.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
)
// 找到第一条自己发的消息,从此时开始作为真正的上下文
const myIndex = _msgs.findIndex((item) => item.senderId === myAccountId)
_msgs = myIndex === -1 ? [] : _msgs.slice(myIndex)
aiConfig = {
accountId: receiverId,
content: { msg: text, type: 0 },
// 取最后 30 条消息作为上下文
messages: _msgs.map((item) => {
const role =
item.senderId === myAccountId
? ('user' as V2NIMAIModelRoleType)
: ('assistant' as V2NIMAIModelRoleType)
return {
role,
msg: item.text || '',
type: 0,
}
}),
}
} else {
aiConfig = {
accountId: receiverId,
}
}
}
// 只保留 @ AI 数字人的 yxAitMsg
const newYxAitMsg = {}
Object.keys(yxAitMsg).forEach((account) => {
if (this.rootStore.aiUserStore.aiUsers.has(account)) {
newYxAitMsg[account] = yxAitMsg[account]
}
})
// 找到最早的 @ AI 数字人
const aiAtAccount = this._findMinStart(newYxAitMsg)
const aiAtMember = this.rootStore.aiUserStore.aiUsers.get(aiAtAccount || '')
// 表示 @ AI 数字人
if (aiAtMember) {
aiConfig = {
accountId: aiAtMember.accountId,
content: { msg: text, type: 0 },
}
}
// 表示此时有回复消息
if (replyMsg && aiConfig) {
// 只有回复的是文本消息需要带上下文,其他类型消息不用带上下文
if (
replyMsg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
) {
aiConfig.messages = [
{
role: 'user' as V2NIMAIModelRoleType,
msg: replyMsg.text || '',
type: 0,
},
]
} else {
aiConfig.messages = void 0
}
}
return aiConfig
}
Dart//根据回复消息设置上下文
List<NIMAIModelCallMessage>? aiMessages;
if (aiAgent != null && replyMsg != null) {
final textMsg = ChatMessageHelper.getAIContentMsg(replyMsg);
if (textMsg != null) {
aiMessages = [
NIMAIModelCallMessage(
type: 0, msg: textMsg, role: NIMAIModelRoleType.user)
];
}
}
JavaScriptimport { ChatRepo } from '@nimkit/chatkit'
import {
V2NIMMessage,
V2NIMSendMessageParams,
V2NIMSendMessageResult
} from '@nimsdk/base'
// 发送消息,在 V2NIMSendMessageParams 中填入 aiConfig
async function sendMessage(
message: V2NIMMessage,
conversationId: string,
params?: V2NIMSendMessageParams
): Promise<void> {
try {
const result: V2NIMSendMessageResult | undefined =
await ChatRepo.sendMessage(message, conversationId, params)
// AI 消息发送成功后提示
if (result && result.message.aiConfig) {
// 可在 UI 层显示 "AI 数字人处理中" 的提示
}
} catch (err) {
// 发送失败处理
}
}
// 完整示例:发送一条文本消息给 AI 数字人
// aiConfig 由上述场景方法构建后传入 params
// const aiConfig = buildAIConfigForSingleChat(message, aiAccountId, allMessages)
// const params: V2NIMSendMessageParams = { aiConfig: aiConfig }
// await sendMessage(message, conversationId, params)
三:收到消息
发送消息的接口采用 onReceiveMessages 回调事件,详情请参考《即时通讯 IM》消息收发。
收到消息后,如果消息的 aiConfig 不为空且 aiConfig.aiStatus 为 2,则为 AI 数字人响应的消息。
Java@Override
public void onReceiveMessages(@NonNull List<IMMessageInfo> messages) {
//...
}
Swift/// 收到消息回调
/// - Parameter messages: 消息列表
public func onReceive(_ messages: [V2NIMMessage]) {
for msg in messages {
// AI 数字人回复的消息
if msg.aiConfig != nil,
msg.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE {
...
}
}
}
JavaScriptnim.V2NIMMessageService.on('onReceiveMessages', () => {
//...
})
Dart //new message
subscriptions.add(
NimCore.instance.messageService.onReceiveMessages.listen((event) async {
}));
JavaScriptimport { ChatRepo } from '@nimkit/chatkit'
import { V2NIMMessage, V2NIMMessageAIStatus } from '@nimsdk/base'
// 注册消息接收监听
ChatRepo.onReceiverMessage((messages: V2NIMMessage[]) => {
for (const message of messages) {
// AI 数字人响应的消息:aiConfig 不为空,且 aiStatus 为 RESPONSE
if (message.aiConfig &&
message.aiConfig.aiStatus === V2NIMMessageAIStatus.V2NIM_MESSAGE_AI_STATUS_RESPONSE) {
// 这是 AI 数字人回复的消息,更新 UI 显示
// 如果是流式输出,aiConfig.aiStreamStatus 还有以下状态:
// V2NIM_MESSAGE_AI_STREAM_STATUS_STREAMING - 正在流式输出
// V2NIM_MESSAGE_AI_STREAM_STATUS_GENERATED - 输出完成
// V2NIM_MESSAGE_AI_STREAM_STATUS_STOPPED - 已停止
// V2NIM_MESSAGE_AI_STREAM_STATUS_UPDATED - 已更新替换
// V2NIM_MESSAGE_AI_STREAM_STATUS_ABORTED - 服务端终止
}
}
})




