1. 简介
概览
在此 Codelab 中,您将了解如何使用 Vertex AI Gemini API 和 Vertex AI 客户端库创建用 Node 编写的基本聊天机器人。此应用使用由 Google Cloud Firestore 提供支持的 Express 会话存储区。
学习内容
- 如何使用 htmx、tailwindcss 和 express.js 构建 Cloud Run 服务
- 如何使用 Vertex AI 客户端库向 Google API 进行身份验证
- 如何创建聊天机器人以与 Gemini 模型互动
- 如何部署到没有 Docker 文件的 Cloud Run 服务
- 如何使用由 Google Cloud Firestore 支持的 Express 会话存储区
2. 设置和要求
前提条件
- 您已登录 Cloud 控制台。
- 您之前已部署过 Cloud Run 服务。例如,您可以按照从源代码部署 Web 服务快速入门中的步骤开始操作。
激活 Cloud Shell
- 在 Cloud Console 中,点击激活 Cloud Shell
。

如果您是第一次启动 Cloud Shell,系统会显示一个介绍其功能的过渡页面。如果您看到了过渡页面,请点击继续。

预配和连接到 Cloud Shell 只需花几分钟时间。

这个虚拟机已加载了所需的所有开发工具。它提供了一个持久的 5 GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证。只需使用一个浏览器即可完成本 Codelab 中的大部分工作。
连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,并且相关项目已设置为您的项目 ID。
- 在 Cloud Shell 中运行以下命令以确认您已通过身份验证:
gcloud auth list
命令输出
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目:
gcloud config list project
命令输出
[core] project = <PROJECT_ID>
如果不是上述结果,您可以使用以下命令进行设置:
gcloud config set project <PROJECT_ID>
命令输出
Updated property [core/project].
3. 启用 API 并设置环境变量
启用 API
在开始使用此 Codelab 之前,您需要启用多个 API。此 Codelab 需要使用以下 API。您可以运行以下命令来启用这些 API:
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
aiplatform.googleapis.com \
secretmanager.googleapis.com
设置环境变量
您可以设置本 Codelab 中全程使用的环境变量。
PROJECT_ID=<YOUR_PROJECT_ID> REGION=<YOUR_REGION, e.g. us-central1> SERVICE=chat-with-gemini SERVICE_ACCOUNT="vertex-ai-caller" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com SECRET_ID="SESSION_SECRET"
4. 创建并配置 Firebase 项目
- 在 Firebase 控制台中,点击添加项目。
- 输入 <YOUR_PROJECT_ID>,将 Firebase 添加到您的一个现有 Google Cloud 项目
- 如果看到相关提示,请查看并接受 Firebase 条款。
- 点击继续。
- 点击确认方案以确认 Firebase 结算方案。
- 您可以选择是否为此 Codelab 启用 Google Analytics。
- 点击添加 Firebase。
- 项目创建完毕后,点击继续。
- 在构建菜单中,点击 Firestore 数据库。
- 点击创建数据库。
- 从位置下拉菜单中选择您所在的区域,然后点击下一步。
- 使用默认的以生产模式开始,然后点击创建。
5. 创建服务账号
Cloud Run 将使用此服务账号来调用 Vertex AI Gemini API。此服务账号还将有权读取和写入 Firestore,以及从 Secret Manager 读取密文。
首先,运行以下命令创建服务账号:
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run to access Vertex AI APIs"
其次,向该服务账号授予 Vertex AI User 角色。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/aiplatform.user
现在,在 Secret Manager 中创建 Secret。Cloud Run 服务将以环境变量的形式访问此密文,该变量会在实例启动时解析。您可以详细了解 Secret 和 Cloud Run。
gcloud secrets create $SECRET_ID --replication-policy="automatic" printf "keyboard-cat" | gcloud secrets versions add $SECRET_ID --data-file=-
并授予服务账号对 Secret Manager 中 Express 会话 Secret 的访问权限。
gcloud secrets add-iam-policy-binding $SECRET_ID \
--member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \
--role='roles/secretmanager.secretAccessor'
最后,为服务账号授予对 Firestore 的读写权限。
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
6. 创建 Cloud Run 服务
首先,创建一个用于存放源代码的目录,然后通过 cd 命令进入该目录。
mkdir chat-with-gemini && cd chat-with-gemini
然后,创建一个包含以下内容的 package.json 文件:
{
"name": "chat-with-gemini",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"nodemon": "nodemon app.js",
"cssdev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
"tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
"dev": "npm run tailwind && npm run nodemon"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/connect-firestore": "^3.0.0",
"@google-cloud/firestore": "^7.5.0",
"@google-cloud/vertexai": "^0.4.0",
"axios": "^1.6.8",
"express": "^4.18.2",
"express-session": "^1.18.0",
"express-ws": "^5.0.2",
"htmx.org": "^1.9.10"
},
"devDependencies": {
"nodemon": "^3.1.0",
"tailwindcss": "^3.4.1"
}
}
接下来,创建一个包含以下内容的 app.js 源文件。此文件包含服务的入口点,并包含应用的主要逻辑。
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const path = require("path");
const fs = require("fs");
const util = require("util");
const { spinnerSvg } = require("./spinnerSvg.js");
// cloud run retrieves secret at instance startup time
const secret = process.env.SESSION_SECRET;
const { Firestore } = require("@google-cloud/firestore");
const { FirestoreStore } = require("@google-cloud/connect-firestore");
var session = require("express-session");
app.set("trust proxy", 1); // trust first proxy
app.use(
session({
store: new FirestoreStore({
dataset: new Firestore(),
kind: "express-sessions"
}),
secret: secret,
/* set secure to false for local dev session history testing */
/* see more at https://expressjs.com/en/resources/middleware/session.html */
cookie: { secure: true },
resave: false,
saveUninitialized: true
})
);
const expressWs = require("express-ws")(app);
app.use(express.static("public"));
// Vertex AI Section
const { VertexAI } = require("@google-cloud/vertexai");
// instance of Vertex model
let generativeModel;
// on startup
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
console.log(`demo1: listening on port ${port}`);
// get project and location from metadata service
const metadataService = require("./metadataService.js");
const project = await metadataService.getProjectId();
const location = await metadataService.getRegion();
// Vertex client library instance
const vertex_ai = new VertexAI({
project: project,
location: location
});
// Instantiate models
generativeModel = vertex_ai.getGenerativeModel({
model: "gemini-1.0-pro-001"
});
});
app.ws("/sendMessage", async function (ws, req) {
if (!req.session.chathistory || req.session.chathistory.length == 0) {
req.session.chathistory = [];
}
let chatWithModel = generativeModel.startChat({
history: req.session.chathistory
});
ws.on("message", async function (message) {
console.log("req.sessionID: ", req.sessionID);
// get session id
let questionToAsk = JSON.parse(message).message;
console.log("WebSocket message: " + questionToAsk);
ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
id="questionToAsk"
class="text-black m-2 text-right border p-2 rounded-lg ml-24">
${questionToAsk}
</div></div>`);
// to simulate a natural pause in conversation
await sleep(500);
// get timestamp for div to replace
const now = "fromGemini" + Date.now();
ws.send(`<div hx-swap-oob="beforeend:#toupdate"><div
id=${now}
class=" text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
${spinnerSvg}
</div></div>`);
const results = await chatWithModel.sendMessage(questionToAsk);
const answer =
results.response.candidates[0].content.parts[0].text;
ws.send(`<div
id=${now}
hx-swap-oob="true"
hx-swap="outerHTML"
class="text-blue-400 m-2 text-left border p-2 rounded-lg mr-24">
${answer}
</div>`);
// save to current chat history
let userHistory = {
role: "user",
parts: [{ text: questionToAsk }]
};
let modelHistory = {
role: "model",
parts: [{ text: answer }]
};
req.session.chathistory.push(userHistory);
req.session.chathistory.push(modelHistory);
// console.log(
// "newly saved chat history: ",
// util.inspect(req.session.chathistory, {
// showHidden: false,
// depth: null,
// colors: true
// })
// );
req.session.save();
});
ws.on("close", () => {
console.log("WebSocket was closed");
});
});
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
// gracefully close the web sockets
process.on("SIGTERM", () => {
server.close();
});
为 tailwindCSS 创建 tailwind.config.js 文件。
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
创建 metadataService.js 文件,以获取已部署的 Cloud Run 服务的项目 ID 和区域。这些值将用于实例化 Vertex AI 客户端库的实例。
const your_project_id = "YOUR_PROJECT_ID";
const your_region = "YOUR_REGION";
const axios = require("axios");
module.exports = {
getProjectId: async () => {
let project = "";
try {
// Fetch the token to make a GCF to GCF call
const response = await axios.get(
"http://metadata.google.internal/computeMetadata/v1/project/project-id",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
if (response.data == "") {
// running locally on Cloud Shell
project = your_project_id;
} else {
// running on Clodu Run. Use project id from metadata service
project = response.data;
}
} catch (ex) {
// running locally on local terminal
project = your_project_id;
}
return project;
},
getRegion: async () => {
let region = "";
try {
// Fetch the token to make a GCF to GCF call
const response = await axios.get(
"http://metadata.google.internal/computeMetadata/v1/instance/region",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
if (response.data == "") {
// running locally on Cloud Shell
region = your_region;
} else {
// running on Clodu Run. Use region from metadata service
let regionFull = response.data;
const index = regionFull.lastIndexOf("/");
region = regionFull.substring(index + 1);
}
} catch (ex) {
// running locally on local terminal
region = your_region;
}
return region;
}
};
创建名为 spinnerSvg.js 的文件
module.exports.spinnerSvg = `<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path></svg>`;
最后,为 tailwindCSS 创建一个 input.css 文件。
@tailwind base; @tailwind components; @tailwind utilities;
现在,创建一个新的 public 目录。
mkdir public cd public
在该 public 目录中,为将使用 htmx 的前端创建 index.html 文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<script
src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"
></script>
<link href="./output.css" rel="stylesheet" />
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<title>Demo 1</title>
</head>
<body>
<div id="herewego" text-center>
<!-- <div id="replaceme2" hx-swap-oob="true">Hello world</div> -->
<div
class="container mx-auto mt-8 text-center max-w-screen-lg"
>
<div
class="overflow-y-scroll bg-white p-2 border h-[500px] space-y-4 rounded-lg m-auto"
>
<div id="toupdate"></div>
</div>
<form
hx-trigger="submit, keyup[keyCode==13] from:body"
hx-ext="ws"
ws-connect="/sendMessage"
ws-send=""
hx-on="htmx:wsAfterSend: document.getElementById('message').value = ''"
>
<div class="mb-6 mt-6 flex gap-4">
<textarea
rows="2"
type="text"
id="message"
name="message"
class="block grow rounded-lg border p-6 resize-none"
required
>
Is C# a programming language or a musical note?</textarea
>
<button
type="submit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
>
Send
</button>
</div>
</form>
</div>
</div>
</body>
</html>
7. 在本地运行服务
首先,请确保您位于 Codelab 的根目录 chat-with-gemini 中。
cd .. && pwd
接下来,运行以下命令来安装依赖项:
npm install
在本地运行时使用 ADC
如果您在 Cloud Shell 中运行,则表示您已在 Google Compute Engine 虚拟机上运行。与此虚拟机关联的凭据(如运行 gcloud auth list 所示)将由应用默认凭证自动使用,因此无需使用 gcloud auth application-default login 命令。您可以跳过此部分,直接前往创建本地会话密钥部分
不过,如果您在本地终端(即不在 Cloud Shell 中)上运行,则需要使用应用默认凭据向 Google API 进行身份验证。您可以选择以下任一方式登录:1) 使用您的凭据登录(前提是您同时具有 Vertex AI User 和 Datastore User 角色);2) 通过模拟此 Codelab 中使用的服务账号登录。
方法 1)使用您的凭据进行 ADC
如果您想使用自己的凭据,可以先运行 gcloud auth list 来验证您在 gcloud 中的身份验证方式。接下来,您可能需要向您的身份授予 Vertex AI User 角色。如果您的身份拥有 Owner 角色,则您已拥有此 Vertex AI 用户角色。如果不是,您可以运行以下命令,为您的身份授予 Vertex AI 用户角色和 Datastore 用户角色。
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/aiplatform.user gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
然后运行以下命令
gcloud auth application-default login
选项 2)为 ADC 模拟服务账号
如果您想使用在此 Codelab 中创建的服务账号,您的用户账号需要具有 Service Account Token Creator 角色。您可以通过运行以下命令来获取此角色:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
接下来,您将运行以下命令,以将 ADC 与服务账号搭配使用
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
创建本地会话密钥
现在,为本地开发创建本地会话 Secret。
export SESSION_SECRET=local-secret
在本地运行应用
最后,您可以运行以下脚本来启动应用。此脚本还会从 tailwindCSS 生成 output.css 文件。
npm run dev
您可以打开“网页预览”按钮,然后选择“预览端口 8080”,以预览网站

8. 部署服务
首先,运行以下命令以开始部署并指定要使用的服务账号。如果未指定服务账号,则使用默认的计算服务账号。
gcloud run deploy $SERVICE \ --service-account $SERVICE_ACCOUNT_ADDRESS \ --source . \ --region $REGION \ --allow-unauthenticated \ --set-secrets="SESSION_SECRET=$(echo $SECRET_ID):1"
如果您收到提示,指出“从源代码进行部署需要 Artifact Registry Docker 代码库来存储构建的容器。系统将创建区域 [us-central1] 中名为 [cloud-run-source-deploy] 的代码库。”,按“y”接受并继续。
9. 测试服务
部署完成后,在 Web 浏览器中打开该服务网址。然后问问 Gemini,例如:“我练习吉他,但也是一名软件工程师。看到“C#”时,我应该将其视为编程语言还是音符?我应该选择哪一个?
10. 恭喜!
恭喜您完成此 Codelab!
建议您查看 Cloud Run 和 Vertex AI Gemini API 文档。
所学内容
- 如何使用 htmx、tailwindcss 和 express.js 构建 Cloud Run 服务
- 如何使用 Vertex AI 客户端库向 Google API 进行身份验证
- 如何创建聊天机器人以与 Gemini 模型互动
- 如何部署到没有 Docker 文件的 Cloud Run 服务
- 如何使用由 Google Cloud Firestore 支持的 Express 会话存储区
11. 清理
为避免意外产生费用(例如,如果 Cloud Run 服务的调用次数意外超过免费层级中每月 Cloud Run 调用次数的分配额),您可以删除 Cloud Run 或删除您在第 2 步中创建的项目。
如需删除 Cloud Run 服务,请前往 Cloud Run Cloud 控制台 (https://console.cloud.google.com/run),然后删除 chat-with-gemini 服务。您可能还需要删除 vertex-ai-caller 服务账号或撤消 Vertex AI User 角色,以避免意外调用 Gemini。
如果您选择删除整个项目,可以前往 https://console.cloud.google.com/cloud-resource-manager,选择您在第 2 步中创建的项目,然后选择“删除”。如果您删除项目,则需要在 Cloud SDK 中更改项目。您可以运行 gcloud projects list 查看所有可用项目的列表。