当前位置:   article > 正文

AI探索实践10 - Typescript开发AI应用2:前端实现本地模型流式响应输出_前端接收流式输出

前端接收流式输出

大家好,我是feng,感谢你阅读我的博文,如果你也关注AI应用开发,欢迎关注公众号和我一起​探索。如果文章对你有所启发,请为我点赞!

在前一篇博文《AI探索实践9 - 不用Python!用前端也能开发一个本地运行的“ChatGPT”!》,实现了仅依靠前端程序,就可以和本地大模型通信,实现了类似ChatGPT聊天的效果。但是在运行程序后,我发现本地模型的响应是一次性显示的,并没有像ChatGPT那样实现打字机的效果。本文就来研究如何实现流式输出的效果。

一、重点回顾

在前端程序中,前端和大模型的通信的主要逻辑是这两行代码:

  1. const response = await chatModel.invoke(prompt.value as string);
  2. console.log(response);
  3. addMessage(response.content as string, '/images/robot.png', false);

从代码中,可以明白chatModel.invoke方法,是接收一个提示语参数,并返回一个大模型的响应。来看看控制台下的这个响应内容:

这是一个类型为AIMessage的对象,我们获取这个返回对象的content属性,就可以得到大模型响应的结果。

二、一次提交多个提示语

大多时候,我们都会尽可能的充分利用大模型的性能以给用户更好的体验。LangChain支持一次性提交多个提示语,代码如下。

  1. const response = await chatModel.batch(['你好吗?', '你能做什么事情?']);
  2. console.log(response);

batch方法,接收一个数组。其中每一个参数为一个提示语。来看看响应:

可以看出,当你一次性发出多个提示语时,大模型会返回一个消息数组,对应问题的多个回答。

三、流式请求与响应

3.1 流式请求

LangChain还提供了一种数据通信方式:流式。代码如下:

  1. const response = await chatModel.stream(prompt.value as string);
  2. console.log(response);

stream方法,返回一个流对象。通过控制台看看实际返回的数据:

3.2 流式响应

返回的是一个 IterableReadableStream 类型的异步对象。来看一下 stream的定义:

stream(input: RunInput, options?: Partial<CallOptions>): Promise<IterableReadableStream<RunOutput>>;

3.3 解析流式响应

由于响应对象是一个异步的可读流集合,因此我们可以使用for循环来读取每次的流响应,为了处理异步,我们需要加上关键字 await。

  1. const response = await chatModel.stream(prompt.value as string);
  2. console.log(response);
  3. for await (const chunk of response) {
  4. console.log(chunk.content);
  5. }

来观察控制台下打印的模型响应:

可以看到,每一行是大模型返回的一个流响应的内容。我们将这个动作增加到页面上,即可实现ChatGPT打字机的效果。

四、实现页面流式响应(打字机效果)

4.1 实现思路

在模板中,显示的对话数据来自于消息数组。每发出或者收到响应消息,都会在数组尾部增加一个消息对象,利用Vue的双向绑定特性,模版中会自动增加最新的消息。

由于流式响应,是对一个回答内容进行多次的补充。从结果上看,是一个回答内容。但是从过程上看,却是多条回答内容。因此如果流式回答也按照上面的方法更新模板(往数组尾部追加消息),就会在页面上显示出多个回答的效果。这显然不是我们想要的。

这里提供一个方法,逻辑实现要点如下,供大家参考。

  1. 模板上设置一个 默认的机器人回答消息框,默认是隐藏的
  2. 消息绑定到一个字符串变量,比如: answer。
  3. 消息发出时,显示 默认的机器人回答消息框
  4. 当收到流式响应数据时,循环响应流对象,并将每次响应流对象内容追加到 answer后。
  5. 当接收全部流式响应数据后
    1. 隐藏 默认的机器人回答消息框。
    2. 将本次回答的完整内容 即 answer的值,增加到消息数组中。
    3. 重置answer的值为空字符串

4.2 升级代码逻辑

  1. const isAnswering = ref(false);
  2. const answer = ref('');
  3. async function sent() {
  4. isAnswering.value = true;
  5. addMessage(prompt.value, '/images/man.png', true);
  6. const response = await chatModel.stream(prompt.value as string)
  7. console.log(response);
  8. for await (const chunk of response) {
  9. answer.value += chunk.content;
  10. console.log(chunk.content);
  11. }
  12. isAnswering.value = false;
  13. addMessage(answer.value, '/images/robot.png');
  14. prompt.value = '';
  15. answer.value = '';
  16. }

4.3 升级模板代码

  1. <template>
  2. <div class="demo full-height column q-pa-md">
  3. <div class="chat-dialog flex justify-center ">
  4. <q-scroll-area class="full-height full-width" :thumb-style="GlobalThumbStyle">
  5. <div style="width: 100%;min-height: 400px; padding-right: 10px;">
  6. <q-chat-message v-for="(msg, idx) in messages" :key="idx" :avatar="msg.avatar" :text="msg.text"
  7. :sent="msg.sent" class="q-mb-lg" />
  8. <div v-if="isAnswering">
  9. <q-chat-message avatar="/images/robot.png" :text="[answer]" />
  10. </div>
  11. </div>
  12. </q-scroll-area>
  13. </div>
  14. <div class="fixed-bottom flex justify-center">
  15. <div class="chat-prompt-dialog q-pa-md">
  16. <q-input v-model="prompt" borderless @keyup.enter="sent">
  17. <template v-slot:append>
  18. <q-icon name="mdi-send" class="cursor-pointer" @click="sent" />
  19. </template>
  20. </q-input>
  21. </div>
  22. </div>
  23. </div>
  24. </template>

4.4 最终效果

从这个动图可以看到,前端程序完全可以做到在和本地大模型通信时,为用户提供更好的体验。

五、总结

文本主要是记录如何用前端技术,实现流式响应(打字机)的一种方法。通过使用前端流请求与响应的处理方法,设计必要的变量来控制数据读取和显示逻辑,来实现设计目标。

代码在用户体验方面还存在不足。不如大模型第一次回答还存在卡顿现象、模型的默认回答框在体验上也差强人意。不过这些都可以通过前端技术的优化来增强用户体验。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
  

闽ICP备14008679号