Sentinel 패턴: 자신의 에이전트조차 신뢰하지 않는 멀티 에이전트 개발 루프
요약
멀티 에이전트 시스템에서 발생하는 정보 압축 및 요약 오류를 방지하기 위한 'Sentinel 패턴'을 소개합니다. 에이전트의 보고서를 신뢰하는 대신, 실제 코드 변경 사항(diff)을 검증 게이트로 사용하는 개발 프로세스를 다룹니다.
핵심 포인트
- 에이전트의 요약 과정에서 발생하는 정보 누락 및 왜곡 문제 지적
- 병렬 작업 시 검증되지 않은 주장이 증폭되는 위험성 경고
- 보고서 대신 실제 diff를 데이터로 활용하는 검증 메커니즘 제안
- 오케스트레이션 에이전트와 전문가 에이전트의 순환 구조 활용
한 코더 에이전트(coder agent)가 자신의 작업을 마치고 깔끔한 보고서를 작성하며, 제가 요청한 사항 중 9가지를 구현했다고 말했습니다. 저는 장부에 9라고 적고 다음 단계로 넘어갔습니다. 나중에 다른 일을 하던 중, 제가 직접 결과물들을 열거하며 세어보니 7개였습니다.
극적인 일은 일어나지 않았습니다. 아무도 버그를 배포하지 않았고, 저는 제때 버그를 잡아냈습니다. 하지만 그때쯤이면 그 숫자는 루프의 나머지 부분이 읽어들이는 파일에 이미 기록된 상태였고, 이는 나중에 수행된 작업이 그 숫자를 인용했음을 의미하며, 결국 잘못된 사실이 출처(provenance)를 얻게 되었음을 의미합니다. 이것이 제가 계속해서 마주하게 되는 실패의 형태입니다. 왜냐하면 이것은 모델의 실패도 아니고, 더 나은 프롬프팅(prompting)으로 해결할 수 있는 문제도 아니기 때문입니다. 에이전트는 자신의 압축된 작업 컨텍스트(working context)로부터 보고서를 생성했는데, 압축 과정에서는 정확히 감사(audit)하고 싶은 세부 사항들이 누락됩니다. 에이전트는 거짓말을 한 것이 아닙니다. 요약(summarising)을 한 것이고, 요약은 반올림을 하기 마련입니다.
이 포스트는 제가 맥북 한 대와 구독 서비스 하나만으로 시맨틱 코드 검색 및 작업 메모리 도구인 vectr를 구축하기 위해 실제로 사용하는 개발 프로세스에 관한 것입니다. 제품 코드를 작성하지 않는 하나의 오케스트레이션 에이전트(orchestrating agent), 교체 가능한 전문가들의 순환 배치, 그리고 결과물이 뒷받침될 때까지 모든 주장을 전언(hearsay)으로 취급하는 중간의 게이트(gate)로 구성됩니다. 역할(roles)이 먼저 오는 이유는 그것이 쉬운 부분이기 때문입니다. 그다음은 규칙(rules)인데, 이것은 어려운 부분입니다. 왜냐하면 그 규칙 하나하나가 무언가 잘못되었을 때 작성되었기 때문입니다.
Part 01: 형태 (The Shape)
01. 작업 단위는 에이전트가 아니다
제약 조건부터 시작해 보겠습니다. 제약 조건이야말로 이 주제를 흥미롭게 만드는 요소이기 때문입니다. 저에게는 기계 한 대, Claude 구독 하나, 그리고 제 주의 집중력보다 더 긴 작업 목록이 있습니다. 단일 에이전트와 작업할 때, 저의 처리량(throughput)은 제가 에이전트가 한 일을 얼마나 빨리 읽을 수 있느냐에 의해 제한됩니다. 그 제한은 실재하며 매우 낮습니다. 디프(diff)를 주의 깊게 읽는 것은 생성하는 것보다 느리며, 모델들이 성능이 좋아진 이후로 그 차이는 더욱 커졌습니다.
그래서 당신은 병렬성 (parallelism)에 의존하게 됩니다. 다섯 명의 에이전트, 다섯 개의 작업, 다섯 개의 브랜치. 저는 이 중 하나를 **레인 (lane)**이라고 부릅니다. 즉, 하나의 에이전트, 하나의 범위가 지정된 작업, 하나의 브랜치가 감독 없이 처음부터 끝까지 실행되는 것을 의미하며, 이 글에서 '단어'가 많은 역할을 하게 될 것입니다. 이러한 설정의 첫 번째 버전은 병합 (merge)을 시도하기 직전까지는 아주 훌륭하게 작동합니다. 하지만 병합을 시도하는 순간, 아무도 아키텍처 다이어그램에 넣지 않는 사실을 발견하게 됩니다. 바로 병렬성은 출력물과 검증되지 않은 주장 (unverified claims)을 정확히 같은 속도로 증폭시킨다는 점입니다. 다섯 개의 레인은 다섯 개의 디프 (diff)와 다섯 개의 보고서를 생성합니다. 디프는 실제 데이터입니다. 하지만 보고서는 디프를 작성한 것과 동일한 프로세스에 의해 작성된 디프의 요약본이며, 그 과정에서 이미 한두 번 압축된 컨텍스트 (context)를 바탕으로 작성됩니다.
diff: 코드의 두 버전 사이의 변경 사항을 한 줄씩 보여주는 집합. 에이전트가 했다고 말하는 것이 아니라, 에이전트가 실제로 수행한 작업입니다.
여기서 "모델이 환각 (hallucination)을 일으킨다"는 것은 잘못된 진단이며, 문제는 구조적인 것임에도 불구하고 당신이 프롬프트 튜닝 (prompt tuning)에 매달리게 만듭니다. 긴 작업을 수행하는 하위 에이전트 (subagent)는 자신의 컨텍스트 윈도우 (context window)를 채우고, 압축되고, 계속 진행하며, 다시 압축됩니다. 에이전트가 "9개를 구현했습니다"라고 작성할 때쯤이면, 그 '9'는 실제 개수가 아닙니다. 그것은 요약의 요약으로부터 재구성된 개수에 대한 기억일 뿐입니다. 사람에게 8시간 동안 일한 후 기억에 의존해 상태 보고서를 작성하라고 시키면 동일한 종류의 오류가 발생하며, 이것이 바로 우리가 커밋 로그 (commit logs)를 발명한 이유입니다.
subagent (하위 에이전트): 별도의 대화와 컨텍스트 윈도우 (context window)를 가지고, 하나의 범위가 지정된 작업을 수행하기 위해 다른 에이전트에 의해 실행되는 하위 에이전트.
context window (컨텍스트 윈도우): 모델이 한 번에 주의를 기울일 수 있는 고정된 토큰 수. 에이전트가 그 순간 알고 있는 모든 것은 그 안에 존재하며, 기록되지 않은 채 범위를 벗어난 것은 사라집니다.
compaction (압축): 컨텍스트 공간을 확보하기 위해 긴 대화를 요약하는 과정. 요약본은 핵심 내용은 유지하지만 정확한 수치, 서명, 줄 번호 등은 손실됩니다.
당신이 끊어내려는 결합 (The coupling you are trying to break)
에이전트는 저렴하지만 검증 (verification)은 그렇지 않습니다. 만약 당신의 프로세스가 작업과 검증을 함께 확장한다면, 당신은 아무것도 병렬화하지 못한 것입니다. 단지 대기열 (queue)을 생성 단계에서 검토 단계로 옮겼을 뿐이며, 대기열을 더 길게 만들었을 뿐입니다. 게이트 (gate)의 핵심 목적은 검증 비용을 생성 비용보다 낮게 만들어, 두 단계가 서로 다른 속도로 확장될 수 있도록 하는 것입니다.
이로부터 도출된 패턴은 하나의 장기 실행 에이전트 (long-lived agent)와 다수의 단기 실행 에이전트 (short-lived agents)로 구성됩니다. 저는 이 장기 실행 에이전트를 센티널 (sentinel)이라고 부릅니다. 이 에이전트는 제품 코드를 작성하지 않습니다. 이 에이전트의 유일한 임무는 작업 범위를 지정하고, 전문가를 실행하며, 그들의 결과물을 공격(검증)하고, 무엇을 병합 (merge)할지 결정하는 것입니다. 이 포스트의 나머지 내용은 이러한 분리의 결과이며, 제가 이 분리 방식을 정착시키기 전까지 여러 번 실패했던 사실에 기반합니다.
02. 세 가지 역할, 그리고 각 역할이 신뢰받는 대상
이 루프에서는 세 종류의 에이전트가 실행됩니다. 이들을 구분하는 것은 능력 (capability)이 아닙니다. 왜냐하면 이들은 모두 동일한 모델 제품군의 인스턴스이기 때문입니다. 이들을 구분하는 것은, 제가 확인 절차 없이 각 에이전트로부터 무엇을 믿을 용의가 있느냐 하는 점입니다.
센티널 (The sentinel)
오케스트레이터 (Orchestrator)이자 게이트키퍼 (gatekeeper)입니다. 브리프 (briefs)를 작성하고, 실행 경로 (lanes)를 가동하며, 결과를 감사 (audit)하고, 원장 (ledger)을 관리하며, 병합을 수행합니다. 두 가지 규칙이 이 역할을 정의하며, 두 규칙 모두 핵심적인 역할을 합니다.
첫째, 그것은 제품 코드(product code)를 작성하지 않습니다. 실질적인 이유는 컨텍스트(context) 때문입니다. 4개의 레인(lane)을 감사(auditing)하는 것은 시스템에서 컨텍스트를 가장 많이 소모하는 작업이며, 리팩터링(refactor)에 깊이 관여한 오케스트레이터(orchestrator)는 이미 그 컨텍스트를 소모해 버린 상태입니다. 구조적인 이유는 머지(merge) 시 디프(diff)를 확인하는 리뷰어는 머지가 성공하는 것에 이해관계가 있기 때문입니다. 언어 모델(language model)이 사람처럼 이를 이해관계의 충돌로 경험할 것이라고 생각하지는 않지만, 검문소(gate)에서 그 사실을 확인하고 싶지는 않습니다.
둘째, 비용이 발생하거나 되돌릴 수 없는 모든 작업은 센티널(sentinel)로부터 실행됩니다. 푸시(push), 태그(tag), 릴리스(release), 그리고 공유 할당량(shared quota) 창을 소비하는 평가 실행(evaluation runs) 등이 이에 해당합니다. 이는 전문가(specialist)들이 무모하게 행동할 것이기 때문이 아니라, 해당 작업들은 되돌릴 수 없으며(no undo), 이러한 작업이 시작될 수 있는 지점이 정확히 한 곳이기를 원하기 때문입니다. 하나의 레인은 릴리스를 제안할 수는 있지만, 직접 생성할 수는 없습니다.
코더 (The coders)
각자 잘 정의된 하나의 작업, 하나의 git worktree 브랜치, 보고하기 전 전체 테스트 스위트(test suite)의 통과(green), 그리고 브리프(brief)에서 지정한 형식의 구조화된 보고서를 작성합니다. 레인이 종료되면 에이전트도 종료됩니다. 작업 전반에 걸쳐 지식을 축적하는 장기 생존형 코더는 없으며, 이는 의도적인 설계입니다. 축적된 컨텍스트는 감사되지 않은 상태(unaudited state)이며, 감사되지 않은 상태는 이 프로세스 전체가 최소화하고자 하는 바로 그 대상이기 때문입니다.
git worktree: 동일한 저장소에서 체크아웃된 별도의 작업 디렉토리로, 자체적인 브랜치를 가지며 하나의 오브젝트 스토어(object store)를 공유합니다. 두 개의 worktree는 서로의 파일을 덮어쓸 수 없습니다.
일회용 코더(disposable coders)에 대한 명백한 반론은, 모든 새로운 코더가 전체 토큰 비용을 지불하며 코드베이스를 처음부터 다시 학습해야 한다는 점입니다. 그 반론은 타당하며 비용 또한 실제적입니다. 이는 전체 루프에서 가장 큰 비효율이며, 이를 견딜 수 있게 만드는 유일한 요소는 발견된 사항들이 다시 발견되는 대신 다음 레인이 검색할 수 있는 곳에 기록된다는 점입니다 (섹션 11). 이것이 없다면 일회용 에이전트는 방어 불가능할 것입니다.
코더(Coders)들은 매우 구체적인 브리프(briefs)를 받습니다. 작업(Task), 수정 가능한 파일, 넘지 말아야 할 가이드라인(rails), 그리고 보고서의 정확한 형식 등이 포함됩니다. 중간 단계 모델(mid-tier model)에 모호한 브리프를 주면, 인접한 문제를 아주 훌륭하게 해결하는 레인(lane)이 생성되곤 합니다.
보고서 형식에서 두 가지 세부 사항은 브리프의 다른 모든 내용을 합친 것보다 저에게 더 큰 도움이 됩니다. 첫째, 명령어가 무엇이라고 말했는지에 대한 레인의 설명 대신, 명령의 **원시 출력값(raw command output)**을 그대로 붙여넣도록 요청하십시오. 설명 과정에서 정보의 왜곡(rounding)이 발생하기 때문입니다. 둘째, 보고서가 설명하는 **커밋 SHA(commit SHA)**를 요청하십시오. 고정된 SHA가 없다면 감사(audit)는 레인과의 경주가 됩니다. 제가 읽고 있는 동안 레인이 다른 커밋을 푸시할 수 있으며, 그렇게 되면 저는 더 이상 존재하지 않는 상태를 검증하게 됩니다.
TASK 한 문장, 하나의 결과물, "그리고 또한" 사용 금지
WHERE worktree wt-<name>, branch feat-<name>
TOUCH src/searcher/*.py, tests/test_searcher.py
...
이 형식은 화면 한 페이지에 들어오며 작성하는 데 10분 정도 걸리는데, 대부분의 시간은 TOUCH와 RAILS 라인을 작성하는 데 소비됩니다. 이 두 라인은 레인이 세 모듈 떨어진 곳의 무언가를 친절하게 리팩터링(refactoring)하려는 시도를 막아줍니다. 이러한 리팩터링은 깔끔한 머지(merge)를 오후 내내 걸리는 작업으로 만들어버리는 실패 모드(failure mode)입니다.
적대적 리뷰어 (The adversarial reviewers)
각각 독립적인 축(axis)을 가지고 오직 작업물을 공격하는 것만을 유일한 임무로 하는 별도의 에이전트들입니다. 저의 루프(loop)에서 그 축들은 다음과 같습니다: 검색(retrieval)이 실제로 올바른 것을 반환하는가, 도구(tool)가 그것을 위해 만들어진 에이전트에 의해 채택되는가 아니면 무시되는가, 그리고 턴(turns), 토큰(tokens), 실제 소요 시간(wall time) 측면에서 도구가 없는 베이스라인(no-tool baseline)을 이기는가입니다. 세 가지 서로 다른 질문, 세 가지 서로 다른 에이전트입니다. 세 가지를 모두 고려하도록 요청받은 단일 리뷰어는 의도적으로 만들지 않았습니다. 세 가지 명령을 가진 단일 리뷰어는 가장 쉬운 축에서 가장 쉬운 실패를 찾아내고 작업을 멈춰버릴 것이기 때문입니다.
리뷰를 유용하게 만든 지침은 리뷰어에게 디프(diff) 대신 브랜치(branch)와 변경 사항 노트(change note)를 제공하는 것이었습니다. 디프를 받은 리뷰어는 디프만을 리뷰합니다. 작동하는 빌드(working build)를 건네주며 그것을 망가뜨리라고 지시받은 리뷰어는 제품을 직접 사용해 보고, 디프 범위를 벗어나 돌아다니며, 디프가 두 모듈 떨어진 곳에서 망가뜨린 부분을 찾아낼 것입니다. 실제 결함은 바로 그런 곳에서 발생해 왔습니다.
검토자(Reviewer)에 대한 두 가지 가드레일(Guardrails). 라운드(Round) 횟수는 제한됩니다. 무제한의 라운드를 가진 적대적 공격자(Adversary)는 항상 또 다른 사소한 결함(Nit)을 찾아낼 것이고, 그러면 당신은 영원히 제품을 출시(Ship)할 수 없기 때문입니다. 또한 검토자의 발견 사항은 무엇인가를 차단(Gate)하기 전에 독립적으로 검증됩니다. 왜냐하면 검토자가 결함을 지어내는 속도는 코더가 코드를 완성하는 속도와 거의 비슷하기 때문입니다. 머지(Merge)를 차단하는 조작된 결함은 전체 루프(Loop)를 한 번 더 돌게 만드는 비용을 발생시키며, 이는 가장 비용이 많이 드는 종류의 오류입니다.
| 역할 (Role) | 제품 코드 작성 | 수명 (Lifetime) | 신뢰 대상 | 신뢰하지 않는 대상 |
|---|---|---|---|---|
| Sentinel | 절대 하지 않음 | 길음, 스프린트(Sprint) 전체에 걸침 | 범위(Scope), 머지 결정, 원장(Ledger), 유료 및 되돌릴 수 없는 작업 | 스스로 검증하지 않은 모든 것 |
| ... |
조립 라인이 아닌, 주방
대부분의 멀티 에이전트(Multi-agent) 다이어그램은 조립 라인처럼 보입니다. 작업이 오른쪽으로 이동하고, 각 스테이션(Station)이 무언가를 추가하며, 마지막 스테이션이 제품을 출하합니다. 제대로 작동하는 주방은 이와 더 가깝습니다. 요리사들은 별도의 스테이션에서 병렬로 작업하며
추가하는 것은 보이는 것보다 더 중요합니다. 변경 가능한 상태 파일은 잘못된 항목을 조용히 수정할 수 있게 하는데, 이는 기능처럼 들리지만, 그렇게 조용히 수정된 오류는 아무것도 가르쳐주지 않는다는 것을 깨달을 때까지는 그렇습니다. 9의 개수가 7이었다고 했을 때, 그 수정은 원래 주장과 그것을 확정하는 열거 아래에 새로운 줄로 기록되었습니다. 바로 그 줄 때문에 규칙이 존재하는 것입니다. 오류를 숨기는 파일은 작고 깔끔한 편집만 만들었을 것이고 규칙은 없었을 것입니다.
[2026-07-14] TASK retrieval-rerank-blend
brief: owning-class prior를 rerank에 혼합(blend)합니다; searcher와 index만 수정합니다
lane: coder / worktree wt-rerank / branch feat-rerank-blend
...
이 항목은 두 가지 역할을 합니다. 증거(evidence)뿐만 아니라 기록을 남기므로, 나중에 읽는 사람이 병합(merge)이 정당했는지 아니면 운이 좋았는지 알 수 있습니다. 또한 감사(audit) 과정을 내가 의도한 것이 아니라 문서화된 단계로 만듭니다. 만약 AUDIT 라인이 누락되었다면, 나는 확인했는지 나중에 알 수 없기 때문에 병합은 일어나지 않은 것입니다.
원장(ledger)은 복구 아티팩트 역할도 합니다. 세션을 완전히 잃어버리더라도, 원장에 git branch --list를 더하면 약 1분 만에 세상의 상태를 재구성할 수 있습니다: 무엇이 할당되었고, 무엇이 병합되었으며, 어떤 브랜치 어딘가에서 아직 열려 있는지 말입니다. 이 속성은 나에게 여러 번 이상 도움이 되었으며, 나중에 발견하는 것보다 의도적으로 설계할 가치가 있습니다.
파트 02: 게이트(The Gate)
04. 규칙 1: 주장으로 병합하지 말고, 아티팩트를 열거하라
보고서는 소문이고, 아티팩트는 증거이며, 이 게이트는 오직 증거만을 받아들입니다. 아래 내용은 이것이 실제로 어떤 비용을 수반하는지를 보여줍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기