Skip to content

工具(Tools)

什麼是工具(Tool)?

在 Agent Development Kit (ADK) 的語境下,工具(Tool)代表賦予 AI agent 特定能力的元件,使其能執行動作並與外部世界互動,突破僅有文字生成與推理的核心能力。能夠有效運用工具,正是有能力的 agent 與基礎大型語言模型 (LLM) 之間的主要區別。

從技術層面來看,工具通常是一個模組化的程式碼元件——例如 Python/Java 函式、類別方法,甚至是另一個專門的 agent——設計來執行明確、預定義的任務。這些任務往往涉及與外部系統或資料的互動。

Agent tool call

主要特點

以行動為導向: 工具用於執行特定動作,例如:

  • 查詢資料庫
  • 發送 API 請求(例如取得天氣資料、預訂系統)
  • 網頁搜尋
  • 執行程式碼片段
  • 從文件中擷取資訊(RAG)
  • 與其他軟體或服務互動

擴展 agent 能力: 工具讓 agent 能夠存取即時資訊、影響外部系統,並突破訓練資料本身的知識限制。

執行預定義邏輯: 關鍵在於,工具僅執行特定、由開發者定義的邏輯。工具本身並不具備像 agent 的核心大型語言模型 (LLM) 那樣的獨立推理能力。LLM 負責判斷何時、以何種輸入使用哪個工具,而工具本身僅執行其指定的功能。

agent 如何使用工具

agent 會透過函式呼叫等機制動態運用工具。一般流程如下:

  1. 推理: agent 的大型語言模型 (LLM) 會分析系統指令、對話歷史與使用者請求。
  2. 選擇: 根據分析結果,LLM 會根據 agent 可用的工具及每個工具的 docstring,決定是否執行某個工具。
  3. 呼叫: LLM 產生所需的參數(輸入),並觸發選定工具的執行。
  4. 觀察: agent 接收工具回傳的輸出(結果)。
  5. 整合: agent 將工具的輸出納入持續的推理過程,用於產生下一步回應、決定後續動作,或判斷目標是否已完成。

你可以將工具想像成 agent 智慧核心(LLM)可隨需取用的專業工具箱,協助完成複雜任務。

ADK 中的工具類型

Agent Development Kit (ADK) 支援多種工具類型,靈活滿足不同需求:

  1. Function Tools 由你自行建立、針對應用需求設計的工具。
  2. Built-in Tools 框架內建、可直接使用的常用工具。 例如:Google Search、程式碼執行、檢索增強生成(RAG)。
  3. Third-Party Tools 無縫整合自熱門外部函式庫的工具。 例如:LangChain Tools、CrewAI Tools。

請參閱上方連結的各類型工具文件頁面,獲取詳細說明與範例。

在 agent 指令中引用工具

在 agent 的指令中,你可以直接以函式名稱引用工具。如果工具的函式名稱docstring足夠清楚,則你的指令可著重於何時讓大型語言模型 (LLM) 使用該工具。這有助於提升指令明確性,並協助模型理解每個工具的預期用途。

明確指示 agent 如何處理工具可能產生的各種回傳值也非常重要。例如,若工具回傳錯誤訊息,你應在指令中說明 agent 應該重試操作、放棄任務,或向使用者請求更多資訊。

此外,ADK 支援工具的串接使用,即一個工具的輸出可作為另一個工具的輸入。實作這類工作流程時,請務必在 agent 指令中描述預期的工具使用順序,以引導模型依序完成必要步驟。

範例

以下範例展示 agent 如何在指令中引用工具的函式名稱來使用工具,同時說明如何引導 agent 處理工具的不同回傳值(如成功或錯誤訊息),以及如何協調多個工具的串接使用來完成任務。

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import asyncio
from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

APP_NAME="weather_sentiment_agent"
USER_ID="user1234"
SESSION_ID="1234"
MODEL_ID="gemini-2.0-flash"

# Tool 1
def get_weather_report(city: str) -> dict:
    """Retrieves the current weather report for a specified city.

    Returns:
        dict: A dictionary containing the weather information with a 'status' key ('success' or 'error') and a 'report' key with the weather details if successful, or an 'error_message' if an error occurred.
    """
    if city.lower() == "london":
        return {"status": "success", "report": "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a chance of rain."}
    elif city.lower() == "paris":
        return {"status": "success", "report": "The weather in Paris is sunny with a temperature of 25 degrees Celsius."}
    else:
        return {"status": "error", "error_message": f"Weather information for '{city}' is not available."}

weather_tool = FunctionTool(func=get_weather_report)


# Tool 2
def analyze_sentiment(text: str) -> dict:
    """Analyzes the sentiment of the given text.

    Returns:
        dict: A dictionary with 'sentiment' ('positive', 'negative', or 'neutral') and a 'confidence' score.
    """
    if "good" in text.lower() or "sunny" in text.lower():
        return {"sentiment": "positive", "confidence": 0.8}
    elif "rain" in text.lower() or "bad" in text.lower():
        return {"sentiment": "negative", "confidence": 0.7}
    else:
        return {"sentiment": "neutral", "confidence": 0.6}

sentiment_tool = FunctionTool(func=analyze_sentiment)


