ReAct — 생각 · 행동 · 관찰의 반복
한 줄 요약
도구를 한 번 쓰고 끝나는 게 아니라, 결과를 보고 다음 행동을 다시 정하는 반복이 필요합니다. 이 구조를 ReAct 라고 부르며, 에이전트가 실제로 동작하는 방식이 바로 이것입니다.
1. 왜 한 번으로는 안 되나
이 질문을 보세요.
"승승 스마트워치 Fit 5는 얼마인가요? 요즘 스마트워치 시장 가격대와 비교하면 어느 수준인지도 알려주세요."
필요한 일이 둘입니다.
① 우리 상품 가격을 안다 → get_product_info
② 시장 가격대를 안다 → search_web
③ 둘을 비교해서 답한다
게다가 순서가 있습니다. ①을 먼저 해야 "어느 수준인지" 비교할 대상이 생깁니다.
3장의 구조로는 이걸 못 합니다.
[3장] 질문 → 요청 → 실행 → 결과 전달 → 답변 (도구 한 번)
결과를 보고 "아직 부족하니 하나 더" 를 할 자리가 없습니다.
2. ReAct — 생각 · 행동 · 관찰
ReAct 는 Reasoning(추론) + Acting(행동) 을 합친 말입니다. 구조는 이렇습니다.
핵심은 "관찰 → 다시 생각" 입니다. 도구 결과를 보고 다음에 무엇을 할지 새로 정합니다.
3. 우리 질문에 대입하면
[1단계] 생각: 자사 가격부터 확인하자
행동: get_product_info(product_name="승승 스마트워치 Fit 5")
관찰: "승승 스마트워치 Fit 5 | 전자기기 | 판매가 159,000원 | 평점 4.8"
[2단계] 생각: 가격은 알았다. 이제 시장 가격대를 알아야 비교할 수 있다
행동: search_web(query="스마트워치 시장 가격대")
관찰: "- 스마트워치 시장 규모... 5만원대 가성비부터 30만원대 프리미엄까지..."
[3단계] 생각: 둘 다 확인했다. 이제 답할 수 있다
행동: (도구 호출 없음)
→ 최종 답변: "159,000원입니다. 시장은 5만~30만원대이므로
중간 가격대입니다."
2단계의 검색어가 1단계 결과를 반영합니다. 우리 상품이 159,000원이라는 걸 알았으니 그에 맞는 시장 정보를 찾는 것이죠. 이게 "관찰하고 다시 생각한다"의 실제 모습입니다.
4. 코드에서는 어떻게 되나
for 루프 하나입니다. 놀랄 만큼 단순합니다.
아래는 code/ch04_web_agent.py 의 일부입니다 — 위에서 이미 붙여 넣었으니 다시 넣지 마세요.
history = [types.Content(role="user", parts=[types.Part(text=question)])]
for step in range(1, MAX_STEPS + 1):
resp = client.models.generate_content(model=..., contents=history, config=config)
if not resp.function_calls: # 부를 도구가 없다 = 끝
return resp.text.strip()
history.append(resp.candidates[0].content) # 모델의 '결정'을 기록
for fc in resp.function_calls:
result = TOOLS[fc.name](**dict(fc.args)) # 실행은 우리가
history.append(types.Content(role="user", parts=[
types.Part.from_function_response(
name=fc.name, response={"result": result})])) # 관찰을 기록
세 개념이 코드의 어느 부분에 해당하는지 짚어 봅시다.
| ReAct | 코드의 어디 |
|---|---|
| 생각 | generate_content(...) 호출 — 모델이 판단하는 자리 |
| 행동 | TOOLS[fc.name](**dict(fc.args)) — 우리가 실행 |
| 관찰 | history.append(...from_function_response...) — 결과를 기록 |
| 반복 | for step in range(...) |
| 종료 | if not resp.function_calls: return |
5. history 가 하는 일 — 2장이 여기서 쓰입니다
2장에서 손으로 대화를 쌓았던 그 방식 그대로입니다. 다만 쌓는 내용이 다릅니다.
[2장 history] [4장 history]
user : "제 이름은 김민준입니다" user : "가격이 얼마인가요?"
model : "김민준 고객님..." model : (get_product_info 호출 요청)
user : "제 이름이 뭐죠?" user : (함수 실행 결과 159,000원)
model : (search_web 호출 요청)
user : (함수 실행 결과 시장 정보)
도구 호출 요청과 실행 결과도 대화의 일부로 쌓입니다. 그래야 모델이 "아, 아까 가격을 알아냈지"를 알 수 있습니다.
2장에서 배운 원칙이 그대로 적용됩니다 — 모델은 무상태다. 기억은 우리가 기록을 쌓아 만드는 것이다.
Part 가 여러 종류인 이유
2장에서 types.Part(text=...) 를 썼습니다. 여기서는 새 종류가 나옵니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
types.Part.from_function_response(name=fc.name, response={"result": result})
2장에서 "한 번의 발언에 글·이미지·도구 결과가 섞일 수 있어서 parts 가 목록" 이라고 했던 게 이것입니다. 도구 실행 결과 전용 Part 입니다.
6. 왜 손으로 짜 보나
5장에서는 이 30줄이 한 줄로 줄어듭니다.
아래는 개념 설명용 코드입니다 — 실습 파일에 넣지 않습니다.
llm = get_chat(temperature=0) # common.py 가 만들어 주는 모델(5장에서 설명)
agent = create_agent(llm, tools=[...], system_prompt=SYSTEM) # 5장
그래도 지금 손으로 짜 보는 이유가 셋입니다.
① 무엇이 감춰지는지 알기 위해
create_agent 가 대신해 주는 것이 정확히 이 루프입니다. 안을 본 적이 있으면, 에이전트가 이상하게 굴 때 "루프가 몇 번 돌았지?", "관찰이 제대로 들어갔나?" 를 떠올릴 수 있습니다.
② 통제권을 어디서 잃는지 알기 위해
MAX_STEPS = 5 같은 안전장치를 우리가 직접 걸어 봤기 때문에, 라이브러리에서 그에 해당하는 설정을 찾을 줄 알게 됩니다.
③ 8장의 밑그림이기 때문에
8장의 팀장(플래너)–담당자(워커) 구조는 이 루프를 한 단계 위에서 다시 그린 것입니다. 담당자 하나하나가 각자 이 루프를 돌고, 팀장이 그 위에서 계획을 세웁니다.
7. ReAct라는 이름에 대해
논문에서 온 정식 용어입니다(ReAct: Synergizing Reasoning and Acting in Language Models, 2022). 검색할 때 이 이름을 쓰면 자료가 많이 나옵니다.
다른 이름으로도 불립니다 — 에이전트 루프(agent loop), 도구 사용 루프(tool-use loop), 툴 콜링 루프. 전부 같은 것을 가리킵니다.
이름은 여러 개지만 내용은
while문 하나입니다. "에이전트"라는 말이 거창하게 들릴 때 이걸 떠올리세요 — 도구를 쓰고, 결과를 보고, 다시 판단하는 반복문. 그게 전부입니다.
핵심 정리
- 도구를 여러 번, 순서대로 써야 하는 질문에는 반복이 필요합니다.
- ReAct = 생각(모델 호출) → 행동(도구 실행) → 관찰(결과 기록) → 다시 생각.
- 코드에서는
for루프 하나입니다. 종료 조건은 "부를 도구가 없다" 입니다. - 도구 호출 요청과 실행 결과도
history에 쌓입니다. 2장에서 대화를 쌓던 것과 같은 원리입니다. - 도구 결과는
Part.from_function_response(...)라는 전용 형태로 넣습니다. - 5장부터 라이브러리가 이 루프를 대신 돌립니다. 지금 손으로 짜 보는 것이 안을 볼 마지막 기회입니다.
- 8장의 멀티에이전트는 이 루프를 한 단계 위에서 다시 그린 것입니다.