Published on

CoT 없이도 추론 강화 - ReAct·ToT 프롬프트 실전

Authors

서로 다른 팀이 같은 LLM을 붙였는데도 결과 품질이 갈리는 경우가 많습니다. 이유는 대개 모델 자체보다 추론을 끌어내는 인터페이스에 있습니다. 하지만 최근에는 보안·컴플라이언스·제품 정책 때문에 Chain-of-Thought(CoT) 같은 상세 추론을 사용자에게 그대로 노출하지 않거나, 아예 모델이 길게 생각하도록 강제하지 않는 방향이 선호되기도 합니다.

이 글은 CoT를 노출하지 않으면서도 결과의 일관성과 정확도를 높이는 대표 패턴인 ReAct와 **Tree-of-Thought(ToT)**를 실전 관점에서 다룹니다. 핵심은 “생각을 보여 달라”가 아니라, 행동 가능한 중간 산출물평가기준을 설계해 모델이 스스로 탐색하도록 만드는 것입니다.

또한 운영 환경에서는 레이트리밋, 지연, 비용이 함께 움직입니다. 프롬프트 전략을 강화하면 호출 수가 늘어날 수 있으니, 트래픽 상황에서는 백오프·배치 전략도 같이 봐야 합니다. 관련해서는 LangChain OpenAI 429 폭주 대응 - 레이트리밋·백오프·배치 글도 함께 참고하면 좋습니다.

CoT 없이도 “추론”을 강화할 수 있는 이유

CoT는 모델이 문제를 풀기 위해 내부적으로 거치는 중간 단계를 텍스트로 풀어내는 방식입니다. 다만 실서비스에서는 다음 이유로 CoT를 그대로 노출하기 어렵습니다.

  • 정책/보안: 내부 규칙, 민감정보가 중간 추론에 섞일 수 있음
  • 환각의 설득력: 그럴듯한 “사고 과정”이 오히려 잘못된 결론을 강화
  • 길이/비용: 토큰이 늘어나고 지연이 증가

그렇다고 추론 자체를 포기할 필요는 없습니다. 핵심은 추론을 텍스트로 길게 쓰게 하는 것이 아니라,

  • 문제를 분해하고
  • 필요한 정보를 조회하고
  • 여러 후보를 탐색하고
  • 기준에 따라 평가한 뒤
  • 최종 답을 검증

하게 만드는 것입니다. ReAct와 ToT는 이 과정을 프롬프트 구조로 강제합니다.

ReAct: Observation-Action 루프로 정확도 올리기

ReAct는 이름 그대로 Reasoning + Acting입니다. 다만 여기서 “Reasoning”을 사용자에게 노출할 필요는 없습니다. 실전에서는 보통 다음 두 가지를 분리합니다.

  • 모델이 수행할 행동(Action): 검색, DB 조회, 계산, 도구 호출
  • 사용자에게 보여줄 결과(Final): 근거 링크, 요약, 결론

ReAct의 기본 골격

ReAct는 대체로 다음 루프를 반복합니다.

  • Observation: 도구 결과, 문서 발췌, 에러 메시지
  • Action: 다음에 호출할 도구와 입력

중요한 점은 관찰은 외부에서 주어지는 사실이어야 한다는 것입니다. 모델이 만들어낸 “관찰”은 환각이 됩니다.

실전 프롬프트 템플릿

아래 템플릿은 CoT를 노출하지 않으면서도, 모델이 도구를 단계적으로 쓰게 만드는 형태입니다. 중간 생각은 private로 두고, 출력은 구조화된 JSON만 내보내게 할 수 있습니다.

역할: 당신은 제품 지원 엔지니어다.
목표: 사용자의 질문에 대해, 필요한 경우에만 도구를 사용해 사실을 확인하고 답한다.

규칙:
- 내부 추론은 출력하지 않는다.
- 도구 호출이 필요하면 다음 형식으로만 출력한다.
  {"tool":"TOOL_NAME","input":{...}}
