Published on

Chain-of-Thought 막히면? Self-Ask+ReAct 디버깅

Authors

서로 다른 LLM 앱을 운영하다 보면, 같은 모델이라도 어떤 요청은 술술 풀리는데 어떤 요청은 갑자기 사고 흐름이 끊기거나(막힘), 같은 말만 반복하거나(루프), 근거 없이 결론을 내리거나(환각) 합니다. 흔히 Chain-of-Thought(CoT)로 “생각을 길게” 시키면 해결될 거라 기대하지만, 디버깅 관점에서는 CoT가 오히려 문제를 숨기기도 합니다. 이유는 간단합니다. 긴 추론이 한 덩어리로 뭉쳐 있으면, 어디에서 가정이 틀렸는지, 어떤 정보가 부족한지, 어떤 도구 호출이 필요했는지 관측하기가 어렵습니다.

이 글에서는 CoT가 막힐 때 Self-Ask(스스로 질문을 쪼개기)와 ReAct(Reason + Act: 추론과 행동을 번갈아 수행)를 조합해, 문제를 단계적으로 드러내고 고치는 디버깅 방법을 정리합니다. 특히 “모델이 생각은 하는데 답이 안 나오는 상태”를 관측 가능하게 만드는 설계에 초점을 둡니다.

관련해서 RAG 환경에서 반복/환각이 발생할 때의 체크리스트도 함께 보면 좋습니다. 문제 증상이 비슷하게 나타나는 경우가 많습니다.

CoT가 “막히는” 대표 증상 5가지

운영에서 자주 보는 막힘 유형을 먼저 분류해두면, 디버깅 루프가 빨라집니다.

  1. 정보 부족형: 답을 내리려면 외부 정보가 필요한데, 모델이 질문을 재정의하지 못하고 계속 내부 추론만 함
  2. 계획 부재형: 목표는 이해했지만, 하위 과업 분해가 안 되어 첫 단추를 못 끼움
  3. 제약 위반형: 포맷, 정책, 도메인 제약이 강한데 이를 놓치고 엉뚱한 방향으로 추론
  4. 도구 미사용형: 검색/DB/코드 실행이 필요한데도 도구를 호출하지 않음(혹은 호출 조건이 프롬프트에 없음)
  5. 루프형: 같은 근거를 반복하며 결론을 못 내림(대개 불확실성을 처리하는 규칙이 없음)

여기서 중요한 포인트는, “막힘”은 모델 능력 부족만이 아니라 오케스트레이션 설계 부재로도 쉽게 발생한다는 점입니다.

Self-Ask: 막힘을 “질문 그래프”로 바꾸기

Self-Ask는 큰 문제를 작은 질문들로 쪼개고, 각 질문에 대해 필요한 정보와 답변을 순차적으로 채워가는 방식입니다. CoT가 한 번에 길게 달리는 것과 달리, Self-Ask는 중간 산출물이 생기기 때문에 디버깅이 쉬워집니다.

Self-Ask 디버깅에서 관측해야 할 것

  • 하위 질문이 제대로 생성되는가(너무 크거나, 너무 많거나, 목표와 무관한가)
  • 각 질문이 “추론으로 풀 문제”인지 “조회가 필요한 문제”인지 분류되는가
  • 질문 간 의존관계가 명확한가(선행 질문이 없으면 후속 질문이 답이 안 나오는 구조인지)

프롬프트 패턴 예시

아래 예시는 모델이 답을 바로 쓰지 못하도록 하고, 먼저 하위 질문 리스트를 내도록 강제합니다. MDX 환경에서 부등호가 노출되면 빌드 에러가 날 수 있으니, 코드 블록으로만 제공합니다.

System:
너는 문제를 디버깅하는 엔지니어다. 최종 답을 쓰기 전에 반드시 하위 질문을 만들고,
각 질문이 (추론)인지 (조회)인지 태깅하라.

User:
"Chain-of-Thought가 막히는" 현상을 Self-Ask로 디버깅하는 절차를 알려줘.

Assistant:
1) 하위 질문 생성:
- Q1. CoT가 막힌다는 증상을 어떻게 분류할 수 있나? (추론)
- Q2. 정보 부족형일 때 어떤 신호가 나타나나? (추론)
- Q3. 어떤 경우에 외부 도구 호출이 필요하나? (추론)
- Q4. 도구 호출을 어떻게 설계하면 재현 가능한가? (추론)
...
2) 각 질문에 답:
...
3) 최종 요약:
...

이렇게 하면 “모델이 왜 막혔는지”가 최소한 질문 단위로 분해되어 보이기 시작합니다.

