1. 概览
AI 和技术领域的发展速度之快,让任何人都难以跟上。每天都有新模型、新论文和新产品发布。如果有一个摘要代理,每天早上都能获取当天的新闻头条、撰写精辟的摘要并生成 PDF,就能解决这个问题。但以前构建这样的代理意味着要选择框架、在 Python 中定义工具、编写编排循环、打包容器并部署到 Cloud Run。在代理发出任何 Web 请求之前,所有这些操作都已完成。
Gemini API 中的托管式智能体改变了这一局面。您只需编写两个 Markdown 配置文件和一个预构建的渲染器脚本,进行一次 API 调用,即可启动真实的 Ubuntu 沙盒、浏览网页、撰写摘要并生成 PDF。没有容器。无部署。无需编排代码。
在此 Codelab 中,您将构建一个这样的代理:从空函数开始,逐步实现每日摘要功能。
构建内容
- 在真实的 Linux 沙盒中创建并运行首个托管式智能体
- 使用详细说明自定义智能体
- 下载代理的 PDF 输出
- 继续对话以优化摘要,而无需重新提取网页内容
- 保存代理配置,并在以后的运行中通过 ID 调用该配置
所需条件
- Python 3.10 及更高版本
- 已启用结算功能的 Gemini API 密钥:aistudio.google.com/api-keys
- 约 1 美元的 API 赠金(每次完整运行的费用为 0.30-1.30 美元)
2. 什么是 Gemini API 中的托管式智能体?
AI 系统的三个级别
在深入了解代码之前,我们先来看看托管式智能体相对于两种替代方案的适用场景:
级别 | 简介 | 谁管理基础设施? |
标准 LLM | 您提出提示,它会以文本形式回复。没有手、没有记忆、没有网络。 | 不适用:无法自行执行任何操作 |
自托管代理 | 您将 ADK/LangChain/AutoGen + Docker + 工具 + 内存连接起来。 | 您:所有内容(或 Agent Engine 等受管理的平台) |
受管代理 | 您为其设定目标。Google 会提供一个安全沙盒。智能体可以自主编写代码、运行代码、读取错误、搜索网络和修复 bug。 | Google:所有内容 |
此 Codelab 重点介绍第三行。您提供任务和配置文件。其他一切都由 Google 处理。
您可以使用 ADK + Cloud Run 构建哪些内容
如需构建一个可浏览网页、运行 Python 并生成 PDF 的新闻摘要智能体,您需要使用 ADK + Cloud Run 实现以下所有功能:
# agent.py: define tools and wire up the agent
from google.adk.agents import LlmAgent
from google.adk.tools import google_search, built_in_code_execution
agent = LlmAgent(
name="digest-agent",
model=MODEL,
instruction=AGENTS_MD, # your editorial voice and rules
tools=[google_search, built_in_code_execution],
)
# app.py: serve the agent over HTTP
from google.adk.runners import FastApiRunner
runner = FastApiRunner(agent=agent)
app = runner.app
# pdf_tool.py: custom tool, install reportlab, render PDF
# scraper.py: custom tool, fetch each news source
# streaming.py: wire agent events to your SSE endpoint
# Dockerfile: package everything
FROM python:3.12
COPY . /app
RUN pip install google-adk reportlab requests
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
# Deploy to Cloud Run
gcloud run deploy digest-agent \
--image gcr.io/your-project/digest-agent \
--set-secrets GEMINI_API_KEY=gemini-key:latest \
--memory 2Gi
即在代理运行一次之前。您仍然拥有沙盒隔离(因此代理无法损坏您的服务器)、软件包安装、工具调用之间的状态管理以及将事件流式传输到客户端的基础设施。
托管式智能体将其替换为
from google import genai
client = genai.Client()
stream = client.interactions.create(
agent="antigravity-preview-05-2026",
input="",
stream=True,
environment={
"type": "remote",
"sources": [ # your config files, mounted at startup
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
)
ADK + Cloud Run 的要求 | 托管式智能体为您处理哪些事务 |
容器映像 + Dockerfile + CI/CD | 完全受管理的 Ubuntu 沙盒(Python 3.12、Node 22、4 个 CPU / 16 GB RAM) |
Cloud Run 部署 + 扩缩 | 按每次互动进行配置,在处于非活跃状态 7 天后自动过期 |
沙盒隔离 | 每次互动隔离 |
自定义 PDF 工具 + | 代理在沙盒内安装软件包 |
SSE 流式传输基础架构 |
|
Python 中的工具定义 | 内置工具:网页浏览、代码执行、文件系统 |
工具调用之间的状态管理 | 内置于代理推理循环中 |
您只需编写配置文件(AGENTS.md、SKILL.md、预构建的脚本)并进行一次 API 调用。其他一切都由 Google 处理。
沙盒的运作方式
interactions.create() call
│
▼
Google provisions Ubuntu sandbox (Python 3.12, Node 22, 4 CPU / 16 GB RAM)
│
▼
Agent reasoning loop:
plan → fetch URLs → run Python → write files → reason → repeat
│
▼
Events stream back in real time: tool calls, text chunks, completion
│
▼
interaction.completed → environment_id + interaction_id
沙盒会在处于非活跃状态 7 天后失效。您可以使用 environment_id 恢复对话,以优化输出、运行后续任务,或将其派生为已保存的命名代理。
3. 设置
选项 A:Cloud Shell(推荐)
点击下方按钮,在 Google Cloud Shell 中打开此 Codelab。所有依赖项均已预安装。
方案 B:本地设置
git clone https://github.com/Saoussen-CH/tech-digest-managed-agent.git
cd tech-digest-managed-agent
根据需要安装 uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
配置 API 密钥
cp .env.example .env
cloudshell edit .env
设置密钥:
GEMINI_API_KEY=your-key-here
安装依赖项
uv sync
4. 发出首次智能体调用
打开起始文件
cloudshell edit run_digest.py
run_digest() 现在有一个 TODO 需要填写,还有三个 TODO 需要在下一步中填写。系统已在上方预填充了两个辅助函数:
load_source(path):从相对于脚本的.agents/读取文件。您将在下一个练习中使用它,将编辑风格、PDF 剧本和渲染程序装载到沙盒中。run_stream(stream):处理事件流并返回(environment_id, interaction_id)。您无需自行编写事件循环。
添加内容
TODO 1:将 pass 替换为(暂时忽略 TODO 3 和 4,这些是下一步要完成的):
from google import genai
client = genai.Client()
stream = client.interactions.create(
agent=BASE_AGENT,
input="Fetch the Hacker News front page and list the top 5 stories.",
stream=True,
environment="remote",
)
environment_id, interaction_id = run_stream(stream)
print(f"\nDone. environment_id={environment_id}")
各部分的用途
genai.Client() 从环境中读取 GEMINI_API_KEY。其他所有内容都通过此客户端进行。
interactions.create() 是核心调用。以下四个参数可让此功能正常运行:
agent=BASE_AGENT:选择 Antigravity 智能体 (antigravity-preview-05-2026),这是一款由 Gemini 3.5 Flash 提供支持的通用型托管式智能体。它随附了三款默认启用的内置工具:code_execution(运行 Bash、Python、Node.js)、google_search和url_context(提取和读取网页)。当您传递environment参数时,系统会自动启用文件系统工具(read_file、write_file、list_files)。只需一次调用,即可预配一个完全托管的 Ubuntu 环境,其中预安装了 Python 3.12、Node.js 22、git、pip 和 curl。没有要构建的容器,也没有要运行的部署。input:相应运行的任务。智能体浏览 Hacker News 并推断结果。environment="remote":为此互动预配全新的云沙盒。stream=True:返回事件的可迭代对象,而不是阻塞。如果不使用此参数,调用会等待 30-90 秒,然后以interaction.output_text形式一次性返回所有输出。通过流式传输,您可以实时查看智能体的推理和行动。流式传输在这里并不是一项高级功能:它是一个合适的默认设置,因为 90 秒的黑框不会向您发出有关代理是否正在工作或卡住的信号。
您刚刚配置的内容:每次 interactions.create() 调用都会启动一个专用沙盒:
组件 | 规范 |
操作系统 | 隔离的 Ubuntu Linux 环境 |
预安装的运行时 | Python 3.12、Node.js 22、Bash |
计算 | 4 个 CPU 核心,16 GB RAM |
上下文管理 | 自动压缩在约 13.5 万个令牌时触发 |
网络 | 通过出站代理进行出站 Web 访问 |
代理可以安装任何包含 pip 或 npm 的软件包、读取和写入文件,以及发出出站 Web 请求。我们绝不会触碰您的机器和凭据。
environment_id 是刚刚运行的沙盒的句柄。在 interaction.completed 之后,沙盒不会关闭,而是会保持运行状态,最长可达 7 天。您可以通过 environment_id 返回到该页面。将其传递给第二次 interactions.create() 调用,代理会在同一文件系统上恢复,并使用相同的文件和已安装的软件包,就像从未离开过一样。下一步使用它来下载 PDF,而无需重新运行代理,再下一步使用它来继续对话。
interaction_id 是刚刚完成的对话轮次的句柄。在下一次调用中将其作为 previous_interaction_id 传递,这样一来,代理就会完全记住它在此轮对话中说过的话和做过的事。
验证
uv run python run_digest.py
您应该会看到代理运行时的实时输出:
[agent started]
[tool] run_code
Here are the top 5 stories currently on the Hacker News front page, retrieved via the official Hacker News API:
1. **Qwen 3.6 27B is the sweet spot for local development** — 471 points
2. **.self: A new top-level domain designed to support self-hosting** — 116 points
...
Done. environment_id=e3de58774073f75a6ef42924c6ce2e88
即使存在 environment="remote",该 API 也会返回实际的 environment_id。沙盒已运行。缺少的是配置:没有语音、没有技能、没有 PDF 生成器。智能体只是将故事打印为文本,然后就停止了。下一步将添加这些内容。
输出的每一行都对应于 run_stream() 中的一个事件:
| 简介 |
|
| 代理提取网址 |
|
| 在沙盒中运行代码的智能体 |
|
| 智能体搜索网页 |
|
| 文件工具和其他工具 |
|
| 代理撰写文字 | 直接流式传输到 stdout |
5. 自定义智能体
该代理没有指令:没有语音、技能或 PDF 生成器。在此步骤中,您将从 .agents/ 加载配置文件,并将它们装载到沙盒中。
要更改的内容
对 run_digest.py 进行以下四处更改:
TODO 2:在 load_source() 下方,添加三个模块级常量(这些常量位于 run_digest() 外部,在文件顶部):
AGENTS_MD = load_source(".agents/AGENTS.md")
SKILL_MD = load_source(".agents/skills/digest-pdf/SKILL.md")
GENERATE_PDF_PY = load_source(".agents/skills/digest-pdf/scripts/generate_pdf.py")
打开每个文件,查看您要加载的内容:AGENTS.md 设置编辑风格和工作流规则;SKILL.md 是分步 PDF playbook;generate_pdf.py 是代理将运行的预构建渲染器。
现在,在 run_digest() 内再进行两项更改:
TODO 3:将 environment 从 "remote" 更改为 sources 字典,并将 input 设置为 "":
environment={
"type": "remote",
"sources": [
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
TODO 4:在 print(f"\nDone. environment_id={environment_id}") 后面添加以下几行代码:
set_key(".env", "ENVIRONMENT_ID", environment_id)
set_key(".env", "INTERACTION_ID", interaction_id)
(set_key 已在 run_digest.py 的顶部导入。)
这会在本次运行期间将这两个 ID 写入 .env,以便下一步可以下载 PDF,而无需重新运行代理。
每个来源的作用
每个来源都是在代理运行之前启动时装载到沙盒文件系统中的文件。target 路径与 Antigravity 框架预期找到它们的路径一致:
.agents/
├── AGENTS.md ← auto-loaded as global instructions
└── skills/
└── digest-pdf/
├── SKILL.md ← auto-discovered and registered as a skill
└── scripts/
└── generate_pdf.py ← pre-built renderer the agent can run
| 变量 | 安全带如何处理它 |
|
| 自动加载为持久性指令:编辑风格、工作流程、执行规则 |
|
| 自动发现并注册为命名技能;代理按名称调用该技能 |
|
| 预构建的 PDF 渲染器;代理写入 |
验证
uv run python run_digest.py
现在,运行时间为 1-3 分钟。您应该会看到代理读取配置文件、撰写摘要并保存 PDF:
[agent started]
[tool] read_file (/.agents/skills/digest-pdf/SKILL.md)
[tool] list_files (/.agents/skills/digest-pdf/scripts)
[tool] read_file (/.agents/skills/digest-pdf/scripts/generate_pdf.py)
[tool] run_code
[tool] write_file (/workspace/summaries.json)
[tool] run_code
[tool] delete_file (/tmp/test_scrape.py)
I have successfully generated today's tech news digest and saved the formatted document to /workspace/digest.pdf.
Done. environment_id=4129ffd75574e308748e9425d7ec828f
environment_id 现在是一个实际值:沙盒已使用您的配置文件运行,并且代理已创建 digest.pdf。下一步是下载该文件。
6. 下载 PDF
代理将 digest.pdf 写入了沙盒内的 /workspace/digest.pdf。环境快照可通过 Gemini Files API 以 tar 归档的形式提供。
根据需要安装 requests:
uv pip install requests
需要填写的内容
打开 download_pdf.py。它有两个 TODO。
TODO 1:填写 requests.get() 调用:
r = requests.get(
f"https://generativelanguage.googleapis.com/v1beta/files/environment-{environment_id}:download",
params={"alt": "media"},
headers={"x-goog-api-key": api_key},
allow_redirects=True,
)
r.raise_for_status()
网址指向沙盒快照。params={"alt": "media"} 返回原始字节,而不是元数据。您现有的 GEMINI_API_KEY 也会对 Files API 进行身份验证。
TODO 2:从 tar 归档中查找并提取 PDF:
member = next(m for m in tar.getmembers() if m.name.endswith("workspace/digest.pdf"))
tar.extract(member, path=tmp, filter="data")
tar 路径前缀因运行而异,因此请按后缀进行搜索,而不是对确切路径进行硬编码。filter="data" 会抑制 Python 3.13 关于不安全 tar 提取的弃用警告。
验证
uv run python download_pdf.py
Saved digest.pdf (48,231 bytes)
打开同一目录中的 digest.pdf。它包含代理从实时网页生成的格式化摘要。
7. 继续对话
您已拥有 digest.pdf。如果您只是想要该文件,那么到此为止就完成了。此步骤与上述步骤不同,它要求代理更改摘要,而无需重新提取网页。
沙盒仍然有效。智能体仍然具有 /workspace/digest.pdf,并且会记住它总结的每个故事。第二次 interactions.create() 调用会将后续消息发送到同一沙盒中。在此示例中,您要求它在每个故事下方添加“重要性”注释,然后它会就地更新 PDF,而无需重新提取和重新总结。
需要填写的内容
打开 refine_digest.py。它有 3 个 TODO。
待办事项 1 和 2:填写 interactions.create() 内的两个多轮参数:
environment=environment_id,
previous_interaction_id=interaction_id,
environment=environment_id 恢复包含文件和软件包的同一沙盒。previous_interaction_id=interaction_id 为代理提供对话历史记录。与第一次调用相比,其他方面没有任何变化。
TODO 3:在事件循环后将新的 interaction_id 持久保存回 .env:
set_key(".env", "INTERACTION_ID", interaction_id)
每次调用 interactions.create() 都会生成一个新的 interaction_id。写回意味着下一次运行会将此细化结果作为 previous_interaction_id 传递,从而正确地链接回合。沙盒 ID 永远不会改变,因此无需更新 ENVIRONMENT_ID。
使多轮对话正常运行的两个参数
ID | 保留的内容 | 类比 |
| 文件、已安装的软件包、系统状态:Linux 文件系统上的所有内容 | 在会议之间保持办公桌不变 |
| 对话历史记录:智能体在之前的对话轮次中说过什么和做过什么 | 记住上次会议的讨论内容 |
您可以单独传递任一 ID:
environment_id:重复使用文件和软件包,但发起全新对话。适用于同一工作区中的新任务。previous_interaction_id:继续对话上下文,但在全新的沙盒中进行(文件已消失)。- 两者:完全连续性,这是此步骤所使用的。
不含 environment_id:空白沙盒,无 PDF。不含 previous_interaction_id:没有上下文,代理无法优化特定部分。
验证
uv run python refine_digest.py
该数据流应快速完成,智能体不会重新提取任何内容。完成后:
Refinement done.
Saved digest_v2.pdf (52,418 bytes)
打开 digest_v2.pdf 并将其与 digest.pdf 进行比较。现在,每篇报道都应添加“重要性”行。
8. 持久保留受管理的代理配置
到目前为止,每次调用都已内联传递 AGENTS.md、SKILL.md 和 generate_pdf.py。这样可以正常运行,但您的调用代码会在每次运行时携带完整的文件内容。agents.create() 将配置烘焙到 Google 端已保存的命名代理中。下一次调用只需传递代理 ID:
Inline calls: send sources on every call
Named agent: bake once → invoke by ID, no sources
需要填写的内容
打开 save_agent.py。它有一个 TODO(TODO 1)。
请注意,常量直接从 run_digest.py 导入(无重复):
from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY
TODO 1:填写 agents.create() 调用:
agent = client.agents.create(
id="my-digest",
base_agent=BASE_AGENT,
description="Daily tech digest with editorial voice and PDF generation.",
base_environment={
"type": "remote",
"sources": [
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
)
base_environment(而非 environment)是与上一步中的内嵌调用之间的主要区别:来源存储在 Google 端,并在每次后续调用时自动装载。运行一次,而不是在每次摘要运行时都运行。
验证:保存代理
uv run python save_agent.py
Saved: my-digest
my-digest: Daily tech digest with editorial voice and PDF generation.
调用已保存的智能体
打开 invoke_agent.py。它通过 ID 调用已保存的代理,但不包含任何来源:
stream = client.interactions.create(
agent="my-digest",
input="",
stream=True,
environment="remote",
)
将其与内嵌调用进行比较:agent=BASE_AGENT 被替换为 "my-digest",包含三个内嵌来源的整个 environment 块被替换为 environment="remote"。配置已在 Google 端烘焙完成。
验证:调用已保存的代理
uv run python invoke_agent.py
您应该会看到与内嵌运行相同的直播,但该调用不包含任何源文件。运行后,.env 中的 ENVIRONMENT_ID 和 INTERACTION_ID 会更新,以便您像之前一样继续使用 refine_digest.py。
[agent started]
[tool] read_file
[tool] write_file
[tool] run_code
I have successfully created today's tech news digest.
Done. environment_id=9a1c3e02-...
9. 清理
沙盒会在处于非活跃状态 7 天后自动过期。没有要停止的服务器。没有要删除的容器。
如果您保存了代理配置,请将其删除:
uv run python delete_agent.py
10. 总结
您从头开始构建了一个受管理的代理,一次学习一个概念。以下是每项练习的教学内容:
锻炼 | 概念 | Key API |
发出首次调用 | 预配真实的 Linux 沙盒并实时直播其事件 |
|
自定义智能体 | 装载配置文件;在同一运行中将 ID 持久保存到 |
|
下载 PDF | 下载 PDF,无需重新运行代理 |
|
继续对话 | 继续对话,而不重新提取网页内容 |
|
持久保留代理配置 | 持久保留代理配置;按 ID 调用,无需来源 |
|
键格式
- 一次调用,一个沙盒:
interactions.create()处理所有基础架构(无需部署容器,无需在本地安装软件包) - 渐进式流式传输:
stream=True将 90 秒的黑框转换为工具调用和文本块的实时 Feed - 内嵌来源:将
AGENTS.md、SKILL.md和预构建的脚本装载到沙盒中,无需执行任何上传或部署步骤 - 利用自动发现功能:系统会自动提取放置在
.agents/中的文件(无需 SDK 配置) - 二维状态:
environment_id跟踪文件和软件包;previous_interaction_id跟踪对话上下文;两者可以单独传递 - 快照下载:环境是一个完整的文件系统 tar,可通过 Gemini Files API 访问
- 命名代理:
agents.create()会永久烘焙配置;未来的调用仅传递代理 ID 和environment="remote",不传递来源
ADK + Cloud Run 与托管式智能体:差异一览
能力 | ADK + Cloud Run | Gemini API 中的托管式智能体 |
配置沙盒 |
|
|
定义工具 | 向智能体注册的 Python 函数 | 内置:网页浏览、代码执行、文件系统 |
安装软件包 | Dockerfile 中的 | 代理在沙盒内运行 |
流式事件 | 自定义 SSE 基础架构 |
|
继续会话 | 会话数据库 + 上下文注入 |
|
配置文件 | 在代理中硬编码或在启动时注入 | 通过 |
要管理的基础设施 | 容器、Cloud Run、IAM、密钥 | 无 |