# Agent
weather_sentiment_agent = Agent(
    model=MODEL_ID,
    name='weather_sentiment_agent',
    instruction="""You are a helpful assistant that provides weather information and analyzes the sentiment of user feedback.
**If the user asks about the weather in a specific city, use the 'get_weather_report' tool to retrieve the weather details.**
**If the 'get_weather_report' tool returns a 'success' status, provide the weather report to the user.**
**If the 'get_weather_report' tool returns an 'error' status, inform the user that the weather information for the specified city is not available and ask if they have another city in mind.**
**After providing a weather report, if the user gives feedback on the weather (e.g., 'That's good' or 'I don't like rain'), use the 'analyze_sentiment' tool to understand their sentiment.** Then, briefly acknowledge their sentiment.
You can handle these tasks sequentially if needed.""",
    tools=[weather_tool, sentiment_tool]
)

async def main():
    """Main function to run the agent asynchronously."""
    # Session and Runner Setup
    session_service = InMemorySessionService()
    # Use 'await' to correctly create the session
    await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)

    runner = Runner(agent=weather_sentiment_agent, app_name=APP_NAME, session_service=session_service)

    # Agent Interaction
    query = "weather in london?"
    print(f"User Query: {query}")
    content = types.Content(role='user', parts=[types.Part(text=query)])

    # The runner's run method handles the async loop internally
    events = runner.run(user_id=USER_ID, session_id=SESSION_ID, new_message=content)

    for event in events:
        if event.is_final_response():
            final_response = event.content.parts[0].text
            print("Agent Response:", final_response)

# Standard way to run the main async function
if __name__ == "__main__":
    asyncio.run(main())
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext; // Ensure this import is correct
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class WeatherSentimentAgentApp {

  private static final String APP_NAME = "weather_sentiment_agent";
  private static final String USER_ID = "user1234";
  private static final String SESSION_ID = "1234";
  private static final String MODEL_ID = "gemini-2.0-flash";

  /**
   * Retrieves the current weather report for a specified city.
   *
   * @param city The city for which to retrieve the weather report.
   * @param toolContext The context for the tool.
   * @return A dictionary containing the weather information.
   */
  public static Map<String, Object> getWeatherReport(
      @Schema(name = "city")
      String city,
      @Schema(name = "toolContext")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();

    if (city.toLowerCase(Locale.ROOT).equals("london")) {
      response.put("status", "success");
      response.put(
          "report",
          "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a"
              + " chance of rain.");
    } else if (city.toLowerCase(Locale.ROOT).equals("paris")) {
      response.put("status", "success");
      response.put(
          "report", "The weather in Paris is sunny with a temperature of 25 degrees Celsius.");
    } else {
      response.put("status", "error");
      response.put(
          "error_message", String.format("Weather information for '%s' is not available.", city));
    }
    return response;
  }

  /**
   * Analyzes the sentiment of the given text.
   *
   * @param text The text to analyze.
   * @param toolContext The context for the tool.
   * @return A dictionary with sentiment and confidence score.
   */
  public static Map<String, Object> analyzeSentiment(
      @Schema(name = "text")
      String text,
      @Schema(name = "toolContext")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    String lowerText = text.toLowerCase(Locale.ROOT);
    if (lowerText.contains("good") || lowerText.contains("sunny")) {
      response.put("sentiment", "positive");
      response.put("confidence", 0.8);
    } else if (lowerText.contains("rain") || lowerText.contains("bad")) {
      response.put("sentiment", "negative");
      response.put("confidence", 0.7);
    } else {
      response.put("sentiment", "neutral");
      response.put("confidence", 0.6);
    }
    return response;
  }

  /**
   * Calls the agent with the given query and prints the final response.
   *
   * @param runner The runner to use.
   * @param query The query to send to the agent.
   */
  public static void callAgent(Runner runner, String query) {
    Content content = Content.fromParts(Part.fromText(query));

    InMemorySessionService sessionService = (InMemorySessionService) runner.sessionService();
    Session session =
        sessionService
            .createSession(APP_NAME, USER_ID, /* state= */ null, SESSION_ID)
            .blockingGet();

    runner
        .runAsync(session.userId(), session.id(), content)
        .forEach(
            event -> {
              if (event.finalResponse()
                  && event.content().isPresent()
                  && event.content().get().parts().isPresent()
                  && !event.content().get().parts().get().isEmpty()
                  && event.content().get().parts().get().get(0).text().isPresent()) {
                String finalResponse = event.content().get().parts().get().get(0).text().get();
                System.out.println("Agent Response: " + finalResponse);
              }
            });
  }

  public static void main(String[] args) throws NoSuchMethodException {
    FunctionTool weatherTool =
        FunctionTool.create(
            WeatherSentimentAgentApp.class.getMethod(
                "getWeatherReport", String.class, ToolContext.class));
    FunctionTool sentimentTool =
        FunctionTool.create(
            WeatherSentimentAgentApp.class.getMethod(
                "analyzeSentiment", String.class, ToolContext.class));

    BaseAgent weatherSentimentAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("weather_sentiment_agent")
            .description("Weather Sentiment Agent")
            .instruction("""
                    You are a helpful assistant that provides weather information and analyzes the
                    sentiment of user feedback
                    **If the user asks about the weather in a specific city, use the
                    'get_weather_report' tool to retrieve the weather details.**
                    **If the 'get_weather_report' tool returns a 'success' status, provide the
                    weather report to the user.**
                    **If the 'get_weather_report' tool returns an 'error' status, inform the
                    user that the weather information for the specified city is not available
                    and ask if they have another city in mind.**
                    **After providing a weather report, if the user gives feedback on the
                    weather (e.g., 'That's good' or 'I don't like rain'), use the
                    'analyze_sentiment' tool to understand their sentiment.** Then, briefly
                    acknowledge their sentiment.
                    You can handle these tasks sequentially if needed.
                    """)
            .tools(ImmutableList.of(weatherTool, sentimentTool))
            .build();

    InMemorySessionService sessionService = new InMemorySessionService();
    Runner runner = new Runner(weatherSentimentAgent, APP_NAME, null, sessionService);

    // Change the query to ensure the tool is called with a valid city that triggers a "success"
    // response from the tool, like "london" (without the question mark).
    callAgent(runner, "weather in paris");
  }
}

