Skip to content

Loop agents

LoopAgent

LoopAgent 是一種 workflow agent(工作流程 agent),能夠以迴圈(即反覆執行)的方式執行其子 agent。它會重複執行 一連串的 agent,直到達到指定的迭代次數,或滿足終止條件為止。

當你的工作流程需要重複執行或逐步優化(例如反覆修訂程式碼)時,請使用 LoopAgent

範例

  • 假設你想建立一個能夠產生食物圖片的 agent,但有時當你希望產生特定數量的物品(例如 5 根香蕉)時,產生的圖片卻出現不同數量的物品(例如出現 7 根香蕉)。你有兩個工具:Generate ImageCount Food Items。由於你希望持續產生圖片,直到正確產生指定數量的物品,或達到一定的迭代次數為止,因此你應該使用 LoopAgent 來建構你的 agent。

如同其他workflow agentLoopAgent 並非由大型語言模型 (LLM) 驅動,因此其執行方式是可預期且具決定性的。不過,workflow agent 只負責執行流程(例如在迴圈中),而不涉及內部邏輯;workflow agent 的工具或子 agent 可以選擇是否使用大型語言模型 (LLM)。

運作方式

當呼叫 LoopAgentRun Async 方法時,會執行以下動作:

  1. 子 agent 執行: 依序遍歷子 agent 清單。對於_每一個_子 agent,會呼叫該 agent 的 Run Async 方法。
  2. 終止條件檢查:

    關鍵在於LoopAgent 本身_不會_自動決定何時停止迴圈。你_必須_實作終止機制,以避免無限迴圈。常見策略包括:

    • 最大迭代次數:LoopAgent 中設定最大迭代次數。當達到該次數時,迴圈會自動終止
    • 由子 agent 升級終止: 設計一個或多個子 agent 來判斷條件(例如:「文件品質是否足夠好?」「是否已達成共識?」)。如果條件達成,子 agent 可以發出終止訊號(例如透過觸發自訂事件、在共用 context 設定旗標,或回傳特定值)。

Loop Agent

完整範例:文件逐步優化

想像一個你希望反覆優化文件的情境:

  • Writer Agent: LlmAgent,負責產生或修訂主題草稿。
  • Critic Agent: LlmAgent,負責評論草稿並指出可改進之處。

    LoopAgent(sub_agents=[WriterAgent, CriticAgent], max_iterations=5)
    

在這個設定中,LoopAgent 會負責管理整個反覆執行(迭代)的流程。CriticAgent 可以設計為當文件達到滿意的品質水準時回傳 "STOP" 訊號,以避免進行更多次的迭代。或者,也可以利用 max iterations 參數來限制流程最多執行固定次數,或是實作外部邏輯來決定何時停止。此迴圈最多會執行五次,確保反覆優化不會無限持續下去。

完整程式碼
# Part of agent.py --> Follow https://google.github.io/adk-docs/get-started/quickstart/ to learn the setup

import asyncio
import os
from google.adk.agents import LoopAgent, LlmAgent, BaseAgent, SequentialAgent
from google.genai import types
from google.adk.runners import InMemoryRunner
from google.adk.agents.invocation_context import InvocationContext
from google.adk.tools.tool_context import ToolContext
from typing import AsyncGenerator, Optional
from google.adk.events import Event, EventActions

# --- Constants ---
APP_NAME = "doc_writing_app_v3" # New App Name
USER_ID = "dev_user_01"
SESSION_ID_BASE = "loop_exit_tool_session" # New Base Session ID
GEMINI_MODEL = "gemini-2.0-flash"
STATE_INITIAL_TOPIC = "initial_topic"

# --- State Keys ---
STATE_CURRENT_DOC = "current_document"
STATE_CRITICISM = "criticism"
# Define the exact phrase the Critic should use to signal completion
COMPLETION_PHRASE = "No major issues found."

# --- Tool Definition ---
def exit_loop(tool_context: ToolContext):
  """Call this function ONLY when the critique indicates no further changes are needed, signaling the iterative process should end."""
  print(f"  [Tool Call] exit_loop triggered by {tool_context.agent_name}")
  tool_context.actions.escalate = True
  # Return empty dict as tools should typically return JSON-serializable output
  return {}

# --- Agent Definitions ---

# STEP 1: Initial Writer Agent (Runs ONCE at the beginning)
initial_writer_agent = LlmAgent(
    name="InitialWriterAgent",
    model=GEMINI_MODEL,
    include_contents='none',
    # MODIFIED Instruction: Ask for a slightly more developed start
    instruction=f"""You are a Creative Writing Assistant tasked with starting a story.
    Write the *first draft* of a short story (aim for 2-4 sentences).
    Base the content *only* on the topic provided below. Try to introduce a specific element (like a character, a setting detail, or a starting action) to make it engaging.
    Topic: {{initial_topic}}

    Output *only* the story/document text. Do not add introductions or explanations.
""",
    description="Writes the initial document draft based on the topic, aiming for some initial substance.",
    output_key=STATE_CURRENT_DOC
)

