logo
홈블로그소개
4,160

Built with Next.js, Bun, Tailwind CSS and Shadcn/UI

·개인정보처리방침
AIPython

Google ADK : Loop Agent · 평가 · API 서버 · SSE · Vertex AI 배포

Toma
2026년 6월 30일
약 33분
목차
🔁 Loop Agent — 반복 실행되는 워크플로우 에이전트
🛑 Loop를 종료시키는 2가지 방법
📊 Agent Evaluations — 비결정적 시스템을 테스트하는 법
🌐 API Server — 웹 서버로 에이전트 실행하기
📡 Server-Sent Events(SSE) — 스트리밍 응답 받기
🔄 Invocation Flow & Runner — 코드만으로 에이전트 실행하기
🗺️ ADK 내부 동작 흐름
💻 Runner로 직접 실행하기
☁️ Deployment to Vertex AI — 실제 서비스로 배포하기
📋 배포 준비 단계
🔗 배포된 에이전트와 상호작용하기
🧭 정리
이전 포스트Google ADK 멀티 에이전트: Workflow Agent · 구조화 출력 · Callback으로 만드는 YouTube Shorts 메이커

목차

🔁 Loop Agent — 반복 실행되는 워크플로우 에이전트
🛑 Loop를 종료시키는 2가지 방법
📊 Agent Evaluations — 비결정적 시스템을 테스트하는 법
🌐 API Server — 웹 서버로 에이전트 실행하기
📡 Server-Sent Events(SSE) — 스트리밍 응답 받기
🔄 Invocation Flow & Runner — 코드만으로 에이전트 실행하기
🗺️ ADK 내부 동작 흐름
💻 Runner로 직접 실행하기
☁️ Deployment to Vertex AI — 실제 서비스로 배포하기
📋 배포 준비 단계
🔗 배포된 에이전트와 상호작용하기
🧭 정리

🧭 이 글은 Google **ADK(Agent Development Kit)**로 Email Refiner(Loop Agent)와 Travel Advisor 에이전트를 만들며 익힌 개념 — Loop Agent 종료 제어, Agent Evaluations, API 서버, SSE(Server-Sent Events), Invocation Flow & Runner, Vertex AI 배포 — 를 정리한 기록입니다.


🔁 Loop Agent — 반복 실행되는 워크플로우 에이전트

💡 Workflow Agent는 LLM이 아니다. Loop·Sequential·Parallel Agent 모두 AI 모델의 판단 없이 서브에이전트의 실행 순서를 결정론적(deterministic)으로 제어하는 함수에 가깝다.

✅ LoopAgent는 서브에이전트 목록을 순서대로 하나씩 순차 실행한다. 모든 서브에이전트가 동시에 작동하는 것이 아니라, AI 모델의 판단 없이 정해진 순서대로 실행된다는 것이 핵심이다.

Email Refiner 프로젝트는 이메일 초안을 5개의 서브 에이전트가 돌아가며 다듬는 구조다. LoopAgent에 서브 에이전트들을 순서대로 배치하면(Sequential Agent와 마찬가지로 배치 순서가 실행 순서), 매 반복마다 clarity → tone → persuasion → synthesis → critic 순으로 이메일을 개선한다.

python
# email-refiner/agent.py
from google.adk.agents import Agent, LoopAgent

email_refiner_agent = LoopAgent(
    name="Email Refiner Agent",
    description=EMAIL_OPTIMIZER_DESCRIPTION,
    sub_agents=[
        clarity_agent,
        tone_stylist_agent,
        persuation_agent,
        email_synthesizer_agent,
        literary_critic_agent,
    ],
)

root_agent = email_refiner_agent

🛑 Loop를 종료시키는 2가지 방법

Loop Agent는 무한히 돌 수 없으므로 종료 조건이 반드시 필요하다.

  1. 최대 반복 횟수 지정 — max_iterations
  2. 특정 조건 충족 시 tool로 종료 — tool_context.actions.escalate

2번 조건을 이용하기 위해서 tool이 필요하다. 어떤 tool이든 tool_context.actions.escalate(점진적으로 확대되다) 가 True 이면 Loop가 종료된다.

