PART 2에서 ReAct 액터 플래닝을 설명했다면, PART 3는 그 설계가 실제 코드와 UI, 그리고 터미널 AI 협업 프로토콜로 어떻게 닫히는지 보여준다. 작성일: 2026-04-14. 대상: PART 1, PART 2를 읽은 개발자.

0. 들어가며

PART 2의 결론은 분명했다. 일반 LLM은 스스로 기다리지 못한다. 그래서 Thinking -> Acting -> Waiting -> Complete를 Akka Become()으로 감싼 ReActActor가 필요했다.

이번 3편은 그 다음 장면이다. 설계 문서에서 끝나지 않고, 실제 AgentWin 코드베이스에 아래 흐름이 어떻게 연결되었는지 본다.

AgentBotWindow -> AgentBotActor -> ReActActor -> StageActor -> TerminalActor
                                             -> AI CLI -> bot-chat DONE -> MainWindow
                                             -> StageActor -> AgentBotActor -> ReActActor

이번 편의 핵심

  • ReAct를 추상 개념이 아니라 실제 상태 머신으로 구현했다.
  • 터미널 AI와의 통신 단절을 /agent-zero + DONE(...) 핸드셰이크로 메웠다.
  • 폭주, 드랍, 무한 대기 같은 실전 문제를 가드와 UI로 제어 가능하게 만들었다.

1. 구현체 한눈에 보기

실제 구현은 Project/AgentZeroWpf 안의 다섯 축으로 닫힌다.

실제 파일

역할

상태 머신

Actors/ReActActor.cs

Thinking / Acting / Waiting / Complete 전이

메시지 계약

Actors/Messages.cs

StartReAct, ReActProgress, TerminalDoneSignal

브로커

Actors/AgentBotActor.cs, Actors/StageActor.cs

UI와 ReActActor, 터미널 메시지 사이 포워딩

UI

UI/APP/AgentBotWindow.xaml.cs

RunReActAsync, ESC 제어, 카드 렌더링

외부 복귀 채널

UI/APP/MainWindow.xaml.cs, bot-chat.ps1

터미널 AI가 DONE(...)으로 다시 복귀하는 문

이 구조를 보면 PART 1의 질문, “Akka는 AI-Agent CLI와 어떻게 소통하는가?”가 이제 추상 답변이 아니라 코드 경로로 설명된다.

2. 귀멸의 형 전환 — ReActActor가 루프를 상태 머신으로 바꾸다

가장 큰 변화는 기존 RunFunctionCallLoopAsync의 동기 루프를 ReActActor로 분리한 점이다.

Idle
  -> StartReAct
  -> Thinking
  -> Acting
  -> Waiting
  -> Thinking ...
  -> Complete
  -> Idle

실제 메시지 계약도 명확하다.

이 구조가 중요한 이유는 간단하다. 루프는 return 하면 죽지만, 액터는 죽지 않는다. Waiting 상태에 들어간 뒤 외부 시그널이 오면 다시 Thinking으로 깨어날 수 있다. PART 2에서 이론으로 설명한 “LLM에게 기다림을 가르친다”가 여기서 실제 코드가 된다.

여기서 supporting 장치로 이미 먼저 깔려 있던 두 기반도 함께 중요했다.

즉 ReAct는 단독 기능이 아니라, 로그와 메모리 위에 얹힌 실행 엔진으로 들어갔다.

3. Phase 2, Phase 3 — 설계가 실제 버튼으로 내려오는 순간

생성 순서대로 보면 구현의 첫 전환점은 두 개의 완료 보고서였다.

3.1 ReAct-Phase2-Complete.md

여기서 실제로 끝난 것은 다음이었다.

즉 “설계”가 아니라, 테스트 가능한 액터가 먼저 생겼다.

3.2 ReAct-Phase3-Complete.md

그 다음은 UI 통합이다.

이 시점부터 ReAct는 백엔드 실험이 아니라, 사용자가 실제로 눌러 쓰는 모드가 된다.

4. 너의 이름은 + 하울의 문 — 터미널 AI와 연결을 만드는 법

여기서부터가 PART 3의 진짜 구현 디테일이다. ReActActor가 아무리 잘 만들어져도, 터미널 안에서 도는 Claude Code 같은 AI가 결과를 다시 보내주지 못하면 협업은 닫히지 않는다.

4.1 이름을 알려줘야 라우팅된다

ReAct-YourName-Identity-Protocol.md에서 드러난 문제는 단순하지만 치명적이었다.

그래서 핸드셰이크 문구가 바뀌었다.

/agent-zero 안녕, 나는 AgentZero야.
너의 터미널 탭 이름은 'Claude1'이야.
응답 후 반드시:
bot-chat.ps1 "DONE(Claude1, 응답요약)" 

이 한 줄로 DONE의 첫 인자가 모델명이 아니라 라우팅 키가 된다.

4.2 /agent-zero가 스킬 로더가 된다

ReAct-SkillActivation-Prompt.md는 그 다음 단계를 설명한다. 터미널 AI는 bot-chat.ps1 자체를 모른다. 그래서 매 대화 앞에 /agent-zero를 붙여 스킬을 강제 로드한다.

/agent-zero 코드 리뷰해줘

이렇게 해야 터미널 AI가 .claude/skills/agent-zero/SKILL.md를 읽고, bot-chat.ps1 사용법을 학습하고, 응답 종료 후 DONE(...)을 보낸다. 전통적인 시스템으로 치면 SKILL.md가 문서형 IDL이고, /agent-zero는 그 계약을 활성화하는 스위치다.

4.3 DONE은 어디로 돌아오는가

