跳转到内容

构建您的首个智能体团队:基于ADK的渐进式天气机器人

本教程基于快速入门示例中的Agent Development Kit进行扩展。现在,您已准备好深入探索并构建一个更复杂的多智能体系统

我们将着手构建一个天气机器人智能体团队,在简单基础上逐步叠加高级功能。从单个能查询天气的智能体开始,我们将逐步添加以下能力:

  • 利用不同的AI模型(Gemini、GPT、Claude)。
  • 为不同任务(如问候和告别)设计专门的子智能体。
  • 实现智能体之间的智能委托。
  • 通过持久会话状态为智能体提供记忆功能。
  • 使用回调函数实现关键的安全防护措施。

为什么需要一个天气机器人团队?

这个用例虽然看似简单,却提供了一个实用且易于理解的场景,用于探索构建复杂现实世界智能体应用所需的核心ADK概念。您将学习如何构建交互结构、管理状态、确保安全性,以及协调多个协同工作的AI"大脑"。

什么是ADK?

需要提醒的是,ADK是一个Python框架,旨在简化由大型语言模型(LLMs)驱动的应用程序开发。它提供了强大的构建模块,用于创建能够推理、规划、使用工具、与用户动态交互以及在团队中有效协作的智能体。

在本高级教程中,您将掌握:

  • 工具定义与使用: 编写Python函数(tools)为智能体赋予特定能力(如获取数据),并指导智能体如何有效使用这些工具。
  • 多LLM灵活性: 通过LiteLLM集成配置智能体以利用各种领先的LLM(Gemini、GPT-4o、Claude Sonnet),让您能为每项任务选择最佳模型。
  • 智能体委派与协作: 设计专用子智能体,并支持在团队内将用户请求自动路由(auto flow)至最合适的智能体。
  • 会话状态记忆功能: 利用 Session StateToolContext 使智能体能够在多轮对话中记住信息,从而实现更具上下文关联的交互。
  • 带回调的安全防护栏: 通过实现before_model_callbackbefore_tool_callback来检查、修改或基于预定义规则阻止请求/工具使用,从而增强应用程序的安全性和控制力。

最终状态预期:

完成本教程后,您将构建一个功能完善的多智能体天气机器人系统。该系统不仅能提供天气信息,还能处理对话细节、记住上次查询的城市,并在设定的安全边界内运行,所有功能都通过ADK进行协调。

前提条件:

  • 扎实掌握Python编程。
  • 熟悉大型语言模型(LLMs)、API接口和智能体概念。
  • 关键点:完成ADK快速入门教程或具备同等基础的ADK知识(智能体、Runner、SessionService、基本工具使用)。 本教程直接基于这些概念展开。
  • ✅ 您计划使用的LLM所需的API密钥(例如用于Gemini的Google AI Studio、OpenAI平台、Anthropic控制台)。

准备好构建您的智能体团队了吗?让我们开始吧!

步骤0:设置与安装

库安装

!pip install google-adk -q
!pip install litellm -q

print("Installation complete.")

导入库

import os
import asyncio
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm # For multi-model support
from google.adk.sessions import InMemorySessionService
from google.adk.runners import Runner
from google.genai import types # For creating message Content/Parts

import warnings
# Ignore all warnings
warnings.filterwarnings("ignore")

import logging
logging.basicConfig(level=logging.ERROR)

print("Libraries imported.")

设置API密钥

# --- IMPORTANT: Replace placeholders with your real API keys ---

# Gemini API Key (Get from Google AI Studio: https://aistudio.google.com/app/apikey)
os.environ["GOOGLE_API_KEY"] = "YOUR_GOOGLE_API_KEY" # <--- REPLACE

# OpenAI API Key (Get from OpenAI Platform: https://platform.openai.com/api-keys)
os.environ['OPENAI_API_KEY'] = 'YOUR_OPENAI_API_KEY' # <--- REPLACE

# Anthropic API Key (Get from Anthropic Console: https://console.anthropic.com/settings/keys)
os.environ['ANTHROPIC_API_KEY'] = 'YOUR_ANTHROPIC_API_KEY' # <--- REPLACE


# --- Verify Keys (Optional Check) ---
print("API Keys Set:")
print(f"Google API Key set: {'Yes' if os.environ.get('GOOGLE_API_KEY') and os.environ['GOOGLE_API_KEY'] != 'YOUR_GOOGLE_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")
print(f"OpenAI API Key set: {'Yes' if os.environ.get('OPENAI_API_KEY') and os.environ['OPENAI_API_KEY'] != 'YOUR_OPENAI_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")
print(f"Anthropic API Key set: {'Yes' if os.environ.get('ANTHROPIC_API_KEY') and os.environ['ANTHROPIC_API_KEY'] != 'YOUR_ANTHROPIC_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}")

# Configure ADK to use API keys directly (not Vertex AI for this multi-model setup)
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False"


# @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above.

定义模型常量以便于使用

MODEL_GEMINI_2_0_FLASH = "gemini-2.0-flash"

# Note: Specific model names might change. Refer to LiteLLM or the model provider's documentation.
MODEL_GPT_4O = "openai/gpt-4o"
MODEL_CLAUDE_SONNET = "anthropic/claude-3-sonnet-20240229"


print("\nEnvironment configured.")

步骤1:您的第一个智能体 - 基础天气查询

让我们从构建天气机器人的基础组件开始:一个能够执行特定任务——查询天气信息的智能体。这涉及创建两个核心部分:

  1. 工具: 一个Python函数,为智能体提供获取天气数据的能力
  2. 智能体: 作为AI的"大脑",它能理解用户的请求,知道自己拥有天气工具,并决定何时以及如何使用该工具。

1. 定义工具

在ADK中,工具是赋予智能体超越文本生成的具体能力的构建模块。它们通常是执行特定操作的常规Python函数,例如调用API、查询数据库或执行计算。

我们的第一个工具将提供一个模拟天气报告。这让我们可以专注于智能体结构,而暂时不需要外部API密钥。之后,您可以轻松地将这个模拟函数替换为调用真实天气服务的函数。

关键概念:文档字符串至关重要! 智能体的LLM在很大程度上依赖于函数的文档字符串来理解:

  • 功能 该工具的作用。
  • 何时使用它。
  • 需要哪些参数 (city: str)。
  • 返回什么信息

最佳实践:为您的工具编写清晰、描述性强且准确的文档字符串。这对于LLM正确使用该工具至关重要。

