본문 바로가기
AI2026년 8월 26일18분 읽기

컨텍스트 엔지니어링 — 프롬프트 그다음, LLM의 컨텍스트 윈도우를 설계하는 법

YS
김영삼
조회 13
컨텍스트 엔지니어링 — 프롬프트 그다음, LLM의 컨텍스트 윈도우를 설계하는 법

모델은 프롬프트가 아니라 컨텍스트 윈도우 전체를 읽는다. 진짜 실력은 그 창을 어떻게 채우느냐에서 갈린다.

컨텍스트 엔지니어링은 시스템 프롬프트·검색 결과·도구 정의·대화 이력·메모리를 하나의 예산 안에서 배치하고 압축하고 순서 짓는 규율이다. 프롬프트 한 줄을 다듬는 일이 아니라, 매 요청마다 조립되는 입력 파이프라인을 설계하는 일이다. 이 글은 청킹·리트리벌·재순위·압축·캐싱·평가까지 실전 관점으로 정리한다.

~50%
긴 컨텍스트 끝단·중앙에서 흔한 정보 소실(lost in the middle) 영향폭
200K~1M
2026년 상용 모델의 대표적 컨텍스트 윈도우 토큰 범위
~90%
프롬프트 캐시 적중 시 입력 토큰 비용 절감폭(공개 특성 기준)
8~20%
청크 간 권장 오버랩 비율(경계 문맥 보존 목적)

프롬프트 엔지니어링과 무엇이 다른가

프롬프트 엔지니어링은 사람이 손으로 쓰는 한 덩어리의 지시문을 최적화한다. "이런 역할로, 이런 형식으로 답하라"를 다듬는 일이다. 이 접근은 정적이다. 입력이 매번 거의 같고, 개발자가 문장을 조율한다.

컨텍스트 엔지니어링은 한 단계 위의 문제를 다룬다. 실제 프로덕션에서 모델에 들어가는 입력은 사람이 미리 쓴 문장이 아니라 런타임에 동적으로 조립되는 여러 조각이다. 사용자 질문, 벡터 검색으로 끌어온 문서, 이전 대화 요약, 장기 메모리, 도구 정의와 도구 실행 결과, 시스템 규칙이 매 턴 합쳐진다. 컨텍스트 엔지니어링은 이 조립 규칙 — 무엇을, 얼마나, 어떤 순서로, 어떻게 압축해서 넣을지 — 을 설계한다.

핵심 통찰은 이것이다. 모델 성능은 "좋은 지시문"만이 아니라 "창 안에 든 정보의 신호 대 잡음비"에 크게 좌우된다. 관련 없는 문서 20개를 넣으면 관련 있는 문서 3개를 넣을 때보다 오히려 정확도가 떨어진다. 그래서 검색·선별·압축·배치가 프롬프트 문장 튜닝보다 중요해진다.

관점프롬프트 엔지니어링컨텍스트 엔지니어링
대상고정된 지시 문장런타임에 조립되는 입력 전체
주 단위단어·문장·예시(few-shot)토큰 예산·청크·검색결과·메모리
병목표현·모호성관련성·순서·길이·비용
개선 수단문구 재작성리트리벌·재순위·압축·캐싱
평가주관적 품질 확인retrieval 지표 + end-to-end 정답률

컨텍스트 윈도우는 예산이다

컨텍스트 윈도우가 200K, 1M 토큰까지 커졌지만 "다 넣으면 된다"는 함정이다. 이유는 세 가지다. 첫째, 비용과 지연이 입력 토큰에 비례한다. 둘째, 창이 길수록 중간 정보의 활용도가 떨어진다(뒤에서 다룬다). 셋째, 잡음이 늘면 모델이 엉뚱한 근거를 붙잡는다.

그래서 창을 고정 예산으로 보고 항목별로 배분하는 사고가 필요하다. 예를 들어 32K 토큰을 실효 예산으로 잡았다면 대략 이렇게 나눈다. 시스템 프롬프트·정책 2~4K, 도구 정의 1~3K, 검색된 근거 문서 8~16K, 대화 이력·요약 4~8K, 사용자 질문과 응답 여유 2~4K. 나머지는 안전 마진으로 비워둔다.

