RAG 파이프라인 세부 탐구4

wldbs._.·2025년 8월 4일
0

AI-LLM

목록 보기
12/21
post-thumbnail

진행중인 실습의 전체적인 파이프라인을 리뷰해보자.


🧪 Streamlit - RAG

🥝 파이프라인

  1. milvus 서버와 연결 → 클라이언트 생성
  1. .envOPENAI_API key 설정 → 클라이언트 생성
  1. get_embeddings 함수: OPENAI text-embedding-3-small 모델 API을 활용
  • 3-1. 사용자가 입력한 질문을 벡터로 변환 (OpenAI API 호출)
  • 3-2. 디버깅
    - 생성된 임베딩 벡터의 개수: "내가 요청한 텍스트 개수와 API가 돌려준 임베딩 개수가 일치하는가"
    - API가 반환한 임베딩 객체: "OPENAI가 어떤 객체를 돌려주었는가"

→ 벡터 하나는 float 숫자 수천 개 (예: 1526차원)

index: 요청한 text 중 몇 번째 텍스트의 임베딩인가

object='embedding’: 해당 객체가 임베딩 데이터임을 표시

model: 어떤 임베딩 모델을 사용했는가

object=’list’: API 응답 전체의 타입 (리스트 형태로 여러 임베딩 데이터 감쌈)

usage: 입력된 텍스트 토큰 수 (임베딩은 prompt_tokens = total_tokens)

  • 실제 임베딩 벡터 데이터(리스트): 각 텍스트마다 어떤 벡터가 들어있는가

  1. search_milvus 함수: milvus 컬렉션에서 주어진 벡터와 유사도가 높은 청크 검색
  • 벡터 기반으로 Milvus에서 유사 문맥 검색

  • 4-1. query_vec과 비슷한 벡터 탐색
  • 4-2. textfilenm 메타데이터로 가져오기
  • 4-3. 기타 설정
    • 1) 코사인 유사도
    • 2) 검색 시 몇개의 클러스터를 탐색할지 지정하는 nprobe
    • 3) 인덱스를 만들 때 몇 개의 클러스터로 쪼갤지 지정하는 nlist
  • 4-4. results[0] 안에서 hit["entity"]["text"] 를 꺼내 context로 묶어 LLM에 넘김
  1. generate_answer_stream 함수: gpt 4.1을 사용하여 답변을 생성 및 반환
  • 5-1. 주어진 질문과 문맥을 기반으로 답변 생성 - 스트리밍 방식 (stream=true)
    • 질문: 사용자가 입력한 텍스트
    • 문맥: Milvus에서 검색한 관련 문맥
  • 5-2. Stream 객체는 OpenAI 서버와 연결을 유지하면서, 모델이 답변을 조금씩 생성할 때마다 chunk(조각)를 전달
👀 OpenAI Response: <openai.Stream object at 0x71bad4322910> 
# response 객체의 타입: # 스트리밍 응답 타입
  • 5-3. 실제 답변 토큰을 보려면?
for chunk in response: 
   print(chunk)
  1. streamlit 구성
  • 6-1. milvus, openai와의 연결 확인
  • 6-2. 질문을 작성할 text_area
  • 6-3. 사이드바
    • milvus collection selectbox 내 선택 - sorted()로 리스트 정렬
    • 검색할 chunk 수 slider 선택
  • 6-4. 질문 입력 & ASK 버튼 클릭 시
    • 처리 시간 측정
    • progress로 진행률 출력
    • 질문 임베딩: get_embeddings
    • milvus 내 문맥 탐색: search_milvus
      • 결과 있을 시, 출력
      • 문맥을 하나의 문자열로 합침
    • LLM 답변 생성: 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)
  • 6-5. 질문 미입력 & ASK 버튼 클릭 시 → 오류 처리

🌻 다이어그램

사용자(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 이상만 출력 및 답변에 사용)

⭐질문과 문서는 같은 임베딩 모델로 벡터화되어야 정확한 유사도 계산이 가능하다.
서로 다른 모델을 쓰면 벡터 공간 자체가 달라져서 유사도 점수가 왜곡된다.


