ADK Go 2.0 멀티 에이전트 설계법: 그래프, HITL, 재시도를 운영 구조로 묶기
Google이 ADK for Go 2.0을 공개했습니다. 핵심은 Go 개발자가 멀티 에이전트 애플리케이션을 그래프로 구성할 수 있게 된 점입니다. 함수 노드, 에이전트 노드, 도구 노드, fan-out/fan-in, human-in-the-loop, retry, timeout, dynamic orchestration이 하나의 실행 모델 안으로 들어왔습니다.
에이전트를 실제 서비스에 넣어본 팀은 압니다. 프로토타입은 프롬프트 하나로 됩니다. 하지만 운영 서비스는 다릅니다. 사용자의 요청을 분류하고, 필요한 도구를 부르고, 실패하면 재시도하고, 위험한 액션은 사람 승인을 받고, 긴 작업은 중간 상태를 저장하고, 서버가 재시작돼도 이어가야 합니다. 이걸 ad-hoc 코드로 짜면 금방 복잡해집니다.
왜 그래프 모델이 필요한가
단일 프롬프트 기반 에이전트는 흐름이 단순할 때 편합니다. 하지만 실제 업무는 조건과 분기가 많습니다. 예를 들어 환불 처리 에이전트를 생각해보면 다음 흐름이 필요합니다.
- 요청 내용을 분류합니다.
- 주문 정보를 조회합니다.
- 환불 가능 여부를 판단합니다.
- 금액이 크면 사람 승인을 받습니다.
- 승인되면 결제 API를 호출합니다.
- 실패하면 재시도하거나 상담원에게 넘깁니다.
- 결과를 사용자에게 설명합니다.
이 흐름을 함수 내부 if문과 for문으로 계속 키우면 관측성, 재시작, 테스트가 어려워집니다. 그래프 모델은 각 단계를 노드로 분리하고, 연결과 라우팅을 명시합니다. 구조가 보이면 테스트와 운영도 쉬워집니다.
ADK Go 2.0은 그래프도 agent.Agent로 실행된다고 설명합니다. 별도 하네스가 아니라 기존 runner, launcher, console에서 같은 방식으로 돌릴 수 있다는 점이 실무적으로 중요합니다.
노드 타입을 어떻게 나눌까
ADK Go 2.0에는 여러 노드 구성 요소가 있습니다.
Function node는 일반 Go 함수를 감쌉니다. 입력과 출력 스키마를 타입으로 관리할 수 있어, LLM 호출 없이 확정적인 처리를 할 때 좋습니다. 예를 들어 주문 상태 계산, 정책 룰 적용, 문자열 정규화, 금액 계산은 Function node가 적합합니다.
Agent node는 LLM agent를 그래프 단계로 넣습니다. 자연어 이해, 요약, 판단, 계획 수립처럼 모델 판단이 필요한 구간에 씁니다.
Tool node는 외부 도구 호출을 단계로 만듭니다. DB 조회, API 호출, 검색, 파일 처리처럼 부작용이 있거나 외부 시스템을 건드리는 작업입니다.
Join node는 여러 분기 결과를 모읍니다. 병렬 리서치, 다중 검증, 여러 데이터 소스 조회 후 합치는 작업에 필요합니다.
Dynamic node는 실행 순서가 런타임 데이터에 따라 바뀔 때 씁니다. 예를 들어 사용자가 입력한 파일 목록 수만큼 반복하거나, 모델이 세운 계획에 따라 필요한 하위 작업을 실행할 때 유용합니다.
핵심은 모든 것을 LLM에 맡기지 않는 것입니다. 확정적 로직은 함수로 두고, 불확실한 판단만 모델에 맡기는 구조가 안정적입니다.
Human-in-the-loop는 기능이 아니라 안전장치다
ADK Go 2.0은 어떤 노드든 사람 입력을 요청하고, 워크플로우를 durable하게 기다렸다가 다시 시작할 수 있다고 설명합니다. 환불 승인, 데이터 삭제, 이메일 발송, 고객 상태 변경, 결제 취소 같은 작업에는 반드시 필요합니다.
중요한 것은 HITL을 UX로만 보지 않는 것입니다. HITL은 운영 안전장치입니다. 승인 요청에는 최소한 다음 정보가 포함돼야 합니다.
- 에이전트가 하려는 액션
- 액션 대상
- 예상 영향
- 되돌릴 수 있는지 여부
- 에이전트가 그 액션을 추천한 근거
- 승인 후 실행될 정확한 도구 또는 API
"승인할까요?"만 묻는 HITL은 부족합니다. 사람이 판단할 수 있는 정보를 같이 줘야 합니다.
재시도와 타임아웃은 노드 단위로 둔다
ADK Go 2.0은 노드별 retry policy, exponential backoff, jitter, timeout, graph-wide concurrency cap을 지원한다고 설명합니다. 이 기능은 운영 안정성의 핵심입니다.
모든 실패를 같은 방식으로 재시도하면 안 됩니다. 네트워크 timeout은 재시도할 수 있지만, 권한 오류나 validation 오류는 재시도해도 의미가 없습니다. 결제 API처럼 부작용이 있는 작업은 idempotency key 없이 재시도하면 중복 처리 위험이 있습니다.
노드별 정책을 다음처럼 나누는 것이 좋습니다.
- 읽기 API: 짧은 backoff로 2~3회 재시도
- 쓰기 API: idempotency key가 있을 때만 재시도
- LLM 판단 노드: 실패 원인이 rate limit이면 재시도, schema 오류면 프롬프트 수정 대상으로 기록
- 사람 승인 노드: timeout 후 자동 거절 또는 상담원 큐로 이동
- 병렬 fan-out: 전체 실패가 아니라 실패한 branch만 재시도
이런 정책은 코드보다 runbook에 먼저 써두는 편이 좋습니다.
1.0에서 2.0으로 옮길 때 볼 변경점
Google 문서에 따르면 ADK Go 2.0은 additive 성격이 강하지만, 런타임 통합으로 인해 몇 가지 기계적인 변경이 있습니다. 노드와 node-function의 첫 번째 파라미터가 agent.InvocationContext에서 agent.Context로 바뀌고, ToolContext와 CallbackContext가 agent.Context로 통합됩니다. custom InvocationContext 구현에는 IsolationScope와 ResumedInput 메서드가 필요합니다.
테스트에서 session.Event의 정확한 equality를 검증하고 있었다면 주의해야 합니다. 이벤트에 node fields와 metadata가 추가됐기 때문에 기존 assertion이 깨질 수 있습니다. 이 경우 전체 객체 비교 대신 필요한 필드만 비교하는 방식이 안전합니다.
운영에 맞는 설계 예시
고객지원 에이전트를 ADK Go 2.0으로 설계한다면 다음 구조가 현실적입니다.
- classify: 사용자의 요청 유형을 분류합니다.
- fetch_context: 주문, 결제, 계정 정보를 조회합니다.
- route: 환불, 배송, 계정, 기타로 분기합니다.
- policy_check: 확정적인 룰을 함수로 적용합니다.
- draft_answer: LLM이 답변 초안을 작성합니다.
- high_risk_approval: 환불, 계정 변경, 개인정보 요청이면 사람 승인을 받습니다.
- execute_action: 승인된 작업만 도구로 실행합니다.
- final_response: 실행 결과와 다음 단계를 사용자에게 설명합니다.
여기서 중요한 점은 LLM이 모든 결정을 하지 않는다는 것입니다. 정책 판단은 함수로, 위험 작업은 승인으로, 외부 호출은 도구 노드로 분리합니다. 그래야 디버깅이 됩니다.
오늘 적용할 체크리스트
- 현재 에이전트 플로우를 단일 프롬프트가 아니라 노드 그래프로 그려본다.
- 확정적 로직, 모델 판단, 외부 도구 호출, 사람 승인을 분리한다.
- 각 노드마다 retry, timeout, idempotency 필요 여부를 정의한다.
- 고위험 액션에는 HITL 승인 메시지 템플릿을 만든다.
- 병렬 fan-out은 독립적으로 실패해도 전체 작업을 망치지 않게 설계한다.
- ADK 1.0 코드가 있다면 agent.Context 변경, event assertion, session store 영향을 먼저 점검한다.
출처: Google Developers Blog ADK for Go 2.0 announcement, ADK Go migration notes.