# @title Define the get_weather Tool
def get_weather(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Args:
        city (str): The name of the city (e.g., "New York", "London", "Tokyo").

    Returns:
        dict: A dictionary containing the weather information.
              Includes a 'status' key ('success' or 'error').
              If 'success', includes a 'report' key with weather details.
              If 'error', includes an 'error_message' key.
    """
    # Best Practice: Log tool execution for easier debugging
    print(f"--- Tool: get_weather called for city: {city} ---")
    city_normalized = city.lower().replace(" ", "") # Basic input normalization

    # Mock weather data for simplicity
    mock_weather_db = {
        "newyork": {"status": "success", "report": "The weather in New York is sunny with a temperature of 25°C."},
        "london": {"status": "success", "report": "It's cloudy in London with a temperature of 15°C."},
        "tokyo": {"status": "success", "report": "Tokyo is experiencing light rain and a temperature of 18°C."},
    }

    # Best Practice: Handle potential errors gracefully within the tool
    if city_normalized in mock_weather_db:
        return mock_weather_db[city_normalized]
    else:
        return {"status": "error", "error_message": f"Sorry, I don't have weather information for '{city}'."}

# Example tool usage (optional self-test)
print(get_weather("New York"))
print(get_weather("Paris"))

2. 定义智能体

现在,让我们创建智能体本身。在Agent Development Kit中,Agent负责协调用户、大语言模型和可用工具之间的交互。

我们配置了几个关键参数:

  • name: 该智能体的唯一标识符(例如:"weather_agent_v1")。
  • model: 指定要使用的LLM(例如MODEL_GEMINI_2_5_PRO)。我们将从一个特定的Gemini模型开始。
  • description: 对智能体整体目的的简要概述。当其他智能体需要决定是否将任务委托给智能体时,这一点变得至关重要。
  • instruction: 为LLM提供详细的行为指导,包括其角色设定、目标以及具体方式和时机来使用分配的tools工具。
  • tools: 一个包含智能体允许使用的实际Python工具函数的列表(例如[get_weather])。

最佳实践:提供清晰具体的instruction提示。指令越详细,LLM越能理解其角色以及如何有效使用工具。如果需要,请明确说明错误处理方式。

最佳实践:namedescription选择描述性的值。这些值会被ADK内部使用,对于自动委托等功能至关重要(将在后面介绍)。

# @title Define the Weather Agent
# Use one of the model constants defined earlier
AGENT_MODEL = MODEL_GEMINI_2_5_PRO # Starting with a powerful Gemini model

weather_agent = Agent(
    name="weather_agent_v1",
    model=AGENT_MODEL, # Specifies the underlying LLM
    description="Provides weather information for specific cities.", # Crucial for delegation later
    instruction="You are a helpful weather assistant. Your primary goal is to provide current weather reports. "
                "When the user asks for the weather in a specific city, "
                "you MUST use the 'get_weather' tool to find the information. "
                "Analyze the tool's response: if the status is 'error', inform the user politely about the error message. "
                "If the status is 'success', present the weather 'report' clearly and concisely to the user. "
                "Only use the tool when a city is mentioned for a weather request.",
    tools=[get_weather], # Make the tool available to this agent
)

print(f"Agent '{weather_agent.name}' created using model '{AGENT_MODEL}'.")

3. 设置运行器和会话服务

为了管理对话并执行智能体,我们还需要两个组件:

  • SessionService: 负责管理不同用户和会话的对话历史与状态。InMemorySessionService是一个简单实现,将所有内容存储在内存中,适用于测试和简单应用场景。它会记录所有交换的消息。我们将在步骤4中更详细探讨状态持久化。
  • Runner: 协调交互流程的引擎。它接收用户输入,将其路由到适当的智能体,根据智能体逻辑管理对LLM和工具的调用,通过SessionService处理会话更新,并生成代表交互进度的事件。
# @title Setup Session Service and Runner

# --- Session Management ---
# Key Concept: SessionService stores conversation history & state.
# InMemorySessionService is simple, non-persistent storage for this tutorial.
session_service = InMemorySessionService()

# Define constants for identifying the interaction context
APP_NAME = "weather_tutorial_app"
USER_ID = "user_1"
SESSION_ID = "session_001" # Using a fixed ID for simplicity

# Create the specific session where the conversation will happen
session = session_service.create_session(
    app_name=APP_NAME,
    user_id=USER_ID,
    session_id=SESSION_ID
)
print(f"Session created: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")

# --- Runner ---
# Key Concept: Runner orchestrates the agent execution loop.
runner = Runner(
    agent=weather_agent, # The agent we want to run
    app_name=APP_NAME,   # Associates runs with our app
    session_service=session_service # Uses our session manager
)
print(f"Runner created for agent '{runner.agent.name}'.")

4. 与智能体交互

我们需要一种方法来向智能体发送消息并接收其响应。由于LLM调用和工具执行可能需要时间,ADK的Runner采用异步方式运行。

我们将定义一个async辅助函数(call_agent_async),该函数:

  1. 接收用户查询字符串。
  2. 将其打包为ADK Content格式。
  3. 调用runner.run_async,提供用户/会话上下文和新消息。
  4. 遍历运行器产生的事件。事件表示智能体执行过程中的各个步骤(例如:请求工具调用、接收工具结果、中间LLM思考、最终响应)。
  5. 使用event.is_final_response()识别并打印最终响应事件。

为什么使用async 与LLMs以及可能的工具(如外部API)的交互属于I/O密集型操作。使用asyncio可以让程序高效处理这些操作而不会阻塞执行。

# @title Define Agent Interaction Function
import asyncio
from google.genai import types # For creating message Content/Parts

async def call_agent_async(query: str):
  """Sends a query to the agent and prints the final response."""
  print(f"\n>>> User Query: {query}")

  # Prepare the user's message in ADK format
  content = types.Content(role='user', parts=[types.Part(text=query)])

  final_response_text = "Agent did not produce a final response." # Default

  # Key Concept: run_async executes the agent logic and yields Events.
  # We iterate through events to find the final answer.
  async for event in runner.run_async(user_id=USER_ID, session_id=SESSION_ID, new_message=content):
      # You can uncomment the line below to see *all* events during execution
      # print(f"  [Event] Author: {event.author}, Type: {type(event).__name__}, Final: {event.is_final_response()}, Content: {event.content}")

      # Key Concept: is_final_response() marks the concluding message for the turn.
      if event.is_final_response():
          if event.content and event.content.parts:
             # Assuming text response in the first part
             final_response_text = event.content.parts[0].text
          elif event.actions and event.actions.escalate: # Handle potential errors/escalations
             final_response_text = f"Agent escalated: {event.error_message or 'No specific message.'}"
          # Add more checks here if needed (e.g., specific error codes)
          break # Stop processing events once the final response is found

  print(f"<<< Agent Response: {final_response_text}")

5. 运行对话

最后,让我们通过向智能体发送一些查询来测试我们的设置。我们将async调用封装在一个主async函数中,并使用await来运行它。

观察输出:

  • 查看用户查询。
  • 注意当智能体使用工具时出现的--- Tool: get_weather called... ---日志。
  • 观察智能体的最终响应,包括它如何处理天气数据不可用的情况(对于巴黎)。
# @title Run the Initial Conversation

# We need an async function to await our interaction helper
async def run_conversation():
    await call_agent_async("What is the weather like in London?")
    await call_agent_async("How about Paris?") # Expecting the tool's error message
    await call_agent_async("Tell me the weather in New York")

# Execute the conversation using await in an async context (like Colab/Jupyter)
await run_conversation()

预期输出:

>>> User Query: What is the weather like in London?

--- Tool: get_weather called for city: London ---
<<< Agent Response: The weather in London is cloudy with a temperature of 15°C.


>>> User Query: How about Paris?

--- Tool: get_weather called for city: Paris ---
<<< Agent Response: Sorry, I don't have weather information for Paris.


>>> User Query: Tell me the weather in New York

--- Tool: get_weather called for city: New York ---
<<< Agent Response: The weather in New York is sunny with a temperature of 25°C.

恭喜!您已成功构建并与您的第一个ADK智能体进行了交互。它能理解用户的请求,使用工具查找信息,并根据工具的结果做出适当的响应。

在下一步中,我们将探讨如何轻松切换支撑该智能体的底层语言模型。

步骤2:使用LiteLLM实现多模型支持

在第一步中,我们构建了一个由特定Gemini模型驱动的功能性天气智能体。虽然有效,但实际应用通常受益于使用不同大型语言模型(LLMs)的灵活性。为什么?

  • 性能: 某些模型在特定任务上表现优异(例如:编程、推理、创意写作)。
  • 成本: 不同模型的价格各不相同。
  • 能力: 模型提供多样化的功能、上下文窗口大小和微调选项。
  • 可用性/冗余性:拥有备选方案可确保即使某个提供商出现问题,您的应用程序仍能正常运行。

ADK通过与LiteLLM库的集成,实现了模型之间的无缝切换。LiteLLM作为统一接口,支持超过100种不同的大型语言模型。

在这一步中,我们将:

  1. 了解如何配置ADK Agent,通过LiteLlm封装器使用来自OpenAI(GPT)和Anthropic(Claude)等提供商的模型。
  2. 定义、配置(使用各自的会话和运行器),并立即测试我们的天气智能体实例,每个实例由不同的LLM支持。
  3. 与这些不同的智能体互动,观察它们在使用相同底层工具时可能产生的响应差异。

1. 导入 LiteLlm

我们在初始设置时已导入此组件(步骤0),但它是支持多模型的关键组件:

# Ensure this import is present from your setup cells
from google.adk.models.lite_llm import LiteLlm

2. 定义和测试多模型智能体

我们不是仅传递一个模型名称字符串(默认使用Google的Gemini模型),而是将所需的模型标识符字符串封装在LiteLlm类中。

  • 关键概念: LiteLlm 封装器: LiteLlm(model="provider/model_name") 语法告诉ADK通过LiteLLM库将该智能体的请求路由到指定的模型提供商。

请确保您已在步骤0中配置好OpenAI和Anthropic所需的API密钥。我们将使用call_agent_async函数(之前定义过,现在接受runneruser_idsession_id参数)在每个智能体设置完成后立即与其进行交互。

以下每个模块将: * 使用特定的LiteLLM模型(MODEL_GPT_4OMODEL_CLAUDE_SONNET)定义智能体。 * 专门为该智能体的测试运行新建独立的InMemorySessionService和会话。这可以保持本次演示中的对话历史相互隔离。 * 创建针对特定智能体及其会话服务配置的Runner。 * 立即调用call_agent_async发送查询并测试智能体。

最佳实践: 使用常量定义模型名称(如步骤0中定义的 MODEL_GPT_4O, MODEL_CLAUDE_SONNET),以避免拼写错误并使代码更易于管理。

错误处理:我们将智能体定义包裹在try...except代码块中。这样当某个提供商的API密钥缺失或无效时,可以防止整个代码单元执行失败,使得教程能够继续使用那些配置好的模型。

首先,让我们使用OpenAI的GPT-4o创建并测试这个智能体。

# @title Define and Test GPT Agent

# Make sure 'get_weather' function from Step 1 is defined in your environment.
# Make sure 'call_agent_async' is defined from earlier.

# --- Agent using GPT-4o ---
weather_agent_gpt = None # Initialize to None
runner_gpt = None      # Initialize runner to None

try:
    weather_agent_gpt = Agent(
        name="weather_agent_gpt",
        # Key change: Wrap the LiteLLM model identifier
        model=LiteLlm(model=MODEL_GPT_4O),
        description="Provides weather information (using GPT-4o).",
        instruction="You are a helpful weather assistant powered by GPT-4o. "
                    "Use the 'get_weather' tool for city weather requests. "
                    "Clearly present successful reports or polite error messages based on the tool's output status.",
        tools=[get_weather], # Re-use the same tool
    )
    print(f"Agent '{weather_agent_gpt.name}' created using model '{MODEL_GPT_4O}'.")

    # InMemorySessionService is simple, non-persistent storage for this tutorial.
    session_service_gpt = InMemorySessionService() # Create a dedicated service

    # Define constants for identifying the interaction context
    APP_NAME_GPT = "weather_tutorial_app_gpt" # Unique app name for this test
    USER_ID_GPT = "user_1_gpt"
    SESSION_ID_GPT = "session_001_gpt" # Using a fixed ID for simplicity

    # Create the specific session where the conversation will happen
    session_gpt = session_service_gpt.create_session(
        app_name=APP_NAME_GPT,
        user_id=USER_ID_GPT,
        session_id=SESSION_ID_GPT
    )
    print(f"Session created: App='{APP_NAME_GPT}', User='{USER_ID_GPT}', Session='{SESSION_ID_GPT}'")

    # Create a runner specific to this agent and its session service
    runner_gpt = Runner(
        agent=weather_agent_gpt,
        app_name=APP_NAME_GPT,       # Use the specific app name
        session_service=session_service_gpt # Use the specific session service
        )
    print(f"Runner created for agent '{runner_gpt.agent.name}'.")

    # --- Test the GPT Agent ---
    print("\n--- Testing GPT Agent ---")
    # Ensure call_agent_async uses the correct runner, user_id, session_id
    await call_agent_async(query = "What's the weather in Tokyo?",
                           runner=runner_gpt,
                           user_id=USER_ID_GPT,
                           session_id=SESSION_ID_GPT)

except Exception as e:
    print(f"❌ Could not create or run GPT agent '{MODEL_GPT_4O}'. Check API Key and model name. Error: {e}")

接下来,我们将对Anthropic的Claude Sonnet执行相同的操作。

# @title Define and Test Claude Agent

# Make sure 'get_weather' function from Step 1 is defined in your environment.
# Make sure 'call_agent_async' is defined from earlier.

# --- Agent using Claude Sonnet ---
weather_agent_claude = None # Initialize to None
runner_claude = None      # Initialize runner to None

try:
    weather_agent_claude = Agent(
        name="weather_agent_claude",
        # Key change: Wrap the LiteLLM model identifier
        model=LiteLlm(model=MODEL_CLAUDE_SONNET),
        description="Provides weather information (using Claude Sonnet).",
        instruction="You are a helpful weather assistant powered by Claude Sonnet. "
                    "Use the 'get_weather' tool for city weather requests. "
                    "Analyze the tool's dictionary output ('status', 'report'/'error_message'). "
                    "Clearly present successful reports or polite error messages.",
        tools=[get_weather], # Re-use the same tool
    )
    print(f"Agent '{weather_agent_claude.name}' created using model '{MODEL_CLAUDE_SONNET}'.")

    # InMemorySessionService is simple, non-persistent storage for this tutorial.
    session_service_claude = InMemorySessionService() # Create a dedicated service

    # Define constants for identifying the interaction context
    APP_NAME_CLAUDE = "weather_tutorial_app_claude" # Unique app name
    USER_ID_CLAUDE = "user_1_claude"
    SESSION_ID_CLAUDE = "session_001_claude" # Using a fixed ID for simplicity

    # Create the specific session where the conversation will happen
    session_claude = session_service_claude.create_session(
        app_name=APP_NAME_CLAUDE,
        user_id=USER_ID_CLAUDE,
        session_id=SESSION_ID_CLAUDE
    )
    print(f"Session created: App='{APP_NAME_CLAUDE}', User='{USER_ID_CLAUDE}', Session='{SESSION_ID_CLAUDE}'")

    # Create a runner specific to this agent and its session service
    runner_claude = Runner(
        agent=weather_agent_claude,
        app_name=APP_NAME_CLAUDE,       # Use the specific app name
        session_service=session_service_claude # Use the specific session service
        )
    print(f"Runner created for agent '{runner_claude.agent.name}'.")

    # --- Test the Claude Agent ---
    print("\n--- Testing Claude Agent ---")
    # Ensure call_agent_async uses the correct runner, user_id, session_id
    await call_agent_async(query = "Weather in London please.",
                           runner=runner_claude,
                           user_id=USER_ID_CLAUDE,
                           session_id=SESSION_ID_CLAUDE)

except Exception as e:
    print(f"❌ Could not create or run Claude agent '{MODEL_CLAUDE_SONNET}'. Check API Key and model name. Error: {e}")

仔细观察两个代码块的输出。你应该看到:

  1. 每个智能体(weather_agent_gptweather_agent_claude)都成功创建(如果API密钥有效)。
  2. 为每个智能体设置专用的会话和运行器。
  3. 每个智能体在处理查询时都能正确识别需要使用get_weather工具的情况(您会看到--- Tool: get_weather called... ---日志)。
  4. 底层工具逻辑保持不变,始终返回我们的模拟数据。
  5. 然而,每个智能体生成的最终文本响应在措辞、语气或格式上可能略有不同。这是因为指令提示由不同的LLM(GPT-4o与Claude Sonnet)进行解释和执行。

这一步展示了ADK + LiteLLM提供的强大功能和灵活性。您可以轻松尝试并使用各种大语言模型部署智能体,同时保持核心应用逻辑(工具、基础智能体结构)的一致性。

在下一步中,我们将超越单个智能体,构建一个小型团队,让智能体之间可以相互委派任务!


步骤3:构建智能体团队 - 问候与告别的委托

在步骤1和步骤2中,我们构建并测试了一个专注于天气查询的单一智能体。虽然它对于特定任务很有效,但实际应用通常需要处理更多样化的用户交互。我们可以继续向这个单一的天气智能体添加更多工具和复杂指令,但这很快就会变得难以管理且效率低下。

一个更稳健的方法是构建一个智能体团队。这包括:

  1. 创建多个专用智能体,每个智能体针对特定功能设计(例如一个用于天气查询,一个用于问候语,一个用于计算)。
  2. 指定一个根智能体(或称协调器)来接收初始用户请求。
  3. 使根智能体能够根据用户意图委托请求给最合适的专业子智能体。

为什么要构建智能体团队?

  • 模块化: 更容易开发、测试和维护单个智能体。
  • 专业化:每个智能体都可以针对其特定任务进行微调(指令、模型选择)。
  • 可扩展性:通过添加新的智能体可以更简单地增加新功能。
  • 效率:允许对简单任务(如问候)使用可能更简单/更便宜的模型。

在这一步中,我们将:

  1. 定义用于处理问候(say_hello)和告别(say_goodbye)的简单工具。
  2. 创建两个新的专用子智能体:greeting_agentfarewell_agent
  3. 将我们的主要天气智能体(weather_agent_v2)更新为根智能体
  4. 配置根智能体及其子智能体,实现自动委派功能。
  5. 通过向根智能体发送不同类型的请求来测试委托流程。

1. 为子智能体定义工具

首先,让我们创建一些简单的Python函数,它们将作为新专业智能体的工具。请记住,清晰的文档字符串对使用这些函数的智能体至关重要。

# @title Define Tools for Greeting and Farewell Agents

# Ensure 'get_weather' from Step 1 is available if running this step independently.
# def get_weather(city: str) -> dict: ... (from Step 1)

def say_hello(name: str = "there") -> str:
    """Provides a simple greeting, optionally addressing the user by name.

    Args:
        name (str, optional): The name of the person to greet. Defaults to "there".

    Returns:
        str: A friendly greeting message.
    """
    print(f"--- Tool: say_hello called with name: {name} ---")
    return f"Hello, {name}!"

def say_goodbye() -> str:
    """Provides a simple farewell message to conclude the conversation."""
    print(f"--- Tool: say_goodbye called ---")
    return "Goodbye! Have a great day."

print("Greeting and Farewell tools defined.")

# Optional self-test
print(say_hello("Alice"))
print(say_goodbye())

2. 定义子智能体(问候与告别)

现在,为我们的专家创建Agent实例。请注意它们高度专注的instruction,更重要的是它们清晰的descriptiondescription根智能体用来决定何时委派给这些子智能体的主要信息。

我们甚至可以为这些子智能体使用不同的LLM!让我们为问候智能体分配GPT-4o,同时让告别智能体继续使用GPT-4o(如果需要且API密钥已设置,您可以轻松将其切换为Claude或Gemini)。

最佳实践:子智能体的description字段应准确简洁地概括其特定能力。这对实现有效的自动委派至关重要。

最佳实践:子智能体的instruction字段应根据其有限范围进行定制,明确告知它们应该做什么以及不应该做什么(例如:"你的唯一任务是...")。

# @title Define Greeting and Farewell Sub-Agents

# Ensure LiteLlm is imported and API keys are set (from Step 0/2)
# from google.adk.models.lite_llm import LiteLlm
# MODEL_GPT_4O, MODEL_CLAUDE_SONNET etc. should be defined

# --- Greeting Agent ---
greeting_agent = None
try:
    greeting_agent = Agent(
        # Using a potentially different/cheaper model for a simple task
        model=LiteLlm(model=MODEL_GPT_4O),
        name="greeting_agent",
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting to the user. "
                    "Use the 'say_hello' tool to generate the greeting. "
                    "If the user provides their name, make sure to pass it to the tool. "
                    "Do not engage in any other conversation or tasks.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.", # Crucial for delegation
        tools=[say_hello],
    )
    print(f"✅ Agent '{greeting_agent.name}' created using model '{MODEL_GPT_4O}'.")
except Exception as e:
    print(f"❌ Could not create Greeting agent. Check API Key ({MODEL_GPT_4O}). Error: {e}")

# --- Farewell Agent ---
farewell_agent = None
try:
    farewell_agent = Agent(
        # Can use the same or a different model
        model=LiteLlm(model=MODEL_GPT_4O), # Sticking with GPT for this example
        name="farewell_agent",
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message. "
                    "Use the 'say_goodbye' tool when the user indicates they are leaving or ending the conversation "
                    "(e.g., using words like 'bye', 'goodbye', 'thanks bye', 'see you'). "
                    "Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.", # Crucial for delegation
        tools=[say_goodbye],
    )
    print(f"✅ Agent '{farewell_agent.name}' created using model '{MODEL_GPT_4O}'.")
except Exception as e:
    print(f"❌ Could not create Farewell agent. Check API Key ({MODEL_GPT_4O}). Error: {e}")

3. 定义包含子智能体的根智能体

现在,我们对weather_agent进行了升级。主要变更包括:

  • 添加sub_agents参数:我们传入一个包含刚创建的greeting_agentfarewell_agent实例的列表。
  • 更新instruction:我们明确告知根智能体关于其子智能体的信息,以及何时应该将任务委派给它们。

核心概念:自动委派(Auto Flow) 通过提供sub_agents列表,ADK实现了自动委派功能。当根智能体接收到用户查询时,其大语言模型不仅会考虑自身的指令和工具,还会评估每个子智能体的description描述。如果模型判定某个查询更符合子智能体描述的能力(例如"处理简单问候"),就会自动生成一个特殊内部动作来移交控制权给该子智能体处理当前轮次。子智能体随后会使用自己的模型、指令和工具来处理该查询。

最佳实践: 确保根智能体的指令能清晰指导其委派决策。按名称提及子智能体,并描述应进行委派的条件。

# @title Define the Root Agent with Sub-Agents

# Ensure sub-agents were created successfully before defining the root agent.
# Also ensure the original 'get_weather' tool is defined.
root_agent = None
runner_root = None # Initialize runner

if greeting_agent and farewell_agent and 'get_weather' in globals():
    # Let's use a capable Gemini model for the root agent to handle orchestration
    root_agent_model = MODEL_GEMINI_2_0_FLASH

    weather_agent_team = Agent(
        name="weather_agent_v2", # Give it a new version name
        model=root_agent_model,
        description="The main coordinator agent. Handles weather requests and delegates greetings/farewells to specialists.",
        instruction="You are the main Weather Agent coordinating a team. Your primary responsibility is to provide weather information. "
                    "Use the 'get_weather' tool ONLY for specific weather requests (e.g., 'weather in London'). "
                    "You have specialized sub-agents: "
                    "1. 'greeting_agent': Handles simple greetings like 'Hi', 'Hello'. Delegate to it for these. "
                    "2. 'farewell_agent': Handles simple farewells like 'Bye', 'See you'. Delegate to it for these. "
                    "Analyze the user's query. If it's a greeting, delegate to 'greeting_agent'. If it's a farewell, delegate to 'farewell_agent'. "
                    "If it's a weather request, handle it yourself using 'get_weather'. "
                    "For anything else, respond appropriately or state you cannot handle it.",
        tools=[get_weather], # Root agent still needs the weather tool for its core task
        # Key change: Link the sub-agents here!
        sub_agents=[greeting_agent, farewell_agent]
    )
    print(f"✅ Root Agent '{weather_agent_team.name}' created using model '{root_agent_model}' with sub-agents: {[sa.name for sa in weather_agent_team.sub_agents]}")

else:
    print("❌ Cannot create root agent because one or more sub-agents failed to initialize or 'get_weather' tool is missing.")
    if not greeting_agent: print(" - Greeting Agent is missing.")
    if not farewell_agent: print(" - Farewell Agent is missing.")
    if 'get_weather' not in globals(): print(" - get_weather function is missing.")

4. 与智能体团队互动

既然我们已经定义了根智能体(weather_agent_team - 注意:确保该变量名称与之前代码块中定义的名称一致,很可能是# @title Define the Root Agent with Sub-Agents,可能将其命名为root_agent)及其专用子智能体,现在让我们测试委托机制。

以下代码块将:

  1. 定义一个async函数run_team_conversation
  2. 在此函数内部,创建一个新的专用 InMemorySessionService以及特定会话(session_001_agent_team),专门用于本次测试运行。这样可以隔离对话历史,以便测试团队动态。
  3. 创建一个配置为使用我们的weather_agent_team(根智能体)和专用会话服务的Runnerrunner_agent_team)。
  4. 使用我们更新的call_agent_async函数向runner_agent_team发送不同类型的查询(问候语、天气请求、告别语)。我们明确为这个特定测试传递了runner、用户ID和会话ID。
  5. 立即执行run_team_conversation函数。

我们预期的工作流程如下:

  1. "Hello there!"查询会发送到runner_agent_team
  2. 根智能体(weather_agent_team)接收该任务,并根据其指令和greeting_agent的描述来委派任务。
  3. greeting_agent 处理查询,调用其 say_hello 工具,并生成响应。
  4. "纽约的天气怎么样?"这个查询不会被委派,而是由根智能体直接使用其get_weather工具处理。
  5. "Thanks, bye!"查询被委托给farewell_agent,该智能体使用其say_goodbye工具。
# @title Interact with the Agent Team

# Ensure the root agent (e.g., 'weather_agent_team' or 'root_agent' from the previous cell) is defined.
# Ensure the call_agent_async function is defined.

# Check if the root agent variable exists before defining the conversation function
root_agent_var_name = 'root_agent' # Default name from Step 3 guide
if 'weather_agent_team' in globals(): # Check if user used this name instead
    root_agent_var_name = 'weather_agent_team'
elif 'root_agent' not in globals():
    print("⚠️ Root agent ('root_agent' or 'weather_agent_team') not found. Cannot define run_team_conversation.")
    # Assign a dummy value to prevent NameError later if the code block runs anyway
    root_agent = None

if root_agent_var_name in globals() and globals()[root_agent_var_name]:
    async def run_team_conversation():
        print("\n--- Testing Agent Team Delegation ---")
        # InMemorySessionService is simple, non-persistent storage for this tutorial.
        session_service = InMemorySessionService()

        # Define constants for identifying the interaction context
        APP_NAME = "weather_tutorial_agent_team"
        USER_ID = "user_1_agent_team"
        SESSION_ID = "session_001_agent_team" # Using a fixed ID for simplicity

        # Create the specific session where the conversation will happen
        session = session_service.create_session(
            app_name=APP_NAME,
            user_id=USER_ID,
            session_id=SESSION_ID
        )
        print(f"Session created: App='{APP_NAME}', User='{USER_ID}', Session='{SESSION_ID}'")

        # --- Get the actual root agent object ---
        # Use the determined variable name
        actual_root_agent = globals()[root_agent_var_name]

        # Create a runner specific to this agent team test
        runner_agent_team = Runner(
            agent=actual_root_agent, # Use the root agent object
            app_name=APP_NAME,       # Use the specific app name
            session_service=session_service # Use the specific session service
            )
        # Corrected print statement to show the actual root agent's name
        print(f"Runner created for agent '{actual_root_agent.name}'.")

        # Always interact via the root agent's runner, passing the correct IDs
        await call_agent_async(query = "Hello there!",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)
        await call_agent_async(query = "What is the weather in New York?",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)
        await call_agent_async(query = "Thanks, bye!",
                               runner=runner_agent_team,
                               user_id=USER_ID,
                               session_id=SESSION_ID)

    # Execute the conversation
    # Note: This may require API keys for the models used by root and sub-agents!
    await run_team_conversation()
else:
    print("\n⚠️ Skipping agent team conversation as the root agent was not successfully defined in the previous step.")

仔细观察输出日志,特别是--- Tool: ... called ---消息。你应该注意到:

  • 对于"Hello there!",调用了say_hello工具(表明由greeting_agent处理了它)。
  • 对于"纽约的天气如何?"这个问题,调用了get_weather工具(表明根智能体处理了该请求)。
  • 对于"Thanks, bye!",调用了say_goodbye工具(表明由farewell_agent处理了该请求)。

这确认了自动委派成功!根智能体在其指令和sub_agentsdescription引导下,正确地将用户请求路由到团队中合适的专业智能体。

现在您已经用多个协作智能体构建了应用程序。这种模块化设计是构建更复杂、更强大智能体系统的基础。下一步,我们将通过会话状态让智能体具备跨轮次记忆信息的能力。

步骤4:利用会话状态添加记忆与个性化功能

到目前为止,我们的智能体团队可以通过委派处理不同的任务,但每次交互都是全新的——智能体不会记住会话中的历史对话或用户偏好。为了创建更复杂且具备上下文感知的体验,智能体需要记忆功能。ADK通过会话状态来实现这一点。

什么是会话状态?

  • 它是一个与特定用户会话绑定的Python字典(session.state),通过APP_NAMEUSER_IDSESSION_ID进行标识。
  • 它在该会话的多次对话轮次中持续保存信息。
  • 智能体和工具可以读取和写入此状态,使它们能够记住细节、调整行为并个性化响应。

智能体如何与状态交互:

  1. ToolContext (主要方法): 工具可以接受一个ToolContext对象(如果声明为最后一个参数,ADK会自动提供)。该对象通过tool_context.state直接访问会话状态,允许工具在执行过程中读取偏好设置或保存结果。
  2. output_key (自动保存智能体响应): 可以配置一个Agent使用output_key="your_key"。ADK会自动将该智能体在当前轮次的最终文本响应保存到session.state["your_key"]中。

在这一步中,我们将通过以下方式增强我们的天气智能体团队:

  1. 使用一个新的InMemorySessionService来演示隔离状态。
  2. 使用用户对temperature_unit的偏好初始化会话状态。
  3. 创建一个状态感知版本的天气工具(get_weather_stateful),该工具通过ToolContext读取此偏好并调整其输出格式(摄氏度/华氏度)。
  4. 更新根智能体以使用这个有状态工具,并通过配置output_key自动将其最终天气报告保存到会话状态。
  5. 运行对话以观察初始状态如何影响工具,手动状态更改如何改变后续行为,以及output_key如何持久化智能体的响应。

1. 初始化新会话服务与状态

为了清晰展示状态管理而不受之前步骤的干扰,我们将实例化一个新的InMemorySessionService。同时我们将创建一个会话,其初始状态会定义用户偏好的温度单位。

# @title 1. Initialize New Session Service and State

# Import necessary session components
from google.adk.sessions import InMemorySessionService

# Create a NEW session service instance for this state demonstration
session_service_stateful = InMemorySessionService()
print("✅ New InMemorySessionService created for state demonstration.")

# Define a NEW session ID for this part of the tutorial
SESSION_ID_STATEFUL = "session_state_demo_001"
USER_ID_STATEFUL = "user_state_demo"

# Define initial state data - user prefers Celsius initially
initial_state = {
    "user_preference_temperature_unit": "Celsius"
}

# Create the session, providing the initial state
session_stateful = session_service_stateful.create_session(
    app_name=APP_NAME, # Use the consistent app name
    user_id=USER_ID_STATEFUL,
    session_id=SESSION_ID_STATEFUL,
    state=initial_state # <<< Initialize state during creation
)
print(f"✅ Session '{SESSION_ID_STATEFUL}' created for user '{USER_ID_STATEFUL}'.")

# Verify the initial state was set correctly
retrieved_session = session_service_stateful.get_session(app_name=APP_NAME,
                                                         user_id=USER_ID_STATEFUL,
                                                         session_id = SESSION_ID_STATEFUL)
print("\n--- Initial Session State ---")
if retrieved_session:
    print(retrieved_session.state)
else:
    print("Error: Could not retrieve session.")

2. 创建状态感知天气工具

现在,我们创建一个新版本的天气工具。它的关键特性是接受tool_context: ToolContext,这使它能够访问tool_context.state。它将读取user_preference_temperature_unit并相应地格式化温度。

关键概念:ToolContext 该对象是桥梁,允许您的工具逻辑与会话上下文交互,包括读取和写入状态变量。如果将其定义为工具函数的最后一个参数,ADK会自动注入它。

最佳实践: 从状态中读取数据时,使用dictionary.get('key', default_value)来处理键可能尚未存在的情况,确保您的工具不会崩溃。

# @title 2. Create State-Aware Weather Tool
from google.adk.tools.tool_context import ToolContext

def get_weather_stateful(city: str, tool_context: ToolContext) -> dict:
    """Retrieves weather, converts temp unit based on session state."""
    print(f"--- Tool: get_weather_stateful called for {city} ---")

    # --- Read preference from state ---
    preferred_unit = tool_context.state.get("user_preference_temperature_unit", "Celsius") # Default to Celsius
    print(f"--- Tool: Reading state 'user_preference_temperature_unit': {preferred_unit} ---")

    city_normalized = city.lower().replace(" ", "")

    # Mock weather data (always stored in Celsius internally)
    mock_weather_db = {
        "newyork": {"temp_c": 25, "condition": "sunny"},
        "london": {"temp_c": 15, "condition": "cloudy"},
        "tokyo": {"temp_c": 18, "condition": "light rain"},
    }

    if city_normalized in mock_weather_db:
        data = mock_weather_db[city_normalized]
        temp_c = data["temp_c"]
        condition = data["condition"]

        # Format temperature based on state preference
        if preferred_unit == "Fahrenheit":
            temp_value = (temp_c * 9/5) + 32 # Calculate Fahrenheit
            temp_unit = "°F"
        else: # Default to Celsius
            temp_value = temp_c
            temp_unit = "°C"

        report = f"The weather in {city.capitalize()} is {condition} with a temperature of {temp_value:.0f}{temp_unit}."
        result = {"status": "success", "report": report}
        print(f"--- Tool: Generated report in {preferred_unit}. Result: {result} ---")

        # Example of writing back to state (optional for this tool)
        tool_context.state["last_city_checked_stateful"] = city
        print(f"--- Tool: Updated state 'last_city_checked_stateful': {city} ---")

        return result
    else:
        # Handle city not found
        error_msg = f"Sorry, I don't have weather information for '{city}'."
        print(f"--- Tool: City '{city}' not found. ---")
        return {"status": "error", "error_message": error_msg}

print("✅ State-aware 'get_weather_stateful' tool defined.")

3. 重新定义子智能体并更新根智能体

为确保这一步自包含且能正确构建,我们首先按照步骤3中的定义重新定义greeting_agentfarewell_agent。然后定义新的根智能体(weather_agent_v4_stateful):

  • 它使用了新的get_weather_stateful工具。
  • 它包含用于委派的问候和告别子智能体。
  • 关键的是,它设置了output_key="last_weather_report",这会自动将其最终的天气响应保存到会话状态中。
# @title 3. Redefine Sub-Agents and Update Root Agent with output_key

# Ensure necessary imports: Agent, LiteLlm, Runner
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.runners import Runner
# Ensure tools 'say_hello', 'say_goodbye' are defined (from Step 3)
# Ensure model constants MODEL_GPT_4O, MODEL_GEMINI_2_5_PRO etc. are defined

# --- Redefine Greeting Agent (from Step 3) ---
greeting_agent = None
try:
    greeting_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="greeting_agent",
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Error: {e}")

# --- Redefine Farewell Agent (from Step 3) ---
farewell_agent = None
try:
    farewell_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="farewell_agent",
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Error: {e}")

# --- Define the Updated Root Agent ---
root_agent_stateful = None
runner_root_stateful = None # Initialize runner

# Check prerequisites before creating the root agent
if greeting_agent and farewell_agent and 'get_weather_stateful' in globals():

    root_agent_model = MODEL_GEMINI_2_0_FLASH # Choose orchestration model

    root_agent_stateful = Agent(
        name="weather_agent_v4_stateful", # New version name
        model=root_agent_model,
        description="Main agent: Provides weather (state-aware unit), delegates greetings/farewells, saves report to state.",
        instruction="You are the main Weather Agent. Your job is to provide weather using 'get_weather_stateful'. "
                    "The tool will format the temperature based on user preference stored in state. "
                    "Delegate simple greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather requests, greetings, and farewells.",
        tools=[get_weather_stateful], # Use the state-aware tool
        sub_agents=[greeting_agent, farewell_agent], # Include sub-agents
        output_key="last_weather_report" # <<< Auto-save agent's final weather response
    )
    print(f"✅ Root Agent '{root_agent_stateful.name}' created using stateful tool and output_key.")

    # --- Create Runner for this Root Agent & NEW Session Service ---
    runner_root_stateful = Runner(
        agent=root_agent_stateful,
        app_name=APP_NAME,
        session_service=session_service_stateful # Use the NEW stateful session service
    )
    print(f"✅ Runner created for stateful root agent '{runner_root_stateful.agent.name}' using stateful session service.")

else:
    print("❌ Cannot create stateful root agent. Prerequisites missing.")
    if not greeting_agent: print(" - greeting_agent definition missing.")
    if not farewell_agent: print(" - farewell_agent definition missing.")
    if 'get_weather_stateful' not in globals(): print(" - get_weather_stateful tool missing.")

4. 交互与测试状态流

现在,让我们执行一个对话,用于测试状态交互,使用runner_root_stateful(关联到我们的有状态智能体和session_service_stateful)。我们将使用之前定义的call_agent_async函数,确保传入正确的runner、用户ID(USER_ID_STATEFUL)和会话ID(SESSION_ID_STATEFUL)。

对话流程如下:

  1. 检查天气(伦敦): get_weather_stateful 工具应从第1节初始化的会话状态中读取初始的"摄氏度"偏好设置。根智能体的最终响应(以摄氏度显示的天气报告)应通过output_key配置保存到state['last_weather_report']中。
  2. Manually update state: We will directly modify the state stored within the InMemorySessionService instance (session_service_stateful).
    • 为什么直接修改? session_service.get_session() 方法返回的是会话的一个副本。修改该副本不会影响后续智能体运行中使用的状态。在这个使用InMemorySessionService的测试场景中,我们访问内部的sessions字典来将user_preference_temperature_unit实际存储状态值改为"Fahrenheit"。注意:在实际应用中,状态变更通常由工具或智能体逻辑返回EventActions(state_delta=...)触发,而非直接手动更新。
  3. 再次查询天气(纽约): get_weather_stateful 工具现在应该从状态中读取更新后的"华氏度"偏好并相应转换温度。由于output_key的设置,根智能体的响应(华氏温度显示的天气)将覆盖state['last_weather_report']中的先前值。
  4. 问候智能体:验证在有状态操作的同时,委托给greeting_agent的功能是否仍然正常工作。在这个特定序列中,此交互将成为output_key保存的最后一个响应。
  5. 检查最终状态: 对话结束后,我们最后一次检索会话(获取副本)并打印其状态,以确认user_preference_temperature_unit确实为"Fahrenheit",观察由output_key保存的最终值(本次运行中将是问候语),并查看工具写入的last_city_checked_stateful值。
# Ensure the stateful runner (runner_root_stateful) is available from the previous cell
# Ensure call_agent_async, USER_ID_STATEFUL, SESSION_ID_STATEFUL, APP_NAME are defined

if 'runner_root_stateful' in globals() and runner_root_stateful:
  async def run_stateful_conversation():
      print("\n--- Testing State: Temp Unit Conversion & output_key ---")

      # 1. Check weather (Uses initial state: Celsius)
      print("--- Turn 1: Requesting weather in London (expect Celsius) ---")
      await call_agent_async(query= "What's the weather in London?",
                             runner=runner_root_stateful,
                             user_id=USER_ID_STATEFUL,
                             session_id=SESSION_ID_STATEFUL
                            )

      # 2. Manually update state preference to Fahrenheit - DIRECTLY MODIFY STORAGE
      print("\n--- Manually Updating State: Setting unit to Fahrenheit ---")
      try:
          # Access the internal storage directly - THIS IS SPECIFIC TO InMemorySessionService for testing
          stored_session = session_service_stateful.sessions[APP_NAME][USER_ID_STATEFUL][SESSION_ID_STATEFUL]
          stored_session.state["user_preference_temperature_unit"] = "Fahrenheit"
          # Optional: You might want to update the timestamp as well if any logic depends on it
          # import time
          # stored_session.last_update_time = time.time()
          print(f"--- Stored session state updated. Current 'user_preference_temperature_unit': {stored_session.state['user_preference_temperature_unit']} ---")
      except KeyError:
          print(f"--- Error: Could not retrieve session '{SESSION_ID_STATEFUL}' from internal storage for user '{USER_ID_STATEFUL}' in app '{APP_NAME}' to update state. Check IDs and if session was created. ---")
      except Exception as e:
           print(f"--- Error updating internal session state: {e} ---")

      # 3. Check weather again (Tool should now use Fahrenheit)
      # This will also update 'last_weather_report' via output_key
      print("\n--- Turn 2: Requesting weather in New York (expect Fahrenheit) ---")
      await call_agent_async(query= "Tell me the weather in New York.",
                             runner=runner_root_stateful,
                             user_id=USER_ID_STATEFUL,
                             session_id=SESSION_ID_STATEFUL
                            )

      # 4. Test basic delegation (should still work)
      # This will update 'last_weather_report' again, overwriting the NY weather report
      print("\n--- Turn 3: Sending a greeting ---")
      await call_agent_async(query= "Hi!",
                             runner=runner_root_stateful,
                             user_id=USER_ID_STATEFUL,
                             session_id=SESSION_ID_STATEFUL
                            )

  # Execute the conversation
  await run_stateful_conversation()

  # Inspect final session state after the conversation
  print("\n--- Inspecting Final Session State ---")
  final_session = session_service_stateful.get_session(app_name=APP_NAME,
                                                       user_id= USER_ID_STATEFUL,
                                                       session_id=SESSION_ID_STATEFUL)
  if final_session:
      print(f"Final Preference: {final_session.state.get('user_preference_temperature_unit')}")
      print(f"Final Last Weather Report (from output_key): {final_session.state.get('last_weather_report')}")
      print(f"Final Last City Checked (by tool): {final_session.state.get('last_city_checked_stateful')}")
      # Print full state for detailed view
      # print(f"Full State: {final_session.state}")
  else:
      print("\n❌ Error: Could not retrieve final session state.")

else:
  print("\n⚠️ Skipping state test conversation. Stateful root agent runner ('runner_root_stateful') is not available.")

通过查看对话流程和最终的会话状态打印输出,您可以确认:

  • 状态读取:天气工具(get_weather_stateful)正确地从状态中读取了user_preference_temperature_unit,初始为伦敦使用"摄氏度"。
  • 状态更新:直接修改成功将存储的偏好更改为"Fahrenheit"。
  • 状态读取(已更新): 当询问纽约天气时,该工具随后读取了"Fahrenheit"并执行了转换。
  • 工具状态写入: 该工具成功通过tool_context.statelast_city_checked_stateful(在第二次天气检查后为"New York")写入状态。
  • 委托:即使在状态修改后,对greeting_agent的"Hi!"功能委托仍能正确执行。
  • output_key: output_key="last_weather_report" 成功保存了根智能体每次轮次中的最终响应,当根智能体是最终响应者时。在这个序列中,最后的响应是问候语("Hello, there!"),因此它覆盖了状态键中的天气报告。
  • 最终状态: 最终检查确认偏好设置已持久化为"Fahrenheit"。

您已成功通过ToolContext集成会话状态来实现智能体行为个性化,手动操作InMemorySessionService进行状态测试,并观察到output_key如何提供将智能体最后响应保存至状态的简单机制。掌握这些状态管理的基础知识至关重要,这将为我们下一步通过回调函数实现安全防护机制奠定基础。


步骤5:添加安全性 - 使用before_model_callback设置输入防护栏

我们的智能体团队正变得越来越强大,能够记住用户偏好并有效使用工具。然而在现实场景中,我们通常需要安全机制来预先控制智能体的行为,甚至在潜在问题请求到达核心大语言模型(LLM)之前就进行干预。

ADK提供回调函数——这些函数允许您接入智能体执行生命周期的特定节点。其中before_model_callback对于输入安全性特别有用。

什么是 before_model_callback

  • 这是您定义的一个Python函数,ADK会在智能体发送其编译的请求(包括对话历史、指令和最新的用户消息)到底层LLM之前执行。
  • 目的:检查请求,根据预定义规则进行必要的修改或完全阻止。

常见应用场景:

  • 输入验证/过滤:检查用户输入是否符合标准或包含不允许的内容(如个人身份信息或关键词)。
  • 防护栏: 防止有害、偏离主题或违反政策的请求被LLM处理。
  • 动态提示修改:在发送请求前,及时将信息(例如来自会话状态)添加到LLM请求上下文中。

工作原理:

  1. 定义一个函数,接受 callback_context: CallbackContextllm_request: LlmRequest 作为参数。
  2. callback_context: 提供对智能体信息、会话状态(callback_context.state)等的访问。
  3. llm_request: 包含发送给LLM的完整载荷(contents内容, config配置)。
  4. 在函数内部:
  5. 检查: 检查 llm_request.contents (特别是最后一条用户消息)。
  6. 修改(谨慎操作): 您可以修改 llm_request 的部分内容。
  7. 拦截(防护栏): 返回一个LlmResponse对象。ADK会立即将此响应返回,跳过该轮次的LLM调用。
  8. 允许: 返回 None。ADK 将继续使用(可能修改过的)请求调用 LLM。

在这一步中,我们将:

  1. 定义一个before_model_callback函数(block_keyword_guardrail),用于检查用户输入中是否包含特定关键词("BLOCK")。
  2. 更新我们的有状态根智能体(来自步骤4的weather_agent_v4_stateful)以使用此回调。
  3. 创建一个与此更新后的智能体关联的新运行器,但使用相同的状态化会话服务以保持状态连续性。
  4. 通过发送正常和包含关键词的请求来测试防护栏。

1. 定义护栏回调函数

该函数将检查llm_request内容中的最后一条用户消息。如果发现"BLOCK"(不区分大小写),它会构建并返回一个LlmResponse来阻止流程;否则返回None

# @title 1. Define the before_model_callback Guardrail

# Ensure necessary imports are available
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types # For creating response content
from typing import Optional

def block_keyword_guardrail(
    callback_context: CallbackContext, llm_request: LlmRequest
) -> Optional[LlmResponse]:
    """
    Inspects the latest user message for 'BLOCK'. If found, blocks the LLM call
    and returns a predefined LlmResponse. Otherwise, returns None to proceed.
    """
    agent_name = callback_context.agent_name # Get the name of the agent whose model call is being intercepted
    print(f"--- Callback: block_keyword_guardrail running for agent: {agent_name} ---")

    # Extract the text from the latest user message in the request history
    last_user_message_text = ""
    if llm_request.contents:
        # Find the most recent message with role 'user'
        for content in reversed(llm_request.contents):
            if content.role == 'user' and content.parts:
                # Assuming text is in the first part for simplicity
                if content.parts[0].text:
                    last_user_message_text = content.parts[0].text
                    break # Found the last user message text

    print(f"--- Callback: Inspecting last user message: '{last_user_message_text[:100]}...' ---") # Log first 100 chars

    # --- Guardrail Logic ---
    keyword_to_block = "BLOCK"
    if keyword_to_block in last_user_message_text.upper(): # Case-insensitive check
        print(f"--- Callback: Found '{keyword_to_block}'. Blocking LLM call! ---")
        # Optionally, set a flag in state to record the block event
        callback_context.state["guardrail_block_keyword_triggered"] = True
        print(f"--- Callback: Set state 'guardrail_block_keyword_triggered': True ---")

        # Construct and return an LlmResponse to stop the flow and send this back instead
        return LlmResponse(
            content=types.Content(
                role="model", # Mimic a response from the agent's perspective
                parts=[types.Part(text=f"I cannot process this request because it contains the blocked keyword '{keyword_to_block}'.")],
            )
            # Note: You could also set an error_message field here if needed
        )
    else:
        # Keyword not found, allow the request to proceed to the LLM
        print(f"--- Callback: Keyword not found. Allowing LLM call for {agent_name}. ---")
        return None # Returning None signals ADK to continue normally

print("✅ block_keyword_guardrail function defined.")

2. 更新根智能体以使用回调

我们重新定义了根智能体,添加了before_model_callback参数并将其指向我们的新护栏函数。为了清晰起见,我们会给它一个新的版本名称。

重要提示: 如果子智能体(greeting_agentfarewell_agent)和有状态工具(get_weather_stateful)在前面的步骤中尚未定义,我们需要在当前上下文中重新定义它们,确保根智能体定义能够访问其所有组件。

# @title 2. Update Root Agent with before_model_callback


# --- Redefine Sub-Agents (Ensures they exist in this context) ---
greeting_agent = None
try:
    # Use a defined model constant
    greeting_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="greeting_agent", # Keep original name for consistency
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Sub-Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Check Model/API Key ({MODEL_GPT_4O}). Error: {e}")

farewell_agent = None
try:
    # Use a defined model constant
    farewell_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="farewell_agent", # Keep original name
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Sub-Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Check Model/API Key ({MODEL_GPT_4O}). Error: {e}")


# --- Define the Root Agent with the Callback ---
root_agent_model_guardrail = None
runner_root_model_guardrail = None

# Check all components before proceeding
if greeting_agent and farewell_agent and 'get_weather_stateful' in globals() and 'block_keyword_guardrail' in globals():

    # Use a defined model constant like MODEL_GEMINI_2_5_PRO
    root_agent_model = MODEL_GEMINI_2_0_FLASH

    root_agent_model_guardrail = Agent(
        name="weather_agent_v5_model_guardrail", # New version name for clarity
        model=root_agent_model,
        description="Main agent: Handles weather, delegates greetings/farewells, includes input keyword guardrail.",
        instruction="You are the main Weather Agent. Provide weather using 'get_weather_stateful'. "
                    "Delegate simple greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather requests, greetings, and farewells.",
        tools=[get_weather],
        sub_agents=[greeting_agent, farewell_agent], # Reference the redefined sub-agents
        output_key="last_weather_report", # Keep output_key from Step 4
        before_model_callback=block_keyword_guardrail # <<< Assign the guardrail callback
    )
    print(f"✅ Root Agent '{root_agent_model_guardrail.name}' created with before_model_callback.")

    # --- Create Runner for this Agent, Using SAME Stateful Session Service ---
    # Ensure session_service_stateful exists from Step 4
    if 'session_service_stateful' in globals():
        runner_root_model_guardrail = Runner(
            agent=root_agent_model_guardrail,
            app_name=APP_NAME, # Use consistent APP_NAME
            session_service=session_service_stateful # <<< Use the service from Step 4
        )
        print(f"✅ Runner created for guardrail agent '{runner_root_model_guardrail.agent.name}', using stateful session service.")
    else:
        print("❌ Cannot create runner. 'session_service_stateful' from Step 4 is missing.")

else:
    print("❌ Cannot create root agent with model guardrail. One or more prerequisites are missing or failed initialization:")
    if not greeting_agent: print("   - Greeting Agent")
    if not farewell_agent: print("   - Farewell Agent")
    if 'get_weather_stateful' not in globals(): print("   - 'get_weather_stateful' tool")
    if 'block_keyword_guardrail' not in globals(): print("   - 'block_keyword_guardrail' callback")

3. 交互测试防护栏

让我们测试防护栏的行为。我们将使用相同的会话(SESSION_ID_STATEFUL)作为第4步中的会话,以展示状态在这些变更中持续存在。

  1. 发送一个常规天气请求(应通过护栏检查并执行)。
  2. 发送一个包含"BLOCK"的请求(应该被回调拦截)。
  3. 发送问候语(应通过根智能体的护栏检查,被委派并正常执行)。
# @title 3. Interact to Test the Model Input Guardrail

# Ensure the runner for the guardrail agent is available
if runner_root_model_guardrail:
  async def run_guardrail_test_conversation():
      print("\n--- Testing Model Input Guardrail ---")

      # Use the runner for the agent with the callback and the existing stateful session ID
      interaction_func = lambda query: call_agent_async(query,
      runner_root_model_guardrail, USER_ID_STATEFUL, SESSION_ID_STATEFUL # <-- Pass correct IDs
  )
      # 1. Normal request (Callback allows, should use Fahrenheit from Step 4 state change)
      await interaction_func("What is the weather in London?")

      # 2. Request containing the blocked keyword
      await interaction_func("BLOCK the request for weather in Tokyo")

      # 3. Normal greeting (Callback allows root agent, delegation happens)
      await interaction_func("Hello again")


  # Execute the conversation
  await run_guardrail_test_conversation()

  # Optional: Check state for the trigger flag set by the callback
  final_session = session_service_stateful.get_session(app_name=APP_NAME,
                                                       user_id=USER_ID_STATEFUL,
                                                       session_id=SESSION_ID_STATEFUL)
  if final_session:
      print("\n--- Final Session State (After Guardrail Test) ---")
      print(f"Guardrail Triggered Flag: {final_session.state.get('guardrail_block_keyword_triggered')}")
      print(f"Last Weather Report: {final_session.state.get('last_weather_report')}") # Should be London weather
      print(f"Temperature Unit: {final_session.state.get('user_preference_temperature_unit')}") # Should be Fahrenheit
  else:
      print("\n❌ Error: Could not retrieve final session state.")

else:
  print("\n⚠️ Skipping model guardrail test. Runner ('runner_root_model_guardrail') is not available.")

观察执行流程:

  1. 伦敦天气: 回调函数为weather_agent_v5_model_guardrail执行,检查消息内容,打印"未找到关键词。允许LLM调用。"并返回None。智能体继续运行,调用get_weather_stateful工具(该工具使用了第4步状态变更中的"华氏度"偏好设置),然后返回天气信息。该响应通过output_key更新了last_weather_report
  2. BLOCK请求: 回调函数会再次运行weather_agent_v5_model_guardrail,检查消息内容,发现"BLOCK"标记,打印"正在阻止LLM调用!",设置状态标志,并返回预定义的LlmResponse。在这个回合中,智能体的底层LLM完全不会被调用。用户将看到回调函数的阻止消息。
  3. 再次问候: 回调函数为weather_agent_v5_model_guardrail运行,允许该请求。根智能体随后委托给greeting_agent注意:根智能体上定义的before_model_callback不会自动应用于子智能体。greeting_agent正常执行,调用其say_hello工具,并返回问候语。

您已成功实现了一个输入安全层!before_model_callback提供了一个强大的机制,可以在进行昂贵或潜在风险的LLM调用之前强制执行规则并控制智能体行为。接下来,我们将应用类似的概念来为工具使用本身添加防护措施。


步骤6:添加安全性 - 工具参数防护栏 (before_tool_callback)

在第5步中,我们添加了一道防护栏,用于在用户输入到达LLM之前进行检查并可能拦截。现在,我们将在LLM决定使用工具之后、但该工具实际执行之前,再添加一层控制。这对于验证LLM想要传递给工具的参数非常有用。

ADK提供了before_tool_callback专门用于此目的。

什么是 before_tool_callback

  • 这是一个Python函数,在特定工具函数运行之前执行,此时LLM已请求使用该工具并确定了参数。
  • 目的:验证工具参数,根据特定输入阻止工具执行,动态修改参数或强制执行资源使用策略。

常见应用场景:

  • 参数验证:检查由LLM提供的参数是否有效、在允许范围内或符合预期格式。
  • 资源保护:防止工具被调用时输入可能造成高成本、访问受限数据或引发不必要副作用的内容(例如,阻止针对某些参数的API调用)。
  • 动态参数调整:在工具运行前,根据会话状态或其他上下文信息调整参数。

工作原理:

  1. 定义一个函数,接受 tool: BaseToolargs: Dict[str, Any]tool_context: ToolContext 作为参数。
  2. tool: 即将调用的工具对象(可检查tool.name)。
  3. args: LLM为工具生成的参数字典。
  4. tool_context: 提供对会话状态(tool_context.state)、智能体信息等的访问。
  5. 在函数内部:
  6. 检查: 检查 tool.nameargs 字典。
  7. 修改: 直接在args字典中更改值。如果返回None,工具将使用这些修改后的参数运行。
  8. 拦截/覆盖(防护栏): 返回一个字典。ADK会将该字典视为工具调用的结果,完全跳过原始工具函数的执行。理想情况下,该字典应与被拦截工具的预期返回格式相匹配。
  9. 允许:返回None。ADK将继续使用(可能修改过的)参数执行实际工具函数。

在这一步中,我们将:

  1. 定义一个before_tool_callback函数(block_paris_tool_guardrail),专门检查是否调用了城市参数为"Paris"的get_weather_stateful工具。
  2. 如果检测到"Paris",回调函数将阻止工具并返回自定义错误字典。
  3. 更新我们的根智能体 (weather_agent_v6_tool_guardrail) 以包含同时包含 before_model_callback 和这个新的 before_tool_callback
  4. 为此智能体创建一个新的运行器,使用相同的状态会话服务。
  5. 通过请求允许的城市和被阻止的城市("巴黎")的天气来测试流程。

1. 定义工具防护栏回调函数

此函数针对get_weather_stateful工具。它会检查city参数。如果参数值为"Paris",则返回一个与该工具自身错误响应类似的错误字典。否则,通过返回None允许工具继续运行。

# @title 1. Define the before_tool_callback Guardrail

# Ensure necessary imports are available
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.tool_context import ToolContext
from typing import Optional, Dict, Any # For type hints

def block_paris_tool_guardrail(
    tool: BaseTool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict]:
    """
    Checks if 'get_weather_stateful' is called for 'Paris'.
    If so, blocks the tool execution and returns a specific error dictionary.
    Otherwise, allows the tool call to proceed by returning None.
    """
    tool_name = tool.name
    agent_name = tool_context.agent_name # Agent attempting the tool call
    print(f"--- Callback: block_paris_tool_guardrail running for tool '{tool_name}' in agent '{agent_name}' ---")
    print(f"--- Callback: Inspecting args: {args} ---")

    # --- Guardrail Logic ---
    target_tool_name = "get_weather_stateful" # Match the function name used by FunctionTool
    blocked_city = "paris"

    # Check if it's the correct tool and the city argument matches the blocked city
    if tool_name == target_tool_name:
        city_argument = args.get("city", "") # Safely get the 'city' argument
        if city_argument and city_argument.lower() == blocked_city:
            print(f"--- Callback: Detected blocked city '{city_argument}'. Blocking tool execution! ---")
            # Optionally update state
            tool_context.state["guardrail_tool_block_triggered"] = True
            print(f"--- Callback: Set state 'guardrail_tool_block_triggered': True ---")

            # Return a dictionary matching the tool's expected output format for errors
            # This dictionary becomes the tool's result, skipping the actual tool run.
            return {
                "status": "error",
                "error_message": f"Policy restriction: Weather checks for '{city_argument.capitalize()}' are currently disabled by a tool guardrail."
            }
        else:
             print(f"--- Callback: City '{city_argument}' is allowed for tool '{tool_name}'. ---")
    else:
        print(f"--- Callback: Tool '{tool_name}' is not the target tool. Allowing. ---")


    # If the checks above didn't return a dictionary, allow the tool to execute
    print(f"--- Callback: Allowing tool '{tool_name}' to proceed. ---")
    return None # Returning None allows the actual tool function to run

print("✅ block_paris_tool_guardrail function defined.")

2. 更新根智能体以同时使用两种回调

我们再次重新定义根智能体(weather_agent_v6_tool_guardrail),这次在步骤5的before_model_callback基础上增加了before_tool_callback参数。

独立执行说明: 类似于步骤5,在定义此智能体之前,请确保所有先决条件(子智能体、工具、before_model_callback)已在执行上下文中定义或可用。

# @title 2. Update Root Agent with BOTH Callbacks (Self-Contained)

# --- Ensure Prerequisites are Defined ---
# (Include or ensure execution of definitions for: Agent, LiteLlm, Runner, ToolContext,
#  MODEL constants, say_hello, say_goodbye, greeting_agent, farewell_agent,
#  get_weather_stateful, block_keyword_guardrail, block_paris_tool_guardrail)

# --- Redefine Sub-Agents (Ensures they exist in this context) ---
greeting_agent = None
try:
    # Use a defined model constant like MODEL_GPT_4O
    greeting_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="greeting_agent", # Keep original name for consistency
        instruction="You are the Greeting Agent. Your ONLY task is to provide a friendly greeting using the 'say_hello' tool. Do nothing else.",
        description="Handles simple greetings and hellos using the 'say_hello' tool.",
        tools=[say_hello],
    )
    print(f"✅ Sub-Agent '{greeting_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Greeting agent. Check Model/API Key ({MODEL_GPT_4O}). Error: {e}")

farewell_agent = None
try:
    # Use a defined model constant like MODEL_GPT_4O
    farewell_agent = Agent(
        model=MODEL_GEMINI_2_0_FLASH,
        name="farewell_agent", # Keep original name
        instruction="You are the Farewell Agent. Your ONLY task is to provide a polite goodbye message using the 'say_goodbye' tool. Do not perform any other actions.",
        description="Handles simple farewells and goodbyes using the 'say_goodbye' tool.",
        tools=[say_goodbye],
    )
    print(f"✅ Sub-Agent '{farewell_agent.name}' redefined.")
except Exception as e:
    print(f"❌ Could not redefine Farewell agent. Check Model/API Key ({MODEL_GPT_4O}). Error: {e}")

# --- Define the Root Agent with Both Callbacks ---
root_agent_tool_guardrail = None
runner_root_tool_guardrail = None

if ('greeting_agent' in globals() and greeting_agent and
    'farewell_agent' in globals() and farewell_agent and
    'get_weather_stateful' in globals() and
    'block_keyword_guardrail' in globals() and
    'block_paris_tool_guardrail' in globals()):

    root_agent_model = MODEL_GEMINI_2_0_FLASH

    root_agent_tool_guardrail = Agent(
        name="weather_agent_v6_tool_guardrail", # New version name
        model=root_agent_model,
        description="Main agent: Handles weather, delegates, includes input AND tool guardrails.",
        instruction="You are the main Weather Agent. Provide weather using 'get_weather_stateful'. "
                    "Delegate greetings to 'greeting_agent' and farewells to 'farewell_agent'. "
                    "Handle only weather, greetings, and farewells.",
        tools=[get_weather_stateful],
        sub_agents=[greeting_agent, farewell_agent],
        output_key="last_weather_report",
        before_model_callback=block_keyword_guardrail, # Keep model guardrail
        before_tool_callback=block_paris_tool_guardrail # <<< Add tool guardrail
    )
    print(f"✅ Root Agent '{root_agent_tool_guardrail.name}' created with BOTH callbacks.")

    # --- Create Runner, Using SAME Stateful Session Service ---
    if 'session_service_stateful' in globals():
        runner_root_tool_guardrail = Runner(
            agent=root_agent_tool_guardrail,
            app_name=APP_NAME,
            session_service=session_service_stateful # <<< Use the service from Step 4/5
        )
        print(f"✅ Runner created for tool guardrail agent '{runner_root_tool_guardrail.agent.name}', using stateful session service.")
    else:
        print("❌ Cannot create runner. 'session_service_stateful' from Step 4/5 is missing.")

else:
    print("❌ Cannot create root agent with tool guardrail. Prerequisites missing.")

3. 交互测试工具防护栏

让我们再次测试交互流程,继续使用之前步骤中的同一个有状态会话(SESSION_ID_STATEFUL)。

  1. 请求"纽约"的天气:通过两个回调,工具执行(使用状态中的华氏温度偏好)。
  2. 请求"巴黎"的天气:通过before_model_callback。LLM决定调用get_weather_stateful(city='Paris')before_tool_callback拦截,阻止工具调用,并返回错误字典。智能体转发此错误。
  3. 请求"伦敦"的天气:通过两个回调,工具正常执行。
# @title 3. Interact to Test the Tool Argument Guardrail

# Ensure the runner for the tool guardrail agent is available
if runner_root_tool_guardrail:
  async def run_tool_guardrail_test():
      print("\n--- Testing Tool Argument Guardrail ('Paris' blocked) ---")

        # Use the runner for the agent with both callbacks and the existing stateful session
      interaction_func = lambda query: call_agent_async(query,
      runner_root_tool_guardrail, USER_ID_STATEFUL, SESSION_ID_STATEFUL
  )
      # 1. Allowed city (Should pass both callbacks, use Fahrenheit state)
      await interaction_func("What's the weather in New York?")

      # 2. Blocked city (Should pass model callback, but be blocked by tool callback)
      await interaction_func("How about Paris?")

      # 3. Another allowed city (Should work normally again)
      await interaction_func("Tell me the weather in London.")

  # Execute the conversation
  await run_tool_guardrail_test()

  # Optional: Check state for the tool block trigger flag
  final_session = session_service_stateful.get_session(app_name=APP_NAME,
                                                       user_id=USER_ID_STATEFUL,
                                                       session_id= SESSION_ID_STATEFUL)
  if final_session:
      print("\n--- Final Session State (After Tool Guardrail Test) ---")
      print(f"Tool Guardrail Triggered Flag: {final_session.state.get('guardrail_tool_block_triggered')}")
      print(f"Last Weather Report: {final_session.state.get('last_weather_report')}") # Should be London weather
      print(f"Temperature Unit: {final_session.state.get('user_preference_temperature_unit')}") # Should be Fahrenheit
  else:
      print("\n❌ Error: Could not retrieve final session state.")

else:
  print("\n⚠️ Skipping tool guardrail test. Runner ('runner_root_tool_guardrail') is not available.")

(运行上面的代码单元格以生成输出。将输出单元格保留在此处的markdown中)


分析输出结果:

  1. 纽约: before_model_callback 允许该请求。大语言模型请求 get_weather_statefulbefore_tool_callback 运行,检查参数 ({'city': 'New York'}),发现不是"巴黎",打印"允许工具..."并返回 None。实际的 get_weather_stateful 函数执行,从状态中读取"华氏度",并返回天气报告。智能体转发此信息,并通过 output_key 保存。
  2. 巴黎: before_model_callback 允许该请求。LLM 请求 get_weather_stateful(city='Paris')before_tool_callback 运行后检查参数,检测到"Paris",打印"阻止工具执行!",设置状态标志,并返回错误字典 {'status': 'error', 'error_message': 'Policy restriction...'}。实际的 get_weather_stateful 函数从未执行。智能体接收该错误字典就像它是工具的输出一样,并根据该错误信息生成响应。
  3. 伦敦: 行为类似纽约,成功传递回调并执行工具。新的伦敦天气报告会覆盖状态中的last_weather_report

您现在已添加了一个关键的安全层,不仅控制哪些内容能到达大语言模型,还根据大语言模型生成的特定参数控制如何使用智能体的工具。像before_model_callbackbefore_tool_callback这样的回调函数对于构建健壮、安全且符合策略的智能体应用至关重要。

结论:您的智能体团队已准备就绪!

恭喜!您已成功完成从构建一个单一基础天气智能体到使用ADK构建复杂多智能体团队的旅程。

让我们回顾一下您已完成的内容:

  • 你从一个基础智能体开始,它配备了一个工具(get_weather)。
  • 您探索了ADK的多模型灵活性,使用LiteLLM运行相同的核心逻辑,但采用了不同的LLM模型如Gemini、GPT-4o和Claude。
  • 您通过创建专门的子智能体(greeting_agentfarewell_agent)并实现从根智能体的自动委派功能,体现了模块化的设计理念。
  • 你为智能体赋予了记忆功能,通过使用Session State,使它们能够记住用户偏好(temperature_unit)和过往交互记录(output_key)。
  • 您通过before_model_callback(阻止特定输入关键词)和before_tool_callback(根据"Paris"等城市参数阻止工具执行)实现了关键的安全防护措施

通过构建这个逐步完善的天气机器人团队,您已经获得了使用ADK核心概念的实际经验,这些概念对于开发复杂的智能应用程序至关重要。

关键要点:

  • 智能体与工具:定义能力和推理的基本构建模块。清晰的指令和文档字符串至关重要。
  • 运行器与会话服务:协调智能体执行并维护对话上下文的引擎和内存管理系统。
  • 委派: 设计多智能体团队可以实现专业化、模块化,并更好地管理复杂任务。智能体的description是实现自动流程的关键。
  • 会话状态 (ToolContext, output_key): 对于创建具有上下文感知、个性化和多轮对话能力的智能体至关重要。
  • 回调函数 (before_model, before_tool): 强大的钩子机制,用于在关键操作(大语言模型调用或工具执行)之前实现安全控制、验证、策略执行和动态修改。
  • 灵活性 (LiteLlm): ADK 让您能够为任务选择最合适的大语言模型,在性能、成本和功能之间取得平衡。

下一步该去哪里?

您的天气机器人团队是一个很好的起点。以下是一些进一步探索Agent Development Kit并增强应用程序的想法:

  1. 真实天气API:get_weather工具中的mock_weather_db替换为调用真实天气API(如OpenWeatherMap、WeatherAPI)。
  2. 更复杂的状态: 在会话状态中存储更多用户偏好(例如首选位置、通知设置)或对话摘要。
  3. 优化委托机制: 尝试不同的根智能体指令或子智能体描述来微调委托逻辑。能否添加一个"forecast"智能体?
  4. 高级回调:
  5. 使用 after_model_callback 在LLM生成响应后,可能对其进行重新格式化或清理。
  6. 使用 after_tool_callback 处理或记录工具返回的结果。
  7. 实现before_agent_callbackafter_agent_callback用于智能体级别的进入/退出逻辑。
  8. 错误处理: 改进智能体处理工具错误或意外API响应的方式。可以考虑在工具内添加重试逻辑。
  9. 持久化会话存储:探索替代InMemorySessionService的方案来实现会话状态的持久化存储(例如使用Firestore或Cloud SQL等数据库——需要自定义实现或未来ADK集成)。
  10. 流式用户界面: 将您的智能体团队与Web框架(如FastAPI,如ADK流式快速入门所示)集成,以创建实时聊天界面。

ADK为构建复杂的LLM驱动应用程序提供了坚实基础。通过掌握本教程涵盖的概念——工具、状态、委托和回调——您已充分准备好应对日益复杂的智能体系统。

快乐构建!