LangChain이나 CrewAI 같은 고수준 프레임워크 덕분에 고급 AI 에이전트를 구축하는 일이 훨씬 쉬워졌습니다.
하지만 이런 프레임워크는 종종 AI 에이전트가 작동하는 근본 원리를 감춰버립니다.
구체적으로 말하면, 이러한 프레임워크들은 내부적으로 리액트(ReAct, Reasoning and Acting) 패턴을 사용합니다. 이 패턴은 대형 언어 모델(LLM)이 문제를 단계적으로 사고하고, 도구를 활용해 실제 행동을 수행할 수 있게 해줍니다.
예를 들어, CrewAI의 에이전트는 일반적으로 리액트 패러다임을 따르면서 작업에 대한 추론과 행동(도구 사용)을 번갈아 수행하여 정보를 수집하거나 단계를 실행합니다.
이러한 접근 방식은 사고 연쇄(chain-of-thought) 추론과 외부 도구 사용을 결합함으로써, LLM 에이전트가 복잡한 작업과 의사결정을 처리하는 능력을 크게 향상시킵니다.
CrewAI 같은 프레임워크는 이러한 로직을 대신 구현해주며, 프로덕션 시스템에서도 계속 사용해야 합니다(전문가들이 만든 것이기 때문입니다). 하지만 패키지화된 라이브러리의 내부 작동 원리를 이해하지 못한 채 사용하면 혼란을 겪을 수 있습니다.
에이전트 프레임워크를 사용하다 보면, “질문에 답하는 과정에서 웹 검색이나 계산을 어떻게 수행하기로 결정하는 걸까?” 하는 의문이 들 수 있습니다.
아래 예시는 에이전트가 응답을 생성하기 전에 일련의 사고 활동을 거치는 과정을 보여줍니다.
이 글에서는 Python과 LLM만을 사용하여 리액트 에이전트를 처음부터 직접 구축하면서 그 과정의 비밀을 밝혀보겠습니다.
이렇게 직접 구축하면 에이전트의 동작을 완전히 제어할 수 있어, 최적화와 문제 해결이 훨씬 쉬워집니다.
이 글에서는 OpenAI를 사용하지만, Ollama 같은 오픈소스 도구로 LLM을 로컬에서 실행하고 Llama3 같은 모델로 에이전트를 구동하고 싶다면 그렇게 해도 됩니다.
진행 과정에서 리액트 패턴을 설명하고, 추론과 도구 사용을 교차하는 에이전트 루프를 설계하며, 에이전트가 호출할 수 있는 여러 도구를 구현할 것입니다.
목표는 리액트 에이전트의 이론과 구현 모두를 이해하도록 돕는 것입니다.
이 글을 마치면, 여러분은 작동하는 에이전트를 갖게 되고 CrewAI 같은 프레임워크가 내부적으로 리액트를 어떻게 활용하는지 명확히 이해하게 될 것입니다.
그럼 시작해볼까요!
리액트 패턴이란 무엇인가?
리액트(ReAct, Reasoning and Acting의 약자)는 AI 에이전트 설계 패러다임으로, 에이전트가 사고 연쇄 추론과 도구 사용 행동을 종합적으로 활용합니다.
리액트 에이전트는 한 번에 직접적인 답변을 생성하는 대신, 단계별로 사고하고 최종 답변을 내놓기 전에 중간 행동(정보 검색이나 값 계산 같은)을 수행할 수 있습니다.
이것을 명확히 이해하기 위해, 리액트 패턴이 어떻게 작동하는지 생각해봅시다.
예시 1
형식적으로 말하면, 리액트를 따르는 LLM은 추론 추적(즉, “사고”)과 작업별 행동(도구 호출)을 교차하여 생성합니다. 즉, 모델의 출력은 다음과 같은 형태입니다:
사고(Thought): 합계를 계산해야겠어.
행동(Action): Calculator("123 + 456")
관찰(Observation): 579
사고: 이제 합이 나왔으니, 다음은 곱셈을 해야겠어.
행동: Calculator("579 * 789")
관찰: 456,831.
사고: 최종 결과를 얻었어.
최종 답변(Final Answer): 456,831.
추론 추적(사고의 연쇄)은 모델이 다음에 무엇을 할지 계획하고 추적하는 데 도움을 주고, 행동은 모델이 외부 소스에 문의하거나 계산을 수행하여 원래는 접근할 수 없었던 정보를 수집할 수 있게 해줍니다.
실제로 모델은 내부 지식에만 국한되지 않습니다. 필요에 따라 도구, 데이터베이스, 인터넷에 접근하고 그 결과에 대해 추론할 수 있습니다.
이는 에이전트가 할 수 있는 일을 크게 확장시킵니다.
IBM은 리액트 에이전트를 LLM “두뇌”를 사용하여 추론과 행동을 조율하고, 구조화되면서도 적응 가능한 방식으로 환경과 상호작용할 수 있는 에이전트로 설명합니다.
정적 지식으로만 답변하는 기본 챗봇과 달리, 리액트 에이전트는 생각하고, 검색하고, 계산한 다음 그 결과들을 하나의 답변으로 결합할 수 있습니다.
예시 2
다중 에이전트 시스템에서 제가 구축한 에이전트의 출력을 살펴봅시다(코드는 곧 살펴보겠습니다):
위 예시에서는 리액트 패러다임을 사용하여 작업을 실행하는 AI 뉴스 리포터 에이전트의 실시간 추적을 볼 수 있습니다. 이 에이전트는 “Agent2Agent Protocol”과 관련된 뉴스 헤드라인을 작성하라는 요청을 받았습니다. 하지만 성급하게 결론을 내리는 대신, 구조화된 추적에서 볼 수 있듯이 단계별로 추론합니다.
이것을 분석해봅시다:
에이전트 역할: 뉴스 리포터 – 이 에이전트는 뉴스 콘텐츠를 선별하고 검증하는 데 특화되어 있습니다.
작업: 에이전트는 뉴스 헤드라인을 생성하고 Agent2Agent Protocol과 관련이 있는지 확인하라는 지시를 받았습니다.
사고: 에이전트는 먼저 프로토콜에 대한 최근 업데이트를 빠르게 검색하여 정보를 검증해야 한다고 내부적으로 추론합니다. 이것이 리액트 사이클의 추론 부분입니다.
행동: 에이전트는 Search the internet이라는 도구를 사용하여, "Agent2Agent Protocol news November 2023"라는 쿼리가 담긴 구조화된 JSON 입력을 전달합니다. 이것이 에이전트가 외부 도구를 활용하여 실제 데이터를 수집하는 행동 단계입니다.
도구 출력: 검색 도구가 검색한 결과(뉴스 스니펫, 기사 요약 또는 관련 URL 등)가 포함되어 있습니다.
이것은 구조화된 사고와 외부 행동을 결합하는 힘을 보여줍니다. 모델의 내부 지식에만 의존하는 대신, 에이전트는 도구를 사용하여 사실을 교차 검증합니다.
이는 리액트 패턴이 에이전트 행동의 투명성, 정확성, 검증 가능성을 어떻게 장려하는지 보여주는 예시입니다. 이는 실제 정보를 종합해야 하는 모든 시스템에 필수적인 기능입니다.
이것을 다중 에이전트 설정으로 더 확장할 수도 있습니다. 뉴스 수집기(News Collector)가 원시 피드를 수집하고, 사실 검증기(Fact Verifier)가 신뢰성을 확인하고, 이 뉴스 리포터가 헤드라인을 작성하는 식으로 모두 추론과 도구 기반 행동을 사용하여 비동기적으로 조율됩니다.
위와 동일한 출력을 재현하고 싶다면 다음 구현을 참고하세요.
Tip
.env 파일을 생성하고 OPENAI_API_KEY를 지정했는지 확인하세요. 그러면 작업이 훨씬 쉽고 빨라집니다. 또한 Jupyter Notebook 환경에서 비동기 작업을 처리하기 위해 다음 두 줄의 코드를 추가하면 Crew Agent에 비동기 호출을 원활하게 수행할 수 있습니다.
import nest_asyncionest_asyncio.apply()
Tip
OpenAI를 사용하지 않고 오픈소스 LLM을 고수하고 싶다면, Ollama를 사용할 수도 있습니다.
from crewai import LLMllm = LLM( model="ollama/llama3.2:1b", base_url="http://localhost:11434")
먼저 crewai에서 필수 클래스와 유용한 도구인 SerperDevTool을 import합니다. 이 도구는 (serper.dev를 통한) 실시간 웹 검색 기능을 래핑하여 에이전트가 인터넷에서 실시간 정보를 가져올 수 있게 해줍니다.
from crewai import Agent, Task, Crew, Processfrom crewai_tools import SerperDevTool
또한 serper.dev에서 Serper Dev API 키를 받아 앞서 생성한 .env 파일에 저장하세요:
SERPER_API_KEY="..."
OPENAI_API_KEY="sk-...."
다음으로, 에이전트가 웹 결과가 필요할 때 호출할 수 있는 웹 검색 도구를 초기화합니다:
news_search_tool = SerperDevTool()
이어서 첫 번째 에이전트인 뉴스 수집기를 정의합니다:
latest_news_agent = Agent( role="뉴스 수집가", goal="주어진 주제에 대한 최신 뉴스를 수집하고 인사이트를 모읍니다", backstory="""당신은 주어진 주제에 대한 최신 뉴스를 수집하는 책임을 맡은 리포터입니다""", verbose=True,)news_search_task = Task( description="{topic}에 대한 최신 뉴스를 검색하세요", expected_output="주어진 주제에 대한 최신 뉴스 목록", tool=news_search_tool, agent=latest_news_agent)
이 에이전트는 디지털 저널리스트처럼 행동하도록 설계되었습니다. Serper 도구를 사용하여 주어진 주제와 관련된 뉴스 기사를 수집하는 것이 이 에이전트의 책임입니다. verbose=True 플래그는 상세한 로깅을 보장하며, 이것이 앞서 보여준 투명한 리액트 스타일 추적을 생성합니다.
또한 작업은 News Collector가 지정된 {topic}에 대한 최신 정보를 적극적으로 검색하도록 지시합니다. tool 매개변수는 작업을 SerperDevTool과 연결하여, 환각(hallucination) 대신 실제로 검색을 수행할 수 있게 합니다.
다음으로 두 번째 에이전트인 뉴스 리포터를 정의합니다:
news_reporter_agent = Agent( role="뉴스 리포터", goal="""뉴스 수집기로부터 최신 뉴스를 사용하여 뉴스 헤드라인을 작성합니다.""", backstory="""당신은 주어진 주제에 대한 최신 뉴스를 활용하여 헤드라인을 작성하는 리포터입니다.""", verbose=True,)news_headline_task = Task( description="""뉴스 수집기로부터 {topic}에 대한 뉴스 헤드라인을 작성하세요. 또한 웹 검색 도구를 사용하여 해당 뉴스가 {topic}과 관련이 있는지 검증하세요.""", expected_output="주어진 주제에 대한 뉴스 헤드라인", tools=[news_search_tool], agent=news_reporter_agent)
이 에이전트는 헤드라인 작성자입니다. 이전 작업의 출력을 받아 간결한 헤드라인을 작성합니다. 수집기처럼 이 에이전트도 verbose입니다. 즉, 로그에서 추론 단계, 도구 호출, 의사결정을 볼 수 있습니다.
이 에이전트의 작업은 특히 흥미로운데, 리포터 에이전트에게 두 가지를 하도록 요구하기 때문입니다:
이전 출력(수집된 뉴스 기사)을 사용합니다.
검색 도구를 다시 사용하여 자체 검증을 수행합니다. 즉, 뉴스가 관련성이 있는지 재확인합니다.
이것은 다음과 같은 상세한 출력을 생성하며, 에이전트가 자연어로 어떻게 “생각”하고, 다음 단계를 계획하고, 외부 도구를 사용하여 행동을 수행하는지 보여줍니다.
하지만 이게 전부가 아닙니다. 이 상세한 출력의 끝부분에서 한 가지를 더 살펴봅시다:
내부적으로 이러한 리액트 스타일의 동작은 매우 구체적인 형식 템플릿, 즉 우리가 행동 프로토콜(action protocol)이라고 부르는 것에 의해 제어됩니다. SerperDevTool 같은 도구를 사용할 때, CrewAI는 LLM에게 엄격한 응답 스키마를 따르도록 지시합니다. 이것은 에이전트가 안전하고, 결정론적이며, 해석 가능한 방식으로 도구와 상호작용하도록 보장하는 데 도움이 됩니다.
먼저 에이전트 도구 프롬프트 형식을 살펴봅시다:
"""당신은 다음 도구에만 접근할 수 있으며,여기에 나열되지 않은 도구를 만들어서는 절대 안 됩니다:Tool Name: Search the internetTool Arguments: {'search_query': {'description': '인터넷 검색에 사용할 필수 검색 쿼리', 'type': 'str'}}Tool Description: search_query로 인터넷을 검색하는 데 사용할 수 있는 도구."""
이것은 LLM 프롬프트에 주입되는 도구 컨텍스트의 일부입니다. 이것은 에이전트에게 다음을 알려줍니다:
어떤 도구를 사용할 수 있는지.
어떤 인자가 필요한지.
도구를 만들어내거나 프로토콜을 벗어나서는 안 된다는 것.
이것은 에이전트 동작에 강력한 제약을 만듭니다. 환각이나 기능 오용을 피하고 싶을 때 중요합니다.
프롬프트에는 다음과 같은 중요한 지시사항도 포함됩니다:
"""중요: 응답에 다음 형식을 사용하세요:```Thought: 무엇을 해야 할지 항상 생각하세요Action: 수행할 행동, [Search the internet] 중 하나의 이름만,작성된 그대로의 이름.Action Input: 행동에 대한 입력, 중괄호로 묶인 간단한 JSON 객체,키와 값을 감싸는 데 "를 사용.Observation: 행동의 결과```"""
이것은 추론 + 행동 루프를 문자 그대로 설명한 것입니다:
Thought: 에이전트가 내부 추론을 표현합니다.
Action: 에이전트가 사용할 도구를 선택합니다—정확히 그대로.
Action Input: 엄격한 JSON으로 형식화된 도구에 대한 인자들.
Observation: 도구가 반환한 것(즉, 원시 출력).
이 스키마는 도구를 안정적으로 실행하고, 일관되게 로깅하고, 명확하게 추적할 수 있도록 보장합니다. 그리고 결정적으로—LLM 친화적입니다. 모든 것이 모델이 이해하고 응답하는 자연어로 작성되어 있습니다.
모든 정보가 수집되면, 에이전트는 다음과 같이 결론을 내리라는 지시를 받습니다:
"""필요한 모든 정보가 수집되면,다음 형식을 반환하세요:```Thought: 이제 최종 답변을 알게 되었습니다Final Answer: 원래 입력 질문에 대한최종 답변```"""
이것은 추론 체인의 종료를 신호합니다. 이 시점에서 에이전트는 조사를 완료했으며 확신 있는 최상위 답변을 생성할 수 있습니다.
이것이 CrewAI를 사용하여 매끄럽게 구현되는 리액트의 본질입니다.
그렇다면 왜 이것이 중요한가?
리액트는 LLM 기반 에이전트를 더 신뢰할 수 있고 강력하게 만드는 중요한 진전으로 소개되었습니다.
모델이 사고 과정을 설명하고 도구를 통해 사실을 확인하게 함으로써, 환각과 오류 전파 같은 문제를 줄일 수 있습니다.
Yao 등의 원래 리액트 연구는 이 접근 방식이 모델이 실제 정보를 검색하여(예: Wikipedia 쿼리) 사실을 검증할 수 있게 함으로써 질문 답변에서 환각을 극복할 수 있음을 보여주었습니다.
또한 사고 연쇄를 검사하여 디버깅하거나 신뢰성을 확인할 수 있기 때문에, 에이전트의 의사결정 투명성도 향상됩니다.
전반적으로 리액트 패턴은 수동적인 LLM을 복잡한 작업을 분해하고 외부 데이터 소스와 상호작용할 수 있는 능동적인 문제 해결사로 전환시킵니다. 마치 자율적인 비서처럼 말이죠.
이것은 거의 모든 에이전틱 프레임워크에서 리액트가 널리 사용되는 이유를 설명합니다. 실제 구현은 다를 수 있지만, 모든 것이 리액트 패턴에서 파생된 무언가로 연결됩니다.
다시 말해, 이 형식은:
LLM이 단계별로 작동하도록 강제합니다,
사고와 행동을 명확히 분리합니다,
도구에 대한 결정론적 입출력 동작을 보장합니다,
그리고 검사하거나 디버그할 수 있는 추적 가능한 추론 체인을 생성합니다.
추론 + 행동: 리액트 에이전트가 작동하는 방식
리액트 에이전트는 사고(Thought) → 행동(Action) → 관찰(Observation)의 루프로 작동하며, 해결책이나 최종 답변에 도달할 때까지 이를 반복합니다.
이것은 인간이 문제를 해결하는 방식과 유사합니다:
무엇을 할지 생각하고
행동을 수행하고(무언가를 찾아보거나 계산을 하는 것처럼),
결과를 관찰하고
그런 다음 그것을 다음 생각에 통합합니다.
리액트 프레임워크는 프롬프트 엔지니어링을 사용하여 이러한 구조화된 접근 방식을 강제하고, 모델의 사고와 행동/관찰을 교대로 수행합니다.
다음은 AI 에이전트에서 리액트 사이클의 단계별 분석입니다:
사고(Thought): 에이전트(LLM 기반)는 사용자의 쿼리와 내부 컨텍스트를 분석하고, 자연어로 추론 단계를 생성합니다. 이것은 일반적으로 최종 사용자에게 표시되지 않지만 에이전트의 자기 대화의 일부입니다. 예를 들어: “이 질문은 한 국가의 인구를 묻고 있어. 최신 수치를 찾기 위해 웹 검색을 사용해야겠어.”
행동(Action): 사고를 바탕으로 에이전트는 수행할 외부 도구나 작업을 결정합니다. 행동을 나타내는 규정된 형식을 출력합니다. 예를 들어: Action: WebSearch("population of Canada 2023"). 에이전트는 본질적으로 함수(도구)를 이름으로 “호출”하며, 종종 일부 입력 매개변수와 함께 호출합니다.
관찰(Observation): 에이전트의 환경(우리 코드)이 요청된 행동을 실행하고 결과(관찰)를 에이전트에게 반환합니다. 예를 들어, 웹 검색 도구는 다음을 반환할 수 있습니다: “Observation: 2023년 캐나다의 인구는 3,800만 명입니다.” 이 관찰은 에이전트의 컨텍스트에 제공됩니다.
에이전트는 새로운 정보를 가지고 1단계(또 다른 사고)로 돌아갑니다. 새로운 데이터로 추론합니다. 우리 예시에서는 다음과 같이 생각할 수 있습니다: “이제 인구 수치를 얻었어. 질문에 답할 수 있어.”
이 사고/행동/관찰 사이클은 반복되어, 필요하면 에이전트가 여러 도구를 연쇄적으로 사용할 수 있습니다(검색, 그 다음 계산, 그 다음 다시 검색 등). 결국 에이전트는 사용자에게 답변할 수 있다고 판단합니다. 그 시점에서 행동 대신 최종 답변을 출력합니다(형식에서 Answer: 또는 Final Answer:로 표시되기도 합니다).
처음부터 구현에서 곧 보게 되겠지만, 이 과정 전체에서 에이전트는 대화와 자신의 중간 단계를 유지합니다.
각 사고와 관찰은 대화 컨텍스트에 추가될 수 있어서 LLM이 지금까지 한 일을 기억합니다.
이것은 일관성을 위해 중요합니다. 최종 결과는 에이전트가 즉석에서 접근 방식을 효과적으로 계획하고, 추론과 행동을 혼합한다는 것입니다.
이러한 동적 접근 방식은 경직된 스크립트나 단일 턴 응답보다 훨씬 더 적응력이 있습니다. 인간이 새로운 정보가 나타날 때 계획을 조정하는 것과 유사하게, 예상치 못한 하위 작업을 처리할 수 있게 해줍니다.
이 모든 “사고”와 “행동” 주석이 LLM의 마법 같은 기능이 아니라는 점에 주목하는 것이 중요합니다. 이것들은 우리가 모델에 프롬프트하는 방식에서 나옵니다.
아래에서 보게 되겠지만, 우리는 모델에게 이 구조화된 방식으로 응답을 형식화하도록 명시적으로 지시합니다. 즉, 리액트는 LLM의 내장 능력이 아니라 신중하게 제작된 프롬프트 템플릿과 파싱 로직을 통해 구현됩니다.
LLM은 우리가 제공하는 예시와 지시사항을 통해 추론하고 행동하는 에이전트처럼 행동하도록 안내받습니다.
이제 리액트 패턴을 개념적으로 이해했으니, 이 로직을 따르는 우리 자신의 에이전트를 구축하기 시작할 수 있습니다. 에이전트의 두뇌 역할을 할 언어 모델, 에이전트가 사용할 수 있는 몇 가지 도구, 그리고 이들을 연결하는 루프가 필요합니다.
다음 섹션에서는 CrewAI에서 벗어나 순수 Python으로 리액트 에이전트를 처음부터 구축할 것입니다. 로컬 LLM과 간단한 도구 정의 세트만 사용합니다. 여기서 다룬 모든 것이 마법이 아니라 스마트한 프롬프트 디자인과 제어된 입출력의 결합일 뿐이라는 것을 알게 될 것입니다.