python
# escalate라고 불리는 이유는 이게 더 상위 단계의 agent로 넘어갈 수 있기 때문.
# 정리하면 LoopAgent 내부에서 escalate를 쓰면 Loop가 종료. 만약 상위의 분류 에이전트가 있고 하위 에이전트가 많이 있는 계층적인 구조에서
# escalate를 사용하면 하위 에이전트에서 escalate를 사용해서 대화의 주도권을 상위 에이전트로 보냄.
async def escalate_email_complete(tool_context: ToolContext):
    """Use this tool only when the email is good to go."""
    tool_context.actions.escalate = True
    return "Email optimization complete."

escalate_email_complete tool은 마지막 서브 에이전트인 literary_critic_agent에만 등록되어, "이메일이 충분히 좋아졌다"고 판단될 때만 호출되어 Loop를 종료시킨다.

python
# email-refiner/agent.py
email_refiner_agent = LoopAgent(
    name="EmailRefinerAgent",
    # 50 이상 loop를 돌 수 없음.
    max_iterations=50,
    description=EMAIL_OPTIMIZER_DESCRIPTION,
    sub_agents=[
        clarity_agent,
        tone_stylist_agent,
        persuation_agent,
        email_synthesizer_agent,
        literary_critic_agent,
    ],
)

✅ 정리 — max_iterations는 안전장치(하드 리밋), escalate는 조건 충족 시 능동적 종료다. 공식 문서도 "setEscalate(true)는 LoopAgent를 멈추고 제어권을 부모에게 넘기는 기본 메커니즘"이라고 명시한다. 실전에서는 두 가지를 함께 걸어 무한 루프를 방지하면서도 품질 기준을 만족하면 더 빨리 끝내는 것이 안전하다.


📊 Agent Evaluations — 비결정적 시스템을 테스트하는 법

⚠️ agent를 테스트·평가하는 것은 테스트 코드를 작성하는 TDD(Test Driven Development)와는 다른 결이다. Agent, AI 모델은 비결정적(non-deterministic) 특성을 가져서 동일한 input을 넣어도 항상 같은 output이 나오지 않기 때문이다.

Google ADK로 agent를 평가할 때는 2가지 축으로 나눠서 본다.

평가 축확인 내용
Tool Trajectory(구도)어떤 tool을 호출하는지, 순서대로 호출하는지
최종 응답 일관성tool 호출 순서보다, 모든 tool을 사용한 후 최종 응답이 일관성 있는지

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-06-29_21.18.42.png

Google ADK Web의 Eval 탭에서 Eval Set(폴더)을 하나 만들고, 그 안에 여러 Eval Session을 추가할 수 있다.

세션을 Eval Set에 추가하고 Run Evaluation을 실행하면 ADK가 새로운 세션을 만들어 이전 세션과 비교하고 설정한 평가지표에 따라 Pass/Fail 같은 종합 결과를 보여준다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-06-29_21.21.43.png

💬 기준이 될 세션에서 agent 응답을 받고 → Eval Set에 추가 → Run Evaluation 실행 → ADK가 새 세션을 만들어 비교 → 평가지표에 따른 Pass/Fail 표시, 순서로 동작한다.


🌐 API Server — 웹 서버로 에이전트 실행하기

API 웹 서버를 통한 상호작용은 실제 운영 환경에서도 웹서버로 배포해서 사용할 수 있는 방식이다. adk api_server를 실행하면 http://127.0.0.1:8000에서 서버가 시작되고, 여기에 /docs를 추가하면 Swagger 문서를 확인할 수 있다.

먼저 애플리케이션에 있는 모든 app을 가져오기 위해 /list-apps API를 호출하고 세션을 만든 뒤 /run 혹은 /run_sse로 에이전트를 실행한다.

python
import requests

BASE_URL = "http://127.0.0.1:8000"
APP_NAME = "travel_advisor_agent"
USER_ID = "u_123"
SESSOIN_ID = "25745dc0-e59d-487e-a53c-ba39814870bb"

# adk web 실행 시 session이 자동으로 생성되었었는데,
# 이를 API로 동일하게 구현하기 위해선 /apps/{app_name}/users/{user_id}/sessions 로 POST 요청
# -> session id 반환
response = requests.post(f"{BASE_URL}/apps/{APP_NAME}/users/{USER_ID}/sessions")
# {'id': '663fd92c-aced-4b8f-8cb7-32501ab0bf20', 'appName': 'travel_advisor_agent', 'userId': 'u_123', 'state': {}, 'events': [], 'lastUpdateTime': 1782737032.916954}
print(response.json())

# 다음으로는 run으로 agent 실행. (/run, /run_sse)
# 2가지 옵션이 있는데, 하나는 run이고 다른 하나는 run_sse임.

