반응형
지난 편에서 Interactions API의 구조와 브레이킹 체인지를 다뤘습니다. 이번 편은 코드만 남습니다. MCP 서버를 Flash에 연결하고, 멀티툴 체인을 구성하고, 에이전트 루프를 프로덕션에서 돌리는 전 과정입니다.
핵심 요약 → 이전 편 복습: Interactions API = 서버사이드 히스토리, previous_interaction_id 패턴 → 이번 편: MCP 서버 연결 → 멀티툴 체인 → 에이전트 루프 → 비용 최적화 → 에러 핸들링 → Gemini SDK의 MCP 통합: ClientSession을 tools= 파라미터에 전달하면 자동 루프 실행 → Flash MCP Atlas 83.6% = 6번에 1번은 도구 호출 실패 — 재시도 로직 필수 → thinking_level: Low가 에이전트 루프 최적 (빠르고 싸면서 SWE 성능 Medium 수준) → 멀티툴 동시 사용: Google Search + 커스텀 MCP + File Search 조합 실전 코드 포함
사전 준비
# SDK 설치 (2.0.0 이상 필수)
pip install -U google-genai
# MCP SDK 설치
pip install mcp fastmcp
# 환경 변수
export GEMINI_API_KEY="your_api_key"
1. 가장 단순한 MCP 연결 — 원격 MCP 서버
import google.generativeai as genai
import datetime
client = genai.Client()
MODEL = "gemini-3.5-flash"
# ── 원격 MCP 서버 연결 (가장 간단한 형태) ──
def simple_mcp_agent(user_query: str) -> str:
"""
원격 MCP 서버를 도구로 사용하는 기본 에이전트
"""
mcp_server = {
"type": "mcp_server",
"name": "weather_service",
"url": "https://your-mcp-server.example.com/mcp"
# stdio, SSE, HTTP 모두 지원
}
today = datetime.date.today().strftime("%Y년 %m월 %d일")
interaction = client.interactions.create(
model=MODEL,
input=user_query,
tools=[mcp_server],
config={
"thinking_level": "low", # 에이전트 루프 최적값
"system_instruction": f"오늘 날짜는 {today}입니다."
}
# store=True 기본값 → 서버가 55일 보관
)
# steps 배열에서 최종 응답 추출 (신규 스키마)
for step in interaction.steps:
if step.type == "model_output":
return step.content[0].text
return ""
# 실행
result = simple_mcp_agent("서울 지금 날씨 어때?")
print(result)
2. 로컬 MCP 서버 — FastMCP로 직접 만들기
# ── 1단계: FastMCP로 커스텀 MCP 서버 정의 ──
from fastmcp import FastMCP
import httpx
mcp = FastMCP("내 서비스 에이전트")
@mcp.tool()
async def search_internal_docs(query: str) -> str:
"""사내 문서 데이터베이스에서 관련 내용을 검색합니다."""
# 실제 구현: 벡터 DB 조회, Elasticsearch 등
response = await httpx.AsyncClient().post(
"http://internal-search.company.com/search",
json={"q": query, "limit": 5}
)
results = response.json()["results"]
return "\n\n".join([r["content"] for r in results])
@mcp.tool()
async def get_customer_info(customer_id: str) -> dict:
"""고객 ID로 고객 정보를 조회합니다."""
# 실제 구현: 내부 DB 조회
response = await httpx.AsyncClient().get(
f"http://internal-api.company.com/customers/{customer_id}"
)
return response.json()
@mcp.tool()
async def create_support_ticket(
customer_id: str,
issue: str,
priority: str = "normal"
) -> dict:
"""고객 지원 티켓을 생성합니다."""
response = await httpx.AsyncClient().post(
"http://internal-api.company.com/tickets",
json={
"customer_id": customer_id,
"issue": issue,
"priority": priority
}
)
return {"ticket_id": response.json()["id"], "status": "created"}
# ── 2단계: FastMCP 클라이언트로 Gemini에 연결 ──
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import google.generativeai as genai
client = genai.Client()
async def agent_with_local_mcp(user_query: str) -> str:
"""로컬 MCP 서버를 사용하는 에이전트"""
# stdio transport로 로컬 MCP 서버 연결
server_params = StdioServerParameters(
command="python",
args=["my_mcp_server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# ClientSession을 tools에 직접 전달
# → SDK가 자동으로 도구 목록 가져오고 루프 실행
interaction = client.interactions.create(
model="gemini-3.5-flash",
input=user_query,
tools=session, # ← ClientSession 직접 전달
config={"thinking_level": "low"}
)
for step in interaction.steps:
if step.type == "model_output":
return step.content[0].text
3. 멀티툴 체인 — Google Search + 커스텀 MCP + File Search
# ── 세 가지 도구를 동시에 사용하는 에이전트 ──
async def multi_tool_agent(user_query: str, session: ClientSession) -> str:
"""
Google Search + 커스텀 MCP + File Search를 동시에 사용
Flash MCP Atlas 83.6% 수치의 실제 활용 패턴
"""
interaction = client.interactions.create(
model="gemini-3.5-flash",
input=user_query,
tools=[
# 도구 1: Google Search (실시간 웹 검색)
{"google_search": {}},
# 도구 2: 커스텀 MCP 서버 (내부 시스템)
session, # ClientSession
# 도구 3: File Search (사내 문서 벡터 검색)
{
"file_search": {
"corpus_id": "company-docs-2026"
}
}
],
config={
"thinking_level": "medium", # 멀티툴은 medium 권장
"system_instruction": """
당신은 고객 지원 에이전트입니다.
1. 사내 문서에서 관련 정책을 먼저 확인하세요
2. 필요시 내부 시스템에서 고객 정보를 조회하세요
3. 최신 정보가 필요하면 웹 검색을 활용하세요
항상 근거를 명시하고 한국어로 답변하세요.
"""
}
)
# steps 전체 출력 (디버깅용)
for step in interaction.steps:
print(f"[{step.type}]", end=" ")
if step.type == "google_search_call":
print(f"검색어: {step.query}")
elif step.type == "function_call":
print(f"함수: {step.name}({step.args})")
elif step.type == "model_output":
print("→ 최종 응답")
return step.content[0].text
else:
print()
return ""
4. 멀티턴 에이전트 루프 — previous_interaction_id 활용
# ── 상태를 유지하는 멀티턴 에이전트 ──
class GeminiMCPAgent:
"""
previous_interaction_id로 컨텍스트를 유지하는 에이전트
히스토리 관리 코드 없음 — 서버가 전담
"""
def __init__(self, mcp_session: ClientSession):
self.client = genai.Client()
self.session = mcp_session
self.previous_id: str | None = None
self.turn_count = 0
def chat(self, user_message: str) -> str:
self.turn_count += 1
interaction = self.client.interactions.create(
model="gemini-3.5-flash",
input=user_message,
previous_interaction_id=self.previous_id, # 컨텍스트 연결
tools=[
self.session,
{"google_search": {}}
],
config={
# 에이전트 루프는 Low가 최적
# → Medium과 SWE 성능 비슷하면서 토큰 45% 절감
"thinking_level": "low",
}
)
# 다음 턴을 위해 ID 저장
self.previous_id = interaction.id
# 최종 응답 추출
for step in interaction.steps:
if step.type == "model_output":
return step.content[0].text
return ""
def reset(self):
"""대화 컨텍스트 초기화"""
self.previous_id = None
self.turn_count = 0
# 사용 예시
async def main():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
agent = GeminiMCPAgent(mcp_session=session)
# 대화 1: 고객 조회
r1 = agent.chat("고객 ID C-12345 정보 조회해줘")
print(f"에이전트: {r1}")
# 대화 2: 이전 컨텍스트 기반 (히스토리 자동 유지)
r2 = agent.chat("이 고객 최근 문의 내역도 보여줘")
print(f"에이전트: {r2}")
# 대화 3: 도구 호출 연계
r3 = agent.chat("이 고객 이름으로 긴급 티켓 하나 만들어줘")
print(f"에이전트: {r3}")
5. 에러 핸들링 — MCP Atlas 83.6%의 현실적 대응
import asyncio
import logging
from typing import Optional
logger = logging.getLogger(__name__)
# MCP Atlas 83.6% = 약 16%의 도구 호출 실패 가능성
# 프로덕션에서는 재시도 + fallback 필수
async def robust_mcp_agent(
user_query: str,
session: ClientSession,
max_retries: int = 3
) -> Optional[str]:
"""
재시도 로직이 포함된 프로덕션 수준 에이전트
"""
last_error = None
for attempt in range(max_retries):
try:
interaction = client.interactions.create(
model="gemini-3.5-flash",
input=user_query,
tools=[session, {"google_search": {}}],
config={"thinking_level": "low"}
)
# 실패 스텝 감지
failed_steps = [
s for s in interaction.steps
if s.type == "function_call" and
getattr(s, "status", None) == "error"
]
if failed_steps:
logger.warning(
f"시도 {attempt+1}: 도구 호출 실패 "
f"({len(failed_steps)}개) — 재시도"
)
# 지수 백오프
await asyncio.sleep(2 ** attempt)
continue
# 성공
for step in interaction.steps:
if step.type == "model_output":
return step.content[0].text
except Exception as e:
last_error = e
logger.error(f"시도 {attempt+1} 오류: {e}")
await asyncio.sleep(2 ** attempt)
# 모든 재시도 실패 → fallback: 도구 없이 응답
logger.warning("모든 재시도 실패 — 도구 없이 응답")
try:
fallback = client.interactions.create(
model="gemini-3.5-flash",
input=f"{user_query}\n(주의: 실시간 데이터 조회 불가, 일반 지식으로만 답변)",
config={"thinking_level": "low"}
)
for step in fallback.steps:
if step.type == "model_output":
return f"[도구 조회 실패, 일반 응답]\n{step.content[0].text}"
except Exception as fe:
logger.error(f"Fallback도 실패: {fe}")
return None
return None
6. 비용 최적화 — thinking_level 라우팅
# ── 태스크 복잡도에 따른 thinking_level 자동 선택 ──
def get_thinking_level(task_type: str) -> str:
"""
태스크 유형별 최적 thinking_level 선택
토큰 비용 비교:
Low → 기준치
Medium → Low 대비 ~2배
High → Low 대비 ~4배
"""
routing = {
# Low: 단순 조회, 빠른 응답 필요
"simple_lookup": "low", # 고객 정보 조회
"classification": "low", # 문의 유형 분류
"data_extraction": "low", # 구조화 데이터 추출
"routing": "low", # 다음 단계 결정
# Medium: 일반 에이전트 루프 (기본값)
"multi_tool": "medium", # 멀티툴 조합
"summarization": "medium", # 문서 요약
"code_review": "medium", # 코드 리뷰
# High: 복잡한 추론이 필요한 경우만
"complex_analysis": "high", # 심층 분석
"hard_coding": "high", # 어려운 알고리즘
"math_reasoning": "high", # 수학적 추론
}
return routing.get(task_type, "medium") # 기본값 medium
async def cost_optimized_agent(
user_query: str,
task_type: str,
session: ClientSession,
previous_id: str | None = None
) -> tuple[str, str]: # (응답, interaction_id)
"""
비용 최적화된 에이전트
Low 설정 시 Medium 대비 토큰 45% 절감
"""
level = get_thinking_level(task_type)
interaction = client.interactions.create(
model="gemini-3.5-flash",
input=user_query,
previous_interaction_id=previous_id,
tools=[session],
config={"thinking_level": level}
)
# 토큰 사용량 모니터링
if hasattr(interaction, "usage"):
logger.info(
f"thinking={level} | "
f"input={interaction.usage.input_tokens} | "
f"output={interaction.usage.output_tokens} | "
f"thoughts={getattr(interaction.usage, 'thought_tokens', 0)}"
)
for step in interaction.steps:
if step.type == "model_output":
return step.content[0].text, interaction.id
return "", interaction.id
7. 스트리밍 + 백그라운드 — 프로덕션 패턴
# ── 스트리밍: 실시간 응답 표시 ──
def streaming_mcp_agent(user_query: str, session: ClientSession):
"""
토큰이 생성되는 즉시 출력
인터랙티브 UI에 적합
"""
for event in client.interactions.create(
model="gemini-3.5-flash",
input=user_query,
tools=[session, {"google_search": {}}],
config={"thinking_level": "low"},
stream=True
):
if event.type == "step.delta":
if event.delta.type == "model_output":
# 토큰 단위로 즉시 출력
print(event.delta.text, end="", flush=True)
elif event.type == "step.complete":
if event.step.type == "function_call":
print(f"\n[도구 호출: {event.step.name}]")
print() # 줄바꿈
# ── 백그라운드: 장시간 실행 에이전트 ──
import time
async def background_analysis_agent(
large_dataset_query: str,
webhook_url: str | None = None
) -> str:
"""
수 분 이상 걸리는 태스크를 비동기로 실행
Webhook URL 설정 시 완료 후 콜백
"""
# 시작 (즉시 반환)
interaction = client.interactions.create(
model="gemini-3.5-flash",
input=large_dataset_query,
config={
"thinking_level": "high",
"background": True, # 비동기 실행
}
# webhook_url=webhook_url # 완료 시 POST 콜백
)
print(f"분석 시작됨 (ID: {interaction.id})")
print("백그라운드에서 실행 중...")
# 폴링 (Webhook 없는 경우)
while True:
updated = client.interactions.get(interaction.id)
if updated.status == "completed":
for step in updated.steps:
if step.type == "model_output":
return step.content[0].text
break
elif updated.status in ["failed", "cancelled"]:
raise Exception(f"에이전트 실패: {updated.status}")
print(f" 진행 중... ({updated.status})")
await asyncio.sleep(10) # 10초 간격 폴링
return ""
8. 실전 완성 예제 — 고객 지원 에이전트
# ── 모든 패턴을 통합한 실전 고객 지원 에이전트 ──
from fastmcp import FastMCP
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import google.generativeai as genai
import asyncio
# MCP 서버 정의
support_mcp = FastMCP("고객지원 서비스")
@support_mcp.tool()
async def get_customer(customer_id: str) -> dict:
"""고객 정보 조회"""
# 실제 DB 조회
return {"id": customer_id, "name": "홍길동", "plan": "Premium"}
@support_mcp.tool()
async def get_order_history(customer_id: str, limit: int = 5) -> list:
"""주문 이력 조회"""
return [{"order_id": f"ORD-{i}", "status": "완료"} for i in range(limit)]
@support_mcp.tool()
async def create_ticket(customer_id: str, issue: str, priority: str) -> dict:
"""지원 티켓 생성"""
return {"ticket_id": "TKT-99999", "status": "접수", "priority": priority}
# 에이전트 메인
async def customer_support_agent():
server_params = StdioServerParameters(
command="python", args=["support_mcp_server.py"]
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
gemini = genai.Client()
previous_id = None
print("고객 지원 에이전트 시작 (종료: 'q')")
print("-" * 50)
while True:
user_input = input("고객: ").strip()
if user_input.lower() == 'q':
break
# 태스크 분류 기반 thinking_level 선택
level = "low" if len(user_input) < 50 else "medium"
try:
interaction = gemini.interactions.create(
model="gemini-3.5-flash",
input=user_input,
previous_interaction_id=previous_id,
tools=[
session, # 내부 MCP 도구
{"google_search": {}}, # 외부 검색
],
config={
"thinking_level": level,
"system_instruction": """
당신은 친절한 고객 지원 담당자입니다.
- 고객 정보 조회 시 항상 ID를 먼저 확인하세요
- 문제 해결 후 만족도를 확인하세요
- 긴급한 경우 우선순위 '긴급'으로 티켓을 생성하세요
"""
}
)
previous_id = interaction.id
# 도구 호출 과정 출력
for step in interaction.steps:
if step.type == "function_call":
print(f" [내부 조회: {step.name}]")
elif step.type == "google_search_call":
print(f" [웹 검색: {step.query}]")
elif step.type == "model_output":
print(f"상담원: {step.content[0].text}")
except Exception as e:
print(f"오류 발생: {e} — 재시도하세요")
if __name__ == "__main__":
asyncio.run(customer_support_agent())
핵심 패턴 요약
# 언제 어떤 패턴을 쓰나
단순 단건 도구 호출
→ simple_mcp_agent() — MCP 서버 URL 하나
멀티툴 복합 조회
→ multi_tool_agent() — Search + MCP + File Search
대화 유지가 필요한 에이전트
→ GeminiMCPAgent 클래스 — previous_interaction_id 관리
프로덕션 안정성 필요
→ robust_mcp_agent() — 재시도 + fallback
실시간 응답 필요 (UI)
→ streaming_mcp_agent() — stream=True
수 분 이상 걸리는 분석
→ background_analysis_agent() — background=True
비용 최적화
→ cost_optimized_agent() — 태스크별 thinking_level 라우팅
결론
✅ Flash + Interactions API + MCP 조합이 강한 이유
- previous_interaction_id → 히스토리 코드 제거, 멀티턴 비용 절감
- MCP 자동 루프 → ClientSession만 넘기면 SDK가 도구 탐색·실행 자동 처리
- thinking_level Low → 에이전트 루프에서 Medium과 SWE 성능 비슷하면서 45% 저렴
- 289 tokens/sec → 루프 빠르게 돌면서 UX 체감
❌ 잊으면 안 되는 것
- MCP Atlas 83.6% = 16% 실패 가능성 → 재시도 로직 필수
- store=True 기본값 → 민감 데이터는 store=False 명시
- Remote MCP 아직 미지원 → stdio/SSE 로컬 서버 방식 사용
- 6월 8일 outputs → steps 스키마 변경 완료 필요
관련 글
반응형
'Gemini' 카테고리의 다른 글
| Gemini 3.5 Flash Thought Preservation 완전분석 — 멀티턴 추론이 자동으로 이어지는 것, 비용은 어떻게 올라가나 (0) | 2026.05.29 |
|---|---|
| Gemini Omni vs Veo 3.1 — Google이 비디오 모델을 두 개 운영하는 이유 (0) | 2026.05.28 |
| Gemini Interactions API 완전분석 — OpenAI Responses API의 대항마, 서버사이드 히스토리 관리의 실체 (0) | 2026.05.28 |
| Gemini 3.5 Flash 출시 9일 — 실제 사용자들은 뭐라고 했나 (0) | 2026.05.28 |
| Gemini 3.5 Flash 가격 3배 인상의 전략적 의미 — Google이 Flash를 프리미엄으로 올린 이유 (0) | 2026.05.28 |