# STEP 2a: Critic Agent (Inside the Refinement Loop)
critic_agent_in_loop = LlmAgent(
    name="CriticAgent",
    model=GEMINI_MODEL,
    include_contents='none',
    # MODIFIED Instruction: More nuanced completion criteria, look for clear improvement paths.
    instruction=f"""You are a Constructive Critic AI reviewing a short document draft (typically 2-6 sentences). Your goal is balanced feedback.

    **Document to Review:**
    ```
    {{current_document}}
    ```

    **Task:**
    Review the document for clarity, engagement, and basic coherence according to the initial topic (if known).

    IF you identify 1-2 *clear and actionable* ways the document could be improved to better capture the topic or enhance reader engagement (e.g., "Needs a stronger opening sentence", "Clarify the character's goal"):
    Provide these specific suggestions concisely. Output *only* the critique text.

    ELSE IF the document is coherent, addresses the topic adequately for its length, and has no glaring errors or obvious omissions:
    Respond *exactly* with the phrase "{COMPLETION_PHRASE}" and nothing else. It doesn't need to be perfect, just functionally complete for this stage. Avoid suggesting purely subjective stylistic preferences if the core is sound.

    Do not add explanations. Output only the critique OR the exact completion phrase.
""",
    description="Reviews the current draft, providing critique if clear improvements are needed, otherwise signals completion.",
    output_key=STATE_CRITICISM
)


# STEP 2b: Refiner/Exiter Agent (Inside the Refinement Loop)
refiner_agent_in_loop = LlmAgent(
    name="RefinerAgent",
    model=GEMINI_MODEL,
    # Relies solely on state via placeholders
    include_contents='none',
    instruction=f"""You are a Creative Writing Assistant refining a document based on feedback OR exiting the process.
    **Current Document:**
    ```
    {{current_document}}
    ```
    **Critique/Suggestions:**
    {{criticism}}

    **Task:**
    Analyze the 'Critique/Suggestions'.
    IF the critique is *exactly* "{COMPLETION_PHRASE}":
    You MUST call the 'exit_loop' function. Do not output any text.
    ELSE (the critique contains actionable feedback):
    Carefully apply the suggestions to improve the 'Current Document'. Output *only* the refined document text.

    Do not add explanations. Either output the refined document OR call the exit_loop function.
""",
    description="Refines the document based on critique, or calls exit_loop if critique indicates completion.",
    tools=[exit_loop], # Provide the exit_loop tool
    output_key=STATE_CURRENT_DOC # Overwrites state['current_document'] with the refined version
)


# STEP 2: Refinement Loop Agent
refinement_loop = LoopAgent(
    name="RefinementLoop",
    # Agent order is crucial: Critique first, then Refine/Exit
    sub_agents=[
        critic_agent_in_loop,
        refiner_agent_in_loop,
    ],
    max_iterations=5 # Limit loops
)

# STEP 3: Overall Sequential Pipeline
# For ADK tools compatibility, the root agent must be named `root_agent`
root_agent = SequentialAgent(
    name="IterativeWritingPipeline",
    sub_agents=[
        initial_writer_agent, # Run first to create initial doc
        refinement_loop       # Then run the critique/refine loop
    ],
    description="Writes an initial document and then iteratively refines it with critique using an exit tool."
)
import static com.google.adk.agents.LlmAgent.IncludeContents.NONE;

import com.google.adk.agents.LlmAgent;
import com.google.adk.agents.LoopAgent;
import com.google.adk.agents.SequentialAgent;
import com.google.adk.events.Event;
import com.google.adk.runner.InMemoryRunner;
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.genai.types.Content;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Flowable;
import java.util.Map;

public class LoopAgentExample {

  // --- Constants ---
  private static final String APP_NAME = "IterativeWritingPipeline";
  private static final String USER_ID = "test_user_456";
  private static final String MODEL_NAME = "gemini-2.0-flash";

  // --- State Keys ---
  private static final String STATE_CURRENT_DOC = "current_document";
  private static final String STATE_CRITICISM = "criticism";

  public static void main(String[] args) {
    LoopAgentExample loopAgentExample = new LoopAgentExample();
    loopAgentExample.runAgent("Write a document about a cat");
  }

  // --- Tool Definition ---
  @Schema(
      description =
          "Call this function ONLY when the critique indicates no further changes are needed,"
              + " signaling the iterative process should end.")
  public static Map<String, Object> exitLoop(@Schema(name = "toolContext") ToolContext toolContext) {
    System.out.printf("[Tool Call] exitLoop triggered by %s \n", toolContext.agentName());
    toolContext.actions().setEscalate(true);
    //  Return empty dict as tools should typically return JSON-serializable output
    return Map.of();
  }

