에이전트 도구가 망가질 때, 30줄이 루프를 막는다

에이전트 도구가 망가질 때, 30줄이 루프를 막는다

Tool Guard 패턴과 MCP 스타터 템플릿이 프론트엔드 개발자에게 건네는 실전 설계 신호

AI 에이전트 Tool Guard MCP 설계 에러 핸들링 Pydantic LLM 도구 연결 무한 루프 방어
광고

에이전트는 착하게 실패하지 않는다

도구가 깨진 JSON을 돌려보낸다. 에이전트는 "다시 해볼게요"라고 말하며 똑같은 인자로 똑같은 도구를 다시 호출한다. 결과는 똑같이 깨진다. 17번째 재시도가 끝날 무렵, 과금 대시보드에는 경보가 울리고 사용자는 스피너를 멍하니 바라보고 있다.

dev.to의 Gabriel Anhaia가 공유한 프로덕션 트레이스가 정확히 이 장면이다. 업스트림 게이트웨이에 4KB 응답 제한이 있었고, 누구도 그걸 문서화하지 않았다. 모델은 잘못이 없다. 재시도는 트랜지언트 실패에 대한 정상적인 반응이다. 문제는 "이 응답이 망가졌다"는 신호를 모델이 이해할 수 있는 언어로 번역하는 레이어가 없었다는 것이다.

실패를 분류해야 처방이 달라진다

핵심 통찰은 도구 실패를 세 클래스로 나누는 것이다. Schema Mismatch—잘못된 JSON이나 필드 타입 불일치. 이 경우 재시도는 절대 도움이 안 된다. Partial Data—유효하지만 불완전한 응답. 페이지네이션이 잘렸거나 타임아웃으로 일부만 왔을 때. 다른 파라미터로 재시도하면 풀릴 수 있다. Semantic Garbage—형식은 완벽하지만 내용이 틀렸다. ID 필드에 이름을 넘겼거나 단위가 어긋난 경우. 이때는 인자를 바꾸거나 다른 도구를 써야 한다.

이 분류의 가치는 이론이 아니라 모델에게 돌려주는 에러 메시지의 모양에 있다. 스택 트레이스는 터미널 앞에 앉은 사람을 위한 디버깅 출력이다. 모델은 그걸 불투명한 텍스트로 읽고 "error"나 "exception" 키워드를 스캔한 뒤 기본 행동인 재시도로 돌아간다. 반면 {"error_class": "schema_mismatch", "hint": "같은 인자로 재시도하지 마세요"} 같은 구조화된 메시지는 모델이 실제로 다음 행동을 결정하는 데 쓸 수 있는 신호다.

30줄 가드가 하는 일

Gabriel의 guarded_call 패턴은 단순하다. 도구 호출을 감싸고, Pydantic 스키마로 응답을 검증하고, 실패 클래스를 분류해 모델이 읽을 수 있는 ToolError 구조체로 돌려준다. JSONDecodeError는 Schema Mismatch로, ValidationError는 필드 경로와 함께 Schema Mismatch로, 의미론적 검사는 선택적 validate_semantics 콜백으로 처리한다.

중요한 건 hint 필드다. "페이지 2로 재시도하세요", "날짜 범위를 넓혀보세요" 같은 구체적인 다음 행동이 담긴다. 에러를 신호로 바꾸는 것이 아니라, 에러를 다음 행동 가이드로 바꾼다. 이 차이가 루프를 막는다.

프론트엔드 개발자 관점에서 보면 이건 낯선 개념이 아니다. API 응답에 status, message, data를 구조화하는 것처럼, 에이전트 도구 응답도 모델이 소비할 수 있는 계약으로 설계해야 한다는 얘기다. 차이는 소비자가 사람이 아니라 LLM이라는 것뿐이다.

MCP 스타터가 가리키는 방향

같은 맥락에서 dev.to에 공개된 MCP 서버 미니멀 스타터를 읽으면 다른 층위가 보인다. TypeScript와 공식 MCP SDK로 read_file, list_directory, grep_search 세 가지 도구를 제공하는 이 템플릿은 보일러플레이트 없이 MCP 도구의 기본 패턴을 보여준다. 이름, JSON Schema 입력 스키마, 핸들러 함수—이 세 요소가 MCP 도구의 최소 단위다.

여기서 주목할 것은 구조 자체다. MCP가 Anthropic이 설계한 LLM-도구 연결 프로토콜이라는 점, 그리고 Claude Desktop, Cursor, VS Code처럼 MCP를 말하는 클라이언트라면 어디든 연결된다는 점은 이미 알려진 사실이다. 그런데 스타터 템플릿을 들여다보면 입력 스키마를 JSON Schema로 정의하는 방식이 앞서 말한 Pydantic 기반 Tool Guard와 정확히 대칭을 이룬다. 도구의 입력 계약을 Schema로 선언하고, 도구의 출력 계약을 Guard로 검증한다. 양쪽에 계약이 있어야 에이전트가 예측 가능하게 동작한다.

프론트엔드 개발자가 설계해야 할 것

에이전트 도구 연결을 설계할 때 프론트엔드 개발자가 기억해야 할 실전 원칙 세 가지가 여기서 나온다.

첫째, 도구 응답은 모델이 소비하는 API 계약이다. 사람이 읽는 에러 메시지와 모델이 읽는 에러 메시지는 다르게 설계해야 한다. 스택 트레이스 대신 error_class, code, hint로 구조화하라.

둘째, 재시도를 막는 것은 더 똑똑한 모델이 아니라 더 명확한 신호다. 모델이 루프에 빠지는 건 멍청해서가 아니라 "이게 재시도로 풀릴 문제인지 아닌지" 판단할 정보가 없어서다. 그 정보를 도구 응답에 담아야 한다.

셋째, MCP 도구 설계는 입력 스키마부터 시작한다. 스타터 템플릿이 보여주듯 JSON Schema로 입력을 명확히 선언하면, 모델이 잘못된 인자를 넘기는 Semantic Garbage 실패의 절반을 사전에 막을 수 있다.

빠른 실험에서 신뢰할 수 있는 에이전트로

프로토타입 단계에서는 에이전트가 도구를 호출하는 것 자체가 신기하다. 그런데 프로덕션에 가까워질수록 "어떻게 실패하는가"가 "어떻게 동작하는가"만큼 중요해진다. 무한 루프는 사용자 경험을 망가뜨리기 전에 과금 이상 알림으로 먼저 발견되는 경우가 많다—그것 자체가 이미 늦은 신호다.

Tool Guard 패턴과 MCP 스키마 설계는 결국 같은 질문을 다른 방향에서 물어본다. "내 에이전트가 예측 가능하게 실패하도록 설계되어 있는가?" 빠른 프로토타이핑의 시대에 이 질문을 초기부터 설계에 내장하는 팀이 에이전트를 실제로 운영할 수 있는 팀이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요