引入一个无构建的超轻量级、使用 webworkers 和 web 组件的 ChatGPT 网页界面。

有成百上千个开源的ChatGPT前端,为什么要再创建一个呢?因为这个版本是超轻型的“无构建”版本,完全使用JavaScript编写,不需要npm或任何其他主机,只需使用静态HTML。同时,我想要尝试使用webworkers构建一些可用于未来项目的东西😉

如果你对我在构建它过程中所学到的内容感兴趣,作为一名有着30多年互联网软件开发经验的软件工程师,可以继续阅读。或者你也可以下载源代码并自己体验一下😊。请注意:你将需要一个OpenAI API密钥。

这是一个“AI一级”项目。

目标:无花俏:尽可能简单。

我想创建一个简洁的前端界面,以OpenAI的GPT API为核心,代表了对话流的本质,因为有一些思想对许多软件工程师来说可能不熟悉。为了保持模板代码最少,我将其精炼为以下形式:

  • 真的,让它保持简单。最近我看了很多这个领域的代码,很容易失控,这使得理解和工作变得困难。我一直专注于这个问题:“这怎么样才能更简单?”。我相信它还没有达到最简单的程度。欢迎提出建议和合并请求!
  • 除非不可避免,否则不要进行构建。现代的JavaScript / 网络浏览器在今天非常出色。 #nobuild 的论点是大多数人使用的框架是几年前构建的,那时浏览器的功能较弱。呼吸新鲜空气,这个仓库不需要进行npm安装 🙂
  • 没有持久性或认证。虽然这些是有用的功能,但已经有很多传统技术可以实现它们了。目前添加这些功能的价值较小。当然可以随意复制并添加。

关键原则:流式文本回复

在幕后,LLMs 这样做:

  1. 获取用户的问题(文本提示)+ 聊天历史
  2. 预测下一个(可能是部分)单词
  3. 重复(2)直到没有更多的单词。

它可能需要几十秒来获得所有的单词。所以大多数聊天系统工作的一种流行方式是当单词准备就绪时,立即将它们传递给用户,以提供更好的终端用户体验,因为您不需要长时间等待。在此处查看我的用户界面演示:

这是一个与过去大多数文本系统不同的交互方式-通常情况下是请求、回应,然后一切搞定。文本流式传输需要一点调试才能达到预期效果。下面是我使用的基本流程(我使用“标记”一词来表示“单词或部分单词”):

  • 将用户请求与之前的对话一起带上。
  • 针对OpenAI流式响应,为每个令牌创建“新令牌”消息。
  • 立即将其添加到当前消息中。有用的是,您可以简单地连接这些令牌:所有所需的标点符号和间距都已内建。(例如,您可能会得到像这样的令牌:[‘早上好’,‘先生’,‘Ploppy’,‘py’,‘,’] — 注意单词前面的空格以及将“Ploppy”分为“Plop”和“py”。这是将人类语言单词分解为更小但有意义的部分的标准技术。)
  • 当当前消息完成时,将其添加到消息历史记录中。
  • 最终样式:在最后进行一些最终的演示修饰(使用Markdown)。OpenAI的回答使用需要转换为HTML的Markdown格式。

高层技术结构

  • 页面上两个元素的网络组件:消息输入区和消息列表区。这样可以实现CSS和布局封装:如果需要,可以轻松生成多个组件的不同版本。
  • 用于处理请求和令牌响应流的网络工作线程。网络工作线程似乎是合适的,因为它们在大部分时间都在忙于处理流,这样就可以保证不会因此而中断任何用户界面交互,尽管在这个特定版本中这方面的交互较少,但对于将来可能会有用。我是不是很周到呢?(相反,对于简单的请求/响应集成来说,网络工作线程可能会过度,详见此处)
  • 用于Web组件和Web Worker之间通信的控制器(controller.js)。这包括将Web组件之间的直接链接和消息的路由连接到Web Worker。
  • Core interactions between the different components
    Here’s a mermaid sequence diagram of the key interactions. Source in the repo. And no I didn’t create it, thanks ChatGPT. Prompt was “please create a mermaid.js diagram showing the core interactions between these components: messageInput.js, controller.js, messagesArea.js, model-worker.js”

教训吸取

我曾经使用过一些基于构建的前端技术,包括Svelte,React和React Native,如果您直接使用javascript进行工作,那么肯定有一些事情我希望避免。

跟踪网页内部的更改仍然相当麻烦。Svelte 是管理这个的最优雅方法之一,我很想使用 Svelte 来构建这个系统进行比较。

Web worker 101:web workers与主线程完全隔离。你只能在这两个线程之间发送消息。有趣的陷阱:如果你发送一个指向主线程对象(比如web组件)的引用,它会高兴地复制它,并且在web worker中所做的任何工作都会在复制品上进行。我浪费了很多时间试图弄清为什么消息没有被接收到。最让我困惑的是,现在大多数AI编码者们真的对此感到困惑,并且一直建议在web worker内使用addEventListener()很有用。顶级提示:其实不是这样的。

Web组件无法封装依赖项。在无需构建上下文中,你无法将所有必要的依赖项封装起来,这意味着即使是绝对必需的依赖项也必须在其他地方进行管理。当你听到这个消息时,可能会立即抱怨,但是下面是我遇到的一个具体问题:记住,#nobuild。所以,再见webpack、vite等等。

  • messageInput.js 依赖于 mark.parse()。
  • 我想要导入mark,但是由于它不可用,所以无法进行导入。
  • 我可以使用标记,但只能在主HTML文件中使用。
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>  
<script src="messagesArea.js" defer></script>

下一步

这里是一些可能的方向,主要是为了个人娱乐和教育:

  • 客户端持久性:一些chatgpt解决方案使用本地存储来持久化状态。这非常简单,也是我会考虑的事情。
  • 服务器端持久性:这变成了一种简陋的东西。
  • 其他项目:我真的创建了这个项目来进行我名为回忆的项目的工作,这是一种从对话中提取结构化内容的方法。我将首先进行客户端版本的开发,如果有必要,再研究服务端版本。

2024-01-25 04:38:02 AI中文站翻译自原文