시나리오, 규칙, 등장인물, 각자의 시야를 YAML 한 파일에 선언하면 — AI 에이전트들이 그 안에서 자율적으로 움직이고, 거짓말하고, 동맹을 맺고, 결국 스스로 이야기를 만들어낸다. Generative Agents의 아이디어를 게임 엔진 수준의 tick 루프 + DSL + LLM Dungeon Master로 재구성한 오픈소스 멀티에이전트 시뮬레이션 엔진.
왜 이 카테고리가 따로 있고, 일반적인 챗봇/에이전트 프레임워크와 뭐가 다른가.
대부분의 AI 에이전트 프레임워크 — LangChain, AutoGen, CrewAI — 는 "태스크 해결"이 목적이다. 사람이 "이 일을 해줘"라고 시키면 에이전트가 도구를 호출하면서 작업을 완수한다. 에이전트는 일이 끝나면 멈춘다. 다른 에이전트와의 관계는 보통 "감독자가 부하에게 일을 시킨다" 같은 위계 구조이고, 끝나면 모두 흩어진다.
그런데 2023년 스탠퍼드의 Generative Agents(Park et al.) 논문이 다른 방향을 제시했다. 25명의 NPC를 한 마을에 풀어놓고, 각자에게 기억·관찰·계획·반성 능력을 주면 — 시키지 않아도 발렌타인 파티를 열고, 소문이 퍼지고, 누군가는 선거에 출마한다. 태스크가 아니라 세계(world)가 있고, 에이전트는 거기서 그냥 살아간다.
WorldSeed가 노리는 자리가 정확히 거기다. 다만 Generative Agents는 Smallville이라는 하드코딩된 마을에 묶여 있었던 반면, WorldSeed는 "무엇을 시뮬레이션할지를 사용자가 YAML로 직접 선언"한다. 같은 엔진이 회사 사무실에서 정리해고 드라마를 돌리고, 민국시대 찻집에서 첩보전을 돌린다. 시나리오 교체는 YAML 파일 교체뿐이다.
{operator: check, left: "$agent.health", op: "<", right: 30} 같은 식으로 규칙·전제조건·효과를 표현하는 자체 DSL을 가진다.전체 글의 핵심 메시지.
WorldSeed는 월드 엔진(world engine)이다. 게임 엔진처럼 시간(tick), 공간(entity), 규칙(DSL), 시야(perception), 판정(DM)을 다 갖추고 있지만, 게임 엔진과 달리 엔진 내부에 어떤 도메인 지식도 없다 — 무기·HP·MP·맵 같은 개념이 코드에 박혀있지 않다.
그 자리에 사용자가 YAML로 "이 세계엔 찻집이 있고, 4명이 있고, 둘은 첩자고, 부채가 접선 신호다"라고 선언하면 — 엔진은 그걸 받아 매 tick마다 에이전트들을 한 번씩 깨워서 "지금 너는 뭘 보고 있어? 뭘 할 거야?"를 묻는다. 그 답이 DSL로 검증되고, 불확실한 부분은 LLM DM이 판정하고, 세계가 한 칸 진행된다. 반복하다 보면 "설계되지 않은 이야기"가 나온다.
TrendShift 9위까지 올라온 맥락.
2026년 멀티에이전트 시뮬레이션 분야의 핫토픽은 두 갈래다. 하나는 학술 — Park의 Generative Agents 후속 연구들(Concordia, Society of Mind 등). 다른 하나는 실무 — 게임 NPC, 인터랙티브 픽션, 사회과학 실험 플랫폼. WorldSeed는 후자 쪽 오픈소스 구현체로서 빈자리를 채우러 들어왔다.
Generative Agents 논문의 Smallville 코드는 "마을 + 25명 NPC"가 거의 통째로 박혀 있다. 새로운 시나리오(병원이나 우주선)를 돌리고 싶으면 Python 코드를 새로 짜야 한다. 결과적으로 학술적 재현 외에는 잘 안 쓰인다.
같은 엔진 바이너리가 configs/teahouse.yaml과 configs/ai_layoffs.yaml을 모두 돌린다. "엔진에 도메인 지식 zero" — health/HP/inventory 같은 개념조차 엔진은 모른다. 사용자가 YAML에서 정의한 property가 곧 세계의 어휘다.
나이브한 멀티에이전트 구현은 LLM에게 "세계의 모든 상태"를 매번 전달한다. 그러면 첩보·미스터리·정치 같은 정보 비대칭이 본질인 시나리오가 작동을 안 한다. 첩자가 자기 정체를 모두에게 broadcast하는 꼴.
perception.visibility에 DSL 규칙을 쓰면, "관찰자의 location과 entity의 location이 같을 때만 보인다" 식으로 시야가 정의된다. 같은 방의 3명이 같은 사건을 봐도 — 어떤 사람은 비밀(secret)을 갖고 있으니까 — 서로 완전히 다른 상황 해석을 들고 다음 tick을 맞이한다.
"에이전트가 방을 이동했을 때 일어나는 일을 LLM에게 매번 물어보면" — 토큰 비용이 폭주하고, hallucination으로 인해 일관성이 깨지고, latency가 게임을 망친다. 동시에 "모든 걸 룰 베이스로만 짜면" — 창의적/예외적 행동(예: "탁자 위 찻주전자로 상대를 위협한다")을 처리할 수 없다.
액션 정의에 effects:(결정적 효과)가 있으면 DSL이 즉시 실행, dm: 블록이 있으면 LLM DM에게 위임. "이동·아이템 전달은 룰, 관찰 결과의 해석·창의적 시도(attempt)는 LLM"이라는 명확한 책임 분리. 결과: 토큰 비용 80% 절감 + 일관성 유지.
여기에 더해 2026년의 "Skills" 트렌드(Anthropic의 Claude Skills, Cowork 등)와 맞물려 — WorldSeed의 skills/ 디렉토리는 create-world 스킬을 포함해 "자연어로 세계 생성"이 가능하다. AI 에이전트가 다른 AI 에이전트가 살아갈 세계를 만들어주는 셈이고, TrendShift 9위까지 올라온 결정타 중 하나다.
백엔드·프론트엔드·인프라가 각각 어떻게 구성됐는가.
WorldSeed의 본체는 Python으로 짜여 있다. pyproject.toml을 보면 의존성이 의외로 적다 — 핵심 7개만 추리면 다음과 같다.
| 패키지 | 버전 | 역할 |
|---|---|---|
pydantic | ≥2.10 | 데이터 검증의 표준. YAML 로딩, LLM 응답 구조 검증, 액션 파라미터 검증까지 전부 pydantic 모델. |
PyYAML | ≥6.0 | 씬 설정 파일 파싱. WorldSeed의 모든 시나리오가 YAML이므로 가장 핵심. |
fastapi + uvicorn | ≥0.115 / 0.34 | 비동기 HTTP/WebSocket 서버. |
httpx | ≥0.28 | 외부 에이전트와 통신용 비동기 HTTP 클라이언트. |
structlog | ≥25.1 | 구조화 로깅. 이벤트 스트림이 JSONL로 저장되므로 일관 구조 필수. |
deepdiff | ≥7.0 | tick 간 state diff 계산. UI의 "뭐가 바뀌었나"에 사용. |
anthropic | ≥0.86.0 | 필수 의존성. Anthropic Claude API SDK — pyproject.toml dependencies에 직접 포함. |
litellm + instructor | ≥1.60 / 1.7 | (선택 의존성, --extra dm) 100+ LLM 프로바이더 통합 + pydantic 강제 출력. |
WorldSeed의 DM은 "Anthropic이든, OpenAI든, Ollama 로컬이든 — 사용자가 원하는 LLM이면 뭐든 OK"여야 한다. 직접 SDK를 다 붙이는 대신 LiteLLM 한 장으로 통일했다. .env에 MODEL=anthropic/claude-sonnet-4 또는 MODEL=ollama/llama3:70b처럼 한 줄로 백엔드 교체가 끝난다.
Instructor는 그 위에서 pydantic 스키마를 강제하는 레이어 — DM 응답이 {narrative: str, effects: list[Effect]} 모델을 따르지 않으면 자동 재시도. "free prose"가 아니라 "structured effects"가 WorldSeed의 모든 룰을 단순하게 만드는 비결.
frontend/ 디렉토리는 별도의 npm 프로젝트로, 빌드 결과(dist/)를 FastAPI가 정적 서빙한다. 대시보드는 "한 자리에 다 보여주는 옵저버 뷰" — 방 카드, 에이전트 프로필 카드, 이벤트 스트림 패널, god-view 토글이 핵심 UI다.
| 레이어 | 선택 | 이유 |
|---|---|---|
| UI 프레임워크 | React 18 + TypeScript | SPA로 충분 — 한 페이지에서 다 본다. |
| 스타일 | Tailwind CSS | 유틸리티 클래스. 컴포넌트별 CSS 모듈 없이 빠르게. |
| 컴포넌트 라이브러리 | shadcn/ui | radix-ui 기반의 헤드리스 컴포넌트. 설치가 아니라 코드 복사. |
| 상태 관리 | Zustand | Redux보다 가볍고 boilerplate 적음. WebSocket → store 푸시. |
| 실시간 통신 | 네이티브 WebSocket | Socket.io 추상 없이 직접 사용. |
WorldSeed는 "하나의 uvicorn 프로세스가 다 한다"는 단일 프로세스 모델이다. Docker도 Redis도 PostgreSQL도 없다. 모든 상태는 메모리(StateStore)에 있고, 영속성은 ~/.worldseed/runs/{run_id}/에 이벤트 소싱(JSONL append-only)로 저장된다.
| 레이어 | 선택 | 특징 |
|---|---|---|
| 패키지 매니저 | uv (Astral) | Rust로 짠 차세대 Python 매니저. uv sync --extra dm 한 방으로 환경 구성. |
| Python | 3.11+ | asyncio 성능 개선, structural pattern matching, TaskGroup 활용. |
| 실행 모델 | 단일 asyncio 프로세스 | FastAPI + 백그라운드 TickRunner + WebSocket이 같은 이벤트 루프. |
| 영속성 | JSONL event log | action, dm_call, perceive, wake, whisper가 시간순 append. DB 없이도 모든 run을 replay 가능. |
| 테스트 | pytest + xdist + playwright | unit / e2e / scenarios 3계층. xdist로 병렬 강제. |
| 타입 체크 | mypy strict + pyright(basic) | 두 체커 동시. pyright는 strict가 아닌 basic 모드(pyrightconfig.json). 엔진 코어 안정성 우선. |
| 린트/포맷 | ruff (check + format) | black/isort/flake8 통합. pre-commit 강제. |
Docker/K8s/외부 DB 없이도 git clone → uv sync → uv run worldseed play config.yaml이 30초 안에. "오픈소스 멀티에이전트 시뮬레이션의 entry barrier를 박살내겠다"는 의지.
시스템 구조도(ASCII) + 핵심 설계 패턴 — tick 루프가 어떻게 도는가.
외부 에이전트가 HTTP/WebSocket으로 접속하고, 중앙의 TickRunner가 매 N초마다 세계를 한 칸 진행시키는 구조.
| 모듈 | 경로 | 역할 |
|---|---|---|
| WorldEngine | world.py | 최상위 파사드. 외부 접근의 단일 진입점. |
| TickEngine | engine/tick.py | 시계 오케스트레이션, tick당 액션 처리. |
| RulesEngine | engine/rules_engine.py | 전제조건 검사 + 효과 실행. |
| StateStore | engine/state_store.py | 엔티티 CRUD. 모든 상태는 flat property. |
| EventLog | engine/event_log.py | TTL 있는 이벤트 저장, 자동 만료. |
| Perceiver | engine/perceiver.py | 에이전트별 시야 필터링. |
| ConsequenceScanner | engine/consequence_scanner.py | 반응형 규칙(조건 충족 시 자동 발화). |
| InboxManager | engine/inbox.py | 에이전트별 inbox — 이벤트와 귓속말. |
| DM Providers | dm/providers/ | LLM DM (litellm) + mock DM (테스트용). |
| DSL Effects | dsl/effects/ | 결정적 효과의 디스패처/실행자 (디렉토리 — entity_ops.py, event_ops.py, state_ops.py 등 7개 파일). |
| Persistence | persistence.py | 이벤트 소싱 → JSONL. |
WorldEngine.step_async() 호출.effects:가 있으면 DSL이 즉시 실행. dm: 블록이 있으면 LLM DM에 위임(asyncio.gather()로 병렬).push: true 이벤트는 WebSocket으로 즉시 에이전트 wake.stream.jsonl에 append-only.Entity는 세계 상태(id, type, flat properties)만. AgentConfig는 별도 레지스트리에 저장되고 DM 프롬프트 빌더만 읽는다. 다른 에이전트도 못 본다. 이게 비밀(secret)이 진짜 비밀이 되는 보장.
state.json은 보조 스냅샷일 뿐, truth는 stream.jsonl에 들어 있다. action/dm_call/perceive/wake/whisper 모두 시간순 append. "이번 run을 처음부터 다시 재생"이 JSONL을 다시 흘리는 거다.
{operator: check, left: "$agent.location", op: "==", right: "$entity.id"} 같은 구조가 곧 "관찰자 위치가 엔티티 자신과 같은가?"라는 조건. operator는 check / exists / not / all / any. 풀파워 PL 없이도 99%의 게임 룰 표현 가능.
에이전트는 think_interval 단위로만 깨어난다(예: 5초). 그 사이엔 잔다. 단 "누가 너를 칼로 찔렀다" 같은 사건은 push: true 플래그로 즉시 WebSocket wake. polling 없이도 실시간성 유지.
DM(LLM)은 매 호출이 독립. context window 의존 없음. 필요한 모든 정보(엔티티 상태, 캐릭터 카드, 액션 hint)는 매번 새로 프롬프트 주입. Instructor가 pydantic 스키마로 응답 강제 → hallucination이 effect로 빠져나가지 않는다.
FPS는 raycast, MUD는 같은 방 비교. WorldSeed는 이 차이를 모두 DSL 규칙으로 흡수. 공간 게임은 $observer.location == $entity.location, 소셜 그래프는 $entity.id in relationships_of($observer, type=follows). 같은 엔진, 시야만 YAML로 다시 그린다.
레포를 처음 열었을 때 어디부터 봐야 하는가.
| 우선순위 | 파일 | 왜 |
|---|---|---|
| 1 | configs/teahouse.yaml | 가장 직관적인 시나리오. "4명, 4개의 방, 7가지 액션"이 전부. |
| 2 | configs/SCENE_CONFIG.md | YAML 스키마 풀 레퍼런스 — 모든 필드 + 예시. |
| 3 | docs/ARCHITECTURE.md | tick 루프, 모듈 책임, 데이터 흐름 10단계. "코드 읽기 전에 반드시". |
| 4 | src/worldseed/engine/tick.py | 한 tick에서 일어나는 일 — 코드 레벨로 확인. |
이 레포에서 무엇을 배울 수 있는가 + 어떻게 실습할 것인가.
배울 것: 에이전트가 "태스크"를 푸는 게 아니라 "세계에 살게" 만드는 사고방식. 시간/공간/시야/정보 비대칭의 추상화.
실습: configs/teahouse.yaml을 복사해 configs/my_office.yaml로 만들고 등장인물·아이템·액션을 자기 회사 상황으로 바꿔본다.
배울 것: 풀파워 PL 없이 표현력 있는 mini-language 만드는 법. operator/operand 분리, $param 경로 해석, 조합자(all, any, not) 설계.
실습: dsl/preconditions.py 읽고 "내 프로젝트 권한 시스템"이나 "기능 플래그 평가"를 같은 패턴으로 짜본다.
배울 것: DB 없이 시간순 이벤트 로그만 가지고 현재 상태 재구성 + replay. 디버깅성·감사 로그·재현성을 동시에.
실습: ~/.worldseed/runs/{run_id}/stream.jsonl을 jq로 파싱해 "tick별 액션 빈도" 통계 추출.
배울 것: HTTP REST와 WebSocket을 한 앱에서 공존. asyncio 백그라운드 태스크(TickRunner)와 요청 핸들러가 같은 루프에서 돈다.
실습: src/worldseed/server/app.py를 읽고, 자기 프로젝트에 "tick 기반 백그라운드 워커 + REST + WS"를 붙여본다.
배울 것: 100+ LLM을 같은 인터페이스로. pydantic으로 출력 강제. "free-form text"를 "structured effects"로 바꾸는 게 LLM 시스템 디자인의 핵심 패턴.
실습: dm/providers/llm.py 참조해 자기 챗봇/RAG에서 LLM 출력을 pydantic 모델로 강제.
배울 것: YAML 로딩 → 검증 → 액션 파라미터 → LLM 응답까지 한 도구로. strict=True로 mypy 만족시키는 모델 디자인.
배울 것: 서버 WebSocket 푸시 → Zustand store → 컴포넌트 selector 구독. Redux 없이 가능한 가벼운 상태 관리.
배울 것: pyproject.toml 한 장으로 의존성·스크립트·테스트·린트·타입체크. pip 시대를 끝내는 도구 체인.
실제로 돌리려면 뭐가 필요한가.
| 항목 | 최소 | 권장 |
|---|---|---|
| OS | macOS / Linux / Windows(WSL2) | macOS or Linux 네이티브 |
| Python | 3.11+ | 3.12 |
| Node.js | 18+ | 20 LTS |
| uv | 최신 | 최신 |
| RAM | 4 GB | 16 GB (로컬 LLM 동시 운용 시) |
| 디스크 | 500 MB | 10 GB (run 로그 누적) |
| LLM 백엔드 | API 키 1개 (Anthropic/OpenAI 등) | 비용 통제용 Ollama 로컬 병행 |
| 네트워크 | LLM API 호출용 외부 접속 | 전부 로컬이면 불필요 |
WorldSeed 자체는 노트북에서 충분히 돈다 — Python 프로세스 1개 + Vite dev. 진짜 비용은 LLM 토큰이다. teahouse 100 tick 정도면 에이전트 4명 × DM 평균 5회/tick ≈ 2000회. Claude Sonnet 대략 $2~5, GPT-4o-mini $0.5 이하.
비용 0으로 만들려면 Ollama로 llama3:8b나 qwen2.5:14b를 띄우고 MODEL=ollama/llama3:8b로. 품질은 낮아도 시나리오 디버깅엔 충분.
난이도별로 — 단계적으로 손에 익히는 법.
git clone → uv sync --extra dm → frontend && npm install && npm run build → .env에 API 키.uv run worldseed play configs/teahouse.yaml 실행.http://localhost:8000 접속 → 4명의 첩자가 자율적으로 움직이는 걸 관찰.configs/teahouse.yaml의 한 에이전트의 character.drives를 바꿔본다 (예: "왕부에게 적극 시비를 건다" 추가).play_music 같은 자기만의 verb를 만들고 effects 정의.uv run worldseed validate로 검증 후 실행.create-world 스킬로 자연어 → YAML 초안 → 손으로 다듬기.dsl/effect_ops.py에 새 operator 추가 (예: multiply, shuffle_list).radio — 같은 주파수면 듣는다).tests/unit/에 단위 테스트, tests/e2e/에 통합 테스트.stream.jsonl 100개 수집 → 결과 분포 분석.WorldSeed를 진짜로 이해하려면 — 곁가지로 알아둘 분야들.
src/worldseed/dsl/ 5개 파일 읽고 "새 operator between 추가하는 가상 PR" 짜본다.stream.jsonl에서 "현재 상태 재구성 reducer"를 손으로 짠다.src/worldseed/server/app.py의 WebSocket 핸들러 한 줄씩 분석.레포를 읽다가 막히지 않게 — 자주 등장하는 용어 정리.
type: agent뿐.{operator: check, left, op, right} 형식. all/any/not으로 조합.DMResponse(narrative, effects)를 반환. Instructor가 pydantic으로 구조 강제.on_change(상태 전이 1회) 또는 every_tick(매 tick) 두 모드.git clone https://github.com/AIScientists-Dev/WorldSeed → cd WorldSeed → uv sync --extra dm → cd frontend && npm install && npm run build && cd ..cp .env.example .env → 자기 Anthropic/OpenAI API 키 한 줄 추가.uv run worldseed play configs/teahouse.yaml → 브라우저 localhost:8000.~/.worldseed/runs/{run_id}/stream.jsonl을 열고 LLM이 어떤 결정을 어떻게 narrative로 풀어냈는지 한두 줄 읽기.더 깊이 들어가고 싶을 때.