참고토큰 수는 대략 영어 1토큰 ≈ 4자, 한국어는 자모 특성상 1토큰이 1~2자에 그칠 때가 많다. 한국어 문서는 같은 글자 수라도 토큰이 2~3배 더 나올 수 있으니 예산을 넉넉히 잡아라. 정확한 값은 각 모델의 토크나이저로 실제 카운트해야 한다.
  • 예산은 항목별 상한(cap)으로 강제한다. 검색이 20청크를 반환해도 상한 안에서 top-k만 넣는다.
  • 상한을 넘으면 버리지 말고 압축한다(요약·중복 제거).
  • 가장 중요한 근거는 창의 앞이나 맨 끝에 둔다.

RAG 청킹 전략: 크기·오버랩·의미 기반

검색 품질의 8할은 청킹에서 결정된다. 청크가 너무 크면 한 청크에 여러 주제가 섞여 임베딩이 뭉개지고, 너무 작으면 문맥이 잘려 답에 필요한 정보가 흩어진다. 실무 출발점은 300~800 토큰 청크에 8~20% 오버랩이다. 오버랩은 문장이 청크 경계에서 잘려 의미가 끊기는 것을 막는다.

고정 크기 분할은 구현이 쉽지만 문단·표·코드 블록을 무시한다. 더 나은 방법은 구조 기반(문단·헤더·마크다운 섹션 경계에서 자르기)과 의미 기반(인접 문장 임베딩 유사도가 급락하는 지점에서 자르기)이다. 의미 기반은 비용이 더 들지만 주제가 바뀌는 자연스러운 경계를 잡아 청크 순도를 높인다.

# 문장 임베딩 유사도가 급락하는 지점에서 자르는 의미 기반 청킹(의사코드)
import numpy as np

def semantic_chunks(sentences, embed, max_tokens=700, drop=0.25):
    embs = embed(sentences)              # (N, dim) 정규화된 임베딩
    chunks, cur, cur_tok = [], [], 0
    for i, s in enumerate(sentences):
        t = count_tokens(s)
        # 이전 문장과의 코사인 유사도가 임계 이하로 떨어지면 주제 전환으로 간주
        boundary = i > 0 and cosine(embs[i], embs[i-1]) < (1 - drop)
        if cur and (cur_tok + t > max_tokens or boundary):
            chunks.append(" ".join(cur))
            # 경계 문맥 보존을 위해 마지막 한 문장을 다음 청크로 오버랩
            cur, cur_tok = cur[-1:], count_tokens(cur[-1])
        cur.append(s); cur_tok += t
    if cur: chunks.append(" ".join(cur))
    return chunks

def cosine(a, b):
    return float(np.dot(a, b))           # 임베딩이 L2 정규화되어 있다고 가정

추가로 각 청크에 메타데이터(문서 제목, 섹션 경로, 날짜, URL)를 붙여라. 검색 시 필터링에 쓰이고, 최종 답변의 출처 표기에도 쓰인다. 청크 앞에 "문서: X > 섹션: Y" 한 줄을 프리픽스로 넣는 것만으로도 임베딩과 답변 근거가 함께 좋아진다.

임베딩·리트리벌·재순위(rerank)

파이프라인은 보통 3단계다. (1) 청크를 임베딩해 벡터 DB에 저장하고, (2) 질문을 같은 공간으로 임베딩해 근접 이웃 top-N(예: 50)을 빠르게 뽑고, (3) 이 후보를 재순위 모델(cross-encoder reranker)로 다시 채점해 진짜 상위 top-k(예: 5)만 컨텍스트에 넣는다.

왜 두 단계인가. 임베딩 검색(bi-encoder)은 질문과 문서를 따로 벡터화하므로 빠르지만 미묘한 관련성을 놓친다. 리랭커는 질문과 문서를 함께 입력해 상호작용을 보므로 정확하지만 느리다. 그래서 "값싼 리콜 → 비싼 정밀"의 2단 구조가 표준이 됐다. 여기에 키워드 기반 BM25를 임베딩과 섞는 하이브리드 검색을 더하면, 고유명사·코드 심볼처럼 임베딩이 약한 정확 일치를 보완한다.

