LLM은 ALU이다 - ZX Spectrum으로부터 얻은 교훈
요약
LLM 에이전트 설계 시 발생하는 토큰 소모와 지연 시간 문제를 아키텍처 관점에서 분석합니다. LLM을 상태를 유지하는 프로세서가 아닌 산술 논리 장치(ALU)로 정의하며, 에이전트 프레임워크의 비효율성을 해결하기 위한 새로운 접근법을 제시합니다.
핵심 포인트
- 도구 호출 시 발생하는 데이터 직렬화와 토큰화는 막대한 비용을 초래함
- LLM은 상태를 유지하지 않는 ALU(산술 논리 장치)와 유사한 특성을 가짐
- 상태가 없는 함수에 상태 머신 역할을 기대하는 것은 아키텍처적 오류임
- 단순히 모델의 성능 향상만으로는 에이전트의 구조적 결함을 해결할 수 없음
당신의 LLM 에이전트가 도구(tool)를 호출하고 그 결과를 읽을 때마다, 당신은 세금을 지불하고 있습니다. 달러가 아니라 — 토큰(tokens), 지연 시간(latency), 그리고 신뢰성(reliability) 측면에서 말이죠 (네, 결국 달러이기도 합니다). 도구의 출력값은 텍스트로 직렬화(serialized)되고, 컨텍스트 윈도우(context window)로 토큰화(tokenized)되며, 모델에 의해 어텐션(attended to)을 받은 뒤, 모델이 다음 행동을 결정할 때 다시 직렬화됩니다. 만약 다음 단계가 또 다른 도구 호출이라면, 이 사이클은 반복됩니다. 모델의 판단이 전혀 필요하지 않은 데이터가 당신의 시스템에서 가장 비싼 구성 요소를 통해 두 번이나 흐르게 됩니다.
이것은 사소한 비효율성이 아닙니다. 이는 모든 에이전트 프레임워크(agent framework)의 숨겨진 비용 센터이며, 복리로 증가합니다. 에이전트가 더 많은 도구를 사용할수록 데이터 운송에 더 많은 컨텍스트를 소모하게 되고, 실제 작업에 대한 신뢰성은 떨어지며, 실패했을 때 무엇이 잘못되었는지 관찰하기는 더 어려워집니다.
저는 이것이 새로운 옷을 입은 오래된 문제이며 해결이 필요하다는 결론을 내리기 전까지, 제 자신의 에이전트에서 이런 일이 발생하는 것을 너무 오랫동안 지켜보았습니다. 에이전트가 고장 난 것이 아니었습니다. 아키텍처(architecture)가 문제였습니다. 저는 상태가 없는 함수(stateless function)에게 상태 머신(state machine)처럼 행동하도록 요구하고 있었고, 그 환상이 깨질 때마다 토큰 세금을 지불하고 있었습니다.
이 기계는 실제로 어떤 종류의 기계인가
대규모 언어 모델(LLM)을 생각하는 본능적인 방식은 느리고 때때로 신뢰할 수 없는 프로세서(processor)로 보는 것입니다. CPU에 더 최적화된 코드를 주는 것처럼 더 나은 프롬프트(prompts)를 제공하고, RAM을 더 많이 주는 것처럼 더 많은 컨텍스트를 제공하며, 클럭 속도(clock-speed) 향상을 기다리는 것처럼 다음 모델 생성을 기다린다면, 거친 부분들 — 망각, 표류하는 어텐션(drifting attention), 절차의 네 번째 도구 호출쯤에서 맥락을 놓치는 경향 — 이 스스로 다듬어질 것이라고 생각합니다.
이것이 합의된 견해라고 생각하지만, 이는 중요한 측면에서 틀렸습니다. 왜냐하면 이 견해는 LLM이 더 많은 자원을 투입하면 해결될 수 있는 '종류'의 결함을 가진 기계라고 가정하기 때문입니다. 그렇지 않습니다. LLM은 부수적인 것이 아니라 정의상 발생하는 결함을 가지고 있습니다.
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)입니다. 왜냐하면 프로그램 카운터는 더 많은 파라미터 (parameter)를 통해 규모를 키워(scale) 존재하게 만들 수 있는 능력이 아니기 때문입니다. 그것은 외부에서 결합되어야 하는 완전히 다른 종류의 것입니다.
주의력 결핍 (Attention deficit), 컨텍스트 팽창 (context bloat), 10회 전의 지시사항을 잊어버리는 습관 — 이것들은 LLM의 버그가 아닙니다. 이것들은 상태가 없는 함수 (stateless function)에게 매번 유일한 입력 슬롯을 통해 기계의 전체 상태를 다시 로드함으로써 상태 머신 (state machine)처럼 행동하도록 요구할 때 발생하는, 완전히 예측 가능한 결과입니다. 그리고 그 과정에서 아무것도 누락되지 않기를 바라는 것에서 비롯됩니다. 더 큰 컨텍스트 윈도우는 그 슬롯을 더 크게 만들 뿐입니다. 그것은 ALU에게 현재 활발하게 전달받고 있지 않은 것들을 보관할 장소를 제공하지 않습니다.
토큰화 비용 (The tokenization tax), 구체적인 사례
다음은 에이전트가 실행할 수 있는 실제 파이프라인입니다: 특정 주제에 대한 최근 기사를 웹에서 검색하고, 상위 결과의 전체 텍스트를 추출하며, 주요 발견 사항을 집계하여 요약본을 생성합니다.
미숙한 방식 (The naive way) — 모든 에이전트 프레임워크가 기본적으로 수행하는 방식:
Turn 1: Agent calls web_search({ query: "LLM agent memory management" })
→ 도구 결과 (10개의 URL + 스니펫, 약 2,000 토큰)가 컨텍스트에 진입
→ 에이전트가 결과를 읽고, 팔로우할 5개의 URL을 선택
...
모델은 모든 중간 결과 — 모든 URL, 모든 스니펫, 모든 전체 기사 — 를 확인했습니다. 왜냐하면 그것이 다음에 무엇을 할지 결정할 수 있는 유일한 방법이었기 때문입니다. 하지만 모델은 그것들을 볼 필요가 없었습니다. 데이터 흐름은 순수하게 기계적이었습니다: 검색 → 추출 → 집계 → 요약. 단계 사이에는 어떠한 판단도 필요하지 않았습니다. 판단은 오직 마지막 단계인 요약 자체를 위해서만 필요했습니다.
구성된 방식 (The composed way): 에이전트는 전체 파이프라인을 오케스트레이션(orchestrate)하는 TypeScript 함수를 작성합니다. 검색 결과는 TypeScript 상태로 유지됩니다. 기사 텍스트도 TypeScript 상태로 유지됩니다. 집계(aggregation) 또한 TypeScript에서 이루어집니다. 유일한 LLM 호출은 요약 자체를 위한 단일하고 격리된 single_turn 호출뿐입니다. 이 호출은 자체적인 컨텍스트 윈도우 (context window)를 가지므로, 메인 대화가 가공되지 않은 기사 내용으로 오염되는 일이 전혀 없습니다. 최종 출력물인 요약, 소스 URL, 그리고 기사 개수만이 메인 모델이 보게 되는 유일한 정보입니다.
토큰 세금 (token tax)은 중간 결과물 약 25,000 토큰에서 최종 출력물 약 200 토큰으로 급감합니다. 하지만 이 이점은 단순히 토큰 수 감소 그 이상입니다.
이것이 왜 하네스 (harness)의 영역인가
matbot이 이 작업을 대신 해주기 때문입니다. matbot은 LLM의 계획과 세션을 코드로 변환하는 강력한 타입 지정(strongly typed) LLM 하네스 (harness)입니다. 그리고 코드를 작성하는 것은 LLM이 잘하는 일 중 하나입니다.
얻을 수 있는 것들
토큰 효율성 (Token efficiency). 가장 명백한 이점입니다. 모델의 판단을 필요로 하지 않고 도구(tool) 사이를 흐르는 데이터는 컨텍스트 윈도우 (context window)에 진입하지 않습니다. 위의 예시에서 25,000 토큰의 기사 내용은 200 토큰의 요약으로 변했습니다. 이것은 미미한 최적화가 아닙니다. 기계적인 단계들을 위한 컨텍스트 소비를 99% 감소시킨 것입니다.
관측 가능성 (Observability). 파이프라인의 모든 단계를 검사할 수 있습니다. 추출 전의 검색 결과를 로그로 남길 수 있고, 기사 수를 세거나, 어떤 URL이 성공하거나 실패했는지 추적할 수 있으며, 실제 디버거를 사용하여 집계 로직을 디버깅할 수 있습니다. 도구 호출 사이의 LLM의 "추론 (reasoning)"은 불투명하지만, TypeScript 파이프라인은 투명합니다.
신뢰성 (Reliability). 파이프라인은 매번 동일한 방식으로 실행됩니다. 검색은 항상 추출 전에 발생합니다. 집계는 항상 동일한 형식을 생성합니다. 모델이 단계를 건너뛰거나, URL을 환각 (hallucination)하거나, 모든 기사를 가져오기 전에 요약을 해버릴 위험이 없습니다.
재현성 (Repeatability). 동일한 입력, 동일한 파이프라인, 동일한 출력 구조를 가집니다. 유일한 비결정론적 (non-deterministic) 단계는 요약뿐이며, 이 단계는 별도의 LLM 호출로 격리되어 있으므로 비결정론적 요소가 제한적이고 테스트 가능합니다.
지불해야 하는 비용
엄격한 타이핑 (Strict typing). 구성 계층 (composition layer)은 모든 도구의 파라미터와 반환 값에 대해 TypeScript 타입을 요구합니다. 이것이 비용입니다. 하지만 이는 개발자가 아닌 프레임워크가 감수하는 비용입니다. matbot은 현재 로드된 실제 도구들로부터 실시간 타입 선언을 생성하므로, 타입 검사기 (type-checker)가 스텁 (stub)이 아닌 실제 계약 (contract)을 바탕으로 사용자의 구성을 검증합니다. LLM은 도구를 함수로 작성하고, 프레임워크는 그것이 타입 안전 (type-safe)하도록 보장합니다.
핵심 통찰: LLM은 코드를 실행하는 것보다 작성하는 데 훨씬 더 능숙합니다.
이것은 제가 에이전트 시스템을 구축하며 배운 가장 중요한 사실입니다. LLM은 절차를 보고 단 한 번에 정확하고 타입 안전한 TypeScript 구현을 생성할 수 있습니다. 하지만 동일한 LLM에게 컨텍스트 내에서 그 절차를 단계별로 _실행_하라고 요청하면, 흐름이 어긋나고, 단계를 건너뛰며, 토큰을 낭비하고, 신뢰할 수 없는 결과를 만들어냅니다.
강점에 집중하십시오. LLM을 인터프리터 (interpreter)가 아닌 컴파일러 (compiler)로 사용하십시오.
LLM이 제공할 수 없는 네 가지 (하지만 TypeScript는 가능한 것)
LLM에게 컨텍스트 내에서 다단계 절차를 실행하도록 요청할 때, 여러분은 구조적으로 LLM이 제공할 수 없는 네 가지를 요구하는 것입니다:
1. 신뢰성 (Reliability) — 매번 동일한 순서로 동일한 단계를 따를 것인가? 보장되지 않습니다. 모델의 어텐션 (attention)은 특히 긴 컨텍스트에서 흐트러집니다. 1회차에는 작동하던 절차가 5회차에는 조용히 단계를 건너뛸 수 있습니다.
2. 재현성 (Repeatability) — 동일한 입력에 동일한 출력인가? 대략적으로만 그렇습니다. 모델은 확률적 함수 (stochastic function)입니다. 동일한 절차를 두 번 실행하더라도 서로 다른 분기를 타거나, 다른 중간 결과물을 생성하거나, 서로 다른 방식으로 실패할 수 있습니다.
3. 관찰 가능성 (Observability) — 2단계와 3단계 사이에서 실제로 무슨 일이 일어났는가? 단계별 실행 (step through), 중단점 (breakpoint) 설정, 또는 중간 상태 (intermediate state) 검사가 불가능합니다. 모델의 "추론 (reasoning)"은 불투명합니다. 여러분은 입력과 출력만을 볼 수 있으며, 프로세스를 신뢰하거나 혹은 신뢰하지 못할 뿐입니다.
4. 토큰 경제 (Token economy) — 여러분의 절차(procedure)에 포함된 모든 분기(branch), 모든 조건문(conditional), 모든 단계 설명은 해당 분기가 실제로 실행되는지 여부와 관계없이 토큰을 소모합니다. 3개의 조건문이 포함된 10단계 워크플로우는, 그중 7개가 아무 작업도 하지 않는 no-op일지라도 매번 10단계 전체의 컨텍스트(context) 비용을 발생시킵니다.
TypeScript는 이 네 가지를 모두 무료로 제공합니다. 결정론적 실행 (Deterministic execution). 동일한 입력에 대한 동일한 출력. 모든 단계에서의 완전한 조사 가능성 (inspectability). 제어 흐름 (control flow)에 대한 토큰 비용 제로.
여기서 얻는 통찰은 LLM이 나쁘다는 것이 아닙니다. LLM은 한 가지 일에는 매우 탁월하지만 — 거대한 피연산자(operands)에 대한 단일 패스 변환 (single-pass transforms) — 구조적으로 다른 일에는 불가능하다 — 지속적이고 신뢰할 수 있으며 관찰 가능한 절차적 실행 (procedural execution) — 는 점입니다. 해결책은 더 나은 프롬프트가 아닙니다. 각 작업에 적합한 도구를 사용하는 것입니다.
나머지 프로세서 구축하기
저는 이 문제를 외면할 수 없는 기계들 사이에서 자랐습니다. 하드웨어가 "계산하는 부분"과 "기억하고 순서를 정하는 부분" 사이의 경계를 완전히 명시적으로 만들었기 때문입니다. 저는 48K RAM을 가진 ZX Spectrum으로 시작했습니다. 이전 생애에서는 33MHz ARM7에서 구동되는 Pogo라는 초기 휴대폰의 커널을 구축하기도 했습니다. 그곳에서의 완전한 컨텍스트 스위칭 (context switch)은 6개의 명령어로 이루어졌습니다. 모든 작업 레지스터를 컨텍스트 블록에 푸시(push)하고, 글로벌 포인터를 교체한 뒤, 새로운 태스크의 레지스터를 다시 팝(pop)하는 과정입니다. 마이크로초 단위의 작업이었죠. 이것이 가능했던 이유는 하드웨어가 이미 작업의 절반을 끝내 놓았기 때문입니다. 그리고 우리는 그것을 더욱 활용했습니다. 인터럽트(interrupt)를 빠져나올 때 레지스터 하나를 복구하지 않은 채 의도적으로 남겨둠으로써, 실제 스위칭을 수행하는 C 루틴이 그 레지스터를 통해 반환 값을 몰래 전달할 수 있도록 했습니다.
이 중 그 어떤 것도 사람들이 말하는 "영리한 코드 (clever code)" 방식의 영리함은 아니었습니다. 그것은 더 오래된 의미에서의 영리함이었습니다. 어떤 상태가 존재하는지, 그것이 정확히 어디에 있는지, 그리고 언제 그것을 건드리는 것이 안전한지를 정확히 알고, 그 상태를 이동하기 위해 필요한 최소한의 작업만을 수행하는 것 말입니다.
첫 번째 작업이 항상 "상태가 없는 코어(stateless core)에 상태를 저장할 장소를 제공하고, 다음에 무엇을 할지 알려주는 카운터를 제공하는 것"이라는 사실을 내재화하고 나면, 나머지 프로세서의 역사는 박물관의 전시물이라기보다 여러분이 곧 마주하게 될 문제들의 체크리스트처럼 읽히게 됩니다:
- 스택 (Stacks) 및 콜 프레임 (call frames) — 코드가 서브루틴 (subroutine)을 호출하고 다시 돌아오고 싶어 하는 순간
- 인터럽트 (Interrupts) — 시퀀스 외부의 무언가가 다음에 실행될 내용을 변경해야 하는 순간
- 메모리 뱅킹 (Memory banking) — 주소 공간 (address space, 또는 컨텍스트 윈도우 (context window))이 해결하려는 문제보다 작을 때
- MMU (Memory Management Units) — 그 스와핑 (swapping) 작업을 수동으로 관리하는 것에 지쳤을 때
- 힙 관리 (Heap management) — 모든 것이 스택 규율 (stack discipline) 안에 깔끔하게 들어맞지는 않기 때문에
- 하드웨어 추상화 (Hardware abstraction) — 장치 위의 소프트웨어가 장치와 상관없이 작동하기를 원하기 때문에
- 컨텍스트 스위칭 (Context switching) — 하나의 CPU를 공유하는 두 프로그램이 마치 자신들만 사용하는 것 같은 환상이 필요하기 때문에
- 컴파일러 (Compilers) — 동일한 절차적 패턴을 수동으로 열 번쯤 작성하다 보면, 그것을 대신 해줄 무언가를 만들게 되기 때문에
이 중 그 어느 것도 프로세서가 더 똑똑해질 것을 요구하지 않았습니다. 8080이 산술 연산 (arithmetic)을 더 잘하게 되어서 8086이 된 것이 아닙니다. 8080은 그 주변에 더 많은 것들이 구축되었기 때문에 더 유용해진 것입니다.
이것이 하네스 (harness)에 의미하는 바
이것은 제가 단순히 불평만 하는 대신 실제로 답하기 시작한 질문이며, 그 답은 matbot입니다. matbot은 위의 오래된 교훈들이 하나하나 실제 작업으로 나타나는 에이전트 프레임워크 (agent framework)입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기