输入关键词搜索,支持 AI 答疑

基于 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 聊的四种场景,预期可实现的效果如下所示:

AI 单聊
AI 单聊.png
AI 聊
AI 聊.png
AI 群聊
AI 群聊.png
AI 助聊
AI 助聊.png

准备工作

添加数字人

根据本文操作前,请确保您已经完成了以下设置:

配置数字人

在实现层面,数字人被视为一种特殊的用户,在网易云信控制台上添加了数字人后,您需要调用服务端 /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 客户端接口调用如下所示:

实现流程

客户端整体实现流程如下图所示:

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 数字人资源才能在应用中有效地使用该功能。

Android
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();
  }
});
iOS
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
})
Web
TypeScriptconst aiUserList = await nim.V2NIMAIService.getAIUserList()
macOS/Windows
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 条消息,且:

  • 消息类型只能是文本消息、换行消息、回复消息。
  • 第一条消息必须是真实用户发送的消息,而非数字人回复。
  • 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Android
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;
}
iOS
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;
}
Web
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',
  }
})
macOS/Windows
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 聊

单聊中艾特(@)数字人要求只能在真实用户一对一单聊中 @ 数字人,且按照用户场景可分为:

  • 直接 @ 数字人,此时不需要传入上下文。
  • 在回复消息中 @ 数字人,此时取被回复消息作为上下文,且:
    • 目前,被回复消息类型只能为文本消息、换行消息。
    • 如果是换行消息(标题+内容),则拼接标题和内容作为上下文。
Android
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);
}
iOS
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
    }
}
Web
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',
  }
})
macOS/Windows
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 数字人的回复结果会以回调的形式异步返回。
Android
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
  }
});
iOS
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)
    }
}
Web
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: '温文尔雅,知书达理的郡主(明朝王爷的小女儿),精通历史,是个古代文学小百科',
    }),
})
macOS/Windows
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
    });
此文档是否对你有帮助?
有帮助
去反馈
  • 业务场景
  • 在线 Demo
  • 效果展示
  • 准备工作
  • 添加数字人
  • 配置数字人
  • 相关接口
  • 实现流程
  • 第一步:获取配置列表
  • 第二步:实现聊天场景
  • 场景一:AI 单聊
  • 场景二:AI 聊
  • 场景三:AI 群聊
  • 场景四:AI 助聊