在上篇教程中我们提到, 近期发布了全新的 API,并且在该教程中,学院君给大家演示了如何基于这个 API 快速实现命令行版 ,今天,让我们看看如何利用它结合 10 构建 网页版,我们把这个网页克隆版的 命名为 。
如果您想直接跳转到源代码,可以在我的 上找到它。
这个新的 API 模型提示方式和之前有点不同,因为它针对的是“聊天”自动完成,所以我们不仅仅是发送一个简单的字符串作为提示,而是发送一整段聊天对话,然后 AI 模型将自动完成对话功能。
下面是我们今天要完成的 网页克隆版 的最终样子,比较简陋,但该有的核心功能也都具备了:
整体功能并不复杂,这里我们使用网页应用开发神器 框架结合 CSS 快速完成应用的开发。
PS:经过数次迭代,这个项目最新面孔已经是这样的了,成为一个支持文字、语音、翻译、画图的多功能聊天机器人,你可以通过这个链接进行体验 ——
初始化项目
作为起点,我们使用 安装器初始化一个新的 10 应用程序:
laravel new geekchat
不了解 框架的可以看下官方文档。
然后通过 安装 PHP 扩展包,该扩展包可用于在 PHP 项目中调用 API 接口:
composer require geekr/openai-laravel
接下来,我们需要发布上面这个扩展包的配置文件并设置 API 密钥。
要发布配置文件,运行以下命令即可:
php artisan vendor:publish --provider="GeekrOpenAILaravelServiceProvider"
发布完成后,可以在 .env 文件中设置 API 密钥,如下所示:
OPENAI_API_KEY="你的 OpenAI 密钥"
此外,这个扩展包还支持代理配置,已解决国内调用不了 接口的问题:
OPENAI_BASE_URI=open.aiproxy.xyz
构建会话表单
我们想要的是一个聊天样式的 UI,所以需要在默认首页视图文件 .blade.php 中添加一个简单的问题输入字段以及一个“重置会话”按钮,就像我们在 中重置当前会话一样:
这个输入字段需要放在一个表单中,该表单用于将输入的问题提交到 应用程序的 POST 路由 /chat,从而完成与 的会话:
@csrf
接下来,我们将完成后端处理会话与重置会话的核心功能。
实现会话核心功能
我们将新建一个控制器 处理会话相关的业务逻辑:
然后在控制器中定义会话处理方法,核心逻辑其实和上篇命令行版 大同小异,只是换了一种语言实现而已:
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('/'); } }
正如我上面提到的,此处的提示有点不同 —— 它是来自用户的消息和从 获得的响应的混合物。新的聊天 API 还允许我们定义“系统”消息,这是某种通用指令,用于告诉聊天模型其一般用途应该是什么。
这里我们通过 $ 数组聚合提示,作为默认值,我们将在其中放置我们的“系统”消息,然后将用户问题消息放进来,就可以使用这个数组并执行 API 请求:
$response = OpenAI::chat()->create([ 'model' => 'gpt-3.5-turbo', 'messages' => $messages ]);
拿到 聊天响应消息后,我们还要将其添加到我们的 $ 数组中:
$messages[] = ['role' => 'assistant', 'content' => $response->choices[0]->message->content];
请注意,这里我们添加来自 API 响应消息时,使用了 “” 角色将其添加到 $ 数组中,以表明这是来自 API 而不是用户的消息。
这样一来,我们就能够提出涉及先前消息和回复上下文的问题了。
由于这些“消息”需要随时间增长,我们需要将它们存储在某个地方,对于这个简单的克隆版本,我们将把消息存储在会话中。现在我们的 $ 数组包含了我们需要的所有消息,我们可以将其存储回会话中并重定向回去:
$request->session()->put('messages', $messages); return redirect('/');
就是这样!下一次用户发送消息时,我们将重用会话中的消息并将新消息附加到其中,就像 一样。
最后不要忘了在路由文件 /web.php 中定义路由与控制器方法的映射关系:
Route::post('/chat', ChatController::class . '@chat');
相比之下,重置会话就简单多了,只需要清空会话中的消息数据,然后重定向到首页即可,还是在 中定义重置会话方法:
/** * Reset the session. */ public function reset(Request $request): RedirectResponse { $request->session()->forget('messages'); return redirect('/'); }
对应的路由映射关系如下:
Route::get('/reset', ChatController::class . '@reset');
完成前端展示
现在,我们在会话中有了所有的消息(包含来自 响应和用户的消息),我们需要做的就是将它们传递给视图并向用户显示它们。这一切都是在首页路由中完成的。
为了不显示内部的“系统”消息,我们可以在将消息传递给视图之前从消息数组中删除它:
Route::get('/', function () { $messages = collect(session('messages', []))->reject(fn ($message) => $message['role'] === 'system'); return view('welcome', [ 'messages' => $messages ]); });
在视图中,我现在只是循环遍历消息,根据其来自用户还是来自 给它们不同的背景颜色,然后使用 解析器解析消息内容并渲染:
@foreach($messages as $message)@endforeach
这就是所有需要做的事情,得益于新的 API,你可以轻松构建自己的 克隆版。
基于 开发部署
如果你对 部署,还可以使用 自带的 Sail 扩展包通过 在本地部署启动应用,开始之前需要先安装 Sail 扩展包:
composer require laravel/sail --dev
由于这个项目比较简单,没有数据库、缓存、消息队列,所以我们直接通过 Sail 启动应用就好了(确保此时 已经启动并运行):
./vendor/bin/sail up -d
如果启动过程中出现类似下面这样的错误:
#0 274.4 tee: /etc/apt/keyrings/ppa_ondrej_php.gpg: No such file or directory
多半是网络原因导致的,可以参照这篇教程设置 软件源的国内镜像,或者参考这个项目的 代码仓库,我把很多不需要数据库、前端依赖都删除掉了,同时移除了本地对 PHP//Sail 的依赖,保留最小可用资源,直接通过 – up -d 启动即可。
启动成功后,就可以通过 在浏览器中访问 网页版了:
如果是部署到生产环境,可以使用 php-fpm 作为 http 服务器,也可以使用 扩展包提供的高性能 http 服务器选项 —— 或者 ,我这个 是基于 + 部署的。
最后,我们用以下对话结束今天的教程:
如果你没有 账号,想要快速体验,可以访问这个演示版:,演示版背后的源码就是今天这篇教程演示的。
附录:国内无法调用 接口的解决办法设置 HTTP 代理
不少同学反映国内无法直接通过代码调用 接口,我在写这个示例项目的时候也遇到这个问题,解决办法也不难,就是在发起 HTTP 请求的时候在请求头中添加代理设置:
'proxy' => 'http://127.0.0.1:10809', 'verify' => false,
如果是通过 curl 发起的请求也是参照这个思路。Go HTTP 代理设置参考这个代码配置。
之前使用的 `-php/` 这个扩展包不支持对代理进行设置,也不支持对 和 进行扩展(都是通过 final 修饰):
因此我重新开发了一个扩展包,也就是今天项目中使用的 geekr/-,主要就是在原来的基础上支持配置代理(以域名代理的方式实现,不是这种配置本地代理,本地代理只能本地使用)。
此外,你还可以使用另一个 PHP 扩展包替代 —— /open-ai,该扩展包支持你对代理进行设置:
通过中间层代理
不过如果你没有本地代理或者不想每个项目配置,还可以使用 解决 和 的 API 无法访问的问题,其实就是把一个国内可访问的域名指向 ,再将 作为代理,转发给 接口进行交互,最后把响应数据返回给客户端:
参照这个思路,使用 AWS 或者其他云服务厂商的 API 网关+ 函数(云函数)也可以实现类似的功能。不想折腾的同学可以使用极客书房提供的腾讯云代理,只需要在发起请求时将 的 API 域名 替换成 ..xyz 即可:
代码里也是一样,以我开发的 geekr/- 为例,它首先从配置文件读取 ,然后在发起请求的时候,以自定义的 $ 为准发起请求,这样就可以通过代理的方式发起对 的接口请求了:
withOrganization($organization); } $client = new GuzzleClient(); $transporter = new HttpTransporter($client, $baseUri, $headers); return new Client($transporter); } }
这个代理的源码我也提交到 仓库里了,其实就是做一层转发而已:GO–PROXY,觉得有帮助就给个 star 吧。