Python 800줄로 Zo Computer의 7개 서브시스템을 재구축했습니다 — 아키텍처, 트레이드오프(tradeoffs), 그리고 생략한
요약
Zo Computer의 7개 핵심 서브시스템을 단 800줄의 Python 코드로 재구축한 ZoClone 프로젝트를 소개합니다. 에이전트 매니저, 메모리 엔진, 스케줄러 등 복잡한 AI 워크스페이스의 인프라를 의존성 없이 경량화하여 구현하는 아키텍처와 트레이드오프를 다룹니다.
핵심 포인트
- 800줄 미만의 코드로 에이전트 매니저, 메모리 엔진 등 7개 서브시스템 구현
- Docker나 DB 없이 Python 표준 라이브러리만으로 경량화된 아키텍처 구축
- OpenAI 호환 스키마를 활용하여 다양한 LLM 제공업체(Groq, Anthropic 등) 통합
- SKILL.md 파일을 통한 간결한 스킬 레지스트리 및 자동 로딩 시스템
Python 800줄로 Zo Computer의 7개 서브시스템을 재구축했습니다 — 아키텍처, 트레이드오프(tradeoffs), 그리고 생략한 부분들
저는 몇 달 동안 Zo Computer를 저의 주요 AI 워크스페이스로 사용해 왔습니다. 제가 계속해서 눈여겨보았던 부분은 모델이 아니라 바로 _기질(substrate)_이었습니다. 즉, 병렬 세션을 생성하는 에이전트 매니저(agent manager), SKILL.md 파일을 자동으로 로드하는 스킬 레지스트리(skills registry), 오래된 컨텍스트를 압축하는 메모리 엔진(memory engine), rrule 기반의 스케줄러(scheduler), 유휴 상태의 머신을 워커(worker)로 전환하는 컴퓨트 풀(compute pool), Groq/OpenAI/Anthropic 사이를 전환하는 BYOK 클라이언트, 그리고 실제로 클릭을 수행하는 헤드리스 브라우저(headless browser) 같은 것들 말입니다.
그래서 저는 당연한 질문을 던졌습니다. 이 중 얼마나 많은 부분이 _개념(concept)_이고, 얼마나 많은 부분이 플랫폼 글루(platform glue, 플랫폼 연결 코드)일까? 노트북에 있는 단일 Python 패키지가 개발자에게 동일한 형태의 80%를 제공할 수 있을까?
ZoClone이 저의 해답입니다. src/ 디렉토리 내의 7개 파일, 의존성이 적은 약 800줄의 Python 코드로 위에서 언급한 모든 서브시스템이 연결되어 있습니다. 데몬(daemon)도, Docker도, Postgres도 없습니다. 그저 ~/.zoclone/*.db 파일과 ThreadPoolExecutor만 있을 뿐입니다.
여기 아키텍처와, 어떤 부분이 복제하기 쉽고 어떤 부분이 실제로 핵심적인 역할을 하는지에 대해 배운 점, 그리고 이 모든 것을 단일 리포지토리(repo)에 담기 위해 취해야 했던 지름길들을 소개합니다.
7개의 파일
ZoClone/
├── src/
│ ├── zo.py # 최상위 오케스트레이터(orchestrator) + ask() 루프
...
총 코드 라인 수(LoC): 775. __init__.py의 마법도, 메타클래스(metaclass) 트릭도, 디렉토리 스캔 이상의 플러그인 탐색도 없습니다. 이러한 제약 조건은 모든 인터페이스가 단순한 함수이거나 세 개의 메서드를 가진 클래스가 되도록 강제했습니다.
오케스트레이터: zo.py
모든 것은 DB 연결, 스레드 풀(thread pool), 그리고 ask() 호출 시 처음으로 지연 생성(lazily constructed)되는 AIClient를 소유하는 단일 ZoClone 클래스를 통해 스레드로 연결됩니다.
class ZoClone:
def __init__(self):
self.db = init_db()
...
핵심은 AIClient입니다. 이는 OpenAI와 호환되어야 하는 유일한 부분인데, 그 이유는 모든 현대적인 제공업체(Groq, Together, OpenRouter, Ollama, LM Studio)가 채팅 완성 (chat completions) 스키마로 수렴했기 때문입니다. Anthropic의 경우 아주 작은 심 (shim)이 필요했지만, Groq는 별도 설정 없이 바로 작동합니다.
스킬 시스템: SKILL.md 자동 로딩
이 부분은 제가 가장 자랑스럽게 생각하는 부분입니다. 디렉토리 스캔은 단 6줄에 불과합니다:
def load_all_skills():
global SKILLS
SKILLS = {}
...
흥미로운 점은 SKILL.md 파서입니다. 이 파서는 Agent Skills 사양과 동일한 프론트매터 (frontmatter) 형태인 name, description, triggers (쉼표로 구분)를 수용하며, scripts/<name>.py를 찾아 run() 또는 execute() 호출 가능한 함수를 찾습니다. 이것이 플러그인 API의 전부입니다. 별도의 등록이나 데코레이터 (decorator), 매니페스트 (manifest)가 필요 없습니다. skills/ 폴더에 폴더를 넣기만 하면 다음 import 시 바로 인식됩니다.
대가로는 버전 관리, 의존성 선언, 스킬별 샌드박스 (sandbox)가 없다는 점이 있습니다. 스킬을 밀폐형 (hermetic)으로 만들고 싶다면 직접 구현해야 합니다. 단일 사용자 노트북 환경에서는 괜찮지만, 멀티 테넌트 (multi-tenant) 플랫폼에서는 그렇지 않습니다.
에이전트 매니저: /zo/ask를 통한 병렬 aiohttp 호출
이 부분에서는 약간의 편법을 썼고, 저는 그것에 만족합니다. 원래의 "병렬 에이전트 생성" 프리미티브 (primitive) 자체가 모델에 대한 원격 호출이며, Zo의 /zo/ask 엔드포인트는 토큰을 가진 누구에게나 열려 있습니다. 따라서 다음과 같이 구현됩니다:
async def spawn(self, agent_id: str, prompt: str, callback=None):
async with aiohttp.ClientSession() as session:
async with session.post(
...
spawn_all은 N개의 동시 요청을 발생시키고, asyncio.gather는 가장 느린 요청이 완료될 때까지 기다린 후 출력 리스트를 반환합니다. 비동기 (async) 방식을 원하지 않는 호출자를 위해 ThreadPoolExecutor(max_workers=10)를 동기식 방식으로 제공할 수도 있습니다. 실제로 병목 현상은 네트워크가 아니라 모델에서 발생합니다. 10개의 병렬 호출은 asyncio를 포화시키기 훨씬 전에 속도 제한기 (rate limiter)를 먼저 포화시킵니다.
메모리 엔진: 플레이스홀더 (placeholder)로서의 TF-IDF
솔직히 말씀드리면, 이 부분이 가장 취약한 서브시스템입니다. embed_tfidf는 토큰을 512차원 벡터로 해싱하고, cosine이 계산을 수행하며, recall()은 임베딩 유사도가 가장 높은 상위 k개의 노드를 반환합니다. 짧은 프롬프트와 작은 코퍼스(corpora)에는 작동하지만, 이는 의미론적(semantic)이지는 않습니다. 즉, database와 sql이 실제 임베딩 모델을 사용할 때처럼 클러스터링되지 않습니다.
그럼에도 불구하고 이를 구현한 이유는 다음과 같습니다. 실제 임베딩 모델(sentence-transformers 또는 원격 호출)로 교체하는 것은 단 한 번의 교체만으로 가능하며, 인터페이스(interface) — memorize(content, meta) -> nid, recall(query, top_k) -> [{id, content, meta}] — 는 변하지 않기 때문입니다. 나중에 Ollama를 통해 nomic-embed-text를 연결하게 되더라도, zo.py 내의 어떤 것도 옮길 필요가 없습니다. 핵심은 먼저 올바른 형태(shape)를 정의하고, 플레이스홀더(placeholder)가 어떤 필드를 가짜로 구현하고 있는지 정직하게 설정하는 것이었습니다.
스케줄러(The scheduler): 30줄로 구현한 rrule
rrule 명세는 50페이지에 달하는 문서입니다. 저는 세 가지 빈도(frequency)와 횟수(count)만 필요했습니다. 그래서 다음과 같이 작성했습니다:
def parse_rrule(rrule: str) -> dict:
result = {"interval": 86400, "count": 0} # 기본값은 일간(daily)
if "FREQ=DAILY" in rrule: result["interval"] = 86400
...
데몬 스레드(daemon thread)가 1분마다 깨어나 SQLite에 WHERE enabled=1 AND next_run <= now를 요청하고, 각 작업의 handler를 실행한 뒤, next_run을 간격(interval)만큼 증가시킵니다. 이것이 자동화 시스템의 전부입니다. 타임존(timezone), 예외 처리, 일광 절약 시간제(DST) 처리는 누락되어 있지만, "매시간 실행"과 같은 용도로는 정확하고 신뢰할 수 있습니다.
컴퓨트 풀(The compute pool): Python dict 기반의 우선순위 FIFO
ComputePool은 self.jobs와 self.nodes를 threading.Lock으로 보호되는 인메모리 딕셔너리(in-memory dicts)로 유지합니다. 하트비트(heartbeats)는 last_heartbeat를 업데이트하며, 디스패치(dispatch)는 대기 중인 작업을 -priority 순으로 정렬하여 가장 높은 우선순위의 작업을 다음 폴링(polling) 노드에 할당합니다. 리더 선출(leader election), Raft, 가십 프로토콜(gossip protocol)은 없습니다.
def assign_job(self, node_id: str) -> dict | None:
with self.lock:
pending = [j for j in self.jobs.values() if j["status"] == "pending"]
...
이것은 정말 위험한 실수(footgun)가 될 수 있습니다. 프로세스 내 상태(in-process state)를 사용한다는 것은 프로세스가 재시작될 때 모든 대기 중인 작업(pending job)을 잃게 된다는 의미입니다. 진정한 그리드(grid) 환경을 구축하려면 Postgres와 행 수준 잠금(row-level locks)을 사용해야 합니다. 하지만 "내 두 번째 노트북에서 작업을 실행하고 싶다"는 목적이라면, pip install만으로 온보딩(onboarding)이 끝납니다.
생략한 부분과 그 이유
다음 세 가지는 패키지에 포함되어 있지 않으며, 아마 앞으로도 포함되지 않을 것입니다:
- 호스팅된 UI (The hosted UI) — 채팅 사이드바, 파일 트리, 에이전트 선택기 등입니다. ZoClone은 앱이 아니라 라이브러리입니다.
zo를 임포트(import)하여 Flask 라우트, Tk 창, Discord 봇, 또는 cron 작업에서zo.ask(...)를 호출하기만 하면 됩니다. - 멀티테넌트 인증 (Multi-tenant auth) — 사용자는 정확히 한 명뿐입니다.
whoami()는 로컬 사용자 이름을 반환합니다. 팀 플랜을 원한다면 리포지토리(repo)를 포크(fork)하세요. - 진정한 벡터 스토어 (A real vector store) — TF-IDF는 자리 채우기용(placeholder)입니다. 다음 반복(iteration)에서는 이를 Ollama의
nomic-embed-text(프라이빗하고 무료이며 동일한 기기에서 실행됨)로 교체할 예정이며, 인터페이스는 동일하게 유지됩니다.
실행해보기
git clone https://github.com/AmSach/ZoClone
cd ZoClone && pip install aiohttp playwright
python -m playwright install chromium
...
새로운 스킬을 추가하고 싶다면, skills/ 폴더에 SKILL.md와 scripts/foo.py를 포함한 폴더를 넣고 PR(Pull Request)을 보내주세요. 24시간 이내에 머지(merge)하겠습니다. 7개 서브시스템 중 실제 버그를 발견했다면, 최소한의 재현 코드(minimal repro)와 함께 이슈(issue)를 생성해 주세요. 검색해야 할 코드는 단 775줄뿐입니다.
7개의 파일, 하나의 Python 프로세스, 클라우드 의존성 없음. 규모보다 형태가 더 중요합니다.
#Python #AI #OpenSource #BuildInPublic #PySide6 #LocalFirst
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기