- Published on
CoT 유출 없이 추론 강화 - ReAct·SC 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
LLM을 제품에 붙이다 보면 곧바로 마주치는 딜레마가 있습니다. 정답률을 올리려면 더 깊은 추론이 필요한데, 전통적인 CoT(Chain-of-Thought)를 그대로 노출하면 다음 문제가 생깁니다.
- 보안·프라이버시: 내부 규칙, 운영 데이터, 고객 식별 정보가 추론 과정에 섞여 노출될 수 있음
- 프롬프트 인젝션 표면 확대: 모델이 “생각 과정을 보여줘” 같은 공격에 취약해짐
- 정책·컴플라이언스: 특정 도메인에서는 추론 로그 자체가 규제 대상이 되기도 함
이 글은 CoT를 유출하지 않으면서도 추론 품질을 높이는 대표 패턴인 ReAct와 **Self-Consistency(SC)**를 실전 기준으로 정리합니다. 핵심은 간단합니다.
- 모델에게는 내부적으로 충분히 “생각”할 여지를 주되
- 사용자에게는 **최종 답변과 근거 요약(증거, 출처, 계산 결과)**만 제공
- 평가·로그에는 구조화된 메타데이터만 남겨 재현 가능성을 확보
아키텍처 관점에서 보면, 운영 환경에서의 장애/병목을 추적하듯이 LLM 추론도 관측 가능하게 만드는 것이 중요합니다. 예를 들어 DB 병목을 pg_stat_statements로 추적하듯, 추론도 “툴 호출 횟수, 실패율, 재시도, 합의율” 같은 지표로 관리해야 합니다. 관련해서는 PostgreSQL 쿼리 폭주? pg_stat_statements로 병목 추적처럼 관측과 진단의 관점을 LLM에도 그대로 가져오면 좋습니다.
CoT 유출이 왜 위험한가: 실제 운영에서의 리스크
CoT는 단순히 “중간 풀이”가 아닙니다. 운영 시스템에서는 다음이 섞이기 쉽습니다.
- 내부 정책 문구, 가격 규칙, 위험 점수 계산식
- 고객 데이터 일부(요약 과정에서 유출)
- 시스템 프롬프트(가드레일), 라우팅 규칙
또한 공격자는 CoT를 이용해 모델의 취약한 규칙을 역추론합니다. 예를 들어 “어떤 단어가 필터를 우회하는지”, “어떤 조건에서 툴을 호출하는지”가 드러나면 인젝션이 쉬워집니다.
따라서 목표는 다음과 같이 재정의하는 게 좋습니다.
- 모델 내부에서는 추론을 최대한 활용
- 외부 출력에서는 추론 과정을 노출하지 않고 검증 가능한 결과물만 제시
이때 ReAct와 SC는 “더 생각하게 만들기”가 아니라, 생각을 시스템적으로 구조화해 품질을 올리는 접근입니다.
패턴 1: ReAct — 추론과 행동(툴)을 교차시켜 정확도 올리기
ReAct는 Reasoning과 Acting을 번갈아 수행합니다. 핵심은 모델이 애매한 기억에 의존하지 않고 필요할 때 외부 도구로 검증하게 만드는 것입니다.
운영에서 ReAct가 특히 유효한 영역은 다음과 같습니다.
- 최신 정보 조회(검색, 사내 위키, 티켓 시스템)
- 정형 계산(세금, 환율, 비용 산정)
- 시스템 상태 진단(로그/메트릭 기반)
예를 들어 EKS에서 503이 나오는 상황을 LLM이 진단한다고 합시다. 모델이 “그럴듯한 추측”을 늘어놓는 대신, Ingress/Service/Pod 상태를 툴로 확인하며 진행해야 합니다. 이런 진단형 워크플로우는 EKS에서 503 Service Unavailable 원인 10분 진단처럼 체크리스트 기반으로 잘 구조화할수록 ReAct 성능이 좋아집니다.
ReAct를 CoT 유출 없이 쓰는 요령
ReAct를 그대로 구현하면 모델이 Thought를 출력하려고 합니다. 제품에서는 다음 원칙을 추천합니다.
- 모델에게는 내부적으로 “계획/검증”을 하게 하되
- 출력 포맷은
Answer와Evidence(툴 결과 요약)만 허용 - 툴 호출은 구조화된 JSON 이벤트로만 기록
즉, “생각”은 내부 상태로 두고, 사용자에게는 행동 결과(관측값) 중심으로 설명합니다.
예제: Node.js로 ReAct 루프 만들기(툴 호출 포함)
아래 예제는 OpenAI 호환 API 형태로 작성했지만, 구조는 대부분의 LLM SDK에 그대로 적용됩니다.
// react-agent.ts
import OpenAI from "openai";
type ToolResult = {
tool: string;
input: unknown;
output: unknown;
};
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function tool_search(query: string) {
// 실제로는 사내 검색/벡터DB/웹 검색 등을 연결
return {
topHits: [
{ title: "Runbook: EKS 503 triage", url: "https://internal/runbook/eks-503", snippet: "Check ingress, endpoints, pod readiness..." }
]
};
}
async function tool_kubectl(args: { namespace: string; resource: string }) {
// 실제로는 RBAC 걸린 실행기(또는 관측 API)로 대체
return {
namespace: args.namespace,
resource: args.resource,
summary: "3 pods Running, 1 pod CrashLoopBackOff, endpoints empty"
};
}
const tools = {
tool_search,
tool_kubectl
};
function safeUserFacingAnswer(answer: string, evidence: ToolResult[]) {
// CoT 대신 증거 요약만 노출
return {
answer,
evidence: evidence.map(e => ({ tool: e.tool, output: e.output }))
};
}
export async function reactSolve(userQuestion: string) {
const evidence: ToolResult[] = [];
const system = [
"You are an operations assistant.",
"Do not reveal chain-of-thought.",
"Use tools when needed.",
"When you respond to the user, output only JSON with keys: answer, evidence.",
"Evidence must be brief and based on tool outputs.",
"If unsure, ask a clarifying question in answer."
].join("\n");
let messages: any[] = [
{ role: "system", content: system },
{ role: "user", content: userQuestion }
];
for (let step = 0; step < 6; step++) {
const resp = await client.chat.completions.create({
model: "gpt-4.1-mini",
messages,
temperature: 0.2,
// function calling 또는 tool calling을 쓰는 방식이 가장 안전
tools: [
{
type: "function",
function: {
name: "tool_search",
description: "Search runbooks/docs.",
parameters: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"]
}
}
},
{
type: "function",
function: {
name: "tool_kubectl",
description: "Get cluster resource summary.",
parameters: {
type: "object",
properties: {
namespace: { type: "string" },
resource: { type: "string" }
},
required: ["namespace", "resource"]
}
}
}
]
});
const msg = resp.choices[0].message;
// 툴 호출이 있으면 실행
if (msg.tool_calls && msg.tool_calls.length > 0) {
messages.push(msg);
for (const call of msg.tool_calls) {
const name = call.function.name as keyof typeof tools;
const input = JSON.parse(call.function.arguments || "{}");
const output = await tools[name](input as any);
evidence.push({ tool: name, input, output });
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(output)
});
}
continue;
}
// 최종 응답은 JSON으로만 받는다
const parsed = JSON.parse(msg.content || "{}");
return safeUserFacingAnswer(parsed.answer, evidence);
}
return safeUserFacingAnswer(
"추가 정보가 필요합니다. 대상 네임스페이스와 서비스/인그레스 이름을 알려주세요.",
evidence
);
}
이 구현에서 CoT 유출을 막는 포인트
- 모델 출력 계약을
answer/evidence로 제한 evidence는 툴 출력 기반 요약만 허용- 중간 단계는 툴 호출 이벤트로만 남고, “생각”은 남기지 않음
운영 로그에는 tool, input, output을 남기되, input에 민감정보가 포함되지 않도록 마스킹 레이어를 두는 것이 좋습니다.
패턴 2: Self-Consistency(SC) — 여러 번 풀어 합의로 품질 올리기
SC는 동일 질문을 여러 샘플로 풀고(온도 약간 높임), 그 결과를 투표/합의로 결정합니다. 단순해 보이지만, 다음 상황에서 효과가 큽니다.
- 문제 풀이/분류/요약처럼 “답 후보가 몇 개로 수렴”하는 작업
- 단일 샘플에서 편향이 강한 경우
- 긴 문맥에서 한 번씩 실수하는 경우
중요한 점은 SC가 “모델을 더 똑똑하게” 만드는 게 아니라, 불안정성을 평균화해 준다는 것입니다.
SC도 CoT 없이 가능하다
SC를 구현할 때 흔히 “각 샘플의 CoT를 비교”하려고 하는데, 제품에서는 그럴 필요가 없습니다.
- 각 샘플은 최종 답만 내게 한다
- 합의 단계는 별도 모델(또는 같은 모델)로 판정만 한다
- 사용자는 합의된 최종 답 + 근거 요약만 본다
예제: Python으로 SC 투표(결과만 수집)
# self_consistency.py
from collections import Counter
import json
from openai import OpenAI
client = OpenAI()
def sample_answer(question: str, seed: int):
system = "\n".join([
"You are a helpful assistant.",
"Do not reveal chain-of-thought.",
"Return only JSON: {\"final\": string}."
])
resp = client.chat.completions.create(
model="gpt-4.1-mini",
temperature=0.8,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": question + "\nUse concise final answer."}
],
)
content = resp.choices[0].message.content or "{}"
return json.loads(content).get("final", "")
def self_consistent(question: str, k: int = 7):
answers = [sample_answer(question, i) for i in range(k)]
counts = Counter(answers)
best, freq = counts.most_common(1)[0]
return {
"final": best,
"agreement": freq / k,
"candidates": counts.most_common(5)
}
if __name__ == "__main__":
q = "EKS에서 503이 발생할 때 가장 먼저 확인할 3가지는?"
print(self_consistent(q, k=9))
실전 팁: 합의율을 SLO로 둬라
SC는 agreement가 낮을수록 “질문이 모호하거나”, “컨텍스트가 부족하거나”, “툴 검증이 필요”하다는 신호입니다.
agreement가0.4이하이면: 사용자에게 уточ 질문(clarifying question) 유도agreement가 낮은데도 바로 답해야 하면: ReAct로 전환해 근거를 수집
이 방식은 인프라에서 error rate를 보고 자동으로 디버깅 플로우를 바꾸는 것과 유사합니다. EKS에서 파드가 ContainerCreating에 멈추면 CNI/CSI/권한을 순서대로 점검하듯, LLM도 합의율 기반으로 플로우를 전환할 수 있습니다. 참고로 운영 체크리스트 예시는 EKS Pod가 ContainerCreating에 멈출 때 10분 진단 같은 글이 좋은 템플릿입니다.
ReAct와 SC를 같이 쓰는 실전 조합
둘은 경쟁 관계가 아니라 조합이 좋습니다.
- 1단계(SC): 빠르게 여러 번 답해 합의율 확인
- 2단계(ReAct): 합의율이 낮거나, 고위험 답변이면 툴로 검증
- 3단계(SC): 툴 결과를 포함한 컨텍스트로 다시 합의(최종 안정화)
이렇게 하면 비용을 통제하면서도 품질을 끌어올릴 수 있습니다.
라우팅 의사코드
if risk == high:
use ReAct (tools required)
else:
run SC
if agreement < threshold:
use ReAct
run SC again with evidence
return final
여기서 risk는 도메인에 따라 정의합니다.
- 결제/환불/장애 조치: high
- 단순 FAQ: low
CoT 유출 방지 체크리스트(프롬프트만으로는 부족)
CoT 유출을 막을 때 “시스템 프롬프트에 쓰면 끝”이라고 생각하기 쉽지만, 실전에서는 출력 계층과 로깅 계층이 더 중요합니다.
1) 출력은 스키마로 강제
- 자연어로 “생각을 말하지 마”는 약합니다.
JSON schema또는function calling으로 출력 형태를 고정하세요.
예: answer, evidence, confidence, next_questions 정도로 제한.
2) 로그에는 CoT 대신 이벤트만
- 저장: 툴 호출명, 입력 파라미터(마스킹), 응답 요약, 합의율, 토큰 수, 지연시간
- 금지: 모델 원문 CoT, 시스템 프롬프트 전문, 사용자 원문 중 민감정보
3) “설명”은 CoT가 아니라 검증 가능한 근거로
사용자가 납득해야 하는 영역에서는 다음을 권장합니다.
- 계산 결과(수식 자체가 아니라 입력과 결과)
- 툴에서 관측한 상태 요약
- 출처 링크(가능하면)
즉, “내가 이렇게 생각했어”가 아니라 “이 데이터를 봤고 그래서 이렇게 결론” 구조로 바꿉니다.
평가: 정답률만 보지 말고 안정성과 재현성을 보자
ReAct·SC를 도입하면 단순 정확도 외에 관리해야 할 지표가 늘어납니다.
- 툴 호출 성공률: 실패하면 환각이 늘어남
- 평균 툴 호출 횟수: 비용과 지연시간에 직결
- 합의율 분포: 질문 난이도/모호성의 대리지표
- fallback 비율: “모르겠다/추가 질문”으로 전환되는 비율
A/B 테스트에서는 다음 두 가지를 분리해서 측정하는 게 좋습니다.
SC만적용했을 때의 개선폭SC + ReAct라우팅을 적용했을 때의 개선폭
마무리: “생각”을 숨기고, “관측”을 보여줘라
ReAct와 Self-Consistency는 CoT를 외부로 노출하지 않아도 충분히 강력합니다. 핵심은 모델의 내적 추론을 사용자에게 그대로 보여주는 대신, 툴 기반 관측과 합의 기반 안정성을 제품 레이어에서 설계하는 것입니다.
- ReAct: 불확실한 기억 대신 검증 가능한 행동으로 정확도 상승
- SC: 샘플링을 통해 불안정성을 평균화하고 합의율로 리스크를 계량
- CoT 보호: 스키마 강제, 이벤트 로깅, 근거 요약 중심 UX
이 조합을 적용하면 “정답률”뿐 아니라 운영에서 중요한 재현성, 감사 가능성, 보안성까지 함께 가져갈 수 있습니다.