@vercel/kv
KnowlegeBase 知识库
Vercel KV 在 AI Chatbot 中的应用
介绍 Vercel AI Chatbot 中如何使用 Vercel Kv 来存储对话历史。
相关代码:https://github.com/vercel/ai-chatbot/blob/main/app/actions.ts
Details
Redis是数据结构存储系统。它支持网络,可以作为数据库、缓存和消息代理。Redis具有高性能、丰富的数据类型支持、原子操作、持久化、复制、高可用性等特性。下面是Redis支持的数据类型列表:
字符串(String):最基本的类型,可以包含任何数据,比如文本、数字或二进制数据。字符串类型是二进制安全的,可以包含最多512MB的数据。
列表(List):简单的字符串列表,按照插入顺序排序。可以在列表的头部或尾部添加元素,也可以读取列表的一个范围内的所有元素。
集合(Set):字符串集合,不允许重复的成员。集合内的元素是无序的,可以执行添加、删除和检测元素是否存在等操作。
有序集合(Sorted Set):不允许重复的成员,每个元素都会关联一个double分数。有序集合中的元素可以按照分数进行范围查询和排序。
哈希(Hash):键值对集合,适合存储对象。Redis的哈希是一个字符串字段到字符串值的映射表,适合存储和查询对象的属性。
位图(Bitmap):通过字符串类型实现,可以视为一个以位为单位的数组,通常用于进行大规模的位操作。
超日志(HyperLogLog):一种概率数据结构,用于高效地估算集合中唯一元素的数量(基数),比如统计访问网站的独立IP数。
地理空间(Geo):可以存储地理位置信息,并进行地理空间查询,比如计算两个位置之间的距离、查找给定范围内的位置等。
流(Stream):Redis 5.0版本引入的新数据类型,适用于构建消息队列系统。流数据结构可以存储消息的时间序列,支持追加新消息、读取消息、消息阻塞读取等操作。
(via kimi)
准备工作
在 Vercel 创建 KV 存储
相关资料:Vercel KV Quickstart
根据文档,在 Vercel 控制台中创建一个 KV 存储。我们将使用如下环境变量来连接它:
KV_URL
KV_REST_API_URL
KV_REST_API_TOKEN
KV_REST_API_READ_ONLY_TOKEN
KV_URL="redis://******:******@rich-gecko-33991.upstash.io:33991"
KV_REST_API_URL="https://rich-gecko-33991.upstash.io"
KV_REST_API_TOKEN="********"
KV_REST_API_READ_ONLY_TOKEN="********"
(连接或将这些写入.env.local
)
Vercel 配置
在Vercel中,连接KV(Key-Value存储服务)通常涉及到配置环境变量,使得在部署的应用程序中能够访问KV存储服务。环境变量可以通过多种方式配置,使用.env
文件是其中的一种方法。以下是步骤的概述:
连接到项目:首先,你需要通过Vercel的CLI工具来将本地的代码仓库连接到现有的Vercel项目。
拉取环境变量:环境变量存储着敏感信息,比如API的URL和访问令牌。你可以通过Vercel的CLI工具拉取最新的环境变量,使用
vercel env pull .env.development.local
命令将这些变量写入到本地的.env
文件中。配置
.env
文件:.env
文件通常用来存储环境变量,这样你的应用在运行时能够读取这些变量。在你的环境文件中可能会包含以下变量:KV_URL
:访问KV服务的基础URL。KV_REST_API_URL
:KV REST API的URL。KV_REST_API_TOKEN
:用于访问KV REST API的访问令牌。KV_REST_API_READ_ONLY_TOKEN
:一个只读的访问令牌,用于在需要的时候提供只读的API访问权限。
手动将这些参数写入到.env
文件中效果上和使用vercel env pull
命令效果相同,不过,使用vercel env pull
的好处是它会自动从Vercel项目的环境变量设置中拉取最新的值,并保持本地环境的同步。
在部署应用程序到Vercel时,这些环境变量需要被配置为生产环境变量。通常,你会在Vercel的项目设置中配置这些变量,这样它们就能在Vercel的服务器上正确地生效。在本地开发中,你的应用程序会从.env
文件中读取这些变量,确保本地开发环境和生产环境的一致性。
使用 @vercel/kv
安装
bash
pm install @vercel/kv
在 AI Chatbot 中的使用
相关代码:https://github.com/vercel/ai-chatbot/blob/main/app/actions.ts
在 Redis 中,我们存储:
ts
pipeline.hmset(`chat:${chat.id}`, chat)
pipeline.zadd(`user:chat:${chat.userId}`, {
score: Date.now(),
member: `chat:${chat.id}`
})
以下是相关函数:
saveChat
ts
export async function saveChat(chat: Chat) {
const session = await auth()
if (session && session.user) {
const pipeline = kv.pipeline()
pipeline.hmset(`chat:${chat.id}`, chat)
pipeline.zadd(`user:chat:${chat.userId}`, {
score: Date.now(),
member: `chat:${chat.id}`
})
await pipeline.exec()
} else {
return
}
}
这个函数的主要目的是保存聊天信息到 KV 中,并将这个聊天信息添加到用户的聊天列表中。
首先,它通过调用auth()
函数获取当前的用户会话。如果没有用户会话或者用户会话中没有用户ID,函数会直接返回。
然后,它创建一个 kv.pipeline()
,这是一种可以一次执行多个操作的机制。
- 它使用
pipeline.hmset()
将聊天信息保存到 KV 中。键是chat:${chat.id}
,值是chat
。 - 它使用
pipeline.zadd()
将这个聊天信息添加到用户的聊天列表中。zadd
是一个方法,用于向有序集合中添加一个或多个成员,这里的键是user:chat:${chat.userId}
,member 是chat:${chat.id}
,score 是当前的时间戳(可能用于排序)。
最后,它使用pipeline.exec()
执行管道中的所有操作。
removeChat
ts
export async function removeChat({ id, path }: { id: string; path: string }) {
const session = await auth()
if (!session) {
return {
error: 'Unauthorized'
}
}
//Convert uid to string for consistent comparison with session.user.id
const uid = String(await kv.hget(`chat:${id}`, 'userId'))
if (uid !== session?.user?.id) {
return {
error: 'Unauthorized'
}
}
await kv.del(`chat:${id}`)
await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`)
revalidatePath('/')
return revalidatePath(path)
}
函数removeChat
接收一个对象作为参数,这个对象包含两个属性:id
和path
。
首先,它通过调用auth()
函数获取当前的用户会话。如果没有用户会话,函数会返回一个错误信息Unauthorized
。
然后,它使用kv.hget()
从 KV 中获取与给定ID相关联的聊天信息中的用户ID。这里的kv
可能是一个 KV 的实例,hget
是一个方法,用于获取与给定键关联的一个字段的值,这里的键是chat:${id}
,字段是userId
。
如果获取到的用户ID与会话中的用户ID不匹配,函数会返回一个错误信息Unauthorized
。
如果匹配:
- 它使用
kv.del()
从 KV 中删除与给定ID相关联的聊天信息。del
是一个方法,用于删除与给定键关联的值,这里的键是chat:${id}
。 - 它使用
kv.zrem()
从 KV 中删除与给定用户ID和聊天ID相关联的值。zrem
是一个方法,用于删除与给定键关联的一个或多个成员,这里的键是user:chat:${session.user.id}
,成员是chat:${id}
。
最后,它调用revalidatePath('/')
和revalidatePath(path)
来重新验证给定的路径,更新用户界面中的聊天列表。
getChat
ts
export async function getChat(id: string, userId: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)
if (!chat || (userId && chat.userId !== userId)) {
return null
}
return chat
}
函数getChat
接收两个字符串类型的参数id
和userId
。
这个函数的主要目的是从 KV 中获取与给定ID和用户ID相关联的聊天信息。
首先,它使用kv.hgetall<Chat>(
chat:${id})
从 KV 中获取与给定ID相关联的聊天信息。这里的kv
可能是一个 KV 的实例,hgetall
是一个方法,用于获取与给定键关联的所有值,这里的键是chat:${id}
。
如果没有找到聊天信息,或者聊天信息中的用户ID与给定的用户ID不匹配,函数会返回null
。
如果找到了匹配的聊天信息,函数会返回这个聊天信息。
shareChat
ts
export async function shareChat(id: string) {
const session = await auth()
if (!session?.user?.id) {
return {
error: 'Unauthorized'
}
}
const chat = await kv.hgetall<Chat>(`chat:${id}`)
if (!chat || chat.userId !== session.user.id) {
return {
error: 'Something went wrong'
}
}
const payload = {
...chat,
sharePath: `/share/${chat.id}`
}
await kv.hmset(`chat:${chat.id}`, payload)
return payload
}
函数shareChat
接收一个字符串类型的参数id
。
首先,它通过调用auth()
函数获取当前的用户会话。如果没有用户会话或者用户会话中没有用户ID,函数会返回一个错误信息Unauthorized
。
然后,它使用kv.hgetall<Chat>()
从 KV 中获取与给定ID相关联的聊天信息。hgetall
是一个方法,用于获取与给定键关联的所有值,这里的键是chat:${id}
。
如果没有找到聊天信息,或者聊天信息中的用户ID与会话中的用户ID不匹配,函数会返回一个错误信息Something went wrong
。
然后,它创建一个新的对象payload
,包含了聊天信息和一个新的属性sharePath
,这个属性的值是/share/${chat.id}
。
最后,它使用kv.hmset(
chat:${chat.id}, payload)
将新的聊天信息存储到 KV 中。hmset
是一个方法,用于设置与给定键关联的多个字段和值,这里的键是chat:${chat.id}
,值是payload
。
函数最后返回payload
。
INFO
疑问,这里的分享的,id 不变吗?(不变)
会不会取的时候重复?
pipeline.hmset()
是在 KV 中设置一个键值对。
如果 KV 中已经存在,那么hmset
方法会更新这个键值对的值为chat
。
如果 KV 中不存在,那么hmset
方法会创建一个新的键值对。
getSharedChat
ts
export async function getSharedChat(id: string) {
const chat = await kv.hgetall<Chat>(`chat:${id}`)
if (!chat || !chat.sharePath) {
return null
}
return chat
}
这个函数名为getSharedChat
,它是一个异步函数,接收一个字符串类型的参数id
。
这个函数的主要目的是从 KV 中获取与给定ID相关联的共享聊天信息。
首先,它使用kv.hgetall<Chat>()
从 KV 中获取与给定ID相关联的聊天信息。
如果没有找到聊天信息,或者聊天信息中没有sharePath
属性(这个属性可能表示聊天是否被共享),函数会返回null
。
如果找到了匹配的聊天信息,函数会返回这个聊天信息。
getChats
ts
export async function getChats(userId?: string | null) {
if (!userId) {
return []
}
try {
const pipeline = kv.pipeline()
const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, {
rev: true
})
for (const chat of chats) {
pipeline.hgetall(chat)
}
const results = await pipeline.exec()
return results as Chat[]
} catch (error) {
return []
}
}
函数getChats
接收一个可选的字符串类型的参数userId
。
这个函数的主要目的是从 KV 中获取与给定用户ID相关联的所有聊天信息。
首先,如果没有提供用户ID,函数会返回一个空数组。
然后,它创建一个 KV 的管道,这是一种可以一次执行多个操作的机制。
接着,它使用kv.zrange()
从 KV 中获取与给定用户ID相关联的所有聊天ID。zrange
是一个方法,用于获取有序集合中的一系列成员,这里的键是user:chat:${userId}
,范围是从0到-1(表示全部),并且以反向顺序(最新的在前)获取。
然后,对于获取到的每个聊天ID,它使用pipeline.hgetall(chat)
将获取聊天信息的操作添加到管道中。hgetall
是一个方法,用于获取与给定键关联的所有值,这里的键是聊天ID。
接着,它使用pipeline.exec()
执行管道中的所有操作,并获取结果。
如果在执行过程中发生错误,函数会捕获错误并返回一个空数组。
如果一切正常,函数会返回结果,这是一个聊天信息的数组。
clearChats
ts
export async function clearChats() {
const session = await auth()
if (!session?.user?.id) {
return {
error: 'Unauthorized'
}
}
const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1)
if (!chats.length) {
return redirect('/')
}
const pipeline = kv.pipeline()
for (const chat of chats) {
pipeline.del(chat)
pipeline.zrem(`user:chat:${session.user.id}`, chat)
}
await pipeline.exec()
revalidatePath('/')
return redirect('/')
}
函数clearChats
不接收任何参数。
这个函数的主要目的是清除与当前用户相关联的所有聊天信息。
首先,它通过调用auth()
函数获取当前的用户会话。如果没有用户会话或者用户会话中没有用户ID,函数会返回一个错误信息Unauthorized
。
然后,它使用kv.zrange()
从 KV 中获取与当前用户ID相关联的所有聊天ID。
如果没有找到任何聊天ID,函数会重定向到/
路径。
如果有,它创建一个 KV 的管道,执行删除操作:
- 对于获取到的每个聊天ID,它使用
pipeline.del(chat)
和pipeline.zrem()
将删除聊天信息和从用户的聊天列表中移除聊天的操作添加到管道中。del
是一个方法,用于删除与给定键关联的值,这里的键是聊天ID。zrem
是一个方法,用于删除有序集合中的一个或多个成员,这里的键是user:chat:${session.user.id}
,成员是聊天ID。
它执行管道中的所有操作。
最后,它调用revalidatePath('/')
来重新验证/
路径,这可能是为了更新用户界面中的聊天列表。
进一步的用途
rate limit
https://github.com/rauchg/genui-demo
https://nextjs.org/learn/dashboard-app/adding-authentication
https://stackoverflow.com/questions/77813700/nextjs-authentication-in-server-action-fetching
next.js api 路由补充资料
NextAuth.js
是一款为 Next.js 应用提供身份验证的库,它封装了一套身份验证流程,简化了与不同提供者(如Google, Facebook, GitHub等)实现OAuth等身份验证协议的过程。如果你对此一无所知,下面将详细解释NextAuth.js在服务器端如何处理认证流程:
安装与配置: 要在Next.js应用中使用NextAuth.js,你需要首先通过npm或yarn安装它:
shnpm install next-auth
然后在你的项目中创建一个名为
[...nextauth].js
的文件,这通常放置在pages/api/auth/
目录下。在这个文件中,你会配置NextAuth.js,并定义身份验证提供者,session设置等。例子:
javascriptimport NextAuth from 'next-auth' import Providers from 'next-auth/providers' export default NextAuth({ providers: [ Providers.GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET, }), // ...其他提供者 ], // ...其他配置 })
身份验证流程: 当用户尝试登录时,会发生以下步骤:
a. 登录页面跳转: 用户通常会被重定向到一个登录页面,这个页面可以是NextAuth.js提供的默认页面,也可以是自定义页面。登录页面会提供不同的身份验证提供者选项以供用户选择。
b. 选择提供者: 用户选择一个身份验证提供者(例如GitHub),然后NextAuth.js会将用户重定向到所选提供者的OAuth授权页面。
c. 授权: 用户在提供者的页面上登录并授权你的应用程序访问其数据。
d. 回调处理: 授权完成后,提供者将用户重定向回Next.js应用的一个特殊API路由
/api/auth/callback/provider-name
。NextAuth.js捕获这个回调请求,并使用从提供者获取的信息(一般是一个OAuth令牌)来登录用户。e. 会话创建与用户对象: NextAuth.js处理来自提供者的信息,创建或更新用户记录,并建立一个会话。会话信息通常存储在JWT(JSON Web Tokens)或数据库中。
f. 客户端会话状态: 登录成功后,NextAuth.js会设置一个会话,并将其传送到前端,通常是通过一个HTTP Only的cookie来完成。这样,前端应用就可以检测到用户的登录状态。
g. 前端访问会话信息: 前端应用可以通过调用NextAuth.js的React Hooks(如
useSession
)来访问当前用户的会话信息,并据此渲染界面,显示登录状态信息或执行用户已登录才能进行的操作。管理会话和认证状态: NextAuth.js提供了一些手段来管理会话和认证状态。例如,使用
getSession
服务器端函数来检索当前会话信息,或者使用signIn
和signOut
函数来控制用户的登录和登出行为。保护API路由: 你可以在API路由中使用NextAuth.js来验证用户的身份,确保只有登录的用户才能访问某些API端点。
例子:
javascript
// pages/api/some-protected-route.js
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
// 用户已登录,正常处理请求
} else {
// 用户未登录,返回错误或权限不足的提示
res.status(401).send('Unauthorized')
}
}
在整个过程中,NextAuth.js通过简化认证流程和管理会话信息来减轻开发者负担,让开发者可以专注于应用程序的其他部分,而不必担心身份验证的复杂性。 保护API路由,服务端怎么知道用户是否登录?
在Next.js中,当使用NextAuth.js保护API路由时,服务端可以通过getSession
这个服务器端方法来检查用户是否已经登录。getSession
会基于用户的请求中携带的会话令牌(通常是Cookie)来确定用户的会话状态。
这里是如何检查用户是否登录的示例步骤:
- 客户端在登录后将接收到的会话令牌存储在Cookie中。
- 每当客户端发起请求时(无论是页面渲染还是API请求),这个令牌都会随请求一起发送到服务器。
- 在服务器端的API路由中,你可以使用
getSession
方法提取请求中的会话信息。 getSession
会根据请求中的会话令牌验证用户的登录状态,并返回会话对象。- 如果返回的会话对象不为空,则表示用户已登录;如果为空,则用户未登录或会话已过期。
以下是一个如何使用 getSession 在Next.js API路由中检查用户登录状态的示例:
javascript
// pages/api/protected.js
import { getSession } from 'next-auth/client'
export default async (req, res) => {
// 尝试获取会话信息
const session = await getSession({ req });
if (session) {
// 如果会话信息存在,则用户已登录
// 现在可以根据需要安全地处理API请求
res.json({ content: 'This is protected content.' });
} else {
// 如果无会话,返回401 Unauthorized响应
res.status(401).json({ error: 'User is not authenticated' });
}
};
当客户端请求该/api/protected
路由时,服务器将运行上述逻辑来确定用户是否已登录。如果用户已登录,它们将能够访问受保护的内容。如果用户未登录,服务端将返回401 Unauthorized
状态码,并可能附带一条错误信息。这样,只有验证过的用户才能访问受保护的API路由。
How to set up NextAuth v5 authentication with middleware and Jest configuration in Next.js 14
How I Integrated Authentication Next-Auth V5 in my NextJS 14 Application
rate limit
是的,您可以使用 Redis Upstash 的速率限制功能来区分付费和未付费用户,为每组用户设置不同的速率限制。例如,您可以允许付费用户每天有更多的请求次数,而限制未付费用户每天只能进行一次请求。这种方法利用了 Upstash 在无服务器环境中实现速率限制的能力,并通过防止滥用和确保系统可用性来有效地管理流量[16]。
要实现这一点,您通常会使用支持基于各种标准配置速率限制的 Upstash 速率限制 SDK。例如,您可以设置一个速率限制器,允许免费用户每10秒进行10次请求,而付费用户每10秒进行60次请求[13]。尽管这个示例展示了基于10秒窗口的速率限制,但您可以调整参数以适应每日限制,将窗口大小设置为86400秒(一天的秒数),并相应地配置最大请求次数。
以下是一个概念性示例,展示了如何为付费和未付费用户配置不同的速率限制:
python
from upstash_ratelimit import Ratelimit, SlidingWindow
from upstash_redis import Redis
class MultiRL:
def __init__(self) -> None:
redis = Redis.from_env()
self.free = Ratelimit(
redis=redis,
limiter=SlidingWindow(max_requests=1, window=86400), # 未付费用户每天1次请求
prefix="ratelimit:free",
)
self.paid = Ratelimit(
redis=redis,
limiter=SlidingWindow(max_requests=60, window=86400), # 根据需要为付费用户调整
prefix="ratelimit:paid",
)
# 使用示例
ratelimit = MultiRL()
# 对于未付费用户
ratelimit.free.limit("unpaidUserIdentifier")
# 对于付费用户
ratelimit.paid.limit("paidUserIdentifier")
这个示例展示了如何使用 Upstash 速率限制 SDK 设置两个不同的速率限制器,一个用于免费(未付费)用户,每天限制1次请求,另一个用于付费用户,限制更高(本例中为每天60次请求)。您需要根据您的付费和未付费用户的具体要求调整 max_requests
和 window
参数[13]。
https://vercel.com/guides/securing-ai-app-rate-limiting
https://vercel.com/templates/next.js/api-rate-limit-upstash
在构建AI应用程序时,一个主要的关注点是防止滥用——恶意行为者可能会利用您的API端点,导致您的应用程序产生过高的使用成本。为了解决这个问题,可以通过使用Vercel AI SDK和Vercel KV来设置速率限制,从而在构建强大的AI体验的同时保持心态平和。
为什么需要速率限制?
- 维护服务可用性:通过实施速率限制,您可以保护服务免受过多请求的冲击。这种对请求量的控制有助于维持服务的高峰性能,确保其持续可用。
- 有效管理成本:速率限制可以帮助您监控和调节计费支出,避免意外的使用激增。这在使用按请求计费的服务时尤为重要。
- 防范恶意活动:在使用AI提供商和大型语言模型(LLMs)时,利用速率限制至关重要。它可以作为一种防御机制,防止恶意活动或滥用,例如DDoS攻击。
- 根据订阅计划实施使用层级:速率限制使得建立不同的使用水平成为可能。例如,免费用户可能每天被限制为特定数量的请求,而高级用户可能获得更宽松的限额。
Vercel是什么?
Vercel的前端云为开发者提供了框架、工作流程和基础设施,以构建更快、更个性化的网络。
Vercel是React框架Next.js的创造者,并对所有主要的前端框架提供零配置支持。
Vercel AI SDK
Vercel AI SDK是一个开源库,旨在帮助开发者用JavaScript和TypeScript构建对话式流媒体用户界面。
使用Vercel AI SDK,您可以仅用几行代码就构建出类似于ChatGPT的流媒体体验。
Vercel KV
Vercel KV是一个全球分布、持久的键值存储。
- 持久性:它提供了一个易于使用的、持久的数据存储,确保您的数据安全存储。
- 低延迟:作为全球分布的服务,它提供来自世界任何地方的低延迟读取。
如何使用Vercel实施速率限制?
从模板开始
如果您更喜欢从模板开始,而不是手动向您的AI项目添加速率限制,Vercel提供了一个使用Vercel AI SDK和Vercel KV(用于处理速率限制)的Next.js模板。
直接实施速率限制的步骤
创建新的Vercel KV数据库:在Vercel控制台中创建一个新的Vercel KV实例。选择您的主要区域和所需的额外读取区域。如果您更喜欢,可以遵循我们的快速入门指南。
将Vercel KV数据库连接到您的项目:将新的KV数据库连接到您的Vercel项目。这将自动添加所需的环境变量以连接到您的新持久Redis数据库。
添加
@upstash/rate-limit
:为了简化速率限制的实施,我们推荐@upstash/rate-limit
,这是一个功能强大的基于HTTP的速率限制库,支持多种算法。这个库允许根据逻辑设置多个速率限制,例如用户的计划。此外,当Vercel Edge Middleware处于"热"状态时,它将智能地缓存并减少对Vercel KV的调用数量,帮助防止不必要的数据库使用。
查看文档以了解此库的所有选项。
添加Vercel AI SDK:Vercel AI SDK为您提供了一些钩子和实用工具,您可以使用它们为您的AI应用程序实现流媒体聊天体验。
在您的Next.js应用程序中,在应用路由(
app/
)内创建一个page.tsx
文件,并添加以下代码:jsx'use client' import { useChat } from 'ai/react' import { toast } from 'sonner' // 您可以选择任何您喜欢的吐司库 export default function Chat() { const { messages, input, handleInputChange, handleSubmit } = useChat({ onError: err => { toast.error(err.message) } }) return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.length > 0 ? messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.content} </div> )) : null} <form onSubmit={handleSubmit}> <input className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl" value={input} placeholder="Say something..." onChange={handleInputChange} /> </form> </div> ) }
如果您使用的是Pages Router,您可以按照这里的说明进行操作。
然后,创建一个路由处理器来从OpenAI流式传输您的聊天响应:
typescriptimport { Configuration, OpenAIApi } from 'openai-edge' import { OpenAIStream, StreamingTextResponse } from 'ai' import { kv } from '@vercel/kv' import { Ratelimit } from '@upstash/ratelimit' // 创建一个OpenAI API客户端(这是边缘友好的!) const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY }) const openai = new OpenAIApi(config) // 将运行时设置为边缘以允许Edge Streaming(https://vercel.fyi/streaming) export const runtime = 'edge' export async function POST(req: Request) { if (process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN) { const ip = req.headers.get('x-forwarded-for') const ratelimit = new Ratelimit({ redis: kv, // 速率限制为每10秒5个请求 limiter: Ratelimit.slidingWindow(5, '10s') }) const { success, limit, reset, remaining } = await ratelimit.limit(`ratelimit_${ip}`) if (!success) { return new Response('You have reached your request limit for the day.', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': reset.toString() } }) } } else { console.log("KV_REST_API_URL and KV_REST_API_TOKEN env vars not found, not rate limiting...") } // 从请求体中提取`prompt` const { messages } = await req.json() // 根据提示请求OpenAI流式聊天完成 const response = await openai.createChatCompletion({ model: 'gpt-3.5-turbo', stream: true, messages: messages.map((message: any) => ({ content: message.content, role: message.role })) }) // 将响应转换为友好的文本流 const stream = OpenAIStream(response) // 响应流 return new StreamingTextResponse(stream) }
在上述代码片段的第17 - 39行中,我们使用@upstash/ratelimit
库和Vercel KV来处理对聊天端点的请求。如果客户端超过了预定义的速率限制(每10秒5个请求),他们将收到一个429
响应,错误信息为"You have reached your request limit for the day."。
简化AI开发中的安全性
保护AI应用程序不必是一个令人生畏的任务。通过使用Vercel AI SDK和Vercel KV实施速率限制,开发者可以轻松地保持服务平稳运行并控制成本。本指南展示了如何简单地设置这些安全措施,无论您是从头开始还是向现有项目添加。有了像@upstash/rate-limit
这样的工具和现成的模板,您可以构建强大的AI体验,而不必担心潜在的滥用或意外的账单。这一切都是为了更智能地构建,而不是更努力地构建,而这些步骤使您迈向了正确的方向。
在提供的示例中,确实使用了客户端的IP地址来进行速率限制。这是通过req.headers.get('x-forwarded-for')
获取的,这个HTTP头通常包含了发起请求的客户端的IP地址。然而,如果需要基于用户身份来进行速率限制,而不是仅仅基于IP地址,可以采取以下几种方法:
使用身份验证令牌(Tokens):
- 为每个用户生成一个唯一的身份验证令牌(例如JWT)。
- 在每次请求时,用户需要在HTTP头中携带这个令牌。
- 服务器端可以验证这个令牌,并将其与用户的会话或账户关联起来。
- 然后,您可以在用户的会话或账户级别上实施速率限制。
使用会话ID(Session IDs):
- 当用户登录或开始会话时,生成一个会话ID。
- 将这个会话ID存储在服务器端,并与用户的账户关联。
- 在速率限制逻辑中,使用会话ID来跟踪和限制请求。
用户账户ID:
- 如果您的应用程序要求用户注册或登录,可以直接使用用户的账户ID来进行速率限制。
- 用户的账户ID可以在数据库中唯一标识用户,并用于在速率限制系统中跟踪用户的请求。
用户唯一标识符(UIDs):
- 如果您的应用程序有用户唯一标识符(例如电子邮件地址、用户名等),可以使用这些标识符来实施速率限制。
- 用户在请求时需要提供这些信息,服务器端可以使用这些信息来识别用户并实施限制。
在实施基于用户的速率限制时,您需要在服务器端维护一个数据结构(例如哈希表或字典),用于存储每个用户的请求计数和时间窗口。这样,您可以在用户进行请求时更新这些计数,并在达到限制时返回错误响应。
例如,如果您使用JWT进行身份验证,可以在中间件中解析JWT,获取用户的唯一标识符,然后根据这个标识符来实施速率限制。这可以通过类似于下面这样的代码实现:
javascript
import { Ratelimit } from '@upstash/ratelimit';
// 假设您已经从JWT中提取了用户ID
const userId = 'some-user-id';
// 创建一个Ratelimit实例
const ratelimit = new Ratelimit({
redis: kv, // 使用Vercel KV作为Redis的替代
limiter: Ratelimit.slidingWindow(5, '10s') // 5个请求每10秒
});
// 在请求处理中实施速率限制
const { success, limit, reset, remaining } = await ratelimit.limit(`ratelimit_${userId}`);
if (!success) {
// 用户已超过速率限制
return new Response('Too many requests. Try again later.', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString()
}
});
}
// 继续处理请求...
在这个例子中,ratelimit_${userId}
是用于跟踪特定用户请求的键。如果用户在10秒内超过了5个请求的限制,服务器将返回429状态码,并提供有关限制的信息。