
Outer Agent Loop: 엔지니어들이 2026년에 프로덕션급 Agentic AI 시스템을 구축하는 방법
요약
2026년 프로덕션급 Agentic AI 시스템 구축을 위한 'Outer Agent Loop' 아키텍처 패턴을 다룹니다. 단순 프롬프팅을 넘어 LLM 코딩 에이전트를 제어하고 평가하는 오케스트레이션 레이어 설계 방법과 모범 사례를 제시합니다.
핵심 포인트
- Inner Loop(LLM)를 감싸는 Outer Loop(Harness) 설계의 중요성
- 에이전틱 시스템의 생산성 향상 및 아키텍처 패턴 탐구
- LLM-as-Judge를 활용한 완료 신호 및 평가 문제 해결
- 관측 가능성, 비용 제한, 인간의 감독을 포함한 설계 원칙
Meta Description: 시니어 엔지니어들이 Claude Code와 같은 LLM 코딩 에이전트(coding agents)를 감싸는 하네스(harness) 수준의 오케스트레이션 레이어인 프로덕션급 outer agent loops를 어떻게 구축하고 있는지 알아보세요. 2026년 Agentic AI 시스템을 위한 아키텍처 패턴, Python 코드, 실패 모드 및 모범 사례를 탐구합니다.
목차
- 소프트웨어 엔지니어링을 집어삼키고 있는 루프 (The Loop)
- 두 개의 루프, 하나의 시스템: Inner Loop vs. Outer Loop
- 프로덕션 하네스(Production Harness)의 해부
- 코드: Python으로 첫 번째 Outer Loop 구축하기
- 완료 신호 문제: LLM-as-Judge
- 루프가 승리하는 곳: ROI가 높은 유스케이스 (Use Cases)
- 루프가 실패하는 곳: 아키텍처 부채의 함정
- Claude Tag와 멀티플레이어 에이전트 패러다임
- 관측 가능성 (Observability), 비용 제한 및 인간의 감독
- 인지적 의존성 문제
- 엔지니어링의 건전성을 유지하는 루프 설계하기
- 결론: 루프는 다가오고 있습니다. 올바르게 구축하세요.
소프트웨어 엔지니어링을 집어삼키고 있는 루프 (The Loop)
"저는 더 이상 Claude에게 직접 프롬프트를 입력하지 않습니다. Claude에게 프롬프트를 입력하고 무엇을 할지 결정하는 루프(loops)를 실행하고 있습니다. 제 업무는 루프를 작성하는 것입니다."
— Boris Cherny
이 인용구는 공상 과학 소설에서 나온 것이 아닙니다. 2026년 6월에 바이럴이 된 한 개발자의 게시물에서 발췌한 것입니다. 같은 주에 Armin Ronacher(Flask, Jinja2 및 오케스트레이션 플랫폼 Pi.dev의 제작자)가 _"The Coming Loop"_라는 제목의 명상글을 게시했는데, 이 글은 Hacker News에서 24시간도 채 되지 않아 349개의 추천과 244개의 댓글을 기록했습니다.
무언가 변화했습니다. 새로운 프레임워크 출시와 함께 오는 점진적이고 미세한 변화가 아닙니다. 이것은 지각 변동과 같은 변화입니다. 직함이 업데이트되기도 전에 직무 기술서가 바뀌어 버리는 그런 종류의 변화 말입니다.
그 변화란 바로 이것입니다: 가장 생산적인 AI 증강 엔지니어들은 더 이상 언어 모델(language models)에 수동으로 프롬프트를 입력하지 않습니다. 그들은 자신을 대신해 언어 모델에 프롬프트를 입력하고, 결과를 평가하며, 계속 진행할지, 재시도할지, 분기할지, 또는 에스컬레이션(escalate)할지를 결정하는 시스템을 설계합니다. 그들은 하네스(harnesses)를 작성하고 있습니다. 그들은 루프(loops)를 구축하고 있습니다.
만약 당신이 아직 이 아키텍처(architecture)에 대해 고민하고 있지 않더라도, 곧 그렇게 될 것입니다. 왜냐하면 "에이전틱 루프 (agentic loops)"에 관한 담론이 그 어떤 이전의 AI 패턴보다도 빠르게 연구 논문에서 프로덕션 대시보드(production dashboards)로 이동했기 때문입니다. Anthropic은 자사 제품 팀 코드의 65%가 그들의 새로운 에이전틱 시스템의 내부 버전인 Claude Tag에 의해 작성되었다고 방금 발표했습니다. Qwen 팀은 1,000만 개 이상의 상호작용 궤적(interaction trajectories)을 사용하여 7개 도메인 전반에 걸쳐 에이전틱 환경을 시뮬레이션하도록 특별히 훈련된 3,970억 개의 파라미터를 가진 언어 모델인 Qwen-AgentWorld-397B를 출시했습니다. 팀들은 5배의 생산성 향상을 보고하고 있습니다. 작성자 본인이나 그 어떤 단일 인간도 완전히 설명할 수 없는 코드베이스(codebases)가 성장하고 있습니다.
이 포스트는 **아우터 에이전트 루프 (outer agent loop)**에 대한 심층적인 기술 가이드입니다. 이것이 무엇인지, 어떻게 구축하는지, 어디에서 승리하고 어디에서 무너지는지, 그리고 이를 출시하면서 어떻게 엔지니어링의 정신적 건강(engineering sanity)을 유지할 수 있는지에 대해 다룹니다.
두 개의 루프, 하나의 시스템: 이너 루프 vs. 아우터 루프 (Inner vs. Outer Agent Loops)
프로덕션 하네스(production harness)를 구축하기 전에, 거의 모든 "에이전틱 AI (agentic AI)" 논의에서 혼동하는 아키텍처적 차이를 이해해야 합니다. 루프는 하나가 아니라 두 개입니다.
이너 루프 (The Inner Loop)
이너 루프는 여러분이 Claude Code, 도구(tools)를 사용하는 GPT-4o, 또는 함수 호출(function calling) 기능이 있는 현대적인 LLM을 사용해 보았다면 이미 익숙한 것입니다. 구조는 다음과 같습니다:
LLM이 메시지를 수신
→ 도구 호출 (read_file, run_tests, search_web)
→ 도구 결과 수신
...
이 루프는 단일 대화 턴(conversation turn) 동안 모델의 컨텍스트 윈도우 (context window) 내부에 존재합니다. 모델이 이를 오케스트레이션(orchestrate)합니다. 모델은 도구 호출을 멈추고 최종 응답을 내보내기 때문에 자신이 "끝났다"는 것을 알고 있습니다. 외부의 오케스트레이션은 없으며, 오직 모델과 그 도구들, 그리고 모델의 자체 종료 로직(self-termination logic)만 존재합니다.
내부 루프(inner loop)는 강력합니다. 하지만 이는 근본적으로 컨텍스트 윈도우(context window)의 제한, 긴 대화 과정에서의 어텐션 드리프트(attention drift), 그리고 모델 스스로 작업이 완료되었다고 선언하려는 의지에 의해 제약을 받습니다.
외부 루프 (The Outer Loop)
외부 루프 — 에이전트 엔지니어링(agentic engineering) 커뮤니티가 현재 "하네스 루프(harness loop)"라고 부르는 것 — 는 모델의 컨텍스트 외부에 존재합니다. 이는 하나 이상의 내부 루프를 감싸는 당신이 작성하는 코드입니다. 이 루프는 다음 사항들을 결정합니다:
- 내부 루프의 출력이 수용 가능한지 여부
- 동일한 세션에 후속 메시지를 주입할지 여부
- 수정된 컨텍스트로 새로운 세션을 시작할지 여부
- 병렬 에이전트로 팬아웃(fan out)하여 결과를 병합할지 여부
- 인간 체크포인트(human checkpoint)로 에스컬레이션할지 여부
- 전체 작업이 진정으로 완료되었는지 여부
[Harness]
→ 작업 큐(task queue)에서 작업 디큐(dequeue)
→ 초기 컨텍스트 구축 (시스템 프롬프트 + 작업 설명 + 제약 조건)
...
외부 루프는 진정한 에이전트 엔지니어링(agentic engineering)이 일어나는 곳입니다. 그리고 Ronacher가 언급했듯이, 이는 현재 AI 툴링 환경에서 가장 강력하면서도 동시에 가장 위험한 패턴입니다.
프로덕션 하네스의 구조 (Anatomy of a Production Harness)
프로덕션급 외부 루프는 단순히 API 호출을 포함한 while True: 문이 아닙니다. 이는 의도적으로 설계되어야 하는 최소 6개의 구성 요소를 가집니다:
3.1 작업 큐 (Task Queue)
작업(Task)은 업무의 단위입니다. 프로덕션 하네스는 작업을 인라인(inline)으로 받는 대신, 내구성이 있는 큐(Redis, SQS, FOR UPDATE SKIP LOCKED가 적용된 Postgres 테이블 등)에서 작업을 디큐(dequeue)합니다. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:
- 지속성 (Persistence): 작업이 크래시(crash) 상황에서도 생존함
- 중복 제거 (Deduplication): 재시도 시 중복 작업이 발생하지 않음
- 우선순위 지정 (Prioritization): 긴급한 작업이 큐를 앞질러 처리됨
- 속도 제한 (Rate limiting): 시간 범위당 API 비용 지출을 제어함
3.2 컨텍스트 빌더 (Context Builder)
각 작업에는 잘 설계된 초기 컨텍스트 (Initial context)가 필요합니다: 에이전트의 역할을 정의하는 시스템 프롬프트 (System prompt), 작업 설명, 관련 코드 또는 파일, 범위에 대한 제약 조건, 그리고 기대되는 출력 형식에 대한 설명 등이 포함됩니다. 컨텍스트 빌더 (Context builder)는 이 모든 것을 모델이 처음 접하게 될 첫 번째 메시지로 직렬화 (Serializing)하는 역할을 담당합니다.
나쁜 컨텍스트가 입력되면(Bad context in), 나쁜 반복 결과가 나옵니다(bad iterations out). 쓰레기가 들어가면(Garbage in), 쓰레기가 그대로 들어갑니다(garbage still in). 다만 이제는 5번의 재시도 사이클(Retry cycles)이 추가될 뿐입니다.
3.3 세션 관리자 (Session Manager)
세션 관리 (Session management)는 실행 중인 대화의 상태를 추적합니다. 메시지 기록을 지속적으로 유지 (Persist)하여, 컨텍스트를 잃지 않고 아우터 루프 (Outer loop) 반복 간에 세션을 계속 이어갈 수 있도록 합니다. 이는 "후속 질문 주입 (Inject follow-up)" 패턴에 있어 매우 중요합니다.
3.4 완료 신호 엔진 (Completion Signal Engine)
가장 미묘한 차이를 다루는 구성 요소입니다. 모델의 출력을 바탕으로 {DONE, CONTINUE, RETRY, ESCALATE} 집합 중 하나의 신호를 생성합니다. 이는 다음과 같은 방식이 될 수 있습니다:
- 기계적 방식 (Mechanical): 테스트 스위트 (Test suite)를 실행하고 종료 코드 (Exit code)를 확인
- 구문적 방식 (Syntactic): 모델이 생성하도록 지시받은 구조화된 JSON 응답을 파싱 (Parse)
- 의미적 방식 (Semantic): 다른 LLM에게 품질을 판단하도록 요청 (LLM-as-judge)
- 하이브리드 방식 (Hybrid): 기계적 방식을 먼저 수행한 후, 의미적 방식으로 폴백 (Fallback)
3.5 반복 예산 (Iteration Budget)
모든 하네스 (Harness)에는 엄격한 제한이 필요합니다: 최대 반복 횟수, 최대 소비 토큰 (Tokens), 최대 실제 실행 시간 (Wall-clock time). 이러한 제한이 없다면, 갇혀버린 루프 (Stuck loop)는 비용을 낭비하고 대기열 (Queue)을 차단하게 됩니다.
3.6 출력 커미터 (Output Committer)
작업이 DONE 상태에 도달하면, 커미터는 최종 결과물 (Artifact)을 가져와 특정 작업을 수행합니다: 풀 리퀘스트 (Pull request)를 생성하거나, 파일을 작성하거나, Slack으로 메시지를 보내거나, 데이터베이스 레코드를 업데이트합니다. 커미터는 외부 세계와 맞닿아 있는 하네스의 경계입니다.
코드: Python으로 첫 번째 아우터 루프 구축하기
Anthropic Python SDK를 사용하여 구체적이고 작동 가능한 아우터 루프 하네스를 구축해 보겠습니다. 이 예제는 자동화된 테스트 기반의 완료 신호를 사용하여 코드 생성 작업을 관리합니다.
import anthropic
import subprocess
import json
...
{% endraw %}
\n{test_output[:3000]}\n
{% raw %}
f"Remember: make invalid states unrepresentable, don't add fallbacks."
)
...
주목할 만한 몇 가지 핵심 설계 결정 사항은 다음과 같습니다:
테스트 우선 완료 검증 (Test-first completion verification). 모델이 스스로 {"status": "done"}이라고 보고하더라도, 우리는 이를 수락하기 전에 테스트 스위트 (test suite)를 실행합니다. 모델은 자신의 작업에 대해 낙관적입니다. 기계적인 검증 없이 자기 보고(self-reports)를 신뢰하지 마세요.
세션 재시작 대신 컨텍스트 주입 (Context injection over session restarts). 테스트가 실패하면, 새로운 대화를 시작하는 대신 실패 출력 내용을 *동일한 대화 (same conversation)*에 다시 주입합니다. 이는 모델이 이미 시도했던 것에 대한 이해를 보존하여, 불필요한 재탐색을 방지합니다.
명시적 예산 집행 (Explicit budget enforcement). max_iterations와 max_tokens_total은 강제 중단 지점입니다. 둘 중 하나라도 도달하면 작업은 인간 대기열 (human queue)로 에스컬레이션되며, 조용히 실패하거나 무한 루프를 돌지 않습니다.
완료 신호 문제: LLM-as-Judge
기계적인 테스트 러너 (test runners)는 검증 가능한 출력이 있는 작업에서 매우 훌륭하게 작동합니다. 즉, 단위 테스트 (unit tests)가 통과하거나 통과하지 못하거나 둘 중 하나인 경우입니다. 하지만 가치 있는 에이전트적 (agentic) 작업의 상당 부분은 어느 정도 맞는 (correct-ish) 출력을 생성합니다. 이러한 출력은 프로그램 방식으로 검증하기는 어렵지만, 지능적인 관찰자가 평가하기에는 쉽습니다. 코드 리팩터링 (code refactoring), 문서 생성 (documentation generation), 아키텍처 분석 (architectural analysis), 그리고 PR 설명 (PR descriptions)이 모두 이 범주에 속합니다.
이러한 작업의 경우, 아우터 루프에는 **의미론적 완료 신호 (semantic completion signal)**가 필요하며, 바로 이 지점에서 LLM-as-judge가 등장합니다.
def llm_judge(
client: anthropic.Anthropic,
task_description: str,
agent_output: str,
rubric: str
) -> tuple[CompletionSignal, str]:
"""
빠르고 저렴한 모델을 사용하여 에이전트의 출력이
루브릭 (rubric)에 따라 작업 요구 사항을 충족하는지 판단합니다.
(signal, reasoning)을 반환합니다.
"""
judge_prompt = f"""당신은 엄격하지만 공정한 코드 리뷰어입니다.
작업 설명 (TASK DESCRIPTION):
{task_description}
평가 루브릭 (EVALUATION RUBRIC):
{rubric}
평가할 에이전트 출력 (AGENT OUTPUT TO EVALUATE):
{agent_output}
---
루브릭에 따라 출력을 평가하세요. 반드시 유효한 JSON으로만 응답하세요:
{{
"verdict": "DONE" | "CONTINUE" | "ESCALATE",
"score": <정수 1-10>,
"reasoning": "<판단 근거를 설명하는 한 단락>",
"specific_issues": ["<문제 1>", "<문제 2>"] // DONE인 경우 빈 리스트
}}"""
response = client.messages.create(
model="claude-haiku-4-5", # 판단을 위해 빠르고 저렴한 모델 사용
max_tokens=512,
messages=[{"role": "user", "content": judge_prompt}]
)
raw = response.content[0].text.strip()
try:
result = json.loads(raw)
verdict_map = {
"DONE": CompletionSignal.DONE,
"CONTINUE": CompletionSignal.CONTINUE,
"ESCALATE": CompletionSignal.ESCALATE,
}
signal = verdict_map.get(result["verdict"], CompletionSignal.ESCALATE)
reasoning = result.get("reasoning", "제공된 판단 근거가 없습니다.")
# 계속 진행하는 경우 실행 가능한 후속 조치 구축
if signal == CompletionSignal.CONTINUE and result.get("specific_issues"):
issues = "\n".join(f"- {i}" for i in result["specific_issues"])
reasoning = f"점수: {res"
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
