开发一个 AI 分身应用
收藏我的收藏
教程目标
通过本教程,可以快速了解开发一个 AI 分身应用的全流程。
前置准备
参考 AI 分身应用接入指南中指引。
- •创建一个抖音开放平台账号。
- •创建一个 AI 分身应用。
- •了解 IDE 开发和抖音云基础功能。
整体流程
一个简单的应用开发
AI 分身应用是基于大语言模型和知识库等 AI 能力为某一场景提供拟人化的解决方案。
服务端接口
在项目开发中,我们需要实现以下几个接口。
- •开场白(必须):
- ◦path:/avatar/serv/onboarding
- ◦Http method:post
- ◦开场白
- •流式对话(必须)
- ◦path:/avatar/serv/chatstream
- ◦Http method:post
- ◦流式对话
- •调试健康检查接口
- ◦path:/ping(调试期必须)
- ◦Http method:get
- ◦resp:实现即可
开场白接口
功能:用户首次或者一定时间后与 AI 分身聊天时,会触发给用户介绍一下本 AI 分身擅长的场景和功能,并以流式传递返回。
// OnBoarding . // @router /onboarding [POST] @PostMapping(value = "/avatar/serv/onboarding") public void OnBoarding(@RequestBody OnBoardingRequestVo onBoardingRequestVo, HttpServletResponse resp) { // 验证参数,可根据业务自行判断 // 将用户入参数复制,复制DTO OnBoardingDTO onBoardingDTO = new OnBoardingDTO(); BeanUtils.copyProperties(onBoardingRequestVo, onBoardingDTO); // 调用服务 onBoardingService.OnBoarding(onBoardingDTO, resp); }
具体实现可参考:
public class OnBoardingServiceImpl implements IOnBoardingService { private static final Logger logger = LoggerFactory.getLogger(OnBoardingServiceImpl.class); @Override public void OnBoarding(OnBoardingDTO onBoardingRequestDTO, HttpServletResponse resp) { // 开场白 demo, 可根据业务自行调整。 List<String> respString = new ArrayList<>(); respString.add("你好,"); respString.add("我是赵小小,"); respString.add("两性情感"); respString.add("专家,"); respString.add("您有什么"); respString.add("想咨询"); respString.add("的问题"); // 初始化一个chunk发送器 ChunkSendWriter<HttpResponse> chunkSendWriter = new ChunkSendWriter<>(); // 循环这个demo数据向调用方发送数据 for (int idx = 0; idx < respString.size(); idx++) { try { // 组装返回数据 OnBoardingResponseDTO onBoardingResponseDTO = new OnBoardingResponseDTO(); onBoardingResponseDTO.setStreamFinish(false); CopilotContent copilotContent = new CopilotContent(); copilotContent.setContentType(ContentTypeEnum.TEXT); copilotContent.setContent(respString.get(idx)); copilotContent.setRoleEnum(RoleEnum.Assistant); copilotContent.setSegFinish(false); if (idx == respString.size() - 1) { copilotContent.setSegFinish(true); } copilotContent.setSegType(SegTypeEnum.ANSWER); onBoardingResponseDTO.setCopilotContent(copilotContent); onBoardingResponseDTO.setBaseResp(new BaseResp()); // 发送 chunkSendWriter.Send(resp, HttpResponse.SuccessResponse(onBoardingResponseDTO)); // 模拟休眠 Thread.sleep(200); } catch (Exception e) { logger.error("OnBoarding.send error={}", e.getMessage(), e); } } // 定义一些sug类型的数据demo List<String> sugList = new ArrayList<>(); sugList.add("异地恋该如何长期维护稳定?"); sugList.add("女朋友长期不回答消息,表明了什么?"); sugList.add("如何给暗恋多年的女神表白?"); // 发送 for (int idx = 0; idx < sugList.size(); idx++) { try { // 组装返回数据 OnBoardingResponseDTO onBoardingResponseDTO = new OnBoardingResponseDTO(); onBoardingResponseDTO.setStreamFinish(false); CopilotContent copilotContent = new CopilotContent(); copilotContent.setContentType(ContentTypeEnum.TEXT); copilotContent.setContent(sugList.get(idx)); copilotContent.setRoleEnum(RoleEnum.Assistant); copilotContent.setSegFinish(false); if (idx == sugList.size() - 1) { copilotContent.setSegFinish(true); onBoardingResponseDTO.setStreamFinish(true); } copilotContent.setSegType(SegTypeEnum.FOLLOWUP); onBoardingResponseDTO.setCopilotContent(copilotContent); onBoardingResponseDTO.setBaseResp(new BaseResp()); // 发送 chunkSendWriter.Send(resp, HttpResponse.SuccessResponse(onBoardingResponseDTO)); Thread.sleep(200); } catch (Exception e) { logger.error("OnBoarding.send error={}", e.getMessage(), e); } } try { chunkSendWriter.EndWriteChunked(resp); } catch (Exception e) { logger.error("OnBoarding.send error={}", e.getMessage(), e); } } }
在 IDE 启动调试的时候,右侧对话便能出现开场白
流式对话
功能:用户与 AI 分身聊天时,用户的消息会流入该接口,并以使用该接口的返回值作为输出。在本接口中,开发者可以调用大模型,记忆等能力来丰富输出给到用户。在本 demo 中仅调用了大模型相关。其余能力详细查看下面的章节。
@PostMapping(value = "/avatar/serv/chatstream") public void ChatStream(@RequestBody ChatStreamRequestVo chatStreamRequestVo, HttpServletResponse response) { // 验证参数, 根据业务自行判断 // 参数复制 ChatStreamDTO chatStreamDTO = new ChatStreamDTO(); BeanUtils.copyProperties(chatStreamRequestVo, chatStreamDTO); // 调用服务 chatStreamService.ChatStream(chatStreamDTO, response); }
大模型配置:
在IDE开发中,IDE会将大模型环境变量以及抖音云服务的环境变量自动注入到IDE的运行环境中,开发者可以在代码中通过获取环境变量的方式获取 大模型的接入点。其他大模型接入点环境变量的名称,可以参考抖音云大模型接入点列表界面的相关说明。在抖音云开通大模型后,将接入点ID通过环境变量的方式获取后填入到Model_EP中,此时即可开始访问,否则会导致调用大模型失败。
public static final String MODEL_EP = "";
具体能力实现:调用记忆能力和豆包大模型的流式接口进行问答。
// ChatStream [POST] 流式对话接口 @Service public class ChatStreamServiceImpl implements IChatStreamService { private static final Logger logger = LoggerFactory.getLogger(ChatStreamServiceImpl.class); @Autowired private AIModelApiRestTemplate aiModelApiRestTemplate; @Autowired private MemoryApiRestTemplate memoryApiRestTemplate; @Override public void ChatStream(ChatStreamDTO chatStreamDTO, HttpServletResponse resp) { // 1. 调用一下长期记忆能力, demo List<String> longHistory = memoryApiRestTemplate.CallMemoryApiRestMethod(chatStreamDTO); // 2. 获取用户的输入 ChatCompletionRequest streamChatCompletionRequest = ChatCompletionRequest.builder() .model(GlobalPromptConfig.MODEL_EP) .messages(ChatContentUtil.FromMessages(chatStreamDTO.getMessage(), chatStreamDTO.getChatContext(), longHistory)) .stream(true) .build(); // 初始化回调处理器 ChatStreamChunkHandle chatStreamChunkHandle = new ChatStreamChunkHandle(new ChunkSendWriter<>()); AIModelConsumer aiModelConsumer = new AIModelConsumer(chatStreamDTO, resp, chatStreamChunkHandle); // 调用大模型, 流式 aiModelApiRestTemplate.CallAIModelRestStream(streamChatCompletionRequest, aiModelConsumer); } }
能力使用
外域接口调用
功能说明:外域接口是开发者在开发 AI 分身应用的时候,有从外部服务器获取用户数据的诉求,就可以实现该功能调用获取外域数据。
// 调用请求函数,这个函数是专门请求外域的统一入口, 可根据自己的要求调整 ExtendApiResponse extendApiResponse = extendApiRestTemplate.CallExtendApiRestMethod(apiID, requestBody); // 调用外部的api接口 public ExtendApiResponse CallExtendApiRestMethod(String apiUrl, String reqContent) { // 调用Post方法 try { Map<String, String> headMap = new HashMap<>(); headMap.put("Content-Type", "application/json"); // 拼接请求链接 ExtendApiRequest extendApiRequest = new ExtendApiRequest(); extendApiRequest.setProxyType(1); extendApiRequest.setApiID(apiUrl); extendApiRequest.setReqBody(reqContent); String responseBody = this.PostCallMethod(EXTEND_API_URL, JSON.toJSONString(extendApiRequest), headMap); return JSON.parseObject(responseBody, ExtendApiResponse.class); } catch (Exception e) { logger.error("CallExtendApiRestMethod 调用异常, error={}", e.getMessage(), e); return null; } }
通过 session 查会话记录
private static final String MEMORY_API_URL = "http://open-ai-byted-org.dyc.ivolces.com/dy_open_api/avatar/atomic/api/recall_memory/"; // 调用记忆召回接口, demo List<String> longHistory = memoryApiRestTemplate.CallMemoryApiRestMethod(chatStreamDTO); // 调用长期记忆的api接口 public List<String> CallMemoryApiRestMethod(ChatStreamDTO chatStreamDTO) { List<String> resultList = new ArrayList<>(); MemoryRecallRequest memoryRecallRequest = new MemoryRecallRequest(); try { // 1. 封装请求参数 MemoryRecallParams memoryRecallParams = new MemoryRecallParams(); memoryRecallParams.setDsl(null); memoryRecallParams.setQuery(chatStreamDTO.getMessage().getMessageContent().getContent()); memoryRecallParams.setLimit(10); // 根据自己的业务代码去组装请求下游获取不同会话的历史数据 AvatarRequestInfo avatarRequestInfo = new AvatarRequestInfo(); avatarRequestInfo.setBizID(chatStreamDTO.getBizContext().getBizID()); avatarRequestInfo.setTrafficSource(chatStreamDTO.getCommonContext().getTrafficSource()); avatarRequestInfo.setOpenID(chatStreamDTO.getCommonContext().getUserInfo().getOpenID()); avatarRequestInfo.setAvatarAppID(chatStreamDTO.getCommonContext().getAvatarInfo().getAvatarAppID()); avatarRequestInfo.setTenantID("ebtest_1"); avatarRequestInfo.setProviderID("ebtest_1"); memoryRecallRequest.setParams(memoryRecallParams); memoryRecallRequest.setRequestInfo(avatarRequestInfo); // 2. 发起api请求逻辑 Map<String, String> headMap = new HashMap<>(); headMap.put("Content-Type", "application/json"); String responseBody = this.PostCallMethod(MEMORY_API_URL, memoryRecallRequest, headMap); MemoryRecallHttpResponse memoryRecallHttpResponse = JSON.parseObject(responseBody, MemoryRecallHttpResponse.class); logger.info("CallExtendApiRestMethod memoryRecallHttpResponse: {}", JSON.toJSONString(memoryRecallHttpResponse)); if (memoryRecallHttpResponse == null) { return resultList; } if (memoryRecallHttpResponse.getData() == null || memoryRecallHttpResponse.getData().getMemories() == null) { return resultList; } for (Memory data : memoryRecallHttpResponse.getData().getMemories()) { resultList.add(data.getContent()); } logger.info("CallExtendApiRestMethod resultList: {}", resultList); return resultList; } catch (Exception e) { logger.error("CallExtendApiRestMethod 调用异常, error={}", e.getMessage(), e); return resultList; } }
常见问题
启动调试失败,如何解决?
[抖音云] 本地代理容器启动失败,失败原因: 本地代理容器健康检查失败
原因:代理容器是根据调试流量动态唤起的,如果长时间未进行测试调用,代理容器资源会被回收。此时再次启动调试时,会触发实例开始启动,提示:"本地代理容器健康检查失败。"
解决办法:稍等1min左右,重新启动调试。
当前小程序下未启动本地调试功能,请前往[抖音云控制台] -> [服务列表] -> [本地调试] 打开本地调试开关
原因:使用调试功能时需要在抖音云控制台打开本地调试按钮。
解决办法:前往抖音云控制台,在「服务列表」-> 「本地调试」打开本地调试开关。
其余启动失败问题排查
- 1.正常情况下启动成功后,打开
Docker Desktop
软件,会发现有如下两个容器正在运行(状态为Running),分别为dycloud-local-proxy
与ai-app-local-debug
:- 2.如果启动失败,上述两个容器状态应该是非 Running,此时可以将这两个容器勾选,然后删除掉。重新启动调试;如果仍然失败,点击容器 Name 一栏,获取容器日志,截图提交技术工单解决。
- 3.如需访问Mysql,Redis,MongoDB请在Dockerfile.debug文件内增加,代码中通过环境变量的方式来访问,在抖音云Dev和Prod部署时会自动注入这几个环境变量
ENV REDIS_PASSWORD=请自行输入对应密码信息 ENV REDIS_ADDRESS=dycloud-local-proxy:6379 ENV MYSQL_PASSWORD=请自行输入对应密码信息 ENV MYSQL_ADDRESS=dycloud-local-proxy:3306 ENV MYSQL_USERNAME= ENV REDIS_PASSWORD=请自行输入对应密码信息 ENV MONGO_ADDRESS=dycloud-local-proxy:3717 ENV MONGO_PASSWORD=请自行输入对应密码信息
对话失败
修改 endpointid,默认模板中没有 endpointid。