Tool Context

對於更進階的情境,Agent Development Kit (ADK) 允許你在工具函式(tool function)中,透過加入特殊參數 tool_context: ToolContext,存取額外的上下文資訊。只要在函式簽名中加入此參數,當 agent 執行期間呼叫你的工具時,ADK 會自動提供一個 ToolContext 類別的實例。

ToolContext 提供了多項關鍵資訊與控制選項:

  • state: State:讀取與修改目前 session state。這裡所做的變更會被追蹤並持久化。

  • actions: EventActions:影響 agent 在工具執行後的後續行為(例如:跳過摘要、轉交給其他 agent 等)。

  • function_call_id: str:由框架指派給本次工具呼叫的唯一識別碼。可用於追蹤與驗證回應的關聯。當單一模型回應中呼叫多個工具時,也很有幫助。

  • function_call_event_id: str:此屬性提供觸發本次工具呼叫的事件唯一識別碼。適合用於追蹤與記錄。

  • auth_response: Any:如果在本次工具呼叫前已完成驗證流程,這個屬性會包含驗證回應/憑證。

  • 服務存取:可用於與已設定的服務(如 Artifacts、Memory)互動的方法。

請注意,不應在工具函式的 docstring 中包含 tool_context 參數。由於 ToolContext 是在大型語言模型 (LLM) 決定呼叫工具函式之後,由 ADK 框架自動注入,因此這個參數與 LLM 的決策無關,包含它反而可能讓 LLM 感到困惑。

State Management

tool_context.state 屬性提供對目前 session state 的直接讀寫存取。它的行為類似於字典,但會確保所有修改都被追蹤為差異(delta),並由 SessionService 持久化。這讓工具可以在不同互動與 agent 步驟間維持與共用資訊。

  • 讀取狀態:可使用標準字典存取方式(tool_context.state['my_key'])或 .get() 方法(tool_context.state.get('my_key', default_value))。

  • 寫入狀態:可直接指定值(tool_context.state['new_key'] = 'new_value')。這些變更會被記錄在產生事件的 state_delta 中。

  • 狀態前綴:請記得標準的 state 前綴:

    • app:*:全應用程式共用。

    • user:*:目前使用者在所有 session 間共用。

    • (無前綴):僅限於目前 session。

    • temp:*:暫時性,不會在多次呼叫間持久化(適合於單次 run 呼叫內傳遞資料,但在工具 context(跨 LLM 呼叫)中通常較少用)。

from google.adk.tools import ToolContext, FunctionTool

def update_user_preference(preference: str, value: str, tool_context: ToolContext):
    """Updates a user-specific preference."""
    user_prefs_key = "user:preferences"
    # Get current preferences or initialize if none exist
    preferences = tool_context.state.get(user_prefs_key, {})
    preferences[preference] = value
    # Write the updated dictionary back to the state
    tool_context.state[user_prefs_key] = preferences
    print(f"Tool: Updated user preference '{preference}' to '{value}'")
    return {"status": "success", "updated_preference": preference}

pref_tool = FunctionTool(func=update_user_preference)

# In an Agent:
# my_agent = Agent(..., tools=[pref_tool])

# When the LLM calls update_user_preference(preference='theme', value='dark', ...):
# The tool_context.state will be updated, and the change will be part of the
# resulting tool response event's actions.state_delta.
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;

// Updates a user-specific preference.
public Map<String, String> updateUserThemePreference(String value, ToolContext toolContext) {
  String userPrefsKey = "user:preferences:theme";

  // Get current preferences or initialize if none exist
  String preference = toolContext.state().getOrDefault(userPrefsKey, "").toString();
  if (preference.isEmpty()) {
    preference = value;
  }

  // Write the updated dictionary back to the state
  toolContext.state().put("user:preferences", preference);
  System.out.printf("Tool: Updated user preference %s to %s", userPrefsKey, preference);

  return Map.of("status", "success", "updated_preference", toolContext.state().get(userPrefsKey).toString());
  // When the LLM calls updateUserThemePreference("dark"):
  // The toolContext.state will be updated, and the change will be part of the
  // resulting tool response event's actions.stateDelta.
}

控制 agent 流程

tool_context.actions 屬性(在 Java 中為 ToolContext.actions())包含一個 EventActions 物件。透過修改此物件的屬性,您的工具可以影響 agent 或框架在工具執行結束後的行為。

  • skip_summarization: bool:(預設值:False)若設為 True,則指示 Agent Development Kit (ADK) 跳過通常會摘要工具輸出的大型語言模型 (LLM) 呼叫。如果您的工具回傳值已經是可直接給使用者的訊息,這會很有用。

  • transfer_to_agent: str:將此屬性設為另一個 agent 的名稱。框架會停止目前 agent 的執行,並將對話控制權轉交給指定的 agent。這讓工具可以動態地將任務交給更專業的 agent 處理。

  • escalate: bool:(預設值:False)設為 True 時,表示目前 agent 無法處理該請求,應將控制權向上傳遞給其父 agent(若有階層結構)。在 LoopAgent 中,若子 agent 的工具設為 escalate=True,則會終止該迴圈。

