- Published on
CoT 차단해도 성능 올리는 ReAct·Self-Consistency
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론부터 결론까지 한 줄로 요약하면, CoT를 보여주지 말라는 요구와 더 잘 추론하라는 요구는 동시에 만족시킬 수 있습니다. 핵심은 모델의 중간 추론을 외부로 노출하지 않는 형태로 강제하고, 결과는 검증 가능한 산출물(행동 로그, 도구 호출 결과, 최종 답)로만 남기는 것입니다.
이 글에서는 CoT(Chain-of-Thought) 노출을 막으면서도 성능을 올리는 대표적인 두 축인 ReAct(Reason + Act), Self-Consistency(다중 샘플 투표) 를 실전 관점에서 다룹니다. 특히 “왜 좋아지는지”보다 “어떻게 설계하면 실패하지 않는지”에 집중합니다.
- ReAct: 추론을 도구 호출과 관찰로 쪼개서 환각을 줄이고, 정답 근거를
관찰(Observation)로 대체 - Self-Consistency: 한 번의 그럴듯한 답 대신 여러 번의 답을 뽑아
집계(aggregation)로 안정화 - CoT 차단: 중간 추론은
스크래치패드로 내부 처리하고, 응답에는 노출하지 않게 설계
관련해서 스크래치패드로 추론을 강제하되 CoT 누출을 막는 프롬프트 패턴은 아래 글도 함께 보면 좋습니다.
왜 CoT를 막아야 하는가: 보안·정책·제품 품질
CoT를 그대로 노출하면 다음 문제가 자주 발생합니다.
- 정책/규정 리스크: 내부 지침, 민감한 추론 힌트, 필터링 우회 시도 등이 그대로 노출될 수 있습니다.
- 프롬프트 인젝션 표면 확대: 사용자가 “추론 과정을 보여줘”라고 요구하면 모델이 시스템 지시를 우회해 내부 정보를 내보내는 경로가 됩니다.
- 제품 품질 저하: 긴 CoT는 사용자가 원하는 답(결론)을 묻어버리고, 때로는 틀린 중간 추론이 신뢰를 깎습니다.
그렇다고 “생각하지 말고 답만 해”로 가면 성능이 떨어지는 경우가 많습니다. 해결책은 간단합니다.
- 모델은 생각하게 하되
- 우리는 생각을 받지 않는다
이를 구현하는 대표적인 방법이 스크래치패드와 ReAct, 그리고 Self-Consistency입니다.
ReAct: 추론을 행동으로 바꿔 CoT 의존도를 낮춘다
ReAct는 모델이 Reason(내부 계획)과 Act(도구 호출)를 번갈아 수행하면서 문제를 해결하는 패턴입니다. 여기서 중요한 포인트는 “Reason을 사용자에게 보여주지 않아도 된다”는 점입니다.
- 사용자에게는
최종 답과근거가 되는 관찰 결과만 제공 - 내부적으로는 모델이 도구를 호출하며 단계적으로 불확실성을 줄임
ReAct가 특히 잘 먹히는 문제 유형
- 최신 정보/사실 확인: 검색, DB, 사내 위키
- 계산/정합성: 계산기, 코드 실행기
- 구조화 출력: 스키마 검증, JSON 생성
- 멀티홉 질문: 여러 문서에서 근거를 모아야 하는 경우
실전 설계 포인트: “도구 호출 로그”가 곧 근거다
CoT를 공개하지 않더라도, ReAct는 Observation을 남깁니다. 이 관찰은 다음을 만족해야 합니다.
- 재현 가능: 같은 쿼리면 같은 결과가 나와야 함(가능한 범위에서)
- 검증 가능: 사람이 읽었을 때 근거로 쓸 수 있어야 함
- 최소 노출: 내부 프롬프트나 정책 문구가 섞이지 않게 해야 함
아래는 OpenAI 호환 API를 상정한 간단한 ReAct 루프 예시입니다. 핵심은 reasoning을 출력하지 않고, 도구 호출과 관찰만으로 진행한 뒤 최종 답만 사용자에게 주는 형태입니다.
import json
from typing import Any, Dict, List
# 예시 도구: 사내 위키 검색(더미)
def wiki_search(query: str) -> str:
# 실제로는 벡터DB/검색엔진/사내 API 호출
corpus = {
"react": "ReAct는 Reasoning과 Acting을 결합한 프롬프트/에이전트 패턴이다.",
"self-consistency": "Self-Consistency는 여러 샘플을 생성해 다수결로 답을 선택하는 기법이다.",
}
return corpus.get(query.lower(), "검색 결과 없음")
TOOLS = {
"wiki_search": wiki_search
}
# LLM 호출은 환경에 맞게 교체
def llm(messages: List[Dict[str, str]]) -> Dict[str, Any]:
"""반드시 JSON만 반환하도록 강제했다고 가정.
반환 예: {"action": "wiki_search", "action_input": "ReAct"} 또는 {"final": "..."}
"""
raise NotImplementedError
SYSTEM = (
"너는 도구를 사용해 정확한 답을 만드는 어시스턴트다. "
"응답은 반드시 JSON 객체 하나로만 출력한다. "
"도구가 필요하면 action/action_input을 출력하고, "
"최종 답일 때만 final을 출력한다. "
"중간 추론은 절대 출력하지 않는다."
)
def react_answer(user_question: str, max_steps: int = 5) -> str:
messages = [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user_question},
]
for _ in range(max_steps):
out = llm(messages)
if "final" in out:
return out["final"]
action = out.get("action")
action_input = out.get("action_input", "")
if action not in TOOLS:
return "도구 호출이 잘못되어 답변을 생성할 수 없습니다."
observation = TOOLS[action](action_input)
# 관찰 결과만 대화에 추가 (CoT는 추가하지 않음)
messages.append({"role": "assistant", "content": json.dumps(out, ensure_ascii=False)})
messages.append({"role": "tool", "content": observation})
return "제한된 단계 내에서 답을 확정하지 못했습니다."
위 구조의 장점은 다음과 같습니다.
- 모델이 내부적으로는 계획을 세우더라도 외부로는
JSON 액션만 나가므로 CoT 누출 위험이 낮음 - 결과의 근거가
tool observation에 남음 - 실패 시에도 어디서 막혔는지(도구 호출/관찰 부족)가 운영 로그로 추적 가능
ReAct의 흔한 실패 모드와 대응
도구 남용(무한 검색, 과도한 호출)
max_steps제한- 동일 쿼리 반복 탐지(최근 N개 action_input 해시)
- 도구 비용 기반 예산(
tool_budget) 도입
관찰을 무시하고 답을 지어냄
- 최종 답 생성 전에 “관찰 인용 필수” 규칙
- 스키마로
citations같은 필드 강제
도구 출력에 프롬프트 인젝션 포함
- 도구 출력은
데이터로만 취급하도록 시스템 지시 - HTML/마크다운/스크립트 제거 등 정제
- 도구 출력은
Self-Consistency: CoT 없이도 정답률을 올리는 가장 단순한 레버
Self-Consistency는 간단히 말해 “한 번 찍지 말고 여러 번 뽑아 다수결로 고르자”입니다. CoT를 공개하지 않아도 됩니다. 오히려 CoT를 숨기는 상황에서 더 유용합니다.
- 단일 샘플: 우연히 잘못된 경로로 가면 그대로 실패
- 다중 샘플 + 집계: 오류 경로가 분산되고, 정답이 반복적으로 등장할 확률이 커짐
언제 효과가 큰가
- 수학/논리/코딩처럼 경로가 여러 개인 문제
- “그럴듯한 오답”이 자주 나오는 문제
- 출력이 정규화 가능한 문제(정답 문자열, 선택지, 숫자 등)
반대로, 창작형/요약형처럼 정답이 단일하지 않은 문제는 집계 기준을 잘 설계해야 합니다.
실전 구현: 정규화 + 투표 + 타이브레이커
아래 예시는 n번 생성한 뒤 정규화한 답으로 투표합니다. 핵심은 “집계 전에 답을 정규화”하는 것입니다.
import re
from collections import Counter
from typing import List
def normalize_answer(text: str) -> str:
# 예: 숫자만 뽑기, 공백 정리, 대소문자 통일 등 문제에 맞게 조정
t = text.strip().lower()
t = re.sub(r"\s+", " ", t)
return t
def generate_once(prompt: str, temperature: float) -> str:
# 실제 LLM 호출로 교체
raise NotImplementedError
def self_consistency(prompt: str, n: int = 7, temperature: float = 0.8) -> str:
samples: List[str] = [generate_once(prompt, temperature) for _ in range(n)]
norm = [normalize_answer(s) for s in samples]
counts = Counter(norm)
best, best_count = counts.most_common(1)[0]
# 타이브레이커: 원문 중 가장 짧거나(간결성), 특정 규칙을 만족하는 것 선택
candidates = [samples[i] for i, x in enumerate(norm) if x == best]
candidates.sort(key=len)
return candidates[0]
운영에서 더 좋은 집계는 다음과 같습니다.
- 검증기 기반 rerank: 규칙/테스트/스키마 검증을 통과한 답만 후보로
- LLM-as-judge: 별도 모델로 채점(단, 비용과 편향 관리 필요)
- 근거 일치성: ReAct 관찰과 모순 없는 답에 가중치
ReAct + Self-Consistency 조합: “도구 기반 다중 샘플”이 강력한 이유
둘을 합치면 효과가 커집니다.
- 각 샘플이 ReAct로 도구를 호출해 근거를 확보
- 여러 샘플의 최종 답을 투표
- 필요하면 관찰까지 포함해 “근거 일치성”으로 집계
특히 검색/문서 QA에서 자주 발생하는 한 번의 검색 실패를 다중 샘플이 상쇄합니다.
조합 패턴 예시
- 질문을 넣고 ReAct 에이전트를
n번 실행 - 각 실행의 최종 답과 인용(관찰)을 수집
- 답을 정규화해 투표
- 최종 답에는 인용만 노출하고, 내부 추론은 저장하지 않음
이때 비용이 급증할 수 있으므로, 레이트리밋과 백오프는 필수 운영 요소입니다. 대량 호출에서 429가 터지는 경우가 많으니 아래 글의 패턴을 그대로 적용할 수 있습니다.
CoT를 막는 실전 프롬프트/출력 설계: “보여줄 것만 스키마로 고정”
CoT 차단은 단순히 “추론을 말하지 마”로 끝나지 않습니다. 출력 채널과 스키마를 설계해야 합니다.
권장 원칙
- 모델 출력은
final과evidence같은 필드만 허용 - 도구 호출은 별도 채널(또는 함수 호출)로 분리
- 로그에는 필요한 최소 정보만 저장(개인정보/민감정보 마스킹)
예를 들어 최종 출력 스키마를 아래처럼 제한할 수 있습니다.
{
"answer": "...",
"evidence": [
{"source": "wiki_search", "quote": "..."}
],
"confidence": "low|medium|high"
}
중요한 점은 evidence가 곧 사용자 신뢰를 만드는 장치라는 것입니다. CoT를 숨겨도 사용자는 “왜 이 결론이 나왔는지”를 알고 싶어합니다. 그때 CoT 대신 관찰/인용을 보여주면 됩니다.
운영 관점 체크리스트: 성능↑와 리스크↓를 동시에
1) 비용과 지연시간
- ReAct는 도구 호출로 지연이 늘고, Self-Consistency는 샘플 수만큼 비용이 늘어납니다.
- 실무에서는 보통
2단계 전략이 좋습니다.- 1차: 단일 샘플로 빠르게 시도
- 2차: 실패 신호가 있을 때만 ReAct 또는 Self-Consistency 승격
실패 신호 예시
confidence=low- 스키마 검증 실패
- 근거 부족(인용 0개)
- 금지어/정책 위반 탐지
2) 품질 측정
- 정답률만 보지 말고
근거 일치율,환각률,재시도율을 함께 봐야 합니다. - ReAct는 관찰이 남으므로 “관찰에 없는 사실을 말했는가”를 자동 평가할 수 있습니다.
3) 보안
- 도구 출력에 포함된 민감정보는 마스킹
- 프롬프트 인젝션 방어: 도구 출력은 지시가 아니라 데이터로 취급
- 내부 추론 저장 금지 또는 최소화
결론: CoT를 숨기면서도 더 똑똑하게 만드는 공식
- ReAct는
CoT를 근거로 쓰는 방식에서관찰 가능한 도구 결과를 근거로 쓰는 방식으로 전환합니다. - Self-Consistency는
한 번의 운을통계적 안정성으로 바꿉니다. - 둘을 결합하면, CoT를 사용자에게 공개하지 않고도 정확도와 일관성을 동시에 올릴 수 있습니다.
실무 적용 순서는 보통 다음이 가장 무난합니다.
- 출력 스키마로 CoT 노출을 구조적으로 차단
- 단일 샘플에 ReAct를 붙여 환각을 줄임
- 어려운 케이스에만 Self-Consistency를 조건부로 적용
- 429/백오프/배치로 운영 안정화
다음 단계로는, “스크래치패드로 추론을 강제하되 외부로는 절대 누출하지 않는” 패턴을 더 정교하게 다듬는 것이 좋습니다.