1

本人有幸在业务当中实现了一个ai聊天会话界面,因此在这里总结下实现的经验。

原理1: 如何实现机器人的流式回复

流式回复的效果就像是打字效果一样,当然与打字效果也有差异,那就是打字效果是一个字一个字的接替出现,而流式效果就是在打字效果的基础上一句话或者一个段落那样接替出现。

要想实现这个效果,那么就需要请求服务端,服务端通过发消息返回给前端,这就不得不提到fetchEventSource api了。

fetchEventSource 是一个用于处理 Server-Sent Events(SSE)的 JavaScript API,通常用于建立客户端与服务器之间的单向通信。它是为了替代传统的 EventSource API 而设计的,提供了一种更现代、灵活且兼容性更强的方式来处理实时流数据,尤其是在服务器向客户端推送信息时。

为什么不用webscoket?

fetchEventSourceWebSocket 尽管都用于在客户端和服务器之间建立持久的实时通信通道,但它们的设计目标、使用场景和工作方式存在显著差异。如下所示:

1. 通信方向

  • fetchEventSource:是 单向通信,即数据只能从服务器推送到客户端(Server-Sent Events, SSE)。客户端不能直接通过 fetchEventSource 向服务器发送消息,只能接收来自服务器的数据流。
  • WebSocket:是 双向通信,允许客户端和服务器互相发送数据。WebSocket 连接一旦建立,客户端和服务器都可以随时发送数据。

2. 协议

  • fetchEventSource:基于 HTTP 协议,通常通过 HTTP/1.1 或 HTTP/2 连接。SSE 是在现有 HTTP 协议的基础上扩展的,所以它与 Web 内容的其他部分(如 RESTful API)能够很好地协作。
  • WebSocket:基于 WebSocket 协议,这是一种完全不同于 HTTP 的协议。WebSocket 使用 ws://wss:// (加密)协议,允许在客户端和服务器之间建立双向全双工(Full-Duplex)通信通道。

3. 用途

  • fetchEventSource:适用于 服务器向客户端推送数据 的场景,典型应用包括实时数据更新、通知、日志流等。例如:实时股市数据、社交媒体更新、新闻流等。
  • WebSocket:适用于需要 双向实时交互 的场景,常见的应用包括在线聊天、多人游戏、实时协作工具等。WebSocket 允许双方随时互相发送数据,是需要双向高效通信的理想选择。

4. 连接方式

  • fetchEventSource:建立连接时客户端会向服务器发送一个普通的 HTTP 请求,服务器响应并持续保持连接。数据流会一直保持开启,直到连接关闭或遇到错误。fetchEventSource 是在 HTTP 请求的基础上实现的,因此它具有一定的兼容性优势,且数据流本身是 事件驱动的。
  • WebSocket:需要先通过 握手(handshake)与服务器建立连接。客户端发送 WebSocket 握手请求,服务器同意后升级协议,形成双向通信通道。WebSocket 是一个独立的协议,与 HTTP 协议不同,但通常可以通过代理服务器进行传输。

5. 支持的数据类型

  • fetchEventSource:通过事件(event.data)传输的数据通常是文本数据,尤其是 JSON 格式。数据传输是 逐行的,每行以 data: <message> 格式传递,且每个事件都是独立的。
  • WebSocket:可以传输 二进制数据(如 Blob 或 ArrayBuffer)或 文本数据,并且数据流可以是 任意格式,没有像 SSE 那样的严格格式要求。

6. 自动重连机制

  • fetchEventSource:具备 自动重连 特性。如果连接断开,fetchEventSource 会自动尝试重新连接。它会依据服务器返回的 HTTP 头部设置重连的间隔。
  • WebSocket:WebSocket 协议本身没有内建的自动重连机制(但是可以通过 JavaScript 代码实现)。如果 WebSocket 连接丢失,应用需要自行处理重连逻辑。

7. 性能与资源使用

  • fetchEventSource:由于基于 HTTP,fetchEventSource 是一种相对轻量的实现方式。它通过 keep-alive 连接持续传输数据,因此在大多数现代浏览器中,它的资源消耗较低,适合发送文本数据(例如日志、通知等)。
  • WebSocket:WebSocket 连接一旦建立,保持长时间的双向通信。WebSocket 协议的性能通常比轮询(Polling)和传统 HTTP 请求更高效,适合高频数据交换的场景,但需要更多的处理能力来维持双向的高效通信。

