프롬프트에서 루프로, 그래프로 — LLM 엔지니어링의 무게중심 이동
"프롬프트 잘 쓰는 법"에서 시작한 판이 지금은 "루프와 그래프를 설계하는 일"이 됐다. 각 세대가 무엇을 최적화했고 왜 다음 단계가 필요해졌는지, 그리고 실무자가 가장 헷갈리는 두 가지 — 주고받기는 실제로 어떻게 도는가, 오케스트레이션(cron?)은 무엇인가 — 를 정리한다.
문장 다듬기→② 컨텍스트
창 채우기→③ 루프
하네스 설계 ⟲→④ 그래프
루프들의 연결
한눈에
| 세대 | 최적화 대상 | 대표 도구 | 한계 (다음 세대가 필요해진 이유) |
|---|---|---|---|
| ① 프롬프트 엔지니어링 | 한 번의 호출 | few-shot, 역할 지정, prompting-cheatsheet | 한 방 호출로는 검증·수정·도구 사용이 안 된다 |
| ② 컨텍스트 엔지니어링 | 창에 무엇을 넣나 | RAG, 메모리, 캐시 배치 — context-engineering, prompt-caching | 잘 채워도 실행이 한 번이면 실수를 복구 못 한다 |
| ③ 루프 엔지니어링 | 모델을 도는 고리 | 툴 콜 루프, 정지 조건, 검증 게이트 — agent-design-patterns, set-up-tool-use | 루프 하나로는 큰 일·병렬 일·긴 일이 버겁다 |
| ④ 그래프 엔지니어링 | 루프들의 연결 | 서브에이전트, 워크플로 그래프, 스케줄 트리거 — agent-frameworks | (지금 판) |
①→② 프롬프트에서 컨텍스트로
프롬프트 엔지니어링은 지시문 자체를 다듬는 일이었다 — 역할을 주고, 예시를 몇 개 붙이고, "단계별로 생각하라"를 붙이는 식. 모델이 좋아지면서 문장 기교의 한계 효용은 줄었고, 성능을 가르는 건 창(context window)에 무엇을 넣어 주느냐가 됐다: 어떤 문서를 검색해 넣을지(RAG), 무엇을 기억시킬지(메모리), 무엇을 앞에 고정해 캐시로 만들지. 이 배분 문제가 context-engineering 이다.
②→③ 컨텍스트에서 루프로
창을 아무리 잘 채워도 호출이 한 번이면 모델은 행동하고 확인할 수 없다. 루프 엔지니어링은 모델을 반복 고리 안에 넣는다: 모델이 판단 → 도구 호출 → 결과 관찰 → 다시 판단 ⟲ → 끝낼 조건에서 종료. 이때 설계 대상은 프롬프트가 아니라 하네스(harness) 다 — 어떤 도구를 줄지, 언제 멈출지, 결과를 어떻게 검증할지, 긴 루프에서 컨텍스트가 안 썩게 할지. 패턴별 정리는 agent-design-patterns, 실전 배선은 set-up-tool-use.
주고받기의 실체 — 루프는 그냥 while 문이다
신비로운 게 없다. API 는 무상태라 상태는 서버가 아니라 내 손의 메시지 배열에 있고, 도는 주체는 모델이 아니라 내 코드다. 모델은 매 턴 "다음 한 수"만 낸다.
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}) # 결과 회신
- 모델이 도구를 쓰고 싶으면 응답이
stop_reason: "tool_use"로 멈춘다. 실행은 앱이 하고, 결과를tool_result로 담아 다시 user 메시지로 보낸다. 이 왕복이 "주고받기"의 전부다. - 병렬 호출이면 결과 여러 개를 한 메시지에 담아 돌려준다 — 따로 보내면 모델이 병렬을 끈다.
- 매 요청에 전체 히스토리를 다시 보내므로, 반복되는 앞부분은 캐시로 싸게 만든다 (prompt-caching).
- 이 while 문을 직접 안 짜도 된다: SDK 툴러너가 루프만 대행해 주고, 더 나아가면 서버가 루프째 대행한다(아래 스펙트럼).
오케스트레이션의 실체 — cron 은 시작 버튼이지 지휘자가 아니다
"매일 알아서 도는 에이전트"의 구조는 두 층으로 나뉜다. 언제 깨울지(트리거)와 깨어난 뒤 어떻게 일할지(루프). cron 은 앞쪽만 담당한다.
"깨운다"의 실체 — 크론 줄에 적힌 건 명령 한 줄이다
크론(launchd·cron)은 정해진 시각에 명령 한 줄을 실행해 줄 뿐이다. 그 한 줄이 무엇이냐에 따라 AI 세션이 어떻게 태어나는지가 갈린다:
| 깨우는 한 줄 | 무슨 일이 일어나나 |
|---|---|
python3 crawl.py |
AI 없음 — 결정적 스크립트만 돈다 (트렌드 크롤이 이 경우) |
claude -p "오늘치 갱신해" |
헤드리스 에이전트 프로세스가 떠서, 그 안에서 위의 while 루프가 돈다 |
API 로 세션 생성 (sessions.create 류) |
서버 하네스에 "새 세션 하나 시작해"를 요청 — 스케줄 배포가 내부적으로 하는 일이 이것이다 |
| 웹훅 핸들러 | HTTP 요청이 도착하면 핸들러 코드가 위 둘 중 하나를 실행한다 — 트리거가 시계가 아니라 사건일 뿐, 그 다음은 같다 |
깨어난 세션은 아무것도 기억하지 못한 채 태어난다(무상태). 그래서 이어서 일하려면 파일에서 컨텍스트를 읽는다 — 리포의 문서·로그·메모리 파일이 그 역할이다. "크론이 깨우고, 세션은 파일을 읽어 상황을 파악하고, 루프를 돌고, 결과를 파일·커밋으로 남기고 죽는다" — 이 한 사이클이 자동화된 에이전트 운영의 최소 단위다.
루프를 누가 도느냐는 스펙트럼이다:
| 방식 | 루프를 도는 주체 | 호스팅 | 쓰는 때 |
|---|---|---|---|
| 수동 while 문 | 내 코드 | 나 | 제어를 전부 갖고 싶을 때 |
| SDK 툴러너 | SDK (루프만 대행) | 나 | 대부분의 커스텀 도구 에이전트 |
| 서버 하네스 (Managed Agents 류) | 서버 (루프+실행 샌드박스) | 서버 | 호스팅·스케줄까지 맡기고 싶을 때 — cron 식 스케줄 배포는 "세션을 생성"하는 트리거일 뿐, 루프는 하네스가 돈다 |
| 에이전트 하네스 제품 (Claude Code 등) | 내장 하네스 | 나 | 파일·셸이 필요한 코딩 에이전트 |
③→④ 루프에서 그래프로
루프 하나가 감당 못 하는 것들이 있다 — 넓게 병렬로 퍼지는 조사, 서로 독립인 작업 트랙, 검증자를 따로 두고 싶은 경우. 그래서 지금 판은 루프들을 노드로 엮는다:
- 서브에이전트 — 부모 루프가 자식 루프를 도구처럼 호출한다. 핵심 효용은 컨텍스트 격리: 자식이 쓴 수만 토큰의 탐색이 부모 창을 오염시키지 않는다.
- 워크플로 그래프 — 노드는 에이전트나 함수, 엣지는 제어 흐름. 결정적인 부분은 코드로, 판단이 필요한 부분만 모델로 돌리는 게 요지다. LangGraph 류 프레임워크가 이 자리다 (agent-frameworks — 대부분은 직접 루프를 짜는 게 낫다는 결론도 같이 볼 것).
- 사람의 자리 — 파괴적 행동 앞의 승인 게이트도 그래프의 노드다 (set-up-tool-use §5).
"그래프 엔지니어링"이라는 이름은 이 판을 가리킨다 (코드팩토리 영상 [aggregator] — 원본 → 2026-08-09-loop-engineering-sources). 이름이 무엇이든 실체는 같다: 프롬프트 문장 → 컨텍스트 배분 → 루프 하네스 → 루프들의 그래프로, 설계의 단위가 계속 커져 왔다는 것.
정리
- 주고받기 = 무상태 API 위에서 내 코드가 도는 while 문. 모델은 매 턴 다음 한 수만 낸다.
- cron = 깨우는 버튼. 오케스트레이션 = 깨어난 루프들을 그래프로 엮는 설계.
- 아래 세대로 갈수록 프롬프트 기교보다 시스템 설계가 성능을 가른다.