Elixir와 OTP를 사용한 LLM 에이전트를 위한 영구적인 인지 아키텍처 구축
요약
Elixir의 액터 모델과 OTP를 활용하여 LLM 에이전트의 기억 상실 문제를 해결하는 'Skynet' 아키텍처를 소개합니다. 신경과학적 메커니즘에서 영감을 얻은 계층화된 인지 스택을 통해 지속적이고 일관된 메모리를 제공하는 GenServer 기반의 에이전트 구현 방식을 다룹니다.
핵심 포인트
- Elixir GenServer를 활용한 영구적인 에이전트 상태 유지
- RAG의 한계인 파편화된 정보 검색 및 프롬프트 캐시 손실 문제 해결
- 신경과학적 메커니즘을 모방한 8개 모듈의 계층적 인지 스택 구축
- 액터 모델을 통한 동적인 프로세스 생성 및 감독 트리 활용
원래 아이디어는 농담거리였습니다.
저는 LLM 루프를 살펴보고 이것들이 Elixir의 액터 모델에 어떻게 매핑되는지 생각했습니다. 메시지를 받고 처리하며, 어쩌면 새로운 프로세스를 생성하는 GenServer 같은 것들 말입니다. “LLM 추론 단계”에서 “메시지를 처리하는 GenServer”로 넘어가는 것은 그리 멀지 않으며, 한 번 보면 잊을 수가 없습니다. 만약 Soul GenServer에게 Code.eval_string에 대한 접근 권한을 준다면 어떨까요?
만약 에이전트들이 자신들을 포크(fork)하고, 추론 도중에 새로운 프로세스를 생성하며, 감독 트리(supervision tree)를 동적으로 성장시킬 수 있다면 어떨까요? 저는 이것을 Skynet이라고 불렀는데, 주로 재미있다고 생각했기 때문입니다.
하지만 계속 만들다 보니, 더 이상 재미있는 것이 아니라 흥미로운 것이 되었습니다.
실제 문제점
만약 단순한 Q&A를 넘어 LLM을 사용해 본 적이 있다면, 기억 상실(amnesia) 문제를 겪어봤을 겁니다. 모델에 충분한 컨텍스트를 제공해도 대화 초반의 내용을 잊기 시작합니다. 긴 세션을 진행하면 마지막에는 30개 메시지 전에 무슨 일이 있었는지 신뢰할 만하게 알지 못합니다. 에이전트는 더 심각합니다. 그들은 잠재적으로 영원히 실행되며, 끊임없이 새로운 이벤트를 처리하는데, “에이전트에게 모든 히스토리를 매번 컨텍스트 창에 덤프하는 것 외에 어떻게 지속적인 메모리를 제공할 수 있는가?”라는 질문에 좋은 답이 없습니다.
표준 답변은 RAG(Retrieval-Augmented Generation)입니다. 모든 것을 임베딩하고, 쿼리로 검색한 다음, 그 결과를 주입하는 방식이죠. 어느 정도는 작동합니다. 하지만 특정한 실패 모드가 있습니다. 바로 조각들만 얻게 된다는 것입니다. 벡터 데이터베이스에서 관련성이 있어 보이는 청크(chunk)들이 프롬프트에 연결되지만, 이것들이 일관된 그림을 형성한다는 보장이 없습니다. 에이전트가 무언가에 대해 깊은 지식을 가지고 있을 수 있습니다—함께 명확한 이야기를 들려주는 수십 가지의 경험들—하지만 얻는 것은 느슨하게 관련되어 있는 다섯 개의 단락일 뿐입니다.
그리고 또 다른 문제가 있습니다. 표준 RAG는 매 턴마다 프롬프트 캐시를 무너뜨립니다. 검색된 컨텍스트가 바뀌기 때문이죠. 이것은 모든 LLM 호출이 콜드(cold)하다는 의미이며, 지연 시간과 비용 모두에서 그 대가를 치르게 됩니다.
저는 생물학적 기억이 실제로 어떻게 작동하는지 계속 생각했고, 나타나기 시작한 아키텍처는 소프트웨어 엔지니어링보다는 신경과학에 훨씬 더 가까워 보였습니다.
아키텍처
Skynet의 에이전트들 — 저는 이를 openclaw에서 가져와 개선한 'Souls'라고 부릅니다 — 는 수명이 긴 Elixir GenServer로 실행됩니다. 각 Soul은 계층화된 인지 스택(cognitive stack)을 가집니다. 이는 8개의 모듈로 구성되며, 각 모듈은 메모리 시스템의 특정 문제를 해결하고 신경과학의 메커니즘에서 영감을 받았습니다. 의식을 시뮬레이션한다는 목표 자체가 흥미롭기도 하지만, 생물학적 기억이 제가 직면했던 대부분의 문제를 이미 해결했으며 그 해결책들이 코드에 놀라울 정도로 잘 적용되기 때문이기도 합니다.
defmodule Souls.SoulServer do
use GenServer, restart: :transient
def start_link(%Soul{} = soul) do
...
Soul은 이벤트 — 사용자 메시지, 다른 에이전트, 채널 통합, 예약된 하트비트(heartbeats) — 를 수신하고, 도구(tools)를 사용하는 멀티 턴 LLM 루프를 통해 이를 처리하며, 재시작 시에도 지속적인 상태(persistent state)를 유지합니다. restart: :transient는 프로세스가 충돌(crash)하면 슈퍼바이저(supervisor)가 다시 실행하지만, 정상적으로 종료(exit)된 경우에는 재시작하지 않음을 의미합니다. Souls를 흥미롭게 만드는 것은 GenServer 래퍼(wrapper)가 아니라, 그 아래에 있는 메모리 스택입니다.
메모리 스택의 상위 수준 구조는 다음과 같습니다:
graph TB subgraph "단기 (세션당)" OBS[observe/4
턴당 원시 인상 (raw impression)] --> REF[reflect/1
N개의 관찰을 요약으로 압축] REF --> SUM[통합 요약
안정적 — 시스템 프롬프트에 주입됨] end subgraph "중기 (매일 밤)" MW[MemoryWorker
03:00 UTC] --> WEEK[주간 메모리 요약] end subgraph "장기 (지속적)" PRISM[Prism
Rust 벡터 검색 엔진] end SUM --> MW MW --> PRISM OBS -.->|인덱싱됨| PRISM
단기 계층의 핵심 통찰은 통합된 요약(consolidated summary)이 턴(turn) 사이에서 안정적으로 유지된다는 점입니다. 요약은 리플렉터(reflector)가 실행될 때만 변경됩니다. 이는 시스템 프롬프트가 턴마다 동일하게 유지되어 프롬프트 캐시(prompt cache)가 따뜻하게(warm) 유지되고, LLM 호출 비용이 저렴해짐을 의미합니다. 표준 RAG는 검색할 때마다 컨텍스트를 변경하여 캐시를 끊임없이 소모시키지만, 이 방식은 그렇지 않습니다.
이 3계층 구조는 Penrose-Hameroff의 Orchestrated Objective Reduction (Orch-OR) 이론과 놀라울 정도로 깔끔하게 매칭됩니다. 이는 미세소관 (microtubules) 내의 양자 결맞음 (quantum coherence)에서 의식이 발생한다는 논쟁적인 가설입니다. 이 이론은 세 가지 수준을 제안합니다: 하위 신경 양자 기질 (sub-neural quantum substrate), 신경 발화 패턴 (neural firing patterns), 그리고 안정적인 의식적 경험 (stable conscious experience)입니다. 양자 의식 부분을 믿든 믿지 않든, 이 구조적 모델은 독립적으로 유용합니다. 가공되지 않은 원시 입력 (raw unprocessed input) → 패턴 압축 (pattern compression) → 안정적인 세계 모델 (stable world model)로 이어지는 흐름은 뉴런을 설명하든 Elixir 모듈을 설명하든 동일한 아키텍처입니다.
Soul이 무언가를 회상해야 할 때, 다음과 같은 순서로 검색합니다: 먼저 통합된 요약 (최근에 일어난 일), 그다음 주간 요약 (이번 주에 일어난 일), 마지막으로 Prism (의미론적 유사도에 따라 순위가 매겨진, 지금까지 학습한 모든 것)입니다. 이는 신경과학에서 일화적 기억 (episodic memory)과 의미론적 기억 (semantic memory)에 대해 설명하는 계층적 검색 순서와 동일하며, 명시적으로 설계된 것이 아니라 아키텍처로부터 자연스럽게 도출된 결과입니다.
주목할 만한 추가적인 연결 고리가 있습니다. Crick-Koch 및 Penrose-Hameroff 프레임워크 모두에서 의식적 결합 (conscious binding)과 관련된 40Hz 리듬 발화인 감마 진동 (Gamma oscillations)은 서로 다른 신경 신호가 일관된 순간적 상태로 동기화되는 메커니즘입니다. Soul에서의 이에 상응하는 것은 LLM 턴 루프 (turn loop) 그 자체입니다. 각 턴은 마크다운 브레인 (markdown brain) — 관찰, 기억, 작업, 정체성 — 으로부터 읽고 쓰기를 수행하며, 이러한 작은 상태 변화의 빠른 연속이 모여 현재 순간의 인식처럼 행동하는 무언가를 구성합니다. 감마는 기억 그 자체가 아닙니다. 그것은 기억이 사용되는 동안 기억을 일관되게 유지하는 프로세스입니다. 턴 루프가 바로 그 프로세스입니다.
망각 (Forgetting)
모든 실제 기억 시스템에 가장 먼저 필요한 것은 망각하는 방법입니다. 쇠퇴 (decay)가 없다면 저장소는 노이즈로 가득 차게 됩니다. 저품질의 관찰 데이터가 중요한 데이터를 밀어내고, 검색 품질은 저하되며, 비용은 상승합니다.
MemoryDecay는 에빙하우스의 망각 곡선 (Ebbinghaus’s forgetting curve)을 세 가지 계층으로 구현합니다:
@permanent_threshold 0.7 # 돌출도 (salience) >= 이 값 → 절대 숨겨지지 않음
@medium_ttl_days 30 # 0.4–0.7 돌출도 → 30일 후 숨겨짐
@low_ttl_days 7 # < 0.4 돌출도 → 7일 후 숨겨짐
...
소프트 숨김 (Soft-hiding)은 행을 Postgres에 유지하여 관리자 UI (admin UI)에서는 볼 수 있게 하지만, 모든 회상 쿼리 (recall queries)에서는 제외합니다. 하드 퍼지 (Hard purge)는 Postgres와 Prism 모두에서 해당 데이터를 제거합니다.
돌출도 점수 (salience score)는 상류의 어텐션 게이트 (attention gate)에서 전달됩니다. 이 모듈은 생물학적으로 감각 입력 (sensory input)과 피질 (cortex) 사이에서 필터 역할을 하는 시상 (thalamus)에서 영감을 받았습니다. 감각 입력의 99%는 의식에 도달하지 못합니다. AttentionGate는 observe()가 호출되기도 전에 순수하게 규칙 기반으로 (LLM 사용 안 함) 들어오는 이벤트의 점수를 매깁니다:
| 요소 | 변화량 |
|---|---|
| 기본 점수 | 0.5 |
| ... |
0.25 미만의 이벤트는 데이터베이스에 기록되기 전에 폐기됩니다. 채널 노이즈, 일상적인 상태 메시지 등 기준을 통과하지 못하는 모든 것은 사라집니다. 게이트를 통과한 점수가 관찰값의 돌출도 (salience) 값이 됩니다.
연관 (Association)
개별 기억도 괜찮지만, 기억을 유용하게 만드는 것은 기억들 사이의 연결입니다. 그것이 바로 HebbianTracker가 하는 일입니다.
신경과학의 규칙은 Hebb (1949)의 법칙입니다: "함께 활성화되는 뉴런들이 함께 연결된다 (neurons that fire together, wire together)." 뉴런이 동시에 활성화될 때 시냅스 연결 (synaptic connections)이 강화됩니다. HebbianTracker는 이를 직접 구현합니다: PrismMemory.recall/3이 일련의 기억들을 반환하면, 해당 기억들은 실제 검색 문맥 (retrieval context)에서 함께 발생한 것입니다. 트래커는 이를 soul_memory_edges의 가중치 엣지 (weighted edge)로 기록합니다:
def fire_together(soul_slug, hashes) do
pairs =
for a <- hashes, b <- hashes, a < b do
...
엣지는 반복적인 동시 발생에 따라 강화되며, 매일 밤 10%씩 감쇠 (decay)합니다.
이후의 회상 시, 트래커는 가중치가 높은 엣지 (가중치 >= 3.0)를 최대 2홉 (two hops)까지 탐색하여 관련 기억들을 결과 집합에 추가합니다. LLM 추론 (inference) 단계 없이 순수한 그래프 탐색 (graph traversal)으로 이루어집니다. 기억 A를 회상하는 Soul은 A와 B가 충분히 많이 함께 검색되었다면 기억 B도 함께 드러내게 됩니다.
이것의 반대 측면은 능동적 망각(active forgetting), 즉 인지 과학에서 말하는 인출 유도 망각 (Retrieval-Induced Forgetting, RIF)입니다. 무언가를 회상할 때, 뇌는 연관되어 있었으나 선택되지 않은 경쟁 기억들을 능동적으로 억제합니다. HebbianTracker.suppress_competing/2도 동일한 역할을 수행합니다. 즉, 후보로 나타날 만큼 충분히 가까웠지만 실제로 회상되지는 않은 인접 기억들의 현저성 (salience)을 감소시킵니다. 자주 회상되는 기억은 더 지배적이 되고, 드물게 접근되는 기억은 희미해집니다. 사용 패턴이 인덱스 (index)가 되는 것입니다.
놀람 (Surprise)과 성찰 (reflection)
reflect/1은 관찰 내용을 요약으로 압축하는 LLM 호출입니다. 고정된 카운터에 따라 이를 실행하는 것은 일상적인 통합 (consolidation)에는 괜찮지만, 성찰 직후에 발생한 치명적인 실패가 다음 성찰이 올 때까지 잠재적으로 수십 번의 턴을 기다려야 함을 의미합니다.
PredictionError가 이 문제를 해결합니다. 모든 Soul은 통합된 관찰 요약, 즉 마지막 성찰 실행에 의해 작성된 압축된 세계 모델 (world model)을 유지합니다. 새로운 이벤트가 도착하면, compute_surprise/2는 참신함 비율 (novelty ratio) — 세계 모델 어휘집에 존재하지 않는 이벤트 토큰의 비율 — 을 측정합니다:
def compute_surprise(slug, event_content) do
world_model = get_world_model(slug)
event_tokens = tokenize(event_content)
...
LLM 호출 없이 순수한 토큰 집합 수학 (token set math)으로 이루어집니다. 0.65 이상의 점수는 해당 이벤트가 진정으로 예상치 못한 것임을 의미하며, 카운터를 기다리는 대신 즉시 성찰을 트리거합니다. 이것이 없다면, 에이전트는 성찰 카운터가 10단계에 도달할 때까지 기다려야 한다는 이유로 10단계 중 3단계에서 발생한 치명적인 배포 실패를 인지하지 못한 채 멍하게 지나칠 수 있습니다. 대신, 높은 놀람 점수가 루프를 중단시키고 세계 모델이 즉시 업데이트되도록 강제합니다. 이는 Karl Friston의 자유 에너지 원리 (free energy principle)에서 유래되었습니다. 뇌는 예측 오차 (prediction error)가 높을 때 내부 모델을 업데이트함으로써 놀람을 최소화합니다. 높은 놀람 = 중요한 일이 발생했음, 지금 업데이트하라.
구체적인 예시를 들어보겠습니다: 하나의 Soul이 아무런 문제 없이 무언가를 천 번 배포합니다. 그러다 한 번의 배포가 실패합니다. 이 실패는 정의상 높은 놀람 (high-surprise)을 유발합니다. 즉, 세계 모델 (world model)에 존재하지 않는 사건입니다. 즉시 성찰 (Reflection)이 실행되고, 실패 사례가 메모리에 기록됩니다. 그리고 다음에 해당 배포가 실행될 때, Soul은 이미 지난번에 무엇이 잘못되었는지 알고 있습니다. 이 오류는 업데이트를 강제할 만큼 충분히 예상치 못한 것이었기에 반복되지 않습니다.
직관과 동기 (Intuition and motivation)
가장 독특한 두 가지 모듈은 SomaticMarker와 Homeostasis입니다.
Damasio는 합리적인 의사결정에 정서적 신호 (affective signals)가 필요하다는 것을 보여주었습니다. 복측전전두피질 (ventromedial prefrontal cortex)이 손상된 그의 환자들은 모든 것을 완벽하게 분석할 수 있음에도 불구하고 여전히 결정을 내리지 못했습니다. "직감 (gut feeling)"은 실제로 중요한 역할을 하고 있습니다. SomaticMarker는 바로 이것을 축적합니다: 축적된 경험으로부터 발생하는 (soul, topic, valence)별 가중치가 부여된 직관 (weighted intuitions)입니다. 만약 특정 주제와 관련하여 Soul이 반복적인 부정적 상호작용을 겪었다면, 이는 가중치가 증가하는 부정적인 신체 표지 (negative somatic marker)로 인코딩됩니다. 해당 주제와 관련된 다음 턴이 되면, 시스템 프롬프트에 GUT FEELINGS 블록이 전달됩니다. 이는 규칙도, 명시적인 플래그 (flag)도 아니며, 단지 경험으로부터 얻은 가중치가 부여된 직관일 뿐입니다.
Homeostasis는 Soul에게 내재적 동기 (intrinsic motivation)를 부여합니다. 각 Soul은 설정 파일에 목표 정서 상태 (target affective state)를 가지고 있습니다:
[homeostasis]
target = ["engaged", "curious", "balanced"]
check_every_minutes = 15
...
매 15분마다 타이머가 실행되어, 현재 상태(집계된 신체 표지로부터 유도됨)와 목표 사이의 거리를 측정합니다. 만약 편차가 임계값을 초과하면, 다음 시스템 프롬프트에 INTERNAL SIGNAL로서 주입되는 넛지 문장 (nudge sentence)을 생성합니다. 유휴 상태이거나 자극이 부족했던 Soul은 누군가 명시적으로 작업을 스케줄링하지 않아도 참여 (engagement)를 향한 이끌림을 느끼게 됩니다. 이것은 프롬프트 트릭이 아닙니다. 시상하부 (hypothalamus)가 허기와 체온 조절에 사용하는 것과 동일한 메커니즘인, 설정점 조절 (set-point regulation)으로서의 내재적 동기입니다.
Soul은 스스로의 깨어남을 예약할 수도 있습니다. 만약 당신이 Soul에게 무언가를 상기시켜 달라고 요청하면, Soul은 자신의 TASKS.md 파일에 @due 주석을 작성합니다. 시스템은 이를 파싱하여 적절한 시점에 타겟팅된 웨이크업(wake-up)을 실행합니다. 이는 일반적인 하트비트(heartbeat) 주기 외부에서 이루어지며, 외부의 크론 잡(cron job)이 필요하지 않습니다. Soul이 스스로 알람 시계를 설정하는 것입니다.
메타인지 (Metacognition)
마지막 주요 모듈은 제가 가장 흥미롭다고 느끼는 부분입니다. Metacognition은 Soul이 행동하기 전에 자신이 실제로 무엇을 알고 있는지 평가할 수 있게 해줍니다.
sense_confidence는 모든 Soul이 사용할 수 있는 도구입니다. 특정 주제가 주어지면, 이 도구는 세 가지 신호를 합성합니다: 메모리 커버리지 (Soul의 최근 메모리 중 얼마나 관련이 있는지), 신체적 신호 (somatic signal, 긍정적 마커와 부정적 마커의 비율), 그리고 기록된 지식 격차(knowledge gaps)입니다. 결과값은 태그가 지정된 튜플(tuple)로 반환됩니다: {:confident, score, ...}, {:uncertain, score, gaps}, 또는 {:low_confidence, score, gaps}
AI 자동 생성 콘텐츠
본 콘텐츠는 Lobste.rs AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기