8. 易用性和实现复杂度

  • fetchEventSource:API 相对简单易用,尤其适用于需要从服务器向客户端推送信息的场景。由于它建立在 HTTP 协议之上,因此在处理和调试时,通常比 WebSocket 更容易与现有的 Web 服务器兼容。
  • WebSocket:WebSocket API 比较底层,客户端和服务器都需要处理更复杂的协议和连接管理。实现双向通信和心跳机制可能比 fetchEventSource 更复杂,尤其是在处理网络断开、重连和错误时。

9. 浏览器兼容性

  • fetchEventSource:基于标准的 HTTP 协议,因此在浏览器中的兼容性很好,尤其是在现代浏览器中。它也比传统的 EventSource 更灵活,能够兼容一些较老或特定的浏览器。
  • WebSocket:WebSocket 在现代浏览器中有很好的支持,但它可能受到代理、防火墙等网络层的限制。在某些受限网络环境下,WebSocket 连接可能会受到影响。

10. 示例代码

fetchEventSource 示例

import { fetchEventSource } from 'fetch-event-source';

fetchEventSource('xxxx', {
  onmessage(event) {
    console.log('接收到通知:', event.data);
  },
  onerror(error) {
    console.error('Error:', error);
  }
});

WebSocket 示例

const socket = new WebSocket('ws://example.com/socket');

// 连接打开时触发
socket.onopen = () => {
  console.log('WebSocket connection opened');
  socket.send('Hello Server');
};

// 服务器发送消息时触发
socket.onmessage = (event) => {
  console.log('Message from server:', event.data);
};

// 连接关闭时触发
socket.onclose = () => {
  console.log('WebSocket connection closed');
};

// 发生错误时触发
socket.onerror = (error) => {
  console.error('WebSocket error:', error);
};

总结

特性fetchEventSourceWebSocket
通信方向单向(服务器→客户端)双向(客户端↔️服务器)
协议基于 HTTP 协议WebSocket 协议(ws://wss://
使用场景服务器推送实时数据(如通知、实时更新等)双向实时通信(如聊天、游戏等)
性能适用于轻量的单向数据流高效的双向通信,适合高频数据交换
实现复杂度较简单,支持 HTTP/1.1、HTTP/2更复杂,涉及到双向通信、连接管理等
兼容性很好的浏览器支持,尤其适用于现代浏览器广泛支持,但可能受限于防火墙和代理

选择使用 fetchEventSource 还是 WebSocket 取决于应用的需求。如果你需要从服务器向客户端推送数据,且只需单向通信,fetchEventSource 是一个更简单的选择;如果你需要双向、低延迟的实时交互,WebSocket 是更合适的方案。

了解了两者的区别,下面我们来看一下我们实现ai会话聊天的场景,场景的功能都是用户问问题,然后服务端返回机器人的数据,不需要客户端给服务端发送数据,因此可以算作是单向通信,所以实现会话聊天的场景选择fetchEventSource更好。

1. 什么是 Server-Sent Events (SSE)?

Server-Sent Events (SSE) 是一种允许服务器主动将信息推送到客户端的技术。与 WebSockets 或轮询相比,SSE 是单向的,意味着数据流只从服务器流向客户端,而客户端不能通过 SSE 直接发送数据到服务器。

  • 用途:SSE 适用于实时应用,如聊天应用、直播推送、实时数据更新等场景。
  • 优点:相比 WebSockets,SSE 更加简单,建立和管理连接更为容易,并且基于 HTTP,易于与现有的 Web 服务器兼容。

2. fetchEventSource 简介

fetchEventSource 是由 whatwg/fetch 库提供的一个 API。它是基于 fetch API 的,专门用于与服务器进行 SSE 通信。相比传统的 EventSourcefetchEventSource 提供了更多的灵活性和控制力,例如能够轻松管理请求头、取消请求以及处理自定义事件。

fetchEventSource 实际上是一个新的 API,用于处理流式数据,作为 EventSource 的替代方案,它能够以更现代、标准化的方式提供服务器推送消息。

3. 基本使用方法

基本语法:

import { fetchEventSource } from 'fetch-event-source';

fetchEventSource(url, {
  method: 'GET', // 请求方法,通常是 GET
  headers: {
    'Authorization': 'Bearer your-token' // 可选:请求头
  },
  openWhenHidden: false, // 可选:控制在页面不可见时是否仍然保持连接
  onmessage(event) {
    console.log('New message:', event.data);
  },
  onerror(error) {
    console.error('Error occurred:', error);
  },
  onopen(response) {
    console.log('Connection opened:', response);
  }
});

4. 参数说明:

  • url:请求的 URL,通常是 SSE 服务的端点地址。
  • options:配置对象,包含以下几个重要选项:

    • method:HTTP 请求方法(默认为 GET)。
    • headers:自定义请求头,适用于身份验证、内容类型等。
    • openWhenHidden:可选,控制浏览器在页面不可见时是否仍保持连接(默认是 false)。
    • onopen(response):连接成功建立时触发的回调函数,可以用来执行初始化操作。
    • onmessage(event):每当服务器推送新的事件时触发的回调函数,event.data 包含推送的数据。
    • onerror(error):当发生错误时触发的回调函数。

5. 使用示例

假设你有一个服务器端应用(例如 Node.js),它通过 SSE 推送实时数据。你可以在客户端使用 fetchEventSource 来接收这些数据。

服务器端(Node.js 示例):

// Express 服务器发送 SSE 数据
const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  let counter = 0;
  const interval = setInterval(() => {
    res.write(`data: ${JSON.stringify({ message: 'Hello', count: counter++ })}\n\n`);
  }, 1000);

  req.on('close', () => {
    clearInterval(interval);
    res.end();
  });
});