Terminal AI
  -> bot-chat.ps1 "DONE(Claude1, 리뷰 3건)"
  -> CliHandler
  -> MainWindow.HandleBotChat()
  -> TerminalDoneSignal
  -> StageActor
  -> AgentBotActor
  -> ReActActor

즉 PART 1에서 설명한 액터 트리가, 이번에는 터미널 밖 프로세스와 다시 연결되는 복귀 통로까지 확보한 셈이다.

5. 결계사의 결계 — 도구 폭주를 어떻게 막았는가

실전에서는 다른 종류의 문제가 바로 터졌다. 모델이 영리해질수록 한 번에 너무 많은 도구를 호출한다.

ReAct-ToolCall-Constraint-Prompting.md의 요약은 이것이다.

이를 막기 위해 구현체는 3층 방어를 쓴다.

  1. 프롬프트 제약 - “한 응답에 최대 5개 tool call”, “stage_send는 한 응답에 1회만”, “term_read는 1회만”
  2. 코드 차단 - response.ToolCalls.Take(5)
  3. 동일 호출 반복 차단 - 같은 (함수 + 인자) 3회 초과 시 에러 반환

이 부분이 중요한 이유는, 에이전트 시스템의 성능이 모델 지능만으로 결정되지 않기 때문이다. 도구를 많이 준 것보다, 그 도구를 어디까지 쓰게 할지 경계를 잘 친 것이 더 중요하다.

6. 주술회전의 영역 해제 — 멈추는 기술이 더 어렵다

ReAct-SequenceControl-ESC.md는 구현 후반부의 핵심 교훈을 담고 있다. 시스템은 잘 달리는 것보다, 잘 멈추는 것이 더 어렵다.

문제는 이랬다.

이걸 해결한 장치가 두 가지다.

6.1 _pendingDone 큐잉

Thinking이나 Acting 중에 TerminalDoneSignal이 와도 버리지 않고 _pendingDone에 저장한다. 다음 Waiting 진입 시 즉시 소비해서 다시 Thinking으로 보낸다. 즉 늦게 도착한 DONE이 더 이상 허공으로 사라지지 않는다.

6.2 ESC 제어

사용자도 루프를 제어할 수 있어야 한다.

이렇게 해서 ReAct는 “자동으로 잘 돌아가는 엔진”에서 끝나지 않고, 사용자가 제어권을 되찾을 수 있는 엔진이 됐다.

7. 유희왕의 카드 필드 — 상태를 눈으로 보이게 만들기

구현이 완성되려면 개발자만 이해해서는 안 된다. 사용자가 지금 어디에 걸려 있는지 보여야 한다.

ReAct-UI-CardSystem.md는 그 결과물이다.

즉 ReAct의 내부 상태가 더 이상 숨은 로직이 아니라, 카드처럼 필드에 배치된다. 사용자는 “지금 추론 중인지, 도구를 실행했는지, 완료를 기다리는지”를 즉시 파악할 수 있다.

특히 RunReActAsync()HandleReActProgress()의 조합은 중요하다. 액터가 내보낸 ReActProgress가 곧바로 UI 카드로 렌더링되기 때문에, ReAct는 로그로만 이해하는 시스템이 아니라 실시간으로 보이는 시스템이 된다.

8. ActorPlanning은 설계서가 아니라, 구현을 설명하는 압축본이 됐다

흥미로운 점은 ReAct-ActorPlanning.md가 처음 설계 문서처럼 보이지만, 실제 작성 시점은 여러 구현 이슈를 뚫은 뒤다. 그래서 이 문서는 PART 2의 계획서를 반복하는 게 아니라, 이미 구현된 내용들을 한 장의 설계도로 다시 압축한 문서가 된다.

정리하면 이렇다.

즉 이번 구현은 위에서 아래로 한 번에 내려온 설계가 아니라, 실패 로그 -> 패치 -> 프로토콜 정립 -> UI 가시화로 다듬어진 결과물이다.

9. 실전 한 턴 요약 — 이제 ReAct는 이렇게 돈다

User
  -> AgentBotWindow.RunReActAsync
  -> AgentBotActor.StartReAct
  -> ReActActor.Thinking
  -> stage_status / stage_send("/agent-zero ...")
  -> Waiting
  -> Claude1이 bot-chat.ps1 "DONE(Claude1, 리뷰 3건)" 호출
  -> MainWindow.HandleBotChat
  -> TerminalDoneSignal
  -> ReActActor.Thinking
  -> 최종 요약 응답 + 카드 UI 렌더링

가능하다. 다만 LLM 하나만 바꾸는 것이 아니라, 액터 상태 머신, 세션 메모리, 로그, 스킬 활성화, DONE 프로토콜, UI 가시성까지 함께 설계해야 한다.

10. 마무리

PART 1이 액터 트리와 통신 구조를 보여줬다면, PART 2는 ReAct 상태 머신의 필요성을 설명했다. 그리고 이번 PART 3는 그 설계가 실제 코드베이스에서 어떻게 살아 움직이는지 보여줬다.

핵심은 화려한 알고리즘 하나가 아니다.

결국 AgentWin의 ReAct 구현은 “LLM이 스스로 똑똑해졌다”는 이야기가 아니라, LLM이 일할 수 있는 런타임을 어떻게 설계했는가에 대한 사례다. 그 점에서 Akka 액터 모델은 단순한 동시성 프레임워크가 아니라, 에이전트 런타임을 조직하는 뼈대로 작동했다.


부록 : 실제 작동장면 - AgentZero 소개 이미지와는 다르게 화려함은 없으며 투박한 DEV TOOL같은 룩앤픽




참고 문서