基于 IM SDK 实现与 AI 数字人聊天
更新时间: 2024/11/18 16:54:19
网易云信在即时通讯 IM 中实现了 AI 数字人聊天功能。本文介绍了 AI 数字人聊天功能的相关场景、效果以及在不同平台项目即时通讯应用中的实现方式。
本文采用 网易云信即时通讯 SDK(NIM SDK) 实现,内容适用的开发平台或框架如下所示:
flowchart TD
classDef default fill:#337EFF,stroke:#337EFF,stroke-width:0px,color:#FFFFFF;
A("Android (Java)")
B("iOS (Swift)")
C("macOS/Windows (C++)")
D("Web/uni-app/小程序 (JavaScript)")
业务场景
与 AI 数字人聊天提供了四种场景,用来覆盖常见的即时通讯聊天形式,分别是:
- AI 单聊:单独与 AI 数字人聊天。用户可以直接与 AI 数字人发起一对一的聊天会话。无论是寻求信息查询、情感陪伴、知识分享还是娱乐互动、实现角色扮演/拟人沟通、AI 客服等场景,AI 数字人都能迅速响应,提供个性化的反馈和服务。
- AI 聊:双人聊中 @AI 数字人引出会话。当两个用户正在进行一对一聊天时,任意一方可以通过艾特(@)AI 数字人的方式,邀请其参与对话。AI 数字人会根据当前聊天的上下文,结合提问回答有用的信息,促进更深层次的交流。
AI 聊 是网易云信即时通讯 IM 的创新功能,终端用户可以在 IM 单聊场景里,直接艾特(@)AI 数字人,快速参与到好友互动中,无需拉群或加好友,以第三人称提供 AI 辅助和聊天互动。 - AI 群聊:群聊中 @AI 数字人。在群聊环境中,AI 数字人同样可以被召唤加入。通过艾特(@)操作,AI 数字人能够理解群聊的主题和氛围,为群组成员提供实时的智能建议、解答疑问或是活跃气氛,成为群聊中的智慧助手,提升整体的沟通效率和娱乐性。用户可以将数字人拉入群中,进行互动,当然您也可以使用 AI 聊 功能替代。
- AI 助聊:预设 AI 聊天提示词选项。通过 NIM SDK 代理接口,结合聊天对方的用户角色属性和 IM 聊天上下文,为用户推荐聊天话题和措词,为用户提供表达建议。
在线 Demo
您可以前往 融合通讯 + AI 场景功能体验 App 体验相关功能。
效果展示
按照 AI 聊的四种场景,预期可实现的效果如下所示:
准备工作
添加数字人
根据本文操作前,请确保您已经完成了以下设置:
-
在 网易云信控制台 上创建至少一个应用。详细步骤请参考 创建应用并获取 AppKey。
-
为您创建的应用,添加一个数字人。详细步骤请参考 开通并添加数字人。
配置数字人
在实现层面,数字人被视为一种特殊的用户,在网易云信控制台上添加了数字人后,您需要调用服务端 /im/v2/users/:{account_id}
接口,将数字人的账号 ID(account_id
)传入,并在 extension
字段中加入 AI 聊数字人的信息扩展,如下所示,即可被 IM UIKit 识别为 AI 聊数字人。
-
aichat
:1 表示该数字人功能定位为 AI 聊数字人,可以进行聊天或者置顶到会话列表。 -
welcomeText
:用户首次进入 AI 数字人聊天页面,AI 数字人发送的欢迎消息。 -
pinDefault
:表示该 AI 数字人是否置顶到会话列表中。1 表示置顶,0 表示不置顶。默认为不置顶。JSON
{ "aiChat": 1, //是否为 AI 聊数字人 "welcomeText": "欢迎使用 AI 聊数字人", //欢迎语 "pinDefault": 1 //是否默认置顶 }
相关接口
本文涉及的 NIM SDK 客户端接口调用如下所示:
- sendMessage:本端发送消息。
- getAIUserList:批量查询 AI 数字人列表。
- onReceiveMessages:消息相关监听器。
- proxyAIModelCall:向 LLM(Large Language Models)发起模型调用请求。
- onProxyAIModelCall:AI 透传接口的响应的回调。
- createTextMessage:创建一条文本消息。
实现流程
客户端整体实现流程如下图所示:
sequenceDiagram
autonumber
participant Dev as App 客户端
participant nim as 网易云信 IM SDK
Dev ->> nim: 通过 IM SDK 获取 AI 数字人列表
Note right of nim: 异步操作
nim -->> Dev: IM SDK 异步回调返回 AI 数字人列表
Dev ->> nim: 通过 IM SDK 初始化发送消息时的 AI 数字人配置入参
Dev ->> nim: 发送消息<br>接口:sendMessage<br>类名:V2NIMMessageService
Note right of nim: 处理消息发送
nim -->> Dev: 回调 onReceiveMessages<br>类名:V2NIMMessageListener
Note right of Dev: 处理接收到的消息
Dev ->> nim: 通过 IM SDK 初始化 AI 数字人代理请求入参
Dev ->> nim: AI 数字人代理接口<br>接口:proxyAIModelCall<br>类名:V2NIMAIService
Note right of nim: 处理 AI 数字人代理请求
nim -->> Dev: 回调 onProxyAIModelCall<br>类名:V2NIMAIListener
Note right of Dev: 处理 AI 数字人代理响应
Dev ->> Dev: 更新客户端 UI
Note right of Dev: 显示最新状态
第一步:获取配置列表
在客户端获取您配置的 AI 数字人列表。这是实现 AI 数字人聊天功能的基础步骤,您需要知道可用的 AI 数字人资源才能在应用中有效地使用该功能。
JavaNIMClient.getService(V2NIMAIService.class).getAIUserList(new V2NIMSuccessCallback<List<V2NIMAIUser>>() {
@Override
public void onSuccess(List<V2NIMAIUser> v2NIMAIUsers) {
//get ai users success
saveAIUsers(v2NIMAIUsers);
}
}, new V2NIMFailureCallback() {
@Override
public void onFailure(V2NIMError error) {
// get ai users error
int code = error.getCode();
}
});
SwiftNIMSDK.shared().v2AIService.getAIUserList({ result in
// get ai users success
if let users = result
{
self.saveAIUsers(users)
}
}, failure: { (error: V2NIMError) in
// get ai users error
let code = error.code
})
TypeScriptconst aiUserList = await nim.V2NIMAIService.getAIUserList()
C++auto& aiService = v2::V2NIMClient::get().getAIService();
aiService.getAIUserList(
[=](nstd::vector<nstd::shared_ptr<V2NIMAIUser>> result) {
// success, handle result
for (auto& user : result)
std::cout << user->accountId.c_str() << std::endl;
},
[=](v2::V2NIMError error) {
// failed, handle error
});
第二步:实现聊天场景
场景一:AI 单聊
单聊时,上下文取值范围为最新的 30 条消息,且:
- 消息类型只能是文本消息、换行消息、回复消息。
- 第一条消息必须是真实用户发送的消息,而非数字人回复。
- 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Java/**
* 发送消息
* @param text 消息内容
* @param aiUser AI 数字人
* @param aiMessageContexts 发送给 AI 数字人的消息上下文
*/
public void sendMessageToAIUser(String text,V2NIMAIUser aiUser,List<V2NIMMessage> aiMessageContexts){
String conversationId = V2NIMConversationIdUtil.p2pConversationId(aiUser.getAccountId());
V2NIMMessage message = V2NIMMessageCreator.createTextMessage(text);
V2NIMSendMessageParams params = V2NIMSendMessageParamsBuilder.builder()
.withAIConfig(getAIConfigParams(aiUser,aiMessageContexts))
.build();
NIMClient.getService(V2NIMMessageService.class).sendMessage(message, conversationId, params, new V2NIMSuccessCallback<V2NIMSendMessageResult>() {
@Override
public void onSuccess(V2NIMSendMessageResult v2NIMSendMessageResult) {
//send message to ai user success
V2NIMMessage msg = v2NIMSendMessageResult.getMessage();
}
}, new V2NIMFailureCallback() {
@Override
public void onFailure(V2NIMError error) {
// send message to ai user error
int code = error.getCode();
}
},null);
}
/**
* 设置消息 AI 配置参数
* @param aiUser AI 数字人
* @param aiMessageContexts 发送给 AI 数字人的消息上下文
* @return
*/
protected V2NIMMessageAIConfigParams getAIConfigParams(V2NIMAIUser aiUser,List<V2NIMMessage> aiMessageContexts) {
V2NIMMessageAIConfigParams aiConfigParams = new V2NIMMessageAIConfigParams(aiUser.getAccountId());
//上下文消息列表
List<V2NIMAIModelCallMessage> aiMessages = new ArrayList<>();
// AI_MESSAGE_SIZE = 30
int size = Math.min(aiMessageContexts.size(), AI_MESSAGE_SIZE);
//第一条消息不能是数字人消息
// 标记是否已经设置过第一条消息
boolean firstSet = false;
for (int i = size; i > 0; i--) {
int index = aiMessageContexts.size() - i;
V2NIMMessage message = aiMessageContexts.get(index);
boolean isFromAIUser = TextUtils.equals(aiUser.getAccountId(),message.getSenderId());
//1 如果第一条是数字人消息,则不再添加
//2 如果消息没有服务器 ID,说明不是发出去的消息,则不再添加
if ((!firstSet && isFromAIUser)
|| TextUtils.isEmpty(message.getMessageServerId())) {
continue;
}
firstSet = true;
aiMessages.add(
new V2NIMAIModelCallMessage(
isFromAIUser ? V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_ASSISTANT: V2NIMAIModelRoleType.V2NIM_AI_MODEL_ROLE_TYPE_USER,
message.getText(),
V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT.getValue()));
}
aiConfigParams.setMessages(aiMessages);
//如果 V2NIMAIUser 中的 modelConfig.prompt 定义了变量,则必填 promptVariables
//JSON 格式的字符串,本字段的键来自于 V2NIMAIUser.modelConfig 里的 promptKeys 属性
//String promptVariables = getPromptVariables(aiUser);
//aiConfigParams.setPromptVariables(promptVariables);
return aiConfigParams;
}
Swift/**
* 发送消息
* @param text 消息内容
* @param aiUser AI 数字人
* @param aiMessageContexts 发送给 AI 数字人的消息上下文
*/
func sendMessageToAIUser(text: String, aiUser: V2NIMAIUser, aiMessageContexts: [V2NIMMessage])
{
let conversationId = V2NIMConversationIdUtil.p2pConversationId(aiUser.accountId ?? "") ?? ""
let message = V2NIMMessageCreator.createTextMessage(text)
let params = V2NIMSendMessageParams()
params.aiConfig = self.getAIConfigParams(aiUser, aiMessageContexts)
NIMSDK.shared().v2MessageService.send(message, conversationId: conversationId, params: params) { result in
// send message to ai user success
let message = result.message
} failure: { error in
// send message to ai user error
let code = error.code
}
}
/**
* 设置消息 AI 配置参数
* @param aiUser AI 数字人
* @param aiMessageContexts 发送给 AI 数字人的消息上下文
* @return
*/
func getAIConfigParams(_ aiUser: V2NIMAIUser, _ aiMessageContexts: [V2NIMMessage]) -> V2NIMMessageAIConfigParams
{
let aiConfigParams = V2NIMMessageAIConfigParams()
aiConfigParams.accountId = aiUser.accountId
// 从最新的 30 条消息中取上下文
// AI_MESSAGE_SIZE = 30
let messageModels = aiMessageContexts.suffix(AI_MESSAGE_SIZE)
// 只支持文本消息
let aiMessageModels = messageModels.filter({message in
return message.messageType == V2NIMMessageType.MESSAGE_TYPE_TEXT
})
//第一条消息不能是数字人消息
// 标记是否已经设置过第一条消息
var firstSet = false
var aiMessages = [V2NIMAIModelCallMessage]()
for (i, message) in aiMessageModels.enumerated() {
let isFromAIUser = aiUser.accountId == message.senderId
//1 如果第一条是数字人消息,则不再添加
//2 如果消息没有服务器 ID,说明不是发出去的消息,则不再添加
if ((!firstSet && isFromAIUser) || message.messageServerId?.count ?? 0 <= 0)
{
continue;
}
firstSet = true
let aiMessage = V2NIMAIModelCallMessage()
if isFromAIUser {
// 数字人响应的消息上下文 role 为 ASSISTANT
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_ASSISTANT
} else {
// 用户发送的消息上下文 role 为 USER
aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER
}
aiMessage.msg = message.text ?? ""
aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
aiMessages.append(aiMessage)
}
aiConfigParams.messages = aiMessages
//如果 V2NIMAIUser 中的 modelConfig.prompt 定义了变量,则必填 promptVariables
//JSON 格式的字符串,本字段的键来自于 V2NIMAIUser.modelConfig 里的 promptKeys 属性
//String promptVariables = [self getPromptVariables:aiUser];
//params.promptVariables = promptVariables
return aiConfigParams;
}
TypeScriptlet message = nim.V2NIMMessageCreator.createTextMessage('hello world')
const conversationId = nim.V2NIMConversationIdUtil.p2pConversationId('AI_ACCOUND_ID')
message = await nim.V2NIMMessageService.sendMessage(message, conversationId, {
aiConfig: {
accountId: 'AI_ACCOUND_ID',
}
})
C++auto& messageService = v2::V2NIMClient::get().getMessageService();
auto message = v2::V2NIMMessageCreator::createTextMessage("Hello");
auto conversationId = v2::V2NIMConversationIdUtil::p2pConversationId("AI user account ID");
v2::V2NIMMessageAIConfigParams aiConfig;
aiConfig.accountId = "AI user account";
v2::V2NIMSendMessageParams sendMessageParams;
sendMessageParams.aiConfig = aiConfig;
messageService.sendMessage(
*message, conversationId, sendMessageParams,
[=](v2::V2NIMSendMessageResult result) {
// succeed, handle result.
},
[=](v2::V2NIMError error) {
// failed, handle error.
},
nullptr);
场景二:AI 聊
单聊中艾特(@)数字人要求只能在真实用户一对一单聊中 @ 数字人,且按照用户场景可分为:
- 直接 @ 数字人,此时不需要传入上下文。
- 在回复消息中 @ 数字人,此时取被回复消息作为上下文,且:
- 目前,被回复消息类型只能为文本消息、换行消息。
- 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Java/**
* 发送消息
* @param conversationId 会话 ID
* @param text 消息内容
* @param aiUser AI 数字人
* @param aiMessageContexts 发送给 AI 数字人的消息上下文
*/
public void sendMessageAtAIUser(String conversationId,String text,V2NIMAIUser aiUser,List<V2NIMMessage> aiMessageContexts){
V2NIMMessage message = V2NIMMessageCreator.createTextMessage(text);
V2NIMSendMessageParams params = V2NIMSendMessageParamsBuilder.builder()
.withAIConfig(getAIConfigParams(aiUser,aiMessageContexts))
.build();
NIMClient.getService(V2NIMMessageService.class).sendMessage(message, conversationId, params, new V2NIMSuccessCallback<V2NIMSendMessageResult>() {
@Override
public void onSuccess(V2NIMSendMessageResult v2NIMSendMessageResult) {
//send message success
V2NIMMessage msg = v2NIMSendMessageResult.getMessage();
}
}, new V2NIMFailureCallback() {
@Override
public void onFailure(V2NIMError error) {
// send message error
int code = error.getCode();
}
},null);
}
Swiftfunc sendMessageAtAIUser(conversationId:String, text: String, aiUser: V2NIMAIUser, aiMessageContexts: [V2NIMMessage])
{
let message = V2NIMMessageCreator.createTextMessage(text)
let params = V2NIMSendMessageParams()
params.aiConfig = self.getAIConfigParams(aiUser, aiMessageContexts)
NIMSDK.shared().v2MessageService.send(message, conversationId: conversationId, params: params) { result in
// send message to ai user success
let message = result.message
} failure: { error in
// send message to ai user error
let code = error.code
}
}
TypeScriptlet message = nim.V2NIMMessageCreator.createTextMessage('hello world')
const conversationId = nim.V2NIMConversationIdUtil.p2pConversationId('OTHER_ACCOUND_ID')
message = await nim.V2NIMMessageService.sendMessage(message, conversationId, {
aiConfig: {
accountId: 'AI_ACCOUND_ID',
}
})
C++auto& messageService = v2::V2NIMClient::get().getMessageService();
auto message = v2::V2NIMMessageCreator::createTextMessage("Hello");
auto conversationId = v2::V2NIMConversationIdUtil::p2pConversationId("Other account ID");
v2::V2NIMMessageAIConfigParams aiConfig;
aiConfig.accountId = "AI user account";
v2::V2NIMSendMessageParams sendMessageParams;
sendMessageParams.aiConfig = aiConfig;
messageService.sendMessage(
*message, conversationId, sendMessageParams,
[=](v2::V2NIMSendMessageResult result) {
// succeed, handle result.
},
[=](v2::V2NIMError error) {
// failed, handle error.
},
nullptr);
场景三:AI 群聊
在群聊场景下提及或召唤数字人(即 @ 数字人操作)的实现逻辑,与在单一聊天对话中执行相同动作时的机制保持一致。详情请参考 单聊中 @ AI 数字人。
场景四:AI 助聊
在 AI 助聊聊天过程中,IM SDK 封装了携带有聊天消息上下文的请求,再调用大语言模型代理接口:
- 在每次会话消息更新时,请求 AI 数字人,为用户提供聊天回复建议。再结合 UI 提示,无需打字用户点击即可发送消息,避免会话沉默。
- 请求回复成功后,AI 数字人的回复结果会以回调的形式异步返回。
Java//注册异步回调
NIMClient.getService(V2NIMAIService.class).addAIListener(new V2NIMAIListener() {
@Override
public void onProxyAIModelCall(V2NIMAIModelCallResult result) {
//proxy ai model call result
int code = result.getCode();
if(code != 200){
//proxy ai model call error
}else{
//proxy ai model call success
//AI 数字人账号 ID
String accountId = result.getAccountId();
// 本次响应的标识,用来和发送请求做匹配
String requestId = result.getRequestId();
// 本地响应的回复内容
V2NIMAIModelCallContent content = result.getContent();
}
}
});
//AI 数字人账号 ID
String aiUserAccountId = "AI User Account";
// 本次请求的唯一标识,响应会携带此标识,用于匹配请求和响应
String requestId = getUUid();
// 本地请求的内容
V2NIMAIModelCallContent content = new V2NIMAIModelCallContent("request content",0);
V2NIMProxyAIModelCallParams params = new V2NIMProxyAIModelCallParams(aiUserAccountId,requestId,content);
NIMClient.getService(V2NIMAIService.class).proxyAIModelCall(params, new V2NIMSuccessCallback<Void>() {
@Override
public void onSuccess(Void unused) {
//proxy ai model call success
}
}, new V2NIMFailureCallback() {
@Override
public void onFailure(V2NIMError error) {
//proxy ai model call failed
}
});
Swiftlet instance = SampleCodeAIListener()
instance.addListener()
instance.proxyAIModelCall(aiUserAccountId: "AI User Account", requestContent: "request content")
Swiftclass SampleCodeAIListener: NSObject, V2NIMAIListener
{
func onProxyAIModelCall(_ data: V2NIMAIModelCallResult)
{
// proxy ai model call result
if(data.code != 200)
{
// proxy ai model call error
} else
{
// proxy ai model call success
// AI 数字人账号 ID
let accountId = data.accountId;
// 本次响应的标识,用来和发送请求做匹配
let requestId = data.requestId;
// 本地响应的回复内容
let content = data.content;
}
}
func proxyAIModelCall(aiUserAccountId: String, requestContent: String)
{
let requestId = UUID().uuidString
let content = V2NIMAIModelCallContent()
content.msg = requestContent
content.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT
let params = V2NIMProxyAIModelCallParams()
params.accountId = aiUserAccountId
params.requestId = requestId
params.content = content
NIMSDK.shared().v2AIService.proxyAIModelCall(params) {
// proxy ai model call success
} failure: { error in
// proxy ai model call failed
}
}
func addListener()
{
NIMSDK.shared().v2AIService.add(self)
}
func removeListener()
{
NIMSDK.shared().v2AIService.remove(self)
}
}
TypeScript// 监听 AI 助聊事件
nim.V2NIMAIService.on('onProxyAIModelCall', (response) => {
const resultString = result.content.msg || '';
结果字符串的格式为: 【msg1】【msg2】【msg3】【msg4】...
const msgList = resultString
.split('【')
.filter((str: string) => str)
.map((str: string) => str.split('】')[0]);
// msgList 为 AI 助聊返回的提示词列表
console.log(msgList)
})
// 发送 AI 助聊请求
await nim.V2NIMAIService.proxyAIModelCall({
// 账号信息,默认:'aizhuliao'
accountId: 'aizhuliao',
// 请求流水号,随机唯一即可
requestId: btoa(`${new Date().valueOf()}`),
// 对方发送的最后一条消息内容,没有则默认为:'您好,您是?'
content: {
msg: lastMsg?.msg || '您好,您是?',
type: 0, // 默认为:0
},
// 最近的 20 条聊天记录
messages: validatedMsgList,
// 对方特征信息
promptVariables: JSON.stringify({
// 姓名
name: '金辰郡主',
// 性别
sex: '女生',
// 年龄
age: '20 岁',
// 兴趣
hobby: '兴趣爱好:古筝、下棋、书法、历史',
// 性格
characteristic: '温文尔雅,知书达理的郡主(明朝王爷的小女儿),精通历史,是个古代文学小百科',
}),
})
C++V2NIMAIListener aiListener;
aiListener.onProxyAIModelCall = [=](V2NIMAIModelCallResult response) {
// handle response
};
auto& aiService = v2::V2NIMClient::get().getAIService();
aiService.addAIListener(aiListener);
V2NIMProxyAIModelCallParams proxyAIModelCallParams;
proxyAIModelCallParams.accountId = "AI user account ID";
proxyAIModelCallParams.requestId = "Generate a request UUID";
proxyAIModelCallParams.content.msg = "Hello";
proxyAIModelCallParams.content.type = 0;
aiService.proxyAIModelCall(
proxyAIModelCallParams,
[=]() {
// success
},
[=](v2::V2NIMError error) {
// failed, handle error
});