範例

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk.agents import Agent
from google.adk.tools import FunctionTool
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools import ToolContext
from google.genai import types

APP_NAME="customer_support_agent"
USER_ID="user1234"
SESSION_ID="1234"


def check_and_transfer(query: str, tool_context: ToolContext) -> str:
    """Checks if the query requires escalation and transfers to another agent if needed."""
    if "urgent" in query.lower():
        print("Tool: Detected urgency, transferring to the support agent.")
        tool_context.actions.transfer_to_agent = "support_agent"
        return "Transferring to the support agent..."
    else:
        return f"Processed query: '{query}'. No further action needed."

escalation_tool = FunctionTool(func=check_and_transfer)

main_agent = Agent(
    model='gemini-2.0-flash',
    name='main_agent',
    instruction="""You are the first point of contact for customer support of an analytics tool. Answer general queries. If the user indicates urgency, use the 'check_and_transfer' tool.""",
    tools=[check_and_transfer]
)

support_agent = Agent(
    model='gemini-2.0-flash',
    name='support_agent',
    instruction="""You are the dedicated support agent. Mentioned you are a support handler and please help the user with their urgent issue."""
)

main_agent.sub_agents = [support_agent]

# Session and Runner
async def setup_session_and_runner():
    session_service = InMemorySessionService()
    session = await session_service.create_session(app_name=APP_NAME, user_id=USER_ID, session_id=SESSION_ID)
    runner = Runner(agent=main_agent, app_name=APP_NAME, session_service=session_service)
    return session, runner

# Agent Interaction
async def call_agent_async(query):
    content = types.Content(role='user', parts=[types.Part(text=query)])
    session, runner = await setup_session_and_runner()
    events = runner.run_async(user_id=USER_ID, session_id=SESSION_ID, new_message=content)

    async for event in events:
        if event.is_final_response():
            final_response = event.content.parts[0].text
            print("Agent Response: ", final_response)

# Note: In Colab, you can directly use 'await' at the top level.
# If running this code as a standalone Python script, you'll need to use asyncio.run() or manage the event loop.
await call_agent_async("this is urgent, i cant login")
import com.google.adk.agents.LlmAgent;
import com.google.adk.runner.Runner;
import com.google.adk.sessions.InMemorySessionService;
import com.google.adk.sessions.Session;
import com.google.adk.tools.Annotations.Schema;
import com.google.adk.tools.FunctionTool;
import com.google.adk.tools.ToolContext;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class CustomerSupportAgentApp {

  private static final String APP_NAME = "customer_support_agent";
  private static final String USER_ID = "user1234";
  private static final String SESSION_ID = "1234";
  private static final String MODEL_ID = "gemini-2.0-flash";

  /**
   * Checks if the query requires escalation and transfers to another agent if needed.
   *
   * @param query The user's query.
   * @param toolContext The context for the tool.
   * @return A map indicating the result of the check and transfer.
   */
  public static Map<String, Object> checkAndTransfer(
      @Schema(name = "query", description = "the user query")
      String query,
      @Schema(name = "toolContext", description = "the tool context")
      ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    if (query.toLowerCase(Locale.ROOT).contains("urgent")) {
      System.out.println("Tool: Detected urgency, transferring to the support agent.");
      toolContext.actions().setTransferToAgent("support_agent");
      response.put("status", "transferring");
      response.put("message", "Transferring to the support agent...");
    } else {
      response.put("status", "processed");
      response.put(
          "message", String.format("Processed query: '%s'. No further action needed.", query));
    }
    return response;
  }

  /**
   * Calls the agent with the given query and prints the final response.
   *
   * @param runner The runner to use.
   * @param query The query to send to the agent.
   */
  public static void callAgent(Runner runner, String query) {
    Content content =
        Content.fromParts(Part.fromText(query));

    InMemorySessionService sessionService = (InMemorySessionService) runner.sessionService();
    // Fixed: session ID does not need to be an optional.
    Session session =
        sessionService
            .createSession(APP_NAME, USER_ID, /* state= */ null, SESSION_ID)
            .blockingGet();

    runner
        .runAsync(session.userId(), session.id(), content)
        .forEach(
            event -> {
              if (event.finalResponse()
                  && event.content().isPresent()
                  && event.content().get().parts().isPresent()
                  && !event.content().get().parts().get().isEmpty()
                  && event.content().get().parts().get().get(0).text().isPresent()) {
                String finalResponse = event.content().get().parts().get().get(0).text().get();
                System.out.println("Agent Response: " + finalResponse);
              }
            });
  }

  public static void main(String[] args) throws NoSuchMethodException {
    FunctionTool escalationTool =
        FunctionTool.create(
            CustomerSupportAgentApp.class.getMethod(
                "checkAndTransfer", String.class, ToolContext.class));

    LlmAgent supportAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("support_agent")
            .description("""
                The dedicated support agent.
                Mentions it is a support handler and helps the user with their urgent issue.
            """)
            .instruction("""
                You are the dedicated support agent.
                Mentioned you are a support handler and please help the user with their urgent issue.
            """)
            .build();

    LlmAgent mainAgent =
        LlmAgent.builder()
            .model(MODEL_ID)
            .name("main_agent")
            .description("""
                The first point of contact for customer support of an analytics tool.
                Answers general queries.
                If the user indicates urgency, uses the 'check_and_transfer' tool.
                """)
            .instruction("""
                You are the first point of contact for customer support of an analytics tool.
                Answer general queries.
                If the user indicates urgency, use the 'check_and_transfer' tool.
                """)
            .tools(ImmutableList.of(escalationTool))
            .subAgents(supportAgent)
            .build();
    // Fixed: LlmAgent.subAgents() expects 0 arguments.
    // Sub-agents are now added to the main agent via its builder,
    // as `subAgents` is a property that should be set during agent construction
    // if it's not dynamically managed.

    InMemorySessionService sessionService = new InMemorySessionService();
    Runner runner = new Runner(mainAgent, APP_NAME, null, sessionService);

    // Agent Interaction
    callAgent(runner, "this is urgent, i cant login");
  }
}
說明
  • 我們定義了兩個 agent:main_agentsupport_agentmain_agent 被設計為初始聯絡點。
  • main_agent 呼叫 check_and_transfer 工具時,該工具會檢查使用者的查詢。
  • 如果查詢內容包含「urgent」這個詞,該工具會存取 tool_context,特別是 tool_context.actions,並將 transfer_to_agent 屬性設為 support_agent
  • 這個動作會通知框架,將對話控制權轉交給名為 support_agent 的 agent
  • main_agent 處理緊急查詢時,check_and_transfer 工具會觸發轉交。接下來的回應理想上會來自 support_agent
  • 對於沒有緊急性的普通查詢,該工具僅會處理查詢,不會觸發轉交。

