ReAct와 ReActV2

ReAct와 ReActV2 (ReAct and ReActV2)

ReAct는 DSPy의 범용 도구 사용 에이전트 루프입니다. 언어 모델이 작업에 대해 추론하고, 도구를 고르고, 결과를 관찰하며, 시그니처의 출력을 만들 수 있을 때까지 반복해요.

출처: 문서

본문

DSPy는 두 구현 사이를 전환 중입니다. dspy.ReAct 는 현재 구현이고, dspy.ReActV2 는 그 실험적인 구조화-히스토리 대체재이며 DSPy 3.5에서 정식 dspy.ReAct 이름 뒤의 구현이 될 예정입니다. dspy.ReActV2 이름은 3.5 릴리스 라인 내내 deprecated 호환 별칭으로 남아 있다가 DSPy 3.6에서 제거될 것입니다.

이 페이지는 그 전환에 대비하고 무엇이 바뀌는지 이해하기 위해 읽으세요. 각 구현이 히스토리를 어떻게 저장·포맷하는지, 도구를 어떻게 호출하는지, 실행을 어떻게 끝내는지, 공급자 프롬프트 캐싱과 어떻게 상호작용하는지가 그것입니다. 도구를 정의하거나 MCP에서 임포트하는 것은 도구와 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 의 deprecated 별칭이 되고 3.6에서 제거
저장된 히스토리 평면 trajectory 사전 구조화된 dspy.History 이벤트
모델-대면 히스토리 전체 trajectory가 현재 입력으로 포맷됨 이전 유저·어시스턴트·도구 메시지가 별도 턴으로 재생됨
도구 선택 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 프로그램은 업그레이드까지 현재 구현에 머물 수 있지만, 아래에 요약된 히스토리·출력 변화에 대비해야 합니다. 실험적 ReActV2 이름을 쓰는 동안에는 DSPy 버전을 고정하세요.

ReAct: trajectory 기반 실행

히스토리가 저장되는 방식

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 로 반환됩니다. 그것은 이 호출을 위한 실행 상태이지, 나중 호출에 넘겨질 대화 객체가 아닙니다.

모델을 위해 히스토리가 포맷되는 방식

매 LM 호출 전에 ReAct._format_trajectory 는 trajectory 키들로부터 임시 시그니처를 만들고 활성 어댑터에게 그것을 포맷하라고 요청합니다. ChatAdapter 는 필드 마커를, JSONAdapter 는 JSON을, XMLAdapter 는 태그를 씁니다. ReAct는 그 포맷된 값을 내부 predictor의 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를 담은 유저 콘텐츠는 매 턴 바뀌며, 추가된 어시스턴트/도구 메시지로 표현되지 않습니다. 공급자는 변하는 부분을 그만큼 효과적으로 재사용할 수 없고, 별도의 추출 요청은 다른 시그니처와 프롬프트 모양을 가집니다.

ReActV2: 구조화-히스토리 실행

히스토리가 저장되는 방식

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= 로 다시 넘기면 이전 실행에서 이어갈 수 있습니다.

모델을 위해 히스토리가 포맷되는 방식

어댑터는 히스토리의 각 이벤트를 하나의 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는 공급자에서 보존됩니다. 네이티브가 아닌 모델 출력이 ID를 공급하지 않을 때는 ReActV2가 결정적 ID를 생성합니다.

네이티브 함수 호출이 비활성이면, 히스토리는 DSPy 안에서 구조화된 채로 유지되지만 어댑터는 어시스턴트 필드를 일반 텍스트/JSON/XML 형식으로 렌더링하고 도구 결과는 뒤따르는 유저 메시지로 렌더링합니다. 이것은 ReActV2가 네이티브 도구 API를 노출하지 않는 모델에서도 하나의 내부 히스토리 표현을 유지하면서 동작하게 합니다.

네이티브 호출은 어댑터 설정입니다:

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 는 위에 보인 명시적 설정을 요구합니다.

도구가 호출되는 방식

내부 predictor가 dspy.ToolCalls 를 반환하므로 한 모델 턴이 여러 도구를 요청할 수 있습니다. ReActV2는 요청된 호출/결과 쌍을 각각 ID로 보존합니다. parallel_tool_calls 어댑터 옵션은 유능한 공급자에게 같은 턴에서 여러 독립 호출을 생성하라고 요청하며, ReActV2는 현재 반환된 호출들을 Python에서 하나씩 순차 실행합니다.

생성자에 공급된 callable은 dspy.Tool 로 변환됩니다. 알 수 없는 도구 이름과 실행 예외는 히스토리에서 오류 결과가 되어 다음 모델 턴이 그것에 대응할 수 있습니다.

