如何使用Gemini与Vercel的无服务器功能来构建人工智能聊天机器人🚀
几个月前,我使用React、Node.js以及Vercel Serverless Functions开发了一个聊天机器人应用程序,并将其应用到了我的网站 buildcv.makeadifference.app 中。在本次教程中,我将详细讲解我是如何构建这个应用程序的——从与Google的Gemini API进行交互的后端无服务器函数,到用于显示AI生成响应的React聊天组件,都会一一介绍。完成本教程后,你将了解以下内容: 如何配置Vercel Serverless函数,使其能够调用Gemini并将生成的响应以小段文本的形式实时发送给浏览器。 为什么这种处理方式能让聊天机器人显得更加快速、反应
如何配置Vercel Serverless函数,使其能够调用Gemini并将生成的响应以小段文本的形式实时发送给浏览器。
为什么这种处理方式能让聊天机器人显得更加快速、反应更灵敏——因为用户不必等待所有回复内容一次性全部传回才看到结果。
如何开发React组件,以便能够读取这些分批发送的响应数据,并在新的文本到达时实时更新聊天窗口显示内容。
我们将涵盖的内容:
🧩 架构概述
该应用程序主要由两部分组成:前端是React聊天组件,用户可以在其中输入消息,AI生成的回复会随着生成进度实时显示在页面上;后端则是Vercel Serverless函数(例如api/chat),它负责验证请求、调用Gemini API,并将响应结果分批发送给浏览器。
你可以通过Vercel的官方文档了解更多关于他们Serverless函数的功能信息。下面是整个流程从开始到结束的运作方式:浏览器向Vercel函数发送请求,Vercel函数再调用Gemini API,而Gemini的响应会分块逐个传回浏览器。
实际上,这意味着后端会在响应数据生成完成后分小部分依次发送给前端,而不是让用户等待所有数据全部生成后才看到任何内容。
✅ 先决条件
在开始之前,请确保您已经具备以下条件:
已在本地安装Node.js(版本需为18或更高版本)
从Google AI Studio获取的Gemini API密钥 🔑
拥有一个Vercel账户
,并熟悉Vercel命令行工具在您的函数项目中安装了
@google/genai包,可以通过运行npm install @google/genai来添加该包
在进行本地开发时,您需要将API密钥存储在名为process.env.GOOGLE_API_KEY的环境变量中;在部署应用程序之前,也请在Vercel项目的控制面板中设置这个环境变量(位于“设置”→“环境变量”选项下),这样应用程序上线后就能正确使用该API密钥了。
📜 API协议
在开始研究代码之前,先了解一下前端与后端之间的“协议”是很有必要的——也就是说,前端应该发送什么数据,后端又期望收到什么响应。
当用户在聊天窗口中输入信息时,前端会向/api/chat发送一个POST请求,请求体中包含两样内容:用户输入的文本信息,以及已经加载到应用程序中的简历数据(因为这个聊天机器人具有简历辅导的功能)。后端会结合这两部分信息来生成针对该用户的具体回复,而不会给出通用性的回应。
请求示例:
POST /api/chat
{
"message": "我该如何改进我的简历摘要?",
"resume": { "name": "...", "experience": [...], "skills": [...] }
}
⚙️ 后端(Vercel无服务器函数)
这就是整个应用程序的核心:一个单一的Vercel无服务器函数,它接收聊天请求、对其进行验证,然后将其传递给Gemini API,同时将AI生成的响应分块传回浏览器。
下面是完整的处理逻辑代码,等您了解了整体结构后,我会详细解释每一部分的功能。
const { GoogleGenAI } = require("@google/genai");
const ai = new GoogleGenAI({
apiKey: process.env.GOOGLE_API_KEY, // GOOGLE_API_KEY可以在Vercel中配置
});
const MAX_TEXT_LENGTH = 2000;
const MAX_ARRAY_LENGTH = 50;
function sanitizeString(input = "") {
// 在这里对输入的字符串进行清洗处理
}
function sanitizeObject(obj = {}) {
// 对传入的对象进行清洗处理
}
const allowCors = fn => async (req, res) => {
res.setHeader('Access-Control-Allow-Origin', '<<你的Web应用地址在这里>>'
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// ✅ 处理预检请求
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
// 另一种处理方式
res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS, POST, PUT, DELETE')
res.setHeader(
'Access-Control-Allow-Headers',
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
)
if (req.method === 'OPTIONS') {
res.status(200).end()
return
}
return await fn(req, res)
};
const handler = async (req, res) => {
if (req.url === '/api/chat' && req.method === 'POST') {
const { message, resume } = req.body || {};
const safeMessage = sanitizeString(message);
const safeResume = sanitizeObject(resume);
if (!safeMessage) {
return res.status(400).json({ error: "输入的信息无效" });
}
if (!safeMessage || !safeResume) {
return res.status(400).json({ error: '缺少信息或简历数据' });
}
try {
const stream = await ai.models.generateContentStream({
model: "",
contents: `
你是一位专业的简历辅导AI。
- 总会清晰、礼貌且专业地回复用户。
- <根据你的需求添加具体的提示指令>
- 简历信息:
${JSON.stringify(safeResume, null, 2)}
用户问题:
${safeMessage}
`,
});
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("Cache-Control", "no-cache");
for await (const chunk of stream) {
const text = chunk.text;
if (text) {
res.write(text);
}
}
res.end();
} catch (error) {
console.error('GEMINI出现错误:', error);
res.status(500).json({ error: 'AI请求失败' });
}
}
// 我添加了一个健康检查接口用于测试handler功能
if (req.url === '/api/chat?type=healthcheck' && req.method === 'GET') {
res.status(200).json({ message: '来自聊天端点的问候!' });
}
}
module.exports = allowCors(handler)
现在,让我们逐项分析这些内容:
配置 Gemini 客户端: 在文件的开头部分,我们使用存储在
GOOGLE_API_KEY环境变量中的 API 密钥来创建一个GoogleGenAI客户端。在后续的代码中,我们将通过这个客户端与 Gemini 进行交互。清洗输入数据: 在处理任何传入的请求之前,我们会使用
sanitizeString和sanitizeObject函数对消息内容和简历信息进行清洗。这些函数会去除所有异常或过长的数据,因此我们绝不会在未经检查的情况下直接将不可信的用户输入传递给 AI。处理 CORS 请求:
allowCors这个封装函数用于处理跨源资源共享问题。由于聊天组件可能被嵌入到与 Vercel 函数所在域名不同的网站上,因此我们需要明确允许来自该域名的请求,并处理浏览器在发送实际的POST请求之前会自动发出的OPTIONS预检请求。验证请求内容: 在处理请求的逻辑中,我们会检查
message和resume这两项数据是否已经通过清洗步骤。如果其中任何一项缺失或无效,我们会立即返回 400 错误码,而不会浪费资源去调用 Gemini 处理那些显然无效的请求。调用 Gemini 并获取响应结果: 这是整个教程的核心部分。我们没有使用传统的 “生成内容” 方法并等待完整的响应结果返回,而是调用了
generateContentStream函数——该函数会返回一个异步可迭代对象。我们通过for await...of循环来处理这个流,每当有新的文本数据到达时,就会立即使用res.write(text)将其写入响应结果中。正是这种处理方式让浏览器能够在 Gemini 完成全部内容生成之前就开始接收文本信息。错误处理与健康检查: 如果在与 Gemini 交互的过程中出现任何问题,我们会捕获错误并记录日志,然后返回 500 错误码,以便前端能够知道出现了故障。此外,还有一个简单的
GET健康检查接口,你可以通过它来确认函数是否正常运行,然后再开始测试实际的聊天流程。
🧪 在搭建前端界面之前先测试端点
在开始开发前端界面之前,最好先确认这个无服务器函数是否能够按照预期的方式发送数据片段。你可以使用简单的 curl 命令来验证这一点。
curl -N -X POST "https://YOUR_APP.vercel.app/api/chat?type=chat" \
-H "Content-Type: application/json" \
-d '{"message":"Give me 3 resume summary tips","resume":{"name":"Test"}}'
-N选项会禁用curl的输出缓冲功能,因此你会在终端中逐渐看到输出内容,而不会一次性全部显示出来。这能让你在编写任何前端代码之前,就确认流式传输功能是否真的能够正常工作。
🎨 前端:读取响应流
这是我用React构建的聊天界面的截图:
我们在这里不会从头开始讲解如何构建这个界面。界面的布局、样式以及消息列表的呈现方式,完全取决于你自己的设计需求。
我们要讨论的是真正驱动这个界面运行的功能:也就是发送按钮背后的代码。这段代码会接收用户输入的内容,将其发送到我们的无服务器函数中,然后读取返回的流式响应数据,这样聊天界面就能实时显示AI给出的回复内容了,就像你在上面的截图中看到的那样。
在介绍这个具体功能之前,先来看看它所在的整个代码结构,这样你就能了解它在组件中的位置和作用了:
function ChatWidget({ resume }) {
const [messages, setMessages] = useState [];
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
async function sendMessage() {
// ...实际的功能代码放在下面
}
return (
);
}
这就是这个功能所在的代码结构:包含了消息列表的状态、输入框的状态、一个表示加载状态的标志,以及一个用于存储错误信息的字段。sendMessage函数会在用户点击发送按钮时被调用。
下面是完整的sendMessage函数代码。每当用户输入内容并点击发送按钮时,这段代码就会执行。它就是将上面展示的界面与我们刚刚构建的后端服务连接起来的关键部分。
async function sendMessage() {
const messageText = input.trim();
if (!messageText || loading) return;
const userMessage = { role: "user", text: messageText };
setMessages((message) => [...message, userMessage]);
setInput("");
setLoading(true);
setError(null);
try {
const res = await fetch("<<你的Vercel无服务器函数URL在这里>>/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: messageText, resume })
});
if (!res.ok) {
throw new Error(`无法获取响应:${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = "";
setMessages((message) => [...message, { role: "assistant", text: "" }]);
while (true) {
const { value, done } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
fullText += chunk;
setMessages((message) => {
const updated = [...message];
updated[updated.length - 1] = { role: "assistant", text: fullText };
return updated;
});
}
} catch (err) {
setError("无法发送消息。请重试.");
setMessages((m) => [...m, {
role: "assistant",
text: "抱歉,出现了错误。请稍后再试。”
}));
} finally {
setLoading(false);
}
}
以下是整个过程的详细步骤:
首先,当用户点击“发送”按钮时,我们会获取他们输入的消息内容,并立即将其添加到`messages`数组中,这样消息就会立刻显示在聊天界面中。
接着,我们会向`/api/chat`端点发送一个`POST`请求,将消息内容以及相关数据一同传递过去。一旦收到响应,我们就会调用`res.body.getReader()`来获取响应体中的`ReadableStreamDefaultReader`对象,然后使用`TextDecoder`将每一段原始字节数据转换为可读文本。
之后,我们会循环读取这些文本数据,将其逐段添加到不断增长的`fullText`字符串中。每次循环结束后,都会用新生成的文本更新聊天界面中的最新消息内容。
正是这个循环机制才产生了你在上面截图中看到的“打字中”效果:当新的文本数据陆续到达时,助手在聊天框中的信息会逐个字地显示出来,而不会一次性全部显示出来。这种处理方式与你们可能熟悉的`fetch().then(res => res.json())`调用方式有所不同,因此如果你之前没有使用过类似的技术来处理响应数据,那么仔细了解这个过程是很有必要的。
🚀 部署
在确认函数和用户界面在本地都能正常运行之后,部署其实只需要三个步骤:
在Vercel中设置环境变量。进入项目控制面板,选择“设置”→“环境变量”,然后添加`GOOGLE_API_KEY`,其值应与你在本地使用的值相同。
更新CORS配置和数据获取URL。在后端代码中,将`allowCors`配置中的`Access-Control-Allow-Origin`头指向你的实际部署域名;在前端代码中,使用该部署后的函数地址代替`localhost`进行数据请求。
执行部署操作。根据你的工作流程,选择合适的部署方式即可。
选项A:通过CLI进行部署
npm install -g vercel # 如果你还没有安装的话
vercel login
vercel --prod
`vercel login`用于验证你的登录信息,而`vercel --prod`则会直接从你的项目目录构建并部署代码到生产环境。这种方式非常适合一次性部署,或者当你需要完全控制部署时间时使用。
选项B:通过GitHub集成进行部署
如果你的项目还没有托管在GitHub上,请先将其推送到相应的仓库中。
在Vercel控制面板中,点击“添加新项目”,然后选择你的仓库。
Vercel会自动检测你的框架配置。确认这些设置后,点击“部署”即可。
从那时起,每次向主分支推送代码时,系统都会自动执行重新部署操作。
我一直更喜欢这个选项,如果你正在积极地对项目进行迭代开发的话,这个选项会显得更加方便,因为你根本不需要自己去记得执行部署命令。部署完成后,Vercel会为你提供应用程序的实时URL。你可以使用之前使用的curl命令进行测试,只需将URL替换为生产环境的地址,从而确认在正式环境中数据流传输功能是否正常运行。
⚠️ 关于嵌入式 widget 的 CORS 注意事项
如果你打算将这个聊天 widget 嵌入到一个与你的 Vercel 应用程序属于不同域名的网站中(例如,将其作为插件嵌入到营销网站上,而其功能本身却部署在其他地方),那么默认情况下你会遇到 CORS 限制。除非服务器明确允许,否则浏览器会阻止对不同源地址的请求。
为了解决这个问题,你的无服务器函数需要响应浏览器在发送实际 POST 请求之前自动发出的 OPTIONS 预检请求,并且需要设置 Access-Control-Allow-Origin 头部信息,使其与 widget 实际运行的域名相匹配。上述后端代码中的 allowCors 包装器正是用来实现这一点的。
🎉 结论
纯文本分块传输技术使得聊天机器人能够在 Vercel 上展现出良好的响应速度。因为用户能够逐渐看到回复内容,所以他们不必等待所有回复信息生成完毕才能在屏幕上看到任何内容。
如果你想进一步扩展这个功能,以下是一些可以考虑的下一步行动:添加速率限制机制以防止对端点的滥用;使用 KV、Redis 或 Postgres 等技术实现对话内容的持久化存储,这样在刷新页面时聊天记录就不会丢失;为 widget 添加一个停止按钮,让用户能够取消正在生成的回复。
如果你用这种方法开发了什么项目,我很乐意听到你的分享!你打算用 Gemini 和 Vercel 构建什么呢?
祝你们度过愉快的一周!😇
相关文章
技术实践
在React中处理高频实时数据:从环形缓冲区到离屏canvas技术
React在很多方面都表现得非常出色。但如果你曾经尝试过每秒向它传输数千个数据点,你就会很快意识到:React并不像一根能输送大量水流的消防水管,而更像是一根普通的花园浇水软管。 如果强迫它处理过多的数据,要么会导致“草坪被淹没”(即DOM结构变得混乱),要么会使得“管道爆裂”(也就是应用程序运行出现严重问题)。 还有另一个与上述观点相关的观察结果:你的笔记本电脑通常拥有8到16个CPU核心,而你的React应用程序几乎总是只使用其中的一个核心。主线程负责处理JavaScript代码、DOM操作、布局计算以及绘制工作;而其他核心则处于闲置状态,因为主线程实在难以维持每秒60帧的渲染速度。 这两
阅读全文
技术实践
在Linux系统中,系统调用究竟是如何工作的
这是一个简单的C程序。它调用了三次`clock_gettime()`函数,然后向标准输出输出了5个字节。 #include #include #include int main(void) { struct timespec ts; for (int i = 0; i 这两者看起来都属于系统调用。它们都是向内核请求程序本身无法获取的信息:当前时间,以及文件描述符的访问权限。 gcc -O0 -o mystery mystery.c strace ./mystery 2>&1 | grep -c clock_gettime 答案是`0`。 write() 函数被立即执行了,而那三次`clock_
阅读全文
技术实践
使用OpenTelemetry实现Claude Code的可观测性
像 Claude Code 、 OpenAI Codex 、 Google Antigravity 以及 Cursor 这样的代理编码工具,在日常软件开发中已经变得无处不在。 随着代理系统的不断发展,开发者让这些系统完成的大部分工作都是通过逐个分配子任务来实现的。许多团队也在探索并使用共享的、多租户式的代理基础设施,这种架构的成本不会与某个特定的所有者挂钩。在这种情况下,可观测性就成为了监控基础设施成本的关键因素。 在本指南中,您将了解可观测性的工作原理,然后学习如何启用Claude Code内置的遥测功能,运行后端程序来收集数据,并读取该系统生成的各类指标、日志及追踪信息。这些内容将帮助您更
阅读全文
技术实践
演示主题:如何通过一份数据同时完成从S3存储系统到GPU的计算任务——重新思考用于机器学习训练的数据加载方式
Onur Satici详细解释了如何利用Vortex这一由Linux基金会推出的开源列式文件格式来彻底改变高吞吐量数据加载的方式。他说明了如何通过层叠式的轻量级编码技术、基于文件结构的数据分段优化机制以及零拷贝内存传输技术,消除CPU与NVMe之间的性能瓶颈,从而让S3存储中的数据能够以高达60 Gbps的速度直接传输到GPU上,而无需在进行任何预处理操作。 作者:Onur Satici
阅读全文