這個範例說明了工具如何透過其 ToolContext 中的 EventActions,動態影響對話流程,將控制權轉交給其他專門的 agent。

驗證(Authentication)

python_only

ToolContext 提供工具與需驗證 API 互動的機制。如果你的工具需要處理驗證,可以使用以下方式:

  • auth_response:若驗證已由框架在呼叫你的工具前處理(常見於 RestApiTool 和 OpenAPI 安全機制),則此處會包含憑證(例如 token)。

  • request_credential(auth_config: dict):如果你的工具判斷需要驗證但尚未取得憑證,請呼叫此方法。這會通知框架根據提供的 auth_config 啟動驗證流程。

  • get_auth_response():在後續呼叫(於 request_credential 成功處理後)呼叫此方法,以取得使用者所提供的憑證。

有關驗證流程、設定與範例的詳細說明,請參閱專門的 Tool Authentication 文件頁面。

具情境感知的資料存取方法

這些方法為你的工具提供便利的方式,以與由已設定服務管理、與 session 或使用者相關的持久性資料互動。

  • list_artifacts()(或 Java 中的 listArtifacts()):透過 artifact_service,回傳目前 session 所儲存的所有 artifact 檔名(或鍵值)清單。artifact 通常是使用者上傳或工具/agent 產生的檔案(如圖片、文件等)。

  • load_artifact(filename: str):從 artifact_service 依檔名取得特定 artifact。你可以選擇性指定版本;若未指定則回傳最新版本。回傳一個 google.genai.types.Part 物件,內含 artifact 資料與 mime type,若找不到則回傳 None。

  • save_artifact(filename: str, artifact: types.Part):將 artifact 的新版本儲存至 artifact_service。回傳新版本號(從 0 開始)。

  • search_memory(query: str) python_only

    使用已設定的 memory_service 查詢使用者的長期記憶。這對於從過往互動或已儲存知識中檢索相關資訊非常有用。SearchMemoryResponse 的結構取決於具體的記憶服務實作,但通常包含相關的文字片段或對話摘錄。

範例

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from google.adk.tools import ToolContext, FunctionTool
from google.genai import types


def process_document(
    document_name: str, analysis_query: str, tool_context: ToolContext
) -> dict:
    """Analyzes a document using context from memory."""

    # 1. Load the artifact
    print(f"Tool: Attempting to load artifact: {document_name}")
    document_part = tool_context.load_artifact(document_name)

    if not document_part:
        return {"status": "error", "message": f"Document '{document_name}' not found."}

    document_text = document_part.text  # Assuming it's text for simplicity
    print(f"Tool: Loaded document '{document_name}' ({len(document_text)} chars).")

    # 2. Search memory for related context
    print(f"Tool: Searching memory for context related to: '{analysis_query}'")
    memory_response = tool_context.search_memory(
        f"Context for analyzing document about {analysis_query}"
    )
    memory_context = "\n".join(
        [
            m.events[0].content.parts[0].text
            for m in memory_response.memories
            if m.events and m.events[0].content
        ]
    )  # Simplified extraction
    print(f"Tool: Found memory context: {memory_context[:100]}...")

    # 3. Perform analysis (placeholder)
    analysis_result = f"Analysis of '{document_name}' regarding '{analysis_query}' using memory context: [Placeholder Analysis Result]"
    print("Tool: Performed analysis.")

    # 4. Save the analysis result as a new artifact
    analysis_part = types.Part.from_text(text=analysis_result)
    new_artifact_name = f"analysis_{document_name}"
    version = await tool_context.save_artifact(new_artifact_name, analysis_part)
    print(f"Tool: Saved analysis result as '{new_artifact_name}' version {version}.")

    return {
        "status": "success",
        "analysis_artifact": new_artifact_name,
        "version": version,
    }


