본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 15. 15:36

Claude Code를 사용하여 자가 개선형 코딩 에이전트를 구축한 방법: 6개월간의 5가지 교훈

요약

Claude Code를 활용하여 상태 유지, 작업 위임, 자기 개선이 가능한 코딩 에이전트를 구축한 6개월간의 실전 경험과 교훈을 공유합니다. 데이터베이스 대신 마크다운 파일을 상태 저장소로 활용하고, 하위 에이전트에게 엄격한 범위를 부여하는 아키텍처 설계 방식을 다룹니다.

핵심 포인트

  • SQLite 대신 마크다운 파일을 사용하여 에이전트의 컨텍스트 이해도 향상
  • STATE.md, DECISIONS.md 등을 통한 단일 진실 공급원(SSOT) 구축
  • 단순 위임이 아닌 엄격한 범위 계약(Hard Scope Contracts)을 가진 서브 에이전트 활용
  • 장기적인 작업 수행을 위한 에이전트의 상태 유지 메커니즘 설계

요약 (TL;DR)

저는 Claude Code를 기반으로 자가 개선형 코딩 에이전트(self-improving coding agent)를 구축하는 데 6개월을 보냈습니다. 이 에이전트는 하위 에이전트(sub-agents)에게 작업을 전달하고, 자신의 상태(state)를 유지하며, 잘못된 작업을 수행했을 때 스스로 프롬프트(prompt)를 다시 작성하는 오케스트레이터(orchestrator)입니다. 여기에는 제가 첫날 누군가 말해줬으면 좋았을 5가지 교훈과, 각 교훈을 깨닫게 해준 실전 경험담이 담겨 있습니다.

문제점

저는 단발성 LLM 코딩 세션에서 계속해서 동일한 벽에 부딪혔습니다.

모델은 30분 동안은 훌륭하게 작업하다가도, 10분 전에 내린 결정을 잊어버리곤 했습니다. 제가 그대로 두라고 요청한 버그를 아주 즐겁게 "수정"해버리기도 했습니다. 함수 이름을 하나 지어내어 세 번이나 호출한 뒤, 테스트 러너(test runner)가 터지고 나서야 해당 함수가 존재하지 않는다는 사실을 알아차리기도 했습니다. 다시 프롬프트를 입력하는 것(Re-prompting)이 도움이 되긴 했지만, 딱 한 번뿐이었습니다. 그 후에는 다시 방향을 잃고 표류했습니다.

제가 원했던 에이전트는 다음과 같았습니다:

  1. 자신의 상태를 유지할 것: 장기간(몇 분이 아닌 며칠 단위)의 작업 동안 상태를 유지
  2. 위임할 것: 하나의 컨텍스트 윈도우(context window)에 모든 것을 밀어 넣는 대신, 전문화된 작업을 하위 에이전트(sub-agents)에게 위임
  3. 스스로 실수를 포착할 것: 제가 지적하기 전에 스스로 실수를 발견
  4. 시간이 지남에 따라 개선될 것: 모델 자체가 아니라, 모델을 둘러싼 시스템이 개선됨

저는 이미 Claude Code(당시 CLI, v0.x 버전)를 사용하고 있었고, 여기에는 하위 에이전트, 도구 호출(tool calls), 훅(hooks)과 같은 적절한 기본 요소(primitives)들이 있었습니다. 그래서 러너(runner)를 새로 만드는 대신, 그 위에 구축하기 시작했습니다.

해결 방법

최종적인 아키텍처는 다음과 같은 모습이 되었습니다:

flowchart TD
    User[User prompt] --> Orchestrator
    Orchestrator -->|reads| State[(State files<br/>STATE.md, DECISIONS.md)]
...

세 가지 요소가 대부분의 작업을 수행했습니다.

1. 데이터베이스가 아닌 일반 Markdown 형태의 상태(State)

처음에는 SQLite 기반의 메모리 저장소를 시도했습니다. 기술적으로는 더 깔끔했지만, 에이전트가 계속해서 쿼리를 잘못 날려 이상하고 반쯤만 맞는 컨텍스트(context)를 생성했습니다. 그래서 이를 제거하고, 에이전트가 매 세션 시작 시 읽어들이는 세 개의 일반 파일로 교체했습니다:

  • STATE.md — "현재 나는 어디에 있는가, 다음 구체적인 단계는 무엇인가"
  • DECISIONS.md — ID(D-001, D-002, …)가 포함된 결정 사항의 추가 전용(append-only) 로그
  • MEMORY.md — 장기적인 사실들의 인덱스