# 하이브리드 리콜 + 재순위 파이프라인(의사코드)
def retrieve(query, k=5, n=50):
    # 1) 값싼 리콜: 벡터 검색과 BM25를 합쳐 후보 N개 확보
    dense = vector_db.search(embed(query), top=n)     # 의미 유사
    sparse = bm25.search(query, top=n)                # 정확 일치(심볼/고유명사)
    cand = dedup(dense + sparse)

    # 2) 비싼 정밀: cross-encoder로 (query, chunk) 쌍을 직접 채점
    scored = reranker.score([(query, c.text) for c in cand])
    ranked = sorted(zip(cand, scored), key=lambda x: x[1], reverse=True)

    # 3) 토큰 예산 안에서만 채택(상한 초과분은 버림)
    out, budget = [], 6000
    for c, s in ranked:
        if s < 0.2:            # 관련성 임계 미달은 제외(잡음 차단)
            break
        if budget - c.tokens < 0:
            break
        out.append(c); budget -= c.tokens
        if len(out) >= k:
            break
    return out
주의top-k를 무작정 키우지 마라. 관련 없는 청크를 하나 더 넣는 것이 관련 있는 청크의 활용을 방해할 수 있다. "임계 미달은 제외"가 "많이 넣기"보다 대체로 유리하다. k는 3~8 사이에서 평가로 정하라.

"lost in the middle": 위치가 성능을 바꾼다

긴 컨텍스트를 다루는 연구에서 반복 관찰된 현상이 있다. 정답 근거가 컨텍스트의 맨 앞이나 맨 뒤에 있을 때 모델이 잘 찾고, 중간에 파묻히면 성능이 눈에 띄게 떨어진다. 성능-위치 곡선이 U자를 그린다. 창이 길수록 이 골이 깊어진다.

실전 대응은 단순하다. 첫째, 재순위로 정말 중요한 근거만 남긴다(중간에 잡음이 없으면 파묻힐 것도 없다). 둘째, 가장 강한 근거를 앞이나 끝에 배치한다. 셋째, 질문·핵심 지시를 컨텍스트 끝 가까이 반복해 최신성 편향을 활용한다. 넷째, 근거가 많아 불가피하게 길어지면 압축으로 중간부의 부피 자체를 줄인다.

"컨텍스트가 크다"는 "많이 넣어도 된다"가 아니라 "많이 넣을 수 있으니 더 신중히 골라야 한다"에 가깝다. 큰 창은 게으름의 면죄부가 아니라 설계의 자유도다.

컨텍스트 압축과 요약

예산을 넘기면 두 축으로 압축한다. 문서 압축은 검색된 청크에서 질문과 무관한 문장을 걷어내는 것이다(추출적 요약, 문장 단위 관련성 필터). 대화 압축은 길어진 히스토리를 요약으로 접는 것이다. 흔한 패턴은 최근 N턴은 원문 그대로 두고, 그보다 오래된 부분은 "지금까지의 결정·사실" 요약 블록으로 대체하는 롤링 요약이다.

# 롤링 요약: 최근 턴은 원문 유지, 오래된 턴은 요약으로 대체
def build_history(turns, keep_recent=6, tok_cap=4000):
    recent = turns[-keep_recent:]
    older = turns[:-keep_recent]
    if not older:
        return render(recent)
    # 오래된 대화는 사실/결정 위주로 손실 요약(장식 문장 제거)
    summary = llm_summarize(
        older,
        instruction="사용자 목표, 확정된 결정, 미해결 항목만 불릿으로. 잡담 제거."
    )
    hist = f"[이전 요약]\n{summary}\n\n[최근 대화]\n{render(recent)}"
    return truncate_to(hist, tok_cap)

압축은 손실이다. 그래서 무엇을 반드시 보존할지를 명시하는 것이 핵심이다. 사용자 목표, 확정된 제약, 고유명사·수치·ID는 원문 그대로 유지하고, 서술적 문장만 접어라. 요약을 요약하는 다단 압축은 사실 왜곡을 누적시키므로, 원본 소스로 되돌아갈 수 있는 참조(문서 ID·오프셋)를 함께 남겨 필요할 때 재수화(rehydrate)하는 편이 안전하다.

