50줄로 만드는 최소 에이전트 루프
왜 “최소”인가
섹션 제목: “왜 “최소”인가”복잡한 에이전틱 프레임워크를 사용하기 전에, 가장 단순한 형태의 루프가 어떻게 생겼는지 이해하는 것이 중요하다. Anthropic의 가이드가 일관되게 강조하는 원칙이기도 하다. 필요 이상의 복잡도를 추가하지 말라. 많은 경우 50줄짜리 루프가 수천 줄짜리 프레임워크가 하는 일을 충분히 할 수 있다.
이 챕터에서 만들 최소 에이전트는 세 가지 핵심 요소만 갖는다.
- while 루프 — 모델 호출과 도구 실행을 반복한다.
- stop_reason 분기 —
end_turn이면 종료,tool_use이면 계속한다. - max iteration 가드 — 루프가 무한히 돌지 않도록 상한을 강제한다.
전체 구현 (48줄)
섹션 제목: “전체 구현 (48줄)”개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.
# minimal_agent.py — 최소 에이전트 루프 (48줄)import jsonfrom typing import Any
MAX_ITERATIONS = 20 # 무한 루프 방지 가드
def execute_tool(name: str, inputs: dict[str, Any]) -> str: """도구 이름과 입력을 받아 결과 문자열을 반환한다.""" if name == "calculator": expression = inputs["expression"] try: return str(eval(expression, {"__builtins__": {}}, {})) except Exception as e: return f"계산 오류: {e}. 올바른 수식을 입력하세요." raise ValueError(f"알 수 없는 도구: {name}")
def run_agent(task: str, model, tools: list[dict]) -> str: """ 최소 에이전트 루프. Returns: 최종 텍스트 응답 Raises: RuntimeError — max iteration 초과 시 """ messages = [{"role": "user", "content": task}]
for iteration in range(MAX_ITERATIONS): # ── REASON ────────────────────────────────────────── response = model.generate(messages=messages, tools=tools)
# ── 종료 조건 ──────────────────────────────────────── if response.stop_reason == "end_turn": return response.text
# ── ACT: 도구 실행 ──────────────────────────────────── tool_results = [] for call in response.tool_calls: result = execute_tool(call.name, call.input) tool_results.append({ "tool_use_id": call.id, "content": result, })
# ── OBSERVE: 결과를 컨텍스트에 추가 ───────────────────── messages.append({"role": "assistant", "content": response.tool_calls}) messages.append({"role": "tool", "content": tool_results})
# ── MAX ITERATION 초과 ──────────────────────────────────── raise RuntimeError( f"에이전트가 {MAX_ITERATIONS}회 이터레이션 안에 완료하지 못했습니다. " "작업을 더 작은 단위로 나누거나 MAX_ITERATIONS를 늘려보세요." )코드 해설: 각 부분의 역할
섹션 제목: “코드 해설: 각 부분의 역할”최소 루프 구조 시각화───────────────────────────────────────────────────────────run_agent("작업", model, tools) │ ├─ messages = [{user: "작업"}] │ └─ for iteration in range(20): ← MAX_ITERATIONS 가드 │ ├─ response = model.generate(...) ← REASON │ ├─ if end_turn: return ← 정상 종료 │ ├─ for call in tool_calls: ← ACT │ result = execute_tool(call) │ └─ messages += [assistant, tool] ← OBSERVE (컨텍스트 축적) │ └─ raise RuntimeError ← 비정상 종료 (MAX 초과)───────────────────────────────────────────────────────────MAX_ITERATIONS 가드는 루프에서 가장 중요한 안전장치다. 모델이 어떤 이유로든 end_turn을 반환하지 않으면 루프는 영원히 돌 수 있다. 이 가드 없이는 AutoGPT가 보여준 무한 루프 실패 모드가 재현된다. 20회라는 기본값은 보수적이다. 실제 작업 복잡도에 따라 조정할 수 있지만, 항상 유한한 상한을 두어야 한다.
도구 정의와 테스트
섹션 제목: “도구 정의와 테스트”# 도구 목록 정의calculator_tool = { "name": "calculator", "description": "수학 표현식을 계산합니다.", "input_schema": { "type": "object", "properties": { "expression": { "type": "string", "description": "계산할 수식 (예: '(2 + 3) * 4')" } }, "required": ["expression"] }}
# 사용 예시# result = run_agent(# task="357 곱하기 428의 결과를 계산하고 설명해줘",# model=my_model,# tools=[calculator_tool]# )# → 모델이 calculator("357 * 428")을 호출# → "152796"을 받아 컨텍스트에 추가# → "357 × 428 = 152,796입니다" 반환이 루프를 확장하는 다음 단계
섹션 제목: “이 루프를 확장하는 다음 단계”최소 루프는 의도적으로 단순하다. 실제 프로덕션 루프는 이 뼈대 위에 다음 요소들을 추가한다.
| 추가 요소 | 역할 | 관련 챕터 |
|---|---|---|
| 상태 관리 (State) | 이터레이션 간 메타데이터 유지 | 2-4 |
| 컨텍스트 압축 | 컨텍스트가 너무 길어지면 요약 | 05-context-in-loop |
| 종료 조건 다양화 | 토큰 캡, 시간 제한, no-progress 감지 | 06-loop-control |
| 에러 재시도 | 도구 실패 시 재시도 로직 | 07-failure-reliability |
| 인간 승인 게이트 | 파괴적 행동 전 확인 요청 | 06-loop-control |
| 관찰성(Observability) | 이터레이션별 트레이싱 | 09-observability |
02-loop-anatomy 섹션에서 우리는 루프 한 이터레이션의 내부 구조(Observe-Reason-Act-Evaluate), 추론 메커니즘(Chain-of-Thought), 도구 인터페이스(Tool Use / ACI), 상태 관리(구조화 출력), 그리고 이 모든 것을 합친 최소 구현까지 살펴봤다. 다음 섹션에서는 이 루프에 더 정교한 패턴들을 얹어, 단일 에이전트가 할 수 있는 것의 한계를 탐구한다.
퀴즈를 불러오는 중…
참고 자료
- Anthropic — Building Effective AI Agents — 접속 2026-06-30
- Anthropic — Writing effective tools for AI agents — 접속 2026-06-30