본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 23. 05:45

LLM은 ALU(산술 논리 장치)이다

요약

LLM을 단순한 프로세서가 아닌 산술 논리 장치(ALU)로 정의하며, 모델의 규모 확장만으로는 해결할 수 없는 상태 유지 및 제어 메커니즘의 부재를 지적합니다. 에이전트 설계 시 LLM의 한계를 이해하고 외부에서 프로그램 카운터와 같은 상태 관리 기능을 결합해야 함을 강조합니다.

핵심 포인트

  • LLM은 본질적으로 상태를 유지하지 못하는 ALU와 유사한 특성을 가짐
  • 모델의 파라미터 증가는 연산 능력은 높이지만 프로그램 카운터 문제를 해결하지 못함
  • 효율적인 에이전트 구축을 위해서는 외부의 상태 관리 및 제어 메커니즘 결합이 필수적임
  • 단순히 컨텍스트를 늘리는 것만으로는 에이전트의 절차적 오류를 근본적으로 해결할 수 없음

왜 당신의 AI 에이전트에게 ZX Spectrum으로 게임을 만들었던 사람이 필요한가

몇 주 전, 나는 내 에이전트에게 왜 특정 습관이 낭비적인지 설명하던 도중, 그 습관이 실제로 발동되는 것을 목격했다. 우리는 대화 자체에서 "이것을 기억해(Remember This)"라고 불리는 기술이 데이터베이스 쓰기(database write) 한 번으로 끝날 일을 처리하기 위해 어떻게 네 번의 LLM 라운드 트립(round-trips)을 소모하고 있는지에 대해 논의하고 있었다. 내가 그 문장을 타이핑하는 동안 트리거가 일치했고, 그 일은 다시 반복되었다. 로봇 메시지, 세 번의 도구 호출(tool calls), 요약, 그리고 마침내 — 네 번의 턴이 지난 후에야 — 해당 사실이 저장소에 기록되었다. 런타임(runtime)은 모델이 반응할 대화가 있다는 사실을 알기도 전에 필요한 모든 데이터를 이미 가지고 있었다. 그것은 마치 CD 플레이어의 "전원" 버튼을 누르기 위해 콘서트 피아니스트를 고용하고, 그가 아주 감정을 담아 버튼을 누르는 것을 지켜보는 것과 같았다.

나는 웃음을 터뜨렸고, 곧 웃음을 멈췄다. 왜냐하면 문제의 형태를 알아차렸기 때문이다. 전에도 본 적이 있는 문제였지만, 단지 이런 모습(costume)으로 나타난 적은 없었을 뿐이다.

이 기계는 실제로 어떤 종류의 기계인가

대규모 언어 모델(LLM)을 생각할 때 본능적인 방식은 느리고 때때로 신뢰할 수 없는 프로세서(processor)로 간주하는 것이다. CPU에 더 최적화된 코드를 주는 것처럼 더 나은 프롬프트(prompt)를 제공하고, RAM을 더 많이 주는 것처럼 더 많은 컨텍스트(context)를 제공하며, 클록 속도(clock-speed) 향상을 기다리는 것처럼 다음 모델 세대를 기다린다면, 거친 부분들 — 망각, 주의력 분산, 절차의 네 번째 도구 호출(tool call)에서 흐름을 놓치는 경향 등 — 이 스스로 다듬어질 것이라고 생각한다. 이것이 일반적인 견해라고 생각하지만, 이는 중요한 측면에서 틀린 생각이다. 왜냐하면 이 견해는 LLM이 더 많은 리소스로 해결할 수 있는 종류의 결함을 가진 기계라고 가정하기 때문이다. 그렇지 않다. LLM의 결함은 부수적인 것이 아니라 정의상(definitional) 발생하는 것이다.

CPU에는 프로그램 카운터(program counter)가 있습니다. 레지스터가 있습니다. 누군가 지켜보든 말든, 명령어 하나하나를 따라 스스로 진행되는 인출-해독-실행(fetch-decode-execute) 사이클이 있습니다. 이 중 그 어떤 것도 지능을 필요로 하지 않습니다. 2달러짜리 마이크로컨트롤러(microcontroller)에도 이 모든 기능이 들어 있습니다. 필요한 것은 _연산 사이에 지속되는 상태(state that persists between operations)_와, 그 상태를 바탕으로 다음에 무엇을 할지 결정하는 메커니즘입니다. LLM에는 이 두 가지가 모두 없습니다. 토큰 행렬(token matrix)을 건네주면 토큰 행렬을 돌려받고, 호출이 반환되는 순간