app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

客户端(使用 fetchEventSource):

import { fetchEventSource } from '@microsoft/fetch-event-source';

fetchEventSource('http://localhost:3000/events', {
  onmessage(event) {
    const data = JSON.parse(event.data);
    console.log('Received message:', data);
  },
  onopen(response) {
    console.log('Connection opened:', response);
  },
  onerror(error) {
    console.error('Error occurred:', error);
  }
});

6. fetchEventSourceEventSource 的区别

  • 兼容性fetchEventSource 支持更多的浏览器环境,尤其是在一些旧版浏览器或受限环境中,fetchEventSource 提供了更好的兼容性。它是在 fetch API 基础上构建的,因此可以享受 fetch 的强大功能。
  • 控制性fetchEventSource 提供了更细粒度的控制,例如可以配置请求头、取消连接等。而传统的 EventSource API 功能相对简单,无法直接设置请求头或自定义错误处理。
  • 灵活性fetchEventSource 可以使用更复杂的 HTTP 请求和响应设置,而 EventSource 更为基础,适合简单的 SSE 连接。

7. 应用场景

fetchEventSource 适用于需要客户端从服务器接收实时数据的场景,例如:

  • 实时聊天:通过 SSE 将消息从服务器推送到客户端。
  • 实时通知:例如,在社交媒体或电商网站上,推送通知、消息等。
  • 实时数据监控:例如股票价格、天气预报等需要实时更新的数据。

8. 优势

  • 基于 HTTP/1.1 或 HTTP/2,适合大多数 Web 服务。
  • 单向通信:客户端无需向服务器发送数据,简化了通信过程。
  • 自动重连EventSourcefetchEventSource 都支持自动重连功能,当连接断开时会自动尝试重连。
  • 节省资源:相比传统的轮询,SSE 更加高效,因为它是基于事件驱动的,服务器只在有数据时才发送消息,减少了资源消耗。

9. 小结

fetchEventSource 是一个现代化、功能强大的 API,专门用于处理服务器向客户端推送事件。它基于 fetch,提供了更多的灵活性和控制力,使得实时数据流的处理变得更加简单和高效。如果你正在开发需要实时推送数据的 Web 应用,fetchEventSource 是一个值得考虑的选择。

原理2: 会话消息的设计

在一个最基本的ai会话聊天当中主要角色有2个,那就是用户和ai机器人,用户和ai采用一问一答的方式来呈现。因此界面功能包含如下模块:

  1. 会话界面,展示用户头像,日期,以及会话内容,机器人同理。
  2. 输入框和发送按钮。

用户点击发送按钮,就会将输入框中的内容,通过fetchEventSource发送给服务端,服务端接收到数据之后,再根据数据,在知识库当中查找,然后返回答案数据,而这个数据不是一下子返回的,是一条一条返回的。

