1. 简介
您好!您对客服人员的想法很感兴趣,他们是小帮手,可以帮您处理各种事务,而您无需动手,对吗?太棒了!但说实话,一个客服人员并不总能胜任,尤其是在您要处理更大、更复杂的项目时。您可能需要一整支这样的团队!这正是多智能体系统的用武之地。
与传统的硬编码相比,由 LLM 支持的代理可为您提供极大的灵活性。不过,它们也存在一系列棘手的挑战。而这正是我们将在本研讨会中深入探讨的内容!
您将学到以下内容,这有助于您提升客服人员的服务水平:
使用 LangGraph 构建您的第一个客服代理:我们将使用 LangGraph(一个热门框架)动手构建您自己的客服代理。您将学习如何创建可连接到数据库的工具、利用最新的 Gemini 2 API 进行一些互联网搜索,以及优化提示和响应,以便您的代理不仅可以与 LLM 互动,还可以与现有服务互动。我们还将向您展示函数调用的工作原理。
按您的方式进行代理编排:我们将探索不同的代理编排方式,从简单的直线路径到更复杂的多路径场景。不妨将其视为指挥客服团队的工作流程。
多代理系统:您将了解如何设置一个系统,让代理能够协作并共同完成工作,这一切都得益于事件驱动型架构。
LLM 自由 - 使用最适合的工作:我们不会局限于使用单个 LLM!您将了解如何使用多个 LLM,为它们分配不同的角色,以便使用酷炫的“思考模型”提升问题解决能力。
动态内容?别担心!:假设您的客服人员能够实时创建专门针对每位用户量身定制的动态内容。我们将向您展示具体操作方法!
使用 Google Cloud 将其移至云端:别再只在笔记本电脑上玩耍了。我们将向您展示如何在 Google Cloud 上构建和部署多代理系统,以便其能够在真实环境中正常运行!
此项目将很好地展示如何使用我们之前介绍的所有技术。
2. 架构
成为一名教师或从事教育工作可能非常有成就感,但让我们面对现实吧,工作量(尤其是所有准备工作)可能非常繁重!此外,教师往往不足,而辅导费用可能很高。因此,我们提出了 AI 赋能的助教方案。此工具可以减轻教育工作者的负担,并帮助弥补因教职员短缺和缺少价格合理的辅导服务而导致的空白。
我们的 AI 教学助理可以快速制作详细的课程计划、有趣的测验、简单易懂的音频重温和个性化作业。这样,教师就可以专注于自己最擅长的事情:与学生建立联系,帮助他们爱上学习。
该系统有两个网站:一个供教师创建未来几周的课程计划,
一个用于教师上传课程内容,另一个用于学生访问测验、音频重温和作业。
好的,我们来了解一下为我们的教学助理 Aidemy 提供支持的架构。如您所见,我们将其分解为几个关键组件,所有这些组件协同工作才能实现此目的。
关键架构元素和技术:
Google Cloud Platform (GCP):整个系统的核心:
- Vertex AI:访问 Google 的 Gemini LLM。
- Cloud Run:用于部署容器化代理和函数的无服务器平台。
- Cloud SQL:用于存储课程数据的 PostgreSQL 数据库。
- Pub/Sub 和 Eventarc:事件驱动型架构的基础,支持组件之间的异步通信。
- Cloud Storage:用于存储音频重听和作业文件。
- Secret Manager:安全地管理数据库凭据。
- Artifact Registry:存储代理的 Docker 映像。
- Compute Engine:部署自托管 LLM,而不是依赖供应商解决方案
LLM:系统的“大脑”:
- Google 的 Gemini 模型:(Gemini 1.0 Pro、Gemini 2 Flash、Gemini 2 Flash Thinking、Gemini 1.5-pro)用于课程规划、内容生成、动态 HTML 创建、知识问答解说和组合作业。
- DeepSeek:用于生成自学作业的专门任务
LangChain 和 LangGraph:LLM 应用开发框架
- 有助于创建复杂的多代理工作流。
- 支持对工具(API 调用、数据库查询、网络搜索)进行智能编排。
- 实现事件驱动型架构,以实现系统可伸缩性和灵活性。
从本质上讲,我们的架构将 LLM 的强大功能与结构化数据和事件驱动型通信相结合,所有这些都运行在 Google Cloud 上。这样,我们就可以构建可扩缩、可靠且有效的助教。
3. 准备工作
在 Google Cloud 控制台的项目选择器页面上,选择或创建一个 Google Cloud 项目。确保您的 Cloud 项目已启用结算功能。了解如何检查项目是否已启用结算功能。
👉点击 Google Cloud 控制台顶部的激活 Cloud Shell(即 Cloud Shell 窗格顶部的终端形状图标),然后点击“打开编辑器”按钮(看起来像一个带有铅笔的打开文件夹)。此操作会在窗口中打开 Cloud Shell 代码编辑器。您会在左侧看到一个文件资源管理器。
👉点击底部状态栏中的 Cloud Code 登录按钮,如图所示。按照说明对插件进行授权。如果您在状态栏中看到 Cloud Code - no project,请选择该选项,然后在下拉菜单中选择“Select a Google Cloud Project”(选择 Google Cloud 项目),然后从您创建的项目列表中选择特定的 Google Cloud 项目。
👉在云端 IDE 中打开终端,
👉在终端中,使用以下命令验证您是否已通过身份验证,以及项目是否已设置为您的项目 ID:
gcloud auth list
👉 然后运行以下命令:
gcloud config set project <YOUR_PROJECT_ID>
👉运行以下命令以启用必要的 Google Cloud API:
gcloud services enable compute.googleapis.com \
storage.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
aiplatform.googleapis.com \
eventarc.googleapis.com \
sqladmin.googleapis.com \
secretmanager.googleapis.com \
cloudbuild.googleapis.com \
cloudresourcemanager.googleapis.com \
cloudfunctions.googleapis.com
这可能需要几分钟的时间。
在 Cloud Shell IDE 中启用 Gemini Code Assist
如图所示,点击左侧面板中的 Code Assist 按钮,然后再选择一次正确的 Google Cloud 项目。如果系统要求您启用 Cloud AI Companion API,请先执行此操作,然后再继续操作。选择 Google Cloud 项目后,请确保您能够在状态栏的 Cloud Code 状态消息中看到该项目,并且在状态栏右侧看到已启用 Code Assist,如下所示:
设置权限
👉设置服务账号权限
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
echo "Here's your SERVICE_ACCOUNT_NAME $SERVICE_ACCOUNT_NAME"
授予权限 👉Cloud Storage(读/写):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/storage.objectAdmin"
👉Pub/Sub(发布/接收):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/pubsub.publisher"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/pubsub.subscriber"
👉Cloud SQL(读/写):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/cloudsql.editor"
👉Eventarc(接收事件):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/iam.serviceAccountTokenCreator"
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/eventarc.eventReceiver"
👉Vertex AI(用户):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/aiplatform.user"
👉Secret Manager(读取):
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role="roles/secretmanager.secretAccessor"
👉在 IAM 控制台中验证结果
4. 构建第一个代理
在深入探讨复杂的多智能体系统之前,我们需要先建立一个基本构建块:单个功能智能体。在本部分中,我们将创建一个简单的“图书提供方”代理,以此为起点,逐步深入了解相关内容。图书提供方代理将类别作为输入,并使用 Gemini LLM 生成该类别中的 JSON 表示法图书。然后,它会以 REST API 端点的形式提供这些图书推荐。
👉在另一个浏览器标签页中,在网络浏览器中打开 Google Cloud 控制台,然后在导航菜单 (☰) 中,前往“Cloud Run”。点击“+... 编写函数”按钮。
👉接下来,我们将配置 Cloud Run 函数的基本设置:
- 服务名称:
book-provider
- 区域:
us-central1
- 运行时:
Python 3.12
- 身份验证:
Allow unauthenticated invocations
已更改为“已启用”。
👉将其他设置保留为默认值,然后点击创建。这会将您转到源代码编辑器。
您会看到预先填充的 main.py
和 requirements.txt
文件。
main.py
将包含函数的业务逻辑,requirements.txt
将包含所需的软件包。
👉现在,我们可以开始编写一些代码了!不过,在深入探究之前,我们先来看看 Gemini Code Assist 能否为我们提供帮助。返回 Cloud Shell 编辑器,点击 Gemini Code Assist 图标,然后将以下请求粘贴到提示框中:
Use the functions_framework library to be deployable as an HTTP function.
Accept a request with category and number_of_book parameters (either in JSON body or query string).
Use langchain and gemini to generate the data for book with fields bookname, author, publisher, publishing_date.
Use pydantic to define a Book model with the fields: bookname (string, description: "Name of the book"), author (string, description: "Name of the author"), publisher (string, description: "Name of the publisher"), and publishing_date (string, description: "Date of publishing").
Use langchain and gemini model to generate book data. the output should follow the format defined in Book model.
The logic should use JsonOutputParser from langchain to enforce output format defined in Book Model.
Have a function get_recommended_books(category) that internally uses langchain and gemini to return a single book object.
The main function, exposed as the Cloud Function, should call get_recommended_books() multiple times (based on number_of_book) and return a JSON list of the generated book objects.
Handle the case where category or number_of_book are missing by returning an error JSON response with a 400 status code.
return a JSON string representing the recommended books. use os library to retrieve GOOGLE_CLOUD_PROJECT env var. Use ChatVertexAI from langchain for the LLM call
然后,Code Assist 会生成一个可能的解决方案,同时提供源代码和 requirements.txt 依赖项文件。
我们建议您将 Code Assist 生成的代码与下方提供的经过测试的正确解决方案进行比较。这样,您就可以评估该工具的效果,并找出任何可能的差异。虽然绝不应盲目信任 LLM,但 Code Assist 是一款非常适合快速原型设计和生成初始代码结构的工具,应用于快速上手。
由于这是研讨会,因此我们将使用下方提供的已验证代码。不过,您可以随时自由地试验 Code Assist 生成的代码,以便更深入地了解其功能和局限性。
👉返回 Cloud Run 函数的源代码编辑器(在另一个浏览器标签页中)。仔细将 main.py
的现有内容替换为以下代码:
import functions_framework
import json
from flask import Flask, jsonify, request
from langchain_google_vertexai import ChatVertexAI
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from pydantic import BaseModel, Field
import os
class Book(BaseModel):
bookname: str = Field(description="Name of the book")
author: str = Field(description="Name of the author")
publisher: str = Field(description="Name of the publisher")
publishing_date: str = Field(description="Date of publishing")
project_id = os.environ.get("GOOGLE_CLOUD_PROJECT")
llm = ChatVertexAI(model_name="gemini-1.0-pro")
def get_recommended_books(category):
"""
A simple book recommendation function.
Args:
category (str): category
Returns:
str: A JSON string representing the recommended books.
"""
parser = JsonOutputParser(pydantic_object=Book)
question = f"Generate a random made up book on {category} with bookname, author and publisher and publishing_date"
prompt = PromptTemplate(
template="Answer the user query.\n{format_instructions}\n{query}\n",
input_variables=["query"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
chain = prompt | llm | parser
response = chain.invoke({"query": question})
return json.dumps(response)
@functions_framework.http
def recommended(request):
request_json = request.get_json(silent=True) # Get JSON data
if request_json and 'category' in request_json and 'number_of_book' in request_json:
category = request_json['category']
number_of_book = int(request_json['number_of_book'])
elif request.args and 'category' in request.args and 'number_of_book' in request.args:
category = request.args.get('category')
number_of_book = int(request.args.get('number_of_book'))
else:
return jsonify({'error': 'Missing category or number_of_book parameters'}), 400
recommendations_list = []
for i in range(number_of_book):
book_dict = json.loads(get_recommended_books(category))
print(f"book_dict=======>{book_dict}")
recommendations_list.append(book_dict)
return jsonify(recommendations_list)
👉将 requirements.txt 的内容替换为以下内容:
functions-framework==3.*
google-genai==1.0.0
flask==3.1.0
jsonify==0.5
langchain_google_vertexai==2.0.13
langchain_core==0.3.34
pydantic==2.10.5
👉我们将设置函数入口点:recommended
👉点击保存并部署以部署函数。等待部署过程完成。Cloud 控制台会显示状态。这可能需要几分钟。
👉部署完成后,返回 Cloud Shell 编辑器,在终端中运行以下命令:
export PROJECT_ID=$(gcloud config get project)
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
curl -X POST -H "Content-Type: application/json" -d '{"category": "Science Fiction", "number_of_book": 2}' $BOOK_PROVIDER_URL
它应显示一些 JSON 格式的图书数据。
[
{"author":"Anya Sharma","bookname":"Echoes of the Singularity","publisher":"NovaLight Publishing","publishing_date":"2077-03-15"},
{"author":"Anya Sharma","bookname":"Echoes of the Quantum Dawn","publisher":"Nova Genesis Publishing","publishing_date":"2077-03-15"}
]
恭喜!您已成功部署 Cloud Run 函数。这是我们在开发 Aidemy 客服人员时将集成的服务之一。
5. 构建工具:将代理连接到 RESTFUL 服务和数据
接下来,我们将下载 Bootstrap 框架项目,请确保您使用的是 Cloud Shell 编辑器。在终端中运行以下命令:
git clone https://github.com/weimeilin79/aidemy-bootstrap.git
运行此命令后,Cloud Shell 环境中会创建一个名为 aidemy-bootstrap
的新文件夹。
现在,您应该会在 Cloud Shell 编辑器的“Explorer”窗格(通常位于左侧)中看到在克隆 Git 代码库 aidemy-bootstrap
时创建的文件夹。在资源管理器中打开项目的根文件夹。您会在其中找到一个 planner
子文件夹,请一并打开它。
我们开始构建客服人员将用来提供实用帮助的工具。如您所知,LLM 擅长推理和生成文本,但它们需要访问外部资源才能执行实际任务并提供准确、最新的信息。不妨将这些工具视为代理的“瑞士军刀”,让其能够与外界互动。
构建代理时,很容易陷入硬编码大量详细信息的误区。这会创建一个不灵活的代理。相反,通过创建和使用工具,代理可以访问外部逻辑或系统,从而同时获得 LLM 和传统编程的优势。
在本部分中,我们将为规划助理创建基础,教师将使用该助理生成课程计划。在该助理开始生成计划之前,我们希望通过提供有关学科和主题的更多详细信息来设置边界。我们将构建三个工具:
- Restful API 调用:与现有 API 交互以检索数据。
- 数据库查询:从 Cloud SQL 数据库中提取结构化数据。
- Google 搜索:访问网络上的实时信息。
从 API 提取图书推荐
首先,我们来创建一个工具,用于从我们在上一部分中部署的 book-provider API 检索图书推荐。这演示了代理如何利用现有服务。
在 Cloud Shell Editor 中,打开您在上一部分中克隆的 aidemy-bootstrap
项目。👉修改 planner
文件夹中的 book.py
,然后粘贴以下代码:
def recommend_book(query: str):
"""
Get a list of recommended book from an API endpoint
Args:
query: User's request string
"""
region = get_next_region();
llm = VertexAI(model_name="gemini-1.5-pro", location=region)
query = f"""The user is trying to plan a education course, you are the teaching assistant. Help define the category of what the user requested to teach, respond the categroy with no more than two word.
user request: {query}
"""
print(f"-------->{query}")
response = llm.invoke(query)
print(f"CATEGORY RESPONSE------------>: {response}")
# call this using python and parse the json back to dict
category = response.strip()
headers = {"Content-Type": "application/json"}
data = {"category": category, "number_of_book": 2}
books = requests.post(BOOK_PROVIDER_URL, headers=headers, json=data)
return books.text
if __name__ == "__main__":
print(recommend_book("I'm doing a course for my 5th grade student on Math Geometry, I'll need to recommend few books come up with a teach plan, few quizes and also a homework assignment."))
说明:
- recommend_book(query: str):此函数将用户的查询作为输入。
- LLM 互动:使用 LLM 从查询中提取类别。这演示了如何使用 LLM 帮助创建工具参数。
- API 调用:向图书提供方 API 发出 POST 请求,传递类别和所需的图书数量。
👉如需测试此新函数,请设置环境变量,然后运行以下命令:
cd ~/aidemy-bootstrap/planner/
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
👉安装依赖项并运行代码以确保其正常运行,请运行以下命令:
cd ~/aidemy-bootstrap/planner/
python -m venv env
source env/bin/activate
export PROJECT_ID=$(gcloud config get project)
pip install -r requirements.txt
python book.py
忽略 Git 警告弹出式窗口。
您应该会看到一个 JSON 字符串,其中包含从 book-provider API 检索到的图书推荐。
[{"author":"Anya Sharma","bookname":"Echoes of the Singularity","publisher":"NovaLight Publishing","publishing_date":"2077-03-15"},{"author":"Anya Sharma","bookname":"Echoes of the Quantum Dawn","publisher":"Nova Genesis Publishing","publishing_date":"2077-03-15"}]
如果您看到此消息,则表示第一个工具正常运行!
我们使用自然语言(“我正在学习课程...”),而不是使用特定参数显式构建 RESTful API 调用。然后,代理会使用 NLP 智能提取必要的参数(例如类别),突出显示代理如何利用自然语言理解与 API 交互。
👉从 book.py
中移除以下测试代码
if __name__ == "__main__":
print(recommend_book("I'm doing a course for my 5th grade student on Math Geometry, I'll need to recommend few books come up with a teach plan, few quizes and also a homework assignment."))
从数据库中获取课程数据
接下来,我们将构建一个工具,用于从 Cloud SQL PostgreSQL 数据库中提取结构化课程数据。这样,客服人员就可以访问可靠的信息来源,以便制定课程计划。
👉在终端中运行以下命令,创建一个名为 aidemy 的 Cloud SQL 实例。此过程可能需要一些时间。
gcloud sql instances create aidemy \
--database-version=POSTGRES_14 \
--cpu=2 \
--memory=4GB \
--region=us-central1 \
--root-password=1234qwer \
--storage-size=10GB \
--storage-auto-increase
👉接下来,在新实例中创建一个名为 aidemy-db
的数据库。
gcloud sql databases create aidemy-db \
--instance=aidemy
我们来在 Google Cloud 控制台中的 Cloud SQL 中验证实例,您应该会看到名为 aidemy
的 Cloud SQL 实例。点击实例名称以查看其详细信息。在 Cloud SQL 实例详情页面中,点击左侧导航菜单中的“SQL Studio”。系统随即会打开一个新标签页。
点击以连接到数据库。登录 SQL Studio
选择 aidemy-db
作为数据库。输入 postgres
作为用户,并输入 1234qwer
作为密码。
👉在 SQL Studio 查询编辑器中,粘贴以下 SQL 代码:
CREATE TABLE curriculums (
id SERIAL PRIMARY KEY,
year INT,
subject VARCHAR(255),
description TEXT
);
-- Inserting detailed curriculum data for different school years and subjects
INSERT INTO curriculums (year, subject, description) VALUES
-- Year 5
(5, 'Mathematics', 'Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.'),
(5, 'English', 'Developing reading comprehension, creative writing, and basic grammar, with a focus on storytelling and poetry.'),
(5, 'Science', 'Exploring basic physics, chemistry, and biology concepts, including forces, materials, and ecosystems.'),
(5, 'Computer Science', 'Basic coding concepts using block-based programming and an introduction to digital literacy.'),
-- Year 6
(6, 'Mathematics', 'Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.'),
(6, 'English', 'Introduction to persuasive writing, character analysis, and deeper comprehension of literary texts.'),
(6, 'Science', 'Forces and motion, the human body, and introductory chemical reactions with hands-on experiments.'),
(6, 'Computer Science', 'Introduction to algorithms, logical reasoning, and basic text-based programming (Python, Scratch).'),
-- Year 7
(7, 'Mathematics', 'Algebraic expressions, geometry, and introduction to statistics and probability.'),
(7, 'English', 'Analytical reading of classic and modern literature, essay writing, and advanced grammar skills.'),
(7, 'Science', 'Introduction to cells and organisms, chemical reactions, and energy transfer in physics.'),
(7, 'Computer Science', 'Building on programming skills with Python, introduction to web development, and cyber safety.');
以下 SQL 代码会创建一个名为 curriculums
的表,并插入一些示例数据。点击运行以执行 SQL 代码。您应该会看到一条确认消息,指示命令已成功执行。
👉展开“探索器”,找到新创建的表,然后点击查询。系统应该会打开一个新的编辑器标签页,其中包含为您生成的 SQL,
SELECT * FROM
"public"."curriculums" LIMIT 1000;
👉点击运行。
结果表中应显示您在上一步中插入的数据行,这表示表格和数据已正确创建。
现在,您已成功创建了一个包含填充的示例课程数据的数据库,接下来我们将构建一个用于检索该数据库的工具。
👉在 Cloud Code Editor 中,修改 aidemy-bootstrap
文件夹中的 curriculums.py
文件,然后粘贴以下代码:
def connect_with_connector() -> sqlalchemy.engine.base.Engine:
db_user = os.environ["DB_USER"]
db_pass = os.environ["DB_PASS"]
db_name = os.environ["DB_NAME"]
encoded_db_user = os.environ.get("DB_USER")
print(f"--------------------------->db_user: {db_user!r}")
print(f"--------------------------->db_pass: {db_pass!r}")
print(f"--------------------------->db_name: {db_name!r}")
ip_type = IPTypes.PRIVATE if os.environ.get("PRIVATE_IP") else IPTypes.PUBLIC
connector = Connector()
def getconn() -> pg8000.dbapi.Connection:
conn: pg8000.dbapi.Connection = connector.connect(
instance_connection_name,
"pg8000",
user=db_user,
password=db_pass,
db=db_name,
ip_type=ip_type,
)
return conn
pool = sqlalchemy.create_engine(
"postgresql+pg8000://",
creator=getconn,
pool_size=2,
max_overflow=2,
pool_timeout=30, # 30 seconds
pool_recycle=1800, # 30 minutes
)
return pool
def init_connection_pool() -> sqlalchemy.engine.base.Engine:
return (
connect_with_connector()
)
raise ValueError(
"Missing database connection type. Please define one of INSTANCE_HOST, INSTANCE_UNIX_SOCKET, or INSTANCE_CONNECTION_NAME"
)
def get_curriculum(year: int, subject: str):
"""
Get school curriculum
Args:
subject: User's request subject string
year: User's request year int
"""
try:
stmt = sqlalchemy.text(
"SELECT description FROM curriculums WHERE year = :year AND subject = :subject"
)
with db.connect() as conn:
result = conn.execute(stmt, parameters={"year": year, "subject": subject})
row = result.fetchone()
if row:
return row[0]
else:
return None
except Exception as e:
print(e)
return None
db = init_connection_pool()
说明:
- 环境变量:代码会从环境变量中检索数据库凭据和连接信息(详见下文)。
- connect_with_connector():此函数使用 Cloud SQL 连接器与数据库建立安全连接。
- get_curriculum(year: int, subject: str):此函数将年份和科目作为输入,查询 curriculums 表,并返回相应的课程说明。
👉在运行代码之前,我们必须设置一些环境变量,在终端中运行以下命令:
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉如需进行测试,请将以下代码添加到 curriculums.py
的末尾:
if __name__ == "__main__":
print(get_curriculum(6, "Mathematics"))
👉运行代码:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python curriculums.py
您应该会在控制台上看到第 6 年级数学课程说明。
Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.
如果您看到课程说明,则表示数据库工具正常运行!继续操作,按 Ctrl+C
停止脚本。
👉从 curriculums.py
中移除以下测试代码
if __name__ == "__main__":
print(get_curriculum(6, "Mathematics"))
👉退出虚拟环境,在终端中运行以下命令:
deactivate
6. 构建工具:从网络获取实时信息
最后,我们将构建一个工具,该工具可使用 Gemini 2 与 Google 搜索的集成来访问网络上的实时信息。这有助于客服人员及时了解最新动态,并提供相关结果。
Gemini 2 与 Google Search API 的集成可提供更准确且与上下文更相关的搜索结果,从而增强客服人员的功能。这样,代理便可获取最新信息,并根据真实数据做出回答,最大限度地减少幻觉。改进后的 API 集成还支持更多自然语言查询,让客服人员能够制定复杂且细致的搜索请求。
此函数将搜索查询、课程、科目和年份作为输入,并使用 Gemini API 和 Google 搜索工具从互联网检索相关信息。如果您仔细观察,就会发现它使用 Google 生成式 AI SDK 进行函数调用,而无需使用任何其他框架。
👉修改 aidemy-bootstrap
文件夹中的 search.py
,然后粘贴以下代码:
model_id = "gemini-2.0-flash-001"
google_search_tool = Tool(
google_search = GoogleSearch()
)
def search_latest_resource(search_text: str, curriculum: str, subject: str, year: int):
"""
Get latest information from the internet
Args:
search_text: User's request category string
subject: "User's request subject" string
year: "User's request year" integer
"""
search_text = "%s in the context of year %d and subject %s with following curriculum detail %s " % (search_text, year, subject, curriculum)
region = get_next_region()
client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
print(f"search_latest_resource text-----> {search_text}")
response = client.models.generate_content(
model=model_id,
contents=search_text,
config=GenerateContentConfig(
tools=[google_search_tool],
response_modalities=["TEXT"],
)
)
print(f"search_latest_resource response-----> {response}")
return response
if __name__ == "__main__":
response = search_latest_resource("What are the syllabus for Year 6 Mathematics?", "Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.", "Mathematics", 6)
for each in response.candidates[0].content.parts:
print(each.text)
说明:
- 定义工具 - google_search_tool:将 GoogleSearch 对象封装在工具中
- search_latest_resource(search_text: str, subject: str, year: int):此函数将搜索查询、主题和年份作为输入,并使用 Gemini API 执行 Google 搜索。Gemini 模型
- GenerateContentConfig:定义它有权访问 GoogleSearch 工具
Gemini 模型会在内部分析 search_text,并确定它能否直接回答问题,还是需要使用 GoogleSearch 工具。这是 LLM 推理过程中的关键步骤。该模型已训练为识别需要使用外部工具的情况。如果模型决定使用 GoogleSearch 工具,Google 生成式 AI SDK 会处理实际调用。SDK 会获取模型的决策及其生成的参数,并将其发送到 Google Search API。此部分在代码中对用户隐藏。
然后,Gemini 模型会将搜索结果集成到其回答中。它可以使用这些信息来回答用户的问题、生成摘要或执行其他任务。
👉如需进行测试,请运行以下代码:
cd ~/aidemy-bootstrap/planner/
export PROJECT_ID=$(gcloud config get project)
source env/bin/activate
python search.py
您应该会看到 Gemini Search API 响应,其中包含与“第 5 年级数学课程大纲”相关的搜索结果。确切输出取决于搜索结果,但它将是一个包含搜索相关信息的 JSON 对象。
如果您看到搜索结果,则表示 Google 搜索工具正常运行!继续操作,按 Ctrl+C
停止脚本。
👉然后移除代码中的最后一部分。
if __name__ == "__main__":
response = search_latest_resource("What are the syllabus for Year 6 Mathematics?", "Expanding on fractions, ratios, algebraic thinking, and problem-solving strategies.", "Mathematics", 6)
for each in response.candidates[0].content.parts:
print(each.text)
👉退出虚拟环境,在终端中运行以下命令:
deactivate
恭喜!现在,您已为规划助理构建了三种强大的工具:API 连接器、数据库连接器和 Google 搜索工具。借助这些工具,客服人员可以访问创建有效教学计划所需的信息和功能。
7. 使用 LangGraph 进行编排
现在,我们已经构建了各个工具,接下来可以使用 LangGraph 来协调它们了。这样,我们就可以创建更复杂的“规划者”代理,该代理可以根据用户的请求智能地决定何时使用哪些工具。
LangGraph 是一个 Python 库,旨在帮助您更轻松地使用大型语言模型 (LLM) 构建有状态的多角色应用。您可以将其视为一个框架,用于编排涉及 LLM、工具和其他代理的复杂对话和工作流。
主要概念:
- 图结构:LangGraph 将应用的逻辑表示为有向图。图中的每个节点都代表流程中的一个步骤(例如,对 LLM 的调用、工具调用、条件检查)。边用于定义节点之间的执行流。
- 状态:LangGraph 会在应用在图表中移动时管理其状态。此状态可以包括用户输入、工具调用的结果、LLM 的中间输出以及需要在步骤之间保留的任何其他信息等变量。
- 节点:每个节点代表一项计算或互动。这些来源可以是:
- 工具节点:使用工具(例如执行网页搜索、查询数据库)
- 函数节点:执行 Python 函数。
- 边:连接节点,定义执行流程。这些来源可以是:
- 直接边缘:从一个节点到另一个节点的简单无条件流。
- 条件边:流程取决于条件节点的结果。
我们将使用 LangGraph 来实现编排。我们来修改 aidemy-bootstrap
文件夹下的 aidemy.py
文件,以定义 LangGraph 逻辑。👉将以下代码附加到 aidemy.py
的末尾:
tools = [get_curriculum, search_latest_resource, recommend_book]
def determine_tool(state: MessagesState):
llm = ChatVertexAI(model_name="gemini-2.0-flash-001", location=get_next_region())
sys_msg = SystemMessage(
content=(
f"""You are a helpful teaching assistant that helps gather all needed information.
Your ultimate goal is to create a detailed 3-week teaching plan.
You have access to tools that help you gather information.
Based on the user request, decide which tool(s) are needed.
"""
)
)
llm_with_tools = llm.bind_tools(tools)
return {"messages": llm_with_tools.invoke([sys_msg] + state["messages"])}
此函数负责获取对话的当前状态,为 LLM 提供系统消息,然后请求 LLM 生成回答。LLM 可以直接回复用户,也可以选择使用某种可用工具。
tools:此列表表示代理可用的工具集。它包含我们在前面的步骤中定义的三个工具函数:get_curriculum
、search_latest_resource
和 recommend_book
。llm.bind_tools(tools):用于将工具列表“绑定”到 llm 对象。绑定工具会告知 LLM 这些工具可用,并向 LLM 提供有关如何使用这些工具的信息(例如工具的名称、它们接受的参数以及它们的用途)。
我们将使用 LangGraph 来实现编排。👉将以下代码附加到 aidemy.py
的末尾:
def prep_class(prep_needs):
builder = StateGraph(MessagesState)
builder.add_node("determine_tool", determine_tool)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "determine_tool")
builder.add_conditional_edges("determine_tool",tools_condition)
builder.add_edge("tools", "determine_tool")
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}
messages = graph.invoke({"messages": prep_needs},config)
print(messages)
for m in messages['messages']:
m.pretty_print()
teaching_plan_result = messages["messages"][-1].content
return teaching_plan_result
if __name__ == "__main__":
prep_class("I'm doing a course for year 5 on subject Mathematics in Geometry, , get school curriculum, and come up with few books recommendation plus search latest resources on the internet base on the curriculum outcome. And come up with a 3 week teaching plan")
说明:
StateGraph(MessagesState)
:创建StateGraph
对象。StateGraph
是 LangGraph 中的核心概念。它以图的形式表示代理的工作流,其中图中的每个节点代表流程中的一个步骤。您可以将其视为定义智能助理推理和行动方式的蓝图。- 条件边缘:
tools_condition
实参源自"determine_tool"
节点,可能是根据determine_tool
函数的输出确定要沿哪条边缘进行搜索的函数。借助条件边,图表可以根据 LLM 决定使用哪种工具(或是否直接回复用户)而分支。这时,代理的“智能”便发挥作用了,它可以根据情况动态调整自己的行为。 - 循环:向图表中添加一条边,将
"tools"
节点连接回"determine_tool"
节点。这会在图表中创建一个循环,让客服人员反复使用工具,直到收集到足够的信息来完成任务并提供令人满意的回答。对于需要多步推理和信息收集的复杂任务,此循环至关重要。
现在,我们来测试一下规划程序代理,看看它如何协调不同的工具。
此代码将使用特定用户输入运行 prep_class 函数,模拟使用课程、图书推荐和最新的互联网资源为第 5 年级的“几何图形”数学课程创建教学计划的请求。
如果您关闭了终端或环境变量已不再设置,请重新运行以下命令
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉运行代码:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
pip install -r requirements.txt
python aidemy.py
在终端中查看日志。您应该能看到证据,证明客服人员在提供最终教学计划之前调用了所有三种工具(获取学校课程、获取图书推荐和搜索最新资源)。这表明 LangGraph 编排正常运行,并且代理正在智能地使用所有可用工具来满足用户的请求。
================================ Human Message =================================
I'm doing a course for year 5 on subject Mathematics in Geometry, , get school curriculum, and come up with few books recommendation plus search latest resources on the internet base on the curriculum outcome. And come up with a 3 week teaching plan
================================== Ai Message ==================================
Tool Calls:
get_curriculum (xxx)
Call ID: xxx
Args:
year: 5.0
subject: Mathematics
================================= Tool Message =================================
Name: get_curriculum
Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.
================================== Ai Message ==================================
Tool Calls:
search_latest_resource (xxxx)
Call ID: xxxx
Args:
year: 5.0
search_text: Geometry
curriculum: {"content": "Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques."}
subject: Mathematics
================================= Tool Message =================================
Name: search_latest_resource
candidates=[Candidate(content=Content(parts=[Part(.....) automatic_function_calling_history=[] parsed=None
================================== Ai Message ==================================
Tool Calls:
recommend_book (93b48189-4d69-4c09-a3bd-4e60cdc5f1c6)
Call ID: 93b48189-4d69-4c09-a3bd-4e60cdc5f1c6
Args:
query: Mathematics Geometry Year 5
================================= Tool Message =================================
Name: recommend_book
[{.....}]
================================== Ai Message ==================================
Based on the curriculum outcome, here is a 3-week teaching plan for year 5 Mathematics Geometry:
**Week 1: Introduction to Shapes and Properties**
.........
按 Ctrl+C
停止该脚本。
👉现在,将测试代码替换为其他提示,该提示需要调用不同的工具。
if __name__ == "__main__":
prep_class("I'm doing a course for year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan")
如果您关闭了终端或环境变量已不再设置,请重新运行以下命令
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉再次运行代码:
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python aidemy.py
这次您注意到了什么?客服人员调用了哪些工具?您应该会发现,这次代理只调用了 search_latest_resource 工具。这是因为提示未指定它需要另外两个工具,而我们的 LLM 足够智能,不会调用其他工具。
================================ Human Message =================================
I'm doing a course for year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan
================================== Ai Message ==================================
Tool Calls:
get_curriculum (xxx)
Call ID: xxx
Args:
year: 5.0
subject: Mathematics
================================= Tool Message =================================
Name: get_curriculum
Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques.
================================== Ai Message ==================================
Tool Calls:
search_latest_resource (xxx)
Call ID: xxxx
Args:
year: 5.0
subject: Mathematics
curriculum: {"content": "Introduction to fractions, decimals, and percentages, along with foundational geometry and problem-solving techniques."}
search_text: Geometry
================================= Tool Message =================================
Name: search_latest_resource
candidates=[Candidate(content=Content(parts=[Part(.......token_count=40, total_token_count=772) automatic_function_calling_history=[] parsed=None
================================== Ai Message ==================================
Based on the information provided, a 3-week teaching plan for Year 5 Mathematics focusing on Geometry could look like this:
**Week 1: Introducing 2D Shapes**
........
* Use visuals, manipulatives, and real-world examples to make the learning experience engaging and relevant.
按 Ctrl+C
停止该脚本。👉移除测试代码,以保持 aidemy.py 文件的整洁:
if __name__ == "__main__":
prep_class("I'm doing a course for year 5 on subject Mathematics in Geometry, search latest resources on the internet base on the subject. And come up with a 3 week teaching plan")
现在,我们已经定义了代理逻辑,接下来,我们来启动 Flask Web 应用。这样,教师就可以通过熟悉的基于表单的界面与客服人员互动。虽然聊天机器人与 LLM 的互动很常见,但我们选择了传统的表单提交界面,因为对许多教育工作者来说,这种界面可能更直观。
如果您关闭了终端或环境变量已不再设置,请重新运行以下命令
export BOOK_PROVIDER_URL=$(gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)")
export PROJECT_ID=$(gcloud config get project)
export INSTANCE_NAME="aidemy"
export REGION="us-central1"
export DB_USER="postgres"
export DB_PASS="1234qwer"
export DB_NAME="aidemy-db"
👉现在,启动 Web 界面。
cd ~/aidemy-bootstrap/planner/
source env/bin/activate
python app.py
在 Cloud Shell 终端输出中查找启动消息。Flask 通常会输出消息,指明它正在运行以及运行在哪个端口上。
Running on http://127.0.0.1:8080
Running on http://127.0.0.1:8080
The application needs to keep running to serve requests.
👉从“网页预览”菜单中,选择“在端口 8080 上预览”。Cloud Shell 会打开一个新的浏览器标签页或窗口,其中显示应用的 Web 预览。
在应用界面中,选择年份 5
,选择主题 Mathematics
,然后在插件请求 中输入
Geometry
在等待响应时,请不要干瞪眼,而是切换到 Cloud Shell Editor 的终端。您可以在模拟器的终端中观察进度以及函数生成的任何输出或错误消息。😁
👉在终端中按 Ctrl+C
停止脚本。
👉退出虚拟环境:
deactivate
8. 将 Planner 代理部署到云端
构建映像并将其推送到注册表
👉现在,我们将其部署到云端。在终端中,创建一个工件仓库来存储我们要构建的 Docker 映像。
gcloud artifacts repositories create agent-repository \
--repository-format=docker \
--location=us-central1 \
--description="My agent repository"
您应该会看到“Created repository [agent-repository]”(已创建代码库 [agent-repository])。
👉运行以下命令以构建 Docker 映像。
cd ~/aidemy-bootstrap/planner/
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .
👉我们需要重新标记映像,以便将其托管在 Artifact Registry 中(而不是 GCR),并将标记的映像推送到 Artifact Registry:
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
推送完成后,您可以验证映像是否已成功存储在 Artifact Registry 中。前往 Google Cloud 控制台中的 Artifact Registry。您应该会在 agent-repository
代码库中找到 aidemy-planner
映像。
使用 Secret Manager 保护数据库凭据
为了安全地管理和访问数据库凭据,我们将使用 Google Cloud Secret Manager。这样可以防止在应用代码中硬编码敏感信息,并增强安全性。
👉我们将为数据库用户名、密码和数据库名称分别创建 Secret。这样,我们就可以独立管理每个凭据。在终端中运行以下命令:
gcloud secrets create db-user
printf "postgres" | gcloud secrets versions add db-user --data-file=-
gcloud secrets create db-pass
printf "1234qwer" | gcloud secrets versions add db-pass --data-file=-
gcloud secrets create db-name
printf "aidemy-db" | gcloud secrets versions add db-name --data-file=-
使用 Secret Manager 是保护应用并防止敏感凭据意外泄露的重要一步。它遵循适用于云部署的安全最佳实践。
部署到 Cloud Run
Cloud Run 是一个全代管式无服务器平台,可让您快速轻松地部署容器化应用。它可以抽象出基础架构管理,让您专注于编写和部署代码。我们将将规划工具部署为 Cloud Run 服务。
👉在 Google Cloud 控制台中,前往“Cloud Run”。点击部署容器,然后选择服务。配置 Cloud Run 服务:
- 容器映像:点击“网址”字段中的“选择”。找到您推送到 Artifact Registry 的映像网址(例如 us-central1-docker.pkg.dev/YOUR_PROJECT_ID/agent-repository/agent-planner/YOUR_IMG)。
- 服务名称:
aidemy-planner
- 区域:选择
us-central1
区域。 - 身份验证:为本研讨会之目的,您可以允许“允许未经身份验证的调用”。对于生产环境,您可能需要限制访问权限。
- 容器标签页(展开“容器”和“网络”):
- “设置”标签页:
- 资源
- 内存:2GB
- 资源
- “变量和 Secret”标签页:
- 环境变量:
- 添加名称:
GOOGLE_CLOUD_PROJECT
和值:<YOUR_PROJECT_ID> - 添加名称:
BOOK_PROVIDER_URL
和值:<YOUR_BOOK_PROVIDER_FUNCTION_网址>
- 添加名称:
- 作为环境变量公开的 Secret:
- 添加名称:
DB_USER
,密钥:选择db-user
,版本:latest
- 添加名称:
DB_PASS
,密钥:选择db-pass
,版本:latest
- 添加名称:
DB_NAME
,密钥:选择db-name
,版本:latest
- 添加名称:
- 环境变量:
- “设置”标签页:
如果您需要检索 YOUR_BOOK_PROVIDER_FUNCTION_网址,请在终端中运行以下命令:
gcloud run services describe book-provider --region=us-central1 --project=$PROJECT_ID --format="value(status.url)"
将“其他”保留为默认值。
👉点击创建。
Cloud Run 将部署您的服务。
部署完成后,点击该服务可前往其详情页面,您可以在顶部找到已部署的网址。
在应用界面中,为“年份”选择 7
,选择 Mathematics
作为主题,然后在“插件请求”字段中输入 Algebra
。这将为客服人员提供生成量身定制课程计划所需的上下文。
恭喜!您已成功使用我们强大的 AI 助理创建教学计划。这表明,代理可以显著减少工作量并简化任务,最终提高效率并让教育工作者的生活更轻松。
9. 多代理系统
现在,我们已成功实现教学计划创建工具,接下来将重点转移到构建学生门户。学生可以通过此门户访问与其课程相关的测验、音频重温和作业。鉴于此功能的范围,我们将利用多代理系统的强大功能来创建模块化且可扩缩的解决方案。
如前所述,多代理系统可让我们将工作负载拆分为更小的专门任务,每个任务由专门的代理处理,而不是依赖单个代理来处理所有事务。这种方法具有以下几个关键优势:
模块化和可维护性:不要创建一个负责所有事务的单个代理,而是构建具有明确定义的职责的小型专用代理。这种模块化结构使系统更易于理解、维护和调试。出现问题时,您可以将其隔离到特定代理,而不必仔细筛查庞大的代码库。
可伸缩性:扩缩单个复杂的代理可能会成为瓶颈。借助多代理系统,您可以根据各个代理的具体需求进行扩缩。例如,如果一个代理正在处理大量请求,您可以轻松启动该代理的更多实例,而不会影响系统的其余部分。
团队专业化:举个例子:您不会要求一位工程师从头开始构建整个应用。而是组建一支由各个领域的专家组成的团队。同样,借助多代理系统,您可以利用不同 LLM 和工具的优势,将它们分配给最适合执行特定任务的代理。
并行开发:不同的团队可以同时处理不同的代理,从而加快开发流程。由于代理是独立的,因此对一个代理所做的更改不太可能影响其他代理。
事件驱动型架构
为了实现这些代理之间的有效通信和协调,我们将采用事件驱动型架构。这意味着代理会对系统中发生的“事件”做出响应。
代理订阅特定事件类型(例如“教学计划生成”“作业创建”)。当有事件发生时,相关代理会收到通知,并可以相应地做出响应。这种解耦有助于提高灵活性、可伸缩性和实时响应能力。
现在,首先,我们需要一种广播这些事件的方法。为此,我们将设置一个 Pub/Sub 主题。首先,创建一个名为 plan 的主题。
👉前往 Google Cloud 控制台 Pub/Sub,然后点击“创建主题”按钮。
👉配置 ID/名称为 plan
的主题,然后取消选中 Add a default subscription
,将其余设置保留为默认值,然后点击创建。
Pub/Sub 页面将刷新,您现在应该会在表格中看到新创建的主题。
现在,我们将 Pub/Sub 事件发布功能集成到我们的规划程序代理中。我们将添加一个新工具,用于向我们刚刚创建的 Pub/Sub 主题发送“plan”事件。此事件将向系统中的其他代理(例如学生门户中的代理)发出新教学计划可用的信号。
👉返回 Cloud 代码编辑器,打开位于 planner
文件夹中的 app.py
文件。我们将添加一个用于发布事件的函数。您需要进行如下替换:
##ADD SEND PLAN EVENT FUNCTION HERE
通过
def send_plan_event(teaching_plan:str):
"""
Send the teaching event to the topic called plan
Args:
teaching_plan: teaching plan
"""
publisher = pubsub_v1.PublisherClient()
print(f"-------------> Sending event to topic plan: {teaching_plan}")
topic_path = publisher.topic_path(PROJECT_ID, "plan")
message_data = {"teaching_plan": teaching_plan}
data = json.dumps(message_data).encode("utf-8")
future = publisher.publish(topic_path, data)
return f"Published message ID: {future.result()}"
- send_plan_event:此函数将生成的教学计划作为输入,创建 Pub/Sub 发布方客户端,构建主题路径,将教学计划转换为 JSON 字符串,并将消息发布到主题。
- 工具列表:
send_plan_event
函数会添加到工具列表中,以便智能体使用。
在同一文件夹下的 app.py
文件中,更新提示,以指示代理在生成教学计划后将教学计划事件发送到 Pub/Sub 主题。替换
### ADD send_plan_event CALL
并包含以下内容:
send_plan_event(teaching_plan)
通过添加 send_plan_event 工具并修改提示,我们已使规划器代理能够将事件发布到 Pub/Sub,从而允许系统的其他组件对创建新的教学计划做出响应。现在,我们将在后续部分构建一个功能齐全的多代理系统。
10. 利用随需测验赋予学生力量
想象一下这样的学习环境:学生可以获得量身定制的无限量知识问答,这些知识问答会提供即时反馈,包括答案和解答,从而帮助学生更深入地理解所学内容。我们希望通过 AI 赋能的知识问答门户,充分发挥这方面的潜力。
为了实现这一愿景,我们将构建一个测验生成组件,该组件可以根据教学计划的内容创建单选题。
👉在 Cloud Code Editor 的“Explorer”窗格中,找到 portal
文件夹。打开 quiz.py
文件,然后复制并将以下代码粘贴到文件末尾。
def generate_quiz_question(file_name: str, difficulty: str, region:str ):
"""Generates a single multiple-choice quiz question using the LLM.
```json
{
"question": "The question itself",
"options": ["Option A", "Option B", "Option C", "Option D"],
"answer": "The correct answer letter (A, B, C, or D)"
}
```
"""
print(f"region: {region}")
# Connect to resourse needed from Google Cloud
llm = VertexAI(model_name="gemini-1.5-pro", location=region)
plan=None
#load the file using file_name and read content into string call plan
with open(file_name, 'r') as f:
plan = f.read()
parser = JsonOutputParser(pydantic_object=QuizQuestion)
instruction = f"You'll provide one question with difficulty level of {difficulty}, 4 options as multiple choices and provide the anwsers, the quiz needs to be related to the teaching plan {plan}"
prompt = PromptTemplate(
template="Generates a single multiple-choice quiz question\n {format_instructions}\n {instruction}\n",
input_variables=["instruction"],
partial_variables={"format_instructions": parser.get_format_instructions()},
)
chain = prompt | llm | parser
response = chain.invoke({"instruction": instruction})
print(f"{response}")
return response
在代理中,它会创建一个 JSON 输出解析器,该解析器专门用于理解和构建 LLM 的输出。它使用我们之前定义的 QuizQuestion
模型,以确保解析后的输出符合正确的格式(题目、选项和答案)。
👉在终端中执行以下命令,以设置虚拟环境、安装依赖项并启动代理:
cd ~/aidemy-bootstrap/portal/
python -m venv env
source env/bin/activate
pip install -r requirements.txt
python app.py
使用 Cloud Shell 的网页预览功能访问正在运行的应用。点击顶部导航栏中的“知识问答”链接,或点击目录页上的卡片。您应该会看到系统为学生随机生成的三份知识问答。这些知识问答基于教学计划,展示了我们依托 AI 技术的知识问答生成系统的强大功能。
如需停止本地运行的进程,请在终端中按 Ctrl+C
。
Gemini 2 思考方式
好的,我们已经有了测验,这是一个良好的开端!但如果学生回答错了,该怎么办?这正是真正的学习过程,对吗?如果我们能解释其答案错误的原因以及如何得出正确答案,他们就更有可能记住正确答案。此外,这还有助于消除任何疑虑并增强他们的信心。
因此,我们将推出 Gemini 2 的“思考”模型这款重量级产品!不妨将其视为给 AI 多一点时间,让其在解释之前仔细思考。这样,它就能提供更详细、更实用的反馈。
我们希望了解该功能能否通过提供帮助、解答问题和详细说明来帮助学生。为了进行测试,我们先从一个难度极高的科目“微积分”开始。
👉首先,前往 Cloud Code Editor,在 portal
文件夹内的 answer.py
中,替换
def answer_thinking(question, options, user_response, answer, region):
return ""
使用以下代码段:
def answer_thinking(question, options, user_response, answer, region):
try:
llm = VertexAI(model_name="gemini-2.0-flash-001",location=region)
input_msg = HumanMessage(content=[f"Here the question{question}, here are the available options {options}, this student's answer {user_response}, whereas the correct answer is {answer}"])
prompt_template = ChatPromptTemplate.from_messages(
[
SystemMessage(
content=(
"You are a helpful teacher trying to teach the student on question, you were given the question and a set of multiple choices "
"what's the correct answer. use friendly tone"
)
),
input_msg,
]
)
prompt = prompt_template.format()
response = llm.invoke(prompt)
print(f"response: {response}")
return response
except Exception as e:
print(f"Error sending message to chatbot: {e}") # Log this error too!
return f"Unable to process your request at this time. Due to the following reason: {str(e)}"
if __name__ == "__main__":
question = "Evaluate the limit: lim (x→0) [(sin(5x) - 5x) / x^3]"
options = ["A) -125/6", "B) -5/3 ", "C) -25/3", "D) -5/6"]
user_response = "B"
answer = "A"
region = "us-central1"
result = answer_thinking(question, options, user_response, answer, region)
这是一个非常简单的 langchain 应用,它会初始化 Gemini 2 Flash 模型,我们会指示它充当有用的老师并提供解释
👉在终端中执行以下命令:
cd ~/aidemy-bootstrap/portal/
python answer.py
您应该会看到类似于原始说明中提供的示例的输出。当前模型可能无法提供详细说明。
Okay, I see the question and the choices. The question is to evaluate the limit:
lim (x→0) [(sin(5x) - 5x) / x^3]
You chose option B, which is -5/3, but the correct answer is A, which is -125/6.
It looks like you might have missed a step or made a small error in your calculations. This type of limit often involves using L'Hôpital's Rule or Taylor series expansion. Since we have the form 0/0, L'Hôpital's Rule is a good way to go! You need to apply it multiple times. Alternatively, you can use the Taylor series expansion of sin(x) which is:
sin(x) = x - x^3/3! + x^5/5! - ...
So, sin(5x) = 5x - (5x)^3/3! + (5x)^5/5! - ...
Then, (sin(5x) - 5x) = - (5x)^3/3! + (5x)^5/5! - ...
Finally, (sin(5x) - 5x) / x^3 = - 5^3/3! + (5^5 * x^2)/5! - ...
Taking the limit as x approaches 0, we get -125/6.
Keep practicing, you'll get there!
在 answer.py 文件中,将 answer_thinking 函数中的 model_name 从 gemini-2.0-flash-001
替换为 gemini-2.0-flash-thinking-exp-01-21
。
这会使 LLM 进行更多推理,从而帮助其生成更合理的说明。然后再次运行。
👉运行以测试新的思维模型:
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python answer.py
以下是思考模型给出的回答示例,该回答更加详尽,并分步说明了如何解微积分题目。这凸显了“思考”模型在生成高质量解释方面的强大能力。您会看到如下所示的输出:
Hey there! Let's take a look at this limit problem together. You were asked to evaluate:
lim (x→0) [(sin(5x) - 5x) / x^3]
and you picked option B, -5/3, but the correct answer is actually A, -125/6. Let's figure out why!
It's a tricky one because if we directly substitute x=0, we get (sin(0) - 0) / 0^3 = (0 - 0) / 0 = 0/0, which is an indeterminate form. This tells us we need to use a more advanced technique like L'Hopital's Rule or Taylor series expansion.
Let's use the Taylor series expansion for sin(y) around y=0. Do you remember it? It looks like this:
sin(y) = y - y^3/3! + y^5/5! - ...
where 3! (3 factorial) is 3 × 2 × 1 = 6, 5! is 5 × 4 × 3 × 2 × 1 = 120, and so on.
In our problem, we have sin(5x), so we can substitute y = 5x into the Taylor series:
sin(5x) = (5x) - (5x)^3/3! + (5x)^5/5! - ...
sin(5x) = 5x - (125x^3)/6 + (3125x^5)/120 - ...
Now let's plug this back into our limit expression:
[(sin(5x) - 5x) / x^3] = [ (5x - (125x^3)/6 + (3125x^5)/120 - ...) - 5x ] / x^3
Notice that the '5x' and '-5x' cancel out! So we are left with:
= [ - (125x^3)/6 + (3125x^5)/120 - ... ] / x^3
Now, we can divide every term in the numerator by x^3:
= -125/6 + (3125x^2)/120 - ...
Finally, let's take the limit as x approaches 0. As x gets closer and closer to zero, terms with x^2 and higher powers will become very, very small and approach zero. So, we are left with:
lim (x→0) [ -125/6 + (3125x^2)/120 - ... ] = -125/6
Therefore, the correct answer is indeed **A) -125/6**.
It seems like your answer B, -5/3, might have come from perhaps missing a factor somewhere during calculation or maybe using an incorrect simplification. Double-check your steps when you were trying to solve it!
Don't worry, these limit problems can be a bit tricky sometimes! Keep practicing and you'll get the hang of it. Let me know if you want to go through another similar example or if you have any more questions! 😊
Now that we have confirmed it works, let's use the portal.
👉从 answer.py
中移除以下测试代码:
if __name__ == "__main__":
question = "Evaluate the limit: lim (x→0) [(sin(5x) - 5x) / x^3]"
options = ["A) -125/6", "B) -5/3 ", "C) -25/3", "D) -5/6"]
user_response = "B"
answer = "A"
region = "us-central1"
result = answer_thinking(question, options, user_response, answer, region)
👉在终端中执行以下命令,以设置虚拟环境、安装依赖项并启动代理:
cd ~/aidemy-bootstrap/portal/
source env/bin/activate
python app.py
👉使用 Cloud Shell 的网页预览功能访问正在运行的应用。点击“测验”链接,回答所有测验,确保至少有一个答案错误,然后点击“提交”
在等待响应时,请不要干瞪眼,而是切换到 Cloud Shell Editor 的终端。您可以在模拟器的终端中观察进度以及函数生成的任何输出或错误消息。😁
如需停止本地运行的进程,请在终端中按 Ctrl+C
。
11. 使用 Eventarc 编排代理
到目前为止,学生门户一直根据一组默认的教学计划生成测验。这很有帮助,但这意味着我们的规划助理和门户的测验助理实际上并未相互通信。还记得我们如何添加了那项功能吗?该功能可让规划程序代理将其新生成的教学计划发布到 Pub/Sub 主题。现在,您可以将其关联到我们的门户客服人员了!
我们希望门户在每次生成新的教学计划时自动更新其测验内容。为此,我们将在门户中创建一个可以接收这些新方案的端点。
👉在 Cloud Code Editor 的“Explorer”窗格中,找到 portal
文件夹。打开 app.py
文件进行修改。在 ## Add your code here 之间添加以下代码:
## Add your code here
@app.route('/new_teaching_plan', methods=['POST'])
def new_teaching_plan():
try:
# Get data from Pub/Sub message delivered via Eventarc
envelope = request.get_json()
if not envelope:
return jsonify({'error': 'No Pub/Sub message received'}), 400
if not isinstance(envelope, dict) or 'message' not in envelope:
return jsonify({'error': 'Invalid Pub/Sub message format'}), 400
pubsub_message = envelope['message']
print(f"data: {pubsub_message['data']}")
data = pubsub_message['data']
data_str = base64.b64decode(data).decode('utf-8')
data = json.loads(data_str)
teaching_plan = data['teaching_plan']
print(f"File content: {teaching_plan}")
with open("teaching_plan.txt", "w") as f:
f.write(teaching_plan)
print(f"Teaching plan saved to local file: teaching_plan.txt")
return jsonify({'message': 'File processed successfully'})
except Exception as e:
print(f"Error processing file: {e}")
return jsonify({'error': 'Error processing file'}), 500
## Add your code here
重新构建并部署到 Cloud Run
好的!您需要更新并将我们的规划器和门户代理重新部署到 Cloud Run。这样可确保它们使用的是最新代码,并配置为通过事件进行通信。
👉接下来,我们将重新构建并推送 planner 代理映像,请返回终端并运行以下命令:
cd ~/aidemy-bootstrap/planner/
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-planner .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-planner us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner
👉我们将执行相同的操作,构建并推送 portal agent 映像:
cd ~/aidemy-bootstrap/portal/
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
在 Artifact Registry 中,您应该会看到 aidemy-planner
和 aidemy-portal
容器映像均已列出。
👉返回终端,运行以下命令以更新规划程序代理的 Cloud Run 映像:
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-planner \
--region=us-central1 \
--image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-planner:latest
您会看到如下所示的输出:
OK Deploying... Done.
OK Creating Revision...
OK Routing traffic...
Done.
Service [aidemy-planner] revision [aidemy-planner-xxxxx] has been deployed and is serving 100 percent of traffic.
Service URL: https://aidemy-planner-xxx.us-central1.run.app
记下服务网址;这是指向已部署的规划者代理的链接。
👉运行此脚本,为门户代理创建 Cloud Run 实例
export PROJECT_ID=$(gcloud config get project)
gcloud run deploy aidemy-portal \
--image=us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal:latest \
--region=us-central1 \
--platform=managed \
--allow-unauthenticated \
--memory=2Gi \
--cpu=2 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID}
您会看到如下所示的输出:
Deploying container to Cloud Run service [aidemy-portal] in project [xxxx] region [us-central1]
OK Deploying new service... Done.
OK Creating Revision...
OK Routing traffic...
OK Setting IAM Policy...
Done.
Service [aidemy-portal] revision [aidemy-portal-xxxx] has been deployed and is serving 100 percent of traffic.
Service URL: https://aidemy-portal-xxxx.us-central1.run.app
记下服务网址;这是指向已部署的学生门户的链接。
创建 Eventarc 触发器
但问题是:当 Pub/Sub 主题中有一个新方案等待处理时,此端点如何收到通知?这时,Eventarc 可以派上用场!
Eventarc 充当桥梁,监听特定事件(例如 Pub/Sub 主题中收到的新消息),并自动触发相应操作。在我们的示例中,它会检测何时发布了新教学计划,然后向门户的端点发送信号,告知其更新时间到了。
借助 Eventarc 处理事件驱动型通信,我们可以无缝连接规划器代理和门户代理,从而打造真正动态且响应迅速的学习系统。这就像有一个智能的即时通讯工具,可自动将最新的课程计划发送到正确的位置!
👉在控制台中,前往 Eventarc。
👉点击“+ 创建触发器”按钮。
配置触发器(基本):
- 触发器名称:
plan-topic-trigger
- 触发器类型:Google 来源。
- 事件提供方:Cloud Pub/Sub
- 事件类型:
google.cloud.pubsub.topic.v1.messagePublished
- 地区:
us-central1
。 - Cloud Pub/Sub 主题:选择
plan
- 向服务账号授予角色
roles/iam.serviceAccountTokenCreator
- 事件目标位置:Cloud Run
- Cloud Run 服务:aidemy-portal
- 服务网址路径:
/new_teaching_plan
- 忽略消息(对“locations/me-central2”的权限遭拒 [或该资源可能不存在])。
点击“创建”。
“Eventarc 触发器”页面将刷新,您现在应该会在表格中看到新创建的触发器。
👉现在,访问计划,然后申请新的教学计划。这次,请尝试使用年份 5
、主题 science
和“添加-否”请求 atoms
如果您忘记了规划程序代理的位置,请在终端中运行以下命令
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner
然后,请等待一两分钟。再次提醒一下,由于此实验的结算限制,系统会出现延迟,但在正常情况下,应该不会出现延迟。
最后,访问学生门户。您应该会看到,这些测验已更新,现在与您刚刚生成的新教学计划保持一致!这表明 Eventarc 已成功集成到 Aidemy 系统中!
如果您忘记了门户代理的位置,请在终端中运行以下命令
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal
恭喜!您已成功在 Google Cloud 上构建了多代理系统,并利用事件驱动型架构提高了可伸缩性和灵活性!您已经打下了坚实的基础,但还有更多内容值得探索。如需深入了解此架构的真正优势、探索 Gemini 2 的多模态 Live API 的强大功能,以及了解如何使用 LangGraph 实现单路径编排,请随时继续阅读接下来的两章。
12. 可选:使用 Gemini 生成音频摘要
Gemini 可以理解和处理来自各种来源(例如文本、图片,甚至音频)的信息,为学习和内容创作开辟了全新的可能性。Gemini 能够“看”“听”和“读”,真正解锁富有创意且富有吸引力的用户体验。
除了创建视觉内容或文字内容之外,学习的另一个重要步骤是有效地总结和回顾。想想看:您是否更容易记住朗朗上口的歌词,而不是教科书中的内容?声音可以让人难以忘怀!因此,我们将利用 Gemini 的多模态功能,为我们的教学计划生成音频摘要。这样,学生就可以以一种便捷且富有吸引力的方式回顾教材,从而通过听觉学习的力量提高记忆力和理解力。
我们需要一个位置来存储生成的音频文件。Cloud Storage 提供可伸缩且可靠的解决方案。
👉前往控制台中的存储部分。点击左侧菜单中的“存储分区”。点击顶部的“+ 创建”按钮。
👉配置存储分区:
- 存储分区名称:aidemy-recap-<UNIQUE_NAME>重要提示:请务必定义一个以“aidemy-recap-”开头的唯一存储分区名称。此唯一名称对于避免在创建 Cloud Storage 存储分区时发生名称冲突至关重要。
- 区域:
us-central1
。 - 存储类别:“标准”。标准适合频繁访问的数据。
- 访问权限控制:保持默认的“统一访问权限控制”处于选中状态。这样可以实现一致的存储分区级访问权限控制。
- 高级选项:在本研讨会中,默认设置通常就足够了。点击创建按钮以创建存储分区。
您可能会看到有关禁止公开访问的弹出式窗口。让该复选框保持选中状态,然后点击 Confirm
。
现在,您会在“存储分区”列表中看到新创建的存储分区。记下存储分区名称,稍后会用到。
👉在 Cloud Code Editor 的终端中,运行以下命令,向服务账号授予对存储分区的访问权限:
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://$COURSE_BUCKET_NAME \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectCreator"
👉在 Cloud Code Editor 中,打开 course
文件夹中的 audio.py
。将以下代码粘贴到文件末尾:
config = LiveConnectConfig(
response_modalities=["AUDIO"],
speech_config=SpeechConfig(
voice_config=VoiceConfig(
prebuilt_voice_config=PrebuiltVoiceConfig(
voice_name="Charon",
)
)
),
)
async def process_weeks(teaching_plan: str):
region = "us-west1" #To workaround onRamp qouta limits
client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
clientAudio = genai.Client(vertexai=True, project=PROJECT_ID, location="us-central1")
async with clientAudio.aio.live.connect(
model=MODEL_ID,
config=config,
) as session:
for week in range(1, 4):
response = client.models.generate_content(
model="gemini-1.0-pro",
contents=f"Given the following teaching plan: {teaching_plan}, Extrace content plan for week {week}. And return just the plan, nothingh else " # Clarified prompt
)
prompt = f"""
Assume you are the instructor.
Prepare a concise and engaging recap of the key concepts and topics covered.
This recap should be suitable for generating a short audio summary for students.
Focus on the most important learnings and takeaways, and frame it as a direct address to the students.
Avoid overly formal language and aim for a conversational tone, tell a few jokes.
Teaching plan: {response.text} """
print(f"prompt --->{prompt}")
await session.send(input=prompt, end_of_turn=True)
with open(f"temp_audio_week_{week}.raw", "wb") as temp_file:
async for message in session.receive():
if message.server_content.model_turn:
for part in message.server_content.model_turn.parts:
if part.inline_data:
temp_file.write(part.inline_data.data)
data, samplerate = sf.read(f"temp_audio_week_{week}.raw", channels=1, samplerate=24000, subtype='PCM_16', format='RAW')
sf.write(f"course-week-{week}.wav", data, samplerate)
storage_client = storage.Client()
bucket = storage_client.bucket(BUCKET_NAME)
blob = bucket.blob(f"course-week-{week}.wav") # Or give it a more descriptive name
blob.upload_from_filename(f"course-week-{week}.wav")
print(f"Audio saved to GCS: gs://{BUCKET_NAME}/course-week-{week}.wav")
await session.close()
def breakup_sessions(teaching_plan: str):
asyncio.run(process_weeks(teaching_plan))
- 流式传输连接:首先,与 Live API 端点建立持久连接。与您发出请求并获取响应的标准 API 调用不同,此连接会保持打开状态,以便持续交换数据。
- 配置多模态:使用配置指定您想要的输出类型(在本例中为音频),您甚至可以指定要使用的参数(例如语音选择、音频编码)
- 异步处理:此 API 以异步方式运行,这意味着它在等待音频生成完成时不会阻塞主线程。通过实时处理数据并分块发送输出,它可提供近乎即时的体验。
现在,关键问题是:此音频生成流程应何时运行?理想情况下,我们希望在创建新的教学计划后,音频重述内容能够立即提供。由于我们已通过将教学计划发布到 Pub/Sub 主题来实现事件驱动型架构,因此只需订阅该主题即可。
不过,我们不会经常生成新的教学计划。让代理持续运行并等待新方案的做法并不高效。因此,将此音频生成逻辑部署为 Cloud Run 函数非常有意义。
通过将其部署为函数,它会一直处于休眠状态,直到有新消息发布到 Pub/Sub 主题。当发生这种情况时,系统会自动触发该函数,该函数会生成音频摘要并将其存储在我们的存储分区中。
👉在 main.py
文件的 course
文件夹下,此文件定义了在有新的教学计划可用时触发的 Cloud Run 函数。它会接收方案并发起音频数据总结生成。将以下代码段添加到文件末尾。
@functions_framework.cloud_event
def process_teaching_plan(cloud_event):
print(f"CloudEvent received: {cloud_event.data}")
time.sleep(60)
try:
if isinstance(cloud_event.data.get('message', {}).get('data'), str): # Check for base64 encoding
data = json.loads(base64.b64decode(cloud_event.data['message']['data']).decode('utf-8'))
teaching_plan = data.get('teaching_plan') # Get the teaching plan
elif 'teaching_plan' in cloud_event.data: # No base64
teaching_plan = cloud_event.data["teaching_plan"]
else:
raise KeyError("teaching_plan not found") # Handle error explicitly
#Load the teaching_plan as string and from cloud event, call audio breakup_sessions
breakup_sessions(teaching_plan)
return "Teaching plan processed successfully", 200
except (json.JSONDecodeError, AttributeError, KeyError) as e:
print(f"Error decoding CloudEvent data: {e} - Data: {cloud_event.data}")
return "Error processing event", 500
except Exception as e:
print(f"Error processing teaching plan: {e}")
return "Error processing teaching plan", 500
@functions_framework.cloud_event:此装饰器会将函数标记为将由 CloudEvents 触发的 Cloud Run 函数。
在本地测试
👉我们将在虚拟环境中运行此脚本,并为 Cloud Run 函数安装必要的 Python 库。
cd ~/aidemy-bootstrap/courses
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
python -m venv env
source env/bin/activate
pip install -r requirements.txt
👉借助 Cloud Run Function 模拟器,我们可以在将函数部署到 Google Cloud 之前在本地对其进行测试。通过运行以下命令启动本地模拟器:
functions-framework --target process_teaching_plan --signature-type=cloudevent --source main.py
👉在模拟器运行时,您可以向模拟器发送测试 CloudEvent,以模拟发布新的教学计划。在新终端中:
👉运行:
curl -X POST \
http://localhost:8080/ \
-H "Content-Type: application/json" \
-H "ce-id: event-id-01" \
-H "ce-source: planner-agent" \
-H "ce-specversion: 1.0" \
-H "ce-type: google.cloud.pubsub.topic.v1.messagePublished" \
-d '{
"message": {
"data": "eyJ0ZWFjaGluZ19wbGFuIjogIldlZWsgMTogMkQgU2hhcGVzIGFuZCBBbmdsZXMgLSBEYXkgMTogUmV2aWV3IG9mIGJhc2ljIDJEIHNoYXBlcyAoc3F1YXJlcywgcmVjdGFuZ2xlcywgdHJpYW5nbGVzLCBjaXJjbGVzKS4gRGF5IDI6IEV4cGxvcmluZyBkaWZmZXJlbnQgdHlwZXMgb2YgdHJpYW5nbGVzIChlcXVpbGF0ZXJhbCwgaXNvc2NlbGVzLCBzY2FsZW5lLCByaWdodC1hbmdsZWQpLiBEYXkgMzogRXhwbG9yaW5nIHF1YWRyaWxhdGVyYWxzIChzcXVhcmUsIHJlY3RhbmdsZSwgcGFyYWxsZWxvZ3JhbSwgcmhvbWJ1cywgdHJhcGV6aXVtKS4gRGF5IDQ6IEludHJvZHVjdGlvbiB0byBhbmdsZXM6IHJpZ2h0IGFuZ2xlcywgYWN1dGUgYW5nbGVzLCBhbmQgb2J0dXNlIGFuZ2xlcy4gRGF5IDU6IE1lYXN1cmluZyBhbmdsZXMgdXNpbmcgYSBwcm90cmFjdG9yLiBXZWVrIDI6IDNEIFNoYXBlcyBhbmQgU3ltbWV0cnkgLSBEYXkgNjogSW50cm9kdWN0aW9uIHRvIDNEIHNoYXBlczogY3ViZXMsIGN1Ym9pZHMsIHNwaGVyZXMsIGN5bGluZGVycywgY29uZXMsIGFuZCBweXJhbWlkcy4gRGF5IDc6IERlc2NyaWJpbmcgM0Qgc2hhcGVzIHVzaW5nIGZhY2VzLCBlZGdlcywgYW5kIHZlcnRpY2VzLiBEYXkgODogUmVsYXRpbmcgMkQgc2hhcGVzIHRvIDNEIHNoYXBlcy4gRGF5IDk6IElkZW50aWZ5aW5nIGxpbmVzIG9mIHN5bW1ldHJ5IGluIDJEIHNoYXBlcy4gRGF5IDEwOiBDb21wbGV0aW5nIHN5bW1ldHJpY2FsIGZpZ3VyZXMuIFdlZWsgMzogUG9zaXRpb24sIERpcmVjdGlvbiwgYW5kIFByb2JsZW0gU29sdmluZyAtIERheSAxMTogRGVzY3JpYmluZyBwb3NpdGlvbiB1c2luZyBjb29yZGluYXRlcyBpbiB0aGUgZmlyc3QgcXVhZHJhbnQuIERheSAxMjogUGxvdHRpbmcgY29vcmRpbmF0ZXMgdG8gZHJhdyBzaGFwZXMuIERheSAxMzogVW5kZXJzdGFuZGluZyB0cmFuc2xhdGlvbiAoc2xpZGluZyBhIHNoYXBlKS4gRGF5IDE0OiBVbmRlcnN0YW5kaW5nIHJlZmxlY3Rpb24gKGZsaXBwaW5nIGEgc2hhcGUpLiBEYXkgMTU6IFByb2JsZW0tc29sdmluZyBhY3Rpdml0aWVzIGludm9sdmluZyBwZXJpbWV0ZXIsIGFyZWEsIGFuZCBtaXNzaW5nIGFuZ2xlcy4ifQ=="
}
}'
在等待响应时,请切换到另一个 Cloud Shell 终端,而不是干瞪眼。您可以在模拟器的终端中观察进度以及函数生成的任何输出或错误消息。😁
返回第二个终端,您应该会看到它返回了 OK
。
👉您将验证存储分区中的数据,前往 Cloud Storage,然后依次选择“Bucket”(存储分区)标签页和 aidemy-recap-xxx
👉在运行模拟器的终端中,输入 ctrl+c
以退出。然后关闭第二个终端。关闭第二个终端,然后运行 deactivate 以退出虚拟环境。
deactivate
部署到 Google Cloud
👉在本地进行测试后,接下来将课程代理部署到 Google Cloud。在终端中,运行以下命令:
cd ~/aidemy-bootstrap/courses
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud functions deploy courses-agent \
--region=us-central1 \
--gen2 \
--source=. \
--runtime=python312 \
--trigger-topic=plan \
--entry-point=process_teaching_plan \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
在 Google Cloud 控制台中前往 Cloud Run 以验证部署。您应该会看到一个名为 courses-agent 的新服务。
如需检查触发器配置,请点击 courses-agent 服务以查看其详细信息。前往“触发器”标签页。
您应该会看到一个触发器,该触发器配置为监听发布到方案主题的消息。
最后,我们来看看端到端运行情况。
👉接下来,我们需要配置门户代理,以便其知道在哪里查找生成的音频文件。在终端中,运行以下命令:
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export PROJECT_ID=$(gcloud config get project)
gcloud run services update aidemy-portal \
--region=us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
👉尝试在规划助理页面中生成新的教学计划。该服务可能需要几分钟才能启动,请不要惊慌,这是无服务器服务。获取您的规划者代理的网址(如果您没有备用,请在终端中运行以下命令):
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner
生成新方案后,请等待 2-3 分钟以生成音频,由于此实验室账号存在结算限制,因此可能还需要再等待几分钟。
您可以通过查看函数的“触发器”标签页,监控 courses-agent
函数是否已收到教学计划。定期刷新页面;您最终应该会看到该函数已被调用。如果超过 2 分钟后函数仍未被调用,您可以尝试重新生成教学计划。不过,请避免快速连续生成方案,因为代理会按顺序使用和处理每个生成的方案,这可能会导致积压。
👉访问该门户,然后点击“课程”。您应该会看到三张卡片,每张卡片显示一则音频数据总结。如需查找门户客服人员的网址,请执行以下操作:
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal
点击每个课程的“播放”按钮,确保音频摘要与您刚刚生成的教学计划保持一致!
退出虚拟环境。
deactivate
13. 可选:使用 Gemini 和 DeepSeek 进行基于角色的协作
拥有多种视角非常重要,尤其是在设计富有吸引力且深思熟虑的作业时。现在,我们将构建一个多智能体系统,该系统利用两个具有不同角色的不同模型来生成作业:一个模型促进协作,另一个模型鼓励自学。我们将使用“单次”架构,其中工作流会遵循固定路线。
Gemini 作业生成器
首先,我们将设置 Gemini 函数,以生成侧重于协作的作业。修改位于
assignment
文件夹中的 gemini.py
文件。
👉将以下代码粘贴到 gemini.py
文件的末尾:
def gen_assignment_gemini(state):
region=get_next_region()
client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
print(f"---------------gen_assignment_gemini")
response = client.models.generate_content(
model=MODEL_ID, contents=f"""
You are an instructor
Develop engaging and practical assignments for each week, ensuring they align with the teaching plan's objectives and progressively build upon each other.
For each week, provide the following:
* **Week [Number]:** A descriptive title for the assignment (e.g., "Data Exploration Project," "Model Building Exercise").
* **Learning Objectives Assessed:** List the specific learning objectives from the teaching plan that this assignment assesses.
* **Description:** A detailed description of the task, including any specific requirements or constraints. Provide examples or scenarios if applicable.
* **Deliverables:** Specify what students need to submit (e.g., code, report, presentation).
* **Estimated Time Commitment:** The approximate time students should dedicate to completing the assignment.
* **Assessment Criteria:** Briefly outline how the assignment will be graded (e.g., correctness, completeness, clarity, creativity).
The assignments should be a mix of individual and collaborative work where appropriate. Consider different learning styles and provide opportunities for students to apply their knowledge creatively.
Based on this teaching plan: {state["teaching_plan"]}
"""
)
print(f"---------------gen_assignment_gemini answer {response.text}")
state["model_one_assignment"] = response.text
return state
import unittest
class TestGenAssignmentGemini(unittest.TestCase):
def test_gen_assignment_gemini(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assigmodel_one_assignmentnment": "", "final_assignment": ""}
updated_state = gen_assignment_gemini(initial_state)
self.assertIn("model_one_assignment", updated_state)
self.assertIsNotNone(updated_state["model_one_assignment"])
self.assertIsInstance(updated_state["model_one_assignment"], str)
self.assertGreater(len(updated_state["model_one_assignment"]), 0)
print(updated_state["model_one_assignment"])
if __name__ == '__main__':
unittest.main()
它使用 Gemini 模型生成作业。
我们可以开始测试 Gemini Agent 了。
👉在终端中运行以下命令以设置环境:
cd ~/aidemy-bootstrap/assignment
export PROJECT_ID=$(gcloud config get project)
python -m venv env
source env/bin/activate
pip install -r requirements.txt
👉您可以运行该脚本进行测试:
python gemini.py
您应该会在输出中看到包含更多小组作业的作业。最后的断言测试也会输出结果。
Here are some engaging and practical assignments for each week, designed to build progressively upon the teaching plan's objectives:
**Week 1: Exploring the World of 2D Shapes**
* **Learning Objectives Assessed:**
* Identify and name basic 2D shapes (squares, rectangles, triangles, circles).
* .....
* **Description:**
* **Shape Scavenger Hunt:** Students will go on a scavenger hunt in their homes or neighborhoods, taking pictures of objects that represent different 2D shapes. They will then create a presentation or poster showcasing their findings, classifying each shape and labeling its properties (e.g., number of sides, angles, etc.).
* **Triangle Trivia:** Students will research and create a short quiz or presentation about different types of triangles, focusing on their properties and real-world examples.
* **Angle Exploration:** Students will use a protractor to measure various angles in their surroundings, such as corners of furniture, windows, or doors. They will record their measurements and create a chart categorizing the angles as right, acute, or obtuse.
....
**Week 2: Delving into the World of 3D Shapes and Symmetry**
* **Learning Objectives Assessed:**
* Identify and name basic 3D shapes.
* ....
* **Description:**
* **3D Shape Construction:** Students will work in groups to build 3D shapes using construction paper, cardboard, or other materials. They will then create a presentation showcasing their creations, describing the number of faces, edges, and vertices for each shape.
* **Symmetry Exploration:** Students will investigate the concept of symmetry by creating a visual representation of various symmetrical objects (e.g., butterflies, leaves, snowflakes) using drawing or digital tools. They will identify the lines of symmetry and explain their findings.
* **Symmetry Puzzles:** Students will be given a half-image of a symmetrical figure and will be asked to complete the other half, demonstrating their understanding of symmetry. This can be done through drawing, cut-out activities, or digital tools.
**Week 3: Navigating Position, Direction, and Problem Solving**
* **Learning Objectives Assessed:**
* Describe position using coordinates in the first quadrant.
* ....
* **Description:**
* **Coordinate Maze:** Students will create a maze using coordinates on a grid paper. They will then provide directions for navigating the maze using a combination of coordinate movements and translation/reflection instructions.
* **Shape Transformations:** Students will draw shapes on a grid paper and then apply transformations such as translation and reflection, recording the new coordinates of the transformed shapes.
* **Geometry Challenge:** Students will solve real-world problems involving perimeter, area, and angles. For example, they could be asked to calculate the perimeter of a room, the area of a garden, or the missing angle in a triangle.
....
使用 ctl+c
停止并清理测试代码。从 gemini.py
中移除以下代码
import unittest
class TestGenAssignmentGemini(unittest.TestCase):
def test_gen_assignment_gemini(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assigmodel_one_assignmentnment": "", "final_assignment": ""}
updated_state = gen_assignment_gemini(initial_state)
self.assertIn("model_one_assignment", updated_state)
self.assertIsNotNone(updated_state["model_one_assignment"])
self.assertIsInstance(updated_state["model_one_assignment"], str)
self.assertGreater(len(updated_state["model_one_assignment"]), 0)
print(updated_state["model_one_assignment"])
if __name__ == '__main__':
unittest.main()
配置 DeepSeek 作业生成器
虽然基于云的 AI 平台很方便,但对于保护数据隐私和确保数据主权而言,自行托管 LLM 至关重要。我们将在 Cloud Compute Engine 实例上部署最小的 DeepSeek 模型(15 亿个参数)。还有其他方法,例如在 Google 的 Vertex AI 平台上托管它或在您的 GKE 实例上托管它,但这只是一场 AI 代理研讨会,我不想让您一直在这里,因此我们就使用最简单的方法吧。不过,如果您有兴趣深入了解其他选项,请查看“assignment”文件夹下的 deepseek-vertexai.py
文件,其中提供了有关如何与部署在 VertexAI 上的模型交互的示例代码。
👉在终端中运行以下命令,创建自托管 LLM 平台 Ollama:
cd ~/aidemy-bootstrap/assignment
gcloud compute instances create ollama-instance \
--image-family=ubuntu-2204-lts \
--image-project=ubuntu-os-cloud \
--machine-type=e2-standard-4 \
--zone=us-central1-a \
--metadata-from-file startup-script=startup.sh \
--boot-disk-size=50GB \
--tags=ollama \
--scopes=https://www.googleapis.com/auth/cloud-platform
如需验证 Compute Engine 实例是否正在运行,请执行以下操作:
在 Google Cloud 控制台中,依次前往 Compute Engine >“虚拟机实例”。您应该会看到列出的 ollama-instance
带有绿色对勾标记,表示它正在运行。如果您没有看到该区域,请确保区域为 us-central1。如果没有,您可能需要进行搜索。
👉我们将安装最小的 DeepSeek 模型并对其进行测试。返回 Cloud Shell 编辑器,在新建终端中,运行以下命令以 SSH 连接到 GCE 实例。
gcloud compute ssh ollama-instance --zone=us-central1-a
建立 SSH 连接后,系统可能会提示您输入以下信息:
“Do you want to continue (Y/n)?”
只需输入 Y
(不区分大小写)并按 Enter 键即可继续。
接下来,系统可能会要求您为 SSH 密钥创建密码。如果您不想使用口令,只需按两次 Enter 键即可接受默认设置(不使用口令)。
👉现在,您已进入虚拟机,请拉取最小的 DeepSeek R1 模型,并测试其能否正常运行?
ollama pull deepseek-r1:1.5b
ollama run deepseek-r1:1.5b "who are you?"
👉退出 GCE 实例,在 SSH 终端中输入以下内容:
exit
关闭新终端,返回原始终端。
👉另外,请不要忘记设置网络政策,以便其他服务可以访问 LLM。如果您想在生产环境中执行此操作,请限制对实例的访问,为服务实现安全登录或限制 IP 访问。运行以下命令:
gcloud compute firewall-rules create allow-ollama-11434 \
--allow=tcp:11434 \
--target-tags=ollama \
--description="Allow access to Ollama on port 11434"
👉如需验证防火墙政策是否正常运行,请尝试运行以下命令:
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
curl -X POST "${OLLAMA_HOST}/api/generate" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Hello, what are you?",
"model": "deepseek-r1:1.5b",
"stream": false
}'
接下来,我们将改进作业代理中的 Deepseek 函数,以生成侧重于个别作业的作业。
👉修改 assignment
文件夹下的 deepseek.py
,将以下代码段添加到末尾
def gen_assignment_deepseek(state):
print(f"---------------gen_assignment_deepseek")
template = """
You are an instructor who favor student to focus on individual work.
Develop engaging and practical assignments for each week, ensuring they align with the teaching plan's objectives and progressively build upon each other.
For each week, provide the following:
* **Week [Number]:** A descriptive title for the assignment (e.g., "Data Exploration Project," "Model Building Exercise").
* **Learning Objectives Assessed:** List the specific learning objectives from the teaching plan that this assignment assesses.
* **Description:** A detailed description of the task, including any specific requirements or constraints. Provide examples or scenarios if applicable.
* **Deliverables:** Specify what students need to submit (e.g., code, report, presentation).
* **Estimated Time Commitment:** The approximate time students should dedicate to completing the assignment.
* **Assessment Criteria:** Briefly outline how the assignment will be graded (e.g., correctness, completeness, clarity, creativity).
The assignments should be a mix of individual and collaborative work where appropriate. Consider different learning styles and provide opportunities for students to apply their knowledge creatively.
Based on this teaching plan: {teaching_plan}
"""
prompt = ChatPromptTemplate.from_template(template)
model = OllamaLLM(model="deepseek-r1:1.5b",
base_url=OLLAMA_HOST)
chain = prompt | model
response = chain.invoke({"teaching_plan":state["teaching_plan"]})
state["model_two_assignment"] = response
return state
import unittest
class TestGenAssignmentDeepseek(unittest.TestCase):
def test_gen_assignment_deepseek(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = gen_assignment_deepseek(initial_state)
self.assertIn("model_two_assignment", updated_state)
self.assertIsNotNone(updated_state["model_two_assignment"])
self.assertIsInstance(updated_state["model_two_assignment"], str)
self.assertGreater(len(updated_state["model_two_assignment"]), 0)
print(updated_state["model_two_assignment"])
if __name__ == '__main__':
unittest.main()
👉我们通过运行以下命令来进行测试:
cd ~/aidemy-bootstrap/assignment
source env/bin/activate
export PROJECT_ID=$(gcloud config get project)
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
python deepseek.py
您应该会看到包含更多自学内容的作业。
**Assignment Plan for Each Week**
---
### **Week 1: 2D Shapes and Angles**
- **Week Title:** "Exploring 2D Shapes"
Assign students to research and present on various 2D shapes. Include a project where they create models using straws and tape for triangles, draw quadrilaterals with specific measurements, and compare their properties.
### **Week 2: 3D Shapes and Symmetry**
Assign students to create models or nets for cubes and cuboids. They will also predict how folding these nets form the 3D shapes. Include a project where they identify symmetrical properties using mirrors or folding techniques.
### **Week 3: Position, Direction, and Problem Solving**
Assign students to use mirrors or folding techniques for reflections. Include activities where they measure angles, use a protractor, solve problems involving perimeter/area, and create symmetrical designs.
....
👉停止 ctl+c
并清理测试代码。从 deepseek.py
中移除以下代码
import unittest
class TestGenAssignmentDeepseek(unittest.TestCase):
def test_gen_assignment_deepseek(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = gen_assignment_deepseek(initial_state)
self.assertIn("model_two_assignment", updated_state)
self.assertIsNotNone(updated_state["model_two_assignment"])
self.assertIsInstance(updated_state["model_two_assignment"], str)
self.assertGreater(len(updated_state["model_two_assignment"]), 0)
print(updated_state["model_two_assignment"])
if __name__ == '__main__':
unittest.main()
现在,我们将使用相同的 Gemini 模型将这两项作业合并为一个新作业。修改位于 assignment
文件夹中的 gemini.py
文件。
👉将以下代码粘贴到 gemini.py
文件的末尾:
def combine_assignments(state):
print(f"---------------combine_assignments ")
region=get_next_region()
client = genai.Client(vertexai=True, project=PROJECT_ID, location=region)
response = client.models.generate_content(
model=MODEL_ID, contents=f"""
Look at all the proposed assignment so far {state["model_one_assignment"]} and {state["model_two_assignment"]}, combine them and come up with a final assignment for student.
"""
)
state["final_assignment"] = response.text
return state
为了结合这两种模型的优势,我们将使用 LangGraph 编排定义的工作流。此工作流程包含三个步骤:首先,Gemini 模型会生成侧重于协作的作业;其次,DeepSeek 模型会生成侧重于个人工作的作业;最后,Gemini 会将这两项作业综合为一个全面的作业。由于我们预定义了步骤序列,而无需 LLM 决策,因此这构成了用户定义的单路径编排。
👉将以下代码粘贴到 assignment
文件夹下的 main.py
文件的末尾:
def create_assignment(teaching_plan: str):
print(f"create_assignment---->{teaching_plan}")
builder = StateGraph(State)
builder.add_node("gen_assignment_gemini", gen_assignment_gemini)
builder.add_node("gen_assignment_deepseek", gen_assignment_deepseek)
builder.add_node("combine_assignments", combine_assignments)
builder.add_edge(START, "gen_assignment_gemini")
builder.add_edge("gen_assignment_gemini", "gen_assignment_deepseek")
builder.add_edge("gen_assignment_deepseek", "combine_assignments")
builder.add_edge("combine_assignments", END)
graph = builder.compile()
state = graph.invoke({"teaching_plan": teaching_plan})
return state["final_assignment"]
import unittest
class TestCreatAssignment(unittest.TestCase):
def test_create_assignment(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = create_assignment(initial_state)
print(updated_state)
if __name__ == '__main__':
unittest.main()
👉如需初步测试 create_assignment
函数并确认将 Gemini 和 DeepSeek 结合使用的工作流是否正常运行,请运行以下命令:
cd ~/aidemy-bootstrap/assignment
source env/bin/activate
pip install -r requirements.txt
python main.py
您应该会看到结合这两种模型的结果,它们分别从学生个人学习和学生小组合作的角度提供反馈。
**Tasks:**
1. **Clue Collection:** Gather all the clues left by the thieves. These clues will include:
* Descriptions of shapes and their properties (angles, sides, etc.)
* Coordinate grids with hidden messages
* Geometric puzzles requiring transformation (translation, reflection, rotation)
* Challenges involving area, perimeter, and angle calculations
2. **Clue Analysis:** Decipher each clue using your geometric knowledge. This will involve:
* Identifying the shape and its properties
* Plotting coordinates and interpreting patterns on the grid
* Solving geometric puzzles by applying transformations
* Calculating area, perimeter, and missing angles
3. **Case Report:** Create a comprehensive case report outlining your findings. This report should include:
* A detailed explanation of each clue and its solution
* Sketches and diagrams to support your explanations
* A step-by-step account of how you followed the clues to locate the artifact
* A final conclusion about the thieves and their motives
👉停止 ctl+c
并清理测试代码。从 main.py
中移除以下代码
import unittest
class TestCreatAssignment(unittest.TestCase):
def test_create_assignment(self):
test_teaching_plan = "Week 1: 2D Shapes and Angles - Day 1: Review of basic 2D shapes (squares, rectangles, triangles, circles). Day 2: Exploring different types of triangles (equilateral, isosceles, scalene, right-angled). Day 3: Exploring quadrilaterals (square, rectangle, parallelogram, rhombus, trapezium). Day 4: Introduction to angles: right angles, acute angles, and obtuse angles. Day 5: Measuring angles using a protractor. Week 2: 3D Shapes and Symmetry - Day 6: Introduction to 3D shapes: cubes, cuboids, spheres, cylinders, cones, and pyramids. Day 7: Describing 3D shapes using faces, edges, and vertices. Day 8: Relating 2D shapes to 3D shapes. Day 9: Identifying lines of symmetry in 2D shapes. Day 10: Completing symmetrical figures. Week 3: Position, Direction, and Problem Solving - Day 11: Describing position using coordinates in the first quadrant. Day 12: Plotting coordinates to draw shapes. Day 13: Understanding translation (sliding a shape). Day 14: Understanding reflection (flipping a shape). Day 15: Problem-solving activities involving perimeter, area, and missing angles."
initial_state = {"teaching_plan": test_teaching_plan, "model_one_assignment": "", "model_two_assignment": "", "final_assignment": ""}
updated_state = create_assignment(initial_state)
print(updated_state)
if __name__ == '__main__':
unittest.main()
为了使作业生成流程自动化并能够响应新的教学计划,我们将利用现有的事件驱动型架构。以下代码定义了一个 Cloud Run 函数 (generate_assignment),每当有新的教学计划发布到 Pub/Sub 主题“plan”时,都会触发该函数。
👉将以下代码添加到 main.py
的末尾:
@functions_framework.cloud_event
def generate_assignment(cloud_event):
print(f"CloudEvent received: {cloud_event.data}")
try:
if isinstance(cloud_event.data.get('message', {}).get('data'), str):
data = json.loads(base64.b64decode(cloud_event.data['message']['data']).decode('utf-8'))
teaching_plan = data.get('teaching_plan')
elif 'teaching_plan' in cloud_event.data:
teaching_plan = cloud_event.data["teaching_plan"]
else:
raise KeyError("teaching_plan not found")
assignment = create_assignment(teaching_plan)
print(f"Assignment---->{assignment}")
#Store the return assignment into bucket as a text file
storage_client = storage.Client()
bucket = storage_client.bucket(ASSIGNMENT_BUCKET)
file_name = f"assignment-{random.randint(1, 1000)}.txt"
blob = bucket.blob(file_name)
blob.upload_from_string(assignment)
return f"Assignment generated and stored in {ASSIGNMENT_BUCKET}/{file_name}", 200
except (json.JSONDecodeError, AttributeError, KeyError) as e:
print(f"Error decoding CloudEvent data: {e} - Data: {cloud_event.data}")
return "Error processing event", 500
except Exception as e:
print(f"Error generate assignment: {e}")
return "Error generate assignment", 500
在本地测试
在部署到 Google Cloud 之前,最好先在本地测试 Cloud Run 函数。这样可以加快迭代速度并简化调试。
首先,创建一个 Cloud Storage 存储分区来存储生成的作业文件,并向服务账号授予对该存储分区的访问权限。在终端中运行以下命令:
👉重要提示:请务必定义一个以“aidemy-assignment-”开头的唯一 ASSIGNMENT_BUCKET 名称。此唯一名称对于避免在创建 Cloud Storage 存储分区时出现命名冲突至关重要。(将 <YOUR_NAME> 替换为任何随机字词)
export ASSIGNMENT_BUCKET=aidemy-assignment-<YOUR_NAME> #Name must be unqiue
👉 然后运行以下命令:
export PROJECT_ID=$(gcloud config get project)
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gsutil mb -p $PROJECT_ID -l us-central1 gs://$ASSIGNMENT_BUCKET
gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectViewer"
gcloud storage buckets add-iam-policy-binding gs://$ASSIGNMENT_BUCKET \
--member "serviceAccount:$SERVICE_ACCOUNT_NAME" \
--role "roles/storage.objectCreator"
👉现在,启动 Cloud Run 函数模拟器:
cd ~/aidemy-bootstrap/assignment
functions-framework --target generate_assignment --signature-type=cloudevent --source main.py
👉在一个终端中运行模拟器时,在 Cloud Shell 中打开第二个终端。在此第二个终端中,向模拟器发送一个测试 CloudEvent,以模拟发布新的教学计划:
curl -X POST \
http://localhost:8080/ \
-H "Content-Type: application/json" \
-H "ce-id: event-id-01" \
-H "ce-source: planner-agent" \
-H "ce-specversion: 1.0" \
-H "ce-type: google.cloud.pubsub.topic.v1.messagePublished" \
-d '{
"message": {
"data": "eyJ0ZWFjaGluZ19wbGFuIjogIldlZWsgMTogMkQgU2hhcGVzIGFuZCBBbmdsZXMgLSBEYXkgMTogUmV2aWV3IG9mIGJhc2ljIDJEIHNoYXBlcyAoc3F1YXJlcywgcmVjdGFuZ2xlcywgdHJpYW5nbGVzLCBjaXJjbGVzKS4gRGF5IDI6IEV4cGxvcmluZyBkaWZmZXJlbnQgdHlwZXMgb2YgdHJpYW5nbGVzIChlcXVpbGF0ZXJhbCwgaXNvc2NlbGVzLCBzY2FsZW5lLCByaWdodC1hbmdsZWQpLiBEYXkgMzogRXhwbG9yaW5nIHF1YWRyaWxhdGVyYWxzIChzcXVhcmUsIHJlY3RhbmdsZSwgcGFyYWxsZWxvZ3JhbSwgcmhvbWJ1cywgdHJhcGV6aXVtKS4gRGF5IDQ6IEludHJvZHVjdGlvbiB0byBhbmdsZXM6IHJpZ2h0IGFuZ2xlcywgYWN1dGUgYW5nbGVzLCBhbmQgb2J0dXNlIGFuZ2xlcy4gRGF5IDU6IE1lYXN1cmluZyBhbmdsZXMgdXNpbmcgYSBwcm90cmFjdG9yLiBXZWVrIDI6IDNEIFNoYXBlcyBhbmQgU3ltbWV0cnkgLSBEYXkgNjogSW50cm9kdWN0aW9uIHRvIDNEIHNoYXBlczogY3ViZXMsIGN1Ym9pZHMsIHNwaGVyZXMsIGN5bGluZGVycywgY29uZXMsIGFuZCBweXJhbWlkcy4gRGF5IDc6IERlc2NyaWJpbmcgM0Qgc2hhcGVzIHVzaW5nIGZhY2VzLCBlZGdlcywgYW5kIHZlcnRpY2VzLiBEYXkgODogUmVsYXRpbmcgMkQgc2hhcGVzIHRvIDNEIHNoYXBlcy4gRGF5IDk6IElkZW50aWZ5aW5nIGxpbmVzIG9mIHN5bW1ldHJ5IGluIDJEIHNoYXBlcy4gRGF5IDEwOiBDb21wbGV0aW5nIHN5bW1ldHJpY2FsIGZpZ3VyZXMuIFdlZWsgMzogUG9zaXRpb24sIERpcmVjdGlvbiwgYW5kIFByb2JsZW0gU29sdmluZyAtIERheSAxMTogRGVzY3JpYmluZyBwb3NpdGlvbiB1c2luZyBjb29yZGluYXRlcyBpbiB0aGUgZmlyc3QgcXVhZHJhbnQuIERheSAxMjogUGxvdHRpbmcgY29vcmRpbmF0ZXMgdG8gZHJhdyBzaGFwZXMuIERheSAxMzogVW5kZXJzdGFuZGluZyB0cmFuc2xhdGlvbiAoc2xpZGluZyBhIHNoYXBlKS4gRGF5IDE0OiBVbmRlcnN0YW5kaW5nIHJlZmxlY3Rpb24gKGZsaXBwaW5nIGEgc2hhcGUpLiBEYXkgMTU6IFByb2JsZW0tc29sdmluZyBhY3Rpdml0aWVzIGludm9sdmluZyBwZXJpbWV0ZXIsIGFyZWEsIGFuZCBtaXNzaW5nIGFuZ2xlcy4ifQ=="
}
}'
在等待响应时,请切换到另一个 Cloud Shell 终端,而不是干瞪眼。您可以在模拟器的终端中观察进度以及函数生成的任何输出或错误消息。😁
它应返回“OK”。
如需确认作业是否已成功生成并存储,请前往 Google Cloud 控制台,然后依次前往存储 >“Cloud Storage”。选择您创建的 aidemy-assignment
存储分区。您应该会在存储分区中看到一个名为 assignment-{random number}.txt
的文本文件。点击该文件即可下载并验证其内容。这会验证新文件是否包含刚生成的新作业。
👉在运行模拟器的终端中,输入 ctrl+c
以退出。然后关闭第二个终端。👉此外,在运行模拟器的终端中,退出虚拟环境。
deactivate
👉接下来,我们将将分配代理部署到云端
cd ~/aidemy-bootstrap/assignment
export ASSIGNMENT_BUCKET=$(gcloud storage buckets list --format="value(name)" | grep aidemy-assignment)
export OLLAMA_HOST=http://$(gcloud compute instances describe ollama-instance --zone=us-central1-a --format='value(networkInterfaces[0].accessConfigs[0].natIP)'):11434
export PROJECT_ID=$(gcloud config get project)
gcloud functions deploy assignment-agent \
--gen2 \
--timeout=540 \
--memory=2Gi \
--cpu=1 \
--set-env-vars="ASSIGNMENT_BUCKET=${ASSIGNMENT_BUCKET}" \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${GOOGLE_CLOUD_PROJECT} \
--set-env-vars=OLLAMA_HOST=${OLLAMA_HOST} \
--region=us-central1 \
--runtime=python312 \
--source=. \
--entry-point=generate_assignment \
--trigger-topic=plan
如需验证部署情况,请前往 Google Cloud 控制台,然后前往 Cloud Run。您应该会看到一个名为 courses-agent 的新服务。
作业生成工作流程现已实现、测试和部署完毕,我们可以继续执行下一步:在学生门户中提供这些作业。
14. 可选:使用 Gemini 和 DeepSeek 进行基于角色的协作 - 继续
动态网站生成
为了改进学生门户并提高其互动度,我们将为作业页面实现动态 HTML 生成。目标是,每当生成新的作业时,都自动使用新颖且视觉上吸引人的设计更新门户。这可以利用 LLM 的编码功能,打造更具动态性和趣味性的用户体验。
👉在 Cloud Shell Editor 中,修改 portal
文件夹中的 render.py
文件,将
def render_assignment_page():
return ""
使用以下代码段:
def render_assignment_page(assignment: str):
try:
region=get_next_region()
llm = VertexAI(model_name="gemini-2.0-flash-001", location=region)
input_msg = HumanMessage(content=[f"Here the assignment {assignment}"])
prompt_template = ChatPromptTemplate.from_messages(
[
SystemMessage(
content=(
"""
As a frontend developer, create HTML to display a student assignment with a creative look and feel. Include the following navigation bar at the top:
```
<nav>
<a href="/">Home</a>
<a href="/quiz">Quizzes</a>
<a href="/courses">Courses</a>
<a href="/assignment">Assignments</a>
</nav>
```
Also include these links in the <head> section:
```
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap" rel="stylesheet">
```
Do not apply inline styles to the navigation bar.
The HTML should display the full assignment content. In its CSS, be creative with the rainbow colors and aesthetic.
Make it creative and pretty
The assignment content should be well-structured and easy to read.
respond with JUST the html file
"""
)
),
input_msg,
]
)
prompt = prompt_template.format()
response = llm.invoke(prompt)
response = response.replace("```html", "")
response = response.replace("```", "")
with open("templates/assignment.html", "w") as f:
f.write(response)
print(f"response: {response}")
return response
except Exception as e:
print(f"Error sending message to chatbot: {e}") # Log this error too!
return f"Unable to process your request at this time. Due to the following reason: {str(e)}"
它使用 Gemini 模型为作业动态生成 HTML。它会将作业内容作为输入,并使用提示指示 Gemini 创建具有创意风格且视觉效果出色的 HTML 网页。
接下来,我们将创建一个端点,每当有新文档添加到作业分桶时,该端点都会触发:
👉在“portal”文件夹中,修改 app.py
文件,并在 ## Add your code here" comments
内(new_teaching_plan
函数之后)添加以下代码:
## Add your code here
@app.route('/render_assignment', methods=['POST'])
def render_assignment():
try:
data = request.get_json()
file_name = data.get('name')
bucket_name = data.get('bucket')
if not file_name or not bucket_name:
return jsonify({'error': 'Missing file name or bucket name'}), 400
storage_client = storage.Client()
bucket = storage_client.bucket(bucket_name)
blob = bucket.blob(file_name)
content = blob.download_as_text()
print(f"File content: {content}")
render_assignment_page(content)
return jsonify({'message': 'Assignment rendered successfully'})
except Exception as e:
print(f"Error processing file: {e}")
return jsonify({'error': 'Error processing file'}), 500
## Add your code here
触发后,该函数会从请求数据中检索文件名和存储分区名称,从 Cloud Storage 下载作业内容,并调用 render_assignment_page
函数来生成 HTML。
👉我们继续在本地运行它:
cd ~/aidemy-bootstrap/portal
source env/bin/activate
python app.py
👉从 Cloud Shell 窗口顶部的“网页预览”菜单中,选择“在端口 8080 上预览”。系统随即会在浏览器的新标签页中打开您的应用。前往导航栏中的作业链接。此时,您应该会看到一个空白页面,这是预期行为,因为我们尚未在作业代理与门户之间建立通信桥梁,以便动态填充内容。
👉如需纳入这些更改并部署更新后的代码,请重新构建并推送门户代理映像:
cd ~/aidemy-bootstrap/portal/
export PROJECT_ID=$(gcloud config get project)
docker build -t gcr.io/${PROJECT_ID}/aidemy-portal .
export PROJECT_ID=$(gcloud config get project)
docker tag gcr.io/${PROJECT_ID}/aidemy-portal us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
docker push us-central1-docker.pkg.dev/${PROJECT_ID}/agent-repository/aidemy-portal
👉推送新映像后,重新部署 Cloud Run 服务。运行以下脚本以强制执行 Cloud Run 更新:
export PROJECT_ID=$(gcloud config get project)
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
gcloud run services update aidemy-portal \
--region=us-central1 \
--set-env-vars=GOOGLE_CLOUD_PROJECT=${PROJECT_ID},COURSE_BUCKET_NAME=$COURSE_BUCKET_NAME
👉现在,我们将部署一个 Eventarc 触发器,用于监听在作业存储分区中创建(最终确定)的任何新对象。创建新的作业文件时,此触发器会自动在门户服务上调用 /render_assignment 端点。
export PROJECT_ID=$(gcloud config get project)
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$(gcloud storage service-agent --project $PROJECT_ID)" \
--role="roles/pubsub.publisher"
export SERVICE_ACCOUNT_NAME=$(gcloud compute project-info describe --format="value(defaultServiceAccount)")
gcloud eventarc triggers create portal-assignment-trigger \
--location=us-central1 \
--service-account=$SERVICE_ACCOUNT_NAME \
--destination-run-service=aidemy-portal \
--destination-run-region=us-central1 \
--destination-run-path="/render_assignment" \
--event-filters="bucket=$ASSIGNMENT_BUCKET" \
--event-filters="type=google.cloud.storage.object.v1.finalized"
如需验证触发器是否已成功创建,请前往 Google Cloud 控制台中的 Eventarc 触发器页面。您应该会在表格中看到 portal-assignment-trigger
。点击触发器名称以查看其详细信息。
新触发器最长可能需要 2-3 分钟才能生效。
如需查看动态作业生成过程,请运行以下命令以查找规划程序代理的网址(如果您没有随手准备好):
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep planner
查找您的门户代理的网址:
gcloud run services list --platform=managed --region=us-central1 --format='value(URL)' | grep portal
在规划器代理中,生成新的教学计划。
等待几分钟(以便系统完成音频生成、作业生成和 HTML 呈现),然后前往学生门户。
👉点击导航栏中的“作业”链接。您应该会看到一个新创建的作业,其中包含动态生成的 HTML。每次生成教学计划时,都应为动态作业。
恭喜您完成 Aidemy 多代理系统!您已获得实用经验,并对以下方面有了宝贵的洞见:
- 多代理系统的优势,包括模块化、可扩缩性、专业化和简化维护。
- 事件驱动型架构在构建响应迅速且松散耦合的应用方面的重要性。
- 战略性地使用 LLM,将合适的模型与任务相匹配,并将其与工具集成以产生实际影响。
- 使用 Google Cloud 服务构建可扩缩且可靠的解决方案的云原生开发实践。
- 将数据隐私保护和自托管模式视为供应商解决方案的替代方案的重要性。
现在,您已经打下了坚实的基础,可以开始在 Google Cloud 上构建依托 AI 技术的复杂应用了!
15. 挑战和后续步骤
恭喜您构建了 Aidemy 多智能体系统!您已为 AI 赋能的教育奠定了坚实的基础。现在,我们来考虑一些挑战和未来可能的增强功能,以进一步扩展其功能并满足实际需求:
通过问答直播进行互动式学习:
- 挑战:您能否利用 Gemini 2 的 Live API 为学生创建实时问答功能?想象一下,在虚拟课堂中,学生可以提问并立即获得 AI 生成的回答。
自动提交作业和评分:
- 挑战:设计并实现一个系统,让学生能够以数字方式提交作业,并由 AI 自动评分,同时还能检测和防止抄袭。这项挑战为我们提供了一个很好的机会来探索检索增强生成 (RAG),以提高评分和抄袭检测流程的准确性和可靠性。
16. 清理
现在,我们已经构建并探索了 Aidemy 多代理系统,接下来需要清理 Google Cloud 环境。
- 删除 Cloud Run 服务
gcloud run services delete aidemy-planner --region=us-central1 --quiet
gcloud run services delete aidemy-portal --region=us-central1 --quiet
gcloud run services delete courses-agent --region=us-central1 --quiet
gcloud run services delete book-provider --region=us-central1 --quiet
gcloud run services delete assignment-agent --region=us-central1 --quiet
- 删除 Eventarc 触发器
gcloud eventarc triggers delete portal-assignment-trigger --location=us --quiet
gcloud eventarc triggers delete plan-topic-trigger --location=us-central1 --quiet
gcloud eventarc triggers delete portal-assignment-trigger --location=us-central1 --quiet
ASSIGNMENT_AGENT_TRIGGER=$(gcloud eventarc triggers list --project="$PROJECT_ID" --location=us-central1 --filter="name:assignment-agent" --format="value(name)")
COURSES_AGENT_TRIGGER=$(gcloud eventarc triggers list --project="$PROJECT_ID" --location=us-central1 --filter="name:courses-agent" --format="value(name)")
gcloud eventarc triggers delete $ASSIGNMENT_AGENT_TRIGGER --location=us-central1 --quiet
gcloud eventarc triggers delete $COURSES_AGENT_TRIGGER --location=us-central1 --quiet
- 删除发布/订阅主题
gcloud pubsub topics delete plan --project="$PROJECT_ID" --quiet
- 删除 Cloud SQL 实例
gcloud sql instances delete aidemy --quiet
- 删除 Artifact Registry 代码库
gcloud artifacts repositories delete agent-repository --location=us-central1 --quiet
- 删除 Secret Manager Secret
gcloud secrets delete db-user --quiet
gcloud secrets delete db-pass --quiet
gcloud secrets delete db-name --quiet
- 删除 Compute Engine 实例(如果为 Deepseek 创建)
gcloud compute instances delete ollama-instance --zone=us-central1-a --quiet
- 删除 Deepseek 实例的防火墙规则
gcloud compute firewall-rules delete allow-ollama-11434 --quiet
- 删除 Cloud Storage 存储分区
export COURSE_BUCKET_NAME=$(gcloud storage buckets list --format="value(name)" | grep aidemy-recap)
export ASSIGNMENT_BUCKET=$(gcloud storage buckets list --format="value(name)" | grep aidemy-assignment)
gsutil rb gs://$COURSE_BUCKET_NAME
gsutil rb gs://$ASSIGNMENT_BUCKET