학습 자료

검색기와 붙인다 — 여기까지가 RAG 다


Article

앞 편 마지막에서 모델이 엉뚱한 소리를 했다. 모델이 우리 문서를 모르기 때문이었다. 그런데 우리한테는 그 문서가 있다. 오늘 그 조각을 질문과 같이 넣어준다.

오늘 여섯 시간 중 지금
  1. ① 모델을 얹는다끝남
  2. ② 파이썬에서 부른다끝남
  3. ③ 검색기와 붙인다지금 여기
  4. ④~⑥ 답을 고치고 잰다오후

1. RAG 는 세 글자가 그대로 순서다

어려운 개념이 아니라 방금 말한 그 순서에 붙은 이름이다.

RAG

Retrieval-Augmented Generation

Retrieval
검색
17강에서 만들어둔 것
Augmented
보태서
오늘 배울 것 · f-string 하나
Generation
생성
앞 편에서 만든 것
오늘 만들 것
  1. 질문「조각을 겹치게 자르는 이유가 뭔가」
  2. 검색9,005조각에서 가까운 3개
  3. 프롬프트 조립조각 + 질문을 한 덩이로
  4. 모델읽고 답한다
셋 중 둘은 이미 만들어져 있다. 오늘 새로 만드는 것은 가운데 하나다.

여기서 제일 흔한 오해가 있다. 모델을 우리 문서로 학습시키는 게 아니다. 모델은 하나도 안 바뀐다 — 받아둔 그 2GB 짜리 파일 그대로다. 바뀌는 것은 매번 보내는 질문지다. 시험지에 비유하면 학생을 다시 가르치는 게 아니라 오픈북으로 바꾸는 것이다.

2. 검색기를 다시 꺼낸다

17강에서 만든 것을 그대로 쓴다. 한 군데만 다르다.

21_rag_search.py
import numpy as np
from sentence_transformers import SentenceTransformer

data = np.load("index.npz", allow_pickle=True)
vecs, slugs, texts = data["vecs"], data["slugs"], data["texts"]

model = SentenceTransformer("intfloat/multilingual-e5-small")


def search(question, k=3):
    """질문과 가까운 조각 k 개를 찾는다."""
    qv = model.encode([question], normalize_embeddings=True)[0]
    scores = vecs @ qv

    # 17강과 다른 곳은 여기 한 줄이다. 글 단위로 묶지 않고
    # 점수 높은 순서대로 조각을 그냥 k 개 집는다
    out = []
    for i in scores.argsort()[::-1][:k]:
        out.append((float(scores[i]), str(slugs[i]), str(texts[i])))
    return out


for score, slug, text in search("조각을 겹치게 자르는 이유가 뭔가"):
    print(f"{score:.3f}  {slug}")
    print(f"        {text[:40]}")
터미널
  • 0.903 ai-eng-04-overlap
  • ## 정리하면\n\n글자 수로만 자르면 경계에 걸친 표현이 사라진다.
  • 0.900 ai-eng-04-overlap
  • 조각이 많아지면 뒤에서 임베딩을 그만큼 더 계산해야 하고,
  • 0.896 ai-eng-04-overlap
  • 겹치면 무슨 일이 생기나\n\n1. 잘린 대목「사용자 눈에 / 보이지 않는」이

17강에서는 한 글에서 제일 잘 맞은 조각 하나만 남겼다. 「어느 글이 답인가」를 물었고, 같은 글이 다섯 번 나오면 목록이 망가지기 때문이다.

오늘은 목적이 다르다. 모델에 먹일 근거를 찾는 것이라 글 전체는 너무 길어 못 넣고, 같은 글의 조각 셋이면 오히려 좋다. 그래서 묶는 코드를 뺐다 — 목적이 바뀌면 잘 만든 코드가 군더더기가 된다.

3. 조각을 질문에 붙인다

여기가 오늘 새로 배우는 유일한 부분인데, f-string 하나가 전부다.

22_build_prompt.py
# (21_rag_search.py 의 위쪽을 그대로 복사해 온다 — data, model, search)

question = "조각을 겹치게 자르는 이유가 뭔가"
hits = search(question, k=3)

# 조각 셋을 빈 줄 하나로 이어 붙인다
context = "\n\n".join(text for score, slug, text in hits)

prompt = f"""아래 자료를 읽고 질문에 답해라.

[자료]
{context}

[질문]
{question}
"""

