4 minute read

LLM은 프롬프트만 기막히게 쓰면 다 되는 줄 알았다(진짜 순진했다).

사실 지금까지는 딱 세 개만 맞추면 원하는 결과가 쏟아져 나온다고 믿었다.

(모델) + (프롬프트) + (컨텍스트)

좋은 모델 고르고, 명확하게 프롬프트 깎고, 필요한 컨텍스트를 충분히 밀어 넣어주기. 실제로 이 공식이면 챗봇 수준의 작업은 거의 다 해결됐다. 그래서 “프롬프트 엔지니어링이 전부 아닌가?”라고 생각했었다.

하지만 Claude Code, Cursor 같은 자율형 AI 코딩 에이전트를 실무 프로젝트에 본격적으로 돌리기 시작하니 멘붕이 왔다. 프롬프트도 명확하게 줬고, 컨텍스트도 다 넣어줬는데, 프로젝트 덩치가 커질수록 에이전트가 허공에 삽질을 해댔다. 파일 시스템을 뒤엎고, 엉뚱한 코드를 짜고, 테스트를 망가뜨리는데 기존 삼박자 공식으로는 도저히 통제가 안 됐다. 이쯤 되면 프롬프트 문제가 아니라 시스템 운영 문제다.

이 답답함을 말로 잘 풀지 못하겠어서, 최근 Gemini와 이 주제로 꽤 깊게 대화를 나눴다. 내가 겪은 현상을 털어놓으며 “왜 이런 대참사가 날까?”를 같이 파고들었는데, 거기서 건진 인사이트가 꽤 흥미로웠다. 결론부터 말하면, 자율형 AI 에이전트를 제대로 굴리려면 프롬프트 너머의 시스템적, 엔지니어링적 설계를 고민해야 하고, 그중에서도 상태(State) 관리가 알파이자 오메가라는 거다.

Gemini와 나눈 대화를 바탕으로 내가 직접 겪고 깨달은 내용을 정리해본다.

에이전트 시대, 코더에서 오케스트레이터로

AI 에이전트를 쓸 때 개발자의 역할은 ‘직접 코드를 치는 사람’에서 ‘AI 시스템을 지휘하는 오케스트레이터(Orchestrator)’로 바뀐다. 말이 다소 거창해 보이지만, 실제로는 기술 PM이나 테크 리드와 비슷한 포지션이다. AI가 제 성능을 내려면 다음 요소들을 시스템적으로 설계해줘야 한다.

AI 친화적인 아키텍처 (RAG & Context) AI가 특정 기능만 딱 떼어서 분석할 수 있도록 모듈화와 관심사 분리(Separation of Concerns)를 꽤 강하게 적용해야 한다. 파일 하나가 500줄 넘어가면 에이전트도 사람처럼 흐름을 잃는다. 사람도 버거운데 모델이라고 갑자기 초월하진 않는다.

자동화된 피드백 루프 (Validation) 에이전트 출력을 시스템적으로 통제하려면 TDD와 린터(Linter)를 파이프라인에 묶어야 한다. 컴파일 에러나 린트 위반을 에이전트가 스스로 감지하고 수정(Self-Correction)하게 만드는 것이 핵심이다. 사람이 매번 눈으로 확인하면 결국 개발자가 직접 일하는 셈이다. 에이전트를 고용해놓고 개발자가 야근하는 이상한 그림이 나온다(아주 흔한 풍경이다).

실행 환경 샌드박싱 (Environment) 에이전트가 rm -rf / 같은 명령을 때려 박지 못하도록 컨테이너 기반으로 환경을 격리해야 한다. 전용 스크립트를 도구(Tool)로 제공해서 에이전트의 행동 반경을 명시적으로 제한하는 것도 중요하다. 믿음도 좋지만 권한은 깎아내야 안전하다.

역할 분배 (Inference Strategy) 아키텍처 설계는 무거운 클라우드 모델에, 단순 리팩토링은 로컬 소형 모델에 맡기는 식으로 비용과 지연 시간(Latency)을 최적화해야 한다. 모든 작업에 가장 비싼 모델을 돌리면 토큰 요금이 깡통 나기 쉽다. 비용은 생각보다 빠르게 현실을 알려준다.

그리고 이 모든 것 중에서 에이전트의 작업 품질을 가장 극적으로 좌우하는 것이 바로 상태와 메모리 관리다.

에이전트의 상태(State)와 작업 메모리 관리

LLM은 본질적으로 상태가 없는(Stateless) 모델이다. 에이전트가 이전 대화나 전체 프로젝트 맥락을 기억하는 것처럼 보이는 것은, 프레임워크가 매 턴마다 현재 파일 상태, 에러 메시지, 대화 내역을 프롬프트에 주입하고 있기 때문이다.

복잡한 애플리케이션에서 상태 관리가 꼬이면 예상치 못한 사이드 이펙트가 터지듯, 에이전트의 메모리도 엄격하게 통제하지 않으면 환각(Hallucination)과 무한 루프에 빠진다. 실무에서는 다음 4가지 전략으로 에이전트 상태를 제어한다.

1. 컨텍스트 오염 방지와 프루닝 (Context Pruning)

에이전트와 작업 세션이 길어지고 수정하는 파일이 많아질수록 작업 메모리(컨텍스트)가 비대해진다. 그러면 모델이 입력 정보의 중간에 있는 핵심 로직을 놓치는 Lost in the Middle 현상이 발생한다.

해결책은 간단하다. 하나의 기능 구현이나 컴포넌트 작성이 끝나면 미련 없이 에이전트 세션을 초기화(Clear)하는 것이다. 새로운 작업을 시작할 때는 깨끗한 상태에서 필요한 파일만 다시 읽도록 유도해야, 과거의 엉뚱한 맥락에 에이전트가 휘둘리지 않는다. 대화 기록이 아까워서 붙잡고 있다가 더 크게 꼬인다.

