- Published on
CoT 노출 없이 추론 정확도 올리는 실전 패턴
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서빙 환경에서 LLM 추론을 붙이다 보면 곧바로 부딪히는 요구사항이 있습니다.
- 정확도는 높이고 싶다
- 그런데 Chain-of-Thought(CoT) 같은 내부 추론 과정은 사용자에게 노출되면 안 된다
이 요구는 단순히 "보안" 때문만이 아닙니다. 내부 추론이 그대로 노출되면 프롬프트 인젝션에 악용되거나, 모델이 만든 그럴듯한 중간 사고가 사실처럼 소비되는 문제가 생깁니다. 또한 제품 UX 관점에서도 길고 장황한 추론 텍스트는 비용과 지연을 키웁니다.
이 글은 CoT를 노출하지 않으면서 정확도를 올리는 방법을 프롬프트 레벨, 오케스트레이션 레벨, 평가/운영 레벨로 나눠서 정리합니다. 예시는 OpenAI 계열 API를 기준으로 하되, 어떤 LLM 스택에도 적용 가능한 형태로 설명합니다.
왜 CoT를 숨기면서도 정확도를 올릴 수 있나
핵심은 "모델이 스스로 생각하게 하되, 출력은 생각이 아니라 결과/근거만 내보내게" 하는 것입니다. 이를 위해 다음을 분리합니다.
- 추론 과정(숨김): 모델이 내부적으로 더 많은 계산을 하도록 유도
- 외부 출력(노출): 짧은 답 + 검증 가능한 근거(인용/계산 결과/정형 데이터)
여기서 "근거"는 CoT 전체가 아니라, 사용자가 검증 가능한 형태의 증거(evidence) 여야 합니다. 예를 들어 문서 RAG라면 인용 구절, 수치 계산이라면 최종 식과 결과, 정책 판단이라면 적용한 규칙 목록 같은 것입니다.
패턴 1: 출력 스키마를 고정하고 추론은 숨기기
가장 먼저 할 일은 출력 형식을 강하게 제한하는 것입니다. 모델이 장황한 추론을 내뱉을 여지를 줄이면, 답이 더 일관되고 후처리도 쉬워집니다.
예시: JSON 스키마 기반의 "답변 + 근거" 출력
아래는 사용자에게 CoT를 보여주지 않고도, 최소한의 근거를 제공하는 형태입니다.
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const schema = {
name: "answer_schema",
schema: {
type: "object",
additionalProperties: false,
properties: {
answer: { type: "string" },
confidence: { type: "number", minimum: 0, maximum: 1 },
evidence: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
type: { type: "string", enum: ["quote", "calc", "rule", "link"] },
detail: { type: "string" }
},
required: ["type", "detail"]
}
},
next_questions: { type: "array", items: { type: "string" } }
},
required: ["answer", "confidence", "evidence"]
}
} as const;
const resp = await client.responses.create({
model: "gpt-4.1-mini",
input: [
{
role: "system",
content: [
{
type: "text",
text:
"너는 정확한 기술 어시스턴트다. 내부 추론 과정은 절대 출력하지 말고, 결과와 검증 가능한 근거만 JSON 스키마에 맞춰 출력하라."
}
]
},
{
role: "user",
content: [{ type: "text", text: "Redis 캐시가 간헐적으로 stale 되는 원인 3가지를 알려줘" }]
}
],
text: {
format: {
type: "json_schema",
...schema
}
}
});
console.log(resp.output_text);
이 방식의 장점은 다음과 같습니다.
- 제품 UI에서 그대로 렌더링 가능
- "근거"를 강제해 환각을 줄임
- 추론 텍스트를 출력할 자리가 없어 CoT 노출 위험이 낮음
주의할 점은, JSON 스키마를 강하게 걸어도 모델이 가끔 규격을 어길 수 있다는 것입니다. 운영에서는 파서 실패 시 재시도하거나, 스키마 위반을 자동 수정하는 리커버리 루프를 추가하는 게 안전합니다.
관련해서 API 호출/모델 지정 문제로 막히는 경우가 자주 있으니, 모델 접근 오류가 난다면 이 글도 함께 보세요: OpenAI Responses API 403 model_not_found 해결 가이드
패턴 2: "숨은 초안"과 "최종 답"을 분리하는 2단계 생성
CoT를 노출하지 않는 가장 실전적인 오케스트레이션은 2단계입니다.
- 모델이 내부적으로 충분히 풀어쓴 초안(또는 계획)을 만든다
- 그 초안을 바탕으로 사용자에게 보여줄 짧은 최종 답을 만든다
핵심은 1단계 산출물을 사용자에게 직접 반환하지 않고, 2단계에서 "요약 + 근거"로 재작성하게 하는 것입니다.
예시: 2단계 파이프라인(초안은 서버 내부에만 보관)
from openai import OpenAI
client = OpenAI()
def draft_reasoning(question: str) -> str:
r = client.responses.create(
model="gpt-4.1-mini",
input=[
{"role": "system", "content": "문제를 철저히 분석하고 해결 전략을 만든다. 출력은 초안이며 사용자에게 노출되지 않는다."},
{"role": "user", "content": question},
],
)
return r.output_text
def final_answer(question: str, draft: str) -> str:
r = client.responses.create(
model="gpt-4.1-mini",
input=[
{
"role": "system",
"content": "초안을 참고하되, 내부 추론은 절대 노출하지 말고 핵심 답과 근거만 간결히 출력하라."
},
{"role": "user", "content": f"질문: {question}\n\n초안(내부용): {draft}\n\n최종 답변을 작성해줘."},
],
)
return r.output_text
q = "RAG에서 검색은 잘 되는데 답이 자꾸 틀리는 이유와 개선책은?"
d = draft_reasoning(q)
print(final_answer(q, d))
이 방식은 "모델에게 생각할 공간"을 주면서도, 외부로는 깔끔한 답만 내보낼 수 있습니다.
추가 팁:
- 1단계 초안에는 체크리스트, 가정, 위험요소, 반례 등을 포함시키면 정확도가 올라갑니다.
- 2단계에서는 출력 포맷을 더 빡세게 제한하세요(예: 5문장 이내, 근거 3개까지).
패턴 3: Self-check 루프(검증자 모델)로 오답을 걸러내기
CoT를 숨기고 정확도를 올리는 데 가장 효과적인 방법 중 하나는 검증 단계를 별도로 두는 것입니다.
- 생성 모델: 답을 만든다
- 검증 모델: 답이 질문을 충족하는지, 근거가 충분한지, 금지된 내용이 섞였는지 검사한다
검증자는 CoT를 요구하지 않아도 됩니다. 대신 "정답/오답" 판정과 "수정 지시"만 내리게 하면 됩니다.
예시: 판정 스키마로 자동 재시도
type Verdict = {
ok: boolean;
issues: string[];
fix_instructions: string;
};
async function verify(answer: string, question: string): Promise<Verdict> {
const r = await client.responses.create({
model: "gpt-4.1-mini",
input: [
{
role: "system",
content:
"너는 엄격한 검증자다. 답변의 정확성/누락/모순/금지된 CoT 노출 여부를 검사하고 Verdict JSON만 출력하라."
},
{ role: "user", content: `질문: ${question}\n답변: ${answer}` }
],
text: {
format: {
type: "json_schema",
name: "verdict",
schema: {
type: "object",
additionalProperties: false,
properties: {
ok: { type: "boolean" },
issues: { type: "array", items: { type: "string" } },
fix_instructions: { type: "string" }
},
required: ["ok", "issues", "fix_instructions"]
}
}
}
});
return JSON.parse(r.output_text) as Verdict;
}
운영에서는 다음과 같이 씁니다.
ok가false면fix_instructions를 시스템 프롬프트로 넣고 재생성- 재시도 횟수는 1~2회로 제한(비용/지연 폭발 방지)
이 루프는 특히 "답은 그럴듯한데 중요한 조건을 하나 빼먹는" 유형의 오류를 줄이는 데 유리합니다.
패턴 4: 도구 사용으로 계산/검색을 외주화해서 환각 줄이기
모델이 CoT로 길게 풀어야 하는 문제는 대개 두 가지입니다.
- 사실 확인(문서/DB/웹)
- 계산/변환(수치 계산, 날짜 계산, 포맷 변환)
이 둘을 모델 내부 추론에만 맡기면 환각이 늘어납니다. 대신 도구 호출을 붙이면 CoT를 노출하지 않아도 정확도가 올라갑니다.
예시: RAG에서 인용 기반 근거만 노출
- 모델에게 "반드시 인용을 포함"하도록 강제
- 인용은 검색 결과의 일부 텍스트로 제한
# 의사코드: 검색 결과 chunks를 넣고, 인용만 근거로 허용
system = """
너는 문서 기반 Q&A다.
- 답변은 제공된 컨텍스트에서만 작성
- 근거는 인용(quote)로만 제시
- 내부 추론은 출력하지 말 것
"""
user = "배포 롤백 기준을 3가지로 정리해줘"
context = """
[doc1] ...
[doc2] ...
"""
resp = client.responses.create(
model="gpt-4.1-mini",
input=[
{"role":"system","content":system},
{"role":"user","content":f"컨텍스트:\n{context}\n\n질문:{user}"}
],
)
print(resp.output_text)
이때 "컨텍스트 밖 지식 사용 금지"를 걸면, 모델이 CoT를 길게 늘어놓는 대신 컨텍스트에 맞춰 답을 압축하는 경향이 커집니다.
패턴 5: 프롬프트에서 "설명"과 "추론"을 분리해서 요구하기
많은 팀이 "설명을 해달라"고 요청했다가 CoT 비슷한 것을 그대로 받습니다. 여기서 중요한 구분은 다음입니다.
- 추론: 모델이 결론에 도달하는 내부 사고 과정(노출 금지)
- 설명: 사용자가 검증 가능한 근거/요약/절차(노출 가능)
프롬프트에 이 구분을 명시하면 품질이 좋아집니다.
권장 문구(그대로 재사용 가능)
- "내부 추론 과정은 출력하지 말고, 결론과 근거(인용/규칙/계산 결과)만 제시하라"
- "근거는 최대 3개, 각 근거는 한 문장"
- "확실하지 않으면 불확실하다고 말하고, 추가로 필요한 정보 질문 2개를 제시하라"
이렇게 하면 모델이 억지로 CoT를 길게 늘어놓기보다, 불확실성을 인정하고 정보 요구로 전환하게 되어 실제 정확도가 개선됩니다.
패턴 6: 난이도 라우팅과 비용 제어(정확도는 올리고 지연은 제한)
CoT를 숨기려면 종종 추가 호출(2단계, 검증 루프 등)이 필요합니다. 그러면 비용과 지연이 늘어납니다. 이를 막으려면 난이도 라우팅을 넣는 게 좋습니다.
- 쉬운 질문: 단일 호출 + 스키마 출력
- 중간 질문: 2단계(초안-최종)
- 어려운 질문: 2단계 + 검증 루프 + 도구 사용
예시: 간단한 라우터
function route(question: string) {
const len = question.length;
const hasNumbers = /\d/.test(question);
const hasPolicy = /(규정|정책|컴플라이언스|PII|개인정보)/.test(question);
if (hasPolicy) return "hard";
if (hasNumbers && len > 80) return "medium";
if (len > 200) return "medium";
return "easy";
}
라우팅이 정교할수록 좋지만, 처음에는 단순 규칙으로도 체감 효과가 큽니다.
운영에서 꼭 챙길 것: 로그/재현성/회귀 테스트
CoT를 저장하지 않으면 디버깅이 어려워질 수 있습니다. 그래서 "추론 텍스트" 대신 아래를 남기는 방식으로 관측 가능성을 확보합니다.
- 입력(질문), 사용된 컨텍스트(RAG chunks), 도구 호출 결과
- 출력(JSON), 검증자 verdict
- 모델/버전, 온도, top_p 같은 파라미터
이렇게 하면 CoT 없이도 "왜 틀렸는지"를 상당 부분 재현할 수 있습니다.
성능/메모리 이슈로 로컬 LLM을 운영한다면, 추론 최적화는 정확도와 별개로 안정성에 직결됩니다. 대규모 모델 OOM을 다루는 글도 참고가 됩니다: Transformers 로컬 LLM OOM 해결 - 4bit+KV캐시
흔한 실수 5가지
1) "자세히 설명해줘"만 넣고 통제하지 않기
설명 요구는 CoT 유출로 이어지기 쉽습니다. "설명 형식"을 제한하세요.
2) 근거를 "느낌"으로 쓰게 두기
"왜냐하면 일반적으로" 같은 문장은 환각의 온상입니다. 근거 타입을 quote, calc, rule 처럼 제한하면 좋아집니다.
3) 검증 단계를 사람에게만 맡기기
QA가 병목이 됩니다. 검증자 모델을 붙여서 1차 필터링을 자동화하세요.
4) 재시도 무한 루프
검증이 실패하면 재시도하되, 횟수 제한과 타임아웃을 강제해야 합니다.
5) 모델 접근/권한 문제를 품질 문제로 오해
특정 모델이 호출되지 않거나 권한이 없으면 결과가 흔들립니다. 오류 코드를 먼저 확인하세요. 앞서 언급한 OpenAI Responses API 403 model_not_found 해결 가이드 같은 케이스가 대표적입니다.
결론: CoT 없이도 "정확도"는 시스템으로 만든다
CoT를 사용자에게 보여주지 않으면서 정확도를 올리는 핵심은 모델에게 "생각"을 맡기는 게 아니라, 생각을 유도하는 구조를 시스템으로 제공하는 것입니다.
- 출력 스키마로 답변을 강제하고
- 초안-최종 2단계로 사고 공간을 분리하고
- 검증 루프로 오답을 걸러내고
- 도구 사용으로 사실/계산을 외주화하고
- 난이도 라우팅으로 비용과 지연을 제어
이 조합이면 CoT를 숨겨도, 오히려 더 일관되고 제품화 가능한 답변 품질을 만들 수 있습니다.