PART 1에서 Akka 액터 트리로 AI 터미널의 질서를 세웠다면, 이번 편은 그 위에서 일반 LLM을 “일하는 에이전트”로 바꾸는 설계를 다룬다. 작성일: 2026-04-14. 대상: PART 1을 읽은 .NET / 서버 개발자.

0. 들어가며

PART 1에서 우리는 관제실을 세웠다. StageActor, WorkspaceActor, TerminalActor, AgentBotActor가 자기 자리를 갖고, 여러 AI 터미널이 서로 안 부딪치게 만들었다. 그런데 여전히 한 가지가 부족했다. 사회자 LLM은 말은 잘하는데, 기다리지 못했다.

명령을 보내고 결과가 늦게 오면 조급하게 같은 도구를 다시 호출하고, 아직 끝나지 않은 작업을 다시 확인하고, 루프가 끝난 뒤에는 외부 완료 시그널을 아예 못 받았다. 즉 똑똑한 채터는 되었지만 아직 일하는 에이전트는 아니었다.

이번 편의 핵심

  • 왜 일반 LLM은 기다림에 약한가
  • 왜 ReAct 패턴에 Waiting이라는 현실적 상태가 필요했는가
  • 왜 Akka Become()이 이 흐름을 상태 머신으로 붙잡아 주는가

1. 겨울왕국의 닫힌 문 — 동기 루프가 왜 막히는가

기존 RunFunctionCallLoopAsync는 얼핏 그럴듯해 보인다.

사용자 입력
  -> LLM 호출
  -> tool call 실행
  -> 결과를 다시 LLM에 전달
  -> 더 할 일이 없으면 return

문제는 여기서 문이 닫힌다는 점이다. 예를 들어 사회자가 npm test를 터미널에 보냈다고 해 보자.

Action: term_send("npm test")
Observation: "Sent to term-0"

여기까지만 보면 성공 같다. 그런데 실제 테스트는 아직 돌고 있다. 결과는 20초 뒤에 온다. 이 사이에 기존 루프는 선택지가 둘뿐이었다.

선택

왜 문제인가

폴링

같은 데이터를 반복 읽으며 컨텍스트를 낭비한다

종료

나중에 온 완료 시그널을 받을 방법이 없다

즉 동기 루프는 “지금 당장 답이 오는 세계”에는 맞지만, “나중에 다시 와서 알려줄게”가 많은 실제 도구 세계에는 약하다.

2. 인사이드 아웃의 관제실 — ReAct를 쉬운 말로 보면

ReAct는 Reasoning + Acting의 줄임말이지만, 어려운 논문 용어보다 먼저 장면으로 이해하는 편이 낫다. 인사이드 아웃 관제실을 떠올려 보자.

이게 ReAct의 기본 리듬이다.

Thought -> Action -> Observation -> Thought -> ...

AgentWin 식으로 바꾸면 더 쉽다.

생각한다
-> 도구를 부른다
-> 즉시 결과인지, 나중 결과인지 본다
-> 기다리거나 다시 생각한다
-> 끝나면 사용자에게 보고한다

논문 ReAct는 주로 관찰이 곧바로 돌아오는 장면을 다룬다. 하지만 실제 앱은 다르다. 테스트는 몇 초에서 몇 분이 걸릴 수 있고, 다른 AI 터미널의 응답은 언제 끝날지 모르며, 파일 작업이나 외부 호출도 비동기다. 그래서 AgentWin은 원본 ReAct에 Waiting이라는 현실적인 중간 상태를 덧붙였다.

3. 닥터 스트레인지의 시간 고리 — Become()이 루프를 상태 머신으로 바꾼다

여기서 Akka Become()이 빛난다. 같은 액터가 상태에 따라 다른 핸들러를 갖게 만들 수 있기 때문이다.

상태

하는 일

Thinking

지금 뭘 해야 하는지 판단

Acting

도구 실행

Waiting

비동기 결과를 기다림

Complete

최종 응답 정리

흐름은 단순하다.

StartReAct
  -> Thinking
  -> Acting
  -> Waiting
  -> Thinking
  -> Complete

이 구조가 중요한 이유는 명확하다. 일반 루프는 return 하면 끝나지만, 액터는 Waiting 상태로 살아남는다. 외부 시그널이 오면 다시 Thinking으로 깨어난다. 즉 LLM 자체를 더 똑똑하게 만든 것이 아니라, LLM을 감싸는 실행 환경에 기다릴 줄 아는 몸을 붙여 준 것이다.

4. 주토피아 신호등 — Waiting 상태가 바꾸는 것

4.1 Before: 동기 루프

term_send("npm test")
-> "sent"
-> term_read
-> term_read
-> term_read
-> ...
-> 10라운드 소진
-> 종료

4.2 After: ReActActor

term_send("npm test")
-> "sent"
-> Become(Waiting)
-> 완료 시그널 수신
-> Become(Thinking)
-> "이제 결과를 읽자"
-> term_read
-> Complete

이 차이를 표로 보면 더 쉽다.

장면

동기 루프

ReActActor

명령 전송 직후

계속 확인하려 듦

기다림으로 전환

비동기 완료

받을 통로가 약함

CompletionSignal로 수신

컨텍스트 사용량

폴링 때문에 커짐

필요한 순간만 다시 생각

실패 복구

루프 끝나면 끝

타임아웃 후 재판단 가능

Waiting은 그냥 잠깐 멈춤이 아니다. 에이전트가 조급함 대신 질서를 선택하게 만드는 장치다.

