재귀 언어 모델: 올인원 심층 탐구
여기서는 Recursive Language Models (RLMs)이 무엇인지, 왜 현재 모든 장기‑컨텍스트 벤치마크에서 승리하고 있는지, 그리고 기존 에이전트형 하네스 설계와 어떻게 다른지 배웁니다!
그리고 우리는 하나의 간단한 사례 연구를 확대해서 배울 것입니다.
지난달에 RLM을 구현하고, 벤치마크를 실행하고, 50분짜리 튜토리얼 영상을 제작하는 데 꽤 많은 시간을 투자했습니다. 그 과정에서 YouTube와 X(구 트위터)에서 RLM에 관한 100개 이상의 질문에 답변했습니다. 이 글은 그 질문들에 답하면서 배운 내용과, “아하!” 하고 느낀 RLM의 구체적인 뉘앙스를 정리한 것입니다.
부가 설명: 별도 명시가 없는 한, 이 글에 사용된 모든 이미지는 저자가 직접 만든 것입니다. 무료 라이선스.

Recursive Language Models가 많은 사람들에게 접근하기 어려워 보이는 주된 이유는, 몇몇 아이디어가 기존 방법(예: ReAct, CodeAct, 일반 서브에이전트 등)과 비교했을 때 꽤 직관에 반하기 때문입니다. RLM을 이해하는 가장 좋은 방법은, 다른 방법들이 어디서 실패하는지를 먼저 파악하고, 에이전트형 하네스에 빠진 단 하나의 조각을 깨닫는 것입니다.
핵심 아이디어: 컨텍스트를 복제하는 대신 참조(reference)로 전달한다.
1. 내가 진행한 복잡한 실험 중 가장 깨우친 실험…
… 가장 통찰력 있었던 실험은 RLM에게 다음과 같은 바보 같은 요청을 했을 때였습니다.
문제 1
“과일 이름 50개를 생성하고, 각 과일에 포함된 ‘R’의 개수를 세어 딕셔너리 형태로 반환해 주세요.”
그리고 조금 더 확장된 변형(문제 2)도 시도했습니다.
문제 2
“다음과 같은 카테고리별 딕셔너리를 생성해 주세요: 과일, 국가, 동물. 각 카테고리마다 50개의 이름을 만들고, 각 이름에 포함된 ‘R’의 개수를 세어 중첩 딕셔너리 형태로 반환해 주세요.”
문제 1에 대한 기대 출력 예시:
{"strawberry": 3, "berry": 2, ... "grape": 1}
문제 2에 대한 기대 출력 예시:
{
"fruits": {"strawberry": 3, "berry": 2, ... "grape": 1, ...},
"countries": {"united states of america": 1, "russia": 1, ...},
"animals": {"kangaroo": 1, "tiger": 1, ... "deer": 1, ...}
}
바보 같은 문제라 생각할 수도 있지만, RLM이 이를 해결하는 방식은 ReAct이나 CodeAct 같은 다른 아키텍처와 근본적으로 다릅니다.
각 방법이 이 장난감 문제를 푸는 방식을 이해하는 것만으로도 RLM의 아름다움을 충분히 감상할 수 있습니다.
시작해볼까요!
2. 에이전트형 풍경
2.1 직접 생성 (Direct Generation)
첫 번째 방법은 그냥 직접 생성하는 것입니다. LLM이 사용자의 요청을 “생각”하고 자동 회귀(auto‑regressive) 방식으로 딕셔너리를 생성합니다.
하네스도, 스캐폴드도 없이, 루프 안에서 다음 토큰을 예측하는 순수한 방식입니다.
이 접근법의 문제점:
- LLM이 수학적으로 정확한지 검증할 방법이 없음
- 알파벳 개수 세기는 근본적으로 “다음 단어 예측” 문제가 아니기 때문에 잘못될 가능성이 높음
- 기본 LLM이 똑똑하더라도 환각(hallucination) 혹은 오류가 발생할 확률이 매우 높음
2.2 ReAct (Reasoning and Acting)
ReAct는 추론‑행동 루프입니다. LLM이 먼저 문제를 생각(체인‑오브‑생각)하고, 그 다음에 도구 호출을 생성합니다. 시스템 프롬프트에 “함수 이름 리스트”와 호출 방법을 전달하는 식이죠.
예를 들어, 아래와 같은 아주 간단한 도구를 LLM에게 제공할 수 있습니다.
def count_alphabets_in_word(word: str, alphabet: str) -> int:
...
위 함수를 이용하면 ReAct 에이전트는 다음을 수행합니다.
- 과일 이름 리스트를 생성
- 도구를 사용해 각 과일 이름을 전달하고 정수 결과를 받음
- 메모리(출력)에서 딕셔너리를 재구성하고 반환
실제 트랜잭션 스택 트레이스는 다음과 같습니다.
# User
Generate a dictionary with 50 fruits and the number of 'r' in each
# Assistant
50 fruit names are: strawberry, berry, grape, ...
# Assistant
count_alphabets_in_word("strawberry", "r")
# Tool_Out (our function executes)
3
# Assistant
count_alphabets_in_word("berry", "r") ## Tool call executed!
# Tool_Out (our function executes)
2
...
# Assistant
I now have everything I need in my message history,
let's construct that dictionary
{ "strawberry": 3, "berry": 2, .... }
문제점이 보이시나요? 먼저 count_alphabet_in_r 같은 전용 함수를 정의해야 합니다. 함수를 정의하지 않으면 에이전트는 옛날 방식(그냥 알파벳 개수 직접 생성)으로 되돌아갑니다.
이 방식은 LLM에게 “출력 형태가 이렇다”는 힌트를 주지만, 여전히 LLM은 메시지 히스토리에서 토큰을 하나씩 생성해야 합니다.
각 단어의 개수를 기억하고, 메모리에서 그대로 재현해야 하므로 전송 오류가 발생할 여지가 남아 있습니다.
문제 2와 같은 다중 카테고리 상황에서는 문제가 더욱 복합적으로 얽힙니다. LLM은 긴 함수 호출 트레이스를 반복하고, 각 단계에서 무슨 일이 있었는지 기억한 뒤 토큰을 하나씩 생성해야 합니다.
개발자 입장에서 ReAct는 좁은 범위의 애플리케이션—예: 웹 검색, 문서 검색, 계산기, 터미널 접근, 파일 편집, diff 적용 등—에 특화된 도구가 필요할 때는 매우 유용합니다. 하지만 일반적인 에이전트를 만들고 싶다면, 이런 특정 도구(count_alphabet_in_r 등)를 일일이 정의해 주기는 현실적이지 않습니다. 일반 에이전트에게는 보편적인 도구만이 의미가 있거든요.
만약 LLM이 스스로 도구를 만들 수 있다면?
2.3 CodeAct
CodeAct는 LLM이 코드를 작성하고 실행하도록 허용합니다.
즉, 인간이 정확한 도구를 미리 만들 필요가 없습니다. LLM에게 “파이썬 코드를 자유롭게 작성하고 샌드박스 터미널에서 실행한 뒤 결과를 읽어와서 출력해라”는 권한을 부여하는 겁니다.
예시 흐름:
# User
Generate a dictionary with 50 fruits and the number of 'r' in each
# Assistant
Okay let's write some python code for this.
python -c '
fruits = ["strawberry", "berry", "grape", ...]
count_r = {k: sum(1 for c in k if c == "r") for k in fruits}
print("Number of fruits:", len(fruits))
print("Counts:", count_r)
'
# Tool Output (Terminal Output)
Number of fruits: 50
Counts are: {"strawberry": 3, "berry": 2, ...}
# Assistant
Okay, I have read the terminal output,
let me write it down again to return the output
{ "strawberry": 3, "berry": 2, ... }
CodeAct의 동작 방식 요약:
- 전체 사용자 메시지를 읽음 (다른 방법과 동일)
- LLM이 코드/쉘 명령을 작성·실행
- 실행 결과를 컨텍스트 윈도우에 로드
- 로드된 결과를 바탕으로 최종 출력 생성
CodeAct도 ReAct와 마찬가지로 전송 오류에 취약합니다. LLM이 여전히 메모리에서 답을 그대로 재현해야 하기 때문이죠. 다만 장점은 인간이 사전에 도구를 구성할 필요가 없다는 점입니다. 에이전트가 스스로 실행 가능한 명령을 만들어냅니다.
ReAct vs CodeAct 사용 가이드
- ReAct: 도구가 명확히 정해져 있고, 문제 해결에 필요한 도구가 제한적인 좁은 제품에 적합합니다.
- CodeAct: 도메인이 일반적이고, 사전 정의된 도구가 없을 때 사용합니다.
- 성능 차이: CodeAct는 LLM이 도구를 스스로 생각하고 작성해야 하므로, 동일 작업을 ReAct보다 느리게 수행합니다.
문제 2와 같은 다중 카테고리 상황에서도 CodeAct는 동일한 전송 오류에 직면합니다. AI는 150개의 서로 다른 이름(과일·국가·동물)과 각 이름에 포함된 ‘r’ 개수를 동시에 추적해야 하므로, 내부 상태 관리가 크게 복잡해집니다.
만약 이 세 카테고리를 각각 독립적으로 처리하고, 결과를 나중에 합칠 수 있다면? (이하 내용은 다음 섹션에서 이어집니다)