🧪 VectorDB - context

"벡터 DB에서 유사한 문맥을 검색한다"는 무슨 의미일까?

1️⃣ 질문(Question)을 임베딩(Embedding) → 벡터(Vector)로 변환

  • OpenAI Embedding API 같은 걸 써서 질문을 1536차원 같은 고차원 벡터로 바꾼다
  • 예: 질문 → [0.013, -0.054, 0.221, ...]

2️⃣ 문서(Document)도 같은 방식으로 임베딩 → 벡터로 변환

  • 문서를 chunk(청크) 단위로 잘라서 각각 임베딩 생성
  • Milvus 같은 벡터DB에 벡터 + 메타데이터(text, 파일명) 저장

3️⃣ 검색(Search)

  • 사용자가 질문하면 그 질문을 임베딩 → 질문 벡터 생성
  • Milvus에게 “이 질문 벡터랑 가장 가까운(=가장 비슷한) 벡터 몇 개 줘” 라고 요청

4️⃣ 벡터 간 유사도 계산

  • Milvus는 metric_type (예: COSINE, L2, IP)에 따라 거리를 계산:
    • COSINE → 두 벡터가 이루는 각도의 유사도 (1에 가까울수록 비슷)
    • L2 → 유클리드 거리 (0에 가까울수록 비슷)
    • IP → 내적(Inner Product)

질문 벡터와 거리/유사도가 가장 가까운 벡터를 top-k만큼 가져옴

5️⃣ 문맥(Context) 추출

  • Milvus는 벡터만 반환하지 않고, 그 벡터와 연결된 텍스트/메타데이터도 같이 반환
  • 예:
    • [0.02, -0.03, ...] → “RAG 시스템이란 …”
    • [0.05, -0.01, ...] → “Milvus는 …”

➡ 이렇게 가져온 텍스트들이 LLM에 들어가는 문맥(context)

즉, 질문과 가장 “벡터 공간에서 가까운” 문서를 찾는 게 벡터 검색.

  • 📌 거리가 가까울수록 → 의미상으로 더 비슷한 문장이라는 뜻.
  • 👉 “질문 임베딩 벡터와 가장 ‘거리(또는 각도)’가 가까운 문서 벡터를 Milvus가 찾아주고, 그 문서를 문맥으로 활용하는 것”

🧪 Embedding model - dimension

본 파이프라인에서 임베딩 모델로 사용한 text-embedding-3-small은 1536차원이라고 한다.

🔍 1️⃣ 임베딩 모델 = 텍스트를 “고정 길이 벡터”로 바꿔주는 모델

  • OpenAI의 text-embedding-3-small 모델을 쓰면 → 1536차원 벡터로 변환.
  • 같은 텍스트를 넣어도 항상 1536개의 float 숫자가 나온다.

👉 예:

"퀀텀 얼라이언스 체계 구축 목적은?"
→ [-0.033, 0.004, 0.021, ..., 0.002]   # 길이가 1536인 벡터

🔍 2️⃣ 임베딩 모델마다 차원이 다르다

  • 모델이 학습될 때 “몇 차원으로 표현할지” 가 이미 결정돼 있다.
  • OpenAI 예시:
    • text-embedding-3-small1536차원
    • text-embedding-3-large3072차원
  • 다른 임베딩 모델들 예:
    • BERT (base) → 768차원
    • E5-Large → 1024차원
    • Cohere embed-multilingual-v3 → 1024차원

👉 즉, 모델을 바꾸면 같은 질문이라도 결과 벡터 길이(차원)가 달라짐.

🔍 3️⃣ 왜 이렇게 차원이 다를까?

  • 임베딩 모델은 텍스트 의미를 수치 공간(Vector Space)에 표현해요.
  • 차원이 많을수록:
    • ✅ 더 많은 의미적 특징을 담을 수 있음 (더 정밀)
    • ❌ 계산 비용이 커짐 (저장 공간↑, 검색 느려짐)

