ReAct와 ReActV2
ReAct와 ReActV2
이 문서가 설명하는 것
ReAct는 DSPy의 범용 도구 사용 에이전트 루프예요. 언어 모델이 작업에 대해 추론하고, 도구를 고르고, 그 결과를 관찰하고, 시그니처의 출력을 만들 수 있을 때까지 반복합니다.
DSPy는 두 구현 사이를 전환하고 있어요. dspy.ReAct가 현재 구현입니다. dspy.ReActV2는 그 실험적·구조화 history 대체품이며, DSPy 3.5에서 정식 dspy.ReAct 이름 뒤의 구현이 될 예정이에요. dspy.ReActV2 이름은 3.5 릴리스 계열 동안 지원 중단된 호환 별칭으로 남고, DSPy 3.6에서 제거됩니다.
이 페이지는 그 전환에 대비하고 무엇이 바뀌는지 이해하기 위해 읽으면 좋아요. 각 구현이 history를 어떻게 저장·포맷하고, 도구를 어떻게 호출하며, 실행을 어떻게 끝내고, 프로바이더 프롬프트 캐싱과 어떻게 상호작용하는지 다룹니다. 도구를 정의하거나 MCP에서 import하는 건 도구와 MCP를 보세요.
ReAct에서 ReActV2로의 전환
두 모듈 모두 같은 작업 시그니처, 도구 목록, 반복 한도를 받아요.
import dspy
def lookup(query: str) -> str:
"""Look up information relevant to a query."""
return search_index(query)
agent = dspy.ReActV2("question -> answer", tools=[lookup], max_iters=10)
result = agent(question="What is DSPy?")
실행 모델은 다릅니다.
dspy.ReAct |
dspy.ReActV2 |
|
|---|---|---|
| 상태 | DSPy 3.4까지 현재 구현; 3.5에서 ReActV2 구현 채택 | 실험적 이름; 3.5 내내 dspy.ReAct의 지원 중단 별칭이 되고 3.6에서 제거 |
| 저장된 history | 평평한 trajectory 딕셔너리 |
구조화된 dspy.History 이벤트 |
| 모델 지향 history | 전체 trajectory를 현재 입력으로 포맷 | 이전 user·assistant·tool 메시지를 별도 턴으로 재생 |
| 도구 선택 | next_tool_name + next_tool_args |
dspy.ToolCalls |
| 모델 턴당 호출 수 | 하나 | 하나 이상 |
| 완료 | finish, 그다음 별도 추출 LM 호출 |
submit이 최종 타입 출력을 직접 전달 |
| 반환되는 진단 | prediction.trajectory |
prediction.history와 prediction.termination_reason |
| 프롬프트 캐싱 | 변화하는 trajectory를 한 필드로 재전송 | 안정된 이전 메시지가 재사용 가능한 접두어로 남음 |
새 에이전트 작업에는, 실험적 API를 받아들일 수 있다면 지금 dspy.ReActV2를 쓰세요. 3.5에서 dspy.ReAct가 될 실행 모델로의 조기 경로를 줍니다. 기존 dspy.ReAct 프로그램은 업그레이드까지 현재 구현에 남을 수 있지만, 아래 요약된 history와 출력 변화에 대비해야 합니다. 실험적 ReActV2 이름을 쓰는 동안에는 DSPy 버전을 고정(pin)하세요.
ReAct: trajectory 기반 실행
history 저장 방식
ReAct는 삽입 순서가 있는 trajectory 딕셔너리 하나를 만듭니다. 매 반복마다 네 개의 키를 추가해요.
{
"thought_0": "I should look up the release notes.",
"tool_name_0": "lookup",
"tool_args_0": {"query": "DSPy release notes"},
"observation_0": "...",
}
딕셔너리는 prediction.trajectory로 돌아옵니다. 이것은 이 호출을 위한 실행 상태이지, 이후 호출에 넘기려는 대화 객체가 아닙니다.
history가 모델에게 포맷되는 방식
모든 LM 호출 전에 ReAct._format_trajectory가 trajectory 키들에서 임시 시그니처를 만들고 활성 어댑터에 그 포맷을 요청합니다. ChatAdapter는 필드 마커를, JSONAdapter는 JSON을, XMLAdapter는 태그를 사용해요. 그런 다음 ReAct는 그 포맷된 값을 내부 예측기의 trajectory 입력으로 제공합니다.
그래서 trajectory는 루프 안에서는 구조적으로 저장되지만, 모델 경계에서는 하나의 포맷된 입력 값이 됩니다. 매 반복마다 완전하고 이제 더 길어진 trajectory가 다시 포맷돼 원래 작업 입력과 함께 전송됩니다.
요청이 컨텍스트 윈도우를 초과하면, ReAct는 가장 오래된 완전한 thought/tool/args/observation 그룹을 버리고 최대 세 번까지 재시도합니다. truncate_trajectory를 오버라이드해 다른 보존 정책을 구현하세요.
도구 호출 방식
내부 dspy.Predict가 next_thought, 등록된 도구 이름으로 제약된 next_tool_name, 그리고 next_tool_args 딕셔너리를 만듭니다. ReAct는 반복마다 **정확히 하나의 선택된 dspy.Tool**을 키워드 인자로 실행합니다.
도구 예외는 잡혀서 다음 observation으로 저장되고, 모델은 다음 반복에서 회복할 수 있습니다. ReAct는 인자가 없는 finish 도구도 등록하는데, 그것을 선택하면 루프가 끝납니다. max_iters는 모델이 finish를 절대 선택하지 않을 때의 하드 스톱을 제공합니다.
루프 이후에는
finish를 선택하거나, max_iters에 도달하거나, 회복 불가능한 모델/컨텍스트 에러를 만나면 탐색이 끝나지만 선언된 출력이 직접 만들어지지는 않아요. ReAct는 원래 작업 입력과 포맷된 trajectory 위에서 별도의 dspy.ChainOfThought 추출기를 실행합니다. 추출기가 시그니처의 출력 필드를 종합하고 타입을 붙입니다. 반환된 예측은 그 필드들과 trajectory를 결합합니다.
즉, 정상적인 ReAct 실행은 도구 루프 이후에 LM 호출이 적어도 하나 더 있습니다.
프롬프트 캐싱 동작
ReAct는 커져가는 trajectory를 매 반복마다 새로 포맷된 입력 하나로 다시 보냅니다. 지시사항과 원래 작업 입력은 여전히 캐시 가능한 접두어를 형성할 수 있지만, trajectory를 담는 user 콘텐츠는 추가된 assistant/tool 메시지로 표현되는 게 아니라 매 턴 바뀝니다. 프로바이더는 변화하는 부분을 효과적으로 재사용하지 못하고, 별도 추출 요청은 시그니처와 프롬프트 모양이 다릅니다.
ReActV2: 구조화 history 실행
history 저장 방식
ReActV2는 dspy.History 하나를 소유하며, 그 messages 목록은 루프 턴 하나당 구조화 이벤트 하나를 담습니다. 이벤트는 다음을 포함할 수 있어요.
- 원래 시그니처 입력 — 첫 번째 새 턴에만 포함.
next_thought— 모델이 돌려줄 때.- 모든 요청된 호출과 그 호출 ID를 담은
dspy.ToolCalls객체. - 각 ID를 도구 이름·값·에러 상태와 짝짓는 첨부된
ToolCallResults. - 턴이 성공적으로
submit을 호출할 때의 시그니처 최종 출력 필드.
개념적으로 턴은 이렇게 생겼어요.
{
"question": "What is DSPy?", # first new turn only
"next_thought": "I should look it up.",
"tool_calls": dspy.ToolCalls(
tool_calls=[
dspy.ToolCalls.ToolCall(
id="call_123",
name="lookup",
args={"query": "DSPy"},
)
],
tool_call_results=...,
),
}
반환된 예측은 이 객체를 prediction.history로 노출합니다. 그 dspy.History(또는 직렬화 표현)를 history=로 다시 넘기면 이전 실행에서 계속할 수 있어요.
history가 모델에게 포맷되는 방식
어댑터는 각 history 이벤트를 하나의 trajectory 필드로 평평화하지 않고 여러 모델 메시지로 바꿉니다.
네이티브 함수 호출이 켜져 있으면, 완료된 턴은 이렇게 재생됩니다.
user original input fields, when present
assistant next_thought plus native tool_calls
tool one result message per call, matched by tool_call_id
호출 ID는 프로바이더에서 보존됩니다. ReActV2는 네이티브가 아닌 모델 출력이 ID를 공급하지 않을 때 결정적 ID를 생성해요.
네이티브 함수 호출이 꺼져 있으면, history는 DSPy 안에서 구조화된 채로 남지만, 어댑터는 assistant 필드를 일반 텍스트/JSON/XML 포맷으로 렌더링하고 도구 결과는 뒤따르는 user 메시지로 렌더링합니다. 이렇게 ReActV2는 네이티브 도구 API를 노출하지 않는 모델에서도 하나의 내부 history 표현을 유지하며 동작합니다.
네이티브 호출은 어댑터 설정입니다.
adapter = dspy.ChatAdapter(
use_native_function_calling=True,
parallel_tool_calls=True,
)
with dspy.context(adapter=adapter):
result = agent(question="Compare two releases.")
프로바이더 지원은 다양합니다. JSONAdapter는 기본적으로 네이티브 함수 호출을 켜고, ChatAdapter는 위에 보인 명시적 설정이 필요해요.
도구 호출 방식
내부 예측기가 dspy.ToolCalls를 돌려주므로, 한 모델 턴이 여러 도구를 요청할 수 있습니다. ReActV2는 각 요청된 호출/결과 쌍을 ID로 보존합니다. parallel_tool_calls 어댑터 옵션은 가능한 프로바이더에게 같은 턴에서 여러 독립 호출을 생성하도록 요청하며, ReActV2는 현재 Python에서 반환된 호출을 하나씩 순차 실행합니다.
생성자에 넘긴 callable은 dspy.Tool로 변환됩니다. 알 수 없는 도구 이름과 실행 예외는 history의 에러 결과가 되어 다음 모델 턴이 대응할 수 있게 합니다.
ReActV2는 submit을 내부 도구로 예약합니다. 그 인자 스키마는 원래 시그니처의 출력 필드에서 생성되므로, 최종 값들이 다른 모든 동작과 같은 구조화 호출 경로를 탑니다.
각 턴 이후와 종료 시
ReActV2는 턴의 모든 호출을 실행한 뒤 그 결과를 ToolCalls에 붙이고, 이벤트를 history에 추가하고, 원래 입력의 중복 사본 없이 모델을 다시 호출합니다. 호출 중 하나가 submit을 성공적으로 실행하면 ReActV2는 termination_reason="submit"과 함께 그 타입 필드를 즉시 돌려줍니다 — 별도 추출 모듈이 없어요.
루프가 max_iters에 도달하거나, 호출을 돌려주지 않거나, 파싱에 실패하거나, 컨텍스트 윈도우를 초과하면 ReActV2는 submit을 요청하는 마지막 LM 호출 한 번을 합니다. 네이티브 함수 호출이 켜져 있으면 tool_choice를 submit으로 설정해 프로바이더가 그 선택을 강제합니다. 네이티브가 아닌 모드에서는 어댑터가 tool_choice를 제거하고 포맷된 프롬프트로 submit을 요청하므로, 구조화 제출이 보장되지는 않아요. 성공적인 폴백은 termination_reason="forced_submit"을 돌려줍니다. 그 시도가 실패하면 예측은 여전히 history와 왜 정상 루프가 멈췄는지 설명하는 종료 이유를 돌려주지만, 선언된 출력 필드를 담지 못할 수 있습니다.
ReAct와 달리 ReActV2는 현재 컨텍스트 오버플로에 기존 history 이벤트를 자르지 않습니다.
프롬프트 캐싱 동작
구조화 history는 각 완료된 턴을 append-only 메시지 그룹으로 만듭니다. 다음 요청에서 시스템 지시사항과 모든 이전 user/assistant/tool 메시지는 안정적인 접두어로 남고, 가장 새로운 결과와 요청만 추가됩니다. 프롬프트 캐싱이 있는 프로바이더는 따라서 계속 커지는 새로 포맷된 trajectory 값을 재처리하는 대신 이전 요청의 더 많은 부분을 재사용할 수 있어요.
Anthropic 모델의 경우, LiteLLM의 cache-control 주입 지점으로 프로바이더 측 프롬프트 캐싱을 LM에 켜세요.
lm = dspy.LM(
"anthropic/claude-sonnet-4-5-20250929",
cache_control_injection_points=[
{"location": "message", "role": "system"},
{"location": "message", "index": -1},
],
)
with dspy.context(lm=lm):
result = agent(question="What is DSPy?")
첫 번째 주입 지점은 안정적인 시스템 지시사항을 캐시합니다. 두 번째는 끝 턴에 체크포인트를 두어, ReActV2가 history를 추가할 때 Anthropic이 앞선 대화 접두어를 재사용하게 합니다. Anthropic은 모델의 최소 토큰 수를 충족하는 접두어만 캐시하고, 기본 임시 캐시는 수명이 제한돼 있습니다. 일반 DSPy 설정은 Using Provider-Side Prompt Caching, 프로바이더 세부는 LiteLLM 프롬프트 캐싱 문서를 보세요.
네이티브 도구 재생이 프로바이더에 가장 깨끗한 구조를 주지만, 네이티브가 아닌 모드도 안정적인 멀티턴 history의 이점을 얻습니다. 내부 테스트에서 일부 작업의 비용이 최대 50% 줄었어요. 실제 절감은 프로바이더의 캐시 정책, 모델, 요청 모양, 도구 결과 크기에 달려 있으며 모든 워크로드에 보장되지는 않습니다.
교체와 마이그레이션 계획
ReActV2는 두 개의 영구 ReAct API의 시작이 아니라 임시 실험적 이름입니다. 그 구조화 history 구현이 DSPy 3.5에서 현재 dspy.ReAct 구현을 대체할 예정이고, 정식 공개 이름은 계속 dspy.ReAct로 남아요.
실험적 이름 사용자가 즉시 이름을 바꾸도록 강요하지 않기 위해, dspy.ReActV2는 DSPy 3.5 릴리스 계열 전체에 걸쳐 지원 중단된 호환 별칭으로 남습니다. 별칭을 쓰면 dspy.ReAct로 마이그레이션하라는 경고가 뜨고, 별칭은 DSPy 3.6에서 제거됩니다. 다시 말해 코드는 3.5로 업그레이드한 후 언제든 dspy.ReActV2(...)에서 dspy.ReAct(...)로 옮길 수 있고, 3.6으로 업그레이드하기 전에 그렇게 해야 합니다.
기존 dspy.ReAct 프로그램은 DSPy 3.5로 업그레이드하기 전에 바꿀 필요가 없습니다. 업그레이드 전에 prediction.trajectory, 별도 추출기 예측기, 턴당 도구 호출 하나, 커스텀 trajectory 자르기에 의존하는 코드를 검토하세요. 그 동작들은 prediction.history, 직접 submit 출력, 턴당 여러 호출, 구조화 메시지 재생으로 대체됩니다.
API 요약
dspy.ReAct(signature, tools, max_iters=20)
안정적인 trajectory 기반 루프와 최종 추출 패스를 실행합니다.
ReAct.forward(**inputs) / ReAct.aforward(**inputs)
동기 또는 비동기 루프를 실행하고 출력 필드와 trajectory를 돌려줍니다.
ReAct.truncate_trajectory(trajectory)
컨텍스트 윈도우 에러 후 가장 오래된 완전한 도구 호출 그룹을 버립니다. 정책을 바꾸려면 오버라이드하세요.
dspy.ReActV2(signature, tools, max_iters=20)
실험적 구조화 history 루프를 실행하고 최종 출력에 submit을 예약합니다.
ReActV2.forward(history=None, max_iters=None, **inputs)
선택적 연속 history와 호출별 반복 오버라이드를 받습니다. 제출이 성공하면 출력과 함께 history와 termination_reason을 돌려줍니다.
관련 문서
- 도구와 MCP — 이 에이전트들이 호출하는 도구를 정의·래핑·검증·import하는 법.
- 어댑터: 시그니처가 프롬프트가 되는 법 —
History,ToolCalls, 네이티브 함수 호출의 포맷 경계. - 모듈: 직접 조합하기 — 두 구현이 공유하는 트레이싱, 상태, 조합.
dspy.ReActAPI 참조dspy.ReActV2API 참조