Self-Ask가 실패하는 흔한 이유

  • 하위 질문이 “조사 가능한 질문”이 아니라 “감상/추상”으로 흐름
  • 질문 수가 과도하게 늘어나 토큰 예산을 잠식
  • 질문 간 의존성이 꼬여서, 앞 질문이 틀리면 뒤가 연쇄적으로 무너짐

이때는 다음 섹션의 ReAct가 유효합니다. Self-Ask가 “질문을 잘 쪼개는 능력”이라면, ReAct는 “필요할 때 행동(도구 호출)을 강제하는 능력”입니다.

ReAct: 추론이 막힐 때 “행동”으로 탈출시키기

ReAct는 모델이 생각만 하다가 막히는 것을 방지하기 위해, **추론(Reason)**과 **행동(Act)**을 번갈아 수행하게 합니다. 여기서 행동은 보통 다음 중 하나입니다.

  • 검색 API 호출
  • 사내 문서/위키/티켓 조회
  • DB 질의
  • 코드 실행(샌드박스)
  • 계산기/파서/검증기 호출

핵심은 “모델이 확신이 없을 때는 행동으로 증거를 수집하라”는 규칙을 시스템적으로 넣는 것입니다.

ReAct의 디버깅 포인트 3가지

  1. 행동 트리거가 명확한가: 언제 도구를 호출해야 하는지 규칙이 없으면 모델은 계속 말로 버팁니다.
  2. 도구 결과를 검증하는가: 도구 출력이 틀릴 수 있습니다. 검증 단계를 넣지 않으면 환각이 도구 결과처럼 포장됩니다.
  3. 행동 비용(토큰/지연)을 관리하는가: 무한 검색 루프를 막는 상한이 필요합니다.

간단한 ReAct 오케스트레이션 예시(의사 코드)

아래는 LangChain 같은 프레임워크가 없어도 구현 가능한 최소 형태입니다.

from dataclasses import dataclass
from typing import Literal, Optional

ActionType = Literal["search", "lookup", "compute", "finish"]

@dataclass
class Step:
    thought: str
    action: ActionType
    action_input: Optional[str] = None

MAX_STEPS = 6

def llm_plan(prompt: str) -> Step:
    """LLM이 다음 스텝을 JSON으로 내도록 강제한다고 가정"""
    raise NotImplementedError

def tool_search(q: str) -> str:
    return f"search_result_for: {q}"

def run_agent(user_input: str) -> str:
    context = ""
    for i in range(MAX_STEPS):
        step = llm_plan(
            prompt=(
                "너는 ReAct 에이전트다. 다음 중 하나를 선택하라: search, lookup, compute, finish. "
                "확신이 없으면 search 또는 lookup을 선택하라. "
                "반드시 JSON으로만 답하라.\n"
                f"user_input: {user_input}\n"
                f"context: {context}\n"
            )
        )

        if step.action == "search":
            obs = tool_search(step.action_input or "")
            context += f"\n[observation] {obs}"
        elif step.action == "finish":
            return step.thought
        else:
            context += "\n[observation] not_implemented"

    return "중간 단계에서 종료: step limit"

이 구조의 장점은, CoT가 막히는 순간에도 “다음 행동이 무엇이었는지”가 로그로 남아 재현 가능한 디버깅이 가능해진다는 점입니다.

Self-Ask + ReAct: 함께 쓸 때의 설계 패턴

두 기법을 섞으면 다음과 같은 파이프라인이 됩니다.

  1. Self-Ask로 하위 질문 생성
  2. 각 질문을 처리할 때 ReAct로 “추론 vs 도구 호출”을 결정
  3. 질문별 결과를 합쳐 최종 답 생성

추천 상태 머신

  • 상태 DECOMPOSE: 하위 질문 생성
  • 상태 SOLVE_Qi: i번째 질문 해결
  • 상태 NEED_EVIDENCE: 도구 호출
  • 상태 VERIFY: 근거 검증 및 상충 해결
  • 상태 SYNTHESIZE: 최종 답 합성

이렇게 상태를 나누면, 장애가 났을 때 “어느 상태에서 실패했는지”가 곧 원인 분석의 출발점이 됩니다. 분산 시스템에서 재시도/중복 처리를 상태 머신으로 잡는 것과 결이 비슷합니다.

실전 디버깅 체크리스트

운영에서 CoT 막힘을 만나면, 아래 순서로 점검하면 빠르게 원인을 좁힐 수 있습니다.