➡ 그래서 작은 모델(1536차원) 은 빠르고, 큰 모델(3072차원) 은 더 정밀하지만 비용이 높음.

정리

  • 질문을 1536차원 벡터로 변환한다는 말이 맞다.
  • 모델을 바꾸면 → 임베딩 벡터의 차원도 달라진다.
  • Milvus(같은 벡터DB)는 컬렉션 생성 시 차원을 고정하기 때문에,
    • text-embedding-3-small(1536차원) → 컬렉션도 1536차원으로 생성
    • 만약 모델을 text-embedding-3-large(3072차원)로 바꾸면 → 새 컬렉션을 만들어야 함.

👉 즉, “임베딩 모델마다 벡터의 차원이 다르고, 그 모델이 정한 차원으로 질문을 변환한다.”


🧪 response_stream chunk

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...'
  • API가 생성한 이번 채팅(completion)의 고유 ID
  • 한 번의 답변(한 세션)마다 하나씩 발급됨.
    • (전체 답변이 끝날 때까지 chunk들이 같은 id를 공유)

2️⃣ choices

  • 가장 중요한 부분.
  • 모델이 생성한 답변 조각(token)들이 담겨 있음.
  • 리스트이지만, 보통 하나의 모델 응답만 받기 때문에 index=0 하나만 존재.
choices=[
    Choice(
        delta=ChoiceDelta(content=' 추진', function_call=None, refusal=None, role=None, tool_calls=None),
        finish_reason=None,
        index=0,
        logprobs=None
    )
]
  • Choice 객체 안의 핵심 필드:
    • delta → 이 chunk에서 새로 들어온 답변 조각(token) 을 담고 있음.
      • content=' 추진' → 이번 chunk에서 모델이 생성한 한 토큰 (예: " 추진")
      • function_call=None, refusal=None, role=None, tool_calls=None → 기능 호출(Function call)이나 거부(refusal) 같은 특별한 응답이 없다는 뜻.
    • finish_reason
      • None이면 아직 답변이 안 끝났다는 뜻.
      • 마지막 chunk가 올 때는 "stop" 이 들어옴 → 스트리밍 종료 신호.
    • index=0 → 첫 번째 choice라는 뜻 (다중 completion을 요청하면 index=1,2... 생길 수 있음)
    • logprobs=None → 토큰별 확률값 (현재 모델에선 기본적으로 제공 안 함)

3️⃣ created

  • 175...
  • 이 chunk가 생성된 Unix timestamp (초 단위).
  • 언제 이 chunk가 만들어졌는지 알 수 있음.

4️⃣ model

  • 'gpt-4.1-mini-2025-04-14'
  • 답변을 생성한 모델 이름과 버전.

5️⃣ object

  • 'chat.completion.chunk'
  • 이게 스트리밍 응답임을 나타내는 타입 정보.
  • 스트리밍이 아닐 땐 chat.completion 이라고만 옴.

6️⃣ service_tier

  • 'default'
  • API 호출 시 적용된 서비스 레벨 (기본(default), 혹은 enterprise tier 등)

7️⃣ system_fingerprint

  • 'fp_6f...'
  • 모델 실행 환경(백엔드 서버)을 식별하는 내부 값.
  • 디버깅이나 문제 추적용.

8️⃣ usage

  • None
  • 스트리밍 응답 중에는 usage(토큰 수) 정보가 오지 않음.
  • 전체 스트리밍이 끝난 후 최종 응답에서만 usage가 포함됨.

📌 정리

  • chunk = 스트리밍 응답의 한 조각
  • 가장 중요한 건 choices[0].delta.content → 실제로 모델이 새로 생성한 텍스트 토큰
  • finish_reasonNone → 아직 계속되는 중, "stop" → 스트리밍 종료
  • 다른 정보(id, created, model 등)는 메타데이터

👉 즉, for chunk in response_stream:

  • 매 chunk마다 chunk.choices[0].delta.content를 이어붙이면 → 전체 답변 완성.
profile
공부 기록용 24.08.05~ #LLM #RAG

0개의 댓글