진행중인 실습의 전체적인 파이프라인을 리뷰해보자.
- milvus 서버와 연결 → 클라이언트 생성
.env
에 OPENAI_API key 설정 → 클라이언트 생성
get_embeddings
함수: OPENAItext-embedding-3-small
모델 API을 활용
→ 벡터 하나는 float 숫자 수천 개 (예: 1526차원)
→ index
: 요청한 text 중 몇 번째 텍스트의 임베딩인가
→ object='embedding’
: 해당 객체가 임베딩 데이터임을 표시
→ model
: 어떤 임베딩 모델을 사용했는가
→ object=’list’
: API 응답 전체의 타입 (리스트 형태로 여러 임베딩 데이터 감쌈)
→ usage
: 입력된 텍스트 토큰 수 (임베딩은 prompt_tokens = total_tokens)
search_milvus
함수: milvus 컬렉션에서 주어진 벡터와 유사도가 높은 청크 검색
query_vec
과 비슷한 벡터 탐색text
와 filenm
메타데이터로 가져오기nprobe
nlist
results[0]
안에서 hit["entity"]["text"]
를 꺼내 context로 묶어 LLM에 넘김
generate_answer_stream
함수: gpt 4.1을 사용하여 답변을 생성 및 반환
stream=true
)👀 OpenAI Response: <openai.Stream object at 0x71bad4322910>
# response 객체의 타입: # 스트리밍 응답 타입
for chunk in response:
print(chunk)
- streamlit 구성
sorted()
로 리스트 정렬get_embeddings
search_milvus
generate_answer_stream
🍙 Streaming Response Chunk:
ChatCompletionChunk(id='chatcmpl-ByW...',
choices=[Choice(delta=ChoiceDelta(
content=' 자료', function_call=None, refusal=None, role=None,
tool_calls=None), finish_reason=None, index=0, logprobs=None)],
created=175..., model='gpt-4.1-mini-2025-04-14',
object='chat.completion.chunk', service_tier='default',
system_fingerprint='fp_6f...', usage=None)
사용자(User)
│
▼
[ Streamlit UI ]
├─ 질문 입력 (text_area)
├─ ASK 버튼 클릭
│
▼
[ 1. 질문 임베딩 (get_embeddings) ]
├─ OpenAI Embedding API 호출
├─ 질문 → 1536차원(text-embedding-3-small) 벡터 변환
│
▼
[ 2. Milvus 벡터 검색 (search_milvus) ]
├─ 질문 벡터와 가장 가까운(top-k) 벡터 탐색
├─ metric_type = COSINE (코사인 유사도)
├─ nprobe = 10 (탐색할 클러스터 수)
└─ 검색 결과: [문맥 청크 + 메타데이터(text, filenm)]
│
▼
[ 3. 문맥(Context) 준비 ]
├─ 검색된 여러 문장(청크) → 하나의 문자열로 합침
└─ 예: "청크1\n청크2\n청크3..."
│
▼
[ 4. LLM 답변 생성 (generate_answer_stream) ]
├─ GPT-4.1-mini 호출 (스트리밍 모드)
├─ (Prompt) 질문 + 문맥 전달
├─ OpenAI Stream 객체 생성
├─ for chunk in response_stream:
│ ├─ chunk.choices[0].delta.content 확인
│ ├─ 토큰 단위로 partial_answer 이어붙임
│ ├─ 진행률(progress bar) 업데이트
│ └─ 실시간 화면 표시(answer_box)
│
▼
[ 5. 답변 완료 처리 ]
├─ Progress Bar 100% 완료 표시
├─ 최종 답변 화면에 출력
└─ 처리 시간(⏱) 표시
│
▼
[ 6. + 후처리/추가기능 ]
├─ "♻️ 다시 질문하기" 버튼 → 같은 질문·문맥으로 답변 재생성
├─ 에러 처리 (질문 없을 때, Milvus 연결 오류 등)
└─ 사이드바 옵션 (컬렉션 선택, 검색할 chunk 수 설정)
└─ 올바른 청크인지 검증 (유사도 0.5 이상만 출력 및 답변에 사용)
⭐질문과 문서는 같은 임베딩 모델로 벡터화되어야 정확한 유사도 계산이 가능하다.
서로 다른 모델을 쓰면 벡터 공간 자체가 달라져서 유사도 점수가 왜곡된다.
"벡터 DB에서 유사한 문맥을 검색한다"는 무슨 의미일까?
1️⃣ 질문(Question)을 임베딩(Embedding) → 벡터(Vector)로 변환
[0.013, -0.054, 0.221, ...]
2️⃣ 문서(Document)도 같은 방식으로 임베딩 → 벡터로 변환
3️⃣ 검색(Search)
4️⃣ 벡터 간 유사도 계산
➡ 질문 벡터와 거리/유사도가 가장 가까운 벡터를 top-k만큼 가져옴
5️⃣ 문맥(Context) 추출
[0.02, -0.03, ...]
→ “RAG 시스템이란 …”[0.05, -0.01, ...]
→ “Milvus는 …”➡ 이렇게 가져온 텍스트들이 LLM에 들어가는 문맥(context)
✅ 즉, 질문과 가장 “벡터 공간에서 가까운” 문서를 찾는 게 벡터 검색.
- 📌 거리가 가까울수록 → 의미상으로 더 비슷한 문장이라는 뜻.
- 👉 “질문 임베딩 벡터와 가장 ‘거리(또는 각도)’가 가까운 문서 벡터를 Milvus가 찾아주고, 그 문서를 문맥으로 활용하는 것”
본 파이프라인에서 임베딩 모델로 사용한 text-embedding-3-small은 1536차원이라고 한다.
🔍 1️⃣ 임베딩 모델 = 텍스트를 “고정 길이 벡터”로 바꿔주는 모델
text-embedding-3-small
모델을 쓰면 → 1536차원 벡터로 변환.👉 예:
"퀀텀 얼라이언스 체계 구축 목적은?"
→ [-0.033, 0.004, 0.021, ..., 0.002] # 길이가 1536인 벡터
🔍 2️⃣ 임베딩 모델마다 차원이 다르다
text-embedding-3-small
→ 1536차원text-embedding-3-large
→ 3072차원👉 즉, 모델을 바꾸면 같은 질문이라도 결과 벡터 길이(차원)가 달라짐.
🔍 3️⃣ 왜 이렇게 차원이 다를까?
➡ 그래서 작은 모델(1536차원) 은 빠르고, 큰 모델(3072차원) 은 더 정밀하지만 비용이 높음.
✅ 정리
- 질문을 1536차원 벡터로 변환한다는 말이 맞다.
- 모델을 바꾸면 → 임베딩 벡터의 차원도 달라진다.
- Milvus(같은 벡터DB)는 컬렉션 생성 시 차원을 고정하기 때문에,
text-embedding-3-small
(1536차원) → 컬렉션도 1536차원으로 생성- 만약 모델을
text-embedding-3-large
(3072차원)로 바꾸면 → 새 컬렉션을 만들어야 함.
👉 즉, “임베딩 모델마다 벡터의 차원이 다르고, 그 모델이 정한 차원으로 질문을 변환한다.”
stream=true
옵션을 사용하여 답변을 생성하고 각 청크를 반환하고 있다.
이를 자세히 살펴보자.
OpenAI API의 스트리밍 응답 한 조각(=
chunk
)를 살펴보자
stream=True
로 호출했을 때 모델이 답변을 토큰 단위로 쪼개서 보내는데, 그 각 조각(chunk)ChatCompletionChunk(
id='chatcmpl-ByT...',
choices=[Choice(delta=ChoiceDelta(content=' 추진', function_call=None, refusal=None, role=None, tool_calls=None), finish_reason=None, index=0, logprobs=None)],
created=175...,
model='gpt-4.1-mini-2025-04-14',
object='chat.completion.chunk',
service_tier='default',
system_fingerprint='fp_6f...',
usage=None
)
finish_reason='stop'
이 들어오면 → “답변 다 끝났다” 라는 신호1️⃣ id
'chatcmpl-ByT...'
id
를 공유)2️⃣ choices
choices=[
Choice(
delta=ChoiceDelta(content=' 추진', function_call=None, refusal=None, role=None, tool_calls=None),
finish_reason=None,
index=0,
logprobs=None
)
]
delta
→ 이 chunk에서 새로 들어온 답변 조각(token) 을 담고 있음.content=' 추진'
→ 이번 chunk에서 모델이 생성한 한 토큰 (예: " 추진")function_call=None
, refusal=None
, role=None
, tool_calls=None
→ 기능 호출(Function call)이나 거부(refusal) 같은 특별한 응답이 없다는 뜻.None
이면 아직 답변이 안 끝났다는 뜻."stop"
이 들어옴 → 스트리밍 종료 신호.3️⃣ created
175...
4️⃣ model
'gpt-4.1-mini-2025-04-14'
5️⃣ object
'chat.completion.chunk'
chat.completion
이라고만 옴.6️⃣ service_tier
'default'
7️⃣ system_fingerprint
'fp_6f...'
8️⃣ usage
None
📌 정리
- chunk = 스트리밍 응답의 한 조각
- 가장 중요한 건
choices[0].delta.content
→ 실제로 모델이 새로 생성한 텍스트 토큰- finish_reason 가
None
→ 아직 계속되는 중,"stop"
→ 스트리밍 종료- 다른 정보(id, created, model 등)는 메타데이터
👉 즉, for chunk in response_stream:
chunk.choices[0].delta.content
를 이어붙이면 → 전체 답변 완성.