1) 입력을 “정답 가능 형태”로 만들었나

  • 질문에 목표/제약/출력 포맷이 명확한가
  • 도메인 컨텍스트가 필요한데 누락되지 않았나
  • 금지 조건(예: 외부 접근 불가, 내부 정책)이 있는데 모델이 이를 모르는가

Self-Ask는 여기서 특히 효과적입니다. 하위 질문이 제대로 나오지 않으면, 대개 입력 자체가 불완전합니다.

2) 도구 호출 트리거가 프롬프트에 명시됐나

  • “모르면 검색” 같은 규칙이 시스템 메시지에 있는가
  • 도구 목록과 파라미터 형식이 명확한가
  • 도구 호출 실패 시 대체 경로가 있는가

ReAct는 트리거가 없으면 작동하지 않습니다. 모델 입장에서는 “도구를 써도 된다”는 확신이 없으면 말로 해결하려고 합니다.

3) 루프 방지 장치가 있나

  • 최대 스텝 수 제한
  • 동일 쿼리 반복 검색 금지(최근 N개 쿼리 중복이면 중단)
  • 불확실성 처리 규칙(예: 가정 목록을 나열하고 사용자에게 확인 질문)

4) 근거 검증 단계가 있나

  • 도구 결과를 그대로 믿지 말고, 상충되는 결과가 나오면 재질의 또는 출처 비교
  • RAG라면 인용 스니펫과 답변이 실제로 정합한지 검사

이 부분은 RAG 디버깅에서 특히 중요합니다. 반복/환각은 “근거 검증 부재”로 강화됩니다.

5) 로그를 “질문 단위”로 남기고 있나

CoT 전체를 저장하는 것보다, 다음을 구조화해서 저장하는 편이 훨씬 유용합니다.

  • 하위 질문 리스트
  • 질문별 선택된 액션(추론/검색/조회)
  • 도구 입력과 출력
  • 최종 합성 시 사용한 근거 목록

자주 쓰는 프롬프트 템플릿 2종

템플릿 A: Self-Ask 우선(기획/설계형 문제)

System:
너는 시니어 엔지니어다. 답변 전에 반드시 문제를 하위 질문으로 분해하고,
각 질문의 해결 방식(추론/조회/계산)을 명시한다.

User:
{문제}

Assistant:
- 하위 질문:
  1. ... (추론)
  2. ... (조회)
- 해결:
  - Q1 답: ...
  - Q2 답: ...
- 최종 답:
...

템플릿 B: ReAct 우선(근거 수집이 필요한 문제)

System:
너는 ReAct 에이전트다.
불확실하면 반드시 도구를 호출해 근거를 확보한다.
가능한 행동: search(query), lookup(key), compute(expr), finish(answer)
반드시 다음 형식으로만 출력:
{"action": "...", "action_input": "..."}

User:
{문제}

이 템플릿은 도구 호출을 강제하기 때문에, “말만 그럴듯하게 하는” 막힘을 빠르게 줄여줍니다.

운영 관점: 막힘은 대개 “리소스/제약” 문제로도 나타난다

LLM 디버깅을 하다 보면, 모델 추론 문제가 아니라 시스템 제약 때문에 막히는 경우도 많습니다.

  • 컨텍스트 윈도우가 부족해 핵심 근거가 잘려 나감
  • 타임아웃 때문에 도구 호출이 중간에 끊김
  • 캐시/세션 꼬임으로 이전 대화가 섞여 들어감

이런 문제는 전형적인 웹/백엔드 장애처럼 접근해야 합니다. 예를 들어 타임아웃/게이트웨이 이슈는 LLM이 아니라 인프라 병목일 수 있습니다.

정리: CoT가 막히면 “더 길게 생각”이 아니라 “더 잘 쪼개고, 더 자주 행동”

  • CoT는 결과만 보면 그럴듯하지만, 막힘의 원인을 숨기기 쉽습니다.
  • Self-Ask는 문제를 하위 질문으로 쪼개 어디에서 정보가 부족한지를 드러냅니다.
  • ReAct는 불확실할 때 도구 호출을 강제해 증거 기반으로 탈출하게 만듭니다.
  • 두 방법을 합치면, 질문 그래프와 행동 로그가 남아 재현 가능한 디버깅 루프를 만들 수 있습니다.

다음에 CoT가 막히는 케이스를 만나면, “모델에게 더 길게 생각하라” 대신, Self-Ask로 하위 질문을 만들고 ReAct로 근거를 수집하게 설계해보세요. 막힘이 문제의 본질이라기보다, 관측과 제어가 부족한 오케스트레이션 문제였다는 걸 훨씬 자주 발견하게 됩니다.