LLM 엔지니어링10장 · MCP 와 Prompt·Context Engineering18 / 22← 이전목차다음 →MetaContents

10장. MCP 와 Prompt·Context Engineering

실습 5 — 컨텍스트 고르기

7장의 IT 지원 안내(30조각)와 사내 규정(30조항)을 모두 프롬프트에 붙일 때와, 검색으로 3조각만 고를 때를 견줍니다. 컨텍스트 창의 크기도 바꿔 봅니다. 창 크기는 OpenAI 호환 주소로는 정할 수 없어서, Ollama 고유의 주소(/api/chat)에 options 의 num_ctx 로 정합니다. 질문 8개의 정답 수, 프롬프트 토큰 수, 시간을 봅니다.

1단계. 문서 60조각과 질문 준비

Python
import json, urllib.request
# 이 PC 의 Ollama 는 오래 쓰다 보면 가끔 깨진 글자(@@@@)만 내는 일이 있었다. 실습을 시작할 때 모델을 내려 깨끗하게 다시 올린다
urllib.request.urlopen(urllib.request.Request("http://localhost:11434/api/generate", headers={"Content-Type": "application/json"},
                                              data=json.dumps({"model": "qwen2.5:3b", "keep_alive": 0}).encode())).read()
import re, time
import numpy as np
from sentence_transformers import SentenceTransformer

help_text = open("it_helpdesk.md", encoding="utf-8").read()
chunks = ["## " + s.strip() for s in help_text.split("\n## ")[1:]]                      # IT 지원 안내 30
chunks += [l for l in open("company_rules.txt", encoding="utf-8").read().split("\n") if re.match(r"^제\d+조\(", l)]   # 규정 30
QUESTIONS = [("메일에 첨부할 수 있는 최대 크기는?", "25MB"), ("계정 문제 티켓은 몇 시간 안에 처리해요?", "4시간"),
             ("손님용 와이파이 이름은?", "SN-Guest"), ("노트북은 몇 년 지나면 바꿀 수 있어요?", "4년"),
             ("해외 출장 일비는 하루 얼마예요?", "60달러"), ("자기계발비는 한 해 얼마까지 지원돼요?", "120만원"),
             ("사원증을 다시 만드는 비용은?", "1만원"), ("필수 교육 기한은 언제예요?", "11월 30일")]
print("조각:", len(chunks), "개 ·", sum(len(c) for c in chunks), "글자")
출력
조각: 60 개 · 4785 글자

2단계. 창 크기를 정해 묻는 함수

Python
SYSTEM = "당신은 솔내기술(가상 회사)의 사내 도우미입니다. [자료]만 보고 한 문장으로 답하세요."