실제로 나도 초기에는 한 세션에 이것저것 다 시켜보려다가, 나중에 보니 앞서 작업하던 파일의 잔해가 새 코드에 엉뚱하게 섞여 들어간 걸 보고 등골이 서늘해졌다. 지금은 작업 단위가 바뀔 때마다 무조건 세션을 새로 파는 게 철칙이다. 약간 번거롭지만, 나중에 피눈물 흘리며 git diff 뜯어보는 것보단 백배 낫다.

2. 파일 기반의 명시적 상태 저장소 (State as a File)

세션을 초기화해도 프로젝트의 전체 방향성은 유지돼야 한다. 이때 에이전트의 ‘외부 기억 장치’ 역할을 할 파일이 필요하다.

프로젝트 루트에 PLAN.md나 TODO.md 파일을 만들어서 현재 진행 상태, 아키텍처 규칙, 남은 작업 목록을 문서화한다. 새로운 세션을 시작할 때마다 “가장 먼저 PLAN.md를 읽고 현재 단계를 파악해”라고 지시하면, 에이전트가 스스로 프로젝트 상태를 동기화하고 큰 그림을 유지할 수 있다.

이것은 마치 데이터베이스의 WAL(Write-Ahead Log)과 같다. 트랜잭션이 끊겨도 어디까지 했는지 복구할 수 있게 로그를 남겨두는 것과 같은 원리다. 개발자답게 말하면 그렇고, 쉽게 말하면 “까먹지 않게 적어두기”다.

3. ReAct 루프의 발산 통제 (Micro-Chunking)

AI 에이전트는 생각(Thought) → 행동(Action) → 관찰(Observation)의 ReAct 루프를 통해 작동한다. 여기서 “결제 시스템을 리팩토링해 줘”처럼 작업 단위(Scope)를 크게 주면 문제가 된다. 에이전트가 마음대로 인터페이스를 변경하다 에러를 내고, 이를 수습하려다 시스템 전체를 망가뜨리는 발산(Divergence)이 일어난다.

이것을 막으려면 작업을 의도적으로 마이크로 청킹(Micro-Chunking)해야 한다.

1. 결제 인터페이스 설계 → 컴파일 확인
2. 결제 모듈 구현 → 테스트 통과 확인
3. 기존 코드에 주입 → 전체 빌드 확인

이렇게 아주 좁은 단위로 쪼개서 단계별 결괏값을 확인한 뒤 다음 명령을 내리는 마이크로 매니지먼트가 필수다. 사실 조금 번거롭지만, 한 번 발산하면 수습하는 데 더 많은 시간이 든다. AI에게 자유를 줬더니 리팩토링 대장정이 시작되는 경험은 생각보다 흔하다(그리고 끔찍하다).

4. Git 기반 체크포인팅 (Checkpointing)

아무리 통제를 잘해도 에이전트는 가끔 환각에 빠져 멀쩡한 코드를 지우거나 수십 개의 파일을 한 번에 엉망으로 포맷팅해버린다. 이때 개발자가 직접 수습하려 들면 코딩 도구를 도입한 의미가 퇴색된다.

가장 확실한 안전망은 Git이다. 에이전트에게 지시를 내리기 전, 아주 사소한 변경점이라도 WIP(Work In Progress) 커밋을 남겨두는 게 좋다. 더 나아가, 에이전트에게 “작업 단위가 하나 끝날 때마다 테스트를 돌려보고, 통과하면 네가 직접 커밋해”라고 지시해서 자동화된 체크포인트 생성을 위임할 수도 있다.

이것은 데이터베이스의 세이브포인트(Savepoint)와 같다. 언제든 특정 시점으로 롤백할 수 있어야 에이전트를 과감하게 쓸 수 있다. 백업 없는 과감함은 그냥 무모한 도박이다.

대화 1을 마치며

나는 여전히 (모델) + (프롬프트) + (컨텍스트)가 중요하다고 생각한다. 기본기인 건 맞으니까. 하지만 Gemini와 대화하면서 확실해진 것은, 이것이 전부가 아니라는 점이다. 에이전트가 단순히 질문에 답하는 것을 넘어서 스스로 파일을 읽고 코드를 작성하는 환경에서는, 그 위에 상태 관리, 샌드박싱, 체크포인팅 같은 시스템적 레이어가 반드시 필요하다.

여기까지가 첫 번째 대화에서 정리한 내용이다. 생각보다 배울 점이 많았다. 그리고 솔직히 반성도 했다. 너무 대충 던져놓고 결과가 이상하다고 투덜댔던 것 같아서다(물론 내 손가락 탓이다).

하지만 정리하고 나니 오히려 더 많은 의문이 남았다. 상태 관리가 중요하다는 것은 알겠는데, 그렇다면 실제로 프로젝트에 이것을 어떻게 구성해야 할까? 에이전트가 길을 잃기 직전에 보내는 신호는 무엇일까? 그리고 여러 에이전트를 동시에 돌리는 멀티 에이전트 환경에서는 상태 관리가 또 어떻게 달라질까?

이런 궁금증이 꼬리를 물어서, Gemini와의 대화를 이어가게 되었다. 다음 글에서는 웹 기반 LLM 사용자와 터미널 기반 에이전트 사용자가 이 원칙을 어떻게 실전 작업 방식으로 바꿀 수 있는지 다룬다.

전편 다음 글: AI 코딩 에이전트를 지휘하는 법 2: 터미널에서 통제력을 만드는 프롬프트 하네스