- 최종 답변은 다음 형식으로만 출력한다.
  {"answer":"...","sources":[...],"confidence":"low|medium|high"}

사용 가능한 도구:
- search_docs(query)
- get_ticket(id)
- run_calc(expression)

질문: {user_question}

Node.js로 ReAct 루프 구현하기

아래는 “모델이 도구 호출 JSON을 내면 실행하고, 관찰을 다시 모델에 넣는” 최소 루프입니다. 포인트는 도구 결과를 그대로 Observation으로 제공하는 것입니다.

import OpenAI from "openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function callModel(messages) {
  const res = await client.chat.completions.create({
    model: "gpt-4.1-mini",
    messages,
    temperature: 0.2,
  });
  return res.choices[0].message.content;
}

async function searchDocs(query) {
  // 실제로는 사내 검색, 벡터DB, 위키 등을 연결
  return {
    hits: [
      { title: "Auth Error Catalog", snippet: "invalid_grant occurs when ..." },
    ],
  };
}

function tryParseJson(text) {
  try { return JSON.parse(text); } catch { return null; }
}

export async function reactAnswer(userQuestion) {
  const system = {
    role: "system",
    content: "내부 추론은 출력하지 말고, 도구 호출 또는 최종 JSON만 출력하라.",
  };

  let messages = [
    system,
    { role: "user", content: `질문: ${userQuestion}` },
  ];

  for (let step = 0; step < 6; step++) {
    const out = await callModel(messages);
    const json = tryParseJson(out);

    if (!json) {
      // 모델이 규칙을 어긴 경우를 대비
      messages.push({ role: "assistant", content: out });
      messages.push({ role: "user", content: "JSON 형식만 출력하라." });
      continue;
    }

    if (json.tool) {
      let obs;
      if (json.tool === "search_docs") {
        obs = await searchDocs(json.input.query);
      } else {
        obs = { error: "unknown_tool" };
      }

      messages.push({ role: "assistant", content: out });
      messages.push({
        role: "user",
        content: `Observation: ${JSON.stringify(obs)}`,
      });
      continue;
    }

    // 최종 답
    return json;
  }

  return {
    answer: "추가 확인이 필요합니다. 질문을 더 구체화해 주세요.",
    sources: [],
    confidence: "low",
  };
}

ReAct가 특히 강한 문제

  • “확인해야 답할 수 있는” 운영 이슈: 로그/문서/티켓 조회
  • 계산/변환이 섞인 업무: 요금 계산, 단위 변환
  • 근거가 필요한 답변: 출처 링크 제공

예를 들어 OAuth 오류 같은 케이스는 문서/로그 확인이 필수입니다. 이런 유형의 트러블슈팅 글로는 OAuth PKCE invalid_grant 실패 8가지 원인 같은 구조가 ReAct와 잘 맞습니다. “원인 후보를 나열하고, 관찰로 분기해 좁혀가는 방식”이기 때문입니다.

ReAct에서 자주 터지는 함정

  • 도구 남발: 모델이 확신이 없을수록 검색을 반복
    • 해결: max_steps, “도구는 최대 2회” 같은 하드 리밋
  • 관찰 오염: 모델이 관찰을 재해석해 사실처럼 말함
    • 해결: Observation은 원문 그대로, 출력은 sources로 묶기
  • JSON 깨짐: 운영에서 가장 흔한 실패
    • 해결: 재시도 프롬프트, 스키마 검증, 함수 호출 기능 사용

ToT: 후보 해를 “탐색”하고 “평가”하는 프롬프트

ToT는 한 번에 답을 내기보다, 여러 후보를 만들어 탐색한 뒤 평가 기준으로 가지치기하는 방식입니다.

CoT와의 차이는 “긴 생각을 쓰게 한다”가 아니라,

  • 후보 생성 단계
  • 평가 단계
  • 선택 단계

명시적으로 분리한다는 점입니다. 이때도 내부 추론을 노출할 필요는 없습니다. 후보는 짧은 요약 형태로, 평가는 체크리스트 점수로 만들면 됩니다.

