사용자의 절반이 에이전트(Agents)일 때 API 설계하기
요약
트래픽의 상당 부분을 차지하는 AI 에이전트를 대상으로 하는 API 설계 전략을 다룹니다. 에이전트의 반복적인 호출 특성을 고려하여 멱등성 유지, 명시적 환경 선택, 기계 친화적인 에러 메시지 제공의 중요성을 강조합니다.
핵심 포인트
- 위험한 작업은 기본값이 아닌 명시적 선택(Opt-in) 방식으로 설계
- 에이전트의 무한 재시도를 방지하기 위해 Idempotency-Key 자동 적용
- 사람이 아닌 에이전트가 파싱하기 쉬운 구조화된 JSON 에러 메시지 제공
- 종료 코드(Exit code)를 세분화하여 에이전트의 추론 정확도 향상
우리 API에 유입되는 트래픽 중 점점 더 많은 비중이 사람이 직접 타이핑하는 것이 아닙니다. 단순한 스크립트도 아닙니다. 바로 에이전트(Agents)입니다. 문서를 읽고, 통합(Integration) 코드를 작성하며, 자신의 작업 내용을 확인하기 위해 엔드포인트(Endpoints)를 호출하고, 모호한 결과가 나올 때마다 재시도하는 모델을 의미합니다.
대부분은 괜찮습니다. 하지만 이 과정에서 여러분의 기본 설정(Defaults)이 드러나게 되는데, 우리의 경우 많은 기본값이 무심코 선택되었다는 사실이 밝혀졌습니다.
우리의 경우 이해관계가 유난히 직설적입니다. 법적으로 우리의 쓰기(Writes) 작업은 영구적이기 때문입니다. 이것은 스페인을 위한 인보이스(Invoicing) API입니다. 모든 운영 환경의 인보이스는 세무 기록이 되며, 해시(Hashed) 처리되어 이전 기록과 체인(Chained)으로 연결된 후 세무 당국에 제출됩니다. 하나를 삭제할 수는 없습니다. 첫 번째 기록이 틀렸음을 명시하는 두 번째 기록을 발행할 수만 있을 뿐이며, 그 기록 또한 영구적입니다.
그래서 우리는 지치지 않는 호출자(Caller)를 위해 인터페이스를 재고하는 데 시간을 보냈습니다. 그 결과물 중 대부분은 세금과는 아무런 관련이 없습니다.
에이전트는 한 번만 실수하지 않는다
잘못된 명령을 실행한 개발자는 즉시 이를 느끼고, 멈추고, 생각합니다. 잘못된 명령을 실행한 에이전트는 그 에러 메시지가 무엇을 의미하는지 파악하는 동안 이미 그 명령을 11번 더 실행했을 것입니다. 여러분의 최악의 단일 호출(Single call)이 무엇이든, 에이전트는 그것을 찾아내어 반복할 것입니다.
그래서 우리가 가장 먼저 한 일은 위험한 환경을 기본값이 아닌 선택 사항(Opt-in)으로 만드는 것이었습니다. 우리의 CLI는 --live 플래그를 전달하지 않는 한 샌드박스(Sandbox) 환경에서 실행됩니다. 그리고 결정적으로, 환경 변수에 라이브 키(Live key)가 있다고 해서 조용히 운영 환경으로 업그레이드되지 않습니다.
export BEEL_API_KEY=beel_sk_live_...
beel invoices issue inv_123 # 에러 발생, 조용한 운영 환경 쓰기 방지
beel invoices issue inv_123 --live # 명시적 실행
이 에러가 사람이 읽기에는 도움이 되지 않는 것처럼 보일 수 있습니다. 하지만 에이전트가 읽기에는 정확히 적절합니다. 왜냐하면 에이전트의 다음 행동은 세무 당국에 40개의 인보이스를 제출했다는 사실을 나중에 깨닫는 것이 아니라, 해당 플래그(Flag)에 대해 추론하는 것이기 때문입니다.
동일한 논리로 인해 우리는 모든 POST 요청에 Idempotency-Key를 자동으로 첨부하게 되었습니다. 재시도(Retrying)는 에이전트(Agents)가 무언가 모호해 보일 때 수행하는 행동이며, 이들은 인간이 개입할 수 있는 속도보다 더 빠르게 재시도합니다.
읽기 쉬운 에러보다 기계가 읽기 쉬운 에러가 낫다
산문 형태의 에러(Prose errors)는 함정입니다. "무언가 잘못되었습니다. 설정을 확인해 주세요"라는 문구를 파싱하는 에이전트는 추측을 하게 될 것이고, 그 추측은 매우 확신에 차 있을 것입니다.
결국 우리는 두 가지 채널을 사용하게 되었습니다. 데이터는 JSON 형식으로 stdout(표준 출력)으로 전송되어 jq로 파이프 연결이 가능하게 합니다. 에러는 역시 JSON 형식으로 stderr(표준 에러)로 전송되며, 코드(code), 메시지(message), HTTP 상태 코드(HTTP status), 그리고 요청 ID(request ID)를 포함합니다. 게다가 종료 코드(exit codes)는 단순히 0 또는 1이 아니라 클래스별로 유형화되어 있습니다:
| 종료 코드 (Exit code) | 의미 (Meaning) |
|---|---|
0 | 성공 (Success) |
| ... |
중요한 구분은 "이것을 재시도하라"와 "멈춰라, 당신이 틀렸다" 사이의 구분입니다. 6은 백오프(back off, 대기 후 재시도)를 의미합니다. 5는 페이로드(payload)가 잘못되었음을 의미하며, 계속해서 요청을 퍼붓는 것은 전혀 도움이 되지 않습니다. 이 두 가지를 모두 1로 통합하는 것이 바로 에이전트가 동일한 잘못된 요청을 스무 번씩 시도하게 만드는 루프(loop)를 생성하는 원인입니다.
모델이 당신의 인터페이스(Surface)를 추측하게 만들지 마라
도구(Tooling)에서 발생하는 일반적인 실패 모드는 실제 API와 동기화되지 않고 수동으로 관리되는 명령어 목록입니다. 에이전트는 이를 읽고 믿고는, 더 이상 존재하지 않는 무언가를 호출합니다.
우리의 명령어는 빌드 타임(build time)에 OpenAPI 명세(spec)로부터 파생되며, 전체 인터페이스를 JSON으로 덤프하는 명령어가 하나 있습니다:
beel commands # 모든 항목에 대한 { command, signature, description, method, path }
beel invoices create --help
단 한 번의 호출로 전체 API를 파악할 수 있으며, 도움말 텍스트를 스크래핑(scraping)할 필요가 없습니다. 또한 명세가 실시간으로 가져오는 것이 아니라 설치된 버전과 함께 번들링되어 있기 때문에, 명백한 허점 하나가 존재합니다. 바로 CLI보다 최신인 엔드포인트(endpoint)가 있을 수 있다는 점입니다. 그래서 우리는 다른 척하지 않는 일반적인 탈출구(escape hatch)를 마련했습니다.
beel request POST /v1/customers --data @customer.json
우리는 에이전트 도구에서 탈출구를 편의 기능이 아닌 필수 요구 사항으로 취급하게 되었습니다. 그 대안은 래퍼(wrapper)가 아직 업데이트되지 않았다는 이유만으로 어떤 작업이 불가능하다고 결론 내려버리는 모델뿐이기 때문입니다.
플러그인은 노후화되며, 이는 에이전트(Agents)를 자신만만하게 틀리게 만듭니다
우리는 Claude Code 플러그인을 출시합니다. 그에 대한 당연한 방식은 API 레퍼런스(API reference)를 플러그인에 내장하는 것이지만, 그 당연한 방식은 틀렸습니다. 플러그인을 게시하는 날부터 정보는 부패하기 시작하며, 노후화된 레퍼런스는 권위 있게 읽히기 때문에 아예 없는 것보다 더 나쁩니다.
현재 로컬(local)에 유지해야 할 부분은 변하지 않는 부분뿐입니다: 인증(authentication), 멱등성(idempotency), 응답 엔벨로프(response envelope), 인보이스 라이프사이클(invoice lifecycle). 엔드포인트(Endpoints), 스키마(schemas), 이벤트 이름(event names)은 필요한 시점에 문서(docs)로부터 가져옵니다. 우리는 정확히 그 목적을 위해 /llms.txt와 /llms-full.txt를 게시하며, 가공되지 않은 OpenAPI 스펙(spec)도 함께 제공합니다.
이러한 분리는 꽤 괜찮은 경험칙(rule of thumb)이 됩니다. 불변량(Invariants)은 도구(tool) 안에 머물 수 있습니다. 버전 번호가 붙은 모든 것은 가져와야(fetched) 합니다.
우리가 아마 과대평가했을지도 모르는 관련 세부 사항이 하나 있습니다: beel docs search는 문서를 한 번 다운로드하여 캐시(cache)하고, 사용자의 로컬 머신에서 필터링합니다. 에이전트는 일치하는 섹션과 쿼리(query)를 전달받으며, 이 쿼리에는 종종 사용자가 구축 중인 형태가 포함되어 있지만 우리의 서버에는 절대 도달하지 않습니다.
검증(Verification)이 게임의 전부입니다
우리의 결함률(defect rate)을 변화시킨 부분은 문서가 아니었습니다. 그것은 에이전트가 누군가 배포하기 전에 통합(integration)이 작동함을 증명할 수 있다는 점이었습니다.
npx @beel_es/cli invoices create --data @invoice.json
npx @beel_es/cli invoices get <invoice_id>
테스트 키(test key)를 사용하면, 모델은 무료이며 무제한인 샌드박스(sandbox)를 대상으로 인보이스를 생성하고, 그 결과로 발생하는 이벤트를 트리거하며, 웹훅 수신기(webhook receiver)가 실제로 이를 처리하는지 확인할 수 있습니다. 일회용 스크립트도, 프로덕션(production) 직전의 마지막 확인 단계로서의 "내가 보기엔 괜찮아 보인다"라는 식의 확인도 필요 없습니다.
이것이 우리가 과소평가했던, 무료이고 무제한이며 진정으로 완전한 샌드박스가 필요한 이유입니다: 그것은 관대함의 기능이 아니라, 자동화된 호출자(automated caller)가 스스로의 피드백 루프(feedback loop)를 닫을 수 있게 해주는 핵심 요소입니다.
내가 어떤 API에서든 가져가고 싶은 부분
만약 에이전트(agent)가 호출할 만한 무언가를 구축하고 있다면, 던져볼 만한 질문은 그리 많지 않습니다. 단 한 번의 호출로 돌이킬 수 없는 피해를 줄 수 있는가, 그리고 기본적으로 접근 가능한 상태인가? 호출자가 영어를 읽지 않고도 "재시도 (retry)"와 "중단 (stop)"을 구분할 수 있는가? 단 한 번의 호출로 당신의 인터페이스(surface)를 탐색할 수 있는가? 그리고 비용이 발생하지 않는 어딘가에서 자신의 작업 내용을 스스로 검증할 수 있는가?
우리는 이러한 질문에 답해야만 했습니다. 왜냐하면 우리의 쓰기(writes) 작업은 법적으로 영구적이기 때문입니다. 대부분의 API는 그렇지 않더라도 고객이 엉망진창이 된 상황을 맞닥뜨리지 않도록 이 질문들에 답할 수 있어야 합니다. 어떤 경우든 질문 목록은 동일합니다.
이 모든 것이 어떻게 연결되어 있는지 확인하고 싶다면, 샌드박스(sandbox)를 포함한 문서를 docs.beel.es에서 확인할 수 있습니다. 그리고 특히 재시도 루프(retry-loop) 문제를 다른 팀들은 어떻게 처리하고 있는지 듣고 싶습니다. 이 문제는 우리가 해결했다고 가장 확신하지 못하는 부분입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기