[AI] 루프 엔지니어링: 에이전트를 움직이는 실행 루프 설계
개요
한 번의 요청에 한 번의 응답으로 끝나는 사용법에서는 프롬프트(Prompt)가 결과의 거의 전부를 결정한다. 그래서 프롬프트를 어떻게 쓰느냐인 프롬프트 엔지니어링(Prompt Engineering)이 오랫동안 핵심 역량으로 여겨졌다.
코딩 에이전트(Agent) 하네스는 이미 다르게 동작한다. 모델이 스스로 도구를 호출하고, 결과를 관찰하고, 다음 판단을 내리는 과정을 세션 하나 안에서 몇십 번 반복한다. 이 반복 자체는 루프 엔지니어링이 아니다. 하네스가 기본으로 제공하는 안쪽 루프(inner loop)다. 관찰하고 추론하고 행동하는 메커니즘은 대부분 하네스의 것이지, 새로 설계하는 것이 아니다.
세션이 끝나면 사람이 결과를 보고, 다 됐는지 판단하고, 아니면 다시 시켰다. 이 바깥 루프(outer loop)의 주체가 사람이었다. 루프 엔지니어링(Loop Engineering)은 이 바깥 루프에서 사람을 빼는 설계다. 발견, 계획, 실행, 검증, 반복이라는 사이클을 사람 개입 없이 시스템이 스스로 돌게 만든다.
루프 엔지니어링이 하네스 위에 실제로 더하는 것은 검증을 기계가 하게 만드는 것과 세션이 끝나도 상태를 잇는 것이다.
| 용어 | 의미 |
|---|---|
| 안쪽 루프(Inner Loop) | 세션 하나 안에서 도는 관찰-추론-행동 반복. 하네스가 제공 |
| 바깥 루프(Outer Loop) | 검증하고 완료를 판단하고 재투입하는 루프. 원래 사람이 돌림 |
| 검증(Verify) | 결과가 목표와 기준을 충족하는지 확인하는 단계 |
| 자기 편향(Self-bias) | 작성한 모델이 자기 결과를 후하게 채점하는 현상 |
| 메모리(Memory) | 세션을 넘겨 진행 상태를 잇는 외부 저장소 |
| 오케스트레이터(Orchestrator) | 목표를 하위 작업으로 나눠 여러 에이전트에 위임하는 조율자 |
하네스가 주는 안쪽 루프
| 하네스가 주는 것 | 설명 |
|---|---|
| 관찰-추론-행동 반복 | 모델이 도구를 부르고 결과를 관찰해 다음 행동을 정하는 안쪽 루프 |
| 도구 호출 | 파일 읽기, 편집, 셸 실행 등 액션 공간 |
| 컨텍스트 컴팩션 | 세션 안에서 컨텍스트가 차면 자동으로 요약 |
| 서브에이전트 | 하위 작업을 독립 컨텍스트에 위임 |
| 스텝 상한 | 세션당 최대 반복 횟수 |
# 하네스의 안쪽 루프: 세션 하나 안에서 자동으로 돈다
def inner_loop(goal, tools, model):
context = [system_prompt(tools), user_message(goal)]
while True:
response = model.generate(context) # 추론
if response.is_final_answer():
return response.text # 모델이 다 됐다고 선언하면 멈춘다
result = tools.execute(response.tool_call) # 행동
context.append(observation(result)) # 관찰이 루프의 핵심 한계가 마지막에 있다. 안쪽 루프는 모델이 다 됐다고 선언하면 멈추고, 그 판단이 맞는지는 확인하지 않는다. 세션이 끝나면 사람에게 돌아오고, 사람이 결과를 본다.
안쪽 루프 안에도 튜닝 지점은 있다. 도구 정의의 품질과 관찰 포맷이다. 도구 설명이 모호하면 모델이 엉뚱한 도구를 부르고, 도구가 수천 줄 로그를 그대로 돌려주면 컨텍스트가 순식간에 차 중요한 한 줄을 놓친다. 다만 이건 안쪽 루프의 질을 높이는 조정이지, 바깥 루프를 자동으로 닫는 일과는 다르다.
def observation(result):
text = result.stdout
# 관찰을 무제한으로 넣지 않는다. 앞뒤만 남기고 가운데를 잘라낸다
if len(text) > 8000:
text = text[:4000] + "\n...[생략]...\n" + text[-2000:]
return {
"role": "tool",
"exit_code": result.exit_code, # 성공/실패를 명시적으로 전달
"content": text,
}사람이 바깥 루프를 담당한다.
프롬프트 엔지니어링에서 반복의 주체는 사람이었다. 결과가 마음에 안 들면 사람이 다시 지시하고, 사람이 검토하고, 사람이 완료를 판단했다. 하네스가 안쪽 루프를 돌려 준 다음, 바깥 루프는 사람이 손으로 돌린 셈이다.
# 프롬프트 방식: 바깥 루프의 주체가 사람이다
result = inner_loop(goal, tools, model) # 하네스가 안쪽 루프를 돈다
review_by_human(result) # 사람이 검토·판단
result = inner_loop(fix_goal, tools, model) # 사람이 다시 지시
# 완료 판단도, 재투입 결정도 사람이 한다| 축 | 프롬프트 엔지니어링 | 루프 엔지니어링 |
|---|---|---|
| 안쪽 루프 | 하네스 | 하네스 |
| 바깥 루프 주체 | 사람 | 시스템 |
| 사람의 일 | 판단하고 다시 지시 | 목표와 기준만 정의 |
| 손대는 지점 | 프롬프트 문장 | 검증과 지속 |
루프 엔지니어링은 이 표의 한 칸, 바깥 루프의 주체를 사람에서 시스템으로 바꾸는 일이다. 그런데 사람을 빼려면 사람이 하던 판단을 시스템이 대신해야 한다.
바깥 루프를 자동으로 닫는다
발견, 계획, 실행, 검증, 반복의 다섯 단계를 보면 앞의 세 단계는 하네스의 안쪽 루프가 이미 한다. 안쪽 루프를 한 번 돌린 결과를 받아 검증하고, 통과하면 완료하고 실패하면 다시 돌린다.
| 단계 | 하는 일 | 주체 |
|---|---|---|
| 발견(Discover) | 목표를 이해하고 필요한 정보를 탐색 | 하네스 안쪽 루프 |
| 계획(Plan) | 수집한 정보로 작업 계획 수립 | 하네스 안쪽 루프 |
| 실행(Execute) | 실제 작업 수행 | 하네스 안쪽 루프 |
| 검증(Verify) | 기준 충족 여부를 기계가 판정 | 사람이 설계 |
| 반복(Iterate) | 실패를 되돌려 다시 돌리고 상태를 지속 | 사람이 설계 |
# 사람이 설계하는 바깥 루프: 안쪽 루프를 감싼다
def outer_loop(goal, run_inner, verify, max_iterations=10):
context = memory.load(goal) # 이전 세션 상태를 이어받음
for i in range(max_iterations):
result = run_inner(goal, context) # 안쪽 루프 한 번 (하네스)
report = verify(result) # 검증 (사람이 더하는 것)
if report.passed:
memory.save(goal, "done")
return result # 통과하면 완료
context.append(report) # 실패면 재투입
memory.save(goal, context) # 상태 지속
return escalate(goal, context) # 상한 도달 시 사람에게이 코드에서 하네스가 주는 부분은 run_inner 한 줄이다. 나머지, 곧 검증하고 완료를 판단하고 실패를 되돌리고 상태를 남기는 부분이 바깥 루프이고 루프 엔지니어링의 실체다. 그리고 이 바깥 루프를 돌게 하는 유일한 조건은 verify 가 사람 없이 동작하는 것이다. 검증이 사람 판단에 의존하면 for 문은 매 반복마다 멈춰 사람을 기다린다.
검증을 시스템이 하게 만든다
바깥 루프를 자동으로 닫으려면 종료 판단을 시스템이 해야 하고, 그러려면 성공 기준이 시스템이 확인 가능해야 한다. 목표를 다 됐나 사람이 판단해야하는 형태로 두면 자동화가 불가능하다. 모든 테스트가 통과하고 빌드가 성공하면 시스템이 판정할 수 있는 형태로 바꿔야 한다.
주관적 판단이 꼭 필요할 때는 작성한 모델에게 맡기지 않는다. 작성자가 자기 결과를 채점하면 후하게 준다. 원문 저자가 같은 글을 측정한 값이 이를 보여 준다. 작성자 본인이 채점하면 9.04 점, 독립된 에이전트가 채점하면 7.43 점으로 1.6 점 차이가 났다. 자기 편향이다. 그래서 검증은 작성자와 다른 에이전트가 맡는다.
# 잘못된 패턴: 작성자가 자기 결과를 채점 (자기 편향)
def verify_self(result, model):
return model.ask(f"이 결과가 충분한가? {result}") # 후하게 준다, 루프가 못 멈춘다
# 올바른 패턴: 기계가 확인 가능한 객관 게이트 + 독립 채점
def verify(result, subagent):
objective = run_tests() # 테스트·빌드는 사람도 모델도 아닌 객관 신호
if not objective.passed:
return Report(False, objective.summary)
review = subagent.review(result, criteria=SUCCESS_CRITERIA) # 작성자가 아닌 에이전트
return Report(review.passed, review.reason)| 종료 조건 | 판단 기준 | 사람 없이 신뢰 가능한가 |
|---|---|---|
| 최종 응답 선언 | 모델이 스스로 완료를 선언 | 아니오, 미완을 완료로 착각 |
| 스텝·예산 상한 | 반복 횟수나 토큰 초과 | 안전장치일 뿐 완료 근거 아님 |
| 객관 검증 통과 | 테스트·빌드가 통과 | 예, 바깥 루프를 닫는 신호 |
| 독립 채점 통과 | 다른 에이전트가 기준으로 채점 | 예, 주관 판단이 필요할 때 |
객관적 기준이 있으면 바깥 루프의 종료 판단이 사람 손을 떠난다. 검증이 실패했을 때 실패를 입력으로 되돌려 다시 도는 사이클을 자동화할 수 있다.
sequenceDiagram autonumber participant O as 바깥 루프 participant I as 안쪽 루프(하네스) participant G as 객관 게이트(테스트) participant S as 독립 서브에이전트 O->>I: 목표와 이전 상태 전달 I-->>O: 결과 제출 O->>G: 테스트·빌드 실행 G-->>O: 실패 O->>I: 실패를 입력으로 재투입 I-->>O: 수정된 결과 O->>G: 재실행 G-->>O: 통과 O->>S: 독립 채점 요청 S-->>O: 통과 O->>O: 완료로 종료
세션을 넘겨 상태를 잇는다
하네스의 컨텍스트 컴팩션은 세션 하나 안의 관리일 뿐이다. 세션이 끝나면 컨텍스트는 사라진다. 그런데 바깥 루프는 종종 세션 하나로 끝나지 않는다. 반복이 여러 세션에 걸치거나, 스케줄에 따라 매일 도는 루프라면 어제 세션의 결과를 오늘 세션이 알아야 한다. 이때 필요한 것이 세션을 넘겨 상태를 잇는 메모리다.
메모리는 두 종류의 파일로 관리한다. 스킬 파일은 성공의 정의와 규칙 같은 프로젝트 지식을 담아 매 세션이 참조하고, 진행 파일은 어디까지 했고 무엇이 미해결인지 남겨 다음 세션이 이어받게 한다.
# SKILL.md: 매 세션이 참조하는 기준
## 성공의 정의
모든 테스트 통과, 린트 경고 0 이면 완료로 본다.
## 금지 사항
@Query 직접 사용 금지, 커밋 메시지에 이모지 금지
# progress.md: 세션을 넘겨 지속되는 상태
- [x] 로그인 API 구현 (테스트 통과)
- [ ] 토큰 재발급 로직 (검증 실패: 만료 케이스 미처리)
- 다음 재개 지점: TokenService.refresh 의 만료 분기세션 하나 안에서 컨텍스트를 다루는 전략과 세션을 넘겨 잇는 방식은 계층이 다르다. 앞의 두 전략은 하네스가 세션 안에서 처리하고, 마지막 외부 메모리가 세션 사이를 잇는 지속에 해당한다.
| 전략 | 범위 | 역할 |
|---|---|---|
| 슬라이딩 윈도우 | 세션 안 | 오래된 메시지부터 버림 |
| 컴팩션(요약) | 세션 안 | 누적 대화를 요약본으로 교체 |
| 외부 메모리 | 세션 간 | 상태를 파일에 남겨 다음 세션이 이어받음 |
컴팩션은 세션 안에서 컨텍스트가 일정 비율을 넘으면 지금까지의 대화를 요약본으로 교체한다. 이때 목표와 미완료 작업, 핵심 결정은 반드시 요약에 살린다.
def maybe_compact(context, model, max_tokens=180_000):
if count_tokens(context) < max_tokens * 0.8:
return context # 여유가 있으면 그대로 둔다
system = context[0] # 시스템 프롬프트는 보존
recent = context[-6:] # 최근 몇 스텝은 원본 유지
older = context[1:-6]
summary = model.generate([summarize_instruction(), *older]) # 목표·미완료·결정을 남김
return [system, summary_message(summary), *recent]컴팩션이 한 세션 안의 압축이라면 메모리는 세션 사이의 지속이다. 이 둘이 있어야 긴 작업이 중단 지점에서 이어지고, 반복이 이전 결과에서 개선된다. 세션마다 처음부터 다시 시작하면 바깥 루프는 개선이 아니라 제자리를 돈다.
바깥 루프의 규모와 경계
행동의 자유도로 보면 오픈 루프와 클로즈드 루프로 갈린다. 오픈 루프는 행동 공간을 넓게 열어 탐색적으로 돌지만 토큰을 크게 소모하고 결과를 예측하기 어렵다. 클로즈드 루프는 목표와 단계, 평가 기준, 종료 지점을 사람이 미리 정의하고, 그 안에서 수행된다.
| 축 | 오픈 루프(Open Loop) | 클로즈드 루프(Closed Loop) |
|---|---|---|
| 경계 | 느슨함, 탐색적 | 목표·기준·종료 지점이 명확 |
| 토큰 소모 | 매우 많음 | 일반적 예산으로 운영 |
| 예측 가능성 | 낮음 | 높음, 매 실행이 이전 결과에서 개선 |
| 적합한 상황 | 미지의 문제 탐색 | 대부분의 실무 |
CLOSED_LOOP = {
"goal": "로그인 API 의 토큰 만료 버그를 고친다",
"success_criteria": "AuthTokenTest 전체 통과, 린트 경고 0", # 기계가 판정 가능
"termination": "검증 통과 또는 반복 10회 도달",
"budget_tokens": 200_000,
}규모로 보면 단일 에이전트 루프와 플릿 루프(Fleet Loop)로 갈린다. 단일 에이전트 루프는 하나가 전체 사이클을 돈다. 플릿 루프는 오케스트레이터가 목표를 나눠 전문가 에이전트에 배분하고 평가 게이트로 품질을 보장한다. 플릿은 토큰이 에이전트 수만큼 늘고 조율이라는 새 실패 지점이 생기므로, 하위 작업이 정말 독립적일 때만 나눈다.
def fleet_loop(goal, model):
subtasks = orchestrator.decompose(goal, model) # 목표를 독립 하위 작업으로
results = run_parallel([
expert(st).run() for st in subtasks # 전문가마다 독립 컨텍스트
])
passed = [r for r in results if evaluation_gate(r).passed] # 평가 게이트 통과분만
return orchestrator.synthesize(goal, passed, model)| 축 | 단일 에이전트 루프 | 플릿 루프 |
|---|---|---|
| 구조 | 하나가 전체 사이클 | 오케스트레이터 → 전문가 → 서브에이전트 |
| 토큰 소모 | 중간 규모 코딩 5만~20만 | 오케스트레이터와 전문가 3명 50만~200만 |
| 적합한 작업 | 집중된 단순 목표 | 넓고 나눌 수 있는 목표 |
오토메이션(트리거), 워크트리(격리), 커넥터(외부 반영) 같은 나머지 요소는 이 바깥 루프를 언제 시작하고 어디에 격리하고 결과를 어디에 반영할지를 정하는 조합의 문제다. 대부분 하네스나 주변 도구가 제공하며, 검증과 지속만큼 루프의 성패를 가르지는 않는다.
실제 패턴과 도입 순서
| 루프 | 하네스가 도는 부분 | 사람이 더하는 검증·종료 |
|---|---|---|
| 코딩 루프 | 지식 파일 읽고 계획해 코드 편집 | 테스트 실행, 실패 시 재수정, 통과 시 종료 |
| 리서치 루프 | 출처 검색하고 요약 | 출처 대조 검증, 신뢰도 임계값 넘으면 종료 |
| 콘텐츠 루프 | 주제 정하고 초안 작성 | 독립 크리틱 채점, 성공 기준 넘으면 게시 |
루프를 처음 세울 때는 여섯 요소를 다 갖추지 않는다.
- 성공의 정의를 스킬 파일에 적는다. 시스템이 판정할 수 있는 기준으로 정의한다.
- 작성자와 분리된 검증을 붙인다. 객관적인 검증을 먼저 두고, 주관적인 판단은 다른 에이전트에게 맡긴다.
- 진행 파일로 상태를 남긴다. 다음 세션이 중단 지점에서 이어받게 한다.
버그 수정 루프를 이 순서로 진행하면 하네스와 사람의 역할이 갈리는 지점이 분명해진다.
- 성공의 기준을 정의한다. 실패하는 테스트 이름을 스킬 파일과 목표에 명시해, 무엇을 통과시켜야 완료인지를 시스템이 알게 한다.
- 안쪽 루프에 맡긴다. 파일을 읽고 원인을 찾아 코드를 고치는 발견과 실행은 하네스의 안쪽 루프가 수행한다.
- 객관적인 기준으로 검증한다. 안쪽 루프가 다 됐다고 하면 그 즉시 테스트를 돌린다. 통과하면 종료하고, 실패하면 로그를 되돌려 다시 돌린다.
- 상한을 두고, 지속적으로 수행한다. 반복 상한을 걸어 무한 반복을 막고, 진행을 파일에 남겨 다음 세션이 이어받게 한다.
이 순서에서 프롬프트 문장을 다듬는 일은 아주 작은 비중이다. 완료율을 끌어올린 것은 검증을 종료 조건으로 건 결정과 성공의 정의를 시스템이 판정 가능하게 만든 결정이다.