doc_analysis_tool = FunctionTool(func=process_document)

# In an Agent:
# Assume artifact 'report.txt' was previously saved.
# Assume memory service is configured and has relevant past data.
# my_agent = Agent(..., tools=[doc_analysis_tool], artifact_service=..., memory_service=...)
// Analyzes a document using context from memory.
// You can also list, load and save artifacts using Callback Context or LoadArtifacts tool.
public static @NonNull Maybe<ImmutableMap<String, Object>> processDocument(
    @Annotations.Schema(description = "The name of the document to analyze.") String documentName,
    @Annotations.Schema(description = "The query for the analysis.") String analysisQuery,
    ToolContext toolContext) {

  // 1. List all available artifacts
  System.out.printf(
      "Listing all available artifacts %s:", toolContext.listArtifacts().blockingGet());

  // 2. Load an artifact to memory
  System.out.println("Tool: Attempting to load artifact: " + documentName);
  Part documentPart = toolContext.loadArtifact(documentName, Optional.empty()).blockingGet();
  if (documentPart == null) {
    System.out.println("Tool: Document '" + documentName + "' not found.");
    return Maybe.just(
        ImmutableMap.<String, Object>of(
            "status", "error", "message", "Document '" + documentName + "' not found."));
  }
  String documentText = documentPart.text().orElse("");
  System.out.println(
      "Tool: Loaded document '" + documentName + "' (" + documentText.length() + " chars).");

  // 3. Perform analysis (placeholder)
  String analysisResult =
      "Analysis of '"
          + documentName
          + "' regarding '"
          + analysisQuery
          + " [Placeholder Analysis Result]";
  System.out.println("Tool: Performed analysis.");

  // 4. Save the analysis result as a new artifact
  Part analysisPart = Part.fromText(analysisResult);
  String newArtifactName = "analysis_" + documentName;

  toolContext.saveArtifact(newArtifactName, analysisPart);

  return Maybe.just(
      ImmutableMap.<String, Object>builder()
          .put("status", "success")
          .put("analysis_artifact", newArtifactName)
          .build());
}
// FunctionTool processDocumentTool =
//      FunctionTool.create(ToolContextArtifactExample.class, "processDocument");
// In the Agent, include this function tool.
// LlmAgent agent = LlmAgent().builder().tools(processDocumentTool).build();

透過運用 ToolContext,開發者可以建立更為進階且具備情境感知能力的自訂工具(custom tools),這些工具能無縫整合至 Agent Development Kit (ADK) 的架構中,並提升 agent 的整體能力。

定義有效的工具函式

當你將某個方法或函式作為 ADK 工具(Tool)使用時,其定義方式會顯著影響 agent 是否能正確使用它。agent 所依賴的大型語言模型 (Large Language Model, LLM) 主要根據該函式的 名稱參數(arguments)型別提示(type hints),以及 docstring / 原始碼註解 來理解其用途並產生正確的呼叫。