因此这里的会话消息主要包含2种数据,1种是用户的问题,另一种则是机器人的答案。数据结构很显然就是一个消息数组。我们可以设计数据结构如下:

export interface Message {
    type: "user" | "bot" // 决定是用户还是机器人的角色类型
    name: string // 用户名或者是机器人名,机器人名字也可以直接就叫bot
    message: string; // 消息内容,可以是一个简单的字符串,也可以是markdown类型的字符串
    // 其它字段
}

用户问一个问题,机器人回答,当用户不再问问题的时候,就会有时间限制,时间一到,本次会话就会结束,在本次会话中涉及到的消息,我们就可以称作是一次会话。

由于机器人回复消息是一条一条返回的,因此,我们最终会将这些数据合并成一条消息来展示,但是合并的规则是有限制的,因此我们需要根据会话来制定合并的规则。因此我们就需要对会话进行分组。我们来看如下一个工具函数:

import { Message } from '../types/message';

export const groupByInterval = <T extends Message>(
  arr: T[],
  filterFn = (item: T) => item?.type !== 'bot'
): T[][] => {
  if (arr.length === 0) {
    return [arr];
  }

  const result: T[][] = [[arr[0]]];
  for (let i = 1; i < arr.length; i++) { 
    const item = arr[i];
    if (filterFn(item)) {
      result.push([item]);
    } else {
      result[result.length - 1].push(item); 
    }
  }

  return result;
};

我们来看一下这段代码的解读。

这段 TypeScript 代码定义了一个名为 groupByInterval 的泛型函数,它用于将一组 Message 类型的数组 arr 根据某些条件分组。下面逐步解读:

1. 函数签名与泛型

export const groupByInterval = <T extends Message>(
  arr: T[],
  filterFn = (item: T) => item?.type !== 'bot'
): T[][] => { ... }
  • groupByInterval 是一个泛型函数,泛型 T 被限定为 Message 类型或其子类型(T extends Message)。
  • 参数 arr 是一个 T[] 类型的数组,即由 Message 对象组成的数组。
  • filterFn 是一个可选的回调函数,用于过滤 Message 项,默认的过滤条件是:只保留 type 不是 'bot' 的项(item?.type !== 'bot')。这个函数返回 truefalse,决定是否将该项分组。

2. 空数组检查

if (arr.length === 0) {
  return [arr];
}
  • 如果输入数组 arr 为空,则直接返回该空数组。无须进行分组操作。

3. 初始化结果数组

const result: T[][] = [[arr[0]]];
  • 初始化一个二维数组 result,其第一个元素是包含数组 arr 第一个元素的单一数组(即 [[arr[0]]])。此初始化是因为我们总是至少有一个元素来开始分组。

4. 遍历数组

for (let i = 1; i < arr.length; i++) {
  const item = arr[i];
  if (filterFn(item)) {
    result.push([item]);
  } else {
    result[result.length - 1].push(item);
  }
}
  • 从数组的第二个元素开始遍历(i = 1)。
  • 对于每个元素 item

    • 如果 filterFn(item)true,则表示这个元素满足条件(即它不是 'bot' 类型),所以创建一个新的子数组 [item],并将其添加到 result 中。
    • 否则,表示当前元素是 'bot' 类型,继续将其添加到 result 数组的最后一个子数组中。

5. 返回结果

return result;
  • 最后,函数返回分组后的 result 数组,包含多个子数组,每个子数组根据 filterFn 函数的返回值进行分组。

示例

假设有如下数据:

const messages = [
  { type: 'user', content: 'Hello' },
  { type: 'bot', content: 'Hi' },
  { type: 'user', content: 'How are you?' },
  { type: 'bot', content: 'I am fine' },
  { type: 'user', content: 'Goodbye' }
];

如果调用 groupByInterval(messages),该函数会返回:

[
  [{ type: 'user', content: 'Hello' }],
  [{ type: 'bot', content: 'Hi' }, { type: 'bot', content: 'I am fine' }],
  [{ type: 'user', content: 'How are you?' }],
  [{ type: 'user', content: 'Goodbye' }]
]

总结

这个函数的作用是根据某种条件(默认是 type 不为 'bot')将相邻的元素分组。满足条件的元素会开始一个新组,而不满足条件的元素会被加到当前组中。