💡 SSE (Server Sent Event)
HTTP는 상태가 없다. 무슨 말이냐면, 내가 서버에 요청을 보내면 연결이 되고, 응답을 받은 뒤에 연결이 끊어지면 서버는 나를 잊어버린다. 그래서 쿠키나 토큰과 같은 것들로 내가 서버에 요청을 보낼 때마다 서버가 준 쿠키를 같이 보냄으로서 서버에게 나를 알려준다.

그러나 SSE는 서버가 실제로 연결 자체를 끊지 않고 계속 유지함으로서 나에게 이벤트를 보내주므로 일반적으로 agent의 업데이트를 알려주는데 사용된다.

/run으로 agent를 실행할 때는 appName, userId, sessionId와 함께 사용자 메세지를 담은 newMessage를 전달한다.

python
message = {
    "appName": APP_NAME,
    "userId": USER_ID,
    "sessionId": SESSOIN_ID,
    "newMessage": {
        "parts": [
            {"text": "I want to go to Tokyo"}
        ],
        "role": "user"
    }
}

response = requests.post(f"{BASE_URL}/run", json=message)

for event in response.json():
    content = event.get("content")
    parts = content.get("parts")
    for part in parts:
        function_call = part.get("functionCall", None)
        if function_call:
            print(function_call.get("name"))
        text = part.get("text", None)
        if text:
            print(text)

📡 Server-Sent Events(SSE) — 스트리밍 응답 받기

SSE를 다루려면 요청 경로를 /run_sse로 바꾸고 stream=True로 요청한다. 클라이언트 라이브러리로 uv add sseclient-py를 설치한 뒤 SSEClient를 생성해 서버가 보내는 이벤트에 반복문을 돌린다.

python
import sseclient
import json

response = requests.post(f"{BASE_URL}/run_sse", json=message, stream=True)

# SSEClient 생성
client = sseclient.SSEClient(response)

# 서버가 보내는 이벤트에 반복문
for event in client.events():
    data = json.loads(event.data)
    content = data.get("content")
    parts = data.get("parts")
    for part in parts:
        function_call = part.get("functionCall", None)
        if function_call:
            print(function_call.get("name"))
        text = part.get("text", None)
        if text:
            print(text)

message에 "streaming": True를 추가하면 adk web에서 Streaming 옵션을 켠 것과 동일하게 응답이 실시간으로 스트리밍된다.

python
# run으로 agent 실행 시 appName, userId, sessionId 등 아래와 같은 값이 필요.
message = {
    "appName": APP_NAME,
    "userId": USER_ID,
    "sessionId": SESSOIN_ID,
    "newMessage": {
        "parts": [
            {"text": "I want to go to Tokyo"}
        ],
        "role": "user"
    },
    "streaming": True
}

실행 결과, 모델이 get_weather → get_local_attractions → get_exchange_rate 순으로 tool을 호출한 뒤 최종 응답을 스트리밍으로 조립해서 보여준다 — 도쿄 날씨, 인기 명소, 환율 정보를 종합한 답변이 이벤트 단위로 순차 도착한다.


🔄 Invocation Flow & Runner — 코드만으로 에이전트 실행하기

별도 서버 없이 코드만으로 agent를 실행하는 방법도 있다. 이미 존재하는 서비스에 에이전트를 통합할 때 유용하다. Agent만을 위해 서버 전체를 할당하고 싶다면 ADK API 서버를 쓰면 되지만, 기존 서버·서비스 안에 agent를 끼워 넣고 싶다면 코드 레벨에서 직접 실행한다.

invocation-flow.png

🗺️ ADK 내부 동작 흐름

  1. 유저가 Runner에게 쿼리를 보냄
  2. Runner가 SessionService로부터 세션을 load
  3. Runner가 사용자 쿼리 Event를 History(SessionService)에 추가
  4. SessionService가 Event가 잘 추가되었다고 응답
  5. Runner가 agent를 실행
  6. Agent가 tool 필요 여부를 판단해 LLM에게 질문 + 사용 가능한 tool 목록 전달
  7. LLM이 FunctionCall로 응답(tool 실행 여부를 묻는 응답)
  8. Agent가 이 FunctionCall을 Event로 감싸 yield하고, 그 자리에서 **실행을 일시 정지(pause)**한다
  9. Runner가 이 FunctionCall event를 받아 SessionService에 기록
  10. UI로 유저에게 표시
  11. Runner가 기록·커밋 완료 신호를 보내면 Agent가 재개(resume)되어 직접 Tool을 실행한다 (tool.run_async(...))
  12. Tool 실행 결과가 Event로 감싸져 Runner에게 전달
  13. Runner가 이 이벤트를 SessionService에 기록
  14. Agent가 tool 결과를 LLM에게 전달
  15. LLM이 최종 결과를 응답, 이 또한 Event로 감싸짐
  16. Runner가 Session에 기록
  17. 최종 응답이 유저에게 전달됨

