스마티지와 글 읽기 – Reflexion 논문 설명 – AI 도 사람처럼 자기 성찰을 통해 성장합니다. 이해하기 쉬운 논문이네요.
Reflexion 논문 설명 – AI 도 사람처럼 자기 성찰을 통해 성장합니다. 이해하기 쉬운 논문이네요. – YouTube
자, 여러분! AI 지식 충전소, 일타 강사 저스틴입니다! 반갑습니다! (0:01) 오늘 우리가 함께 파헤쳐 볼 주제는 바로 AI 추론 능력의 새로운 지평을 여는 기술, “리플렉션(Reflexion) 추론 방법론” 입니다! (0:01-0:03) 어? 지난 시간에도 리플렉션에 대해 배웠다고요? 맞습니다! 하지만 지난 시간에 다룬 것은 ‘베이직 리플렉션’이었죠. (0:05-0:09) 오늘은 그 두 번째 시간, 과연 무엇이 다르고, 얼마나 더 강력해졌는지! 저스틴과 함께 낱낱이 파헤쳐 보도록 하겠습니다! 준비되셨죠? 출발합니다!
베이직 리플렉션 vs. 리플렉션(Reflexion): 뭐가 다를까?
자, 화면 왼쪽을 보시면 지난 시간에 배운 ‘베이직 리플렉션’이 있습니다. (0:13-0:17) 오른쪽은 오늘 배울 ‘리플렉션’이고요. 어? 자세히 보니 철자가 살짝 다르네요? 왼쪽은 ‘Reflection’, 오른쪽은 ‘Reflexion’입니다. (0:17-0:21) “쌤, 이거 오타 아니에요?” 하실 수 있는데, 아닙니다! 제가 또 누굽니까, 저스틴 아닙니까? 알아보니 오른쪽의 ‘Reflexion’은 주로 영국에서 사용되는 철자라고 하더라고요. 의미는 똑같습니다! (0:22-0:28)
그럼 이 둘, 근본적으로 뭐가 다를까요? 기본 골격은 같습니다. 사용자 요청(Request)에 대해 초기 답변(Initial Response)을 생성하고, 그 답변을 다시 검토(Reflection)해서 “이 부분은 좋았고, 저 부분은 부족했네”라는 피드백을 만들어서 다시 생성자(Generator)에게 전달하죠. (0:39-0:54) 그러면 LLM은 처음 답변과 피드백을 종합해서 더 나은 답변을 만들어내는, 이 과정을 반복하는 구조입니다. (0:56-1:03)
자, 여기서 별표 다섯 개! 오늘 배울 오른쪽 ‘Reflexion’의 핵심 차이점! 바로 “액터(Actor)”와 “툴(Tool)” 개념이 추가되었다는 겁니다! (1:08-1:11) 사용자의 요청에 대해 초기 답변을 생성하는 것까지는 똑같은데, 그 이후에 “툴을 사용할 것이냐, 말 것이냐”를 결정하는 단계가 들어갑니다. (1:18-1:23) 그리고 이 툴을 통해 얻은 답변을 다시 검토(Reflection)하고, 그 결과를 바탕으로 또 툴 사용 여부를 결정해서 답변을 업데이트하는 과정을 반복하는 거죠. (1:24-1:39)
이 ‘Reflexion’ 방법론은 기존 프로그래밍의 함수(Tool)와 LLM이 서로 소통해야 하기 때문에, **구조화된 출력(Structured Output)**이 중요합니다. 그래서 ‘파이덴틱(Pydantic)’ 모듈을 사용하고요, 전체 시스템은 크게 액터(Actor), 평가자(Evaluator), 그리고 평가 결과를 저장하는 메모리(Memory), 이렇게 세 가지 모듈로 구성됩니다. (1:43-2:08) 그리고 이 모든 프로세스는 바로 “Reflexion”이라는 어마어마하게 중요한 논문을 기반으로 만들어졌다는 사실! (2:09-2:12) 이 논문을 모르고서는 ‘Reflexion’을 제대로 이해했다고 말할 수 없습니다!
“Reflexion” 논문 파헤치기: AI도 사람처럼 반성하고 성장한다!
자, 그래서 오늘 저스틴이 특별히 준비했습니다! 이 “Reflexion” 논문을 함께 살펴보는 시간을 갖도록 하죠! (2:14-2:19) 논문 링크는 영상 설명란에 있으니 꼭 한번 직접 보시고요. (2:21) “쌤, 논문 너무 어려워요!” 하시는 분들을 위해 제가 공부하는 꿀팁 하나 드리자면, 논문 내용을 스크린샷 찍어서 챗GPT 같은 AI에게 “이거 설명해줘” 하면 아주 자세히 알려줍니다. (2:30-2:38)
오늘은 랭그래프(LangGraph) 소스 코드를 이해하는 데 필요한 핵심 개념들만 쏙쏙 뽑아서 알려드릴게요!
1. 앱스트랙트(Abstract) – 모든 것은 “시행착오(Trial and Error)”로부터! (2:53)
LLM에게 질문 한 번에 완벽한 답변을 얻기란 하늘의 별 따기죠. (2:57-3:01) 처음에는 틀린 답을 내놓기도 하고요. 이런 초기 시행착오에 대해 **”성찰(Reflection)”**하고, 그로부터 빠르고 효율적으로 학습하는 과정이 필요합니다. (3:03-3:09)
기존의 강화학습(Reinforcement Learning)은 엄청난 학습 데이터와 모델 튜닝 비용이 들었습니다. 모델의 가중치까지 변경해야 했죠. (3:11-3:22) 하지만! 이 논문이 제시하는 “Reflexion”은 다릅니다! 밑줄 쫙! 가중치를 변경하지 않고, 오직 “언어적 피드백”을 통해 에이전트를 강화하는 방식입니다! (3:25-3:32) 답변이 부족하거나 틀렸을 때, 그 피드백을 언어로 요약해서 “에피소딕 메모리(Episodic Memory)”에 저장하고, 다음 시도에서는 이 메모리를 참고해서 더 나은 결정을 내리게 하는, 일종의 **”소프트웨어적인 강화학습”**인 셈이죠! (3:35-3:51)
2. 인트로덕션(Introduction) – AI, 인간의 학습 방식을 모방하다! (4:09)
이 논문의 핵심 아이디어는 아주 간단하고 명쾌합니다. “사람이 실패를 통해 배우는 것처럼, AI도 스스로 반성하고 다시 시도하면서 성장한다!” (4:14-4:25) 우리도 어떤 일을 하고 나서 “아, 이 부분이 좀 아쉬웠네. 다음엔 이렇게 해봐야겠다” 하고 반성하잖아요? 그 개념을 그대로 AI에 적용한 겁니다! (4:27-4:37)
리플렉션은 단순한 성공/실패 판단뿐만 아니라, 자주 발생하는 실수 패턴(휴리스틱스)을 파악하거나, 프로그래밍의 경우 유닛 테스트를 생성해서 검증하는 방식까지 포함합니다. (4:43-4:57) 그리고 이 모든 피드백을 자연어로 요약해서 메모리에 저장했다가 다시 활용하는 거죠. (5:00-5:02)
- 장점은? 가볍다! (LLM 파인튜닝 불필요) (5:04-5:08), 정밀한 피드백 가능! (5:11), 해석 가능성 높음! (인간이 이해하기 쉬움) (5:13-5:17), 메모리 저장으로 추후 확인 및 활용 가능! (5:19-5:22)
- 단점은? LLM 성능에 의존적! (5:31-5:35), AI 특유의 비일관성 (항상 같은 결과 보장 안 됨) (5:37-5:46)
이 “Reflexion” 방법론은 언어 기반 강화학습의 새로운 지평을 열었고, LLM의 자기 성찰 능력을 통한 효율적인 학습 가능성을 검증했으며, 다양한 작업에서 기존 방식보다 뛰어난 성능 향상을 보여주었습니다! (5:53-6:10)
3. 본격 탐구! “Reflexion: 언어적 자기반성을 통한 강화학습” (6:21)
자, 드디어 본론입니다! 이 논문은 “Reflexion” 시스템을 크게 세 가지 핵심 모듈로 설명합니다. (6:34)
- 액터(Actor): 텍스트와 행동(Action)을 생성하는 주체. (6:39)
- 평가자(Evaluator): 액터가 내놓은 결과를 평가하는 심판. (6:44)
- 자기-성찰자(Self-Reflector): 액터가 더 나은 답변을 만들도록 언어적 피드백을 생성하는 조언자. (6:50)
이 세 모듈이 함께 작동하면서, 마치 우리가 경험을 통해 배우듯, 시행착오(Trial and Error)를 겪고, 그 경험을 “언어적 자기 반성(Verbal Reflection)”을 통해 학습하고 성장하는 겁니다! (6:57-7:06)
프로세스를 그림으로 한번 볼까요? (7:07)
- **액터(Actor)**가 주어진 환경(Environment)과 상호작용하며 초기 행동 계획, 즉 **”궤적(Trajectory)”**을 생성합니다. (이건 단기 기억(Short-term Memory)에 저장돼요) (7:12-7:27)
- 생성된 궤적은 **평가자(Evaluator)**에게 전달되고, 평가자는 그 결과를 평가해서 점수나 피드백을 줍니다. (7:37-7:43)
- 이 피드백과 액터가 생성한 궤적은 **자기-성찰자(Self-Reflector)**에게 넘겨집니다. (7:45-7:52)
- 자기-성찰자는 실패 원인과 개선 방향을 자연어로 요약하고, 이 귀중한 “경험(Experience)”을 **장기 기억(Long-term Memory)**에 저장합니다! (7:52-8:00)
- 액터는 이 장기 기억을 참고해서 다음번에는 한 단계 더 발전된, 더 나은 답변(궤적)을 만들어냅니다! (8:21-8:27)
이 과정을 목표를 달성할 때까지, 혹은 정해진 횟수만큼 반복하면서 점점 더 똑똑해지는 거죠! (8:02-8:06, 8:27-8:31) 이 그림이 바로 “Reflexion” 논문의 핵심 아이디어입니다! 별표 다섯 개! (8:31)
4. 핵심 구성 요소 상세 분석: 액터, 평가자, 자기-성찰자, 그리고 메모리 (8:37)
- 액터(Actor): (8:45) LLM 기반으로 텍스트나 행동을 생성합니다. 이때 ‘생각의 사슬(Chain-of-Thought)’이나 ‘리액트(ReAct)’ 같은 추론 방식을 사용할 수 있고, 툴(Tool) 사용도 가능합니다! (8:51-9:00) 액터는 단기 기억(현재 궤적)과 장기 기억(과거 성찰 내용)을 모두 참고해서 더 나은 결과를 만듭니다. (9:10-9:17)
- 평가자(Evaluator): (9:19) 액터가 만든 궤적을 평가하는 것이 주 임무입니다. 평가 기준은 작업에 따라 다른데, 예를 들어 정답이 있는 문제에서는 ‘정확히 일치하는지(Exact Match)’를, 의사결정 문제에서는 ‘일반적으로 발생하는 실수 패턴(Heuristics)’ 등을 기준으로 삼을 수 있습니다. (9:22-9:35) 액터와 평가자는 서로 다른 LLM을 사용할 수도 있습니다. 어떤 LLM은 답변 생성에 강점이 있고, 어떤 LLM은 평가에 더 뛰어날 수 있으니까요. (9:37-9:54)
- 자기-성찰자(Self-Reflector): (9:55) 액터와 평가자가 만들어낸 결과를 바탕으로, **구체적이고 유용한 “언어 피드백”**을 생성합니다. (9:58-10:03) “두 번째 행동에서 물체를 잘못 골랐네” 또는 “괄호 개수만 비교해서 오류가 났네”처럼 실수 원인과 수정 방향을 명확히 알려주는 문장을 만들어서 장기 기억에 저장하고, 액터가 다음 행동에 참고하도록 합니다. (10:05-10:21)
- 메모리(Memory): (10:24) 크게 두 가지가 있습니다. **단기 기억(Short-term Memory)**은 방금 생성한 궤적을 담고 있고, **장기 기억(Long-term Memory)**은 자기-성찰자가 만든 요약문들이 누적된 것입니다. (10:24-10:34) 이 장기 기억은 랭그래프의 경우 ‘스테이트(State)’로 관리될 수도 있고, 물리적인 데이터베이스(DB)에 저장되어 활용될 수도 있습니다. (10:36-10:43)
다시 한번 요약하자면! 액터가 요청에 대한 답변을 만들고 → 평가자가 그걸 평가하고 → 자기-성찰자가 실패 원인 분석 및 개선 방안을 담은 성찰 결과를 만들어서 → 메모리에 저장하고 → 액터는 이 메모리를 바탕으로 다음 행동을 더 잘하게 되는! 이 아름다운 선순환 구조! (10:43-11:05) 이것이 바로 “Reflexion”의 정수입니다! (11:05-11:12)
이 논문을 딱 한 문장으로 요약한다면? (11:15) “Reflexion은 액터, 평가자, 자기-성찰자 세 모듈을 조합하여, 에이전트가 시행착오 경험을 언어로 반성하고, 이 리플렉션을 메모리에 쌓아 더 나은 미래 행동을 선택하도록 학습하는 시스템이다!” (11:17-11:32) 완벽하죠? (11:33)
5. 실험 결과: “Reflexion”, 실제로 얼마나 효과적인가? (11:39)
논문에는 이 “Reflexion” 방법론을 다른 방법론들과 비교 분석한 실험 결과들이 나옵니다. (11:41) 도표를 살짝 보면, 그냥 리액트(ReAct)만 사용했을 때보다 (회색) 리플렉션을 함께 적용했을 때 (파란색-휴리스틱스, 녹색-GPT 기반 셀프 리플렉션) 훨씬 좋은 성과를 보여주는 것을 확인할 수 있습니다. (11:48-12:06) 환각(Hallucination) 현상이나 비효율적인 계획(Inefficient Planning)도 리플렉션을 적용하면 급격하게 감소하는 모습을 보이죠! (12:10-12:32) 다양한 QA 데이터셋에서도 리플렉션을 적용했을 때 훨씬 뛰어난 결과를 나타냅니다. (12:32-12:48)
6. 마무리: 한계점과 미래 연구 방향 (12:50)
물론 이 방법론에도 한계는 있습니다. LLM을 통해 평가하기 때문에 최적의 답이 아닌 국지적 최적해(Local Minima)에 빠질 수 있고, 메모리 용량의 한계도 존재합니다. (12:52-13:08) 특히 프로그래밍 관련 영역에서는 더 많은 연구가 필요하고요. (13:13-13:20)
긍정적인 영향으로는 인터넷, 소프트웨어, 로봇 등 다양한 환경에서 효율성을 높일 수 있지만, 잘못 사용될 경우 위험도 커질 수 있으므로 안전성과 윤리성 강화에 더 많은 노력이 필요하다고 저자들은 강조합니다. (13:22-13:40) 다만, 기존 강화학습의 문제점이었던 긴 학습 시간과 많은 리소스 소모를 완화했다는 점에서 “소프트웨어적인 리플렉션” 방법론의 큰 성과라고 할 수 있겠습니다! (13:40-14:00)
결론적으로, 이 논문은 “Reflexion”이라는 언어 기반 강화학습을 통해 에이전트가 과거 실수로부터 스스로 학습하게 만드는 혁신적인 접근 방식을 제시했으며, 기존 의사결정 기반 에이전트보다 훨씬 뛰어난 성능을 보여주었습니다. (14:02-14:18) 앞으로 전통적인 강화학습 기법들과 결합하면 더욱 강력한 성능을 낼 수 있을 것으로 기대되며, 특히 코드 생성과 같은 분야에서는 생성된 코드의 안전성 검증이 반드시 필요하다는 점을 강조하며 마무리합니다. (14:20-15:06)
랭그래프(LangGraph)와 “Reflexion”의 만남!
자, 이 모든 논문의 정수를 담아 랭그래프에서 만든 튜토리얼 소스 코드가 바로 이것입니다! (15:27-15:32) 이 튜토리얼은 “Reflexion” 아키텍처를 기반으로, **액터(Actor), 외부 평가자(External Evaluator), 그리고 에피소딕 메모리(Episodic Memory)**라는 세 가지 주요 구성 요소를 사용합니다. (15:47-16:24)
다만, 랭그래프 튜토리얼에서는 평가자와 메모리 부분은 태스크별로 매우 다양하게 구현될 수 있기 때문에, 주로 “액터” 부분에 중점을 두어 빌드하고, 나머지 두 부분은 비교적 간단하게 처리하고 있습니다. (16:24-16:41)
- 더욱 정교한 평가(Evaluation)를 원한다면? “라가스(RAGAS)”와 같은 이밸류에이션 프레임워크가 별도로 연구되고 있습니다. (16:43-17:06)
- 더욱 강력한 메모리 관리를 원한다면? “RAG(Retrieval Augmented Generation)” 분야에서 데이터베이스를 활용한 메모리 관리 기법들이 활발히 연구되고 있죠. (17:08-17:25)
이처럼 평가와 메모리 부분은 앞으로 AI 어플리케이션 개발에서 계속해서 풀어 나가야 할 중요한 과제라고 할 수 있겠습니다. (17:27-17:37)
자, 오늘 이렇게 “Reflexion” 논문의 핵심 내용들을 쭉 훑어봤는데요, 정말 AI가 사람처럼 스스로 생각하고 발전해나가는 모습이 놀랍지 않습니까? (17:37-17:40) 다음 시간에는 이 논문을 바탕으로 랭그래프에서는 과연 소스 코드를 어떻게 구현했는지 직접 살펴보고 실행해보는 시간을 갖도록 하겠습니다! (17:40-17:48) 기대 많이 해주시고요, 오늘 강의는 여기까지! 저스틴이었습니다! 감사합니다! (17:48-17:50)
저스틴) LG_Reflexion.ipynb을 일타 강사 저스틴과 함께 읽습니다.
알겠습니다! 제공해주신 “LG_Reflexion.ipynb” 노트북 파일의 내용을 바탕으로, 일타 강사 저스틴 스타일로 핵심만 쏙쏙 뽑아 설명해 드리겠습니다! 자, 오늘 수업은 실제 코드를 보면서 진행되니 눈 크게 뜨고 따라오세요!
자, 여러분! 드디어 올 것이 왔습니다! 지난 시간, 우리가 이론으로만 접했던 바로 그 “Reflexion” 아키텍처를 이제 실제 코드로 만나볼 시간입니다! (짝짝짝!) “Shinn 외 연구진이 개발한 Reflexion은 언어적 피드백과 자기 성찰을 통해 학습하도록 설계된 아키텍처”라고 배웠죠? 에이전트가 스스로 자신의 답변을 냉철하게 비판하고 개선해서, 최종적으로는 훨씬 더 높은 퀄리티의 답변을 만들어내는, 하지만 시간은 쪼~끔 더 걸리는 그런 마법 같은 시스템이라고 했습니다.
이 논문에서 제시한 세 가지 핵심 구성 요소, 다들 기억나시죠?
- 자기 성찰 기능을 갖춘 액터(Actor) (에이전트)
- 외부 평가자(External Evaluator) (이건 작업에 따라 달라요. 예를 들면 코드 컴파일 같은 거)
- 성찰 결과를 저장하는 에피소드 메모리(Episodic Memory)
자, 그런데 이 노트북에서는요, “외부 평가자”랑 “에피소드 메모리”는 실제 작업에 따라 너무나 다양하게 구현될 수 있기 때문에, 일단은 가장 중요한 핵심! 바로 “액터(Actor)”를 랭그래프(LangGraph)로 직접 만들어 볼 겁니다! “쌤, 저는 그래프 정의부터 보고 싶은데요?” 하시는 분들은 스크롤 쭉~ 내려서 “Construct Graph” 섹션으로 바로 가셔도 됩니다!
1단계: 준비운동 – 필요한 라이브러리 설치 및 환경 설정
자, 모든 코딩의 시작은 뭐다? 그렇죠! 필요한 연장부터 챙겨야죠!
langgraph
: 우리 Reflexion 아키텍처를 만들 뼈대!langchain_anthropic
: 똑똑한 LLM(여기서는 앤트로픽의 클로드)을 데려올 도구! (물론 OpenAI의 GPT 같은 다른 모델로 바꿔도 됩니다!)tavily-python
: 검색 엔진(타빌리)을 우리 에이전트의 눈과 귀로 만들어줄 도구!
1 2 3 |
%pip install -U --quiet langgraph langchain_anthropic tavily-python langchain_community |
그리고 API 키 설정하는 거, 이제 다들 익숙하시죠? (ANTHROPIC_API_KEY, TAVILY_API_KEY 설정)
1 2 3 4 5 6 7 8 9 10 11 |
import getpass import os def _set_if_undefined(var: str) -> None: if os.environ.get(var): return os.environ[var] = getpass.getpass(var) _set_if_undefined("ANTHROPIC_API_KEY") _set_if_undefined("TAVILY_API_KEY") |
아, 그리고 우리 개발 과정을 한눈에 보고 문제점을 바로바로 찾아낼 수 있게 도와주는 LangSmith 설정도 잊지 마시고요! (LANGCHAIN_API_KEY, LANGCHAIN_PROJECT 설정) 이건 선택이지만, 실력 향상을 원한다면 강력 추천합니다!
1 2 3 4 5 6 7 8 9 10 11 |
def set_env_variable(var_name: str, hidden: bool = True): if var_name not in os.environ: if hidden: os.environ[var_name] = getpass.getpass(f"{var_name}: ") else: os.environ[var_name] = input(f"{var_name}: ") set_env_variable("LANGCHAIN_API_KEY", hidden=True) set_env_variable("LANGCHAIN_PROJECT", hidden=False) # 예: "Reflexion_PreTest" os.environ["LANGCHAIN_TRACING_V2"] = "true" |
2단계: 우리의 뇌, LLM 정의하기
자, 이제 우리 에이전트의 뇌 역할을 할 LLM을 정해봅시다. 이 노트북에서는 앤트로픽의 claude-3-haiku-20240307
모델을 사용했네요. (물론 더 강력한 claude-3-5-sonnet-20240620
이나 OpenAI의 gpt-4-turbo-preview
같은 다른 모델로 변경 가능합니다!)
1 2 3 4 |
from langchain_anthropic import ChatAnthropic llm = ChatAnthropic(model="claude-3-haiku-20240307") |
3단계: 주인공 등장! 액터(Actor) 만들기 (자기 성찰 기능 탑재!)
자, 드디어 오늘의 주인공, **Reflexion의 핵심인 “액터(Actor)”**를 만들어 볼 시간입니다! 이 액터는요, 자기가 한 답변을 스스로 되돌아보고(“아, 이건 좀 부족했네?”), 그 비판을 바탕으로 더 나은 답변을 내놓기 위해 다시 한번 생각하고 실행하는 아주 똑똑한 녀석입니다.
액터는 크게 세 가지 하위 요소로 구성돼요.
- 도구 / 도구 실행 (Tools / Tool execution): 정보를 찾거나 특정 작업을 수행할 때 필요하겠죠?
- 초기 응답자 (Initial responder): 일단 첫 번째 답변과 함께 자기 성찰 내용(셀프 피드백)을 만들어냅니다.
- 수정자 (Revisor): 이전 성찰 내용을 바탕으로 답변을 수정하고, 또다시 성찰합니다.
3-1. 액터의 손과 발: 도구 만들기 (Construct tools)
먼저 액터가 사용할 도구부터 만들어보죠. 여기서는 타빌리(Tavily) 검색 엔진을 사용합니다. 검색 결과는 최대 5개까지 가져오도록 설정했네요.
1 2 3 4 5 6 |
from langchain_community.tools.tavily_search import TavilySearchResults from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper search = TavilySearchAPIWrapper() tavily_tool = TavilySearchResults(api_wrapper=search, max_results=5) |
3-2. 첫인상이 중요! 초기 응답자 (Initial responder) 만들기
자, 이제 사용자의 질문에 대한 첫 번째 답변을 생성하는 “초기 응답자”를 만들 차례입니다. 여기서 아주 중요한 점! 답변의 형식을 미리 정의하기 위해 파이댄틱(Pydantic)을 사용합니다. (Pydantic v2 BaseModel 기준이라 langchain-core
0.3 이상 버전이 필요해요!)
먼저, 성찰(Reflection)의 구조부터 정의해볼까요? “missing” (뭐가 빠졌는지)과 “superfluous” (뭐가 불필요한지) 두 가지를 비판하도록 되어 있네요. 아주 날카롭죠?
1 2 3 4 5 6 |
from pydantic import BaseModel, Field class Reflection(BaseModel): missing: str = Field(description="Critique of what is missing.") superfluous: str = Field(description="Critique of what is superfluous") |
다음은 질문에 답변하는 AnswerQuestion
클래스입니다. 여기에는 세 가지 필드가 있어요.
answer
: 약 250 단어 내외의 상세한 답변.reflection
: 방금 만든Reflection
클래스 형태의 자기 성찰 내용.search_queries
: 현재 답변의 문제점을 개선하기 위한 1~3개의 검색어 제안. (이게 바로 다음 단계를 위한 발판이죠!)
1 2 3 4 5 6 7 8 |
class AnswerQuestion(BaseModel): """Answer the question. Provide an answer, reflection, and then follow up with search queries to improve the answer.""" answer: str = Field(description="~250 word detailed answer to the question.") reflection: Reflection = Field(description="Your reflection on the initial answer.") search_queries: list[str] = Field( description="1-3 search queries for researching improvements to address the critique of your current answer." ) |
그리고 혹시 LLM이 우리가 정해준 형식대로 답변을 안 할 수도 있으니, 최대 3번까지 재시도하는 ResponderWithRetries
클래스도 만들어줍니다. 만약 유효성 검사(Validation)에 실패하면, 어떤 부분이 잘못됐는지 알려주고 다시 답변하도록 친절하게 안내까지 해주죠. (이런 디테일! 아주 칭찬해!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
from langchain_core.messages import HumanMessage, ToolMessage from langchain_core.output_parsers.openai_tools import PydanticToolsParser from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from pydantic import ValidationError class ResponderWithRetries: def __init__(self, runnable, validator): self.runnable = runnable self.validator = validator def respond(self, state: dict): response = [] for attempt in range(3): response = self.runnable.invoke( {"messages": state["messages"]}, {"tags": [f"attempt:{attempt}"]} ) try: self.validator.invoke(response) return {"messages": response} except ValidationError as e: state["messages"] = state["messages"] + [ # state 딕셔너리의 messages 리스트를 직접 수정 response, ToolMessage( content=f"{repr(e)}\n\nPay close attention to the function schema.\n\n" + self.validator.model_json_schema() # 최신 Pydantic 방식 + " Respond by fixing all validation errors.", tool_call_id=response.tool_calls[0]["id"], ), ] return {"messages": response} # 마지막 시도 결과 반환 |
자, 이제 이 모든 걸 합쳐서 **”초기 응답 체인(initial_answer_chain)”**을 만듭니다. 시스템 프롬프트에는 “당신은 전문 연구원입니다. 현재 시간은 입니다.”라고 시작해서, 1) 상세한 답변 제공, 2) 답변에 대한 냉정한 비판, 3) 개선을 위한 검색어 추천이라는 3단계 지시가 명확하게 들어가 있죠. 그리고 사용자의 질문과 지금까지의 과정을 되돌아보고 AnswerQuestion
함수를 사용해서 응답하라는 리마인더까지! 아주 꼼꼼합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import datetime actor_prompt_template = ChatPromptTemplate.from_messages( [ ( "system", """You are expert researcher. Current time: {time} 1. {first_instruction} 2. Reflect and critique your answer. Be severe to maximize improvement. 3. Recommend search queries to research information and improve your answer.""", ), MessagesPlaceholder(variable_name="messages"), ( "user", "\n\n<system>Reflect on the user's original question and the" " actions taken thus far. Respond using the {function_name} function.</reminder>", ), ] ).partial( time=lambda: datetime.datetime.now().isoformat(), ) initial_answer_chain = actor_prompt_template.partial( first_instruction="Provide a detailed ~250 word answer.", function_name=AnswerQuestion.__name__, ) | llm.bind_tools(tools=[AnswerQuestion]) validator = PydanticToolsParser(tools=[AnswerQuestion]) print("Validator is ", validator) first_responder = ResponderWithRetries( runnable=initial_answer_chain, validator=validator ) |
한번 테스트해볼까요? “AI에서 리플렉션은 왜 유용한가요?” 라는 질문을 던져봅시다.
1 2 3 4 5 6 |
example_question = "Why is reflection useful in AI?" initial = first_responder.respond( {"messages": [HumanMessage(content=example_question)]} ) # initial["messages"].tool_calls[0]["args"] # 결과 확인 (주석 처리) |
(실제 실행 시 initial["messages"].tool_calls["args"]
를 통해 어떤 답변과 성찰, 그리고 검색어가 나왔는지 확인해보세요! 아마 깜짝 놀라실 겁니다!)
3-3. 더 나은 나를 위해! 수정자 (Revision) 만들기
자, 첫 번째 답변과 성찰, 그리고 다음 검색어까지 얻었습니다. 이제 이걸 바탕으로 답변을 **”수정(Revision)”**할 차례입니다!
수정 단계에서의 지시사항(revise_instructions
)은 더 구체적입니다.
- 이전 비판을 활용해서 중요한 정보를 추가해라!
- 검증 가능하도록 반드시 숫자 인용을 포함해라!
- 답변 하단에 “References” 섹션을 추가해라! (단어 수 제한에는 포함 안 됨)
- 이전 비판을 활용해서 불필요한 정보는 삭제하고, 반드시 250단어 이내로 맞춰라! (아주 엄격하죠?)
수정된 답변의 형식을 정의하기 위해 ReviseAnswer
클래스를 만듭니다. 기존 AnswerQuestion
을 상속받고, 여기에 references
(인용된 출처 목록) 필드를 추가합니다. 모델에게 인용을 강제함으로써 더 근거 있는 답변을 유도하는 거죠!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
revise_instructions = """Revise your previous answer using the new information. - You should use the previous critique to add important information to your answer. - You MUST include numerical citations in your revised answer to ensure it can be verified. - Add a "References" section to the bottom of your answer (which does not count towards the word limit). In form of: - [1] https://example.com - [2] https://example.com - You should use the previous critique to remove superfluous information from your answer and make SURE it is not more than 250 words. """ class ReviseAnswer(AnswerQuestion): """Revise your original answer to your question. Provide an answer, reflection, cite your reflection with references, and finally add search queries to improve the answer.""" references: list[str] = Field( description="Citations motivating your updated answer." ) |
이제 “수정 체인(revision_chain)”을 만듭니다. actor_prompt_template
은 동일하게 사용하되, first_instruction
을 revise_instructions
로, function_name
을 ReviseAnswer.__name__
으로 바꿔주면 되겠죠?
1 2 3 4 5 6 7 8 |
revision_chain = actor_prompt_template.partial( first_instruction=revise_instructions, function_name=ReviseAnswer.__name__, ) | llm.bind_tools(tools=[ReviseAnswer]) revision_validator = PydanticToolsParser(tools=[ReviseAnswer]) revisor = ResponderWithRetries(runnable=revision_chain, validator=revision_validator) |
자, 그럼 아까 initial
단계에서 얻은 첫 번째 답변과 추천 검색어를 가지고, 타빌리 검색을 실행한 후, 그 결과를 ToolMessage
로 만들어서 수정자에게 전달해봅시다!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import json # initial 단계에서 얻은 search_queries 중 첫 번째 검색어를 사용 # 실제로는 모든 검색어를 사용하거나, 더 정교한 로직이 필요할 수 있습니다. if initial["messages"].tool_calls and initial["messages"].tool_calls[0]["args"]["search_queries"]: search_query_to_use = initial["messages"].tool_calls[0]["args"]["search_queries"][0] tool_results = tavily_tool.invoke({"query": search_query_to_use}) revised = revisor.respond( { "messages": [ HumanMessage(content=example_question), initial["messages"], # 이전 LLM의 응답 (AIMessage) ToolMessage( tool_call_id=initial["messages"].tool_calls[0]["id"], content=json.dumps(tool_results), # 검색 결과 ), ] } ) # revised["messages"].tool_calls[0]["args"] # 결과 확인 (주석 처리) else: print("초기 응답에서 검색어가 생성되지 않았습니다.") revised = {"messages": initial["messages"]} # 검색어가 없으면 이전 답변을 그대로 사용 (예시) |
(실행 후 revised["messages"].tool_calls["args"]
를 통해 수정된 답변과 새로운 성찰, 그리고 인용 정보까지 확인해보세요! 확실히 업그레이드된 것을 느낄 수 있을 겁니다!)
4단계: 도구 실행을 위한 만능 노드! 도구 노드(Tool Node) 만들기
초기 응답자든 수정자든, 결국 검색어를 만들면 실제 검색을 실행할 누군가가 필요하겠죠? 이때 사용하는 것이 바로 **”도구 노드(ToolNode)”**입니다. 우리는 LLM에게 서로 다른 함수 이름(AnswerQuestion
, ReviseAnswer
)을 알려줬지만, 실제로는 둘 다 동일한 run_queries
함수(타빌리 검색 실행)로 연결되도록 만들어 줄 겁니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from langchain_core.tools import StructuredTool from langgraph.prebuilt import ToolNode def run_queries(search_queries: list[str], **kwargs): """Run the generated queries.""" return tavily_tool.batch([{"query": query} for query in search_queries]) tool_node = ToolNode( [ StructuredTool.from_function(run_queries, name=AnswerQuestion.__name__), StructuredTool.from_function(run_queries, name=ReviseAnswer.__name__), ] ) |
5단계: 드디어 완성! 전체 흐름을 꿰는 그래프(Graph) 만들기
자, 이제 우리가 만든 모든 조각들(초기 응답자, 수정자, 도구 노드)을 하나로 멋지게 엮어서 하나의 자동화된 흐름, 즉 그래프(Graph)를 만들 시간입니다! 랭그래프의 StateGraph
를 사용해서 상태(여기서는 메시지 목록)를 관리하고, 각 단계를 노드(Node)로, 단계 간의 전환을 엣지(Edge)로 정의할 거예요.
먼저, 그래프의 상태를 정의하는 State
클래스를 만듭니다. 메시지 목록을 저장하도록 하죠.
1 2 3 4 5 6 7 8 9 |
from typing import Literal from langgraph.graph import END, StateGraph, START from langgraph.graph.message import add_messages # 수정된 임포트 from typing import Annotated # TypedDict 대신 사용 from typing_extensions import TypedDict # TypedDict는 여기서 가져옵니다. class State(TypedDict): messages: Annotated[list, add_messages] |
이제 StateGraph
객체를 만들고, 우리가 정의한 노드들을 추가합니다.
draft
: 초기 응답자 (first_responder.respond
)execute_tools
: 도구 노드 (tool_node
)revise
: 수정자 (revisor.respond
)
그리고 이 노드들을 어떻게 연결할지 엣지를 정의합니다.
draft
노드 다음에는 무조건execute_tools
노드로!execute_tools
노드 다음에는 무조건revise
노드로!
1 2 3 4 5 6 7 8 9 10 |
MAX_ITERATIONS = 5 # 최대 반복 횟수 builder = StateGraph(State) builder.add_node("draft", first_responder.respond) builder.add_node("execute_tools", tool_node) builder.add_node("revise", revisor.respond) builder.add_edge("draft", "execute_tools") builder.add_edge("execute_tools", "revise") |
자, 여기서 하이라이트! 수정(revise)을 한 다음에는 어떻게 해야 할까요? 답변이 완벽해질 때까지 계속 도구를 실행하고 수정하는 과정을 반복해야겠죠? 하지만 무한정 반복할 수는 없으니, **최대 반복 횟수(MAX_ITERATIONS)**를 정해두고, 이 횟수를 넘어가면 종료(END)하도록 조건을 걸어줍니다. 아직 반복 횟수가 남았다면 다시 execute_tools
노드로 돌아가서 개선 작업을 계속하고요!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def _get_num_iterations(state: State): # State 타입 명시 i = 0 # messages 키가 있는지 확인 if "messages" not in state or not state["messages"]: return 0 for m in state["messages"][::-1]: # messages 리스트를 뒤에서부터 순회 # HumanMessage 와 AIMessage (tool_calls 포함) 만 카운트 if m.type not in {"tool", "ai"}: # AIMessage도 ai 타입으로 간주될 수 있음 break i += 1 return i def event_loop(state: State): # State 타입 명시 num_iterations = _get_num_iterations(state) if num_iterations > MAX_ITERATIONS: return END return "execute_tools" builder.add_conditional_edges("revise", event_loop, ["execute_tools", END]) |
마지막으로, 그래프의 시작점(START)을 draft
노드로 지정하고, compile()
메소드를 호출하면 드디어 우리의 Reflexion 그래프가 완성됩니다!
1 2 3 |
builder.add_edge(START, "draft") graph = builder.compile() |
자, 이제 이 멋진 그래프가 어떻게 생겼는지 한번 시각적으로 확인해볼까요? (필수 라이브러리가 없으면 오류가 날 수도 있지만, 실행에는 문제없으니 걱정 마세요!)
1 2 3 4 5 6 7 8 |
from IPython.display import Image, display try: display(Image(graph.get_graph().draw_mermaid_png())) except Exception: # This requires some extra dependencies and is optional pass |
6단계: 실전 투입! 그래프 실행해보기
자, 이제 대망의 순간입니다! “기후 위기에 어떻게 대처해야 할까요?”라는 심오한 질문을 우리 Reflexion 에이전트에게 던져보고, 과연 어떤 과정을 거쳐 답변을 개선해나가는지 그 여정을 함께 따라가 봅시다! stream_mode="values"
를 사용하면 각 단계별 진행 상황을 실시간으로 확인할 수 있어요.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
events = graph.stream( {"messages": [("user", "How should we handle the climate crisis?")]}, stream_mode="values", ) for i, step in enumerate(events): print(f"--- Step {i} ---") # messages 키가 있는지, 그리고 비어있지 않은지 확인 if "messages" in step and step["messages"]: step["messages"][-1].pretty_print() else: print("No messages in this step or 'messages' key is missing.") print("\n") |
(실행 결과를 보면, 에이전트가 초기 답변을 만들고 -> 검색하고 -> 답변을 수정하고 -> 또 검색하고 -> 또 수정하는 과정을 반복하면서 점점 더 정교하고 근거 있는 답변으로 발전해나가는 모습을 직접 확인할 수 있을 겁니다! 정말 신기하죠?)
마지막으로, 저스틴의 꿀팁! (Conclusion)
자, 이렇게 해서 우리는 Reflexion 액터를 직접 만들어봤습니다! (짝짝짝!) 이 에이전트를 여러분의 작업에 적용하실 때 시간을 아껴드릴 몇 가지 관찰 결과를 공유할게요.
- 시간 vs. 품질의 트레이드오프: 이 에이전트는 여러 단계를 거쳐 출력을 비판하고 수정하도록 강제합니다. 그래서 보통 (항상 그런 건 아니지만!) 답변 품질은 올라가지만, 최종 답변을 얻기까지 시간이 꽤 오래 걸린다는 점, 기억하세요!
- 외부 피드백과의 시너지: ‘리플렉션(성찰)’ 과정에 검증기(validator) 같은 추가적인 외부 피드백을 결합하면 액터를 더욱 효과적으로 이끌 수 있습니다.
- 외부 메모리 활용 가능성: 논문에서는 AlfWorld라는 환경에서 외부 메모리를 사용했다고 하죠? 성찰 내용을 요약해서 외부 저장소에 저장했다가 다음 시도에 활용하는 방식으로요. 우리도 더 복잡한 작업을 할 때는 이런 방식을 고려해볼 수 있겠죠?
어떠셨나요, 여러분? 오늘 저스틴과 함께 Reflexion 아키텍처의 실제 코드를 속속들이 파헤쳐 봤는데, 이제 AI가 어떻게 스스로 학습하고 성장하는지 조금 더 감이 잡히시죠? 앞으로 이 Reflexion 개념을 잘 활용하셔서 여러분만의 더욱 강력하고 똑똑한 AI 에이전트를 만들어보시길 바랍니다!
궁금한 점이 있다면 언제든지 댓글로 질문 남겨주시고요, 오늘 수업은 여기까지! 이상, 일타 강사 저스틴이었습니다! 감사합니다!