如此,我们就完成了会话消息的分组。一个聊天ai可能会包含多个会话,每一个会话包含多个消息。

原理3: 会话消息的合并

我们只需要对每次的服务端返回的机器人的数据消息进行合并,具体的合并规则根据返回的答案数据结构而定。比如返回的数据结构类似这样:

interface BotMessage {
    type: string; // 问题类型
    content: string; // 问题答案
}

这样,我们就需要将会话进行合并,为了保证顺序没问题,我们需要为每一条消息添加messageId,再合并之后,我们需要重新更新一下messageId,由于是前端添加messageId,根据索引值来决定,因此我们就需要在合并之后重新根据索引值来设置messageId,如此一来,我们的合并后的数据才会没有问题。类似合并函数代码如下:

interface Message {
  messageId: number;
  type: string;
  name: string;
  time: string;
}
export const mergeMessagesByType = <T extends Message>(arr: T[]) => {
  const stepMerge = (arr: T[], filterFn: (item: T) => boolean) => {
    let temp = {} as T;
    let orderTypeId = -1;
    let result: T[] = [];

    arr.forEach((item, index) => {
      const { name, time, messageId, type, ...rest } = item;
      if (filterFn(item)) {
        temp[type] = { type, ...rest };
        temp.name = name;
        temp.time = time;
        if (orderTypeId === -1) {
          orderTypeId = arr[Math.max(index - 1, 0)].messageId;
        }
      } else {
        result.push(item);
      }
    });

    if (Object.keys(temp).length > 0) {
      const spliceIndex = result.findIndex(
        (item) => item.messageId === orderTypeId
      );
      result.splice(spliceIndex + 1, 0, { ...temp, messageId: orderTypeId });
    }
    return result.map((item, index) => ({ ...item, messageId: index + 1 }));
  };

  return stepMerge(arr, (item) => item?.type === "bot");
  // 如果还有其它合并条件,还可以继续合并,比如消息里面可能也会根据type值来进行合并
};

我们来看一下这段代码的详解,这段 TypeScript 代码定义了一个名为 mergeMessagesByType 的泛型函数,该函数处理一组消息(Message 类型的数组),并通过某种规则合并消息。具体实现方式是通过内部的 stepMerge 函数处理数组中的元素,并根据消息的 type 属性是否是机器人消息进行合并。下面详细解读代码的各个部分。

1. Message 接口

interface Message {
  messageId: number;
  type: string;
  name: string;
  time: string;
}
  • Message 是一个接口,表示每条消息的基本结构。

    • messageId: 消息的唯一标识符。
    • type: 消息的类型(例如用户消息、机器人消息)。
    • name: 消息发送者的名称。
    • time: 消息发送的时间。

2. mergeMessagesByType 函数

export const mergeMessagesByType = <T extends Message>(arr: T[]) => { ... }
  • mergeMessagesByType 是一个泛型函数,泛型参数 T 表示消息类型(继承了 Message 接口)。它接受一个 T[] 类型的数组 arr,即一组消息对象。
  • 该函数的作用是处理这组消息,并返回一个经过合并后的新数组。

3. 内部函数 stepMerge

const stepMerge = (arr: T[], filterFn: (item: T) => boolean) => { ... }
  • stepMerge 是一个内嵌的函数,它负责执行实际的消息合并操作。它接受两个参数:

    • arr: 要处理的消息数组。
    • filterFn: 一个过滤函数,判断每个消息是否符合合并条件。默认情况下,filterFn 会检查消息的 type 属性是否存在,并且是机器人的消息(即是否为有效的类型)。

4. 变量定义

let temp = {} as T;
let orderTypeId = -1;
let result: T[] = [];
  • temp: 用来暂存合并后的消息数据,初始为空对象。通过合并的消息信息会存储在这里。
  • orderTypeId: 记录上一个消息的 messageId。用于确定将合并后的消息插入到 result 数组中的位置。
  • result: 存储最终结果的数组。

5. 遍历数组并合并消息