ToT가 유리한 문제

  • 설계/아키텍처: 선택지 비교가 핵심인 문제
  • 모호한 요구사항: 여러 해법을 세워보고 트레이드오프를 비교
  • 최적화/튜닝: 여러 가설을 실험 순서로 정렬

예를 들어 성능 튜닝 글을 쓸 때도 “가능한 원인 후보를 세우고, 계측으로 좁혀가는” 구조가 강력합니다. DB에서 인덱스 미사용을 찾는 과정은 ToT의 전형적인 탐색 문제이고, MySQL EXPLAIN ANALYZE로 인덱스 미사용 잡기 같은 접근과 궁합이 좋습니다.

ToT 프롬프트 템플릿

아래는 후보를 3개 만들고, 평가표로 점수화해 1개를 선택하는 형태입니다.

당신은 시니어 엔지니어다. 내부 추론은 출력하지 않는다.

문제: {problem}
제약: {constraints}

1) 후보를 3개 제시하라. 각 후보는 2~3문장으로 요약하라.
2) 아래 기준으로 후보별 점수를 1~5로 매기고, 한 줄 근거만 쓰라.
   - 구현 난이도
   - 운영 리스크
   - 비용
   - 확장성
3) 총점이 가장 높은 후보 1개를 선택하고, 실행 계획을 5단계로 제시하라.

출력은 반드시 JSON:
{
  "candidates":[...],
  "scores":[...],
  "choice":{...}
}

Python으로 ToT 스타일 탐색 구현

ToT를 “프롬프트만”으로 끝내면 모델이 마음대로 가지치기합니다. 운영에서는 탐색을 애플리케이션에서 통제하는 편이 안정적입니다.

아래 예시는 후보를 여러 번 샘플링하고, 별도의 평가 프롬프트로 점수화한 뒤 상위 후보만 남기는 간단한 빔 서치 형태입니다.

import json
from openai import OpenAI

client = OpenAI()

def chat(model, messages, temperature=0.7):
    res = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=temperature,
    )
    return res.choices[0].message.content

def gen_candidates(problem, k=3):
    prompt = (
        "내부 추론은 출력하지 말고, 후보만 JSON으로 출력하라. "
        "형식: {\"candidates\":[{\"title\":...,\"summary\":...}]}\n"
        f"문제: {problem}\n"
        f"후보 {k}개 생성"
    )
    out = chat("gpt-4.1-mini", [{"role":"user","content":prompt}], temperature=0.9)
    return json.loads(out)["candidates"]

def score_candidate(problem, cand):
    prompt = (
        "내부 추론은 출력하지 말고 점수만 JSON으로 출력하라. "
        "형식: {\"difficulty\":1-5,\"risk\":1-5,\"cost\":1-5,\"scale\":1-5}\n"
        f"문제: {problem}\n"
        f"후보: {cand['title']} - {cand['summary']}\n"
    )
    out = chat("gpt-4.1-mini", [{"role":"user","content":prompt}], temperature=0.0)
    s = json.loads(out)
    total = s["difficulty"] + s["risk"] + s["cost"] + s["scale"]
    return total, s

def tot_select(problem, rounds=2, beam=2):
    pool = []
    for _ in range(rounds):
        pool.extend(gen_candidates(problem, k=3))

    scored = []
    for cand in pool:
        total, detail = score_candidate(problem, cand)
        scored.append((total, cand, detail))

    scored.sort(key=lambda x: x[0], reverse=True)
    return scored[:beam]

if __name__ == "__main__":
    top = tot_select("RAG 응답 지연을 50% 줄이고 싶다. 가능한 접근을 비교해줘")
    for total, cand, detail in top:
        print(total, cand["title"], detail)

이런 방식은 RAG 최적화에서도 유용합니다. 예를 들어 인덱스나 ANN 파라미터 튜닝은 선택지가 많고, 계측 기반으로 좁혀야 합니다. 관련해서는 pgvector HNSW 튜닝으로 RAG 지연 50% 줄이기 같은 글의 문제 정의를 ToT로 그대로 옮길 수 있습니다.