에이전트는 이 파일들을 가장 먼저 읽고, 작업하면서 여기에 내용을 기록하며, 파일 간의 내용이 일치하지 않을 때는 STATE.md를 단일 진실 공급원 (Single Source of Truth)으로 취급합니다.

지루해 보이나요? 맞습니다. 하지만 에이전트는 산문을 이해하는 것과 같은 방식으로 마크다운 (Markdown)을 이해하며, git diff 덕분에 모든 상태 변화를 사람이 검토할 수 있습니다.

2. 엄격한 범위 계약 (Hard Scope Contracts)을 가진 서브 에이전트 (Sub-agents)

단순한 위임은 작동하지 않았습니다. 만약 제가 "X를 조사하기 위한 서브 에이전트를 생성해"라고 말하면, 서브 에이전트는 X를 조사할 뿐만 아니라 동시에 그것을 수정하려 시도하고, 인접한 파일을 리팩터링하며, 새로운 테스트를 작성하기도 했습니다. 영향 범위 (Blast radius)를 예측할 수 없었습니다.

효과가 있었던 방법은 다음과 같습니다: 모든 서브 에이전트는 생성 시점에 계약 (Contract)을 부여받습니다.

// 의사 코드 (pseudo-code), 실제 API 아님
spawnSubAgent({
  role: "researcher",
...

제가 엄격하게 강제해야 한다고 배운 두 가지 규칙은 다음과 같습니다:

  • 역할별 도구 허용 목록 (Tool allowlists). 연구자 (Researcher)는 쓸 수 없습니다. 검토자 (Reviewer)는 편집할 수 없습니다. 계획가 (Planner)는 실행할 수 없습니다. 이것이 가장 큰 레버리지 포인트입니다.
  • 구조화된 반환 값 (Structured return values). 서브 에이전트의 마지막 메시지가 곧 반환 값입니다. 만약 서브 에이전트가 산문을 반환하도록 두면, 오케스트레이터 (Orchestrator)가 이를 다시 파싱해야 했고 종종 오류가 발생했습니다. 스키마 (Schema)를 강제함으로써 하류 (Downstream)의 잘못된 오류를 극적으로 줄일 수 있었습니다.

3. "검토 단계 (Reviewer pass)"를 통한 자가 수정

사소하지 않은 모든 변경 사항 이후에, 오케스트레이터는 단 하나의 임무를 가진 검토 서브 에이전트를 생성합니다:

디프 (Diff)를 읽으세요. 잘못되어 보이거나, 불완전하거나, DECISIONS.md의 결정 사항과 모순되는 것이 있는지 찾으세요. 우려 사항 목록을 반환하거나 빈 목록을 반환하세요.

목록이 비어 있지 않으면, 오케스트레이터는 이를 직접 수정하거나 저에게 에스컬레이션 (Escalate)합니다. 이 방식은 제가 그냥 배포했을 법한 버그의 약 30%를 잡아냈습니다. 여기에는 구현자가 즐겁게 // TODO: 에러 처리를 추가하자마자 검토자가 3초 만에 이를 지적했던 기억에 남는 사례도 포함됩니다.

핵심은 검토자가 희망 사항에 의존하는 것이 아니라, **프롬프트에 의해 적대적 (Adversarial by prompt)**이 되도록 만드는 것이었습니다:

기본적으로 의심하십시오. 어떤 것이 정확한지 판단할 수 없다면, 그것을 우려 사항으로 반환하십시오. 거짓 양성 (False positives)은 비용이 적게 들지만, 거짓 음성 (False negatives)은 버그를 배포하게 만듭니다.

배운 점들

1. 상태 파일 (State files)이 상태 저장소 (State stores)보다 낫다

매 턴마다 자신의 컨텍스트를 읽는 에이전트에게는, git diff로 확인할 수 있는 일반 마크다운 (Markdown) 형식이 그 어떤 데이터베이스 (DB)보다 디버깅하기 쉽습니다. 이제 저는 에이전트가 수천 개의 레코드에 걸쳐 상태를 쿼리해야 할 때만 SQLite/KV를 사용합니다.

2. 도구 허용 목록 (Tool allowlists)은 핵심적인 안전 장치입니다

"아무것도 수정하지 마세요"라고 말하는 프롬프트 (Prompts)는 약 80%의 확률로 작동합니다. 하지만 forbidden: ["Edit"] 목록은 100%의 확률로 작동합니다. 실행기 (Runner)를 신뢰할 수 있을 때, 모델의 규율에 의존하지 마세요.

3. 서브 에이전트 (Sub-agent)의 반환 값은 메시지가 아니라 데이터입니다

서브 에이전트를 RPC (Remote Procedure Call)처럼 취급하세요. 반환 스키마 (Return schema)를 사전에 정의하십시오. 산문 (Prose)을 파싱하는 오케스트레이터 (Orchestrator)는 화요일에 고장 나는 오케스트레이터입니다.

4. 실패는 조용히 하지 말고, 크게 드러내세요

초기 버전은 오류를 포착하면 "다음 것을 시도"하곤 했습니다. 버그는 눈에 보이는 무언가가 망가질 때까지 보이지 않게 축적되었습니다. 저는 모든 오류 경로를 깔끔하게 성공하거나, 실패를 드러내거나, 혹은 중단하도록 다시 작성했습니다. 조용히 열 번 재시도하는 것보다, 크게 실패를 알리는 것이 훨씬 가치 있습니다.

5. 프롬프트에 버전 스탬프를 찍으세요

Claude Sonnet 4.5에서 작동하던 프롬프트가 Claude Sonnet 4.6에서 항상 동일하게 작동하지는 않았습니다. 이제 저는 모든 프롬프트 파일에 마지막으로 검증한 모델과 날짜를 스탬프로 찍습니다. 모델 업그레이드가 이루어질 때, 저는 미스터리에 빠지는 대신 재검증 체크리스트를 갖게 됩니다.

다음 단계

제 리스트에는 몇 가지 사항이 있습니다:

  • 더 나은 리뷰어 다양성 (Better reviewer diversity) — 서로 다른 관점(정확성, 성능, 계약 적합성)을 가진 3명의 리뷰어를 병렬로 실행하고 그들의 우려 사항을 병합하는 것입니다. 초기 실험에 따르면 이는 단일 리뷰어보다 다른 종류의 버그를 잡아내는 것으로 나타났습니다.
  • 브랜치 및 병합 계획 (Branch-and-merge planning) — 하나의 경로를 확정하는 대신, 오케스트레이터가 격리된 git 워크트리 (Worktrees)에서 N개의 접근 방식을 시도하고 승자를 선택하도록 하는 것입니다.
  • 에이전트 자체를 위한 평가 하네스 (Eval harness for the agent itself) — 프롬프트 변경 후 회귀 (Regression)를 측정하기 위해 실행할 수 있는 고정된 작업 세트입니다.

무엇이 실제로 작동하는지(그리고 무엇이 다이어그램 상에서만 똑똑해 보이는지) 배우게 될 때마다 이 각각의 내용들을 정리하여 작성하겠습니다.

마무리 / CTA

만약 여러분이 Claude Code나 다른 도구 사용 (tool-use) LLM을 기반으로 에이전트를 구축하고 있다면, 핵심적인 교훈은 다음과 같습니다: 모델은 쉬운 부분이며, 그 주변의 시스템을 구축하는 것이 어려운 부분입니다. 이를 실제 분산 시스템 (distributed system)처럼 취급하세요. 즉, 명시적인 계약 (explicit contracts), 구조화된 반환 값 (structured returns), 명확한 실패 알림 (loud failures), 그리고 지속적인 상태 (persistent state)가 필요합니다.

이 내용이 공감이 되었다면:

  • 💡 Dev.to에서 저를 팔로우하세요. 제가 이 프로젝트를 더 발전시켜 나가는 과정의 빌드 로그 (build logs)를 더 많이 공유하겠습니다.
  • ⚙️ 아직 사용해 보지 않으셨다면 Claude Code를 사용해 보세요. 이러한 패턴의 대부분은 여러분이 사용하는 어떤 러너 (runner)에도 적용될 수 있습니다.
  • 💬 여러분의 에이전트 설정에서 무엇이 제대로 작동하지 않는지 댓글로 남겨주세요. 모든 답글을 읽고 있으며, 후속 포스트를 위해 실패 모드 (failure modes)를 수집하고 있습니다.

코딩 에이전트를 실행하면서 여러분이 겪은 가장 놀라운 실패 모드 (failure mode)는 무엇인가요?

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0