도구·메모리·시스템 프롬프트의 자리 배치

컨텍스트는 순서가 있는 구조물이다. 일반적으로 잘 작동하는 레이아웃은 다음과 같다.

위치넣는 것이유
최상단(고정)시스템 규칙·역할·정책·도구 정의거의 안 바뀌어 캐시에 유리, 전 구간에 영향
중상단장기 메모리·사용자 프로필안정적 배경, 매 턴 참조
중단검색 근거(top-k, 재순위 후)질문마다 달라지는 부분
하단대화 이력·요약최신 맥락, 자주 갱신
맨 끝현재 질문 + 출력 형식 지시최신성 편향으로 가장 잘 반영됨

두 가지 원칙을 기억하라. 첫째, 변하지 않는 것은 앞으로, 자주 변하는 것은 뒤로. 이 배치는 프롬프트 캐시 적중률을 극대화한다(아래). 둘째, 도구는 필요한 것만 노출하라. 도구 정의가 30개면 모델의 선택 정확도가 떨어지고 토큰만 먹는다. 상황에 따라 도구 목록을 동적으로 좁히는 것도 컨텍스트 엔지니어링의 일부다.

메모리는 "무엇을 기억하고 무엇을 잊을지"의 정책 문제다. 매 턴 전체 이력을 넣는 대신, 별도 저장소에 사실을 적립하고 질문과 관련된 메모리만 검색해 넣는다. 즉 메모리도 리트리벌 대상으로 취급하면 창을 아끼면서 개인화를 유지할 수 있다.

프롬프트 캐싱: 같은 앞부분을 재사용하라

많은 상용 API가 프롬프트 캐시를 제공한다. 요청들의 공통 접두부(prefix)를 서버가 캐시해 두고, 다음 요청에서 그 접두부가 그대로면 재계산을 건너뛴다. 효과는 두 가지다. 캐시 적중 구간의 입력 비용이 크게 내려가고(공개된 특성상 대략 최대 90% 수준까지), 첫 토큰까지의 지연도 줄어든다.

캐시를 살리는 설계 규칙은 명확하다. 안정적인 내용을 맨 앞에, 매번 달라지는 내용을 맨 뒤에 두어라. 시스템 프롬프트·도구 정의·불변 지식은 접두부로, 사용자 질문·검색 결과는 접미부로. 앞부분의 단 한 글자만 바뀌어도 그 지점부터 캐시가 무효화되므로, 타임스탬프나 요청 ID 같은 변동 값을 접두부에 섞지 마라.

# 캐시 친화적 조립: 접두부(불변)와 접미부(가변)를 분리
def build_messages(user_q, retrieved):
    prefix = [
        {"role": "system", "content": SYSTEM_RULES},   # 고정
        {"role": "system", "content": TOOL_SPECS},     # 고정
    ]  # <-- 이 경계까지 캐시 대상으로 표시(예: cache_control)
    suffix = [
        {"role": "user", "content": render(retrieved) + "\n\n질문: " + user_q},
    ]  # 매 요청 달라지는 부분
    return prefix + suffix
참고캐시 정책(최소 토큰 수, 유효 시간, 명시적 표시 방식)은 제공자마다 다르다. Claude 계열은 cache_control로 캐시 구간을 명시하는 방식을 쓴다. 실제 절감폭·수명은 반드시 사용하는 모델의 최신 문서에서 확인하라.

평가: 무엇을 어떻게 측정하나

컨텍스트 엔지니어링은 감으로 하면 무너진다. 두 층위로 나눠 측정한다. 리트리벌 층은 검색 자체의 품질이다. 정답 청크가 후보에 들어왔는지(recall@k), 상위에 놓였는지(MRR·nDCG), 재순위가 순위를 개선했는지를 본다. 엔드투엔드 층은 최종 답의 품질이다. 정답 근거를 실제로 인용했는지(faithfulness/근거성), 질문에 답했는지(answer relevance), 환각이 없는지를 본다.

