欢迎回到我们正在学习如何将人工智能产品集成到 Web 应用程序中的系列:
- 简介和设置
- 您的第一个 AI 提示
- 流式响应
- 人工智能如何运作
- 快速工程
- 人工智能生成的图像
- 安全性和可靠性
- 正在部署
上次,我们解决了所有样板工作。
在这篇文章中,我们将学习如何使用 获取
。我们希望通过从后端执行这些 HTTP 请求来确保我们不会泄漏 API 密钥。
在本文结束时,我们将拥有一个基本但有效的人工智能应用程序。
生成 OpenAI API 密钥
在我们开始构建任何内容之前,您需要访问 platform.openai.com/account /api-keys 并生成一个 API 密钥以在您的应用程序中使用。
<图>
图>
请确保在某处保存一份副本,因为您只能看到它一次。
使用您的 API 密钥,您将能够向 OpenAI 发出经过身份验证的 HTTP 请求。因此,熟悉 API 本身是个好主意。我鼓励您简要浏览一下 OpenAI 文档 并熟悉一些概念。 模型特别容易理解,因为它们具有不同的功能。 p>
如果您想熟悉 API 端点、预期负载和返回值,请查看 OpenAI API 参考。它还包含有用的示例。
您可能会注意到 NPM 上提供的 JavaScript 包名为 openai
。我们将不会使用这个,因为它不太支持我们想要做的一些事情,而fetch
可以。
发出您的第一个 HTTP 请求
我们要构建的应用程序将根据用户输入进行人工智能生成的文本补全。为此,我们需要使用 聊天端点(请注意,完成端点已弃用)。
我们需要使用 'Authorization'
设置为 'Bearer OPENAI_API_KEY'
(您需要将 OPENAI_API_KEY 替换为您的 API 密钥),并且 body< /code> 设置为包含要使用的 GPT 模型的 JSON 字符串(我们将使用
gpt-3.5-turbo
)和一组消息:
fetch('https://api.openai.com/v1/chat/completions', {
方法:'POST',
标题:{
'内容类型':'应用程序/json',
'授权': '承载 OPENAI_API_KEY'
},
正文:JSON.stringify({
'型号': 'gpt-3.5-turbo',
“消息”:[
{
'角色':'用户',
'content': '给我讲一个有趣的笑话'
}
]
})
})
您可以直接从浏览器控制台运行此命令,并在开发工具的“网络”选项卡中查看请求。
响应应该是一个带有一堆属性的 JSON 对象,但我们最感兴趣的是“choices”
。它将是文本完成对象的数组。第一个应该是一个带有 "message"
对象的对象,该对象具有带有聊天完成信息的 "content"
属性。
{
“id”:“chatcmpl-7q63Hd9pCPxY3H4pW67f1BPSmJs2u”,
"object": "聊天完成",
“创建”:1692650675,
“型号”:“gpt-3.5-turbo-0613”,
“选择”:[
{
“索引”:0,
“信息”: {
“角色”:“助理”,
"content": "为什么科学家不相信原子?\n\n因为它们构成了一切!"
},
"finish_reason": "停止"
}
],
“用法”: {
“提示令牌”:12,
“完成令牌”:13,
“total_tokens”:25
}
}
恭喜!现在您可以随时请求一个平庸的笑话。
构建表单
上面的fetch
请求很好,但它不完全是一个应用程序。我们想要的是用户可以与之交互以生成像上面这样的 HTTP 请求。
组件,我们不再需要设置 preventdefault:submit
, onSubmit$
,或调用action.submit()
。我们只需将操作传递给 action
属性,它就会为我们处理工作。此外,如果 JavaScript 由于某种原因不可用,它也可以工作(我们也可以使用 HTML 版本来完成此操作,但工作量会更大)。
从 '@builder.io/qwik' 导入 { component$ };
从'@builder.io/qwik-city'导入{routeAction$,Form};
导出 const useAction = RouteAction$(() => {
console.log('服务器上的操作')
返回 { o: 'k' }
});
导出默认组件$(() => {
常量操作 = useAction()
返回 (
<表单动作={动作}>
表格>
)
})
所以这是对开发者体验的改进。我们还要改善用户体验。
在 ActionStore
中,我们可以访问 isRunning
数据,该数据跟踪请求是否处于待处理状态。这是方便的信息,我们可以用它来让用户知道请求何时进行。
我们可以通过修改提交按钮的文本来做到这一点,在空闲时说“告诉我”,然后在加载时说“一秒钟......”。我还喜欢分配 aria-disabled
属性以匹配 isRunning
状态。这将向辅助技术暗示它还没有准备好被点击(尽管技术上仍然可以)。它还可以使用 CSS 来提供视觉样式,表明它还没有准备好再次单击。
按钮>" data-lang="text/html">
显示结果
好吧,我们已经做了太多工作,却没有在页面上实际看到结果。是时候改变这一点了。让我们将之前在浏览器中原型化的 fetch
请求引入到我们的应用程序中。
我们可以将 fetch
代码直接复制/粘贴到操作处理程序的正文中,但要访问用户的输入数据,我们需要访问提交的表单数据。幸运的是,传递给 action.submit()
方法的任何数据都可以作为第一个参数提供给操作处理程序。它将是一个序列化对象,其中键对应于表单控件名称。
请注意,我将使用 await
关键字,这意味着我还必须将处理程序标记为 异步
函数。
从 '@builder.io/qwik' 导入 { component$ };
从'@builder.io/qwik-city'导入{routeAction$,Form};
导出 const useAction = RouteAction$(async (formData) => {
const prompt = formData.prompt // 来自
在操作处理程序的末尾,我们还想为前端返回一些数据。 OpenAI 响应以 JSON 形式返回,但我认为我们不妨只返回文本。如果您还记得我们在上面看到的响应对象,该数据位于 responseBody.choices[0].message.content
。
如果我们设置正确,我们应该能够在 ActionStore
的 value
属性中访问操作处理程序的响应。这意味着我们可以有条件地在模板中的某个位置渲染它,如下所示:
{action.value && (
{动作.值}
)}
使用环境变量
好吧,我们已将 OpenAI 请求移至后端,并保护我们的 API 密钥免遭窥探,我们收到一个(平庸的笑话)响应,并将其显示在前端。该应用程序正在运行,但还有一个安全问题需要处理。
由于某些原因,将 API 密钥硬编码到源代码中通常不是一个好主意:
- 这意味着您无法在不公开密钥的情况下公开共享存储库。
- 您可能会在开发、测试和登台期间增加 API 使用量。
- 更改 API 密钥需要更改代码并重新部署。
- 每当有人离开组织时,您都需要重新生成 API 密钥。
更好的系统是使用环境变量。通过环境变量,您可以仅向需要访问它们的系统和用户提供 API 密钥。
例如,您可以使用 OpenAI 密钥的值创建一个名为 OPENAI_API_KEY
的环境变量,仅用于生产环境。这样,只有能够直接访问该环境的开发人员才能访问它。这大大降低了 API 密钥泄露的可能性,使公开共享代码变得更加容易,而且由于您将对密钥的访问权限限制在最少的人范围内,因此您不需要因为有人离开而经常更换密钥公司。
在 Node.js 中,通常从命令行 (ENV_VAR=example npm start
) 或使用流行的 dotenv
包。然后,在服务器端代码中,您可以使用 process.env.ENV_VAR
访问环境变量。
Qwik 的工作方式略有不同。
Qwik 可以针对不同的 JavaScript 运行时(不仅仅是 Node),并通过 process.env
是一个特定于 Node 的概念。为了使事情更加与运行时无关,Qwik 通过 RequestEvent
对象,可用作路由操作处理函数的第二个参数。
这就是我们访问环境变量的方式,但是我们如何设置它们呢?
不幸的是,对于生产环境,设置环境变量会根据平台的不同而有所不同。对于标准服务器 VPS,您仍然可以使用终端进行设置,就像在节点(ENV_VAR=example npm start
)。
在开发中,我们也可以创建一个包含环境变量的 local.env
文件,它们将自动分配给我们。这很方便,因为我们花费了更多的时间来启动开发环境,这意味着我们可以只向需要的人提供适当的 API 密钥。
因此,在创建 local.env
文件后,您可以将 OPENAI_API_KEY
变量分配给您的 API 密钥。
OPENAI_API_KEY="your-api-key"
(您可能需要重新启动开发服务器)
然后我们可以通过RequestEvent
参数访问环境变量。这样,我们就可以使用 模板文字。
导出 const usePromptAction = routeAction$(async (formData, requestEvent) => {
const OPENAI_API_KEY = requestEvent.env.get('OPENAI_API_KEY')
常量提示 = formData.prompt
常量主体 = {
型号:'gpt-3.5-turbo',
消息:[{角色:'用户',内容:提示}]
}
const 响应 = 等待 fetch('https://api.openai.com/v1/chat/completions', {
方法:'发布',
标题:{
'内容类型':'应用程序/json',
授权:`承载${OPENAI_API_KEY}`,
},
正文:JSON.stringify(body)
})
const data = 等待response.json()
返回 data.choices[0].message.content
})
有关 Qwik 中环境变量的更多详细信息,请参阅他们的文档。
摘要
- 当用户提交表单时,Qwik 的优化器会拦截默认行为,该优化器会延迟加载事件处理程序。
- 事件处理程序使用 JavaScript 创建一个 HTTP 请求,其中包含要发送到服务器以由路由操作处理的表单数据。
- 路由的操作处理程序将有权访问第一个参数中的表单数据,并可以访问第二个参数(
RequestEvent
对象)中的环境变量。 - 在路由的操作处理程序中,我们可以使用从表单中获取的数据以及从环境变量中提取的 API 密钥构建 HTTP 请求并将其发送到 OpenAI。
- 通过 OpenAI 响应,我们可以准备发送回客户端的数据。
- 客户端收到操作的响应并可以相应地更新页面。
这是我的最终组件的样子,包括一些 Tailwind 类和稍微不同的模板。
const OPENAI_API_KEY = requestEvent.env.get('OPENAI_API_KEY')
常量提示 = formData.prompt
常量主体 = {
型号:'gpt-3.5-turbo',
消息:[{角色:'用户',内容:提示}]
}
const 响应 = 等待 fetch('https://api.openai.com/v1/chat/completions', {
方法:'发布',
标题:{
'内容类型':'应用程序/json',
授权:`承载${OPENAI_API_KEY}`,
},
正文:JSON.stringify(body)
})
const data = 等待response.json()
返回 data.choices[0].message.content
})
导出默认组件$(() => {
常量操作 = usePromptAction()
返回 (
<主类=“max-w-4xl mx-auto p-4”>
嗨
<表单操作 = {action} class =“网格间隙-4”>
{action.isRunning ? '一秒钟...' : '告诉我'}
按钮>
表格>
{动作.值&&(
{动作.值}
文章>
)}
主要>
);
});" data-lang="text/javascript">
从“@builder.io/qwik”导入 { component$ };
从“@builder.io/qwik-city”导入{routeAction$,Form};
导出 const usePromptAction = routeAction$(async (formData, requestEvent) => {
const OPENAI_API_KEY = requestEvent.env.get('OPENAI_API_KEY')
常量提示 = formData.prompt
常量主体 = {
型号:'gpt-3.5-turbo',
消息:[{角色:'用户',内容:提示}]
}
const 响应 = 等待 fetch('https://api.openai.com/v1/chat/completions', {
方法:'发布',
标题:{
'内容类型':'应用程序/json',
授权:`承载${OPENAI_API_KEY}`,
},
正文:JSON.stringify(body)
})
const data = 等待response.json()
返回 data.choices[0].message.content
})
导出默认组件$(() => {
常量操作 = usePromptAction()
返回 (
<主类=“max-w-4xl mx-auto p-4”>
嗨
<表单操作 = {action} class =“网格间隙-4”>
<按钮类型=“提交”aria-disabled={action.isRunning}>
{action.isRunning ? '一秒钟...' : '告诉我'}
按钮>
表格>
{动作.值&&(
{动作.值}
文章>
)}
主要>
);
});
结论
好吧!我们已经从一个使用 AI 获取平庸笑话的脚本转变为一个成熟的应用程序,该应用程序安全地向后端发出 HTTP 请求,该后端使用 AI 获取平庸的笑话,并将它们发送回前端,将这些平庸的笑话放在页。
你应该自我感觉良好。
但还不太好,因为还有改进的空间。
在我们的应用程序中,我们正在发送请求并获取 AI 响应,但我们正在等待生成该响应的整个正文,然后再将其显示给用户。这些人工智能响应可能需要一段时间才能完成。
如果您过去使用过人工智能聊天工具,您可能会熟悉这种体验,它看起来就像是在生成回复时一次一个单词地输入您的回复。这并不会加快总请求时间,但它确实可以更快地向用户返回一些信息,并且感觉体验更快。
在下一篇文章中,我们将学习如何使用 HTTP 流构建相同的功能,这是令人着迷且强大的,但也可能有点令人困惑。因此,我将专门写一整篇文章来讨论这个问题。
我希望您喜欢这个系列并计划坚持下去。与此同时,享受制造一些平庸笑话的乐趣。
非常感谢您的阅读。如果您喜欢这篇文章,并且想支持我,最好的方法是 分享并在 Twitter 上关注我。