ReAct(Reasoning and Acting) 에이전트 구조는 대형 언어 모델(LLM)을 기반으로 도구 호출 기능을 통합한 지능형 에이전트 시스템을 구현하는 설계 방식이다. 사용자의 지시를 추론하고 필요한 경우 외부 도구를 호출하여 목표를 달성하는 능력을 지니며, 최근 AutoGPT, BabyAGI, Manus 등 다양한 오픈소스 프로젝트에서 채택되고 있다. 이 글에서는 ReAct 에이전트 프레임웍 내부 코드 구조를 직접 개발해본다. 아울러, ReAct 에이전트의 문제점들을 살펴보고 솔류션을 생각해본다.
ReAct 구조
ReAct 시퀀스 처리 구조는 LLM 기반 에이전트가 사고(Think), 행동(Act), 관찰(Observe), 결론(Final)의 단계로 사용자 질의에 응답하는 체계적 프로세스이다. 이 구조는 복잡한 문제 해결을 위해 LLM이 도구와 상호작용하면서 점진적으로 정답을 유도해내도록 설계되었다.
다음은 해당 코드에서 구현된 ReAct 시퀀스의 흐름이다.
-
Think
LLM은 질문에 대한 분석을 수행하며 필요한 경우 어떤 도구를 사용할지를 판단한다. 이 단계는 주로 텍스트 상의 추론 내용으로 표현되며, 도구 실행 없이 논리 전개만 이루어진다. -
Act
필요하다고 판단되면Act: ToolName[Input]
또는Act: ToolName(Input)
형식으로 도구 호출을 지시한다. 이 지시문은 정규표현식으로 감지되어 실제 도구 실행이 트리거된다. -
Observe
호출된 도구가 실행되고, 그 결과는Observe:
접두사를 붙여 기록된다. 이 결과는 이후 컨텍스트에 포함되어 다음 LLM 호출의 입력으로 사용된다. -
Final
충분한 정보가 수집되면Final Answer:
형식으로 최종 응답을 제공한다. 이는 루프를 종료시키는 조건이기도 하며, 에이전트의 실행 결과로 사용자에게 출력된다.
이러한 시퀀스를 통해 LLM은 단순히 한 번에 답변하는 것이 아니라, 도구를 여러 번 사용하고 그 결과를 반영하며 점진적으로 정확한 응답에 도달한다.
ReAct 에이전트 내부 코드 구현해보기
앞서 언급된 개념을 고려해 ReAct 에이전트의 핵심적인 코드만 구현해 본다. 다음과 같이 관련 라이브러리를 설치한다.
pip install langchain
다음과 같이 코드를 구현한다.
이 코드는 ReAct 시퀀스를 중심으로 구성되어 있다. ReAct란 LLM이 단순한 질의응답을 넘어서서, 생각하고 도구를 사용하고 그 결과를 바탕으로 다시 추론한 뒤 최종적인 답을 내리는 일련의 추론 패턴이다. 이 흐름은 크게 네 단계로 나뉜다.
첫째, Think 단계에서는 주어진 질문에 대해 어떻게 접근할지를 LLM이 서술적으로 사고한다. 둘째, Act 단계에서는 필요하다고 판단한 도구를 지정된 형식으로 호출한다. 이때 Act: ToolName[Input] 형식을 사용한다. 셋째, Observe 단계에서는 실행된 도구의 결과를 받아 LLM의 다음 추론에 포함될 수 있도록 기록한다. 넷째, Final 단계에서는 Final Answer: 답변 형식으로 최종 결론을 도출한다.
이러한 시퀀스를 실제로 구현하기 위해 코드에서는 특정 정규표현식을 사용하여 각 단계를 감지하고 분기 처리한다. 예를 들어, Act 단계는 Act:라는 키워드를 통해 감지되고, 대응되는 도구 이름과 입력값이 추출된다. 대응되는 도구가 존재하면 해당 함수나 객체의 run 메서드를 호출하여 결과를 얻고, 이를 Observe로 기록하여 다음 프롬프트 생성 시 문맥으로 전달한다. Final Answer가 감지되면 그 값을 최종 출력으로 반환하면서 루프를 종료한다.
코드는 먼저 환경변수를 불러오고, 수학 계산용 calculator 함수와 웹 검색용 Tavily 도구를 정의하고 tools 목록에 등록한다. 각 도구는 Tool 객체로 구성되며, name, func, description 속성을 갖는다. 이 도구들의 인터페이스 정보는 get_tools_info 함수를 통해 LLM 프롬프트에 전달할 수 있는 형식으로 변환된다.
ReActAgent 클래스는 핵심 로직을 담당한다. 생성자에서는 도구를 이름으로 접근 가능하도록 딕셔너리 형태로 구성하며, 프롬프트 템플릿에 포함할 도구 설명을 자동으로 생성한다. run 메서드는 LLM과의 대화를 이끄는 루프이다. 사용자의 입력 쿼리를 바탕으로 Think부터 Final 단계까지의 응답을 순차적으로 생성하고, 도구 호출이 필요한 경우 자동으로 감지하여 실행한다.
LLM은 ChatOpenAI 클래스를 통해 초기화되며, 주 모델과 검증용 모델이 따로 구성된다. 현재 코드는 검증 부분이 주석 처리되어 있어 정답 검증은 수행되지 않지만 validate_answer 메서드는 존재한다. 해당 메서드는 question과 answer를 받아 프롬프트에 삽입하고 gpt-4-turbo 모델로부터 응답을 받아 yes 또는 no 여부와 사유를 반환한다.
main 함수는 인터랙티브한 사용자 입력을 받아 ReActAgent를 호출하는 실행 루틴이다. 사용자가 exit이라고 입력할 때까지 질의를 받고 응답을 출력하는 루프를 구성한다. 이로써 ReActAgent는 반복적인 질의응답 상황에서도 도구를 활용하여 논리적 사고 과정을 수행하고, 외부 정보에 기반한 정답을 도출할 수 있도록 설계되어 있다.
주요 문제 유형
ReAct 기반 에이전트는 구현 및 실전 적용 과정에서 여러 구조적 문제와 비효율이 발생한다. 이러한 문제 사례를 분석하고, 실제 커뮤니티에서 공유된 해결 전략과 함께 개선 방향을 확인해 본다.
추론 오류
에이전트가 잘못된 전제나 문맥에 기반하여 부적절한 도구를 선택하거나 오답을 도출하는 문제가 빈번히 발생한다. 이는 LLM의 한계와 프롬프트 설계 미비, 또는 이전 상태 정보의 왜곡에 기인한다.
반복 및 루프 문제
에이전트가 목표에 도달하지 못하고 동일한 행동을 반복 수행하는 루프에 빠지는 문제가 자주 발생한다. 이는 max_steps 제한이 없거나, 실패 판단 기준이 모호한 경우에 두드러진다.
결과 검증 실패
에이전트가 도출한 결과가 부정확하거나 무의미함에도 불구하고, 이를 최종 결과로 판단하고 종료되는 문제가 있다. 이는 정답 여부를 평가할 수 있는 검증 메커니즘의 부재 혹은 Verifier LLM의 오판 때문이다.
지연 및 비용 문제
ReAct 구조는 추론 Think, 행동 Act, 검증 Verify 단계에서 각각 LLM 호출이 필요하므로 응답 지연이 누적되며, 고성능 모델 사용 시 비용 또한 급증하게 된다.
상태 관리 실패
에이전트가 이전 문맥을 적절히 유지하지 못하거나, 기억을 잘못 참조하여 추론에 실패하는 경우가 있다. 이는 memory overflow, context 길이 초과 등으로 인해 발생한다.
해외 커뮤니티 보고 사례
Reddit, GitHub, Hacker News 등 해외 커뮤니티에서는 위 문제들이 반복적으로 보고되고 있다. 주요 사례는 다음과 같다
r/React 에서는 Langchain의 늦은 React 에이전트 성능에 대한 문제가 제기된 적이 있다. 이런 사례는 수도 없이 많다.
r/AutoGPT에서는 도구 호출 반복, final answer 오류 등 루프와 검증 문제를 다룬 글들이 다수 존재한다.
r/MachineLearning에서는 hallucination 문제, tool selection 오류에 대한 토론이 있다
GitHub Issues의 OpenDevin, Voyager 등의 리포지터리에서도 루프, 상태 관리 문제, 도구 실행 실패가 자주 보고되고 있다
이는 ReAct 구조의 본질적인 설계 한계와 관련되며, 이를 보완하기 위한 다양한 시도들이 커뮤니티에서 논의되고 있다
해결 방안
Verifier LLM의 도입
추론 결과에 대해 별도의 고성능 LLM을 사용하여 정답 여부를 판단하도록 구성하는 방식이다 예를 들어 GPT 35 기반 ReAct 에이전트를 GPT 4 기반 Verifier로 보완하는 사례가 있다
Confidence 기반 조건 실행
LLM이 응답에 대한 신뢰도 confidence score를 함께 출력하도록 하고, 일정 기준 미만일 경우에만 검증자 호출 또는 재시도를 수행하는 방식이다
행동 다양성 유도 및 페널티 적용
같은 도구를 반복 호출하지 않도록 행동 선택 시 다양성을 보장하거나 페널티 기반 scoring 방식을 적용한다
결과 품질 기반 Soft Fallback 적용
결과가 일정 기준 미달일 경우 완료 실패 메시지를 출력하거나, 대체 응답을 제공하는 방식으로 흐름을 마무리한다
도메인 기반 grounding 기법
도메인 특화 지식을 embedding하거나 retriever를 통해 grounding context를 제공함으로써 hallucination을 감소시키는 방식이다
LLM 처리 지연 보완을 위한 전략
ReAct 구조에서는 LLM을 여러 번 호출하는 구조로 인해 응답 시간이 증가하는 문제가 발생한다. 이를 해결하기 위한 보완 전략은 다음과 같다
첫째, 동일한 LLM을 사용하는 것이 아니라 추론 단계는 경량 LLM을 사용하고 검증 단계는 고성능 LLM을 사용하는 방식으로 역할 분리를 통해 효율을 높이는 방안이 있다
둘째, LLM의 confidence score를 이용하여 신뢰도가 충분히 높은 경우에는 검증 단계를 생략함으로써 전체 호출 횟수를 줄일 수 있다
셋째, 여러 행동 중 하나를 선택할 때 prefetch 또는 batch decoding 방식을 사용하여 예측을 병렬적으로 수행하면 전체 응답 지연을 줄일 수 있다
넷째, 반복되는 도구 호출에 대해서는 캐싱 메커니즘을 도입하거나 결과를 저장하여 재활용함으로써 불필요한 호출을 방지할 수 있다
다섯째, 검증자 역할을 하는 LLM을 완전한 LLM이 아니라 룰 기반 평가기 또는 소형 모델로 대체하여 빠르게 판단하게 하는 방식도 고려할 수 있다
코드 기반 구현 예시 요약
ReAct 기반 구조는 보통 다음 흐름으로 구성된다
- 사용자 입력 수신
- LLM을 통해 행동 계획 수립 Think
- 해당 도구 실행 Act
- 결과 수신 후 Verifier를 통해 정답 여부 판단 Verify
- 정답 시 종료, 실패 시 재시도 또는 fallback 처리
이러한 구조는 Python 코드로 구현할 수 있으며, 각 함수는 다음과 같은 역할을 수행한다
- think goal 행동 계획 생성
- act action 도구 실행 및 결과 수신
- verify result, goal 결과 검증
- run goal 전체 흐름 통제 및 반복 로직 포함
결론
ReAct 기반 에이전트는 고도의 자율성을 지닌 시스템이나, 구조적으로 여러 한계가 존재한다. 특히 결과 검증 실패, 반복 루프, hallucination, 비용 증가 문제는 실제 운영 및 서비스화에 있어 큰 장애 요인이 된다. 이를 해결하기 위해서는 검증자 LLM 도입, confidence 기반 흐름 제어, 도구 호출의 다양성 보장, grounding 및 fallback 전략이 함께 설계될 수 있다.
부록: 보고서 형식 AI 에이전트 패턴
ReAct 에이전트는 Reasoning and Acting의 약어로, LLM이 툴과 상호작용하며 문제를 해결하는 패턴이다. 이 구조는 인간처럼 사고하고, 필요한 정보를 외부 도구에서 검색하거나 계산을 수행한 뒤, 최종적으로 응답을 생성한다. 이로 인해 복잡한 질의나 멀티스텝 추론이 필요한 경우에 적합한 패턴으로 여겨진다. 그러나 이러한 장점에도 불구하고, LLM이 여러 차례 호출되고 툴도 반복적으로 작동하므로 응답 시간이 느려지고, API 사용량 증가로 인해 비용이 상승하는 단점이 존재한다.
한편 단순히 보고서를 생성하는 용도로 LLM을 사용하는 경우라면, 복잡한 Reasoning이나 여러 번의 외부 툴 호출은 필요하지 않다. 이때는 ReAct 에이전트를 사용하는 것이 오히려 비효율적이며, 불필요한 구조적 복잡성과 지연을 유발한다. 따라서 보고서 자동 생성이라는 목적에 최적화된 에이전트 구조는 단일 호출 기반 구조가 적합하다.
이 구조는 Single-Shot LLM Invocation 패턴으로 명명된다. 이 방식은 LLM을 한 번만 호출하여 입력된 요구 사항에 맞는 보고서를 즉시 생성한다. 별도의 도구 호출이나 intermediate step 없이 하나의 잘 설계된 프롬프트를 중심으로 전체 작업이 이루어진다. 이 패턴은 속도가 빠르고, 처리 흐름이 단순하며, LLM API 호출 횟수를 최소화하므로 비용이 낮다. 특히 단일 보고서, 요약, 이메일 초안, 블로그 콘텐츠와 같은 목적에 적합하며, 사용자의 입력 내용에 따라 문서 포맷이나 톤을 조정하는 것만으로 충분한 성능을 발휘한다.
단일 호출 구조는 별도의 외부 툴이나 체인을 필요로 하지 않기 때문에 LangChain이나 ReAct 구조 없이도 Python 코드 수준에서 간단히 구현이 가능하다. 예를 들어, 사용자가 요구사항을 입력하면 그에 맞는 템플릿 기반 프롬프트를 작성하고, 이를 LLM에 전달해 한 번에 결과를 반환받는 방식이다. 이러한 프롬프트에 보고서의 형식, 어조, 길이 등을 명시하면, 결과물의 일관성과 품질도 충분히 확보할 수 있다.
이 외에도 필요에 따라 Plan-and-Write 패턴을 고려할 수 있다. 이 패턴은 먼저 전체 문서의 개요를 작성한 뒤, 각 항목에 따라 내용을 생성하는 방식이다. 좀 더 명확한 구성과 섹션 구분이 필요할 때 유용하다. 통계 수치나 외부 데이터를 포함해야 할 경우에는 Toolformer와 같이 선택적으로 툴을 호출하는 패턴을 도입할 수 있다. 마지막으로 문서가 일정한 형식을 유지해야 하거나 후처리를 자동화해야 할 때는 Structured Prompting 패턴을 사용할 수 있으며, JSON 또는 Markdown 형식으로 문서 구조를 고정하여 일관성을 높인다.
결론적으로, 보고서 자동 생성처럼 복잡한 추론이나 외부 툴 연동이 불필요한 작업에는 ReAct와 같은 다단계 에이전트 구조는 적합하지 않다. 대신 단일 LLM 호출로 작업을 완료하는 Single-Shot 방식이 가장 효율적이며, 프롬프트 최적화만으로 높은 품질의 결과를 얻을 수 있다.
부록: AI 에이전트 오케스트레이션
사용자 질문에 따라 적절한 에이전트 패턴을 선택하기 위해서는 입력의 목적과 복잡성을 빠르게 분류할 수 있는 에이전트 오케스트레이션 메커니즘이 필요하다. 다음 의사코드는 솔류션을 보여준다.
여기서는 classify_input 함수가 사용되며, 이 함수는 사용자의 입력 문장을 분석하여 어떤 처리 패턴이 적합한지를 판단하는 역할을 수행한다. 하지만 이 함수 자체가 느리거나 LLM 호출을 과도하게 유발하면 전체 시스템의 응답성이 떨어지므로, 이를 빠르게 실행할 수 있는 설계가 중요하다.
classify_input을 빠르게 실행하기 위한 방법은 크게 세 가지로 구분된다. 첫 번째는 경량 LLM을 사용하는 방식이다. 이 방법은 GPT-3.5 Turbo, Claude Instant, DistilBERT 등의 속도가 빠른 경량 언어 모델을 활용하여 입력을 분류하는 방식이다. 프롬프트는 미리 정의된 범주 중에서 입력이 어떤 태스크 유형에 해당하는지를 선택하게 하며, 이에 따라 적절한 패턴으로 연결된다. 이 방식은 유연하고 적응력이 높지만, 여전히 LLM 호출이기 때문에 응답 속도에 영향을 줄 수 있다. 그럼에도 불구하고 ReAct 등 복잡한 구조보다는 훨씬 빠르고 저비용이다.
두 번째 방식은 규칙 기반 분류 방식이다. 이는 키워드 기반으로 입력 문장을 빠르게 분류하는 방법으로, 예를 들어 ‘보고서’, ‘작성해’ 등의 단어가 포함되어 있으면 단순 생성 태스크로 간주하고, ‘비교’, ‘계산’, ‘표로’ 등의 단어가 있으면 툴 기반 또는 구조화된 출력을 요구하는 태스크로 분류하는 것이다. 이 방식은 매우 빠르며, 수 마이크로초 이내에 실행이 가능하고, 비용도 발생하지 않는다. 그러나 복잡한 문장이나 여러 의미가 섞인 문장에 대해서는 정확하게 분류하지 못할 위험이 있다.
세 번째는 하이브리드 방식이다. 이 방식은 앞서 언급한 규칙 기반 분류기를 우선 사용하고, 그 결과가 불확실하거나 unknown일 경우에만 경량 LLM을 호출하여 보완 분류를 수행한다. 이 구조는 속도와 정확도 사이의 균형을 잡기 위해 매우 현실적인 대안이며, 실제로 OpenAI API 기반 에이전트나 LangChain의 라우팅 모듈에서도 유사한 방식이 채택된다. 구현은 상대적으로 복잡할 수 있으나, 전체 시스템의 응답성과 품질을 함께 유지할 수 있는 방법이다.
결론적으로 classify_input 함수를 빠르게 실행하기 위해서는 입력의 유형과 빈도에 따라 최적화 전략을 선택해야 한다. 단순한 텍스트 입력이 반복되는 시스템에서는 규칙 기반 분류기로 충분하며, 복합 입력이 자주 등장하는 환경에서는 하이브리드 분류기를 설계하는 것이 이상적이다.