arr.forEach((item, index) => {
  const { name, time, messageId, type, ...rest } = item;
  if (filterFn(item)) {
    temp[type] = { type, ...rest };
    temp.name = name;
    temp.time = time;
    if (orderTypeId === -1) {
      orderTypeId = arr[Math.max(index - 1, 0)].messageId;
    }
  } else {
    result.push(item);
  }
});
  • 遍历消息数组 arr 中的每个元素 item,对每条消息进行处理。

    • 使用解构语法提取消息的 name, time, messageId, type,其余的属性存入 rest
    • 如果该消息满足 filterFn(item)(默认是 type 存在),则将其 type 和其他信息存储到 temp 中。
    • temp.nametemp.time 保持为当前遍历的消息的 nametime(假设相同类型的消息应具有相同的发送者和时间)。
    • 如果 orderTypeId 还没有被赋值(即第一次处理合并消息),则将上一个消息的 messageId 存入 orderTypeId,这个 messageId 用来确定合并后消息的位置。
    • 如果消息不符合合并条件(即 filterFn 返回 false),则直接将该消息推入 result 数组中。

6. 将合并后的消息插入到 result

if (Object.keys(temp).length > 0) {
  const spliceIndex = result.findIndex(
    (item) => item.messageId === orderTypeId
  );
  result.splice(spliceIndex + 1, 0, { ...temp, messageId: orderTypeId });
}
  • 在遍历结束后,如果 temp 中有数据(即至少有一条消息符合 filterFn),表示需要将合并后的消息插入到 result 中。
  • 找到 result 中第一个 messageId 等于 orderTypeId 的元素的位置(spliceIndex),然后在该位置之后插入合并后的消息。
  • 合并后的消息由 temp 组成,并且 messageId 被赋值为 orderTypeId

7. 更新 messageId

return result.map((item, index) => ({ ...item, messageId: index + 1 }));
  • 最后,返回合并后的结果数组,其中每个消息的 messageId 会重新计算为其在数组中的索引(index + 1)。这意味着,最终返回的数组中的消息 messageId 会是一个递增的数字,从 1 开始。

8. 默认 filterFn 的实现

return stepMerge(arr, (item) => item?.type === 'bot');
  • mergeMessagesByType 函数调用 stepMerge,并传入一个默认的 filterFn,该函数会检查消息是否具有 type 属性,只有 type 存在并且是机器人的消息才会被合并。

总结

mergeMessagesByType 函数的目的是合并相同 type 的消息,并返回一个经过合并处理的新消息数组。它的工作流程如下:

  1. 遍历消息数组,过滤出符合条件的消息(根据 filterFn)。
  2. 对符合条件的消息进行合并,合并后的消息会在 result 数组中找到合适的位置。
  3. 最终,返回一个新数组,其中每条消息的 messageId 被重新计算为其在数组中的位置。

示例

假设我们有以下消息数组:

const messages = [
  { messageId: 1, type: 'user', name: 'Alice', time: '10:00', content: 'Hi!' },
  { messageId: 2, type: 'bot', name: 'Alice', time: '10:01', content: 'How are you?' },
  { messageId: 3, type: 'bot', name: 'Bot', time: '10:02', content: 'I am fine.' },
  { messageId: 4, type: 'user', name: 'Alice', time: '10:03', content: 'Goodbye.' }
];

调用 mergeMessagesByType(messages) 后,可能的返回结果是:

[
  { messageId: 1, type: 'user', name: 'Alice', time: '10:00', content: 'Hi!' },
  { messageId: 2, type: 'bot', name: 'Bot', time: '10:02', content: 'I am fine.' },
  { messageId: 3, type: 'user', name: 'Alice', time: '10:03', content: 'Goodbye.' }
]

通过 filterFn 和合并逻辑,合并的结果可能会将用户的连续消息(type: 'bot')合并到一起,并调整消息 ID。

注意这里由于用户消息是固定的,因此我们没有必要去进行合并。以上还只是一个简单根据是否是机器人消息的步骤来进行合并,如果机器人消息的回答也分类的话,我们可能也需要去进行合并,无非就是多调用一次stepMerge方法,并设置合并条件进行合并罢了。

在我的实际业务场景当中,合并是分了三次进行合并的,因为机器人消息分成了3种类型展示,第一种是展示答案,直接渲染markdown内容,第二种是根据json-schema来渲染表单,第三种则是根据表单来渲染表单的结果。

最后

以上是我认为我在实现ai会话聊天当中最重要的三个原理,感谢阅读本文,阅读本文如有收获,希望不要吝啬点赞收藏。

下篇文章,我就带大家根据这三个核心原理,来实现一个模拟的ai会话界面项目。


夕水
5.3k 声望5.8k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。