
만능 에이전트 1개를 포기하기 — GitHub Copilot 커스텀 에이전트와 자체 제작 MCP 서버로 '분업' 설계하기
요약
GitHub Copilot 커스텀 에이전트와 MCP 서버를 활용하여 단일 에이전트의 한계를 극복하는 '분업형 에이전트' 설계 방식을 소개합니다. 역할을 명사(직업)가 아닌 동사(동작) 단위로 최소화하여 에이전트의 정확도와 관측 가능성을 높이는 구현 전략을 다룹니다.
핵심 포인트
- 단일 에이전트 사용 시 긴 절차로 인한 지시 사항 망각 및 판단 오류 발생
- 역할을 명사가 아닌 '동사' 단위로 쪼개어 에이전트의 경계를 명확히 정의
- MCP 서버를 통해 외부 SaaS와 에이전트를 연결하는 구조 설계
- 에이전트 분업을 통해 실패 원인 파악 및 프롬프트 최적화 용이성 확보
이 기사에서 다루는 내용
나리지 베이스(Help Center)의 기사 업데이트를 GitHub Copilot의 커스텀 에이전트(Custom Agent)에게 분업시킨 구성의 설계와 구현을 정리합니다.
- 총괄 역할 1개 + 전문 에이전트 3개라는 구성
- 외부 SaaS는 자체 제작한 MCP 서버를 통해 조작한다
- 실제로 구동하며 알게 된 점과 미해결 과제로 남아 있는 과제
예상 독자는 커스텀 에이전트를 '편리한 페르소나 1개' 이상으로 활용하고 싶은 사람, MCP 서버를 직접 제작하여 수중에 있는 업무 시스템에 연결하고 싶은 사람입니다.
전제 조건 및 이 기사의 코드에 대하여
- VS Code의 GitHub Copilot Chat(커스텀 에이전트와 MCP에 대응하는 버전)을 사용하고 있습니다.
- 대상은 '기사를 REST API로 CRUD 할 수 있는 나리지 베이스 SaaS'입니다. 이후, 자체 제작한 MCP 서버를
kb-articles라는 이름으로 작성하겠습니다. 동일한 형태의 API를 가진 서비스라면 바꾸어 생각할 수 있습니다. - 에이전트 정의는 실제로 구동한 구성을 바탕으로 정리한 것입니다.
- 프롬프트 파일의 프론트매터(Frontmatter)는 집필 시점의 공식 문서 사양에 맞춰 업데이트했습니다 (후술).
- MCP 서버 코드는 설명을 위한 발췌본입니다. 인증 정보 처리, 페이지네이션(Pagination), 에러 핸들링(Error Handling)은 생략했습니다.
- 이 영역은 사양 업데이트가 빠르므로, 프론트매터의 키 이름은 공식 문서에서 확인하시기 바랍니다.
에이전트 1개로 통합했을 때 발생한 문제
처음에는 에이전트 1개에 모든 것을 작성했습니다. "매뉴얼을 읽고, 기존 기사를 조사하고, 없으면 만들고, 있다면 내용이 어긋날 경우 다시 만들고, 마지막으로 리뷰해"라고 나열했을 뿐입니다.
작동은 합니다. 하지만 구동해 보니 다음과 같은 세 가지 문제가 나타났습니다.
- 절차가 길어질수록 후반부 지시가 희미해진다. 기사는 만들어지지만, 마지막에 작성한 리뷰 관점이 지켜지지 않는 형태로 무너집니다.
- 유사하지만 다른 조작이 동일한 문맥에 공존한다. "새로 만들기"와 "삭제 후 다시 만들기"가 하나의 프롬프트에 나열되어 있으면 판단이 흔들려, 만들기만 하면 되는 상황에서 기존 기사를 아카이브(Archive)하러 갑니다.
- 실패의 원인을 구분할 수 없다. 결과물이 좋지 않을 때, 읽기가 잘못된 것인지, 쓰는 방식이 잘못된 것인지, 리뷰가 미흡했던 것인지 알 수 없습니다.
세 번째가 가장 까다로웠습니다. 프롬프트를 수정해도 어디에 효과가 있었는지 관측할 수 없습니다. 1개에 긴 절차를 맡기는 한, 수정 방식은 추측에 의존할 수밖에 없습니다.
그래서 1개의 프롬프트를 길게 만드는 것을 그만두고, 짧은 프롬프트를 가진 에이전트를 여러 개 준비하여, 호출 측은 순서와 입출력만을 가지는 구성으로 변경했습니다.
설계 방침: 역할을 최소한으로 하기 (결과적으로 '동사'로 구분됨)
설계 시 의식했던 것은 1개에 맡기는 역할을 가능한 한 작게 만드는 것뿐이었습니다. "이것도 맡길 수 있겠는데"라며 추가하다 보면, 앞 절에서 언급한 "절차가 길어질수록 후반부가 희미해지는" 문제로 되돌아가기 때문입니다.
깎아 나간 결과, 남은 것은 명사(라이터, 편집자, 교열자)가 아니라 동사(만들기, 다시 만들기, 대조하기)였습니다. 명사로 나누면 "편집자는 신규 기사를 써도 되는가?"가 정의되지 않아 경계가 모호한 채로 남습니다. 동사까지 깎아내면 해당 에이전트에게 전달해야 할 도구(Tool)가 자동으로 결정됩니다.
| 에이전트 | 담당하는 동사 | 필요한 조작 |
|---|---|---|
create-article | 존재하지 않는 기사를 만들기 | 검색 / 섹션 목록 / 생성 |
recreate-article | 어긋난 기사를 다시 만들기 | 검색 / 아카이브 / 생성 |
review-article | 기사와 원본 자료를 대조하기 | 검색 / (필요 시 재생성) |
lead | 순서와 반복을 결정하기 | 서브 에이전트 호출 |
이러한 구분 방식에서 설계상의 판단 두 가지가 도출됩니다.
1. 상태는 에이전트가 갖지 않고 외부 시스템에 둔다
"어떤 기사를 만들었는가"를 에이전트의 기억에 담아두면, 대화가 길어진 시점에서 망가집니다. 정답은 항상 SaaS 측에 있다고 가정하고, 각 에이전트의 절차 1단계는 반드시 "검색"부터 시작하도록 했습니다. 결과적으로 도중에 실패하더라도 동일한 지시를 다시 던질 수 있습니다.
2. 도구의 입도(Granularity)가 에이전트의 입도를 결정한다
MCP 서버 측에 어떤 도구를 배치하고 어떤 것을 전달하느냐에 따라 에이전트가 취할 수 있는 수단이 결정됩니다. 이번에는 결과적으로 "기사 업데이트"를 사용하지 않는 구성이 되었으며, 수정은 반드시 "아카이브 후 다시 만들기"가 되었습니다. 그 경위와 부작용은 후반부에서 다루겠습니다.
물론, 이러한 분할 방식이 유일한 정답이라고 생각하지는 않습니다. 역할을 나눈 이유는 한 개의 에이전트에게 너무 긴 절차를 맡기면 시스템이 무너지기 때문입니다. 모델의 성능이 향상되면, '편집자'와 같이 한 명에게 작성과 업데이트를 모두 맡겨도 충분히 제어할 수 있게 될지도 모릅니다. 분업은 모델의 제약 사항에 대응하기 위한 수단이지, 그 자체가 목적은 아닙니다.
파일 구성은 다음과 같습니다.
.github/
├── agents/
│ ├── lead.agent.md # 총괄 역할
...
구현 1: 외부 시스템을 조작하게 하는 자체 제작 MCP 서버
왜 API를 직접 호출하게 하지 않는가
"HTTP 요청을 보낼 수 있는 도구를 전달하고, API 명세를 프롬프트에 작성한다"는 방식도 일단 작동은 합니다. 하지만 이 방식을 포기한 이유는 세 가지입니다.
- API 명세를 프롬프트에 작성하면, 그것만으로도 에이전트 정의가 길어집니다 (앞서 언급한 "후반부가 부실해지는" 문제가 재발합니다).
- 엔드포인트(Endpoint)의 조합을 에이전트가 매번 생각해야 하므로, 동일한 작업이라도 절차가 어긋날 수 있습니다.
- 위험한 조작(삭제 등)을 허용하지 않겠다는 제어를 프롬프트에만 의존하게 됩니다.
MCP 서버로 만들면, 도구 이름(Tool name)과 docstring이 그대로 명세가 되며, 전달할 도구를 제한하는 것이 곧 권한 설계가 됩니다.
구현
Python 기반의 경량 MCP 서버 구현 라이브러리를 사용하면, 함수 이름이 그대로 도구 이름이 되고, docstring은 설명이 되며, 타입 힌트(Type hint)가 입력 스키마(Input schema)가 됩니다. 도구 정의만을 위해 작성해야 하는 코드는 거의 없습니다.
# mcp/kb_articles.py
import os
import httpx
...
작성하면서 효과적이라고 느낀 점은 다음 두 가지입니다.
docstring은 사람을 위한 주석이 아니라, 에이전트를 위한 명세서로서 작성한다.
특히 "반환값에 무엇이 포함되는지", "호출하기 전에 무엇을 완료해야 하는지"를 적어두면, 후속 공정에서 ID를 혼동하는 사고를 줄일 수 있습니다. 위의 search_articles docstring에 "여기서 얻은 id를 사용할 것"이라고 적어둔 이유도 바로 이 때문입니다.
API의 생(Raw) JSON을 그대로 반환하지 않는다.
응답을 그대로 반환하면 토큰을 많이 소비할 뿐만 아니라, 에이전트가 본론과 관계없는 필드(업데이트 일시나 라벨 등)를 근거로 판단을 내리기 시작합니다. 필요한 필드만으로 압축하여 반환하는 것은 성능 대책인 동시에, 판단의 편차를 막기 위한 대책이기도 합니다.
VS Code에 등록
워크스페이스 단위라면 .vscode/mcp.json에 작성합니다.
{
"servers": {
"kb-articles": {
...
구현 2: 전문 에이전트 3개
각 에이전트는 .github/agents/ 하위에 <name>.agent.md로 배치합니다. 프런트매터(Frontmatter)에서 설명과 사용할 수 있는 도구를 선언하고, 본문에 절차를 작성합니다.
신규 생성 에이전트
---
description: '매뉴얼에 기반하여, 지식 베이스(Knowledge Base) 상에 존재하지 않는 기사를 신규 생성한다.'
tools: ['todo', 'kb-articles']
...
절차 2의 "존재하는 경우에는 생성하지 않고 종료한다"와 제약 사항의 "아카이브하지 않는다"가 핵심입니다. 책임 범위를 벗어난 조작을, 도구를 전달하지 않는 것과 문장 모두를 통해 이중으로 차단하고 있습니다.
다만 절차 2는, 검색이 기존 기사를 제대로 찾아준다는 것을 전제로 합니다. 이 부분이 이번에 가장 잘 풀리지 않았던 부분이라, 후반부에서 다시 다루겠습니다.
재작성 에이전트
---
description: '매뉴얼에 기반하여, 내용이 괴리된 기존 기사를 다시 만든다.'
tools: ['todo', 'kb-articles']
...
효과를 보고 있는 것은 절차 3입니다. "수행하지 않을 조건"을 명시적인 분기로 설정해두지 않으면, 에이전트는 작업을 수행하는 방향으로 치우치게 됩니다. "재작성하는 에이전트"로 호출된 이상, 아무것도 하지 않고 끝낸다는 선택지는 명시해두지 않으면 존재하지 않습니다.
리뷰 에이전트
---
description: '지식 베이스의 기사가 원래 매뉴얼을 따르고 있는지 리뷰한다.'
tools: ['todo', 'kb-articles']
...
이 에이전트에게는 쓰기 권한을 부여하지 않았습니다. 리뷰와 수정을 동일한 에이전트에게 맡기면, 지적이 나올 때마다 그 자리에서 기사가 다시 작성되어 무엇이 어떻게 바뀌었는지 추적할 수 없게 되기 때문입니다. 읽는 역할과 쓰는 역할을 분리해 두면, 적어도 "무엇을 문제라고 판단했는가"가 output으로 남습니다.
구현 3: 통합 에이전트 (統括エージェント)
---
description: '매뉴얼을 입력으로 하여, 지식 베이스 (Knowledge Base) 기사의 작성·재작성·리뷰를 통합 관리한다.'
tools: ['agent', 'todo', 'kb-articles']
...
위임(Delegation)을 작성할 때 효과적이었던 점은 다음 3가지입니다.
1. 전달하는 것은 "이전 공정의 출력"이지, 대화 전체가 아니다.
무엇을 전달할지 명시하지 않으면, 통합 역할의 에이전트가 자신의 요약본을 전달합니다. 요약 과정에서 누락된 정보는 다시 돌아오지 않습니다.
2. 반복 작업에는 종료 조건과 상한을 명시한다.
"부족함이 없어질 때까지 반복한다"라고만 하면, 판정이 주관적이기 때문에 끝나지 않습니다. 횟수 상한과, 상한에 도달했을 때의 동작(사람에게 전달)까지 작성합니다.
3. 통합 역할에게 실무 도구를 부여한다면, 사용하지 않을 조건도 작성한다.
kb-articles를 부여한 것은 검색(현황 파악)을 위해서입니다. 다만, 쓰기가 가능한 도구가 손에 있는 상태에서 "기사를 업데이트해줘"라는 명령을 받으면, 스스로 해버릴 여지가 남습니다. 부여하는 도구와, 사용해도 좋은 범위는 별도로 작성하기로 했습니다.
실행 방법
통합 에이전트를 선택한 상태에서 "기사를 업데이트해줘"라고 던지면 작동하지만, 선택을 잊으면 단순한 범용 에이전트로 작동해 버립니다. 프롬프트 파일로 고정해 두면 사고를 줄일 수 있습니다.
---
description: '매뉴얼로부터 지식 베이스 기사를 업데이트한다'
agent: 'lead'
...
참고로, 프롬프트 파일에서 실행 에이전트를 지정하는 키는 이전에는 mode라는 이름이었습니다. 집필 시점의 VS Code 문서에서는 agent로 되어 있습니다. 로컬에서 작동하지 않는다면 먼저 이 부분을 의심해 보세요.
실행하며 알게 된 점
솔직히 말하자면, "나쁘지는 않지만, 그대로 무인 운용은 할 수 없다"가 결론이었습니다. 막혔던 부분과 설계 시 유효했던 판단 4가지를 꼽겠습니다.
1. 공정을 넘어가면 전달된 정보가 요약된다
이것은 운용 중에 곤란했다는 이야기가 아닙니다. 실제로 후술하듯 입력을 기능 단위로 좁혀 두었기에 크게 파탄 나지는 않았습니다. 다만, 구조적으로 이렇게 되어 있다는 점은 파악해 둘 가치가 있습니다.
공정 간의 전달은 결국 프롬프트 문자열입니다. 이전 공정이 읽은 자료의 세부 사항은 요약된 만큼만 다음으로 전달됩니다. 전달하는 양이 많아질수록 이 정보 손실은 커집니다.
따라서 다루는 자료가 큰 경우에는 중간 결과물을 파일(또는 지식 베이스 측의 초안)로 내려서, 본문이 아닌 참조처를 전달하는 것이 더 안전하다고 생각합니다. 문자열로 전달하면 요약되지만, 참조처라면 다음 공정이 직접 읽으러 갈 수 있습니다. 이번 구성에서도 기사의 실체는 지식 베이스 측에 있고, 공정 간에 전달하는 것은 id와 url이므로, 의식하지 않아도 실질적으로는 이 형태가 되어 있었습니다.
2. 성공이 반환되는데, 업데이트되지 않는다
앞선 코드 발췌에는 포함하지 않았지만, update_article에 해당하는 도구도 구현했습니다. 그런데 호출하면 응답은 200이 반환되는데, 기사 내용이 바뀌지 않는 현상이 발생했습니다.
에이전트 입장에서는 성공이므로 그대로 다음 공정으로 진행합니다. 리뷰에서 "고쳐지지 않았다"라고 지적받고, 다시 업데이트하여 성공이 반환되는... 식의 헛바퀴를 돌게 됩니다. 실패가 실패로서 반환되지 않는 경로가 단 하나라도 있으면, 상류의 프롬프트를 아무리 잘 다듬어도 의미가 없어집니다.
원인을 끝까지 파헤치기 전에, 우선 업데이트 도구를 사용하지 않는 방침으로 전환했습니다. 에이전트에게 주는 도구에서 제외하고, 수정은 "아카이브 후 재작성"으로 일원화했습니다. 신뢰할 수 없는 결과를 반환하는 도구를 쥐여주는 것보다, 돌아가더라도 결과를 관측할 수 있는 경로로 몰아주는 것이 낫다는 판단입니다.
다만, 이 회피책에는 대가가 따릅니다.
- 기사의 URL이 바뀜 (외부에서 붙여넣은 링크가 깨짐)
- 조회수 등 기사에 연결된 통계가 리셋됨
- 리뷰에서 세세한 지적이 하나 나올 때마다 기사 전체가 재작성됨
에이전트에게 전달할 도구는 기능이 갖춰져 있는지뿐만 아니라, "돌아오는 결과가 신뢰할 수 있는가"를 기준으로 선택해야 합니다. 사람이 사용하는 API라면 화면을 보고 알아챌 수 있는 불일치도, 에이전트는 반환값(return value)만을 보고 판단하기 때문입니다.
3. 입력의 범위가 곧 정밀도에 직결된다
이것은 시행착오가 아니라 처음부터 취했던 방침입니다. 프롬프트(Prompt)에는 자료 전체를 전달하지 않고, "이 매뉴얼의 ○○라는 기능에 대해 기사를 작성해줘"와 같이 기능 단위까지 스코프(Scope)를 좁혀서 전달했습니다. 범위를 좁힐수록 참조해야 할 부분이 유일하게 결정되어 정밀도가 높아질 것이라고 생각했기 때문입니다.
자료를 통째로 전달할 경우, 에이전트는 자신의 판단에 따라 읽는 범위를 결정합니다. 어디를 읽었는지는 출력 결과만 봐서는 알 수 없으므로, 누락이 발생해도 감지할 수 없습니다. 입력의 범위를 끊는 방식은 곧 "출력을 사람이 검증할 수 있는가"와 직결됩니다. 총괄 역할의 '절차 1'에서 장(Chapter) 단위로 분할하게 만든 것도 이러한 사고방식을 구성 측면에 도입한 것입니다.
4. 멱등성(Idempotency)은 직접 만들어야 한다 (그리고 아직 완벽하지 않다)
같은 지시를 두 번 실행하면, 순순히 중복 기사가 만들어집니다. "만들기 전에 반드시 검색한다"를 각 에이전트의 '절차 1'로 고정하고 나서야 겨우 안정되었습니다. 외부 시스템에 기록하는 에이전트는 재시도(Retry)될 것을 전제로 절차를 설계해야 합니다.
하지만 이것으로 해결되었다고는 말할 수 없습니다. 사전 검색이 생각만큼 잘 맞지 않았습니다. 정확히 계산한 것은 아니어서 인상에 의존하는 부분이 있지만, 이미 기사가 존재함에도 불구하고 "찾을 수 없음 = 신규 생성 필요"라고 판단되는 상황이 기억으로는 꽤 빈번했다고 느낍니다.
이론적으로는 설명이 가능합니다. 검색어를 구성하는 것은 에이전트이고, 기사의 제목을 붙인 것도 별도의 호출을 수행한 에이전트입니다. 같은 기능을 가리키더라도 표현이 어긋나면 검색되지 않으며, 매번 같은 검색어가 구성된다는 보장도 없습니다. 중복 판정을 전문 검색(Full-text search) 결과에 의존하는 이상, 이 허점은 구조적으로 남을 수밖에 없습니다.
그리고 그 영향은 중복 기사가 늘어나는 것뿐만이 아닙니다. 신규 생성 에이전트에는 "기존 기사가 있으면 생성하지 않고 종료한다"라는 분기(Branch)를 두었지만, 그 분기의 판정 재료가 검색 결과이기 때문에, 검색이 빗나가는 순간 호출되지 말아야 할 상황에서 신규 생성이 실행됩니다. "하지 않을 조건"을 적어두어도, 조건의 판정 재료를 신뢰할 수 없다면 효과가 없습니다. 앞서 언급한 업데이트 도구의 사례와 같은 구조입니다.
무엇이 정답인지는 아직 모릅니다. 기사 측에 원본 자료의 식별자(Identifier)를 갖게 하여 대조하거나, 검색이 아닌 목록 전체를 가져와서 판정하는 등의 방향은 생각할 수 있지만, 아직 시도해 보지는 않았습니다. "검색한 뒤에 만든다"는 중복을 줄여주지만, 완전히 막아주는 것은 아니다라는 전제로 운용하고 있습니다. 이 부분은 미결 과제입니다.
이 구성이 적합한 케이스 · 적합하지 않은 케이스
적합함
- 공정이 명확히 나누어져 있고, 각각의 성패를 사람이 판정할 수 있는 작업
- 대상 시스템에 대한 쓰기 작업이 API화 되어 있음 (= 도구로 분리할 수 있음)
- 리뷰 기준을 문장으로 정의할 수 있는 작업
적합하지 않음
- 한 번의 판단으로 끝나는 작업. 분업에 따른 오버헤드(위임·요약·왕복)가 더 큽니다
- 정답이 유일하지 않은 창작 위주의 작업. 리뷰 공정이 수렴하지 않습니다
- 롤백(Rollback)이 어려운 조작을 포함하는 작업. 상한 횟수에서 멈추더라도 중간까지 기록된 상태가 남습니다
요약
- 역할은 작게 유지한다. 한 개체에 맡기는 절차를 줄여나가다 보니, 결과적으로 동사 단위의 분할이 되었고, 전달해야 할 도구가 결정되었으며, 실패 원인을 격리(Isolation)할 수 있게 되었습니다.
- 도구는 "있는가"가 아니라 "결과를 신뢰할 수 있는가"로 선택한다. 성공이 반환되었음에도 반영되지 않거나, 존재하는 것을 찾지 못하는 경우. 이런 경로가 단 하나라도 있으면 상류(Upstream)의 설계는 무효화됩니다.
- 입력의 범위가 정밀도를 결정한다. 자료를 통째로 전달하지 않고 기능 단위까지 좁혀서 전달할수록, 출력을 사람이 검증하기 쉬워집니다.
- 무인화보다 사람이 확인할 수 있는 형태를 우선한다. 읽는 역할과 쓰는 역할을 나누고, 반복에 상한을 두며, 해결되지 않는 것은 인간에게 돌려주는 설계로 만든 것이 결과적으로 쓸모 있는 결과물이 되었습니다.
- 단, 분업은 모델의 제약에 대응하는 수단입니다. 성능이 올라가면 이 정도로 나누지 않아도 성립하게 될지도 모릅니다. "나누는 것" 자체를 목적으로 삼지는 않는 것이 좋다고 생각합니다.
참고
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기