ReActV2는 submit 을 내부 도구로 예약합니다. 그 인자 스키마는 원래 시그니처의 출력 필드에서 생성되므로, 최종 값이 다른 모든 행동과 같은 구조화된 호출 경로를 통해 이동합니다.

각 턴 후와 종료 시에 일어나는 일

한 턴의 모든 호출을 실행한 뒤 ReActV2는 그 결과를 ToolCalls 에 첨부하고, 이벤트를 히스토리에 추가하며, 원래 입력의 중복 복사본 없이 모델을 다시 호출합니다. 호출 중 하나가 submit 을 성공적으로 호출하면 ReActV2는 termination_reason="submit" 과 함께 그 타입 필드를 즉시 반환합니다. 별도 추출 모듈은 없습니다.

루프가 max_iters 에 도달하거나, 호출을 반환하지 않거나, 파싱에 실패하거나, 컨텍스트 창을 초과하면 ReActV2는 submit 을 요청하는 마지막 LM 호출을 한 번 합니다. 네이티브 함수 호출이 활성이면 tool_choice 를 submit 으로 설정해 공급자가 그 선택을 강제합니다. 네이티브가 아닌 모드에서는 어댑터가 tool_choice 를 제거하고 포맷된 프롬프트를 통해 submit 을 요청하므로, 구조화된 제출이 보장되지 않습니다. 성공적인 폴백은 termination_reason="forced_submit" 를 반환합니다. 그 시도가 실패하면 예측은 여전히 히스토리와 보통 루프가 왜 멈췄는지 설명하는 종료 이유를 반환하지만, 선언된 출력 필드를 담지 않을 수 있습니다.

ReAct와 달리 ReActV2는 현재 컨텍스트 오버플로 시 오래된 히스토리 이벤트를 잘라내지 않습니다.

프롬프트 캐싱 동작

구조화된 히스토리는 각 완료된 턴을 추가 전용(append-only) 메시지 그룹으로 만듭니다. 다음 요청에서 시스템 지시문과 이전의 모든 유저/어시스턴트/도구 메시지는 안정적인 프리픽스로 남고, 가장 새로운 결과와 요청만 추가됩니다. 프롬프트 캐싱이 있는 공급자는 따라서 계속 커지고 새로 포맷되는 하나의 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가 히스토리를 추가할 때 Anthropic이 앞선 대화 프리픽스를 재사용하게 합니다. Anthropic은 모델의 최소 토큰 수를 충족하는 프리픽스만 캐시하고, 기본 일시적 캐시는 제한된 수명을 가집니다. 일반 DSPy 설정은 공급자 측 프롬프트 캐싱 사용하기를, 공급자 세부사항은 LiteLLM 프롬프트 캐싱 문서를 참고하세요.

네이티브 도구 재생이 가장 깨끗한 공급자-가시 구조를 주지만, 네이티브가 아닌 모드도 안정적인 다중 턴 히스토리의 혜택을 받습니다. 내부 테스트에서 일부 작업은 최대 50%의 비용 절감을 보았습니다. 실제 절감은 공급자의 캐시 정책, 모델, 요청 모양, 도구 결과의 크기에 달려 있으며, 모든 워크로드에 보장되지는 않습니다.

교체 및 마이그레이션 계획

ReActV2는 두 개의 영구 ReAct API의 시작이 아니라 임시 실험적 이름입니다. 그 구조화-히스토리 구현이 DSPy 3.5에서 현재 dspy.ReAct 구현을 교체합니다. 정식 공개 이름은 dspy.ReAct 로 유지됩니다.

실험적 이름의 사용자들이 즉시 이름을 바꾸지 않도록, dspy.ReActV2 는 DSPy 3.5 릴리스 라인 내내 deprecated 호환 별칭으로 남아 있습니다. 별칭을 쓰면 사용자에게 dspy.ReAct 로 마이그레이션하라고 경고하며, 3.6에서 제거됩니다. 즉, 코드는 3.5로 업그레이드한 후 언제든 dspy.ReActV2(...) 에서 dspy.ReAct(...) 로 옮길 수 있고, 3.6으로 업그레이드하기 전에 해야 합니다.

기존 dspy.ReAct 프로그램은 DSPy 3.5로 업그레이드하기 전에 바꿀 필요가 없습니다. 업그레이드 전에 prediction.trajectory, 별도 추출기 predictor, 턴당 하나의 도구 호출, 커스텀 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) 실험적 구조화-히스토리 루프를 실행하고 최종 출력을 위해 submit 을 예약합니다.

ReActV2.forward(history=None, max_iters=None, **inputs) 선택적 연속 히스토리와 호출별 반복 오버라이드를 받습니다. 제출이 성공하면 출력과 함께 history 와 termination_reason 을 반환합니다.

크로스링크

더 알아보기 (Learn more)