AI 에이전트를 위한 빌드 방법
요약
AI 에이전트를 위한 API 설계 원칙을 다룹니다. JSON 중심의 설계에서 벗어나 텍스트 기반 인터페이스, grep 가능한 한 줄 출력, 짧은 식별자 사용 등 에이전트의 토큰 효율성과 처리 능력을 극대화하는 방법을 제안합니다.
핵심 포인트
- JSON 대신 텍text를 기본 인터페이스로 사용하여 토큰 비용 절감
- grep이 가능하도록 데이터를 한 줄로 구성하여 조합성 확보
- 에이전트가 읽기 쉽도록 주석과 라벨을 활용한 구조 설계
- 토큰 효율을 위해 식별자를 짧게 유지
전통적으로 API는 브라우저나 클라이언트 라이브러리를 위해 설계되었습니다. 반대편의 무언가가 응답을 파싱(parse)하여 화면이나 객체로 변환할 것이라는 전제가 있었습니다. 하지만 이제 당신의 API를 호출하는 것은 토큰 예산(token budget)과 터미널을 가진 루프 안의 모델이며, 모델은 더 이상 당신의 JSON을 렌더링하지 않습니다. 대신, 그것은 읽습니다.
저는 이전에도 인간이 아닌 존재를 위해 빌드해야 한다고 주장한 바 있습니다. 그것이 '왜(why)'에 대한 것이었다면, 이번에는 '어떻게(how)'에 대한 것이며, 이는 거의 전적으로 당신의 API에 달려 있습니다. 왜냐하면 API는 에이전트가 접촉하는 유일한 접점(surface)이기 때문입니다.
다음에 이어질 내용의 대부분은 에이전트가 구동하도록 의도적으로 설계된 실험적인 CRM인 crmkit을 구축하며 느리게 배운 것들입니다. 이것은 헤드리스(headless) 방식이며 UI가 전혀 없고, 에이전트가 유일한 사용자입니다. 이는 좋은 스승이 되었습니다. 화면 뒤에 나쁜 결정을 숨길 곳이 없기 때문입니다. 우리는 이를 몇몇 프로젝트에 내부적으로 실행하고 있습니다. 완벽하지는 않지만, 올바른 방향을 가리키고 있다고 생각합니다.
형식(format)부터 시작하십시오. 텍스트가 기본 인터페이스입니다. JSON이 아닙니다. JSON은 그것을 파싱할 프로그램들을 위한 직렬화 형식(serialization format)이며, 에이전트는 아무것도 파싱하지 않습니다. 그것은 읽는 것입니다. 모든 중괄호, 따옴표, 반복되는 키 이름은 매 응답마다 에이전트에게 비용을 부과하는 토큰이며, 그 중 어느 것도 읽는 이에게 의미를 전달하지 않습니다. 텍스트를 우선적으로 반환하고, accept 헤더를 통해 구조화된 출력(structured output)을 폴백(fallback)으로 제공하십시오. 실제 프로그램이 반대편에 있을 때 ?format=json과 같이 다른 방식을 추가하는 것은 잘못된 것이 아닙니다. 다만 그것을 기본값으로 만드는 것을 멈추십시오. 왜냐하면 그것은 미래에 일반적인 사례가 되지 않을 것이기 때문입니다.
그다음 grep이 가능하도록 만드십시오 (make it grepable). 가능한 한 많은 유용한 정보를 단 한 줄에 노출하십시오. 에이전트가 당신의 출력물을 받았을 때 가장 먼저 취하는 본능은 그것을 grep으로 파이프(pipe)하는 것입니다. 왜냐하면 그것이 에이전트가 살아가는 환경이기 때문입니다. 예쁘게 출력된(pretty printed) JSON의 15줄에 걸쳐 퍼져 있는 레코드는 슬라이싱(slice)할 수 없습니다. 반면 동일한 레코드가 한 줄에 있다면, 당신에게 다시 요청하거나 모델을 한 번 더 거칠 필요 없이 필터링(filter), 카운트(count), 컷(cut), 정렬(sort)할 수 있습니다. crmkit에서 연락처는 다음과 같이 반환됩니다:
contact_k7m2q name="Jane Doe" email=jane@acme.com stage=lead updated=2026-06-04T09:13Z
# 1 contact(s)
레코드당 라벨이 붙은 한 줄을 사용하고, 레코드가 아닌 것은 에이전트가 읽거나 무시할 수 있는 주석(comment) 줄에 배치합니다. 페이지네이션(Pagination)도 같은 트릭을 사용합니다. 페이지 끝에 # next: <cursor>를 붙이는 식입니다. 항목당 한 줄을 사용하는 것은 미적인 선호도가 아닙니다. 전혀 아닙니다. 그것은 에이전트가 이미 사용하는 법을 알고 있는 다른 모든 것들과 당신의 출력물을 조합 가능(composable)하게 만드는 방법입니다.
식별자(Identifiers)는 짧아야 합니다. 5개의 토큰(token) 내로 설명할 수 있을 만큼 짧아야 하지만, 충돌(collision)이나 혼란을 야기할 정도로 짧아서는 안 됩니다. UUID는 36자리의 소음이며, 에이전트는 이후의 모든 호출 과정에서 이를 들고 다녀야 하고, 당신은 매번 그 비용을 지불해야 합니다. 이럴 때는 cus_abc123이나 token_xxx와 같은 접두사(prefix)가 큰 도움이 됩니다. 접두사는 이중 역할을 합니다. 식별자가 주변 맥락 없이 나타나더라도 에이전트에게 그것이 어떤 종류의 것인지 알려주며, 토큰이 들어가야 할 자리에 고객 ID를 전달하는 실수를 방지합니다. Stripe는 에이전트가 존재하기 훨씬 전부터 인간을 위해 이 방식을 완성했습니다. 알고 보니 이 추론은 토큰 단위로 비용을 지불하는 독자에게 훨씬 더 잘 들어맞았습니다. 실무 경험을 바탕으로 한 가지 덧붙이자면, 식별자를 두 가지 형태 모두로 다시 받아들이십시오. crmkit은 GET /contacts/k7m2q와 GET /contacts/contact_k7m2q를 모두 수용하며 동일한 것으로 취급합니다. 에이전트는 추측할 것이고, 문장 부호는 틀리더라도 레코드를 정확히 찾아내는 것은 다시 요청(round trip)을 보낼 만큼의 실수가 아니기 때문입니다.
다음에 무엇이 필요할지 예측하세요. 만약 에이전트가 리소스 목록을 요청한다면, 단순히 목록만 반환하지 마세요. 에이전트가 자신이 무엇을 보고 있는지 이해하는 데 도움이 되는 해당 리소스 주변의 정보를 함께 반환하세요. 그렇지 않으면 단 하나의 질문이 10개의 후속 호출로 확산(fan out)되며, 각 호출은 매번 왕복(round trip)을 발생시키고, 더 많은 토큰과 더 높은 지연 시간(latency)을 초래합니다. 그리고 이 모든 과정은 예측 가능했습니다. 당신은 다음 질문이 무엇일지 알고 있습니다. 지금 바로 동일한 응답 내에서 그 질문에 답하세요. crmkit의 모든 연락처 라인(contact line)이 정확히 이 이유로 activities=N과 last_activity를 포함하고 있는 것입니다. 에이전트는 각 행을 가져와서 확인하는 대신, 이미 가지고 있는 목록을 읽음으로써 활성 레코드와 휴면 레코드를 구분할 수 있습니다.
벌크 작업(bulk operations)을 추가하세요. 한 번의 요청으로 10개의 레코드를 삽입하는 것은 10번의 개별 요청을 수행하는 것보다 훨씬 쉽습니다. 인간 클라이언트에게는 이것이 좋은 최적화(optimisation)일 뿐이지만, 에이전트에게는 단 한 번의 결정과 열 번의 결정 사이의 차이입니다. 왜냐하면 모델은 매 호출 사이에 사고(thinking)를 해야 하며, 사고는 비용이 많이 들기 때문입니다. 이는 제가 압축하되, 확장하지 마세요 (compress, don't expand)에서 썼던 것과 동일한 직관입니다. 에이전트에게 업무의 10분의 1씩 수행하는 10번의 호출 대신, 전체 작업을 수행하는 단 한 번의 호출을 제공하세요.
제가 과소평가했던 이 방식의 절반은 나가는 방향(outbound)에서의 벌크 작업입니다. 읽기(Read) 작업도 쓰기(Write) 작업만큼이나 나쁘게 확산(fan out)되지만, 화면에서는 페이지네이션(pagination)으로 문제를 해결했을 것이기에 아무도 이를 고칠 생각을 하지 않습니다. crmkit의 activity 엔드포인트는 핸들(handles) 목록을 받습니다. 따라서 기업 목록 한 페이지를 보유한 에이전트는 단 한 번의 호출로 그들 모두의 이력을 가져온 뒤 로컬에서 결과를 그룹화할 수 있습니다. 이는 N+1 문제와 같지만, 여기서의 모든 추가 쿼리는 매번 사고 단계(thinking step)의 비용을 발생시킵니다.
에러 메시지는 서술적이어야 합니다. 에러는 상태 코드(status code)가 아닙니다. 사람이 400 에러를 마주하면 직접 검색해 보거나, 포럼 스레드를 읽거나, 매뉴얼을 찾아봅니다. 하지만 에이전트(agent)는 그럴 수 없습니다. 에이전트는 당신이 건네준 정보 외에는 아무것도 알지 못합니다. 따라서 에이전트가 읽을 수 있는 실제 문서와 매뉴얼을 가리켜 주고, 무엇을 다르게 해야 하는지에 대한 힌트(hint)를 반환하세요. crmkit의 모든 에러는 ERROR 코드와 다음 행동을 알려주는 HINT로 구성된 두 줄로 이루어져 있습니다. 존재하지 않는 레코드를 요청하면, 먼저 목록을 조회하라고 말하며 해당 엔드포인트(endpoint)를 제공합니다. 존재하지 않는 필드로 필터링하면 존재하는 필드들을 출력해 줍니다. 이를 통해 에이전트가 당신이 그냥 건네줄 수도 있었던 어휘를 추측하며 헤매는 일을 방지합니다.
실패하는 과정을 지켜보며 제가 깨달은 점은, 힌트에는 다시 시도해야 하는지 여부까지 포함되어야 한다는 것입니다. 벽에 부딪혔을 때 에이전트의 기본 반응은 다시 시도하는 것이며, 어떤 벽은 아무리 밀어도 움직이지 않습니다. crmkit은 플랜(plan)이 가득 차서 쓰기(write)를 거부할 때, 이를 말로 설명하며 에이전트에게 중단하고 사람과 대화하라고 지시합니다. 아무리 재시도해도 공간을 더 확보할 수 없기 때문입니다. 재시도 가능한 에러와 종료형(terminal) 에러가 비슷하게 보인다면, 루프(loop) 속에서 예산을 낭비하게 될 것입니다.
API 내부에 매뉴얼을 포함하여 배포하세요. 문서 사이트나 검색창이 있는 포털이 아니라, 엔드포인트(endpoint) 형태로 제공해야 합니다. crmkit은 GET /help 요청에 대해, 우리를 평가하는 개발자가 아닌 에이전트를 위해 작성된 전체 운영 매뉴얼을 한 페이지에 담아 응답하며, 찾아오는 모든 것을 위해 /.well-known/agent.md에도 사본을 제공합니다. 이 매뉴얼은 서비스가 무엇인지, 무엇을 할 수 있는지, 인증(auth)이 어떻게 작동하는지를 설명하는 몇 줄의 YAML로 시작합니다. 이를 통해 기계는 산문(prose)을 읽는 데 토큰(token)을 소비하기 전에 계속 읽을지 여부를 결정할 수 있습니다.
그 결과(consequence)를 이해하는 데는 시간이 좀 걸렸습니다. crmkit으로 들어가는 방법은 단 하나의 URL입니다. 에이전트(agent)에 한 줄을 붙여넣고, https://api.crmkit.ai/start를 읽고 따라가기만 하면, UI를 건드리는 사람 없이도 사용자가 로그인되고, 실제 연락처가 등록되며, 리마인더(reminder)가 설정됩니다. 그 페이지는 문서(documentation)가 아닙니다. 그것은 설명을 위한 참조(reference)라기보다 실행해야 할 지침으로서 명령형(imperative)으로 작성되었습니다. 배포할 SDK도 없고, 동기화해야 할 클라이언트 라이브러리(client library)도 없으며, 유지 관리해야 할 온보딩 퍼널(onboarding funnel)도 없습니다. 배포는 링크 하나로 축소되고, 매뉴얼이 곧 제품의 접면(product surface)이 됩니다.
모든 쓰기(write) 작업은 두 번 발생한다고 가정하십시오. 에이전트는 재시도(retry)를 합니다. 타임아웃(timeout)이 발생하여 다시 시도하거나, 맥락을 놓치거나, 처음부터 다시 시작하거나, 혹은 첫 번째 시도가 성공했는지 확신하지 못해 다시 시도할 수도 있습니다. 그러므로 쓰기 작업이 반복되어도 안전하게 만드십시오. crmkit에 연락처를 게시하는 것은 이메일 주소를 기준으로 업서트(upsert)를 수행하므로, 동일한 사람을 두 번 보내더라도 두 번째 복사본을 만드는 대신 기존 정보를 업데이트하며, 응답을 통해 생성되었는지 업데이트되었는지를 명확히 알려줍니다. 이미 첨부된 항목을 다시 첨부하는 것은 에러(error)가 아니라 비용이 들지 않는 무작정 연산(no-op)으로 처리됩니다. 호출자가 타임아웃이 있는 확률적 프로세스(probabilistic process)일 때, 멱등성(Idempotency)은 고급 기능이 아닙니다.
그것이 유일한 작업자가 아니라고 가정하십시오. 여러 에이전트가 동시에 동일한 레코드(record)를 작업하며, 주변 사람들 또한 마찬가지입니다. crmkit의 모든 레코드는 버전(version)을 가집니다. 업데이트 시 버전을 함께 보내면, 다른 에이전트가 방금 수행한 작업을 조용히 덮어쓰는 대신 오래된 쓰기(stale write)는 거부됩니다. 버전을 제외하면 마지막 쓰기 승리(last write wins) 방식이 되지만, 적어도 그것은 당신의 선택입니다. 또한 모든 행(row)에 에이전트인지 사람인지 작성자를 기록하고 필드 수준의 차이(field level diff)를 유지합니다. 왜냐하면 데이터 군단(fleet)이 당신의 데이터를 다루기 시작하는 순간, "누가 이것을 무엇으로 변경했는가"는 더 이상 수사적인 질문이 아니기 때문입니다.
되돌릴 수 없는 작업은 차단하십시오. crmkit에서의 삭제(delete)는 실제로 삭제하지 않습니다. 첫 번째 호출이 돌아오면 확인을 요청하며 토큰을 전달하고, 에이전트는 해당 토큰을 첨부하여 호출을 반복하기 전에 반드시 사람에게 가서 물어봐야 합니다. 누군가를 관리자(admin)로 승격시키거나 워크스페이스(workspace)를 삭제하는 작업은 반드시 회신되어야 하는 코드를 이메일로 보냅니다. 이 검증 로직은 에이전트의 판단이나 프롬프트(prompt) 속의 엄격한 문장이 아니라, API 내부에 존재합니다. 이것이 제가 human in the loop, just not like this에서 의도했던 바의 솔직한 버전입니다. 모델에게 조심해달라고 요청하는 것이 아닙니다. 되돌릴 수 없는 호출을 한 단계 만에 완료하는 것이 불가능하도록 만들어, 에이전트가 일시 정지를 생각했든 아니든 상관없이 일시 정지가 발생하도록 만드는 것입니다.
이 모든 것을 관통하는 맥락은 동일합니다. 당신의 호출자(reader) 뒤에는 제2의 호출자가 없습니다. 페이로드(payload)를 살펴볼 개발자도 없고, 거친 부분을 매끄럽게 다듬어줄 클라이언트 라이브러리(client library)도 없으며, 렌더링을 수행하는 브라우저(browser)도 없습니다. 네트워크(wire)를 통해 전송되는 모든 것이 곧 전체 경험이며, 이는 모든 호출마다, 영원히 토큰(token) 비용으로 지불됩니다. 이는 API 디자인을 미학적인 문제라기보다 예산 관리의 문제로 재정의하게 만듭니다.
이 중 어느 것도 생소한 것이 아닙니다. 대부분은 호출자가 셸(shell)을 사용하고, 인내심이 없으며, 실행되는 내내 비용이 발생하는 주니어 엔지니어였더라도 어차피 당신이 했을 방식들입니다.
당신의 API에는 새로운 호출자가 생겼습니다. 이 호출자는 터미널(terminal)에서 작동하며 토큰 단위로 비용을 지불합니다. 그러니 그에 맞춰 작성하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기