  // --- Agent Definitions ---
  public void runAgent(String prompt) {
    // STEP 1: Initial Writer Agent (Runs ONCE at the beginning)
    LlmAgent initialWriterAgent =
        LlmAgent.builder()
            .model(MODEL_NAME)
            .name("InitialWriterAgent")
            .description(
                "Writes the initial document draft based on the topic, aiming for some initial"
                    + " substance.")
            .instruction(
                """
                    You are a Creative Writing Assistant tasked with starting a story.
                    Write the *first draft* of a short story (aim for 2-4 sentences).
                    Base the content *only* on the topic provided below. Try to introduce a specific element (like a character, a setting detail, or a starting action) to make it engaging.

                    Output *only* the story/document text. Do not add introductions or explanations.
                """)
            .outputKey(STATE_CURRENT_DOC)
            .includeContents(NONE)
            .build();

    // STEP 2a: Critic Agent (Inside the Refinement Loop)
    LlmAgent criticAgentInLoop =
        LlmAgent.builder()
            .model(MODEL_NAME)
            .name("CriticAgent")
            .description(
                "Reviews the current draft, providing critique if clear improvements are needed,"
                    + " otherwise signals completion.")
            .instruction(
                """
                    You are a Constructive Critic AI reviewing a short document draft (typically 2-6 sentences). Your goal is balanced feedback.

                    **Document to Review:**
                    ```
                    {{current_document}}
                    ```

                    **Task:**
                    Review the document for clarity, engagement, and basic coherence according to the initial topic (if known).

                    IF you identify 1-2 *clear and actionable* ways the document could be improved to better capture the topic or enhance reader engagement (e.g., "Needs a stronger opening sentence", "Clarify the character's goal"):
                    Provide these specific suggestions concisely. Output *only* the critique text.

                    ELSE IF the document is coherent, addresses the topic adequately for its length, and has no glaring errors or obvious omissions:
                    Respond *exactly* with the phrase "No major issues found." and nothing else. It doesn't need to be perfect, just functionally complete for this stage. Avoid suggesting purely subjective stylistic preferences if the core is sound.

                    Do not add explanations. Output only the critique OR the exact completion phrase.
                    """)
            .outputKey(STATE_CRITICISM)
            .includeContents(NONE)
            .build();

    // STEP 2b: Refiner/Exiter Agent (Inside the Refinement Loop)
    LlmAgent refinerAgentInLoop =
        LlmAgent.builder()
            .model(MODEL_NAME)
            .name("RefinerAgent")
            .description(
                "Refines the document based on critique, or calls exitLoop if critique indicates"
                    + " completion.")
            .instruction(
                """
                    You are a Creative Writing Assistant refining a document based on feedback OR exiting the process.
                    **Current Document:**
                    ```
                    {{current_document}}
                    ```
                    **Critique/Suggestions:**
                    {{criticism}}

                    **Task:**
                    Analyze the 'Critique/Suggestions'.
                    IF the critique is *exactly* "No major issues found.":
                    You MUST call the 'exitLoop' function. Do not output any text.
                    ELSE (the critique contains actionable feedback):
                    Carefully apply the suggestions to improve the 'Current Document'. Output *only* the refined document text.

                    Do not add explanations. Either output the refined document OR call the exitLoop function.
                """)
            .outputKey(STATE_CURRENT_DOC)
            .includeContents(NONE)
            .tools(FunctionTool.create(LoopAgentExample.class, "exitLoop"))
            .build();

    // STEP 2: Refinement Loop Agent
    LoopAgent refinementLoop =
        LoopAgent.builder()
            .name("RefinementLoop")
            .description("Repeatedly refines the document with critique and then exits.")
            .subAgents(criticAgentInLoop, refinerAgentInLoop)
            .maxIterations(5)
            .build();

    // STEP 3: Overall Sequential Pipeline
    SequentialAgent iterativeWriterAgent =
        SequentialAgent.builder()
            .name(APP_NAME)
            .description(
                "Writes an initial document and then iteratively refines it with critique using an"
                    + " exit tool.")
            .subAgents(initialWriterAgent, refinementLoop)
            .build();

    // Create an InMemoryRunner
    InMemoryRunner runner = new InMemoryRunner(iterativeWriterAgent, APP_NAME);
    // InMemoryRunner automatically creates a session service. Create a session using the service
    Session session = runner.sessionService().createSession(APP_NAME, USER_ID).blockingGet();
    Content userMessage = Content.fromParts(Part.fromText(prompt));

    // Run the agent
    Flowable<Event> eventStream = runner.runAsync(USER_ID, session.id(), userMessage);

    // Stream event response
    eventStream.blockingForEach(
        event -> {
          if (event.finalResponse()) {
            System.out.println(event.stringifyContent());
          }
        });
  }
}