ReAct vs ToT: 언제 무엇을 쓰나

의사결정 기준

  • 외부 사실 확인이 핵심이면 ReAct
    • 도구 호출과 관찰의 신뢰성이 성패를 가름
  • 여러 해법의 비교/탐색이 핵심이면 ToT
    • 후보 다양성, 평가 기준의 명확성이 성패를 가름

함께 쓰는 하이브리드 패턴

현업에서는 ToT로 “계획 후보”를 만든 뒤, 각 후보를 ReAct로 검증하는 흐름이 강합니다.

  • ToT: 접근 A/B/C 생성, 평가표로 1~2개로 축소
  • ReAct: 선택된 후보에 대해 문서/로그/지표를 조회해 사실 확인

이 조합은 “그럴듯한 설계”를 “검증된 설계”로 바꿉니다.

CoT 노출 없이 품질을 올리는 프롬프트 디테일

1) 출력 스키마를 강제하라

자연어 답변은 예쁘지만 운영에서 깨집니다. 다음 중 하나를 권장합니다.

  • JSON 스키마 고정
  • 함수 호출 인터페이스 사용
  • 최소한 answer, sources, confidence 같은 필드 강제

2) 평가 기준을 텍스트가 아니라 체크리스트로

ToT에서 “왜 이게 좋은가”를 장문으로 쓰게 하면 다시 CoT처럼 길어집니다. 대신 아래처럼 짧은 근거 + 점수로 제한합니다.

  • 구현 난이도 1~5
  • 리스크 1~5
  • 비용 1~5
  • 롤백 용이성 1~5

3) 불확실성을 제품 UX로 승격

CoT를 숨기면 사용자는 모델이 확신하는지 알기 어렵습니다. 그래서 confidence를 노출하거나, 다음 질문을 유도하는 확인 질문을 템플릿으로 넣는 편이 낫습니다.

답을 내기 위해 추가로 필요한 정보 2가지를 질문하라.
단, 이미 Observation에서 확인된 내용은 다시 묻지 말라.

4) 비용과 지연을 예산으로 관리

ReAct는 도구 호출이 늘어날수록 비용이 증가합니다. 다음을 같이 설계하세요.

  • max_steps 하드 리밋
  • 도구별 쿼터: search_docs는 최대 2회
  • 캐시: 동일 질의는 TTL 캐시
  • 레이트리밋 대응: 백오프, 배치, 큐잉

레이트리밋 실전 대응은 앞서 언급한 LangChain OpenAI 429 폭주 대응 - 레이트리밋·백오프·배치 내용을 그대로 적용할 수 있습니다.

실전 적용 체크리스트

  • ReAct

    • Observation은 외부 결과만 넣었는가
    • 도구 호출 형식이 파싱 가능한가
    • 도구 호출 횟수에 상한이 있는가
    • 출처를 sources로 강제했는가
  • ToT

    • 후보 수와 다양성(temperature)을 분리했는가
    • 평가 프롬프트는 temperature를 0에 가깝게 두었는가
    • 점수 기준이 제품 목표와 연결되는가
    • 상위 후보만 남기는 가지치기가 있는가

마무리

CoT를 노출하지 않는다고 해서 추론 성능을 포기할 필요는 없습니다. ReAct는 사실 확인과 도구 사용을 통해 정답률을 끌어올리고, ToT는 후보 탐색과 평가로 설계 품질을 올립니다. 둘 다 핵심은 “길게 생각하기”가 아니라, 중간 산출물을 구조화하고, 검증 가능한 루프를 만들고, 평가 기준을 명시하는 데 있습니다.

다음 단계로는, 현재 서비스에서 가장 자주 실패하는 질문 유형을 3개로 분류한 뒤

  • A유형은 ReAct
  • B유형은 ToT
  • C유형은 하이브리드

로 라우팅해 보는 것을 권합니다. 이때는 반드시 호출 수, 지연, 정답률을 함께 계측해서 프롬프트 전략이 실제로 비용 대비 효과가 있는지 확인하세요.