그리고 바로 그 지점이 "다음 모델을 기다려라"라는 말이 범주 오류 (category error)인 이유입니다. 더 빠르고 더 똑똑한 ALU라 할지라도 여전히 ALU일 뿐입니다. 행렬 곱셈 (matrix multiply)을 더 크게 만들고, 컨텍스트 윈도우 (context window)를 더 길게 하며, 다음 토큰 예측 (next-token prediction)을 더 날카롭게 만들면, 산술 오류가 줄어들고 피연산자 (operand)의 범위가 넓어지는 등 확실히 더 잘 작동하는 ALU를 얻게 될 것입니다. 하지만 아무리 한계까지 밀어붙여도 얻을 수 없는 것이 있는데, 바로 프로그램 카운터 (program counter)입니다. 프로그램 카운터는 더 많은 파라미터 (parameters)를 통해 규모를 키워 존재하게 만들 수 있는 역량이 아니기 때문입니다. 그것은 외부에서 결합되는 전혀 다른 종류의 것입니다. 주의력 결핍 (attention deficit), 컨텍스트 팽창 (context bloat), 10번의 턴 이전에 주어진 명령을 잊어버리는 습관 — 이것들은 LLM(또는 ALU)의 버그가 아닙니다. 이것들은 상태가 없는 함수 (stateless function)에게 매번 유일한 입력 슬롯을 통해 전체 머신의 상태를 다시 로드하여 상태 머신 (state machine)처럼 행동하도록 요구하고, 그 과정에서 아무것도 누락되지 않기를 바라는 데서 발생하는 완전히 예측 가능한 결과입니다. 더 큰 컨텍스트 윈도우는 그 슬롯을 더 크게 만들 뿐입니다. 그것은 ALU에게 현재 활발하게 전달받고 있지 않은 것들을 보관할 장소를 제공하지 않습니다.

프로세서의 나머지 부분을 구축하기

저는 이 문제를 외면할 수 없는 기계들 사이에서 자랐습니다. 하드웨어가 "계산하는 부분"과 "기억하고 순서를 제어하는 부분" 사이의 경계를 실리콘 상에, 그리고 아침 식사를 하며 읽을 수 있는 데이터 시트 상에 완전히 명시적으로 만들어 두었기 때문입니다. 그래서 제가 LLM 문제의 형태를 인식했을 때, 발동된 본능은 "프롬프트를 더 잘 작성하라"가 아니었습니다. 훨씬 더 오래된 본능이었습니다. 만약 연산 코어 (compute core)에 자체적인 상태와 시퀀싱 (sequencing)이 없다면, 1970년대 이후 모든 프로세서 설계자들이 해왔던 방식처럼 코어 주변에 그것을 구축해야 한다는 것이었습니다. 그렇게 하면 정확히 어떻게 해야 하는지에 대해 60년 동안 어렵게 얻은 교훈들을 가져다 쓸 수 있습니다.

이전 생애에서, 저는 2001년에 33MHz ARM7에서 실행되는 커널을 직접 구축했던 Pogo라는 초기 휴대폰 작업을 수행한 적이 있습니다. 당시 전체 컨텍스트 스위치 (Context Switch)는 단 6개의 명령어로 이루어졌습니다. 모든 워킹 레지스터 (Working Register)를 컨텍스트 블록 (Context Block)에 푸시(push)하고, 글로벌 포인터 (Global Pointer)를 교체한 뒤, 새로운 태스크의 레지스터를 다시 팝(pop)하는 방식이었습니다. 마이크로초 단위의 작업이었죠. 이것이 가능했던 이유는 ARM7이 여러 레지스터의 읽기/쓰기를 위한 단일 명령어를 가지고 있었기 때문입니다. 즉, 하드웨어가 이미 작업의 절반을 끝내 놓은 상태였고, 우리는 이를 더욱 활용했습니다. 인터럽트 (Interrupt)를 빠져나갈 때 하나의 레지스터를 의도적으로 복구하지 않은 채 남겨둠으로써, 실제 스위치를 수행하는 C 루틴이 해당 레지스터를 통해 반환 값을 몰래 전달할 수 있도록 했습니다. 이 중 그 어떤 것도 사람들이 말하는 소위 "영리한 코드 (Clever Code)"와 같은 의미의 영리함은 아니었습니다. 그것은 더 오래된 의미에서의 영리함이었습니다. 즉, 어떤 상태가 존재하는지, 그것이 정확히 어디에 사는지, 그리고 언제 그것을 건드리는 것이 안전한지를 정확히 알고, 그것을 이동시키는 데 필요한 절대적인 최소한의 작업만을 수행하는 것이었습니다.

