ReAct 패턴 — 생각과 행동을 분리하는 이유
지금까지 만든 루프는 잘 돌아갑니다. 그런데 잘 안 될 때 문제입니다. 모델이 엉뚱한 도구를 부르거나 같은 도구를 반복 호출하면, 로그에는 tool_use 블록과 인자만 남지 "왜 그렇게 판단했는지"는 안 보입니다. ReAct(Reasoning + Acting)는 이 문제를 아주 단순한 방법으로 풉니다 — 모델에게 행동하기 전에 생각을 말로 뱉게 시킵니다.
프롬프트 한 줄이 하는 일
거창한 프레임워크가 아닙니다. 시스템 프롬프트에 이런 지시를 추가하는 게 전부입니다.
SYSTEM_PROMPT = """도구를 호출하기 전에, 왜 그 도구가 필요한지 한두 문장으로 먼저 설명하라.
설명 없이 바로 도구를 호출하지 마라.
도구 결과를 받은 뒤에는, 그 결과가 사용자의 질문에 충분한 답을 주는지 스스로 점검하라."""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=SYSTEM_PROMPT,
tools=tools,
messages=messages,
)이걸 넣으면 응답의 content가 이렇게 바뀝니다.
[
{"type": "text", "text": "부산 날씨도 확인해야 두 도시를 비교할 수 있으니, 서울에 이어 부산도 조회하겠습니다."},
{"type": "tool_use", "name": "get_weather", "input": {"city": "부산"}}
]이 텍스트 블록 자체는 실행에 아무 영향을 주지 않습니다. 어차피 루프 코드는 tool_use 블록만 실행하니까요. 그런데도 이게 왜 도움이 될까요.
왜 "생각을 말하게" 하면 결과가 좋아지는가
정확한 이유는 저도 완전히 확신하지는 못합니다. 다만 실무에서 관찰되는 효과는 두 가지입니다.
첫째, 디버깅이 쉬워집니다. 모델이 왜 그 도구를 그 인자로 불렀는지 로그에 텍스트로 남으니, 이상 동작이 생겼을 때 "모델이 애초에 무슨 생각이었는지" 바로 알 수 있습니다. 인자만 보고 추측하는 것과는 차이가 큽니다.
둘째, 판단 정확도 자체가 달라지는 경향이 있습니다. 모델이 결론(도구 호출)을 바로 내놓게 하는 것보다, 먼저 근거를 말로 풀어내게 하면 실수가 줄어드는 현상은 여러 벤치마크에서 보고된 바 있습니다. 다만 이게 모든 상황에서 그렇다고 일반화하기는 어렵고, 간단한 도구 호출 한 번짜리 작업에서는 차이가 거의 없을 수도 있습니다.
공짜는 아니다
ReAct 스타일 프롬프트를 쓰면 응답 토큰이 늘어납니다. 매 턴마다 추론 텍스트가 붙으니, 루프가 여러 번 돌면 그만큼 출력 토큰 비용이 쌓입니다. 저는 개발/디버깅 단계에서는 이 지시를 반드시 켜 두고, 프로덕션에서 이미 안정적으로 검증된 좁은 작업(도구 하나짜리 단순 조회 등)에는 꺼서 비용을 아끼는 편입니다. 모든 에이전트에 무조건 ReAct를 강제할 필요는 없습니다.
텍스트를 파싱하지 마라
여기서 흔히 하는 실수 하나. 모델이 뱉은 추론 텍스트에서 다음 행동을 문자열 파싱으로 뽑아내려는 시도입니다.
# 하지 마세요
if "부산" in text_block.text:
call_weather_tool("부산")이건 ReAct 이전 시대의 방식이고, 지금은 완전히 불필요합니다. Tool Use API가 나온 뒤로는 모델이 무엇을 실행할지는 tool_use 블록이라는 구조화된 형태로 이미 주어집니다. 텍스트는 사람이 읽고 디버깅하기 위한 부가 정보일 뿐, 실행 로직이 텍스트를 다시 해석할 이유가 없습니다. 텍스트 파싱으로 행동을 결정하면 모델이 표현을 조금만 바꿔도 코드가 깨지는, 아주 취약한 시스템이 됩니다.
다음 장에서는 이 추론 텍스트와 도구 결과가 쌓이면서 컨텍스트가 어떻게 불어나는지, 그리고 그걸 어떻게 관리하는지를 다룹니다.