✅ FunctionCall 이후 흔히 헷갈리는 지점은 Tool을 실행하는 주체가 누구인가? 이다. 공식 문서는 "Execution of the agent logic pauses immediately after the yield statement... It waits for the Runner to complete step 3(processing and committing)"라고 명시한다. 즉, Tool을 실행하는 주체는 Runner가 아니라 Agent다. Runner는 event를 기록하고 "처리 완료" 신호만 보내며, 그 신호를 받은 Agent가 재개되어 직접 tool을 호출한다.

💬 요약하면: 유저 쿼리 → session 저장 → Runner가 agent 실행 → agent가 LLM 호출 → LLM이 FunctionCall 응답 → Agent가 event를 yield하고 일시정지 → Runner가 기록 · 재개 신호 → Agent가 재개되어 tool 실행 → tool 결과가 event로 Runner에 전달 → session 저장 → LLM에 결과 재전달 → 최종 응답도 event로 Runner에 전달·저장, 이 과정이 반복(for loop)된다.

💻 Runner로 직접 실행하기

Runner는 기본적으로 agent를 호출하고 agent로부터 event를 받는다.

Session을 아래와 같이 생성하면 state와 함께 미리 preload 할 수 있다.

일반적으로 state 조작은 tool을 통해 tool_context로 state를 조작하거나 agent에게 output_key 설정하는 방법이 있는데, 이러한 방법들을 활용하더라도 state를 preload 할 수 있다는 내용은 따로 없다.

그러나 session에 state가 포함되어 있으므로 이를 활용하면 agent를 실행할 때마다 state를 미리 준비할 수 있고 또, 이를 prompt에 미리 가져다가 사용할 수도 있다.

python
from google.adk.agents import Agent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools.tool_context import ToolContext

from google.adk.sessions import DatabaseSessionService  # OpenAI Agent SDK에 있는 SQLiteSession과 비슷한 역할.
from google.adk.runners import Runner
from google.genai import types

# SessionService 생성
session_service = DatabaseSessionService(db_url="sqlite+aiosqlite:///./session.db")

# session 생성
session = await session_service.create_session(
    app_name="weather_agent",
    user_id="u_123",
    state={
        "user_name": "toma"
    }
)

MODEL = LiteLlm(model="openai/gpt-4o")


async def get_weather(tool_context: ToolContext, location: str):
 ...

async def get_exchange_rate(
    tool_context: ToolContext, from_currency: str, to_currency: str, amount: float
):
 ...


travel_advisor_agent = Agent(
 ...
)

# Runner 생성 -> agent 전달
runner = Runner(
    agent=travel_advisor_agent,
    session_service=session_service,
    app_name="weather_agent"
)

# ADK의 메세지 형식(규칙)
message = types.Content(role="user", parts=[types.Part(text="I'm going to Vietnam, tell me all about it.")])

# Runner 실행
async for event in runner.run_async(user_id="u_123", session_id=session.id, new_message=message):
    if event.is_final_response():
        print(event.content)
    else:
        # Event 안에서 FunctionCall을 찾을 필요 없이 이 메서드들을 호출하면 됨.
        print(event.get_function_calls())
        print(event.get_function_responses())

💡 세션 state를 미리 채워두기(preload) — create_session에 state={"user_name": "toma"}처럼 초기값을 넣으면, agent가 실행될 때마다 이 state가 미리 로드되어 prompt에서 바로 참조할 수 있다. tool에서 tool_context.state로 수정하거나 agent의 output_key로 채우는 것과 달리, 세션 생성 시점에 초기 state를 주입하는 세 번째 방법이다.

✅ event 헬퍼 메서드 — event 안에서 FunctionCall을 직접 파싱할 필요 없이 event.get_function_calls(), event.get_function_responses()를 호출하면 된다.


☁️ Deployment to Vertex AI — 실제 서비스로 배포하기