첫 번째 작업은 언제나 "상태가 없는 코어 (Stateless Core)에 상태를 보관할 장소를 제공하고, 다음에 무엇을 할지 알려주는 카운터를 제공하는 것"이라는 사실을 내면화하고 나면, 나머지 프로세서 (Processor)의 역사는 박물관의 전시물이라기보다 여러분이 곧 마주하게 될 문제들의 체크리스트처럼 읽히게 됩니다. 스택 (Stack)과 콜 프레임 (Call Frame)은 코드가 서브루틴 (Subroutine)을 호출하고 다시 돌아오고 싶어 하는 순간 발생하는 현상입니다. 즉, 복귀 주소 (Return Address)를 기억할 장소와 이를 안정적으로 중첩(Nesting)할 규율이 필요하게 됩니다. 인터럽트 (Interrupt)는 실행 중인 프로그램이 요청하지 않았음에도 키 입력, 타이머, 들어오는 패킷과 같이 시퀀스 외부의 무언가가 다음에 실행될 것을 변경해야 하는 순간 발생하는 현상입니다. 상태를 저장하고, 핸들러 (Handler)로 벡터링(Vectoring)하고, 복구한 뒤 재개합니다. 여기서 핵심은 그 저장/복구 과정이 매우 저렴하여(Cheap) 끊임없이 수행할 수 있을 만큼 효율적이어야 한다는 점입니다. 메모리 뱅킹 (Memory Banking)은 주소 공간 (Address Space)이 해결해야 할 문제보다 작을 때 발생하는 현상입니다. 따라서 고정된 크기의 프레임 안에 고정된 크기의 윈도우 (Window)를 교체하며 넣고 빼는 방식을 취하며, 한동안 프로그램의 절반이 나머지 절반에게 보이지 않는 상태가 되는 것을 받아들이게 됩니다.

MMU(Memory Management Unit, 메모리 관리 장치)는 그러한 스와핑 (swapping)을 수동으로 관리하는 것에 지쳐, 이를 투명하게 수행하는 하드웨어로 넘겨버렸을 때 발생하는 것입니다. 프로그램은 단순히 주소에 접근하고, 만약 적절한 페이지 (page)가 상주하고 있지 않다면, 결함 핸들러 (fault handler)가 이를 가져오며 프로그램은 아무 일도 일어나지 않은 것처럼 느끼게 됩니다.

그리고 프로세서가 정리되고 나면, 그 위의 운영체제 (operating system)는 이후 20년 동안 고난을 통해 배운 자신만의 병렬적인 교훈들을 갖게 됩니다. 즉, 모든 것이 스택 (stack) 규율 안에 깔끔하게 들어맞지는 않으며 일부 할당은 그것을 생성한 호출 (call)보다 실제로 더 오래 지속되기 때문에 필요한 힙 관리 (heap management); 장치가 5MB/s 속도의 하드 디스크이든 RAM 캐시이든, 두 장치가 동일한 프로토콜을 사용하는 한 그 위의 소프트웨어가 작동하기를 원하기 때문에 필요한 하드웨어 추상화 (hardware abstraction); 단지 레지스터 (register) 수준이 아닌 프로세스 수준에서의 적절한 컨텍스트 스위칭 (context switching) — 왜냐하면 하나의 CPU를 공유하는 두 프로그램은 마치 자신만이 CPU를 독점하고 있다는 환상이 필요하기 때문입니다; 그리고 결국 컴파일러 (compiler)입니다. 왜냐하면 동일한 절차적 패턴을 수동으로 열 번쯤 작성하고 나면, 의도를 명령어로 직접 번역하는 것을 멈추고 이를 대신 해줄 무언가를 단 한 번 구축하게 되며, 이를 통해 향후 모든 실행은 재도출 (re-deriving)의 비용이 아닌 실행 비용만을 지불하게 되기 때문입니다.

이 중 그 어느 것도 프로세서가 더 똑똑해질 것을 요구하지 않았습니다. 8080이 산술 연산을 더 잘하게 되어 8086이 된 것이 아닙니다. 8080에서의 곱셈은 대부분 8086에서도 여전히 곱셈입니다. 8086이 더 유용해진 이유는 그 주변에 더 많은 것이 구축되었기 때문입니다. 더 많은 상태 (state), 그 상태를 순차적으로 처리하고 인터럽트 (interrupt) 하는 더 많은 방법들, 그리고 결국 실리콘 (silicon)에 대해 전혀 생각하지 않아도 되게 해주는 전체 추상화 계층 (abstraction layer)이 구축되었습니다. 이 교훈은 정확히 전이됩니다. 왜냐하면 그것이 메우고 있는 간극의 형태가 정확히 같기 때문입니다.

