PART 2에서 ReAct 액터 플래닝을 설명했다면, PART 3는 그 설계가 실제 코드와 UI, 그리고 터미널 AI 협업 프로토콜로 어떻게 닫히는지 보여준다. 작성일: 2026-04-14. 대상: PART 1, PART 2를 읽은 개발자.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-hero-runtime.png](/download/attachments/125731568/2026-04-14-react-part3-hero-runtime.png?version=1&modificationDate=1776107994456&api=v2)
PART 2의 결론은 분명했다. 일반 LLM은 스스로 기다리지 못한다. 그래서 Thinking -> Acting -> Waiting -> Complete를 Akka Become()으로 감싼 ReActActor가 필요했다.
이번 3편은 그 다음 장면이다. 설계 문서에서 끝나지 않고, 실제 AgentZero 코드베이스에 아래 흐름이 어떻게 연결되었는지 본다.
AgentBotWindow -> AgentBotActor -> ReActActor -> StageActor -> TerminalActor
-> AI CLI -> bot-chat DONE -> MainWindow
-> StageActor -> AgentBotActor -> ReActActor |
이번 편의 핵심
|
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-runtime-map.png](/download/attachments/125731568/2026-04-14-react-part3-runtime-map.png?version=1&modificationDate=1776107995088&api=v2)
실제 구현은 Project/AgentZeroWpf 안의 다섯 축으로 닫힌다.
축 | 실제 파일 | 역할 |
|---|---|---|
상태 머신 |
|
|
메시지 계약 |
|
|
브로커 |
| UI와 ReActActor, 터미널 메시지 사이 포워딩 |
UI |
|
|
외부 복귀 채널 |
| 터미널 AI가 |
이 구조를 보면 PART 1의 질문, “Akka는 AI-Agent CLI와 어떻게 소통하는가?”가 이제 추상 답변이 아니라 코드 경로로 설명된다.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-state-machine.png](/download/attachments/125731568/2026-04-14-react-part3-state-machine.png?version=1&modificationDate=1776107995684&api=v2)
가장 큰 변화는 기존 RunFunctionCallLoopAsync의 동기 루프를 ReActActor로 분리한 점이다.
Idle -> StartReAct -> Thinking -> Acting -> Waiting -> Thinking ... -> Complete -> Idle |
실제 메시지 계약도 명확하다.
StartReActReActProgressCompletionSignalSkipWaitingCancelReActTerminalDoneSignalReActResult이 구조가 중요한 이유는 간단하다. 루프는 return 하면 죽지만, 액터는 죽지 않는다. Waiting 상태에 들어간 뒤 외부 시그널이 오면 다시 Thinking으로 깨어날 수 있다. PART 2에서 이론으로 설명한 “LLM에게 기다림을 가르친다”가 여기서 실제 코드가 된다.
여기서 supporting 장치로 이미 먼저 깔려 있던 두 기반도 함께 중요했다.
AiMode-DiagnosticLogging.md - [AI-REQ], [AI-FnCall], [AI-TOOL], [AI-RESP] 로그 체계OnDevice-SessionMemory.md - 액터 상태에 최근 작업 이력을 저장하고 첫 라운드 System 메시지로 주입즉 ReAct는 단독 기능이 아니라, 로그와 메모리 위에 얹힌 실행 엔진으로 들어갔다.
생성 순서대로 보면 구현의 첫 전환점은 두 개의 완료 보고서였다.
ReAct-Phase2-Complete.md여기서 실제로 끝난 것은 다음이었다.
Actors/ReActActor.cs 신설Actors/Messages.cs 확장Actors/AgentBotActor.cs에 ReActActor 자식 생성Project/AgentTest/Actors/ReActActorTests.cs에 상태 전환 테스트 추가즉 “설계”가 아니라, 테스트 가능한 액터가 먼저 생겼다.
ReAct-Phase3-Complete.md그 다음은 UI 통합이다.
AgentBotWindow.xaml에 ReAct 체크박스 추가AgentBotWindow.xaml.cs에 RunReActAsync() 추가StreamAiResponseAsync()에서 기존 FnCall 루프 대신 ReAct 경로 분기SetReActCallbacks로 ReActProgress와 ReActResult를 UI에 직접 연결이 시점부터 ReAct는 백엔드 실험이 아니라, 사용자가 실제로 눌러 쓰는 모드가 된다.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-handshake-door.png](/download/attachments/125731568/2026-04-14-react-part3-handshake-door.png?version=1&modificationDate=1776107996366&api=v2)
여기서부터가 PART 3의 진짜 구현 디테일이다. ReActActor가 아무리 잘 만들어져도, 터미널 안에서 도는 Claude Code 같은 AI가 결과를 다시 보내주지 못하면 협업은 닫히지 않는다.
ReAct-YourName-Identity-Protocol.md에서 드러난 문제는 단순하지만 치명적이었다.
Claude1인지 Claude2인지 모르고,DONE(Claude, ...)처럼 모델명으로 응답해 버렸다.그래서 핸드셰이크 문구가 바뀌었다.
/agent-zero 안녕, 나는 AgentZero야. 너의 터미널 탭 이름은 'Claude1'이야. 응답 후 반드시: bot-chat.ps1 "DONE(Claude1, 응답요약)" |
이 한 줄로 DONE의 첫 인자가 모델명이 아니라 라우팅 키가 된다.
/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는 그 계약을 활성화하는 스위치다.
Terminal AI -> bot-chat.ps1 "DONE(Claude1, 리뷰 3건)" -> CliHandler -> MainWindow.HandleBotChat() -> TerminalDoneSignal -> StageActor -> AgentBotActor -> ReActActor |
즉 PART 1에서 설명한 액터 트리가, 이번에는 터미널 밖 프로세스와 다시 연결되는 복귀 통로까지 확보한 셈이다.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-guard-barrier.png](/download/attachments/125731568/2026-04-14-react-part3-guard-barrier.png?version=1&modificationDate=1776107996959&api=v2)
실전에서는 다른 종류의 문제가 바로 터졌다. 모델이 영리해질수록 한 번에 너무 많은 도구를 호출한다.
ReAct-ToolCall-Constraint-Prompting.md의 요약은 이것이다.
term_read(tab_index=0..78) 같은 인덱스 폭주term_read 반복 호출meeting_create 같은 과잉 확장이를 막기 위해 구현체는 3층 방어를 쓴다.
response.ToolCalls.Take(5)(함수 + 인자) 3회 초과 시 에러 반환이 부분이 중요한 이유는, 에이전트 시스템의 성능이 모델 지능만으로 결정되지 않기 때문이다. 도구를 많이 준 것보다, 그 도구를 어디까지 쓰게 할지 경계를 잘 친 것이 더 중요하다.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-escape-control.png](/download/attachments/125731568/2026-04-14-react-part3-escape-control.png?version=1&modificationDate=1776107997603&api=v2)
ReAct-SequenceControl-ESC.md는 구현 후반부의 핵심 교훈을 담고 있다. 시스템은 잘 달리는 것보다, 잘 멈추는 것이 더 어렵다.
문제는 이랬다.
Waiting이 끝난 뒤 5초 늦게 DONE이 오면Thinking 상태에서 드랍될 수 있다이걸 해결한 장치가 두 가지다.
_pendingDone 큐잉Thinking이나 Acting 중에 TerminalDoneSignal이 와도 버리지 않고 _pendingDone에 저장한다. 다음 Waiting 진입 시 즉시 소비해서 다시 Thinking으로 보낸다. 즉 늦게 도착한 DONE이 더 이상 허공으로 사라지지 않는다.
사용자도 루프를 제어할 수 있어야 한다.
ESC 1회: SkipWaitingESC 2회: CancelReAct이렇게 해서 ReAct는 “자동으로 잘 돌아가는 엔진”에서 끝나지 않고, 사용자가 제어권을 되찾을 수 있는 엔진이 됐다.
![DevBegin > ReAct는 실제 코드에서 어떻게 살아 움직이는가 — AgentZero 구현 완성편 [PART 3] > 2026-04-14-react-part3-ui-cards.png](/download/attachments/125731568/2026-04-14-react-part3-ui-cards.png?version=1&modificationDate=1776107998219&api=v2)
구현이 완성되려면 개발자만 이해해서는 안 된다. 사용자가 지금 어디에 걸려 있는지 보여야 한다.
ReAct-UI-CardSystem.md는 그 결과물이다.
ThinkingWaitingComplete즉 ReAct의 내부 상태가 더 이상 숨은 로직이 아니라, 카드처럼 필드에 배치된다. 사용자는 “지금 추론 중인지, 도구를 실행했는지, 완료를 기다리는지”를 즉시 파악할 수 있다.
특히 RunReActAsync()와 HandleReActProgress()의 조합은 중요하다. 액터가 내보낸 ReActProgress가 곧바로 UI 카드로 렌더링되기 때문에, ReAct는 로그로만 이해하는 시스템이 아니라 실시간으로 보이는 시스템이 된다.
흥미로운 점은 ReAct-ActorPlanning.md가 처음 설계 문서처럼 보이지만, 실제 작성 시점은 여러 구현 이슈를 뚫은 뒤다. 그래서 이 문서는 PART 2의 계획서를 반복하는 게 아니라, 이미 구현된 내용들을 한 장의 설계도로 다시 압축한 문서가 된다.
정리하면 이렇다.
ReAct-ActorPlanning.md는 그 여정을 다시 상태 머신 관점으로 정리한 최종 청사진이다.즉 이번 구현은 위에서 아래로 한 번에 내려온 설계가 아니라, 실패 로그 -> 패치 -> 프로토콜 정립 -> UI 가시화로 다듬어진 결과물이다.
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 가시성까지 함께 설계해야 한다.
PART 1이 액터 트리와 통신 구조를 보여줬다면, PART 2는 ReAct 상태 머신의 필요성을 설명했다. 그리고 이번 PART 3는 그 설계가 실제 코드베이스에서 어떻게 살아 움직이는지 보여줬다.
핵심은 화려한 알고리즘 하나가 아니다.
ReActActor로 기다릴 수 있게 만들고/agent-zero와 DONE(...)으로 다른 AI와 다시 연결하고결국 AgentZero의 ReAct 구현은 “LLM이 스스로 똑똑해졌다”는 이야기가 아니라, LLM이 일할 수 있는 런타임을 어떻게 설계했는가에 대한 사례다. 그 점에서 Akka 액터 모델은 단순한 동시성 프레임워크가 아니라, 에이전트 런타임을 조직하는 뼈대로 작동했다.
Tech/DOC/Actor/improvement/ReAct-Phase2-Complete.mdTech/DOC/Actor/improvement/ReAct-Phase3-Complete.mdTech/DOC/Actor/improvement/ReAct-YourName-Identity-Protocol.mdTech/DOC/Actor/improvement/ReAct-ToolCall-Constraint-Prompting.mdTech/DOC/Actor/improvement/ReAct-SequenceControl-ESC.mdTech/DOC/Actor/improvement/ReAct-ActorPlanning.mdTech/DOC/Actor/improvement/ReAct-DONE-Handshake-Protocol.mdTech/DOC/Actor/improvement/ReAct-SkillActivation-Prompt.mdTech/DOC/Actor/improvement/ReAct-UI-CardSystem.mdTech/DOC/Actor/improvement/AiMode-DiagnosticLogging.mdTech/DOC/Actor/improvement/OnDevice-SessionMemory.md더 다양한 정보는 AKKA를 연구하는 페북커뮤공간