10장. MCP 와 Prompt·Context Engineering
실습 5 — 컨텍스트 고르기
7장의 IT 지원 안내(30조각)와 사내 규정(30조항)을 모두 프롬프트에 붙일 때와, 검색으로 3조각만 고를 때를 견줍니다. 컨텍스트 창의 크기도 바꿔 봅니다. 창 크기는 OpenAI 호환 주소로는 정할 수 없어서, Ollama 고유의 주소(/api/chat)에 options 의 num_ctx 로 정합니다. 질문 8개의 정답 수, 프롬프트 토큰 수, 시간을 봅니다.
1단계. 문서 60조각과 질문 준비
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단계. 창 크기를 정해 묻는 함수
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단계. 세 가지 설정 견주기
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단계. 답의 생김새
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 라면 토큰 수만큼 비용이 들고 문서가 늘면 창을 넘습니다. 넣을 수 있다고 다 넣지 않고 골라 넣는 까닭입니다. 답에 "每年" 같은 한자가 섞인 것은 이 작은 모델의 버릇입니다.