-
Recursive Language Models (RLM)이코에코(Eco²)/Foundations 2026. 1. 9. 23:17

논문: Recursive Language Models
저자: Alex L. Zhang, Tim Kraska, Omar Khattab
출처: arXiv:2512.24601 (2025)
링크: https://arxiv.org/abs/2512.24601
1. 핵심 개념
1.1 문제 정의: Context Window의 한계

현대의 대규모 언어 모델(LLM)은 고정된 컨텍스트 윈도우를 가지며, 이로 인해 두 가지 문제가 발생합니다:
- Context Length Limit: 입력이 컨텍스트 윈도우를 초과하면 처리 불가
- Context Rot: 컨텍스트가 길어질수록 성능이 급격히 저하되는 현상
Context Rot 현상: ┌────────────────────────────────────────────────────────────────┐ │ │ │ 성능 │ ████████████ │ │ │ ████████████████████ │ │ │ ████████████████████████████████ │ │ │ ████████████████████████████████████████░░░░░░░░░░░░░ │ │ └────────────────────────────────────────────────────── │ │ 짧은 컨텍스트 →→→→→→→ 긴 컨텍스트 │ │ │ │ ※ 입력 길이가 증가함에 따라 "잊어버리는" 현상 발생 │ │ │ └────────────────────────────────────────────────────────────────┘1.2 기존 접근법의 한계
접근법 방식 한계 Context Compaction 긴 컨텍스트를 요약하여 압축 세부 정보 손실, 복잡한 작업 부적합 RAG 관련 청크만 검색하여 주입 전체 문맥 파악 어려움 Sliding Window 최근 N 토큰만 유지 이전 정보 완전 손실 Sparse Attention 일부 토큰에만 어텐션 구현 복잡, 정보 손실 가능 기존 방식들은 정보 손실을 수반하며, 프롬프트의 여러 부분에 동시에 접근해야 하는 복잡한 작업에서는 효과적이지 않음.
2. Recursive Language Models (RLM)
2.1 핵심 아이디어
프롬프트를 신경망에 직접 입력하지 않고, 외부 환경의 일부로 취급한다.
RLM은 프롬프트를 프로그래밍 환경의 변수로 설정,
LLM이 코드를 작성하여 프롬프트를 검사하고, 분해하며, 필요한 부분에 대해 재귀적으로 자신을 호출할 수 있게 합니다.
기존 LLM 호출: ┌─────────────────────────────────────────────────────────┐ │ │ │ [매우 긴 프롬프트 전체] ──→ LLM ──→ [응답] │ │ │ │ ※ 전체 프롬프트가 컨텍스트 윈도우 내에 있어야 함 │ │ │ └─────────────────────────────────────────────────────────┘ RLM 호출: ┌─────────────────────────────────────────────────────────┐ │ │ │ [질문만] ──→ LLM (코드 작성) ──→ [코드 실행] │ │ │ │ │ │ │ ▼ │ │ │ 프롬프트 검사/분해 │ │ │ │ │ │ │ ▼ │ │ │ RLM(부분1), RLM(부분2) │ │ │ │ │ │ ▼ ▼ │ │ [중간 결과 통합] ──→ [최종 응답] │ │ │ │ ※ 프롬프트는 환경 변수로 존재, LLM은 코드로 접근 │ │ │ └─────────────────────────────────────────────────────────┘2.2 REPL 환경 기반 아키텍처
RLM은 REPL(Read-Eval-Print Loop) 프로그래밍 환경을 사용합니다:
# RLM 의사코드 class RLMEnvironment: """RLM 실행 환경.""" def __init__(self, prompt: str, question: str): self.prompt = prompt # 환경 변수로 저장 self.question = question # 질문 self.results = {} # 중간 결과 저장 def run(self) -> str: """RLM 실행.""" # LLM에게 코드 생성 요청 (프롬프트 전체가 아닌 질문만 전달) code = self.llm_generate_code(f""" 당신은 다음 질문에 답해야 합니다: {self.question} 환경에는 'prompt' 변수에 긴 텍스트가 저장되어 있습니다. 필요한 정보를 얻기 위해 다음 함수들을 사용할 수 있습니다: - inspect(prompt, start, end): 프롬프트의 특정 범위 조회 - search(prompt, query): 프롬프트에서 관련 부분 검색 - rlm_call(sub_prompt, sub_question): 재귀적 RLM 호출 코드를 작성하여 질문에 답하세요. """) # 생성된 코드 실행 result = self.execute_code(code) return result2.3 핵심 연산
RLM이 사용하는 세 가지 핵심 연산:
연산 기능 용도 Inspect 프롬프트의 특정 범위 조회 관심 영역 상세 확인 Search 프롬프트에서 관련 부분 검색 필요한 정보 위치 파악 Recursive Call 하위 문제에 대해 RLM 재호출 분할 정복 # 핵심 연산 예시 def inspect(prompt: str, start: int, end: int) -> str: """프롬프트의 특정 범위 조회.""" return prompt[start:end] def search(prompt: str, query: str) -> list[tuple[int, int, str]]: """프롬프트에서 관련 부분 검색.""" # 의미론적 검색 또는 키워드 검색 results = semantic_search(prompt, query) return [(start, end, snippet) for start, end, snippet in results] def rlm_call(sub_prompt: str, sub_question: str) -> str: """재귀적 RLM 호출.""" sub_env = RLMEnvironment(sub_prompt, sub_question) return sub_env.run()
3. 작동 원리
3.1 분할 정복 (Divide and Conquer)
RLM은 분할 정복 전략을 사용하여 긴 프롬프트를 처리합니다:
1백만 토큰 프롬프트 처리: ┌──────────────────────────────────────────────────────────────────┐ │ │ │ [1M 토큰 프롬프트] │ │ │ │ │ ▼ │ │ RLM: "프롬프트를 4개로 나누고 각각에서 관련 정보를 찾자" │ │ │ │ │ ┌────┴────┬────────┬────────┐ │ │ ▼ ▼ ▼ ▼ │ │ RLM(250K) RLM(250K) RLM(250K) RLM(250K) │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ 결과1 결과2 결과3 결과4 │ │ │ │ │ │ │ │ └────┬────┴────────┴────────┘ │ │ ▼ │ │ RLM: "4개 결과를 통합하여 최종 답변 생성" │ │ │ │ │ ▼ │ │ [최종 응답] │ │ │ └──────────────────────────────────────────────────────────────────┘3.2 재귀 깊이와 비용
RLM의 시간 복잡도는 재귀 깊이에 따라 달라집니다:
재귀 깊이 분석: ───────────────────────────────────────────────────────── 입력 크기 | 컨텍스트 윈도우 | 재귀 깊이 | 총 LLM 호출 ───────────────────────────────────────────────────────── 128K | 128K | 1 | 1 256K | 128K | 2 | ~3 512K | 128K | 3 | ~7 1M | 128K | 4 | ~15 ───────────────────────────────────────────────────────── ※ 재귀 깊이 = log₂(입력 크기 / 컨텍스트 윈도우) ※ 비용은 증가하지만, 기존 방식으로는 불가능한 작업을 수행 가능3.3 Context Rot 해결
RLM은 각 호출에서 컨텍스트 윈도우의 신선한 부분만 사용하여 Context Rot을 방지합니다:
기존 LLM (Context Rot 발생): ┌────────────────────────────────────────────────────────┐ │ [처음 부분] ───────────── [중간 부분] ───── [끝 부분] │ │ ◀── 이 부분에 대한 주의력 저하 ──▶ │ │ │ │ ※ 중간 부분의 정보가 "잊혀짐" │ └────────────────────────────────────────────────────────┘ RLM (Context Rot 방지): ┌────────────────────────────────────────────────────────┐ │ 호출 1: [처음 부분만] ──→ 신선한 컨텍스트 │ │ 호출 2: [중간 부분만] ──→ 신선한 컨텍스트 │ │ 호출 3: [끝 부분만] ──→ 신선한 컨텍스트 │ │ │ │ ※ 각 호출에서 해당 부분에 완전한 주의력 유지 │ └────────────────────────────────────────────────────────┘
4. 실험 결과
4.1 성능 비교
논문에서 보고된 실험 결과:
작업 기존 LLM RLM 개선율 128K 문서 Q&A 78% 89% +14% 256K 문서 Q&A 52% 85% +63% 512K 문서 Q&A 31% 81% +161% 1M 문서 Q&A 불가능 76% - 4.2 주요 발견
- 컨텍스트 윈도우 2배 초과 처리 가능: 128K 윈도우 모델이 256K+ 입력 처리
- 짧은 프롬프트에서도 성능 향상: 분할 정복이 복잡한 추론에 도움
- 비용 효율성: 총 토큰 사용량 증가하지만, 불가능→가능으로 전환
- 일반화: 다양한 장문 컨텍스트 작업에서 효과적
5. 적용 사례
5.1 Chat 서비스 적용 가능성
RLM 개념을 Chat 서비스의 긴 문서 분석에 적용할 수 있습니다:
# Chat 서비스에서의 RLM 적용 예시 class DocumentAnalysisNode: """긴 문서 분석을 위한 RLM 스타일 노드.""" async def execute( self, state: ChatState, writer: StreamWriter, ) -> ChatState: document = state["document"] # 긴 문서 question = state["message"] # 사용자 질문 if len(document) <= self.context_limit: # 짧은 문서: 직접 처리 answer = await self._direct_answer(document, question) else: # 긴 문서: RLM 스타일 분할 처리 answer = await self._recursive_answer(document, question, writer) return {**state, "answer": answer} async def _recursive_answer( self, document: str, question: str, writer: StreamWriter, ) -> str: """RLM 스타일 재귀 처리.""" # 1. 문서 분할 chunks = self._split_document(document) # 2. 각 청크에서 관련 정보 추출 partial_results = [] for i, chunk in enumerate(chunks): writer({ "type": "progress", "stage": "analysis", "status": "processing", "message": f"📄 문서 분석 중... ({i+1}/{len(chunks)})", }) # 재귀 호출 (청크가 여전히 크면) if len(chunk) > self.context_limit: result = await self._recursive_answer(chunk, question, writer) else: result = await self._direct_answer(chunk, question) if result: partial_results.append(result) # 3. 결과 통합 writer({ "type": "progress", "stage": "synthesis", "status": "processing", "message": "🔗 결과 통합 중...", }) final_answer = await self._synthesize(partial_results, question) return final_answer5.2 LangGraph 통합
RLM을 LangGraph의 서브그래프로 구현할 수 있습니다:
# LangGraph에서의 RLM 서브그래프 def create_rlm_subgraph( llm: LLMPort, context_limit: int = 100_000, ) -> StateGraph: """RLM 스타일 문서 분석 서브그래프.""" async def should_recurse(state: RLMState) -> str: """재귀 필요 여부 판단.""" if len(state["document"]) <= context_limit: return "direct_answer" return "split_and_recurse" async def split_node(state: RLMState) -> RLMState: """문서 분할.""" chunks = split_document(state["document"], context_limit) return {**state, "chunks": chunks, "results": []} async def process_chunk_node(state: RLMState) -> RLMState: """청크 처리 (재귀 가능).""" current_chunk = state["chunks"][len(state["results"])] # 재귀 호출 (서브그래프 내에서) if len(current_chunk) > context_limit: result = await rlm_graph.ainvoke({ "document": current_chunk, "question": state["question"], }) else: result = await llm.answer(current_chunk, state["question"]) return {**state, "results": state["results"] + [result]} async def synthesize_node(state: RLMState) -> RLMState: """결과 통합.""" final = await llm.synthesize(state["results"], state["question"]) return {**state, "answer": final} # 그래프 구성 graph = StateGraph(RLMState) graph.add_node("split", split_node) graph.add_node("process_chunk", process_chunk_node) graph.add_node("synthesize", synthesize_node) graph.add_node("direct_answer", direct_answer_node) graph.set_entry_point("check") graph.add_conditional_edges("check", should_recurse) graph.add_edge("split", "process_chunk") graph.add_conditional_edges( "process_chunk", lambda s: "process_chunk" if len(s["results"]) < len(s["chunks"]) else "synthesize" ) graph.add_edge("synthesize", END) graph.add_edge("direct_answer", END) return graph.compile()
6. 한계와 고려사항
6.1 한계
한계 설명 비용 증가 재귀 호출로 인해 총 토큰 사용량 증가 지연 시간 순차적 재귀 호출로 인한 지연 구현 복잡성 REPL 환경 및 코드 실행 환경 필요 오류 전파 하위 호출의 오류가 상위로 전파 6.2 적용 시 고려사항
- 비용-성능 트레이드오프: 중요한 작업에만 RLM 적용
- 병렬화: 독립적인 청크는 병렬 처리하여 지연 최소화
- 캐싱: 동일 청크에 대한 결과 캐싱
- Fallback: RLM 실패 시 기존 방식으로 폴백
7. 결론
7.1 핵심 인사이트
- 프롬프트의 재해석: 프롬프트는 신경망 입력이 아닌 환경 변수
- 프로그래밍적 접근: LLM이 코드를 작성하여 프롬프트 처리
- 분할 정복: 복잡한 문제를 하위 문제로 분해
- Context Rot 해결: 각 호출에서 신선한 컨텍스트 사용
7.2 Chat 서비스에의 시사점
- 긴 문서 분석: 배출 규정 전체 문서 분석 시 RLM 패턴 적용 가능
- 멀티턴 대화: 긴 대화 히스토리 처리에 활용
- RAG 개선: 단순 청크 검색 대신 재귀적 분석 적용
8. 참고 자료
'이코에코(Eco²) > Foundations' 카테고리의 다른 글
FLP Impossibility: 분산 합의의 불가능성 (0) 2025.12.28 Sharding & Routing: 분산 데이터 파티셔닝과 라우팅 (0) 2025.12.27 Consensus Algorithms: 분산 합의 알고리즘 (0) 2025.12.27 Redis Streams (0) 2025.12.25 Idempotent Consumer: 중복 메시지 처리 패턴 (0) 2025.12.25