在上篇教程中我们提到,OpenAI 近期发布了全新的 ChatGPT API,并且在该教程中,学院君给大家演示了如何基于这个 API 快速实现命令行版 ChatGPT,今天,让我们看看如何利用它结合 Laravel 10 构建 ChatGPT 网页版,我们把这个网页克隆版的 ChatGPT 命名为 GeekChat。
如果您想直接跳转到源代码,可以在我的 GitHub 上找到它。
这个新的 API 模型提示方式和之前有点不同,因为它针对的是“聊天”自动完成,所以我们不仅仅是发送一个简单的字符串作为提示,而是发送一整段聊天对话,然后 AI 模型将自动完成对话功能。
下面是我们今天要完成的 ChatGPT 网页克隆版 GeekChat 的最终样子,比较简陋,但该有的核心功能也都具备了:
整体功能并不复杂,这里我们使用网页应用开发神器 Laravel 框架结合 Tailwind CSS 快速完成应用的开发。
PS:经过数次迭代,这个项目最新面孔已经是这样的了,成为一个支持文字、语音、翻译、画图的多功能聊天机器人,你可以通过这个链接进行体验 —— https://wen.study.geekai.co
初始化项目
作为起点,我们使用 Laravel 安装器初始化一个新的 Laravel 10 应用程序:
laravel new geekchat
不了解 Laravel 框架的可以看下官方文档。
然后通过 Composer 安装 OpenAI PHP 扩展包,该扩展包可用于在 PHP 项目中调用 OpenAI API 接口:
composer require geekr/openai-laravel
接下来,我们需要发布上面这个扩展包的配置文件并设置 OpenAI API 密钥。
要发布配置文件,运行以下命令即可:
php artisan vendor:publish --provider="GeekrOpenAI\Laravel\ServiceProvider"
发布完成后,可以在 .env
文件中设置 OpenAI API 密钥,如下所示:
OPENAI_API_KEY="你的 OpenAI 密钥"
此外,这个扩展包还支持代理配置,已解决国内调用不了 OpenAI 接口的问题:
OPENAI_BASE_URI=open.aiproxy.xyz
构建会话表单
我们想要的是一个聊天样式的 UI,所以需要在默认首页视图文件 welcome.blade.php
中添加一个简单的问题输入字段以及一个“重置会话”按钮,就像我们在 ChatGPT 中重置当前会话一样:
这个输入字段需要放在一个表单中,该表单用于将输入的问题提交到 Laravel 应用程序的 POST 路由 /chat
,从而完成与 ChatGPT 的会话:
<form class="p-4 flex space-x-4 justify-center items-center" action="/chat" method="post"> @csrf <input id="message" placeholder="输入你的问题..." type="text" name="message" autocomplete="off" class="border rounded-md p-2 flex-1" required /> <button class="flex items-center justify-center px-4 py-2 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base" type="submit"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" /> </svg> </button> <button class="flex items-center justify-center px-4 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-md text-sm md:text-base" onclick="window.location.href='/reset'"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> </svg> </button> </form>
接下来,我们将完成后端处理会话与重置会话的核心功能。
实现会话核心功能
我们将新建一个控制器 ChatController
处理会话相关的业务逻辑:
然后在控制器中定义会话处理方法,核心逻辑其实和上篇命令行版 ChatGPT 大同小异,只是换了一种语言实现而已:
<?php namespace App\Http\Controllers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use GeekrOpenAI\Laravel\Facades\OpenAI; class ChatController extends Controller { /** * Handle the incoming prompt. */ public function chat(Request $request): RedirectResponse { // 系统消息 $messages = $request->session()->get('messages', [ ['role' => 'system', 'content' => 'You are GeekChat - A ChatGPT clone. Answer as concisely as possible.'] ]); // 用户消息 $messages[] = ['role' => 'user', 'content' => $request->input('message')]; $response = OpenAI::chat()->create([ 'model' => 'gpt-3.5-turbo', 'messages' => $messages ]); // 响应消息 $messages[] = ['role' => 'assistant', 'content' => $response->choices[0]->message->content]; $request->session()->put('messages', $messages); return redirect('/'); } }
正如我上面提到的,此处的提示有点不同 —— 它是来自用户的消息和从 OpenAI 获得的响应的混合物。新的聊天 API 还允许我们定义“系统”消息,这是某种通用指令,用于告诉聊天模型其一般用途应该是什么。
这里我们通过 $messages
数组聚合提示,作为默认值,我们将在其中放置我们的“系统”消息,然后将用户问题消息放进来,就可以使用这个数组并执行 API 请求:
$response = OpenAI::chat()->create([ 'model' => 'gpt-3.5-turbo', 'messages' => $messages ]);
拿到 OpenAI 聊天响应消息后,我们还要将其添加到我们的 $messages
数组中:
$messages[] = ['role' => 'assistant', 'content' => $response->choices[0]->message->content];
请注意,这里我们添加来自 API 响应消息时,使用了 “assistant” 角色将其添加到
$messages
数组中,以表明这是来自 API 而不是用户的消息。
这样一来,我们就能够提出涉及先前消息和回复上下文的问题了。
由于这些“消息”需要随时间增长,我们需要将它们存储在某个地方,对于这个简单的克隆版本,我们将把消息存储在会话中。现在我们的 $messages
数组包含了我们需要的所有消息,我们可以将其存储回会话中并重定向回去:
$request->session()->put('messages', $messages); return redirect('/');
就是这样!下一次用户发送消息时,我们将重用会话中的消息并将新消息附加到其中,就像 ChatGPT 一样。
最后不要忘了在路由文件 routes/web.php
中定义路由与控制器方法的映射关系:
Route::post('/chat', ChatController::class . '@chat');
相比之下,重置会话就简单多了,只需要清空会话中的消息数据,然后重定向到首页即可,还是在 ChatController
中定义重置会话方法:
/** * Reset the session. */ public function reset(Request $request): RedirectResponse { $request->session()->forget('messages'); return redirect('/'); }
对应的路由映射关系如下:
Route::get('/reset', ChatController::class . '@reset');
完成前端展示
现在,我们在会话中有了所有的消息(包含来自 OpenAI 响应和用户的消息),我们需要做的就是将它们传递给视图并向用户显示它们。这一切都是在首页路由中完成的。
为了不显示内部的“系统”消息,我们可以在将消息传递给视图之前从消息数组中删除它:
Route::get('/', function () { $messages = collect(session('messages', []))->reject(fn ($message) => $message['role'] === 'system'); return view('welcome', [ 'messages' => $messages ]); });
在视图中,我现在只是循环遍历消息,根据其来自用户还是来自 GeekChat 给它们不同的背景颜色,然后使用 Markdown 解析器解析消息内容并渲染:
@foreach($messages as $message) <div class="flex rounded-lg p-4 @if ($message['role'] === 'assistant') bg-green-200 flex-reverse @else bg-blue-200 @endif "> <div class="ml-4"> <div class="text-lg"> @if ($message['role'] === 'assistant') <a href="#" class="font-medium text-gray-900">GeekChat</a> @else <a href="#" class="font-medium text-gray-900">你</a> @endif </div> <div class="mt-1"> <p class="text-gray-600"> {!! \Illuminate\Mail\Markdown::parse($message['content']) !!} </p> </div> </div> </div> @endforeach
这就是所有需要做的事情,得益于新的 OpenAI ChatGPT API,你可以轻松构建自己的 ChatGPT 克隆版。
基于 Docker 开发部署
如果你对 Laravel 部署,还可以使用 Laravel 自带的 Sail 扩展包通过 Docker 在本地部署启动应用,开始之前需要先安装 Sail 扩展包:
composer require laravel/sail --dev
由于这个项目比较简单,没有数据库、缓存、消息队列,所以我们直接通过 Sail 启动应用就好了(确保此时 Docker Desktop 已经启动并运行):
./vendor/bin/sail up -d
如果启动过程中出现类似下面这样的错误:
#0 274.4 tee: /etc/apt/keyrings/ppa_ondrej_php.gpg: No such file or directory
多半是网络原因导致的,可以参照这篇教程设置 Ubuntu 软件源的国内镜像,或者参考这个项目的 Github 代码仓库,我把很多不需要数据库、前端依赖都删除掉了,同时移除了本地对 PHP/Composer/Sail 的依赖,保留最小可用资源,直接通过 docker-compose up -d
启动即可。
启动成功后,就可以通过 http://localhost
在浏览器中访问 ChatGPT 网页版了:
如果是部署到生产环境,可以使用 php-fpm 作为 http 服务器,也可以使用 Octane 扩展包提供的高性能 http 服务器选项 —— Swoole 或者 RoadRunner,我这个 GeekChat 是基于 Docker + RoadRunner 部署的。
最后,我们用以下对话结束今天的教程:
如果你没有 ChatGPT 账号,想要快速体验,可以访问这个演示版:https://wen.study.geekai.co,演示版背后的源码就是今天这篇教程演示的。
附录:国内无法调用 OpenAI 接口的解决办法
设置 HTTP 代理
不少同学反映国内无法直接通过代码调用 OpenAI 接口,我在写这个示例项目的时候也遇到这个问题,解决办法也不难,就是在发起 HTTP 请求的时候在请求头中添加代理设置:
'proxy' => 'http://127.0.0.1:10809', 'verify' => false,
如果是通过 curl 发起的请求也是参照这个思路。Go HTTP 代理设置参考这个代码配置。
之前使用的 `openai-php/laravel` 这个扩展包不支持对代理进行设置,也不支持对 Client
和 Headers
进行扩展(都是通过 final
修饰):
因此我重新开发了一个扩展包,也就是今天项目中使用的 geekr/openai-laravel,主要就是在原来的基础上支持配置代理(以域名代理的方式实现,不是这种配置本地代理,本地代理只能本地使用)。
此外,你还可以使用另一个 OpenAI PHP 扩展包替代 —— orhanerday/open-ai,该扩展包支持你对代理进行设置:
通过中间层代理
不过如果你没有本地代理或者不想每个项目配置,还可以使用 Cloudflare Workers 解决 OpenAI 和 ChatGPT 的 API 无法访问的问题,其实就是把一个国内可访问的域名指向 Cloudflare Workers,再将 Cloudflare Workers 作为代理,转发给 OpenAI 接口进行交互,最后把响应数据返回给客户端:
参照这个思路,使用 AWS 或者其他云服务厂商的 API 网关+ Lambda 函数(云函数)也可以实现类似的功能。不想折腾的同学可以使用极客书房提供的腾讯云代理,只需要在发起请求时将 OpenAI 的 API 域名 api.openai.com
替换成 openai.aiproxy.xyz
即可:
代码里也是一样,以我开发的 geekr/openai-laravel
为例,它首先从配置文件读取 base_uri
,然后在发起请求的时候,以自定义的 $baseUri
为准发起请求,这样就可以通过代理的方式发起对 OpenAI 的接口请求了:
<?php declare(strict_types=1); namespace GeekrOpenAI\Laravel\Client; use GuzzleHttp\Client as GuzzleClient; use OpenAI\Client; use OpenAI\Transporters\HttpTransporter; use OpenAI\ValueObjects\ApiKey; use OpenAI\ValueObjects\Transporter\BaseUri; use OpenAI\ValueObjects\Transporter\Headers; class OpenAI { /** * Creates a new Open AI Client with the given API token. */ public static function client(string $apiKey, $baseUri, string $organization = null): Client { $apiKey = ApiKey::from($apiKey); $baseUri = BaseUri::from($baseUri); $headers = Headers::withAuthorization($apiKey); if ($organization !== null) { $headers = $headers->withOrganization($organization); } $client = new GuzzleClient(); $transporter = new HttpTransporter($client, $baseUri, $headers); return new Client($transporter); } }
这个代理的源码我也提交到 Github 仓库里了,其实就是做一层转发而已:GO-OPENAI-PROXY,觉得有帮助就给个 star 吧。
有docker版的么.php不是很熟悉.
我晚上整理一下 Laravel 可以非常方便地集成到docker里面 官方提供扩展包了
好了,看github项目readme:https://github.com/geekr-dev/geekchat,设置下OpenAI密钥,直接通过docker-compsoe up -d 启动应用就可以了
先过一遍,明天实践下~
如果使用官方SDK,可以使用下面的方法代理
支持markdown的,这样代码好看点
这就是我上面说的第一种方案 —— 设置HTTP代理
学院君你好,我是一名在校学生 我在使用代理地址:openai.aiproxy.xyz 时 , 第一次成功了,之后都是出现 401 的错误 ,怎么解决呀
open.aiproxy.xyz 是这个域名哈
我用的就是这个域名,我评论的时候复制错了,第一次成功,后面401是因为账号被封了😂