📋 배포 준비 단계

  1. Google Cloud CLI 설치 (MacOS는 HomeBrew 이용)
  2. gcloud auth application-default login 실행
  3. Google Cloud Console에서 New Storage Bucket 생성
  4. uv add "google-cloud-aiplatform[adk, agent_engines]" cloudpickle
  5. Google Cloud Console에서 Vertex AI API 활성화
  6. deploy.py 작성
python
# deploy.py
import dotenv

dotenv.load_dotenv()

import os

import vertexai
import vertexai.agent_engines
from vertexai.preview import reasoning_engines

from travel_advisor_agent.agent import travel_advisor_agent

PROJECT_ID = "프로젝트 ID"
LOCATION = "AI 엔진 지원 지역"
BUCKET = "gs://내 버킷이름"

# vertexai 초기화
vertexai.init(
    project=PROJECT_ID,
    location=LOCATION,
    staging_bucket=BUCKET,
)

app = reasoning_engines.AdkApp(
    agent=travel_advisor_agent,
    enable_tracing=True,
)

# 구글 클라우드 서비스인 agent engines 안에 agent 생성
remote_app = vertexai.agent_engines.create(
    display_name="Travel Advisor Agent",
    agent_engine=app,
    # 필요한 의존성
    requirements=[
        "google-cloud-aiplatform[adk,agent_engines]",
        "litellm",
    ],
    # 에이전트를 실행하는데 필요한 모든 폴더
    extra_packages=["travel_advisor_agent"],
    env_vars={
        "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"),
    },
)

uv run deploy.py를 실행하면 완료 후 Google Cloud Console에서 배포된 에이전트를 확인할 수 있다.

🔗 배포된 에이전트와 상호작용하기

에이전트를 배포했다면, 유저가 직접 상호작용 하는게 아니라 중재자(remote 스크립트)를 통해 상호작용하도록 만들어야 한다.

python
# remote.py
import vertexai
from vertexai import agent_engines

PROJECT_ID = "프로젝트 ID"
LOCATION = "지역"

vertexai.init(
    project=PROJECT_ID,
    location=LOCATION,
)

# 배포된 것을 가져옴
# deployments = agent_engines.list()

# for deployment in deployments:
#     print(deployment)

DEPLOYMENT_ID = "위 스크립트 실행 시 표시되는 ID"

SESSION_ID = "생성한 세션의 ID"

# 연결
remote_app = agent_engines.get(DEPLOYMENT_ID)

# 필수! 에이전트를 삭제하지 않으면 과금이 발생함!!!
remote_app.delete(force=True)


# 세션 생성
# # remote_session = remote_app.create_session(user_id="u_123")

# # print(remote_session["id"])

# 유저-중재자-에이전트 간 상호작용

# for event in remote_app.stream_query(
#     user_id="u_123",
#     session_id=SESSION_ID,
#     message="I'm going to Laos, any tips?",
# ):
#     print(event, "\n", "=" * 50)

⚠️ 불필요 과금 주의 — 소스 주석에 그대로 적혀있듯, remote_app.delete(force=True)로 배포된 에이전트를 삭제하지 않으면 계속 과금된다. Vertex AI Agent Engine은 배포 상태 자체로 비용이 발생하므로, 테스트가 끝나면 반드시 삭제할 것.


🧭 정리

📝 Email Refiner(Loop Agent)와 Travel Advisor(API/SSE/Runner/배포) 두 프로젝트를 만들며 익힌 ADK 핵심 개념을 한눈에.

개념핵심 요약
Loop Agent서브에이전트를 순서대로 반복 실행(동시 실행 X). max_iterations(하드 리밋) + escalate(조건부 종료)로 제어
Agent Evaluations비결정적 특성 때문에 TDD와 다른 접근 필요. Tool Trajectory + 최종 응답 일관성 2축 평가
API Serveradk api_server로 FastAPI 서버 실행. /run, /run_sse로 세션 기반 에이전트 호출
SSE연결을 끊지 않고 서버가 이벤트를 push. stream=True • sseclient로 스트리밍 응답 수신
Invocation Flow & RunnerAgent가 event를 yield하고 pause, Runner가 기록·재개 신호를 보내면 Agent가 직접 tool을 실행. Runner는 오케스트레이션·기록 담당, Agent가 실행 주체
Vertex AI 배포reasoning_engines.AdkApp • agent_engines.create()로 배포. 사용 후 delete(force=True) 필수(과금 방지)