실무 루프는 이렇다. (1) 대표 질문 50~200개와 기대 근거·정답으로 골든 셋을 만든다. (2) 청크 크기·오버랩·k·리랭커 유무를 바꿔가며 리트리벌 지표를 측정한다. (3) 상위 후보 구성으로 실제 답을 생성하고 근거성·정답률을 LLM 심사자(judge)나 사람 라벨로 채점한다. (4) 회귀를 잡기 위해 이 평가를 CI에 넣는다. 파라미터 하나를 바꿀 때마다 두 층 지표가 어떻게 움직이는지 봐야, "청크를 키웠더니 recall은 올랐지만 근거성이 떨어졌다" 같은 트레이드오프를 발견한다.

  • retrieval: recall@k, MRR, nDCG, 재순위 전후 비교
  • generation: 근거성(인용 일치), answer relevance, 환각율
  • 운영: 입력 토큰 수, 캐시 적중률, 지연(TTFT), 요청당 비용

자주 묻는 질문

컨텍스트 윈도우가 크면 RAG는 필요 없지 않나요?

아니요. 창이 커도 세 가지 이유로 RAG가 남습니다. 전체 지식 베이스는 어떤 창보다도 크고, 무관한 문서를 다 넣으면 정확도와 비용이 나빠지며(lost in the middle), 최신 데이터는 검색으로 그때그때 끌어와야 합니다. 큰 창은 RAG를 없애는 게 아니라 "한 번에 더 많은 근거를 넣을 수 있게" 해줄 뿐입니다. 여전히 무엇을 넣을지 골라야 합니다.

청크 크기는 몇 토큰이 정답인가요?

단일 정답은 없고 데이터에 따라 다릅니다. 출발점은 300~800 토큰, 오버랩 8~20%입니다. FAQ·짧은 규정처럼 사실이 촘촘하면 작게, 서사·설명 문서면 크게 잡습니다. 반드시 골든 셋으로 recall@k와 근거성을 함께 보며 조정하세요. 가능하면 고정 크기보다 문단·섹션 경계를 존중하는 구조/의미 기반 분할을 쓰는 편이 낫습니다.

재순위(rerank)는 항상 넣어야 하나요?

후보가 많고 정밀도가 중요하면 강하게 권장합니다. 임베딩 검색만으로는 미묘한 관련성을 놓치는데, cross-encoder 리랭커가 top-50을 다시 채점해 진짜 상위 5개를 뽑아주면 근거성이 뚜렷이 좋아집니다. 다만 지연과 비용이 늘어나므로, 초저지연이 필수인 경우나 후보가 이미 매우 적은 경우엔 생략하고 임계값 필터로 대체할 수 있습니다.

프롬프트 캐시를 최대한 활용하려면요?

변하지 않는 내용(시스템 규칙·도구 정의·불변 지식)을 프롬프트 맨 앞에 모으고, 매 요청 달라지는 내용(사용자 질문·검색 결과)을 맨 뒤에 두세요. 접두부에 타임스탬프·요청 ID 같은 변동 값을 넣지 마세요. 앞쪽 한 글자만 바뀌어도 그 뒤 캐시가 전부 무효화됩니다. 구체적 절감폭과 캐시 수명은 사용하는 모델 문서에서 확인하세요.

컨텍스트를 압축하면 정보가 손실되지 않나요?

손실됩니다. 그래서 무엇을 보존할지 명시하는 게 핵심입니다. 사용자 목표·확정된 제약·고유명사·수치·ID는 원문 그대로 두고 서술적 문장만 접으세요. 요약을 다시 요약하는 다단 압축은 왜곡을 누적시키니 피하고, 원본으로 되돌아갈 참조(문서 ID·오프셋)를 남겨 필요할 때 재수화하는 방식이 안전합니다.

컨텍스트 엔지니어링의 성패는 어떻게 판단하나요?

두 층으로 봅니다. 리트리벌 지표(recall@k, MRR, nDCG)로 "필요한 근거가 상위에 왔는가"를, 엔드투엔드 지표(근거성·answer relevance·환각율)로 "그 근거로 실제로 잘 답했는가"를 측정합니다. 여기에 토큰 수·캐시 적중률·지연·비용 같은 운영 지표를 더해 품질과 비용의 균형을 함께 봅니다. 이 평가를 CI에 넣어 회귀를 막는 것이 핵심입니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록