def answer(question, context, num_ctx=4096):
    """Ollama 고유 API 로 묻는다. num_ctx: 컨텍스트 창의 크기(토큰). 돌려주는 것: 답, 실제 프롬프트 토큰 수"""
    body = {"model": "qwen2.5:3b", "stream": False,
            "options": {"num_ctx": num_ctx, "temperature": 0, "seed": 42, "num_predict": 80},
            "messages": [{"role": "system", "content": SYSTEM},
                         {"role": "user", "content": "[자료]\n" + "\n".join(context) + "\n\n[질문]\n" + question}]}
    req = urllib.request.Request("http://localhost:11434/api/chat", data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    r = json.loads(urllib.request.urlopen(req).read())
    return r["message"]["content"], r.get("prompt_eval_count")     # prompt_eval_count: 모델이 읽은 프롬프트 토큰 수

a, n = answer("메일에 첨부할 수 있는 최대 크기는?", chunks[:10])
print(n, "토큰 →", a)
출력
763 토큰 → 메일에 첨부할 수 있는 최대 크기는 25MB입니다. 그보다 큰 파일은 사내 클라우드 '솔내드라이브'에 올려 공유 링크를 통해 보내야 합니다.

3단계. 세 가지 설정 견주기

Python
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
E = emb.encode(["passage: " + c for c in chunks], normalize_embeddings=True)

def top3(question):
    q = emb.encode("query: " + question, normalize_embeddings=True)
    return [chunks[i] for i in np.argsort(-(E @ q))[:3]]

samples = {}
for name, pick, ctx in [("모두 붙이기 · 창 4,096", lambda q: chunks, 4096),
                        ("모두 붙이기 · 창 8,192", lambda q: chunks, 8192),
                        ("검색 3조각 · 창 4,096", top3, 4096)]:
    start, right, tokens = time.time(), 0, []
    for q, key in QUESTIONS:
        a, n = answer(q, pick(q), ctx)
        right += key.replace(" ", "") in a.replace(" ", "").replace(",", "")
        tokens.append(n)
    samples[name] = a                                   # 마지막 질문의 답을 남겨 둔다
    print(f"{name}: 정답 {right}/8 · 프롬프트 토큰 평균 {np.mean(tokens):.0f} · {time.time() - start:.0f}초")
출력
모두 붙이기 · 창 4,096: 정답 8/8 · 프롬프트 토큰 평균 3635 · 37초
모두 붙이기 · 창 8,192: 정답 8/8 · 프롬프트 토큰 평균 3635 · 45초
검색 3조각 · 창 4,096: 정답 8/8 · 프롬프트 토큰 평균 253 · 34초

4단계. 답의 생김새

Python
for name, a in samples.items():
    print(f"{name}: {a[:60]!r}")
출력
모두 붙이기 · 창 4,096: '필수 교육의 기한은每年11월 30일입니다.'
모두 붙이기 · 창 8,192: '필수 교육의 기한은每年11월 30일입니다.'
검색 3조각 · 창 4,096: '필수 교육의 기한은 매년 11월 30일까지입니다.'

이 실습이 뜻하는 것

이 실습은 시험 볼 때 교과서 전체를 들고 들어가는 것과 필요한 쪽만 골라 가는 것을 견주는 일과 같습니다. 교과서 60쪽과 문제 8개를 준비하고(1단계), 책상 크기(한 번에 펼쳐 볼 수 있는 양)를 정해 문제를 푸는 방법을 만든 뒤(2단계), 전체를 들고 들어가며 책상 크기를 두 가지로 바꿔 보고, 관련된 3쪽만 골라 가 보고(3단계), 마지막 문제의 답을 직접 읽어 봅니다(4단계). 여기서 교과서가 문서 60조각, 책상 크기가 컨텍스트 창(num_ctx), 3쪽 고르기가 임베딩 검색입니다.

단계출력뜻
1. 준비조각: 60 개 · 4785 글자IT 지원 안내 30조각과 사내 규정 30조항, 모두 4,785글자를 모았습니다
2. 시험 호출763 토큰 → … 25MB입니다조각 10개를 붙이자 모델이 읽은 글이 763토큰이었고, 답도 맞았습니다. 토큰은 모델이 글을 잘라 읽는 단위입니다
3. 모두 · 창 4,096정답 8/8 · 프롬프트 토큰 평균 3635 · 37초60조각을 다 붙여도 8문제를 모두 맞혔습니다. 다만 창 4,096토큰 가운데 89%쯤을 썼습니다
3. 모두 · 창 8,192정답 8/8 · 프롬프트 토큰 평균 3635 · 45초창을 넓혀도 읽은 양과 정답은 같고, 시간만 늘었습니다
3. 검색 3조각정답 8/8 · 프롬프트 토큰 평균 253 · 34초3조각만 골라 붙이자 읽은 양이 14분의 1쯤으로 줄었는데 정답은 똑같았습니다
4. 답의 생김새"기한은每年11월 30일" / "매년 11월 30일까지"세 판 모두 날짜는 맞았습니다. 앞의 둘에 섞인 "每年"(매년)은 작은 모델의 버릇입니다

한 줄로 줄이면, 넣을 수 있다고 다 넣지 않고 골라 넣어도 정답은 그대로이면서 읽는 양은 크게 줄어든다는 것을 보인 실습입니다. 이 PC 에서는 시간 차이가 몇 초뿐이었지만, 토큰 수만큼 요금을 내는 외부 API(Application Programming Interface, 프로그램끼리 기능을 주고받는 창구)라면 그 차이가 곧 비용 차이입니다. 문제가 8개뿐이고 문서도 창 안에 들어갈 만큼 짧았으므로, 문서가 창을 넘을 만큼 길 때도 모두 붙이기가 다 맞힌다는 뜻은 아닙니다.

코드 설명

  • prompt_eval_count 는 모델이 실제로 읽은 프롬프트의 토큰 수입니다. 본문 "토큰 어림하기"의 어림 대신 정확한 수를 봅니다. OpenAI 호환 주소에서는 usage.prompt_tokens 가 같은 값입니다.
  • 60조각을 모두 붙이면 프롬프트가 평균 3,635토큰으로, 창 4,096 의 89% 를 썼습니다. 그래도 이번 실행에서는 8개를 모두 맞혔습니다. 정답 조각이 어디 있든 이 길이에서는 찾아냈습니다.
  • 창을 8,192 로 늘려도 정답은 같았습니다. 창 크기를 바꾸면 Ollama 가 모델을 다시 올리므로 그 시간이 더 들었습니다(45초). 더 긴 문서를 넣어야 할 때 쓰는 손잡이입니다.
  • 검색으로 3조각만 붙이면 프롬프트가 평균 253토큰으로 모두 붙일 때의 14분의 1 이고, 정답은 똑같이 8개입니다. 이 PC 에서는 시간 차이가 작았지만(34초와 37초), 외부 API 라면 토큰 수만큼 비용이 들고 문서가 늘면 창을 넘습니다. 넣을 수 있다고 다 넣지 않고 골라 넣는 까닭입니다. 답에 "每年" 같은 한자가 섞인 것은 이 작은 모델의 버릇입니다.
← 이전 절10장 · 실습 4 — 프롬프트 다듬기다음 절 →10장 · 실습 6 — 실패에 강한 호출