print(f"자료 {len(context)}자 / 프롬프트 전체 {len(prompt)}자")
print("-" * 40)
print(prompt)
터미널 — 잘라서 보여준다
  • 자료 468자 / 프롬프트 전체 519자
  • ----------------------------------------
  • 아래 자료를 읽고 질문에 답해라.
  • [자료]
  • ## 정리하면
  • 글자 수로만 자르면 경계에 걸친 표현이 사라진다. 건너뛰는 폭을 조각
  • 크기보다 좁게 하면 조각끼리 겹치고, 찢겼던 대목이 어느 한 조각에는
  • 온전히 들어간다. ... (조각 셋이 이어진다)
  • [질문]
  • 조각을 겹치게 자르는 이유가 뭔가
이 프롬프트에서 눈여겨볼 세 가지
부분왜 그렇게 썼나
"""...""" 세 겹 따옴표여러 줄 글자를 그대로 쓴다. 줄바꿈을 \n 으로 안 적어도 된다
[자료] · [질문] 표시어디까지가 자료고 어디부터가 질문인지 모델에 알려준다
"\n\n".join(...) 조각 사이를 빈 줄로 띄운다. 붙여 놓으면 한 덩어리로 읽는다
대괄호 표시는 규칙이 아니다. 사람이 봐도 구분되게 적으면 모델도 대개 구분한다.

f-string 안의 중괄호는 그 자리에 변수 값을 넣으라는 표시다. 그래서 중괄호를 글자 그대로 쓰려면 두 번 적어야 하는데, 내일 JSON 을 다룰 때 이게 바로 사고가 된다.

4. 모델에 넣는다 — 여기서 완성된다

앞 편에서 쓴 호출을 그대로 쓴다. content 에 들어가는 글자만 바뀐다.

23_first_rag.py
# (21 · 22 의 위쪽을 그대로 복사해 온다 — search, question, hits, context, prompt)
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

res = client.chat.completions.create(
    model="qwen2.5:3b",
    messages=[{"role": "user", "content": prompt}],   # 질문 대신 프롬프트 전체
)

print(res.choices[0].message.content)

먼저 근거를 안 준 쪽부터 본다.

터미널 — 근거 없이 질문만
  • "조각을 겹치게 자르는 것"이라는 표현은 구체적인 상황이나 제품이 무엇인지
  • 잘 모르겠습니다. 하지만 일반적으로 이 용어를 사용할 수 있는 몇 가지
  • 상황들을 설명해 드리겠습니다:
  • 1. 공예와 조형 예술: 조각 작업에서 특정 모양을 만들 때, 여러 부분으로
  • 자르는 것을 통해 원하는 형태를 얻을 수 있습니다.
  • 2. 정밀공학 제품 제조: 일부 기계 공학 및 정밀가공 시스템에서, 특정
  • 크기 또는 형상의 부품을 만드는데 사용됩니다.

이제 근거를 넣은 쪽이다.

터미널 — 조각 3개를 같이 넣고
  • 조각을 겹치게 자르는 이유는 주로 저장과 계산 효율성을 개선하려는
  • 목적입니다. 조각이 많아지면, 각 조각에 대해 저장 및 관리하는 작업들이
  • 늘어나며, 이러한 과정은 비효율적일 수 있습니다.
  • 하지만 겹침은 겹쳤을 때의 상실된 내용들을 보완하는 역할도 하므로,
  • 과도하게 겹치는 것은 오히려 좋지 않은 값을 가져올 수 있습니다.

근거가 없을 때는 공예 · 정밀가공 같은 우리 수업에 없는 단어가 나왔고, 모른다고는 하지 않았다. 근거를 넣으니 저장 · 계산 · 겹침 · 보완 — 9강에 실제로 있는 말들이 나온다. 모델은 안 바뀌었고 프롬프트 한 덩어리가 바뀌었을 뿐이다.

여기까지가 RAG 다. 검색해서, 붙여서, 물어봤다.

5. 🔴 그런데 답을 자세히 읽어보자

축하는 여기까지다. 위 답의 첫 문장을 다시 읽는다.

자료에 적힌 것은 「겹치면 조각 수가 늘어나고, 그래서 저장과 계산이 늘어난다」였다. 겹침은 그 비용을 치르고 사는 안전장치다. 그런데 모델은 겹치는 이유를 「저장과 계산 효율성을 개선하려는 목적」이라고 했다. 정반대로 말했다. 그런데 문장은 매끄럽다.

자료를 안 읽은 게 아니다. 자료의 단어를 쓰면서 뜻을 뒤집었다.

근거를 안 줬을 때는 틀린 게 티가 났다 — 공예 이야기가 나오면 누구나 안다. 근거를 주고 나면 우리 문서의 단어를 쓰고 있어서 읽는 사람은 맞는 줄 안다. RAG 는 문제를 없앤 게 아니라 어렵게 만들었다.