以下是定義有效工具函式的關鍵指引:

  • 函式名稱:

    • 使用描述性、以動詞-名詞組合為主的名稱,清楚指出動作內容(例如:get_weathersearchDocumentsschedule_meeting)。
    • 避免使用像 runprocesshandle_data 這類過於通用的名稱,或 doStuff 這種過於模糊的名稱。即使描述良好,像 do_stuff 這樣的名稱也可能讓模型混淆該在何時使用此工具,而不是例如 cancelFlight
    • LLM 在選擇工具時,會將函式名稱作為主要識別依據。
  • 參數(Arguments):

    • 你的函式可以有任意數量的參數。
    • 請使用清楚且具描述性的名稱(例如:city 取代 csearch_query 取代 q)。
    • 在 Python 中為所有參數提供型別提示(type hints)(例如:city: struser_id: intitems: list[str])。這對於 ADK 產生正確的 LLM schema 至關重要。
    • 確保所有參數型別皆為 JSON 可序列化。所有 Java 原始型別以及標準 Python 型別(如 strintfloatboollistdict)及其組合通常都安全。除非自訂類別實例有明確的 JSON 表示,否則請避免直接作為參數傳遞複雜的自訂類別實例。
    • 請勿為參數設定預設值。例如:def my_func(param1: str = "default")。底層模型在產生函式呼叫時,對預設值的支援並不可靠。所有必要資訊應由 LLM 從情境中取得,或在缺少時明確要求。
    • self / cls 自動處理: 隱含參數如 self(用於實例方法)或 cls(用於類別方法)會由 ADK 自動處理,並從提供給 LLM 的 schema 中排除。你只需為工具實際需要 LLM 提供的邏輯參數定義型別提示與描述即可。
  • 回傳型別:

    • 函式的回傳值在 Python 中必須為字典(dict,在 Java 中則為 Map
    • 若你的函式回傳非字典型別(如字串、數字、串列),ADK 框架會自動將其包裝為字典/Map(如 {'result': your_original_return_value}),再將結果回傳給模型。
    • 設計字典/Map 的鍵與值時,請確保對 LLM 來說具描述性且易於理解。請記住,模型會讀取這個輸出來決定下一步行動。
    • 請包含有意義的鍵。例如,不要只回傳錯誤碼(如 500),而是回傳 {'status': 'error', 'error_message': 'Database connection failed'}
    • 強烈建議在回傳值中加入 status 鍵(例如:'success''error''pending''ambiguous'),以明確告知模型工具執行的結果。
  • Docstring / 原始碼註解:

    • 這非常重要。 docstring 是 LLM 取得描述性資訊的主要來源。
    • 明確說明工具的「功能」。 具體描述其用途與限制。
    • 說明「何時」應使用此工具。 提供情境或範例情境,協助 LLM 做出判斷。
    • 清楚描述「每個參數」。 解釋 LLM 需要為該參數提供哪些資訊。
    • 說明預期 dict 回傳值的結構與意義,特別是不同的 status 值及其相關資料鍵。
    • 不要描述被注入的 ToolContext 參數。 請避免在 docstring 描述中提及可選的 tool_context: ToolContext 參數,因為這不是 LLM 需要知道的參數。ToolContext 會由 ADK 注入, LLM 決定呼叫該工具之後才會傳入。

    良好定義的範例:

def lookup_order_status(order_id: str) -> dict:
  """Fetches the current status of a customer's order using its ID.

  Use this tool ONLY when a user explicitly asks for the status of
  a specific order and provides the order ID. Do not use it for
  general inquiries.

  Args:
      order_id: The unique identifier of the order to look up.

  Returns:
      A dictionary indicating the outcome.
      On success, status is 'success' and includes an 'order' dictionary.
      On failure, status is 'error' and includes an 'error_message'.
      Example success: {'status': 'success', 'order': {'state': 'shipped', 'tracking_number': '1Z9...'}}
      Example error: {'status': 'error', 'error_message': 'Order ID not found.'}
  """
  # ... function implementation to fetch status ...
  if status_details := fetch_status_from_backend(order_id):
    return {
        "status": "success",
        "order": {
            "state": status_details.state,
            "tracking_number": status_details.tracking,
        },
    }
  else:
    return {"status": "error", "error_message": f"Order ID {order_id} not found."}
/**
 * Retrieves the current weather report for a specified city.
 *
 * @param city The city for which to retrieve the weather report.
 * @param toolContext The context for the tool.
 * @return A dictionary containing the weather information.
 */
public static Map<String, Object> getWeatherReport(String city, ToolContext toolContext) {
    Map<String, Object> response = new HashMap<>();
    if (city.toLowerCase(Locale.ROOT).equals("london")) {
        response.put("status", "success");
        response.put(
                "report",
                "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a"
                        + " chance of rain.");
    } else if (city.toLowerCase(Locale.ROOT).equals("paris")) {
        response.put("status", "success");
        response.put("report", "The weather in Paris is sunny with a temperature of 25 degrees Celsius.");
    } else {
        response.put("status", "error");
        response.put("error_message", String.format("Weather information for '%s' is not available.", city));
    }
    return response;
}
  • 簡潔與專注:
    • 保持工具聚焦: 每個 tool 應該盡可能只執行一個明確定義的任務。
    • 參數越少越好: 大型語言模型 (LLM) 通常能更可靠地處理參數較少且定義清楚的工具,相較於有許多可選或複雜參數的工具。
    • 使用簡單資料型別: 優先使用基本型別(如 PythonstrintboolfloatList[str],或 Javaintbyteshortlongfloatdoublebooleanchar),盡量避免將複雜的自訂類別或深層巢狀結構作為參數。
    • 拆解複雜任務: 將執行多個不同邏輯步驟的函式拆分為更小、更專注的工具。例如,與其設計一個單一的 update_user_profile(profile: ProfileObject) 工具,不如考慮分別設計 update_user_name(name: str)update_user_address(address: str)update_user_preferences(preferences: list[str]) 等工具。這能讓大型語言模型 (LLM) 更容易選擇並使用正確的功能。

遵循這些指引,能為大型語言模型 (LLM) 提供所需的清晰結構,讓其更有效率地運用你的自訂工具函式,進而帶來更強大且可靠的 agent 行為。

工具組(Toolsets):分組與動態提供工具 python_only

除了單一工具外,Agent Development Kit (ADK) 引入了 工具組(Toolset) 的概念,透過 BaseToolset 介面(定義於 google.adk.tools.base_toolset)。工具組允許你管理並動態提供一組 BaseTool 實例給 agent。

這種方式的優點包括:

  • 組織相關工具: 將具有共同用途的工具分組(例如:所有數學運算工具,或所有與特定 API 互動的工具)。
  • 動態工具可用性: 讓 agent 能根據當前情境(例如:使用者權限、session state 或其他執行時條件)動態擁有不同的工具。工具組的 get_tools 方法可決定要暴露哪些工具。
  • 整合外部工具提供者: 工具組可以作為外部系統(如 OpenAPI 規格或 MCP server)工具的轉接器,將其轉換為 ADK 相容的 BaseTool 物件。

BaseToolset 介面

任何作為 ADK 工具組的類別,都應實作 BaseToolset 抽象基底類別。此介面主要定義兩個方法:

  • async def get_tools(...) -> list[BaseTool]: 這是工具組的核心方法。當 ADK agent 需要知道可用工具時,會對其 tools 清單中的每個 BaseToolset 實例呼叫 get_tools()

    • 此方法會接收一個可選的 readonly_contextReadonlyContext 的實例)。這個 context 提供唯讀存取,例如當前 session state(readonly_context.state)、agent 名稱及呼叫 ID。工具組可利用這個 context 動態決定要回傳哪些工具。
    • 必須回傳一個 list,內容為多個 BaseTool 實例(如 FunctionToolRestApiTool)。
  • async def close(self) -> None: 這個非同步方法會在工具組不再需要時,由 ADK 框架呼叫,例如 agent server 關閉或 Runner 被關閉時。可在此實作任何必要的清理動作,例如關閉網路連線、釋放檔案控制代碼,或清理工具組管理的其他資源。