5. 토이 스토리 무전기 — 어떤 메시지가 오가야 하나

상태 머신은 메시지 계약이 분명해야 돌아간다. AgentWin은 ReAct용 메시지를 따로 정의했다.

메시지

역할

StartReAct

세션 시작

ReActProgress

UI에 중간 상태 전달

CompletionSignal

외부 완료 시그널 전달

CancelReAct

사용자 취소

액터 트리에서도 자리 배치가 중요했다.

ActorSystem("AgentZero")
└── /user/stage
    ├── /bot
    │   └── /react   <- ReActActor
    └── /ws-{name}
        └── /term-{id}

ReActActorbot의 자식으로 두었을까? 봇 세션 메모리와 가깝고, 터미널 완료 시그널을 포워딩하기 쉽고, UI와 진행 상태를 묶기 좋기 때문이다. 메시지 이름과 위치가 헷갈리면 에이전트도 금방 길을 잃는다.

6. 업(UP)의 메모책 — 세션 메모리와 로그가 같이 있어야 하는 이유

ReAct 상태 머신만 있다고 문제가 다 풀리진 않았다. 특히 온디바이스 LLM은 “지금 어디까지 했는지”를 자주 잊었다. 그래서 두 가지 보조 장치가 붙었다.

6.1 세션 메모리

현재 상태: Waiting
완료된 작업: meeting_create, term_send("npm test")
대기 중: term-0의 테스트 결과
다음 예상: 결과 읽기 후 사용자 보고

이 메모리는 액터 상태에서 자동 생성되어 매 LLM 호출 앞에 붙는다. 다시 말해 에이전트는 매 턴마다 자기 수첩을 펼쳐 보고 일하는 셈이다.

6.2 진단 로그

로그 태그

의미

[REACT-THINK]

어떤 판단을 했는지

[REACT-ACT]

어떤 도구를 실행했는지

[REACT-WAIT]

언제 기다리기 시작했고 무엇을 받았는지

[REACT-STATE]

어떤 상태로 넘어갔는지

메모리는 에이전트가 자기 일을 잊지 않게 하고, 로그는 개발자가 에이전트의 실수를 추적하게 한다. 하나는 에이전트를 위한 기억이고, 다른 하나는 사람을 위한 기억이다.

7. 마블 팀업 장면 — 왜 액터 모델이 에이전트 런타임에 잘 맞았나

2025~2026년 프레임워크는 많다. LangGraph, OpenAI Agents SDK, AutoGen, CrewAI, Semantic Kernel 모두 장점이 있다. 그런데 AgentWin은 왜 굳이 액터 모델을 붙잡았을까?

관점

일반적인 에이전트 프레임워크

Akka 액터

상태

dict, 히스토리, 러너 내부 상태

Become()으로 상태가 명시적

장애 복구

수동 재시도 중심

감독 전략 내장

동시성

async 코드 설계에 크게 의존

액터 경계로 자연 격리

분산 확장

별도 설계 필요

Akka 계열과 자연 연결

타임아웃/대기

개별 구현 필요

ReceiveTimeout 같은 기본 도구 존재

정리하면 이렇다. 일반 프레임워크가 에이전트를 빨리 시작하게 해 주는 도구라면, Akka는 에이전트가 오래 살아남는 구조를 잘 준다. 여러 터미널이 동시에 움직이고, 외부 완료 시그널이 언제 올지 모르고, 실패 복구와 상태 관찰이 중요한 환경에서는 이 차이가 크게 드러났다.

8. 닥터 스트레인지의 슬링 링 — 자율성에는 반드시 고삐가 필요하다

에이전트가 스스로 움직인다는 말은, 스스로 폭주할 수도 있다는 뜻이다. 그래서 안전장치가 함께 들어갔다.

장치

의미

MaxRounds

무한 루프 방지

10

ReceiveTimeout

너무 오래 기다리면 재판단

30초

동일 호출 제한

같은 도구 반복 차단

3회

MaxCallsPerRound

한 턴 폭주 방지

30

CancelReAct

사용자 즉시 중단

즉시

중요한 건 자율성을 없애는 게 아니다. 자율성이 길을 벗어날 때 다시 트랙 위로 올리는 제어권을 남겨 두는 것이다. 나중에 PART 3에서 보게 되겠지만, 이 안전장치는 결국 DONE 핸드셰이크, _pendingDone, ESC 제어, UI 카드 같은 더 구체적인 형태로 확장된다.

9. 마무리 — PART 3 예고

이번 편에서 본 것은 화려한 구현이 아니라, 그 구현이 필요했던 이유다.

이제 질문은 자연스럽게 다음 회차로 넘어간다.

그 설계를 실제 코드에 넣고 보니, 어디서 깨졌고 무엇을 더 붙여야 했을까?

PART 3에서는 바로 그 장면이 열린다. ReActActor는 코드에서 어떻게 구현됐는가, 터미널 AI와는 어떤 프로토콜로 다시 연결됐는가, 왜 DONE(...) 핸드셰이크가 필요했는가, 왜 카드 UI와 ESC 제어, 큐잉 같은 장치가 추가됐는가를 본다.

NEXT - PART 3

PART 2가 “사회자에게 기다리는 법을 가르치는 설계 편”이었다면, PART 3는 “그 사회자를 실제 무대에 올려 보니 무슨 일이 벌어졌는지 보는 구현 완성편”이다.

-> ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentWin 구현 완성편 [PART 3]


참고