- Published on
ReAct vs Plan-and-Execute로 툴콜 실패 줄이기
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버/에이전트가 툴을 호출하는 순간, LLM은 더 이상 “그럴듯한 텍스트 생성기”가 아니라 “API 클라이언트”가 됩니다. 이때 실패는 모델 성능보다 오케스트레이션 패턴(ReAct, Plan-and-Execute), 스키마 설계, 재시도 정책, 관측 가능성에 의해 더 크게 좌우됩니다.
이 글에서는 실무에서 자주 터지는 툴콜 실패 유형을 분해하고, ReAct와 Plan-and-Execute(이하 PnE)를 어떤 조건에서 선택해야 실패율을 낮출 수 있는지, 그리고 두 패턴 모두에 적용 가능한 가드레일을 코드와 함께 정리합니다.
관련해서 툴콜 자체가 400으로 깨지는 케이스는 아래 글도 함께 보면 좋습니다.
툴콜 실패의 대표 원인 6가지
툴콜 실패를 “모델이 멍청해서”로 뭉뚱그리면 개선이 어렵습니다. 실패는 보통 아래 레이어 중 하나에서 발생합니다.
- 스키마 불일치: 필수 필드 누락, 타입 불일치, enum 오타, 날짜 포맷 불일치
- 툴 선택 오류: 필요한 툴이 아닌 다른 툴을 호출하거나, 툴을 호출하지 않고 답변을 생성
- 인자 과대/과소 추론: 사용자가 주지 않은 값을 모델이 임의로 채우거나(환각), 반대로 필요한 값을 비워둠
- 상태 관리 실패: 이전 툴 결과를 잘못 참조, 스텝 간 컨텍스트 유실
- 무한루프/반복 호출: 같은 툴을 같은 인자로 반복 호출
- 타임아웃/레이트리밋: 외부 API 지연, 동시성 폭발, 재시도 폭주
무한루프 방지는 별도 체크리스트가 도움이 됩니다.
ReAct와 Plan-and-Execute의 차이: 실패가 나는 지점이 다르다
ReAct: “생각-행동-관찰”의 짧은 루프
ReAct는 한 번에 큰 계획을 세우기보다, 한 스텝씩 툴을 호출하고 관찰 결과를 반영합니다.
- 장점
- 불확실한 문제에서 강함: 검색, 진단, 탐색형 작업
- 중간 결과를 보고 다음 행동을 수정하므로 “초기 계획 오류”에 강함
- 각 스텝이 작아 인자 생성 난이도가 낮아지는 경우가 많음
- 단점
- 루프가 길어져 툴 호출 횟수 증가 → 실패 확률 누적
- 종료 조건이 약하면 무한루프 위험
- 여러 툴을 조합해야 하는 업무(예: 결제-배송-알림)에서 상태가 꼬이기 쉬움
Plan-and-Execute: “먼저 계획, 그 다음 실행”
PnE는 먼저 전체 계획(또는 최소한의 실행 그래프)을 만들고, 그 계획대로 툴을 실행합니다.
- 장점
- 툴 호출이 구조화되어 중복 호출/루프가 줄어듦
- 단계별 입력/출력을 계약처럼 정의하기 쉬워 스키마 안정성이 올라감
- 비용/지연 예측이 쉬움(스텝 수가 상한을 가짐)
- 단점
- 초기 계획이 틀리면 전체가 틀어짐(특히 탐색형 문제)
- 계획 단계에서 이미 “없는 데이터”를 가정하는 환각이 나오면, 실행이 연쇄 실패
- 계획이 과도하게 상세하면 오히려 유연성이 떨어짐
정리하면,
- 탐색/진단/검색 기반: ReAct가 유리(단, 루프 가드 필수)
- 업무 프로세스/트랜잭션 기반: PnE가 유리(단, 계획 검증 필수)
어떤 패턴이 툴콜 실패를 더 줄이나: 실전 기준
1) “필수 입력이 명확한가?”가 1순위
- 필수 입력이 명확하고 누락이 치명적이면 PnE가 유리합니다.
- 예:
create_ticket(project_id, title, severity)같은 업무형
- 예:
- 입력이 불완전하고, 툴로 정보를 찾아 채워야 하면 ReAct가 유리합니다.
- 예: “장애 원인 찾아줘” → 로그 조회/메트릭 조회/설정 확인
2) 툴 호출 비용이 비싼가?
툴 호출이 비싸거나 레이트리밋이 빡빡하면, 호출 횟수를 예측 가능한 PnE가 실패율(특히 429/timeout)을 낮춥니다.
3) 실패가 “부분 실패”로 끝나도 되는가?
ReAct는 중간 스텝이 실패해도 다른 경로로 우회하기 쉽습니다. 반면 PnE는 계획대로 못 가면 전체가 멈추는 설계가 많습니다.
ReAct로 툴콜 실패 줄이는 5가지 가드레일
1) 스텝 상한 + 반복 호출 감지
ReAct는 “좋은 종료 조건”이 핵심입니다.
- 최대 스텝 수 제한
- 동일 툴+동일 인자 반복 시 중단
- 관찰 결과가 개선되지 않으면 중단
// TypeScript 예시: ReAct 실행 루프 가드
const MAX_STEPS = 8;
const seen = new Map<string, number>();
function fingerprint(toolName: string, args: unknown) {
return `${toolName}:${JSON.stringify(args)}`;
}
for (let step = 1; step <= MAX_STEPS; step++) {
const action = await decideNextAction(state); // LLM
const key = fingerprint(action.tool, action.args);
seen.set(key, (seen.get(key) ?? 0) + 1);
if ((seen.get(key) ?? 0) >= 2) {
state.warn = "Repeated tool call detected";
break;
}
const obs = await callTool(action.tool, action.args);
state = updateState(state, action, obs);
if (isDone(state)) break;
}
2) “부족한 입력은 질문으로 되돌리기” 규칙
모델이 모르는 값을 임의로 채우는 순간 스키마는 맞아도 결과는 틀립니다.
- 규칙: 필수 필드가 없으면 툴 호출하지 말고 사용자에게 확인 질문
- 예:
customer_id가 없으면 조회 툴을 먼저 호출하거나, 사용자에게 물어보기
프롬프트에 아래 문장을 강하게 넣는 것이 효과적입니다.
- 필수 인자가 불명확하면, 값을 추측하지 말고
clarifying_question을 반환하라.
3) 관찰(Observation)을 요약해 상태에 고정
ReAct는 컨텍스트가 길어질수록 “이전 관찰을 잘못 참조”하는 실패가 늘어납니다.
- 매 스텝마다 관찰을 구조화된 요약으로 저장
- 상태에는 원문이 아니라 “결정에 필요한 필드”만 남김
{
"host": "api-1",
"symptom": "p95 latency spike",
"recent_change": "deployed v1.8.2",
"next_hypothesis": "db connection pool exhaustion"
}
4) 툴 선택을 분리: 라우터 모델 또는 룰 기반
“어떤 툴을 쓸지”를 메인 모델에게만 맡기면 엉뚱한 툴을 호출합니다.
- 단순한 경우: 룰 기반 라우팅(키워드/상태 기반)
- 복잡한 경우: 작은 라우터 프롬프트(혹은 경량 모델)
5) 툴 결과 검증(validator) 추가
툴이 성공해도 결과가 기대 포맷이 아니면 다음 스텝이 무너집니다.
- JSON 스키마 검증
- 필수 키 존재 확인
- 값 범위 검증
Plan-and-Execute로 툴콜 실패 줄이는 6가지 가드레일
1) “계획 스키마”를 엄격하게
PnE의 핵심은 계획이 곧 계약이라는 점입니다.
- 각 스텝은
id,tool,args_template,depends_on,success_criteria를 포함 args_template는 이전 출력 참조만 허용(임의 값 생성 금지)
주의: 본문에서 부등호를 직접 쓰면 MDX에서 JSX로 오인될 수 있으니, 비교 연산은 인라인 코드로 표기합니다. 예: a < b
{
"steps": [
{
"id": "s1",
"tool": "search_incidents",
"args": { "service": "billing", "window_minutes": 60 },
"success_criteria": "returns at least 1 incident or empty list"
},
{
"id": "s2",
"tool": "get_incident_detail",
"depends_on": ["s1"],
"args": { "incident_id": "{{s1.items[0].id}}" },
"success_criteria": "detail has root_cause or timeline"
}
]
}
2) 계획 검증 단계(Plan Validation)를 별도로 둔다
실무에서 PnE가 실패하는 가장 흔한 이유는 “계획이 말은 되는데 실행이 불가능”하기 때문입니다.
검증 체크리스트:
- 존재하지 않는 툴을 호출하는가?
- 의존성이 순환하는가?
- 각 스텝의 인자에
null가능성이 있는가? - 레이트리밋/비용 상한을 넘는가?
3) 실행기는 “템플릿 치환만” 하고, 인자 생성은 하지 않는다
실행기가 똑똑해지는 순간, 모델의 환각이 런타임 환각으로 변합니다.
- 실행기는
{{s1.xxx}}같은 참조만 치환 - 치환 실패 시 즉시 중단하고 재계획
# Python 예시: args 템플릿 치환 실패 시 재계획
from jinja2 import Template, StrictUndefined
def render_args(template_dict, context):
out = {}
for k, v in template_dict.items():
if isinstance(v, str) and "{{" in v:
t = Template(v, undefined=StrictUndefined)
out[k] = t.render(**context)
else:
out[k] = v
return out
4) 재계획은 “전체 재계획”이 아니라 “부분 재계획”
PnE는 한 스텝 실패로 전체를 다시 만들면 비용과 지연이 커집니다.
- 실패한 스텝과 이후 의존 스텝만 재계획
- 이미 성공한 스텝 결과는 고정
5) 계획에 “관측 기반 분기”를 허용하되 제한적으로
PnE가 탐색형 문제에 약한 이유는 분기가 없기 때문입니다.
if분기를 허용하되, 조건은 툴 결과의 명시적 필드만 사용- 분기 수 상한을 둬서 폭발 방지
6) 최종 응답 전 “결과 감사(Result Audit)”
PnE는 실행이 끝나면 모델이 그럴듯하게 요약하면서 사실을 섞는 경우가 있습니다.
- 최종 답변은 스텝 출력에 근거한 문장만 허용
- 근거 없는 문장은 제거하거나 “추정”으로 표시
두 패턴을 섞는 하이브리드가 실패율을 가장 많이 낮춘다
실무에서는 ReAct와 PnE 중 하나만 고집하기보다, 아래처럼 섞는 방식이 툴콜 실패를 가장 많이 줄였습니다.
권장 아키텍처: “PnE로 뼈대, ReAct로 탐색”
- PnE로 상위 워크플로를 고정
- 예:
진단→원인 후보 수집→검증→조치안 작성
원인 후보 수집같은 불확실 구간만 ReAct 서브루프로 제한
- ReAct는 스텝 상한을 더 짧게(예: 4)
- 수집 결과는 구조화된 리스트로만 반환
- 다시 PnE로 돌아와 검증/조치안을 실행
이 방식은
- PnE의 예측 가능성(비용, 스텝 수)
- ReAct의 유연성(탐색)
을 동시에 가져가면서, ReAct의 무한루프와 PnE의 계획 환각을 모두 완화합니다.
실패율을 낮추는 프롬프트/스키마 설계 포인트
1) 툴 스키마는 “LLM 친화적으로”
- 필드명은 짧고 명확하게
- enum은 동의어를 줄이고, 설명을 붙이기
- 날짜/시간은 포맷을 강제(예:
YYYY-MM-DDTHH:mm:ssZ)
2) 에러를 모델에게 “구조화”해서 돌려주기
툴이 실패했을 때 단순 문자열 에러만 주면 모델이 제대로 복구하지 못합니다.
{
"error_type": "VALIDATION_ERROR",
"tool": "create_ticket",
"field": "severity",
"message": "severity must be one of P0,P1,P2,P3",
"retryable": true
}
이렇게 주면 모델이 재시도 시 무엇을 고쳐야 하는지 명확해집니다.
3) “재시도는 1회만” 같은 정책을 시스템적으로 강제
모델에게 “재시도하지 마”라고 말하는 것보다, 런타임에서 강제하는 편이 실패율을 더 잘 낮춥니다.
- 동일 에러 타입 재시도는 최대 1회
retryable=false면 즉시 중단
관측 가능성: 툴콜 실패는 로그 설계가 절반이다
툴콜 실패를 줄이려면 원인을 빠르게 분류해야 합니다.
최소 로깅 권장 필드:
trace_id,conversation_id,step_idtool_name,tool_args_hashtool_latency_ms,tool_statusvalidation_error_fieldsmodel_name,prompt_version
특히 DB나 외부 시스템 병목으로 툴이 느려져 타임아웃이 나면, 애초에 “모델 문제”가 아닙니다. 병목 추적은 아래처럼 별도 도구가 필요합니다.
체크리스트: 선택과 적용을 빠르게
ReAct를 선택할 때
- 문제 정의가 불명확하고 탐색이 필요하다
- 툴 결과를 보고 다음 행동을 바꿔야 한다
- 대신 아래를 반드시 적용한다
- 스텝 상한
- 반복 호출 감지
- 관찰 요약 고정
Plan-and-Execute를 선택할 때
- 워크플로가 고정되어 있고 필수 입력이 명확하다
- 비용/지연 상한이 필요하다
- 대신 아래를 반드시 적용한다
- 계획 스키마 엄격화
- 계획 검증 단계
- 템플릿 치환 기반 실행기
가장 추천하는 하이브리드
- 상위는 PnE
- 불확실 구간만 제한된 ReAct
마무리
툴콜 실패를 줄이는 핵심은 “더 똑똑한 모델”이 아니라, 실패가 나기 쉬운 지점을 패턴 수준에서 분리하고, 각 패턴에 맞는 런타임 가드레일을 강제하는 것입니다.
- ReAct는 유연하지만 호출 수가 늘어 실패가 누적되므로, 루프 제어와 상태 요약이 필수
- PnE는 안정적이지만 계획 환각이 치명적이므로, 계획 검증과 실행기 단순화가 필수
- 실무에서는 PnE 뼈대 + ReAct 탐색의 하이브리드가 실패율/비용/지연의 균형이 가장 좋았습니다.
다음 단계로는, 현재 시스템의 툴콜 실패 로그를 스키마/선택/루프/타임아웃으로 태깅해 분류한 뒤, 가장 많은 비중의 실패 유형부터 위 가드레일을 적용하는 순서로 개선하는 것을 권장합니다.