| **RAG** | 질문할 때마다 문서에서 **관련 부분만 찾아** 함께 줌 | 문서가 바뀌면 다시 색인만, **출처**가 남음 | 찾는 것이 틀리면 답도 틀림 | **오늘** |
**RAG**(Retrieval-Augmented Generation, 검색 증강 생성)는 질문할 때마다 규정에서 **관련 부분만 찾아** LLM에게 같이 주고, 그 부분으로만 답하게 합니다. 이름 그대로 **검색(Retrieval)이 생성(Generation)을 돕습니다.**
- 색인은 **가끔**(문서가 바뀔 때), 질문은 **매번**. 두 흐름은 다른 워크플로우입니다 (오늘 `my-index`, `my-search`).
- 오늘 1단계는 **찾기까지**만 합니다. 답을 쓰는 것은 2단계입니다. 찾는 것이 틀리면 아무리 좋은 LLM도 맞는 답을 쓸 수 없기 때문에, 찾기를 따로 재고 확인합니다.
### 1.3 67일차 FAQ 검색과 무엇이 다른가
| | 67일차 FAQ 검색 | 오늘 RAG |
| --- | --- | --- |
| 찾는 법 | 낱말이 겹치나 (`keywords`) | **뜻**이 가까운가 (임베딩) |
| "암호를 바꿔야 해?" | "암호"가 키워드에 없으면 못 찾음 | "비밀번호" 섹션을 찾음 (실측 아래 2절) |
| 찾는 단위 | FAQ 한 개 | 문서의 한 섹션 (청크) |
| 준비 | 사람이 FAQ마다 키워드를 적음 | 문서를 그대로 색인 |
| 틀리는 방식 | 못 찾음 (0개) | **언제나 무언가를 찾음** (엉뚱한 것도) |
마지막 줄이 오늘 하루의 숙제입니다. 낱말 검색은 못 찾으면 "없음"이라고 하지만, 뜻 검색은 점심 메뉴를 물어도 가장 가까운 규정 3개를 돌려줍니다. 2단계의 관련도 기준과 답 검사가 이것 때문에 필요합니다.
## 2. 임베딩과 "가깝다"는 것
### 2.1 임베딩은 글을 숫자 목록으로
**임베딩**은 글 한 덩어리를 숫자 목록(벡터) 하나로 바꾸는 것입니다. 오늘 쓰는 `text-embedding-3-large`는 글 하나를 **3,072차원** 벡터(숫자 3,072개짜리 목록)로 바꿉니다 (모델 기본값). 뜻이 비슷한 글은 비슷한 숫자 목록이 되도록 학습된 모델입니다.
```mermaid
flowchart LR
A["암호를 얼마나 자주<br/>바꿔야 해?"] --> EA["[0.01, -0.03, …]<br/>3,072차원"]
B["## 2. 변경 주기<br/>비밀번호는 90일마다…"] --> EB["[0.02, -0.02, …]<br/>3,072차원"]
EA --> S{{"얼마나 같은 방향?<br/>(코사인 유사도)"}}
EB --> S
S --> R["0.576"]
```
- 숫자 하나하나에 뜻이 있는 것은 아닙니다. **두 목록이 얼마나 같은 방향을 가리키는지**(코사인 유사도)만 씁니다. 그림의 숫자 목록은 모양을 보여 주는 예이고, 0.576은 실제로 잰 점수입니다.
### 2.2 왜 3,072차원인가
3,072차원은 벡터가 숫자 몇 개로 되어 있는지(**차원 수**)입니다. 글이 섹션 하나든 질문 한 줄이든, 이 모델은 늘 3,072차원 벡터 하나를 돌려줍니다.
| 물음 | 답 |
| --- | --- |
| 왜 하필 3,072차원? | 데이터에서 나온 값이 아니라 **모델을 만들 때 정한 크기**입니다. 같은 회사의 작은 모델 `text-embedding-3-small`은 1,536차원. 많을수록 뜻의 미묘한 차이를 더 담을 수 있지만, 저장 공간과 계산이 늘어납니다 |
| 숫자 하나는 무슨 뜻? | 3,072차원 공간의 **좌표**입니다. 지도에서 (위도, 경도) 두 숫자로 위치를 나타내듯, 글 하나를 3,072차원 공간의 한 점으로 나타냅니다. 다만 각 축에 사람이 읽을 뜻("3번째 = 보안 정도")은 없습니다. 모델이 학습하며 스스로 정한 축입니다 |
| 질문과 청크를 다른 모델로? | 안 됩니다. 차원 수가 다르면 비교가 안 되고, 같아도 모델이 다르면 공간이 달라 점수가 의미 없습니다. **모델을 바꾸면 전부 다시 색인**하고 관련도 기준값도 다시 잽니다 |
| 저장 크기는? | 숫자 하나를 보통 4바이트로 저장하면 청크 하나에 약 12KB. 오늘 섹션 청크 25개면 약 300KB로 작지만, 수십만 청크면 GB 단위가 됩니다. 7교시 `tdoc`이 SQLite에 저장하는 것이 이 숫자 목록입니다 |
| 줄일 수 있나? | text-embedding-3 모델은 요청할 때 `dimensions`(예: 256, 1024)로 더 짧은 벡터를 받을 수 있습니다. 가볍고 빠르지만 품질이 조금 떨어질 수 있습니다 (오늘은 쓰지 않았고 재지도 않음) |
- n8n의 메모리 저장소는 이 점수를 0~1로 돌려줍니다. 클수록 가깝습니다.
### 2.3 실제로 잰 점수 읽기 (섹션 청크 25개, 2026-10-22)
| 질문 | 1등 | 2등 | 3등 |
| --- | --- | --- | --- |
| 비밀번호는 몇 자 이상이어야 하나요? | 비밀번호 · 1. 만들기 **0.748** | 비밀번호 · 2. 변경 주기 0.54 | 비밀번호 · 3. 잠금 0.484 |
| 암호를 얼마나 자주 바꿔야 해? | 비밀번호 · 2. 변경 주기 **0.576** | 비밀번호 · 1. 만들기 0.512 | 비밀번호 · 4. 다단계 인증 0.456 |
| 첨부 파일이 너무 커서 메일이 안 보내져요 | 메일 · 2. 첨부 파일 **0.616** | 메일 · 1. 메일함 0.455 | 보안 사고 · 1. 피싱 메일 0.335 |
| 컴퓨터가 바이러스에 걸린 것 같아요 | 보안 사고 · 2. 악성코드 **0.478** | 보안 사고 · 1. 피싱 0.357 | 원격근무 · 3. 재택 보안 0.249 |
`top_k: 5`로 같은 바이러스 질문을 물으면 4등 0.246(비밀번호 · 잠금), 5등 0.241(소프트웨어 · 설치)이 더 나옵니다.
읽는 법:
1.**"암호"는 규정에 없는 낱말**입니다 (규정은 "비밀번호"). 그래도 1등이 "2. 변경 주기"입니다 → 뜻으로 찾았습니다.
2. 점수는 **확률이나 퍼센트가 아닙니다.** 0.478은 "47.8% 맞음"이 아니라, 같은 질문의 다른 청크보다 가깝다는 **순서**의 근거입니다. 질문마다 1등 점수가 0.478~0.748로 다릅니다.
3. 1등과 2등의 **차이**도 봅니다. 바이러스 질문은 1등 0.478, 2등 0.357 → 1등이 뚜렷합니다. 3등 이하는 0.25 근처로 모두 비슷합니다 → "관련 없는 것들의 점수대"입니다.
4. 점심 메뉴처럼 규정과 상관없는 질문의 1등은 0.202였습니다. 이 "관련 없는 점수대"를 2단계에서 기준값으로 씁니다.
### 2.4 같은 질문인데 점수가 조금 다르다
같은 질문 "비밀번호를 잊어버리면 어디서 바꿔요?"의 1등 점수가 한 실행에서는 0.551, 다른 실행에서는 0.55였습니다 ("보안 교육" 질문도 0.352와 0.353). 임베딩도 셋째 자리에서 조금씩 흔들립니다. 그래서 기준값을 어떤 질문의 점수 바로 옆(예: "보안 교육"이 0.352~0.353이니 0.353)에 두지 않습니다. 그러면 같은 질문이 실행마다 통과했다 걸렸다 합니다. 두 무리 사이 **한가운데**에 여유 있게 둡니다 (오늘 0.33: 규정 밖 최고 0.261, 규정 질문 최저 0.464, 2단계 3절).
## 3. 정답표가 먼저
### 3.1 검색을 만들기 전에 사람이 정답을 정한다
| 질문 | 답이 있는 문서 | 섹션 | 답 문구 |
| --- | --- | --- | --- |
| 비밀번호는 몇 자 이상이어야 하나요? | `password-policy` | 1. 비밀번호 만들기 | 12자 |
| 암호를 얼마나 자주 바꿔야 해? | `password-policy` | 2. 변경 주기 | 90일 |
- 답이 **없는** 질문도 정답표에 넣습니다. 검색은 언제나 무언가를 돌려주기 때문입니다 (1.3절 마지막 줄).
- 정답표 없이 검색 결과를 보면, 나온 것이 그럴듯해 보여서 "잘 되네요"가 됩니다. **결과를 보기 전에** 써야 합니다.
### 3.2 질문을 고르는 법: 네 종류를 섞는다
| 종류 | 예 | 무엇을 시험하나 |
| --- | --- | --- |
| 규정 낱말 그대로 | 비밀번호는 몇 자 이상? | 기본이 되는가 |
| 직원 말투 · 다른 낱말 | 암호를 얼마나 자주 바꿔야 해? / 회사를 그만두면 | 뜻으로 찾는가 |
| 규정에 답이 없음 | 연차 휴가는 며칠? / 노트북 사양은? | 모를 때 넘기는가 (2단계) |
| 틀린 전제 | 비밀번호는 30일마다 바꾸면 되죠? | 질문이 아니라 규정을 따르는가 (2단계) |
오늘 Lab의 24문항(`cases/retrieval-*.json`)은 규정 8개에서 2~4개씩, 낱말 그대로와 다른 낱말을 섞어 만들었습니다.
### 3.3 정답표도 틀린다
오후 **Lab 03 과제**에서 쓰는 "어려운 질문" 8개를 수업 준비 때 강사가 처음 만들 때, "윈도우 업데이트 안 하고 미루면 안 돼요?"의 기대 문서를 `security-incident`로 적었습니다. 테스트는 실패했습니다 (1등 `software-usage` 0.309, 다시 돌려 0.324). 규정을 다시 열어 보니 윈도우 업데이트 이야기는 **어디에도 없었습니다.** 검색이 틀린 것이 아니라 **정답표가 틀렸습니다.**
그 뒤에 한 일:
| 순서 | 한 일 | 결과 |
| --- | --- | --- |
| ① | 이 질문이 든 어려운 질문 8개를 두 번 돌림 | 두 번 다 **5/8** (보고서 [첫 번째](../../Last3Mon/71/answers/prep-20261018/71-lab03-challenge-cases-20261018-075719.json), [두 번째](../../Last3Mon/71/answers/prep-20261018/71-lab03-challenge-cases-20261018-075742.json)) |
| ② | 규정을 열어 보니 이 질문의 답이 없음 → **정답표가 틀린 것** | — |
| ③ | 이 질문을 "모르는 프로그램이 자꾸 저절로 떠요"로 바꿈 | **6/8** → 지금 Lab 03 과제에서 쓰는 케이스 파일 |
- 보고서 두 개는 수업이 끝난 뒤 공개되는 `Last3Mon/71/answers/prep-20261018/`에 있습니다. 수업 중에는 강사 화면에서 엽니다.
- 정답표는 기억이 아니라 **원문을 열어 확인**하고 씁니다 (문서 이름, 섹션, 답 문구를 복사).
- 검색이 "틀렸다"고 나오면, 고치기 전에 정답표부터 다시 봅니다.
## 4. 청크 · 메타데이터
### 4.1 오늘의 문서: 규정 8개, 섹션 25개
| `name` (데이터 API) | 제목 | 섹션 |
| --- | --- | --- |
| `password-policy` | 비밀번호·계정 보안 규정 | 4 (만들기 · 변경 주기 · 잠금 · 다단계 인증) |
| `device-issue-return` | 업무용 장비 지급·반납 규정 | 4 (지급과 교체 · 분실과 도난 · 반납 · 개인 장비) |
| `"title": "비밀번호·계정 보안 규정"` | `"title": "비밀번호·계정 보안 규정"` |
| (`section` 칸 **없음**) | `"section": "1. 비밀번호 만들기"` |
| `"text": "비밀번호·계정 보안 규정\n## 1. 비밀번호 만들기\n- 비밀번호는 12자 이상으로 …"` | (같음) |
왼쪽을 보면 섹션 제목 "1. 비밀번호 만들기"가 **`text` 안에는 글자로 들어 있습니다.** 사람이 읽으면 어느 섹션인지 압니다. 하지만 `section`이라는 **칸**은 없습니다.
**그래서 테스트가 실패합니다.** 과제 테스트(`cases/lab01-challenge.json`)는 `hits.0.section` 칸을 읽어 "비밀번호 만들기"가 있는지 봅니다. 칸이 없으니 실패입니다.
```text
✘ 섹션 · 비밀번호 길이 · 0.4s · hits.0.section has none of ['비밀번호 만들기']
```
과제 테스트 4개 중 섹션을 보는 2개가 이렇게 실패하고, 개수(`top_k`)를 보는 2개는 통과해 **2/4**입니다 (10/18, 10/22 두 번 다).
**배울 것.** 2단계에서 답에 출처를 붙이는 Code, 3단계에서 채점하는 테스트는 모두 프로그램입니다. 프로그램은 글을 읽고 짐작하지 않고 **정해진 칸**만 읽습니다. 출처로 쓸 것은 처음부터 칸으로 만들어야 합니다.
**어떻게 고치나.** 섹션 제목은 **색인할 때** 청크를 자르면서 알 수 있으므로 그때 메타데이터(`section`)로 함께 저장합니다. 검색은 저장된 메타데이터를 꺼내 `section` 칸에 넣기만 하면 됩니다. 색인 · 검색 두 워크플로우를 모두 고치고, 다시 색인한 뒤 테스트하면 4/4입니다.
### 4.5 청크 크기는 어떻게 정하나 (3단계 예고)
| 너무 작으면 | 너무 크면 |
| --- | --- |
| 규칙 하나가 둘로 잘려 답 문구가 다음 청크로 넘어감 | 여러 주제가 섞여 점수가 흐려짐, LLM에 주는 글이 길어짐 |
| "변경 주기"만 있고 무엇의 주기인지 없음 | 1등 청크에 답이 있어도 어디인지 찾기 어려움 |
정답은 "몇 자"가 아니라 **내 문서의 모양**입니다. 섹션이 있는 문서는 섹션부터 시작하고, 3단계에서 숫자로 확인합니다.
## 5. 메모리 저장소
오늘은 n8n 안의 **메모리** 벡터 저장소를 씁니다. 설치가 필요 없지만 주의할 점이 있습니다.
그래서 push로 저장소가 비어 있어도 `test`는 먼저 색인하고 시작하며, 출력 첫 줄에 `✔ before: POST /webhook/my-index · HTTP 200 · … {"indexed_chunks": 25, …}`가 찍힙니다. `--no-before`를 붙이면 이 호출을 건너뜁니다 (3단계에서 내가 다른 설정으로 색인해 둔 것을 그대로 잴 때).
잰 시간: push 직후 첫 색인 3.8초(2026-10-22), 그 뒤 `before` 칸으로 한 다시 색인 0.4~2.2초, 검색 한 번 0.2~0.9초 (수업 준비 때 색인 1.2초, 검색 0.2~0.4초).
### 5.1 운영에서는
| | 메모리 (오늘) | 운영용 벡터 DB (Qdrant, PGVector 등) |
| --- | --- | --- |
| 설치 | 없음 | 서버 · 컨테이너 |
| 재시작 | 지워짐 | 남음 |
| 다시 색인 | 매번 전부 | 바뀐 문서만 |
| 여러 워크플로우 · 여러 서버 | 같은 n8n 안에서만 | 어디서나 |
오늘 배운 흐름(청크 → 임베딩 → 저장 → 검색)은 그대로이고, 저장소 노드만 바뀝니다. 7교시 `tdoc`은 SQLite 파일에 저장해 "껐다 켜도 남게" 직접 만듭니다.