본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 23. 02:32

InfiniteLoop 구축하기: AI 루프 엔지니어링의 실시간 시각화

요약

AI 에이전트의 사고 및 행동 루프를 실시간으로 시각화하여 디버깅할 수 있는 오픈 소스 플랫폼 'InfiniteLoop'를 소개합니다. 탈출 게임 환경을 통해 에이전트의 관찰, 사고, 행동, 성찰 과정을 투명하게 관찰할 수 있는 아키텍처를 다룹니다.

핵심 포인트

  • AI 에이전트의 루프(Observe-Think-Act-Reflect)를 실시간 시각화하는 디버거 역할
  • 결정론적 동작을 보장하기 위해 LLM과 분리된 Room Engine(상태 머신) 설계
  • 단기, 작업, 장기 메모리로 구성된 3단계 메모리 시스템 구축
  • React 기반의 3패널 UI를 통한 에이전트 상태 및 타임라인 렌더링

탈출 게임을 통해 AI 에이전트가 학습하는 과정을 한 번에 하나의 루프씩 관찰할 수 있는 인터랙티브 플랫폼을 어떻게 구축했는지 소개합니다.

대부분의 AI 데모는 블랙박스(black box)입니다. 프롬프트를 입력하면 모델이 텍스트를 생성하고, 사고 과정은 허공 속으로 사라져 버립니다. 왜 특정 행동을 선택했는지, 실패로부터 어떻게 회복했는지, 혹은 그 과정에서 무엇을 배웠는지 결코 볼 수 없습니다.

InfiniteLoop는 이를 뒤집습니다. 이는 AI 기반 탈출 게임을 통해 AI 에이전트의 관찰(Observe) → 사고(Think) → 행동(Act) → 성찰(Reflect) → 반복(Repeat) 사이클을 실시간으로 시각화하는 오픈 소스 인터랙티브 웹 애플리케이션입니다. 이를 창발적 지능(emergent intelligence)을 위한 디버거(debugger)라고 생각하면 됩니다.

에이전트가 가설을 세우고, 행동을 취하고, 실패하고, 실수를 성찰하며, 적응하는 과정을 볼 수 있습니다. 이 모든 과정은 루프별 상세 정보를 확장할 수 있는 3패널 UI로 렌더링됩니다.

10,000피트 높이에서의 아키텍처 (The Architecture at 10,000 Feet)

시스템은 네 가지 계층으로 구성됩니다:

  1. Room Engine (룸 엔진) — 게임 상태를 관리하는 결정론적(deterministic)이며 LLM에 독립적인 상태 머신(state machine)
  2. Agent Runtime (에이전트 런타임) — OpenRouter를 통해 LLM을 호출하여 의사결정을 내린 후, 그 결과를 엔진에 다시 전달
  3. Memory System (메모리 시스템) — 루프 사이의 교훈을 유지하는 3단계 메모리(단기, 작업, 장기 메모리)
  4. UI Layer (UI 계층) — 룸 상태, 에이전트 뇌 상태, 전체 루프 타임라인을 보여주는 3패널 React 앱

핵심 설계 결정: Room Engine은 절대로 LLM과 대화하지 않습니다. 엔진은 오직 방, 객체, 아이템, 연결, 잠금장치, 코드, 그리고 액션 핸들러(action handlers)만을 알고 있습니다. 에이전트는 타입이 지정된 액션 인터페이스(typed action interface)를 통해 엔진과 상호작용합니다.

UI Layer (React / Zustand / Framer Motion)
    ↓ fetch /api/game
API Route (Next.js App Router)
...

The Room Engine: 결정론적 상태 머신

Room Engine은 순수 로직(pure logic)입니다. 실행하기 전에 모든 액션을 검증하고, 결정론적으로 상태를 업데이트하며, 매 루프가 끝난 후 승리/패배 조건을 확인합니다.

export class RoomEngine {
  private room: RoomDefinition;
  public state: GameState;
...

이러한 분리는 의도적인 것입니다. 엔진을 LLM으로부터 분리함으로써 다음과 같은 이점을 얻을 수 있습니다:

  • 결정론적 동작 (Deterministic behavior) — 동일한 입력은 항상 동일한 출력을 생성합니다.
  • 테스트 가능한 액션 (Testable actions) — 모든 핸들러는 순수 함수 (pure function)입니다.
  • 모델 불가지론적 게임플레이 (Model-agnostic gameplay) — 엔진은 어떤 모델이 에이전트를 구동하는지 상관하지 않습니다.
  • 오프라인 가능 룸 디자인 (Offline-capable room design) — LLM 없이도 룸 JSON을 작성하고 검증할 수 있습니다.

액션 시스템 (The Action System)

12개의 액션 핸들러가 있으며, 각 핸들러는 타입이 지정된 입력과 출력을 가진 순수 함수 (pure function)입니다:

export const actionHandlers: Record<string, ActionHandler> = {
  search_object: (state, room, target) => { /* 객체 내 아이템/단서 탐색 */ },
  inspect_object: (state, room, target) => { /* 상세 설명 공개 */ },
...

각 핸들러는 success, message, stateChanges, 그리고 선택 사항인 discovered 아이템을 포함하는 ActionResult를 반환합니다. 엔진은 stateChanges를 적용하여 인벤토리, 발견된 객체, 잠금 해제된 아이템, 방문한 방 등을 업데이트합니다.

에이전트 런타임 (The Agent Runtime): 하나의 루프, 하나의 LLM 호출

에이전트 런타임에서 가장 중요한 설계 결정은 루프당 3~4회였던 LLM 호출을 정확히 1회로 줄이는 것이었습니다.

기존 아키텍처는 다음과 같이 별도의 에이전트를 사용했습니다:

  • 관찰자 (Observer) — "무엇이 보이는가?" (LLM 호출 1회)
  • 계획가 (Planner) — "무엇을 해야 하는가?" (LLM 호출 1회)
  • 행동가 (Actor) — "액션 실행" (LLM 호출 0회 — 결정론적)
  • 성찰가 (Reflector) — "무엇을 배웠는가?" (LLM 호출 1회)

이는 루프당 3회의 LLM 호출이 발생함을 의미합니다. 무료 티어 모델에서 호출당 6~7초가 소요된다면, 단일 루프에 20초 이상이 걸릴 수 있습니다. 20회 이상의 루프로 구성된 전체 게임은 사용하기 어려울 정도로 느려질 것입니다.

해결책: 관찰자, 계획가, 행동가를 하나의 결합된 프롬프트 (combined prompt)로 통합합니다. LLM은 단 한 번의 호출로 이 세 가지를 모두 포함하는 구조화된 JSON 객체를 반환합니다. 성찰가 (Reflector)는 규칙 기반 (rule-based)으로 작동하며 LLM을 전혀 사용하지 않습니다.

async function callModel(config, systemPrompt, userPrompt, schema) {
  const body = {
    model: config.model,
...

모델이 채워야 하는 결합된 스키마 (combined schema):

const STEP_SCHEMA = {
  type: "object",
  properties: {
...

성찰 엔진 (The Reflection Engine)

성찰 (Reflection)은 결정론적 함수 (deterministic function)에 의해 처리되며, LLM 호출이 필요하지 않습니다:

function generateReflection(action: string, result: string, confidence: number): Reflection {
  const failWords = ["fail", "can't", "cannot", "not found", "don't have", ...];
  const succeeded = !failWords.some(w => result.toLowerCase().includes(w));
...

이러한 접근 방식의 의미는 다음과 같습니다:

  • 추가 토큰 비용 제로 (Zero extra token cost): 성찰을 위한 추가 비용이 발생하지 않습니다.
  • 즉각적인 피드백 (Instant feedback): LLM이 성찰할 때까지 6~7초를 기다릴 필요가 없습니다.
  • 결정론적 동작 (Deterministic behavior): 에이전트가 실패로부터 항상 올바른 교훈을 얻습니다.
  • 투명한 로직 (Transparent logic): 모든 성찰 규칙이 코드 내에서 명확히 보입니다.

메모리 시스템 (The Memory System): 3계층 아키텍처 (Three-Tier Architecture)

에이전트는 인지 과학 (cognitive science)에서 영감을 얻은 세 가지 메모리 계층을 가집니다:

interface ShortTermMemory {
  currentRoom: string;
  inventory: string[];
...

각 루프(loop)는 관찰 결과(observations)를 단기 메모리 (short-term memory)로, 가설(hypotheses)을 작업 메모리 (working memory)로, 그리고 교훈(lessons)을 장기 메모리 (long-term memory)로 전달합니다. getContextForPrompt() 메서드는 이 세 계층을 모두 상태 프롬프트 (state prompt)로 직렬화(serialize)하여 LLM이 축적된 지식에 접근할 수 있도록 합니다.

Zustand v5: 복잡성 없는 상태 관리 (State Without Complexity)

React Context나 Redux 대신 Zustand를 선택한 데에는 몇 가지 이유가 있습니다:

  1. Provider 불필요 (No providers) — 스토어(store)가 모듈 수준의 싱글톤(singleton)으로 존재하여 직접 임포트(import)할 수 있습니다.
  2. 평탄한 셀렉터 (Flat selectors)useGameStore(s => s.activeSessionId)는 해당 값이 변경될 때만 리렌더링(re-render)됩니다.
  3. getState() 패턴 — 오래된 클로저 (stale closures) 문제 없이 최신 상태가 필요한 이벤트 핸들러 (event handlers)에 필수적입니다.

스토어는 세션 (sessions), 루프 (loops), 코멘터리 (commentaries), 게임 상태 (game state), 그리고 방 정의 (room definitions)를 관리합니다. 각 게임은 고유한 설정 (config), 루프 배열 (loops array), 그리고 상태 (state)를 가진 고유한 세션 ID를 할당받습니다.

export const useGameStore = create<GameStore>((set, get) => ({
  createSession: (roomId, config) => {
    const sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
...

방 정의 (Room Definitions): JSON 기반 게임 디자인 (JSON-Driven Game Design)

다섯 개의 방 모두 순수 JSON이며, 빌드 타임(build time)에 Zod 스키마(schema)를 통해 검증됩니다. 이는 TypeScript 코드를 전혀 건드리지 않고도 새로운 방을 추가할 수 있음을 의미합니다:

{
  "id": "tutorial-room",
  "name": "The Tutorial Chamber",
...

Zod 스키마 시스템은 다음 사항을 보장합니다:

  • 모든 방(room), 오브젝트(object), 아이템(item), 단서(clue), 연결(connection)이 올바른 형태(shape)를 가짐
  • 목표(objectives)가 유효한 대상(targets)을 참조함
  • 아이템이 usableOn에서 유효한 오브젝트를 참조함
  • 컨테이너(containers)가 contains에서 유효한 아이템 ID를 참조함

이를 통해 런타임(runtime)이 아닌 임포트 타임(import time)에 설정 오류를 잡아낼 수 있습니다.

주요 기술적 과제 (Key Technical Challenges)

과제 1: OpenRouter 무료 티어의 신뢰성 (OpenRouter Free Tier Reliability)

기본 모델(openai/gpt-oss-20b:free on OpenInference)은 무료이지만 일관성이 없습니다. 정확히 동일한 프롬프트로 직접 curl 테스트를 수행하면 약 6.7초 내에 유효한 JSON을 반환합니다. 하지만 Next.js 앱에서는 가끔 빈 콘텐츠(empty content)를 반환합니다.

해결책: 재시도(retries)를 하지 않습니다. 모델이 빈 값을 반환하면, 에이전트는 3번 재시도하며 45초 이상을 낭비하는 대신 즉시 examine_room으로 폴백(fallback)합니다. 모델 상태와 관계없이 각 루프는 10초 이내에 완료됩니다.

const modelResult = await callModel(config, SYSTEM_PROMPT, buildStatePrompt(engine, memory), STEP_SCHEMA);
const agentStep = modelResult ?? {
  actionType: "examine_room",
...

과제 2: Zustand v5의 오래된 클로저 (Stale Closures in Zustand v5)

Zustand v5는 v4와 다른 내부 스케줄러(internal scheduler)를 사용합니다. 렌더링 시점에 스토어(store) 값을 캡처한 이벤트 핸들러(event handlers)는 오래된 상태(stale state)를 기반으로 동작하게 됩니다.

해결책: 이벤트 핸들러 내부에서는 항상 useGameStore.getState()를 사용합니다. 반응형 재렌더링(reactive re-renders)을 위해서만 useGameStore(s => s.prop)를 사용합니다.

const runLoop = useCallback(async () => {
  const sid = useGameStore.getState().activeSessionId;
  // ... sid 사용
...

과제 3: LangChain의 구조화된 출력 우회하기 (Bypassing LangChain's Structured Output)

LangChain의 withStructuredOutput()은 재시도/폴백 (retry/fallback) 로직으로 인해 OpenRouter의 무료 티어 모델에서 멈추는 현상이 발생했습니다. 모델은 6~7초 내에 유효한 응답을 반환하지만, LangChain의 래퍼 (wrapper)가 계속해서 재시도를 반복하는 것이 문제였습니다.

해결책: 메인 호출 시 LangChain을 완전히 우회합니다. 검증된 curl 동작과 정확히 일치하도록 OpenRouter에 직접 fetch()를 사용합니다. LangChain은 도구 정의 (tool definitions)를 위해 여전히 사용할 수 있지만, 핵심 루프 (core loop)는 가공되지 않은 HTTP (raw HTTP)를 사용합니다.

3분할 UI

인터페이스는 세 개의 패널로 나뉩니다:

  1. 방 시각화 (Room Visualization) (왼쪽) — 현재 방의 설명, 보이는 객체 (컨테이너/잠금 배지 포함), 출구 (잠금 상태 포함), 그리고 에이전트의 인벤토리 (inventory)를 보여줍니다.

  2. 에이전트 브레인 (Agent Brain) (중앙) — 에이전트의 현재 목표, 가설 (신뢰도 백분율 포함), 마지막 행동 및 그 결과, 축적된 교훈, 그리고 최근 관찰 내용을 보여줍니다. 애니메이션이 적용된 내레이터 해설이 framer-motion 전환 효과와 함께 상단에 나타납니다.

  3. 루프 타임라인 (Loop Timeline) (오른쪽) — 각 루프에 대한 확장 가능한 카드들이 최신순으로 정렬되어 나타납니다. 각 카드는 전체 루프 사이클을 보여줍니다: 관찰 (Observation) → 계획 (Plan) → 행동 (Action) → 결과 (Result) → 성찰 (Reflection) → 내레이터 해설 (Narrator Commentary). 성공한 루프는 녹색 왼쪽 테두리가, 실패한 루프는 빨간색 테두리가 표시됩니다.

모바일에서는 이 패널들이 방 / 브레인 / 타임라인 탭이 있는 탭 뷰 (tabbed view)로 축소됩니다.

개발자 대시보드

별도의 /dashboard 페이지에서 다음을 제공합니다:

  • 벤치마크 러너 (Benchmark Runner) — 방을 선택하고 여러 모델을 대상으로 실행합니다. 결과에는 사용된 루프, 발생한 실수, 소요 시간, 소비된 토큰, 그리고 예상 비용이 표시됩니다. 나란히 비교 (side-by-side comparison)가 가능합니다.
  • 에이전트 트레이스 (Agent Traces) (계획 중) — 전체 도구 호출 (tool-call) 트레이스와 상태 전환 (state transitions)
  • 비용 분석 (Cost Analytics) (계획 중) — 루프당, 방당, 모델당 토큰 사용량
  • 메모리 인스펙터 (Memory Inspector) (계획 중) — 에이전트의 단기, 작업, 장기 메모리에 대한 실시간 보기

다음 단계

멀티 에이전트 모드 (Multi-agent mode)가 다음 주요 기능입니다 — 동일한 방에서 서로 다른 모델을 사용하는 2~3개의 에이전트를 병렬로 실행하여, 그들의 전략을 나란히 비교할 수 있습니다. 다른 계획된 기능으로는 커스텀 룸 빌더 UI (custom room builder UI), 세션 간 에이전트 메모리 지속성 (agent memory persistence), 체스 스타일의 리플레이 (chess-style replay), 그리고 추가적인 룸 테마 등이 있습니다.

직접 해보기

git clone https://github.com/harishkotra/infinite-loop
cd infinite-loop
npm install
...

http://localhost:3000을 열고, Tutorial Chamber를 선택하여 AI 에이전트가 탈출하는 법을 배우는 과정을 지켜보세요. GPU는 필요하지 않습니다 — 브라우저와 무료 OpenRouter API 키만 있으면 됩니다.

스크린샷

Loop Engineering 101 - 1

Loop Engineering 101 - 2

Loop Engineering 101 - 3

Loop Engineering 101 - 4

코드 및 기타 정보: https://www.dailybuild.xyz/project/171-infinite-loop

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0