성공적인 AI 에이전트 시스템을 만들려면
Seo Sanghyeon @sanxiyn@hackers.pub
AI 에이전트 시스템을 만들며 배운 것
이 글은 AI 에이전트 시스템을 만들며 쌓은 경험을 정리한 것으로, 2026년 2월에 썼다. 두 부분으로 구성된다. 첫 번째 부분은 총론이고, 두 번째 부분은 각론이다.
이 분야는 빠르게 변하고 있어, 여기에 쓴 교훈도 금방 낡을 수 있다. 컨텍스트 윈도는 이 글 전반에 걸쳐 반복되는 제약이지만, 미래에는 그렇지 않을 수도 있다. "차근차근 생각하라(Let's think step by step)"는 2022년 발표된 이래 널리 권장되었지만 [1], 최근 연구에 따르면 이 기법은 이제 덜 중요해졌다 [2]. 이 글의 내용도 일부는 그런 운명을 맞을 것이다.
총론
목표 수립
AI 에이전트 시스템을 위한 목표는 명확하고, 유용하고, 달성 가능해야 한다.
명확하다는 것은 테스트할 수 있을 만큼 구체적이라는 뜻이다. "개발자의 코딩 작업을 돕는다"는 목표가 아니라 범주다. 목표는 "GitHub 이슈 설명과 Python 저장소가 주어지면, 기존 테스트를 통과하는 PR을 생성한다" 같은 것이다. 후자에서는 어떤 입력을 준비해야 하는지, 어떤 출력을 평가해야 하는지, 평가 기준이 무엇인지가 드러난다. 목표의 범위를 좁혀야 한다. 범용 시스템은 평가하기 어렵고, 개선하기 어렵고, 제대로 작동하는지 알기도 어렵다. 좁은 범위에서 시스템이 잘 작동하면 범위를 넓히는 것을 고려할 수 있다.
유용하다는 것은 목표를 달성했을 때 실제 문제가 해결된다는 뜻이다. 이는 명확함과는 다른 개념이다. "코드베이스를 감사하여 보안 문제를 발견한다"는 잘 정의된 목표일 수 있고, 내 경험상 달성 가능하기도 하다. 하지만 이미 알고 있는 보안 문제를 해결하는 데도 허덕이고 있다면, 잠재적 오탐을 포함하는 더 많은 문제를 대기열에 추가하는 것은 유용하지 않다. 질문은 단순히 "이것을 할 수 있는가?"가 아니라 "이것이 실제로 원하는 것인가?"다.
달성 가능하다는 것은 현재 모델 능력을 감안했을 때 현실적이라는 뜻이다. SWE-bench Verified 같은 벤치마크는 현재 AI가 어떤 코딩을 할 수 있는지 대략적인 감을 준다. 다른 분야에도 비슷한 벤치마크가 있다. 목표를 달성하기 위해 현재 모델이 일관되게 실패하는 무언가를 안정적으로 해야 한다면 그것은 현실적으로 어려울 수 있다. 모델 능력은 꾸준히 발전하고 있으므로, 작업이 급하지 않다면 현재 한계에 맞춰 어떻게든 시스템을 구축하기보다 기다리는 것이 나을 수 있다.
인간 전문가가 목표를 달성하기 위해 무엇을 할지 모호함 없이 설명할 수 있는가? 결과물이 나오면 실제로 사용하겠는가? 그렇지 않다면, 작업을 시작하기 전에 목표를 더 다듬어야 한다.
평가 설계
측정할 수 없는 것은 개선할 수 없다. 평가는 에이전트에 가한 변경 -- 다른 모델, 새 프롬프트, 다른 도구 -- 이 상황을 좋아지게 했는지 나빠지게 했는지 판단하는 수단이다. 평가 없이는 눈을 감고 운전하는 것과 같다.
평가는 객관적인 것이 좋다. 목표가 "기존 테스트를 통과하는 PR을 생성한다"라면, 평가는 테스트를 실행하는 것이다. 테스트는 통과하거나 실패할 것이다. 객관적 평가는 빠르고, 저렴하고, 일관적이다. 목표를 객관적 기준을 중심으로 설계할 수 있다면, 그렇게 하라.
객관적 평가가 어렵다면 주관적 평가도 가능하다. 주관적 평가는 사람이 할 수도 있고 AI가 할 수도 있다 (LLM-as-a-judge). 두 경우 모두 채점 기준(scoring rubric)이 도움이 된다. 채점 기준이란 각 항목별로 어떤 것이 좋은 답변이고 어떤 것이 나쁜 답변인지 명확하게 설명한 기준 목록이다. 채점 기준이 없으면 평가자 간 일치도(inter-rater agreement)가 낮다. 동일한 출력을 두 평가자가 평가하면 의견이 갈리고, 같은 평가자도 시간이 지나면 기준이 흔들린다. 채점 기준은 여러 평가 실행 간에 점수를 비교 가능하게 한다. AI가 평가하는 경우에도 명확한 채점 기준을 받은 모델은 그렇지 않은 모델보다 더 일관된 점수를 준다.
평가가 반드시 갖춰야 할 두 가지 속성이 있다. 목표를 반영해야 하고, 속이기 어려워야 한다.
목표를 반영한다는 것은 평가에서 좋은 점수를 받으면 목표도 실제로 달성된다는 뜻이다. 실제와 동떨어진 평가는 에이전트를 엉뚱한 방향으로 최적화한다. 에이전트의 목표가 고객 지원 티켓 처리라면, 비슷해 보이지만 실제의 복잡함이 없고 단순한 합성 데이터보다 실제 고객 지원 티켓으로 평가하는 것이 바람직하다.
속이기 어렵다는 것은 에이전트가 지표를 조작해 좋은 점수를 받을 수 없어야 한다는 뜻이다. 평가가 "테스트를 통과하는가"를 측정하는데 에이전트가 테스트를 수정할 수 있다면, 지표를 믿을 수 없다. 적대적으로 생각할 필요가 있다. 에이전트가 좋은 점수를 받았다면, 그것이 항상 실제로 더 좋은 것인가? SWE-bench Verified는 교훈적인 사례다. OpenAI는 실패의 절반 이상이 테스트 결함 때문임을 발견했다. 어떤 테스트는 문제 설명에 언급되지 않은 특정 구현 세부사항을 강제했고, 다른 어떤 테스트는 명세되지 않은 기능을 테스트했다. 그 외에도, 테스트된 모든 프론티어 모델이 정답 패치를 암기해서 그대로 재현할 수 있었는데, 이는 훈련 데이터가 오염되었음을 시사한다. 그 결과 OpenAI는 SWE-bench Verified 점수 보고를 중단했다 [3].
작게 시작하라. 개발 초기에는 평가 예시 열 개로도 충분한 경우가 많다. 초기에는 에이전트가 작동하지 않는 상태에서 작동하는 상태로 전환되고 있으므로, 열 개의 예시만으로도 변화를 감지할 수 있다. 에이전트가 성숙해 더 작은 점진적인 개선을 하게 되면 열 개의 예시로는 감지가 어려워진다. 감지하려는 개선이 작아질수록 평가 예시를 늘려야 한다.
로그 인프라
에이전트의 로그는 큰 가치가 있다. 실패한 에이전트의 로그를 읽어보는 것만으로도 많은 것을 알 수 있다. 모델은 생성하면서 생각하기 때문에 그 사고 과정이 로그에 남는다. 그러한 로그를 읽으면 모델이 도구 결과를 잘못 읽고, 잘못된 가정을 하고, 이후 여러 턴에 걸쳐 잘못된 가정을 고수하는 모습을 볼 수 있다. 이것은 다른 방법으로는 얻을 수 없는 귀한 정보이며, 그렇기 때문에 로그에는 투자할 가치가 있다.
최소한 모델의 모든 상호작용을 기록해야 한다. 기록해야 하는 항목으로 사용한 모델, 전체 입력 (시스템 프롬프트와 도구 정의 포함), 전체 출력, 소요 시간, 비용이 있다. 비용 추적을 하지 않으면 나중에 재구성하기 어렵다. 지연 시간 데이터는 모델이나 프롬프트 변경을 비교해 속도와 품질 간의 트레이드오프를 분석해야 할 때 꼭 필요하다.
로그는 사람이 직접 살펴보는 것과 자동화된 분석을 지원해야 한다. 사람이 직접 살펴보려면 로그를 보기 쉬운 형태로 읽을 수 있어야 하고 시간, 작업, 결과, 비용 등으로 검색할 수 있어야 한다. 자동화된 분석을 위해서는 모델이 질의할 수 있는 구조화된 형식으로 데이터가 존재해야 한다. 텍스트 파일로만 존재하는 로그는 개별 실패를 디버깅하는 데 유용하지만, 질의 가능한 형식으로 저장된 로그는 통계적인 질문을 할 수 있게 한다. 어떤 작업의 실패율이 가장 높은가? 어떤 프롬프트가 가장 긴 추론을 만드는가? 성공한 실행당 평균 비용은 얼마인가?
AI 에이전트 로그 관리를 위한 도구들이 있다. Langfuse [4]와 Logfire [5] 모두 살펴볼 가치가 있다. 하지만 기존 도구가 해결해 주지 않는 필요가 있다면 직접 도구를 만드는 것도 생각해봐야 한다. 그것은 독립적인 도구일 수도 있고 기존 플랫폼 위에 구축된 것일 수도 있다.
로그를 나중에 추가할 인프라로 생각해서는 안된다. 로그가 없는 고통을 느낄 때가 되면, 가장 큰 도움이 되었을 로그는 이미 잃어버린 뒤다.
모델, 프롬프트, 도구
에이전트를 개선하기 위해 모델, 프롬프트, 도구를 바꿀 수 있다.
모델은 시스템 성능에서 가장 중요한 요소다. 더 좋은 모델은 평범한 프롬프트를 구제할 수 있지만, 나쁜 모델은 완벽한 프롬프트로도 구제할 수 없다. 새로운 모델은 계속해서 나온다. 핵심은 새 모델을 빠르게 테스트할 수 있어야 한다는 것이다. 모델을 교체하고, 평가를 실행하고, 점수를 비교한다. 평가가 잘 갖춰져 있다면 이 작업은 며칠이 아니라 몇십 분이 걸려야 한다. 모델 평가가 쉬워야 새 모델을 빠르게 적용할 수 있다.
프롬프트는 일상적인 개선이 일어나는 곳이다. 프롬프트를 개선하는 올바른 방법은 모델이 원하는 것을 상상하는 것이 아니라 로그를 읽는 것이다. 실패한 실행의 로그는 보통 모델이 무엇을 오해했는지, 프롬프트의 어떤 부분이 전달되지 않았는지 보여준다. 변경사항은 평가로 검증해야 한다. 어떤 실패를 고치는 프롬프트 변경이 다른 어떤 경우를 조용히 망가뜨릴 수 있다.
도구는 모델과 세상 사이의 인터페이스이며, 사용자 인터페이스처럼 주의 깊게 설계해야 한다. 설계의 목표는 올바른 사용을 쉽게 하고 잘못된 사용을 어렵게 만드는 것이다. 모델이 도구를 자주 잘못 사용한다면 -- 인자를 잘못된 형식으로 전달하거나, 잘못된 맥락에서 호출하거나, 출력을 오해한다면 -- 그것은 모델의 문제가 아니라 도구의 문제다. 사용자가 계속 같은 실수를 하면 사용자가 아니라 UI를 다시 설계하듯이, 모델이 의도하지 않은 행동을 계속하면 모델에 맞춰주는 것이 좋을 수 있다.
세 가지 모두 직관보다는 평가로 변경하는 것이 바람직하다. 무엇이 도움이 될지에 대한 직관은 자주 틀리지만, 평가 점수는 그렇지 않다.
스킬과 배경지식
언어 모델은 세상에 대한 방대한 지식을 가지고 있다. 프로그래밍 언어와 과학의 개념과 역사적 사실을 이해한다. 하지만 우리 조직과 코드베이스, 내부 도구, 해당 분야의 관습에 대해서는 잘 모른다. 모델의 잘못이 아니라 알려주지 않았기 때문이다.
실용적인 해결책은 모델에게 필요한 것을 주는 것이다. 정보 소스와 사용법을 제공해야 한다. 에이전트가 내부 데이터베이스를 질의해야 한다면, 그렇게 할 수 있는 CLI를 주고 사용법 문서도 같이 준다. 코드베이스 특유의 관습을 따라야 한다면, 그 관습을 파일에 적어 둔다. 지식 베이스를 참조해야 한다면, 검색 도구를 주고 스키마를 설명한다. 모델의 추론 능력보다 추론할 재료가 문제인 경우가 많다.
Anthropic은 이 패턴을 에이전트 스킬 [6]로 공식화했다. 스킬은 지침이 담긴 SKILL.md 파일과 지원 스크립트 및 리소스로 구성된 폴더다. 시작할 때 에이전트는 설치된 각 스킬의 이름과 설명만 미리 읽어둔다. 작업이 관련 스킬을 트리거하면, 에이전트는 전체 지침과 링크된 파일을 필요에 따라 읽는다. 이 점진적 공개 설계 덕분에 컨텍스트 윈도에는 한계가 있지만 스킬에는 그보다 많은 컨텍스트를 담을 수 있다.
Anthropic의 스킬 형식을 사용하지 않더라도, 아이디어는 일반적으로 적용할 수 있다.. 에이전트에게 필요하지만 일반 지식으로는 유추할 수 없는 컨텍스트가 무엇인지 파악하고, 그 컨텍스트를 발견 가능한 리소스로 패키징하고, 에이전트가 필요에 따라 접근할 수 있는 도구를 주어라.
비용 제어
새 모델과 큰 모델이 능사는 아니다. 프론티어 모델은 비싸고 느리다. 에이전트 시스템 내의 많은 작업은 더 작은 모델로 충분하다. 실용적인 접근법은 각 작업을 안정적으로 할 수 있는 가장 작은 모델을 사용하는 것이다. 여러 모델을 대상으로 평가를 실행해 변곡점을 찾고, 그보다 한 단계 위의 모델을 쓰면 된다.
출력 토큰은 입력 토큰보다 비싸다. 따라서 입력 토큰보다 출력 토큰을 아껴야 한다. 필요한 것만 요청해 출력을 최소화하라. 작업을 위해 무거운 처리 전에 분류나 필터링이 필요하다면, 가벼운 단계를 먼저 하라. 천 개의 항목을 분류해 깊게 분석할 가치 있는 스무 개를 찾는 것은 천 개 모두 전체 분석을 실행하는 것보다 훨씬 저렴하다. 그리고 분류 단계는 분석 단계보다 더 작은 모델을 쓸 수 있는 경우가 많다.
프롬프트 캐싱은 입력 비용을 줄이는 가장 효과적인 수단 중 하나다. OpenAI와 Anthropic 모두 반복되는 프롬프트 접두사를 캐싱하므로, 매 요청 앞에 등장하는 내용 -- 시스템 프롬프트와 도구 정의 -- 은 한 번 캐싱되면 이후 호출에서 훨씬 저렴해진다. 안정적인 내용을 컨텍스트 앞에 두고 호출 사이에 편집하지 마라. 컨텍스트 편집 -- 대화의 앞부분을 재배열하거나, 요약하거나, 다듬는 것 -- 은 캐시를 파괴하므로 신중하게 접근해야 한다.
해야할 일을 배치 작업으로 구조화할 수 있다면 -- 실시간 요건 없이 처리되는 많은 독립적 입력 -- OpenAI와 Anthropic 모두 상당한 할인율로 배치 API를 제공한다. 배치 처리는 대화형 에이전트에는 적합하지 않지만, 평가 실행, 대규모 분류 작업, 지연 시간 제약이 없는 워크로드에서 비용을 크게 줄일 수 있다.
각론
멀티 에이전트 시스템
멀티 에이전트 시스템은 단순히 병렬로 실행하는 방법이 아니다. 두 가지 목적이 있다. 적당한 크기로 작업을 분해하는 것, 그리고 희소하고 제한된 자원인 컨텍스트 윈도를 아끼는 것이다.
컨텍스트 윈도 제약을 과소평가하는 경우가 많다. 컨텍스트 윈도 안의 모든 것이 모델의 주의를 두고 경쟁한다. 모든 중간 결과, 모든 도구 응답, 탐색하다 막힌 모든 막다른 길. 하나의 에이전트가 큰 작업을 처리하면 이 모든 것이 한 곳에 쌓인다. 정작 중요한 부분에 다다를 때쯤이면, 이전 단계에서 나온 무관하거나 오히려 방해가 되는 자료들로 컨텍스트가 가득 차 있다. 별도의 에이전트는 각자 자신의 작업에만 관련된 깔끔하고 집중된 컨텍스트를 갖는다.
작업 분해가 또 다른 이유다. 하나의 에이전트가 잘 처리하기에 너무 큰 작업은 보통 상호 의존성이 적은 하위 작업으로 나눌 수 있다. 오케스트레이터의 역할은 그 구조를 파악하는 것이다. 어떤 부분이 독립적으로 진행될 수 있는지, 어떤 것이 순서를 지켜야 하는지, 어떤 결과를 마지막에 종합해야 하는지. 이것은 단순한 프롬프팅 문제가 아니라 설계의 문제이다.
멀티 에이전트 아키텍처가 항상 올바른 선택은 아니다. 작업이 본질적으로 순차적이라면 -- 각 단계에서 이전의 모든 것에 대한 완전한 지식이 필요하다면 -- 에이전트를 분리해도 얻을 것이 거의 없고 문제점만 많아진다. 넓은 작업, 즉 병렬로 진행되다가 마지막에 종합하는 작업이 잘 맞는다. 단계 간 상호 의존성이 강한 촘촘하게 결합된 작업은 그렇지 않다.
멀티 에이전트 시스템은 비싸다. Anthropic에 따르면 멀티 에이전트 연구 시스템은 표준 채팅보다 약 15배 많은 토큰을 사용했다 [7]. 작업이 충분히 복잡하고 출력이 가치 있을 때만 그러한 비용이 정당화될 수 있다. 단일 에이전트로 처리할 수 있는 작업을 멀티 에이전트 시스템으로 하는 것은 낭비일 뿐이다.
서브에이전트
서브에이전트는 오케스트레이터가 특정 하위 작업을 처리하기 위해 생성하는 에이전트이다. 별도의 컨텍스트 윈도와 도구, 실행 루프를 갖는다. 오케스트레이터는 작업을 위임하고, 결과를 기다리고, 그 결과를 자신의 컨텍스트에 통합한다.
서브에이전트에는 명확한 종료 조건이 필요하다. 완료되었음을 알리고 오케스트레이터가 사용할 수 있는 결과를 내놓는 무언가가 있어야 한다. 가장 깔끔한 메커니즘은 전용 출력 도구다. 모델이 출력 도구를 호출하면 실행이 끝나고 결과가 반환된다. 이것은 서브에이전트의 마지막 응답을 출력으로 사용하는 것보다 낫다. 명시적이고, 구조화되어 있고, 파싱하기 쉽기 때문이다.
Armin Ronacher는 모델이 출력 도구를 호출하지 못하는 경우가 있다고 지적한다 [8]. 이것은 실제 문제지만 해결할 수 없는 문제는 아니다. OpenAI와 Anthropic API 모두 특정 도구를 강제로 호출하게 할 수 있는 tool_choice 파라미터를 지원한다. 서브에이전트 실행이 끝날 때 작업을 마친 후 tool_choice를 출력 도구로 설정해 마지막 API 호출을 할 수 있다. 이렇게 하면 모델이 스스로 출력 도구를 호출하지 않으려 하더라도 구조화된 출력을 내도록 강제할 수 있다.
더 까다로운 문제는 실패다. 서브에이전트는 실패할 수 있으며, 가장 큰 피해를 주는 실패 방식은 명확한 오류가 아니라 진전 없이 길게 이어지는 실행이다. 문제가 되는 것은 오류의 증폭이다. 모델이 도구 결과를 잘못 읽고, 잘못된 방향으로 나아가고, 이후의 각 단계가 그 잘못된 기반 위에 쌓인다. 이런 실행은 턴 수 측면에서 가장 긴 경향이 있다. 성공할 서브에이전트는 대개 예측 가능한 턴 수 안에 성공한다. 그 지점을 넘어서도 계속 가는 것은 대개 막힌 것이다.
실용적인 해결책은 턴수 제한이다. 턴수 제한은 에이전트를 여러 작업에 실행해보고 성공적인 실행이 어디서 끝나는지 관찰해서 경험적으로 결정한다. 제한에 도달하면, 실행을 계속하게 두는 대신 포기하고 다시 시도한다. 깔끔한 컨텍스트로 새로 시작하면 길게 늘어진 실행이 실패할 곳에서 성공하는 경우가 많다. 이것은 에이전트가 점진적으로 진전을 이루고 있다고 생각한다면 직관에 반하지만, 오류 증폭은 막힌 에이전트가 나아지는 것이 아니라 종종 나빠지고 있음을 의미한다.
턴수 제한은 개발 중에 유용하기도 하다. 에이전트가 일상적으로 제한에 도달한다면, 그것은 작업 분해가 손질이 필요하다는 신호이거나, 도구가 모델에게 필요한 것을 주지 않고 있거나, 프롬프트가 언제 작업이 완료되었는지 충분히 명확하지 않다는 신호이다.
코드 생성
에이전트가 도구로 복잡한 작업을 해야 할 때 -- 여러 도구를 순서대로 호출하거나, 큰 결과를 필터링하거나, 항목 목록을 반복 처리하는 것 -- 단순한 접근법은 모델이 도구를 하나씩 호출하고, 호출 사이마다 모델을 거치는 것이다. 이것은 작동하지만 비싸고 느리다. 더 나은 방법이 있다. 모델에게 그 모든 것을 하는 코드를 생성하게 한 다음 코드를 실행하는 것이다.
이것이 가능한 이유는 언어 모델이 코드 생성에 유독 뛰어나기 때문이다. 언어 모델은 훈련에서 도구 호출보다 훨씬 많은 실제 코드를 보았다. 도구를 프로그래밍 언어의 호출 가능한 함수로 제시하면, 모델은 프로그래머가 그러듯이 반복문, 조건문, 오류 처리에 대해 추론할 수 있다. Cloudflare는 Code Mode [9]에서 명시적으로 그렇게 주장한다. 도구 호출은 모델이 드물게 접하는 패턴에 의존하지만, 코드 생성은 모델이 깊이 내면화한 패턴에 의존한다.
코드 생성의 토큰 절약 효과는 크다. 전통적인 도구 호출 루프에서는 모든 중간 결과가 모델의 컨텍스트 윈도를 거친다. 2시간짜리 회의 녹취를 가져와 CRM에 첨부하면, 전체 녹취가 컨텍스트에 두 번 들어간다. 20명 직원의 예산 데이터를 하나씩 조회하면, 요약하기 전에 20개의 응답이 모두 컨텍스트에 적재된다. 코드 생성을 사용하면 중간 결과가 실행 환경에 머물고, 최종 출력 -- 필터링된 요약, 합계 -- 만 모델에게 돌아간다. Anthropic은 대표적인 사례에서 토큰 사용량을 15만에서 2천으로 줄였다고 보고한다 [10].
실용적인 구현에는 세 가지가 필요하다.
코드 실행 환경. 생성된 코드가 어딘가에서 실행되어야 한다. 샌드박스가 필요하다. 네트워크 접근을 제한하고, 의도한 것 이외의 파일시스템 접근을 금지해야 한다. Cloudflare는 V8 isolate를 사용하고, Anthropic은 Python 컨테이너를 사용한다. 직접 구축한다면 인프라가 간단하지는 않지만, 샌드박스는 일반적으로 사용할 수 있다.
함수로 노출된 도구. 모델은 어떤 함수가 사용 가능하고 무엇을 반환하는지 알아야 한다. 출력 형식에 대한 설명이 중요하다. 도구가 JSON을 반환한다면 스키마를 설명하라. 모델이 코드를 작성하려면 기대해야 하는 결과를 알아야 한다.
도구별 옵트인. 모든 도구가 생성된 코드에서 호출 가능해야 하는 것은 아니다. Anthropic의 API는 각 도구 정의의 allowed_callers 필드로 이를 구현한다 [11]. 모델이 직접 호출하는 도구와 코드에서 호출하는 도구를 구분한다. 이 구분은 보안상 중요하다. 부작용이 있거나 민감한 출력을 가진 도구는 두 맥락에서 다른 처리가 필요할 수 있다.
도구 사용 외에도 같은 원칙이 적용된다. 에이전트가 데이터를 처리해야 할 때 -- 파일을 변환하거나, 질의 결과를 집계하거나, 목록을 필터링하는 -- 코드를 작성하게 하고 그 코드를 실행하는 것이 자연어로 데이터에 대해 추론하게 하는 것보다 나은 경우가 많다. 모델의 코드 생성 능력은 코딩 에이전트만을 위한 기능이 아니라 기본적인 도구이다.
이 패턴을 채택하고 싶다면, MCPorter [12]가 도움이 될 수 있다. MCPorter는 MCP 서버의 도구 정의에서 TypeScript 래퍼를 생성하는 오픈 소스 TypeScript 라이브러리이다.
한 가지 주의사항이 있다. 이 패턴은 실행 환경이 진정으로 격리되어 있어야 한다. 생성된 코드는 신뢰할 수 없는 입력이다. 에이전트가 악의적인 코드를 생성하게 하는 프롬프트로 공격당할 수 있다. 샌드박싱은 선택사항이 아니다.
구조화된 출력
에이전트가 기계가 읽을 수 있는 출력을 내놓아야 할 때는 -- 분류, 결정, 필드 추출 -- 구조화된 출력이 올바른 도구다. 텍스트를 파싱하는 대신, 스키마를 정의하고 모델이 채운다. 더 신뢰할 수 있고, 테스트하기 더 쉽고, 파싱 버그를 통째로 제거한다.
언어 모델은 토큰을 왼쪽에서 오른쪽으로 순서대로 생성한다. 스키마를 {"answer": "..."} 로 정의하면, 모델은 바로 답을 정한다. {"reasoning": "...", "answer": "..."} 로 정의하면, 모델은 먼저 추론하도록 강제되고 그 추론이 답에 영향을 미친다. 추론 필드가 스키마에서 답 필드보다 앞에 오므로, 출력에서도 답 앞에 온다.
나중에 추론을 완전히 버리고 답만 사용해도 된다. 성능상의 이점은 추론을 읽는 것이 아니라 모델이 추론을 생성했다는 데서 온다. 이 방법은 특별한 모델 지원 없이 추론 모델과 같은 효과를 얻을 수 있게 해 준다.
파일 편집
에이전트가 파일을 수정해야 한다면 파일 편집을 어떻게 구현하느냐가 시스템 성능에 중요한 영향을 미친다.
이것은 내 경험만이 아니다. Anthropic은 파일 편집 신뢰성을 명시적으로 어려운 문제 중 하나로 꼽았다 [13]. Anthropic이 API로 제공하는 텍스트 편집기 도구 [14]를 보면, str_replace 명령은 정확한 문자열 일치를 필요로 하며, 하니스는 문자열이 일치하지 않거나 여러 번 일치할 때 오류를 반환해야 한다. 문제가 충분히 어렵기 때문에 Anthropic은 도구 설계에 우회책을 내장했다 (예를 들어 파일의 절대 경로를 요구하는 것은 명시적인 오류 방지 조치다).
어려운 점은 모델이 원하는 변경에 대해 추론할 뿐 아니라, 모호함이나 오류 없이 파일에 기계적으로 적용할 수 있는 형식으로 출력을 내놓아야 한다는 것이다. 이것은 서로 다른 일이며, 어떤 형식을 선택하느냐에 따라 기계적인 적용 단계가 얼마나 자주 실패하는지가 달라진다.
현재 사용되는 주요 접근법은 다음과 같다.
전체 파일 재작성. 모델이 파일의 내용을 완전히 새로 출력한다. 구현하고 파싱하기 단순하며, 형식 오류로 실패하지 않는다. 단점은 비용(출력 토큰이 파일 크기에 비례해 증가함)과 주변 컨텍스트 손실이다. 작은 파일에서만 실용적이다.
문자열 교체. 모델이 이전 문자열과 새 문자열을 출력하면, 하니스가 찾아서 교체한다. Anthropic이 사용하는 방식이다 [14]. 실패 방식은 잘 알려져 있다. 모델은 공백과 들여쓰기를 포함해 이전 문자열을 글자 하나 하나 그대로 재현해야 하는데, 이것을 자주 틀린다. "교체할 문자열을 찾지 못했다"는 오류는 에이전트 실패의 흔한 원인이다.
patch/diff 형식. 모델이 변경사항을 설명하는 구조화된 diff를 출력한다. OpenAI의 Codex는 *** Begin Patch 와 *** End Patch 마커가 있는 커스텀 패치 형식을 사용한다. 그 자체로는 쉽게 망가지지만 Codex는 제약된 샘플링(constrained sampling)으로 이를 해결한다. 패치 형식을 Lark 문맥 자유 문법(context free grammar)으로 표현하고, 추론 시 모델 출력을 문법에 맞게 제한한다 [15]. 이것은 형식 오류를 통째로 제거한다. 이것이 OpenAI의 공개 API를 사용해 이루어진다는 점이 중요하다 [16]. 이 기법은 누구나 사용할 수 있다.
훈련된 병합 모델. Cursor는 모델의 편집 의도를 원본 파일과 병합하는 별도의 70B 모델을 훈련했다. 병합 견고성을 학습된 능력으로 만들어 형식 문제를 완전히 우회한다. 명백한 비용은 전용 모델을 훈련하고 서빙하는 데 상당한 자원이 필요하다는 것이다.
Can Bölük은 16개 모델을 180개 과제에서 벤치마킹하여 형식 선택만으로도 성공률이 달라질 수 있음을 보였다 [17]. 그의 글은 이 장과 함께 읽을 가치가 있다. 그가 제안한 형식은 각 줄에 줄 번호와 짧은 해시를 태그한다. 주요 이점은 모델이 정확한 내용을 재현하지 않고 식별자로 줄을 참조할 수 있다는 것인데, 이것이 모델에게는 훨씬 쉽다. 해시는 줄 번호에 더해서 체크섬 역할을 한다. 이전 편집으로 줄이 밀렸다면, 예상 해시와 실제 줄 내용 사이의 불일치가 잘못된 줄을 조용히 편집하는 대신 오류를 잡아낸다.
도구 인가 제어
에이전트에게 도구를 준다는 것은 세상에서 실제 행동을 취할 수 있는 능력을 주는 것이다 -- 파일 읽기, 파일 쓰기, 명령 실행, 외부 서비스 호출. 도구 인가 제어는 에이전트가 자율적으로 취할 수 있는 행동과 사람의 승인이 필요한 행동을 결정하는 방법이다. 이것을 제대로 하는 것은 안전과 사용성 모두에 중요하다. 너무 제한적이면 에이전트가 일을 할 수 없고, 너무 허용적이면 모르는 사이에 피해를 줄 수 있는 자율 시스템이 된다.
먼저 이해해야 할 것은 인가와 샌드박싱이 상호 보완적이며 서로 대체할 수 없다는 것이다. 인가는 에이전트가 무엇을 하기로 결정하는지를 제어하며 에이전트 수준에서 작동한다. 샌드박싱은 에이전트가 무엇을 결정하든 관계없이 OS 수준에서 제한을 강제한다. Claude Code의 문서 [18]는 이 구분을 명확히 한다. 인가는 에이전트가 제한된 행동을 시도하는 것을 막고, 샌드박싱은 에이전트가 제한된 행동을 시도하더라도 그러한 행동이 실제로 실행되는 것을 막는다. 둘 다 사용해야 한다.
덜 명백한 점은 Bash 인가 규칙이 보이는 것보다 강한 점도 있고 약한 점도 있다는 것이다.
보이는 것보다 강한 이유는 셸 명령이 문자열로만 매칭되지 않고 파싱되기 때문이다. Claude Code는 오픈 소스가 아니지만, Bun을 사용한다고 알려져 있는데, Bun에는 셸 파서가 포함되어 있다. Codex(오픈 소스)는 Tree-sitter의 Bash 파서로 같은 작업을 한다 [19]. 스크립트가 완전한 AST로 파싱되고, 단순한 명령 이외의 것이 포함되면 파싱이 거부된다. 허용된 연산자 (&&, ||, ;, |)는 각 개별 명령을 추출하고 각각을 인가 규칙과 별도로 확인하는 방식으로 처리된다. 즉 Bash(safe-cmd *)는 safe-cmd && malicious-cmd를 허용하지 않는다. 파서가 두 개의 명령을 보고 둘 다 확인한다.
보이는 것보다 약한 이유는 명령 이름 수준에서 안전성을 알 수 없기 때문이다. 고전적인 예시가 있다. rm을 거부하고 find를 허용해도 파일 삭제가 막히지 않는다. find에는 -delete 옵션이 있기 때문이다. 많은 유닉스 명령이 이처럼 다목적이다. 특정 명령이 안전하다는 가정 하에 작성된 단순한 허용 목록은 이러한 구멍이 생기는 경향이 있으며, 에이전트나 프롬프트 인젝션을 통해 에이전트를 제어하는 공격자는 그러한 구멍을 찾아낼 수 있다.
코딩 에이전트를 위한 실용적인 인가 모델은 이런 모습일 수 있다. 읽기 작업은 승인이 필요 없다. 파일 편집은 세션당 한 번 승인이 필요하다. 셸 명령은 명령당 승인이 필요하되, 테스트 실행이나 프로젝트 빌드 같은 일반적이고 안전한 작업은 미리 승인된 허용 목록에 넣는다.
파일이나 웹를 읽는 에이전트는 그 내용으로부터 공격자의 지시를 받을 수 있다. 엄격한 인가 규칙이 주요 방어책이다. 데이터를 유출하라는 지시는 에이전트가 외부 URL에 도달할 수 없다면 성공할 수 없다.
참고문헌
[1] Takeshi Kojima et al., "Large Language Models are Zero-Shot Reasoners", 2022-05-24. https://arxiv.org/abs/2205.11916
[2] Lennart Meincke et al., "Prompting Science Report 2: The Decreasing Value of Chain of Thought in Prompting", 2025-06-08. https://arxiv.org/abs/2506.07142
[3] OpenAI, "Why SWE-bench Verified no longer measures frontier coding capabilities", 2026-02-23. https://openai.com/index/why-we-no-longer-evaluate-swe-bench-verified/
[4] Langfuse. https://langfuse.com/
[5] Pydantic Logfire. https://pydantic.dev/logfire
[6] Agent Skills. https://agentskills.io/
[7] Anthropic, "How we built our multi-agent research system", 2025-06-13. https://www.anthropic.com/engineering/multi-agent-research-system
[8] Armin Ronacher, "Agent Design Is Still Hard", 2025-11-21. https://lucumr.pocoo.org/2025/11/21/agents-are-hard/
[9] Cloudflare, "Code Mode: the better way to use MCP", 2025-09-26. https://blog.cloudflare.com/code-mode/
[10] Anthropic, "Code execution with MCP: Building more efficient agents", 2025-11-04. https://www.anthropic.com/engineering/code-execution-with-mcp
[11] Anthropic, "Programmatic tool calling". https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling
[12] Peter Steinberger, MCPorter. https://github.com/steipete/mcporter
[13] Anthropic, "Raising the bar on SWE-bench Verified with Claude 3.5 Sonnet", 2025-01-06. https://www.anthropic.com/engineering/swe-bench-sonnet
[14] Anthropic, "Text editor tool". https://platform.claude.com/docs/en/agents-and-tools/tool-use/text-editor-tool
[15] OpenAI, codex-rs/core/src/tools/handlers/apply_patch.rs, tool_apply_patch.lark. https://github.com/openai/codex
[16] OpenAI, "Function calling". https://developers.openai.com/api/docs/guides/function-calling
[17] Can Bölük, "I Improved 15 LLMs at Coding in One Afternoon. Only the Harness Changed.", 2026-02-12. https://blog.can.ac/2026/02/12/the-harness-problem/
[18] Anthropic, "Configure permissions". https://code.claude.com/docs/en/permissions
[19] OpenAI, codex-rs/shell-command/src/bash.rs. https://github.com/openai/codex