
제2장 자율형 AI 에이전트의 「사고·행동 루프(Agentic Loop)」
요약
자율형 AI 에이전트의 핵심인 '사고·행동 루프(Agentic Loop)'의 설계 패턴을 다룹니다. ReAct 패턴의 작동 원리부터 복잡한 목표 달성을 위한 Plan-Execute-Evaluate 사이클까지 구체적인 구현 방법과 구조를 설명합니다.
핵심 포인트
- ReAct 패턴: 사고(Thought), 행동(Action), 관찰(Observation)의 3단계 순환 구조
- ReAct의 한계: 복잡한 목표 수행 시 대국적 관점 상실 및 무한 루프 위험
- Plan-Execute-Evaluate: 목표를 서브 태스크로 분해하고 실행 및 평가하는 매크로 루프
- 에이전트 제어: 상태 전이 및 자기 수정(Self-Correction)을 통한 동적 워크플로우 구축
자율형 AI 에이전트의 핵심은 정적인 프롬프트 입출력(일문일답)이 아니라, 상황에 따라 동적으로 추론과 행동을 반복하는 **「루프(순환 구조)」**에 있습니다. LLM(대규모 언어 모델)에 자율적인 거동을 부여하기 위해서는 이 루프를 어떻게 설계하고 제어하느냐가 매우 중요합니다.
본 장에서는 에이전트를 자율적으로 구동하기 위한 대표적인 제어 패턴인 「ReAct」나 「Plan-Execute-Evaluate」, 에러를 스스로 해결하는 「자기 수정(Self-Correction)」, 무한 루프를 방지하는 「정지 조건」, 상태 전이(State Machine)를 통한 복잡한 루프 워크플로우 구축에 대해 구체적인 Python 코드와 Mermaid 다이어그램을 곁들여 체계적으로 해설합니다.
자율형 에이전트의 가장 기본적이면서도 강력한 루프 설계 패턴이 **ReAct(Reasoning and Acting)**입니다 (Yao et al., 2022).
기존의 프롬프트 기법에서는 LLM에게 「사고(Reasoning)」만을 수행하게 하거나 (Chain-of-Thought 등), 「행동(Acting: API 호출 등)」만을 수행하게 하는 방식 중 하나였습니다.
ReAct는 이들을 융합하여, **「사고(Thought) → 행동(Action) → 관찰(Observation)」**이라는 3단계 루프를 돌립니다.
Thought (사고): 현재 상황을 분석하고, 목표를 향해 다음에 무엇을 해야 할지 논리적으로 생각한다. -
Action (행동): 사고를 바탕으로 외부 도구(검색 API, 데이터베이스, 계산기 등)를 호출하기 위한 구체적인 쿼리를 생성한다. -
Observation (관찰): 도구의 실행 결과(외부로부터의 피드백)를 받아 이를 프롬프트에 추가하여 다음 루프로 연결한다.
LLM이 「사고」를 거침으로써 다음에 실행해야 할 도구의 선택 실수가 줄어들고, 나아가 「관찰」 결과를 바탕으로 자신의 추론을 동적으로 궤도 수정할 수 있게 됩니다.
프레임워크(LangChain이나 LangGraph 등) 내부에서 어떤 일이 일어나고 있는지 이해하기 위해, 외부 라이브러리를 사용하지 않고 순수한 Python으로 ReAct 루프를 구현해 보겠습니다.
여기서는 LLM API 호출을 모의(Mock) 함수로 유사하게 재현하여, 실제로 「Web 검색 도구」를 호출하며 답변을 도출하는 흐름을 보여줍니다.
import re
from typing import Callable, Dict
# =====================================================================
...
ReAct 패턴은 「한 걸음 나아가고 한 걸음 생각한다」는 국소적인 애드혹(Ad-hoc) 처리에는 적합하지만, 복잡하고 큰 목표(예: 「어떤 웹사이트의 모든 API 문서를 읽고, Python 모의 클라이언트 코드를 생성하며, 테스트를 실행하여 버그를 수정한다」)에 대해서는 대국적인 관점을 잃고 무한 루프에 빠지기 쉽다는 약점이 있습니다.
따라서 중요해지는 것이 보다 대국적인 매크로 루프인 Plan-Execute-Evaluate(플랜·실행·평가) 사이클입니다.
플래닝(Plan): - LLM에게 대목표를 부여하고, 이를 구체적인 의존 관계를 가진 「서브 태스크 리스트(유향 비순환 그래프: DAG)」로 분해하게 합니다.
-
플랜은 정적인 것이 아니라, 도중에 언제든 수정 가능한 구조(가변 배열이나 큐)로 만들어 둡니다.
실행(Execute): - 플랜에서 정의된 현재의 서브 태스크를 전용 에이전트나 모듈(Executor)에 할당하여 실행하게 합니다.
-
Executor는 앞서 언급한 「ReAct」 등의 마이크로 루프로 도구를 구사하여 결과물을 생성합니다.
자기 평가(Evaluate / Critique): - 생성된 결과물이 당초 정의된 서브 태스크의 「종료 요건(Definition of Done)」을 충족하는지 별도의 LLM(또는 규칙 기반 평가 로직)으로 테스트 및 평가합니다.
- 평가 결과가 NG라면 무엇이 부족한지를 피드백하여 플랜을 재구성(Re-Plan)합니다.
AI 에이전트 운용에서 가장 빈번하게 발생하는 트러블은 「도구 호출 에러(Tool-use error)」나 「LLM 출력 포맷 에러(JSON 파싱 에러 등)」입니다. 이러한 문제가 발생했을 때 시스템 전체를 크래시(Crash)시키는 것이 아니라, 에러 자체를 LLM에 대한 입력(Observation)으로 피드백하여 LLM 스스로 자기 수정(Self-correction)하게 만드는 것이 자기 복구 루프(Self-healing loop)입니다.
- LLM이 부적절한 JSON을 출력한다.
- 시스템 측에서 파싱을 시도하지만,
json.JSONDecodeError: Expecting ',' delimiter가 발생한다. - 예외(Exception)를 캐치하여, **"당신이 방금 출력한 텍스트는 JSON으로서 유효하지 않습니다. 에러 내용: [에러 내용]. 다음의 올바른 포맷으로 다시 한번 출력해 주세요"**라는 프롬프트를 작성하여 LLM에 다시 전달한다.
다음은 LLM이 지정된 스키마(Schema)에 부합하지 않는 부적절한 데이터를 출력했을 때, 에러 메시지를 피드백하여 재시도하게 만드는 구현 패턴입니다.
import json
from typing import Dict, Any
# 기대하는 스키마 정의
...
# (예시 코드의 일부)
# ...
# JSON 파싱
# ...
자율형 에이전트 개발에 있어 가장 실무적이고 예산 관리상 중요한 것이 바로 **「루프의 폭주(무한 루프) 방지」**입니다.
LLM의 사고가 같은 지점을 루프하기 시작하거나, 에러의 자기 복구가 무한히 반복되면, API 토큰 과금이 몇 분 만에 수만 엔으로 치솟는 등의 인시던트(Incident)가 발생할 수 있습니다.
- Tool-Use Loop: 도구 A를 호출한 결과(에러)에 대해, LLM이 "다시 동일한 파라미터로 도구 A를 호출"하는 동작을 반복함.
- Thought Loop (Hallucination Loop): "답변을 도출하기 위해 정보가 부족하다. 검색하자" → "정보를 찾을 수 없다" → "정보가 부족하다. 검색하자"와 같이 자기만족적인 탐색을 반복하는 루프.
- Format Loop: LLM이 몇 번이나 수정을 지시받아도, 특정 포맷 에러(예: JSON 파싱 미스)를 반복해서 출력함.
무한 루프를 방지하기 위해, 루프 제어 로직에는 반드시 다음과 같은 가드레일(Guardrail)을 구현해야 합니다.
| 정지 조건 | 개요 | 구현 접근 방식 |
|---|---|---|
| 1. 최대 반복 횟수 (Max Iterations) | 루프의 총 스텝(Step) 수를 제한한다. 가장 확실한 방벽. | 루프 카운터(Loop counter)를 통한 제어, 또는 LangGraph의 recursion_limit을 이용. |
| 2. 타임아웃 제한 (Timeout) | 실행 시간(초)을 제한한다. 외부 API의 행(Hang)이나 데드락(Deadlock) 대책. | 비동기 처리의 asyncio.wait_for 등을 사용하여 타임아웃 설정. |
| 3. 비용·토큰 제한 (Token Budget) | 1세션에서 소비 가능한 누적 토큰 수(또는 달러 환산 비용)에 상한을 둔다. | API 호출 시마다 사용 토큰 수를 추가·가산하여, 임계치를 초과하면 에러 종료. |
| 4. 중복 탐지 (Duplicate Detection) | 과거 N회의 「Action(도구 호출 인자)」 이력을 기록하고, 최근 이력 내에서 동일한 실행이 중복될 경우 루프를 탐지하여 강제 종료한다. | 과거의 Action 시그니처(도구 이름 + 인자)의 해시셋(Hash set)을 생성하여 탐지. |
# 4. 중복 탐지(Loop Detection)의 간이 로직 예시
class ActionHistoryTracker:
def __init__(self, window_size: int = 3):
...
단순한 스크립트라면 while 루프로 에이전트를 구축할 수 있지만, 프로덕션 환경의 애플리케이션(웹 서비스의 백엔드 등)에서는 단순한 루프 기술만으로는 한계가 있습니다.
- 비동기 및 상태 관리의 어려움: 에이전트 동작 중에 "사용자의 개입 (Human-in-the-Loop)"이 발생할 경우, 루프를 일시 중지하고 상태를 데이터베이스에 저장한 뒤 몇 시간 후에 재개해야 한다.
- 이력의 비대화 및 관리: 대화나 도구(Tool)의 입출력 이력이 복잡하게 분기되는 경우, 선형적인 루프 코드로는 관리가 불가능해진다.
- 병렬 처리 (Fork-Join) 제어: 여러 태스크를 병렬로 실행하고, 모두 완료되면 다음 단계로 진행하는 것과 같은 합류 제어가 어렵다.
이러한 문제들을 해결하는 것이 바로 **「유한 상태 기계 (Finite State Machine: FSM)」**를 통한 모델링입니다. 현재 널리 보급된 주요 에이전트 프레임워크(특히 LangGraph)는 이 스테이트 머신(State Machine) 모델을 기반으로 설계되어 있습니다.
- State (상태): 그래프 전체에서 공유되는 읽기/쓰기 가능한 키-값(Key-Value) 형태의 데이터 스토어 (스레드 세이프(Thread-safe)하게 관리됨).
- Nodes (노드): 각 단계의 구체적인 처리 (Python 함수). 상태(State)를 입력으로 받아, 업데이트된 상태(State의 일부)를 출력하여 갱신한다.
- Edges (에지): 노드 간의 전이 규칙. 일반적인 "다음 노드로 진행 (Normal Edge)"과, 현재 상태에 기반하여 다음에 진행할 노드를 결정하는 "조건부 분기 (Conditional Edge)" 두 종류가 있다.
다음은 LangGraph의 아키텍처 설계를 모사한, 상태 전이 기반의 심플한 ReAct 에이전트 구현 코드입니다. 실용적인 그래프 정의의 흐름을 이해할 수 있습니다.
from typing import TypedDict, Annotated, Sequence, Literal
import operator
# =====================================================================
...
[!NOTE]
위 코드의 AgentState에서 정의하고 있는 Annotated[Sequence[dict], operator.add]는, 실제 LangGraph에서 "노드가 반환한 새로운 메시지를 기존 메시지 리스트에 자동으로 병합(추가)하는" 특별한 어노테이션 (Reducer)입니다.
본 모의 구현 (SimpleGraphRunner)에서는 어노테이션을 해석하는 대신, 러너 내부에서 명시적으로 리스트를 병합 (list(state["messages"]) + list(node_output["messages"])) 함으로써 이 동작을 재현하고 있습니다.
이와 같이 루프를 "상태 (State)"와 "처리 (Nodes)"로 완전히 분리하여 모델링함으로써, 다음과 같은 실무 운영상의 이점을 얻을 수 있습니다.
- 각 노드가 독립적인 순수 함수(Pure Function)에 가까워지므로, 유닛 테스트 (Unit Test)가 매우 용이해진다.
- 에지(Edge)를 변경하는 것만으로, 프로그래밍 방식으로 "특정 조건에서 인간의 승인 (Approve)을 요청"하는 등의 루프 제어 확장이 가능하다.
- 스테이트(State)가 영속화 DB에 기록되어 있다면, 며칠에 걸친 장기 비동기 워크플로우도 안전하게 핸들링할 수 있게 된다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기