그래서 이것이 하네스 (harness)에 의미하는 바는 무엇인가

이것이 제가 단순히 불평하는 대신 실제로 답하기 시작한 질문이며, 그 답은 matbot입니다. 이는 앞서 언급한 오래된 교훈들이 단순한 장식이 아니라 실제 작업을 수행하며 나타나는 에이전트 프레임워크 (agent framework)입니다.

LLM이 가지지 못한 상태는 **스토어 (stores)**에 존재합니다. 이는 플러그인이 모델을 거치는 왕복 과정(round-trip) 없이 직접 읽고 쓸 수 있는, 타입이 지정되고 주소 지정이 가능하며 내구성을 갖춘 컬렉션이지만, 모델은 엄격한 프로토콜을 통해 이에 접근할 수 있습니다. 세션 히스토리 (Session history)는 누산기 (accumulator) 역할을 합니다. 즉, 다음 호출 시 항상 다시 전달되는 유일한 상태 조각입니다. 이것이 이 분야 전체에서 컨텍스트 관리 (context management)가 핵심적인 비용 문제인 가장 큰 이유이며, 이는 더 많은 파라미터 (parameters)로 해결될 수 있는 문제가 아닙니다. 이는 운영체제 (OS)가 힙 (heap)을 관리하는 것과 마찬가지로, 세션이 어떻게 관리되는지의 문제입니다.

프로그램 카운터 (program counter)는 단순한 코드입니다. 모델이 수행할 수 없는 fetch-decode-execute 작업을 수행하는 평범하고 지루하며 디버깅 가능한 TypeScript 코드입니다. 즉, 다음에 무엇이 일어날지 결정하고, 루프를 돌고, 분기하며, 진정한 판단이 필요한 바로 그 순간에 모델을 호출하되, 그보다 한 단계 앞서거나 뒤에서 호출하지 않도록 하는 작업입니다. **훅 (Hooks)**은 인터럽트 라인 (interrupt lines)입니다. 메시지가 도착하거나, 턴 (turn)이 완료되거나, 도구 (tool)가 결과를 반환하는 등의 이벤트가 발생하면 등록된 핸들러 (handlers)가 동기적이고 결정론적으로 실행되며, 핸들러가 필요하다고 판단하는 경우에만 모델이 관여합니다. **트리거 (Triggers)**는 벡터링 (vectoring)을 추가합니다. 주변 장치의 인터럽트 활성화 비트 (interrupt-enable bit)가 요청이 컨트롤러에 도달할지 여부를 결정하는 방식처럼, 특정 조건(저렴하고 단일 턴이며 컨텍스트가 필요 없는 분류기 호출)이 인터럽트를 발생시킬지 여부를 결정합니다.

그 모든 것이 존재하기 전에는, 모델의 의도를 강제하기 위해 대화 속에 가짜 메시지를 작성하고 모델이 이를 알아차리기를 바라는 방식을 사용했습니다. 즉, 실제로는 하네스(harness)가 사용자를 대신해 말한 것이지만, 마치 사용자가 방금 무언가를 말한 것처럼 보이는 '로보 메시지(robo message)'를 사용하는 방식이었습니다. 이는 1980년대의 특정 트릭들이 작동했던 방식과 유사하게 작동했습니다. 이는 자기 수정형 트램펄린(self-modifying trampoline)으로의 JMP와 같았습니다. 인덱스 간접 점프(indexed indirect jump)가 필요했지만 ROM에서 실행 중이었고, 어쨌든 CPU 설계자가 해당 명령어를 누락했기 때문입니다. 수동으로 복귀 주소(return address)를 설정하고, RAN의 해당 루틴으로 점프한 뒤, 돌아오는 길에 스택(stack)을 망가뜨리는 것이 없기를 바라는 식이었죠. 실제 문제는 모델의 자체 API가 출력 스트림(output stream)에 그 어떤 것도 주입할 수 있는 방법을 제공하지 않았다는 점이며, 이는 CPU 설계자가 복잡한 점프 명령어를 신경 쓰지 않았던 것과 같습니다. 인터럽트 벡터 테이블(interrupt vector table)을 패치할 수도 없었는데, 왜냐하면 벡터 테이블이 기능적으로 마스크 ROM(mask ROM), 즉 이 경우에는 LLM의 학습 데이터에 구워져(baked into) 있었기 때문입니다. API가 코드에 도구 호출(tool call)을 직접 주입할 수 있게 된 순간, 로보 메시지는 임시 방편(workaround)에서 박물관 전시품이 되었습니다. 2025년경의 기술 트리거 시스템(skill trigger system), 수작업으로 만든 트램펄린을 주목하십시오. 그 나름대로 우아했습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0