在 agent 中使用工具組

你可以直接將自訂的 BaseToolset 實作實例,與個別 BaseTool 實例一起,加入 LlmAgenttools 清單中。

當 agent 初始化或需要判斷可用功能時,ADK 框架會遍歷 tools 清單:

  • 若項目為 BaseTool 實例,則直接使用。
  • 若項目為 BaseToolset 實例,則會呼叫其 get_tools() 方法(傳入目前的 ReadonlyContext),並將回傳的 BaseTool 清單加入 agent 的可用工具中。

範例:簡單的數學工具組

以下將建立一個提供基本算術運算的工具組範例。

# 1. Define the individual tool functions
def add_numbers(a: int, b: int, tool_context: ToolContext) -> Dict[str, Any]:
    """Adds two integer numbers.
    Args:
        a: The first number.
        b: The second number.
    Returns:
        A dictionary with the sum, e.g., {'status': 'success', 'result': 5}
    """
    print(f"Tool: add_numbers called with a={a}, b={b}")
    result = a + b
    # Example: Storing something in tool_context state
    tool_context.state["last_math_operation"] = "addition"
    return {"status": "success", "result": result}


def subtract_numbers(a: int, b: int) -> Dict[str, Any]:
    """Subtracts the second number from the first.
    Args:
        a: The first number.
        b: The second number.
    Returns:
        A dictionary with the difference, e.g., {'status': 'success', 'result': 1}
    """
    print(f"Tool: subtract_numbers called with a={a}, b={b}")
    return {"status": "success", "result": a - b}


# 2. Create the Toolset by implementing BaseToolset
class SimpleMathToolset(BaseToolset):
    def __init__(self, prefix: str = "math_"):
        self.prefix = prefix
        # Create FunctionTool instances once
        self._add_tool = FunctionTool(
            func=add_numbers,
            name=f"{self.prefix}add_numbers",  # Toolset can customize names
        )
        self._subtract_tool = FunctionTool(
            func=subtract_numbers, name=f"{self.prefix}subtract_numbers"
        )
        print(f"SimpleMathToolset initialized with prefix '{self.prefix}'")

    async def get_tools(
        self, readonly_context: Optional[ReadonlyContext] = None
    ) -> List[BaseTool]:
        print(f"SimpleMathToolset.get_tools() called.")
        # Example of dynamic behavior:
        # Could use readonly_context.state to decide which tools to return
        # For instance, if readonly_context.state.get("enable_advanced_math"):
        #    return [self._add_tool, self._subtract_tool, self._multiply_tool]

        # For this simple example, always return both tools
        tools_to_return = [self._add_tool, self._subtract_tool]
        print(f"SimpleMathToolset providing tools: {[t.name for t in tools_to_return]}")
        return tools_to_return

    async def close(self) -> None:
        # No resources to clean up in this simple example
        print(f"SimpleMathToolset.close() called for prefix '{self.prefix}'.")
        await asyncio.sleep(0)  # Placeholder for async cleanup if needed


# 3. Define an individual tool (not part of the toolset)
def greet_user(name: str = "User") -> Dict[str, str]:
    """Greets the user."""
    print(f"Tool: greet_user called with name={name}")
    return {"greeting": f"Hello, {name}!"}


greet_tool = FunctionTool(func=greet_user)

# 4. Instantiate the toolset
math_toolset_instance = SimpleMathToolset(prefix="calculator_")

# 5. Define an agent that uses both the individual tool and the toolset
calculator_agent = LlmAgent(
    name="CalculatorAgent",
    model="gemini-2.0-flash",  # Replace with your desired model
    instruction="You are a helpful calculator and greeter. "
    "Use 'greet_user' for greetings. "
    "Use 'calculator_add_numbers' to add and 'calculator_subtract_numbers' to subtract. "
    "Announce the state of 'last_math_operation' if it's set.",
    tools=[greet_tool, math_toolset_instance],  # Individual tool  # Toolset instance
)

在此範例中:

  • SimpleMathToolset 實作了 BaseToolset,其 get_tools() 方法會針對 add_numberssubtract_numbers 回傳 FunctionTool 實例。此外,還會使用前綴字自訂這些工具的名稱。
  • calculator_agent 同時設定了個別的 greet_tool 以及一個 SimpleMathToolset 實例。
  • 當執行 calculator_agent 時,Agent Development Kit (ADK) 會呼叫 math_toolset_instance.get_tools()。此時 agent 的大型語言模型 (LLM) 將能存取 greet_usercalculator_add_numberscalculator_subtract_numbers,以處理使用者請求。
  • add_numbers 工具展示了如何寫入 tool_context.state,而 agent 的指令則提及了讀取這個狀態。
  • 會呼叫 close() 方法,以確保工具組所持有的任何資源都能被釋放。

工具組(Toolsets)提供了一種強大的方式,能夠組織、管理並動態地將多個工具集合提供給你的 ADK agent,讓 agent 應用程式更加模組化、易於維護且具備高度適應性。