6. 함수 하나로 묶는다

오늘 만든 것을 ask() 하나로 접어둔다. 오늘 이후 계속 이 함수를 고쳐 쓴다.

24_ask.py
import numpy as np
from sentence_transformers import SentenceTransformer
from openai import OpenAI

data = np.load("index.npz", allow_pickle=True)
vecs, slugs, texts = data["vecs"], data["slugs"], data["texts"]

model = SentenceTransformer("intfloat/multilingual-e5-small")
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")


def search(question, k=3):
    qv = model.encode([question], normalize_embeddings=True)[0]
    scores = vecs @ qv
    return [
        (float(scores[i]), str(slugs[i]), str(texts[i]))
        for i in scores.argsort()[::-1][:k]
    ]


def ask(question, k=3):
    """검색 -> 조립 -> 생성. 답과 근거를 같이 돌려준다."""
    hits = search(question, k)
    context = "\n\n".join(text for score, slug, text in hits)

    prompt = f"""아래 자료를 읽고 질문에 답해라.

[자료]
{context}

[질문]
{question}
"""
    res = client.chat.completions.create(
        model="qwen2.5:3b",
        messages=[{"role": "user", "content": prompt}],
    )
    # 답만 주면 어디서 왔는지 알 수 없다. 근거 글 이름도 같이 돌려준다
    return res.choices[0].message.content, [slug for score, slug, text in hits]


for q in ["브랜치는 왜 따로 만드나", "엑셀 파일을 파이썬으로 어떻게 여나"]:
    answer, sources = ask(q)
    print(f"\n[질문] {q}")
    print(f"[근거] {sources}")
    print(f"[답] {answer[:80]}")
터미널
  • [질문] 브랜치는 왜 따로 만드나
  • [근거] ['git-21-branch', 'git-21-branch', 'git-21-branch']
  • [답] 브랜치를 따로 만들 때, 원본(기존의) 코드에는 영향이 없다는 것을 강조한
  • [질문] 엑셀 파일을 파이썬으로 어떻게 여나
  • [근거] ['ai-eng-07-excel-report', 'ai-eng-07-excel-report', 'ai-eng-07-excel-report']
  • [답] 엑셀 파일을 파이썬으로 열고 사용하려면, 먼저 설치된 openpyxl 라이브러리를

returnsources 를 붙인 이유가 있다. 답이 이상할 때 모델이 이상한 건지 검색이 이상한 건지를 여기서 가른다. 근거가 엉뚱한 글이면 검색 문제이고, 근거는 맞는데 답이 틀리면 모델 문제다 — 고칠 자리가 완전히 다르다.

7. 근거를 넣으면 느려진다

앞 편에서 예고한 것이 여기서 숫자로 나온다. 자료를 붙이면 입력이 45토큰에서 397토큰이 된다.

같은 질문 · 근거 유무 — qwen2.5:3b 실측
입력 토큰CPU 프리필CPU 총GPU 총
근거 없이450.4초9.4초4.5초
조각 3개3973.8초13.2초4.6초
출력은 150토큰으로 고정했다. 총 시간에는 모델을 메모리에 올리는 3초쯤이 섞여 있다.

GPU 쪽은 4.5초에서 4.6초라 티도 안 난다. CPU 쪽은 9.4초에서 13.2초가 되고, 늘어난 것은 답을 만드는 시간이 아니라 입력을 읽는 시간이다. 학원 노트북은 CPU 쪽이라 k 를 아무 생각 없이 10 으로 올리면 수업이 멈춘다. k 를 몇으로 둘지는 오늘 마지막 편에서 재서 정한다.

이번 편에 나온 것

정리
쓴 것하는 일
RAG검색(Retrieval) · 보태서(Augmented) · 생성(Generation)
scores.argsort()[::-1][:k]점수 높은 조각 k 개. 글 단위로 안 묶는다
"\n\n".join(...)조각 사이를 빈 줄로 띄워 이어 붙인다
f"""...{context}..."""여러 줄 프롬프트 안에 값을 끼운다
[자료] · [질문]어디까지가 근거인지 모델에 알려주는 표시
print(prompt)모델에 실제로 뭐가 갔는지 눈으로 본다
ask(question)셋을 묶은 함수. 답과 근거를 같이 돌려준다
return answer, sources검색 문제인지 모델 문제인지 가르는 근거

다음 편에서 이 틀림을 종류별로 잡아본다.

Share
  • 파이썬
  • AI
  • LLM
  • RAG
  • Ollama