프롬프트를 다듬던 시대에서 루프를 짜는 시대로
모델과 앱이 주고받는 건 중간에 어떻게 돌까? 매일 알아서 도는 에이전트는 cron 일까? 답은 프롬프트 문장 안이 아니라 while 문 하나와 트리거 한 줄에 있었다.
LLM 레퍼런스를 만들면서 끝까지 안 풀린 질문이 둘 있었다. 모델과 앱이 "주고받는" 건 중간에 대체 어떻게 돌까? 매일 알아서 도는 에이전트는 그냥 cron 일까, 뭔가 더 있을까?
"LLM 을 잘 쓰는 법"이라고 하면 보통 프롬프트부터 떠올린다. 역할을 주고, 예시를 몇 개 붙이고, "단계별로 생각하라"를 덧붙이는 기술. 그런데 두 질문의 답은 그 문장 안에 없었다.
자료를 모아 놓고 보니 흐름은 네 단계로 보였다. 프롬프트, 컨텍스트, 루프, 그래프. 뒤로 갈수록 "글 쓰기"보다 "시스템 설계"에 가깝다. 앞 단계가 사라지는 건 아니다. 뒷 단계의 부품이 될 뿐이다.
| 세대 | 최적화 대상 | 한계 |
|---|---|---|
| ① 프롬프트 엔지니어링 | 한 번의 호출 | 한 방 호출로는 검증·수정·도구 사용이 안 된다 |
| ② 컨텍스트 엔지니어링 | 창에 무엇을 넣나 | 잘 채워도 실행이 한 번이면 실수를 복구 못 한다 |
| ③ 루프 엔지니어링 | 모델을 도는 고리 | 루프 하나로는 큰 일·병렬 일·긴 일이 버겁다 |
| ④ 그래프 엔지니어링 | 루프들의 연결 | (지금 판) |
문장을 다듬어도 더는 안 오르는 지점
모델이 좋아지면서 문장 기교로 얻는 몫은 점점 줄었다. 성능을 가르는 건 창(context window)에 무엇을 넣느냐가 됐다. 어떤 문서를 검색해 넣을지, 무엇을 기억시킬지, 무엇을 앞에 고정해 캐시로 만들지.
Anthropic 은 이걸 컨텍스트 엔지니어링이라 부르고, 지도 원칙을 한 줄로 적었다. "the smallest set of high-signal tokens that maximize the likelihood of some desired outcome". 원하는 결과가 나올 확률을 가장 높이는, 가장 작은 고신호 토큰 묶음이다.
왜 "작은"이 붙을까?
컨텍스트는 예산이라서다. 트랜스포머에서는 모든 토큰이 다른 모든 토큰을 보니까 n개 토큰에 n² 개의 관계가 생기고, 그만큼 주의(attention)가 조금씩 깎인다. 절벽처럼 한 번에 무너지지 않는다. 서서히 흐려진다. Anthropic 은 이걸 context rot 라고 부른다. Chroma 가 18개 모델로 돌린 연구도 비슷한 걸 봤다. 쉬운 과제에서도 입력 길이에 따라 성능이 크게 흔들렸고, 엉뚱한 문서(distractor)가 하나만 섞여도 떨어졌다.
위치도 탄다. 관련 정보가 중간에 묻히면 처음이나 끝에 있을 때보다 성능이 떨어진다(lost-in-the-middle). 그래서 중요한 지시는 맨 앞이나 맨 뒤에 둔다. 한국어라면 사정이 더 빠듯하다. cl100k_base 기준으로 한글은 문자당 대략 1토큰이라 영어보다 약 4배를 먹는다.
이 단계에서 일의 모양이 바뀐다. 시스템 프롬프트는 짧게 쓰고 캐시 앞머리로 고정한다. 이번 턴에 안 쓸 도구 정의는 뺀다. 대화 이력은 관련된 조각만 남긴다. 큰 툴 결과는 창 밖에 두고 요약만 넣는다. Anthropic 이 "the safest, lightest touch form of compaction"이라고 부른 게 이 툴 결과 오프로딩이다. 거기서 시작해서 모자라면 압축(compaction), 그다음 서브에이전트 순으로 올린다.
창을 잘 채워도 한 번 부르고 끝이면
그런데 창을 아무리 잘 채워도 호출이 한 번이면 모델은 행동하고 확인할 수가 없다. 틀린 답을 냈을 때 고칠 기회가 없다.
루프 엔지니어링은 모델을 고리 안에 넣는다. 판단하고, 도구를 부르고, 결과를 보고, 다시 판단한다. 끝낼 조건이 오면 멈춘다. 이때 설계하는 대상은 프롬프트가 아니라 하네스(harness)다. 어떤 도구를 줄지, 언제 멈출지, 결과를 어떻게 검증할지, 긴 루프에서 컨텍스트가 썩지 않게 할지.
긴 루프의 함정은 조용하다는 점이다. 40턴째부터 답이 미묘하게 빗나가는데 에러는 하나도 안 난다. 추론 모델이면 thinking 토큰도 예산을 먹는다. 대화 중간에 effort 값을 바꾸면 그 값이 프롬프트에 렌더되는 탓에 프롬프트 캐시가 깨진다. 대화 수명 동안 effort 는 고정해 두는 게 맞다.
주고받기의 정체는 while 문 하나였다
첫 번째 질문의 답은 허무할 만큼 단순했다.
messages = [{"role": "user", "content": "..."}]
while True:
r = client.messages.create(model=..., tools=tools, messages=messages)
if r.stop_reason != "tool_use": # end_turn 이면 끝
break
messages.append({"role": "assistant", "content": r.content}) # 모델의 수
results = [실행(b) for b in r.content if b.type == "tool_use"]
messages.append({"role": "user", "content": results}) # 결과 회신API 는 무상태다. 상태는 서버가 아니라 내 손의 메시지 배열에 있고, 도는 주체도 모델이 아니라 내 코드다. 모델은 매 턴 "다음 한 수"만 낸다. 도구를 쓰고 싶으면 응답이 stop_reason: "tool_use" 로 멈추고, 실행은 앱이 한다. 결과를 tool_result 로 담아 user 메시지로 돌려보내면 한 바퀴가 끝난다.
작은 규칙이 몇 개 붙는다. 병렬 호출이면 결과 여러 개를 한 메시지에 담아야 한다. 따로 보내면 모델이 병렬을 끈다. 매 요청에 전체 히스토리를 다시 보내니, 반복되는 앞부분은 캐시로 싸게 만든다.
이 while 문을 꼭 직접 짤 필요는 없다. 누가 루프를 도느냐는 스펙트럼이다.
| 방식 | 루프를 도는 주체 | 쓰는 때 |
|---|---|---|
| 수동 while 문 | 내 코드 | 제어를 전부 갖고 싶을 때 |
| SDK 툴러너 | SDK (루프만 대행) | 대부분의 커스텀 도구 에이전트 |
| 서버 하네스 (Managed Agents 류) | 서버 (루프+실행 샌드박스) | 호스팅·스케줄까지 맡기고 싶을 때 |
| 에이전트 하네스 제품 (Claude Code 등) | 내장 하네스 | 파일·셸이 필요한 코딩 에이전트 |
cron 은 시작 버튼이지 지휘자가 아니다
두 번째 질문도 층을 나누니 풀렸다. 매일 알아서 도는 에이전트에는 층이 둘 있다. 언제 깨울지(트리거), 그리고 깨어난 뒤 어떻게 일할지(루프). cron 은 앞쪽만 맡는다.
크론 줄에 적힌 건 명령 한 줄이다. 그 한 줄이 뭐냐에 따라 일어나는 일이 갈린다. python3 crawl.py 면 AI 는 없고 결정적 스크립트만 돈다. claude -p "오늘치 갱신해" 면 헤드리스 에이전트 프로세스가 뜨고, 그 안에서 위의 while 루프가 돈다. API 로 세션을 만들면 서버 하네스에 "새 세션 하나 시작해"를 요청하는 셈이다. 웹훅도 다르지 않다. 시계 대신 사건이 깨울 뿐, 그다음은 같다.
깨어난 세션은 아무것도 기억하지 못한 채 태어난다. 그래서 이어서 일하려면 파일에서 맥락을 읽는다. 저장소의 문서, 로그, 메모리 파일이 그 역할이다. 크론이 깨우고, 세션은 파일을 읽어 상황을 파악하고, 루프를 돌고, 결과를 파일과 커밋으로 남기고 죽는다. 자동화된 에이전트 운영의 최소 단위가 이 한 바퀴다.
내 사이트의 트렌드 갱신은 오히려 앞의 python3 crawl.py 쪽에 가깝다. 크롤·배포·커밋은 launchd 가 깨운 스크립트가 하고 AI 는 없다. 새 진입 소개처럼 판단이 필요한 일은 숙제 파일로 남겨 다음 세션이 읽는다.
루프 하나로 모자랄 때 그래프가 된다
루프 하나가 버거운 일이 있다. 넓게 병렬로 퍼지는 조사, 서로 독립인 작업 트랙, 검증자를 따로 두고 싶은 경우. 지금 판은 루프들을 노드로 엮는다.
서브에이전트는 부모 루프가 자식 루프를 도구처럼 부르는 구조다. 쓸모는 컨텍스트 격리에 있다. 자식이 수만 토큰을 들여 탐색해도 부모 창은 더럽혀지지 않고, 돌아오는 건 보통 1,000~2,000 토큰짜리 요약이다. 워크플로 그래프에서는 노드가 에이전트나 함수, 엣지가 제어 흐름이다. 결정적인 부분은 코드로, 판단이 필요한 부분만 모델로 돌린다. 파괴적 행동 앞의 승인 게이트, 그러니까 사람의 자리도 그래프의 노드다.
"그래프 엔지니어링"이라는 이름도 돌기 시작했다. 코드팩토리 채널에 「또다른 트렌드 Graph Engineering 알려드림」이라는 영상이 올라와 있다. 다만 나는 제목까지만 확인했고 본문은 보지 못했다. 이름이 뭐든 실체는 같다. 설계의 단위가 문장에서 창으로, 창에서 고리로, 고리에서 고리들의 연결로 계속 커져 왔다.
내 작업 방식에서 실제로 바뀐 것
매일 쓰는 Claude Code 에서 이 변화가 제일 잘 보인다. 레퍼런스에 적어 둔 일상 루프는 Spec, Context, Plan, Implement, Verify, Review, Ship 이다. 여기서 레버리지가 가장 큰 칸은 Context 다. CLAUDE.md 에 관례, 명령, 아키텍처, 가드레일을 넣어 두는 일.
규칙을 지키게 만드는 방법도 달라졌다. 프롬프트로 "하지 마"라고 쓰면 모델은 무시할 수 있다. 훅은 코드라서 항상 실행된다. PreToolUse 훅에서 .env 읽기나 rm -rf 를 막으면 부탁이 아니라 차단이 된다. 깊은 탐색은 서브에이전트로 떼어 내 부모 창을 깨끗하게 지키고, 창이 차기 전에 /compact 를 하거나 새 세션을 연다.
검증은 루프 안으로 들어왔다. write, run tests, fix 가 도는 곳에서 에이전트가 제일 빛난다. 단 테스트가 있어야 한다. 그리고 에이전트가 쓴 코드는 무검토로 머지하지 않는다. 리뷰 서브에이전트든 두 번째 모델이든, 쓴 쪽이 놓친 걸 잡을 독립된 눈을 루프 끝에 하나 더 단다.
드리프트가 보이면 멈춘다. 헷갈리는 게 뭔지 모르겠으면 추측하지 말고 멈춰서 이름부터 붙이라는 Karpathy 의 규칙이 있다. 헷갈린 채 도는 루프는 내버려 두지 않는다.
핵심 한 줄설계의 단위는 문장에서 창으로, 창에서 루프로, 루프에서 그래프로 커졌다.
자주 묻는 것
컨텍스트 엔지니어링과 프롬프트 엔지니어링 차이
프롬프트 엔지니어링은 단발 요청의 지시문을 다듬는 일이다. 컨텍스트 엔지니어링은 에이전트가 도는 내내 창에 들어갈 토큰을 골라 넣고 버리는 일이다. 원하는 결과를 낼 가장 작은 고신호 토큰 묶음이 목표다.
에이전트를 cron 으로 돌리면 cron 이 오케스트레이션을 하나?
아니다. cron 은 정해진 시각에 명령 한 줄을 실행하는 트리거다. 그 한 줄이 claude -p 같은 헤드리스 세션을 띄우면 루프는 그 안의 하네스가 돈다. 세션은 무상태라 파일에서 맥락을 읽는다.