티스토리 뷰
Python으로 공부하는 OpenAI 9편 — FastAPI 채팅에 RAG를 붙이면, 이제 내 문서를 근거로 답하기 시작합니다
octo54 2026. 4. 3. 11:39Python으로 공부하는 OpenAI 9편 — FastAPI 채팅에 RAG를 붙이면, 이제 내 문서를 근거로 답하기 시작합니다
7편, 8편까지 오면 채팅이 꽤 그럴듯해집니다.
대화도 이어지고, 히스토리도 관리하고, 오래된 맥락은 압축해서 들고 가고… 여기까지도 사실 꽤 잘 만든 구조예요.
근데 실무에서는 거의 반드시 이런 순간이 옵니다.
“모델이 말은 잘하는데, 우리 문서 기준으로 답하게 하려면?”
“사내 위키, FAQ, 정책 문서, 블로그 글을 바탕으로 답하게 하고 싶은데?”
“대화 기억이랑 문서 검색은 뭐가 다르지?”
바로 여기서 RAG가 들어옵니다.
RAG는 거창하게 말하면 Retrieval-Augmented Generation이고, 더 쉽게 말하면 “모델이 답을 만들기 전에, 관련 문서를 먼저 찾아서 그걸 근거로 답하게 하는 방식”입니다. OpenAI 공식 Retrieval 가이드는 Retrieval API가 semantic search를 통해 데이터에서 의미적으로 관련 있는 결과를 찾고, 이 검색 결과를 모델과 결합해 응답을 합성하는 데 특히 유용하다고 설명합니다. 또 이 Retrieval API는 vector stores를 기반으로 동작한다고 안내합니다. (OpenAI 개발자)
이번 글에서는 이걸 FastAPI 채팅 구조에 붙여보겠습니다.
- 채팅 기억과 RAG는 어떻게 다른지
- 언제 히스토리만으로 답하고, 언제 문서를 검색해야 하는지
- OpenAI Retrieval / Vector Store를 어떻게 붙이는지
- FastAPI 서비스 계층에서 어떤 역할로 나누면 좋은지
- 주니어 개발자가 처음 만들 때 가장 덜 꼬이는 구조
1. 먼저 헷갈리는 것부터 정리: “대화 기억”과 “문서 검색”은 다른 문제입니다
이 부분, 진짜 많이 헷갈립니다.
지금까지 만든 채팅 히스토리는 이런 역할을 합니다.
- 사용자가 방금 뭘 물었는지
- 이전 답변에서 어떤 전제를 썼는지
- 사용자가 어떤 스타일을 선호하는지
- 지금 대화가 어떤 흐름으로 이어지고 있는지
반면 RAG는 이런 역할을 합니다.
- 회사 환불 정책이 정확히 뭐였는지
- 사내 가이드 문서에 어떤 절차가 있는지
- 제품 설명서에 어떤 기능 제약이 있는지
- 블로그 원문에서 어떤 내용이 실제로 쓰였는지
즉,
**히스토리는 “대화 맥락”**이고
**RAG는 “외부 지식 근거”**입니다.
OpenAI 문서도 Retrieval을 “자체 데이터에 대해 semantic search를 수행하는 기능”으로 설명하고, File Search 도구는 업로드한 문서에서 관련 내용을 찾아 모델 응답에 활용할 수 있다고 안내합니다. 특히 File Search 가이드는 먼저 knowledge base를 vector store로 준비하고 파일을 업로드해야 한다고 설명합니다. (OpenAI 개발자)
이 둘을 섞어 쓰면 진짜 강해집니다.
대화는 이어가되, 내용은 내 문서 기준으로 답하게 만들 수 있으니까요.
2. RAG를 왜 붙이냐고 묻는다면, 제 대답은 늘 비슷합니다
처음엔 모델이 되게 똑똑해 보여요.
근데 서비스에 들어가면 금방 알게 됩니다.
똑똑한 것과 내 문서를 근거로 정확하게 답하는 것은 완전히 다른 문제입니다.
예를 들어 사용자가 이렇게 묻는다고 해볼게요.
- “우리 서비스 환불 규정이 뭐야?”
- “사내 온보딩 문서 기준으로 배포 절차 다시 설명해줘”
- “내가 올린 PDF에서 이 개념이 어디에 나와?”
- “작년 기술 블로그에서 Redis 구조 어떻게 설명했지?”
이런 질문은 모델 일반 지식만으로 답하게 두면 위험합니다.
왜냐하면 그럴듯하게 말할 수는 있어도, 우리 문서와 다르게 말할 가능성이 있으니까요.
OpenAI File Search 가이드는 File Search가 모델 바깥의 지식, 예를 들어 사용자가 제공한 문서나 사유 정보를 활용해 답하게 해준다고 설명합니다. Retrieval 가이드 역시 semantic similarity 기반 검색 결과를 모델 응답 합성에 결합하는 흐름을 소개합니다. (OpenAI 개발자)
그래서 실무에서는 보통 이렇게 생각해야 합니다.
- 일반 개념 설명 → 모델 기본 능력
- 우리 문서 기반 답변 → RAG
- 둘 다 필요할 때 → 히스토리 + RAG + 생성
3. 지금 OpenAI 기준으로 RAG를 붙이는 큰 방법은 두 갈래입니다
이 글에서는 일부러 이걸 명확히 나눠서 설명할게요.
방식 A. 직접 임베딩 + 직접 검색
- 문서를 chunk로 나눈다
- embeddings API로 벡터를 만든다
- pgvector, Qdrant, Pinecone, Elasticsearch 같은 저장소에 넣는다
- 질의가 오면 직접 검색해서 상위 chunk를 찾는다
- 그 chunk를 모델 입력에 붙여 답을 생성한다
방식 B. OpenAI Retrieval / File Search 활용
- vector store를 만든다
- 파일을 업로드한다
- Retrieval API나 File Search 도구로 관련 chunk를 찾는다
- 그 결과를 응답 생성에 활용한다
OpenAI Retrieval 가이드는 vector store를 만들고 파일을 업로드한 뒤, client.vector_stores.search(...)로 semantic search를 수행하는 Quickstart를 제공합니다. File Search 가이드는 Responses API에서 tools=[{"type":"file_search", ...}] 형태로 file search tool을 붙이는 예제를 보여줍니다. (OpenAI 개발자)
초기 FastAPI 서비스에서는 저는 보통 이렇게 추천합니다.
- 문서 규모가 작고, 빨리 붙여보고 싶다 → OpenAI File Search / Retrieval
- 인프라를 세밀하게 제어하고 싶다 → 직접 임베딩 + 자체 벡터DB
이번 글은 입문 시리즈 흐름상 OpenAI Vector Store 기반 Retrieval 쪽부터 다루겠습니다.
처음 만들 때 이게 훨씬 덜 힘들어요.
4. Retrieval과 File Search의 차이도 감각적으로 알아야 합니다
이름이 비슷해서 좀 헷갈리는데, 역할이 약간 다릅니다.
Retrieval API
검색 결과를 직접 받아서, 내가 그걸 조립해 모델에 넣는 방식에 가깝습니다. OpenAI Retrieval 가이드는 vector store를 만들고 파일을 업로드한 뒤 client.vector_stores.search(...)로 검색하는 Quickstart를 보여줍니다. (OpenAI 개발자)
File Search tool
Responses API 안에서 모델이 file search tool을 사용하도록 붙이는 방식입니다. OpenAI File Search 가이드는 Responses API 요청에 tools: [{"type":"file_search", "vector_store_ids":[...]}]를 넣어 사용하고, max_num_results나 include=["file_search_call.results"]로 검색 결과 수와 반환 형태를 조절할 수 있다고 설명합니다. (OpenAI 개발자)
실무 감각으로 보면:
- 검색 결과를 내가 더 많이 통제하고 싶다 → Retrieval
- 모델이 문서를 참고해 바로 답하게 하고 싶다 → File Search tool
이 시리즈에서는 FastAPI 서비스 계층 구조를 보여주기 위해
먼저 Retrieval 방식으로 “검색과 생성”을 분리해서 설명하겠습니다.
이게 구조를 이해하기 제일 좋아요.
5. 이번 편에서 만들 구조
8편 구조를 조금 확장해서 갑니다.
app/
├── main.py
├── core/
│ └── config.py
├── schemas/
│ └── rag_chat.py
├── services/
│ ├── chat_history_service.py
│ ├── context_builder_service.py
│ ├── retrieval_service.py
│ └── openai_rag_chat_service.py
└── api/
└── rag_chat_router.py
역할은 이렇게 나눕니다.
- chat_history_service.py
최근 대화 관리 - context_builder_service.py
히스토리 + 검색 결과 + 현재 질문을 모델 입력으로 조립 - retrieval_service.py
vector store 검색 담당 - openai_rag_chat_service.py
최종 생성 담당 - rag_chat_router.py
HTTP API 레이어
이렇게 나누면 좋아요.
검색 로직과 생성 로직이 분리돼서, 나중에 Retrieval을 File Search tool로 바꾸거나, 반대로 pgvector로 바꿔도 다른 계층을 덜 건드리게 됩니다.
6. 먼저 vector store는 어떻게 준비하나
OpenAI Retrieval 가이드의 Quickstart는 Python에서 client.vector_stores.create(...)로 vector store를 만들고, client.vector_stores.files.upload_and_poll(...)로 파일을 업로드하는 흐름을 보여줍니다. (OpenAI 개발자)
즉, 준비 코드는 대략 이렇게 갑니다.
from openai import OpenAI
client = OpenAI()
vector_store = client.vector_stores.create(
name="Company Docs"
)
client.vector_stores.files.upload_and_poll(
vector_store_id=vector_store.id,
file=open("company_faq.txt", "rb")
)
print(vector_store.id)
이 과정이 끝나면, 이제 이 vector store 안에서 semantic search를 할 수 있습니다. Retrieval 가이드는 vector stores가 Retrieval API의 기반 인덱스라고 설명합니다. (OpenAI 개발자)
여기서 중요한 포인트 하나.
문서는 앱 코드 안에 하드코딩하는 게 아니라, 먼저 검색 가능한 인덱스로 준비한다
이게 RAG의 시작입니다.
7. 그다음 검색은 진짜로 이렇게 합니다
OpenAI Retrieval Quickstart는 client.vector_stores.search(vector_store_id=..., query=...) 형태의 검색 예제를 제공합니다. (OpenAI 개발자)
즉, 검색 서비스는 이런 모양으로 잡을 수 있습니다.
app/services/retrieval_service.py
from openai import OpenAI
from app.core.config import Settings
class RetrievalService:
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.client = OpenAI(api_key=settings.openai_api_key)
def search(self, query: str):
results = self.client.vector_stores.search(
vector_store_id=self.settings.openai_vector_store_id,
query=query,
)
return results
이건 정말 단순한 1차 버전입니다.
여기에 나중에 top-k 제한, 결과 포맷 정리, score 필터링 같은 걸 붙이면 됩니다.
Retrieval은 semantic search이기 때문에, 단순 키워드가 정확히 안 겹쳐도 의미상 관련 결과를 찾는 데 강점이 있습니다. OpenAI Retrieval 가이드는 keyword match가 거의 없어도 semantically relevant results를 찾을 수 있다고 설명합니다. (OpenAI 개발자)
8. 그런데 검색 결과를 그대로 모델에 넣으면 안 됩니다
이건 꼭 강조하고 싶어요.
많은 사람이 RAG를 처음 만들면
검색 결과를 그대로 문자열로 붙여버립니다.
물론 그것도 됩니다.
근데 구조가 금방 지저분해져요.
검색 결과에는 보통 이런 정보가 섞입니다.
- 본문 chunk 텍스트
- 파일명
- 메타데이터
- 점수(score)
- file id
- chunk 위치 정보
그래서 실무에서는 “모델에 줄 문서 컨텍스트” 형태로 한 번 정리하는 단계가 필요합니다.
예를 들면 이렇게요.
app/schemas/rag_chat.py
from pydantic import BaseModel, Field
class RagChatRequest(BaseModel):
session_id: str = Field(min_length=1, max_length=100)
message: str = Field(min_length=1, max_length=2000)
class RetrievedChunk(BaseModel):
source: str
content: str
그리고 Retrieval 결과를 이 모델로 정리합니다.
app/services/retrieval_service.py
from openai import OpenAI
from app.core.config import Settings
from app.schemas.rag_chat import RetrievedChunk
class RetrievalService:
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.client = OpenAI(api_key=settings.openai_api_key)
def search(self, query: str, limit: int = 4) -> list[RetrievedChunk]:
results = self.client.vector_stores.search(
vector_store_id=self.settings.openai_vector_store_id,
query=query,
)
chunks: list[RetrievedChunk] = []
for item in results.data[:limit]:
source = getattr(item, "filename", "unknown")
content = getattr(item, "content", "") or ""
if content:
chunks.append(
RetrievedChunk(
source=source,
content=content,
)
)
return chunks
여기서 exact response object shape는 SDK 버전에 따라 조금 다를 수 있어서, 실제 프로젝트에서는 한 번 응답 payload를 찍어보고 필드 이름을 맞추는 게 좋습니다. 다만 Retrieval 가이드상 핵심 흐름 자체, 즉 vector store 검색 → 관련 결과 반환은 공식적으로 정리되어 있습니다. (OpenAI 개발자)
9. 이제 히스토리와 검색 결과를 합쳐서 모델 입력을 만듭니다
여기가 진짜 핵심이에요.
8편에서는:
- session summary
- recent messages
- current question
이렇게 조립했죠.
이제 여기에:
- retrieved chunks
를 끼워 넣습니다.
app/services/context_builder_service.py
from app.schemas.chat import ChatMessage
from app.schemas.rag_chat import RetrievedChunk
class ContextBuilderService:
def build_messages(
self,
current_user_message: str,
recent_messages: list[ChatMessage],
retrieved_chunks: list[RetrievedChunk],
session_summary: str | None = None,
) -> list[dict]:
messages: list[dict] = []
if session_summary:
messages.append({
"role": "system",
"content": (
"다음은 이전 대화의 요약입니다. "
"필요한 맥락으로만 참고하세요.\n"
f"{session_summary}"
)
})
if retrieved_chunks:
rag_text = "\n\n".join(
f"[출처: {chunk.source}]\n{chunk.content}"
for chunk in retrieved_chunks
)
messages.append({
"role": "system",
"content": (
"다음은 검색된 참고 문서입니다. "
"답변할 때 이 문서를 우선 참고하고, 문서에 없는 내용은 추측하지 마세요.\n\n"
f"{rag_text}"
)
})
for msg in recent_messages:
messages.append({
"role": msg.role,
"content": msg.content,
})
messages.append({
"role": "user",
"content": current_user_message,
})
return messages
이 구조가 좋은 이유는 하나예요.
대화 기억과 문서 근거가 한 입력 안에서 만나기 때문입니다.
즉, 모델은 이제
“이 사용자가 방금 무슨 대화를 했는지”도 알고,
“관련 문서에서 어떤 내용이 검색됐는지”도 같이 보게 됩니다.
10. 최종 생성 서비스는 이렇게 갑니다
OpenAI Python SDK README는 client.responses.create(...)가 현재 기본 텍스트 생성 흐름이고, 동기/비동기 클라이언트를 모두 제공한다고 설명합니다. (GitHub)
그래서 최종 생성 서비스는 대략 이렇게 쓸 수 있습니다.
app/services/openai_rag_chat_service.py
from openai import AsyncOpenAI
from app.core.config import Settings
class OpenAIRagChatService:
def __init__(self, settings: Settings) -> None:
self.settings = settings
self.client = AsyncOpenAI(api_key=settings.openai_api_key)
async def answer(self, messages: list[dict]) -> str:
response = await self.client.responses.create(
model=self.settings.openai_model,
instructions=(
"당신은 주니어 개발자를 돕는 친절한 Python 멘토입니다. "
"항상 한국어로 답하세요. "
"검색된 문서가 있으면 그 내용을 우선 근거로 사용하세요. "
"문서에 없는 내용은 단정하지 말고 불확실하다고 말하세요."
),
input=messages,
)
return response.output_text
이제 거의 다 됐습니다.
- 히스토리에서 최근 대화 가져오고
- Retrieval로 문서 찾고
- 둘을 합쳐 messages 만들고
- Responses API로 최종 답 생성
이 흐름이 바로 가장 기본적인 RAG 채팅입니다.
11. FastAPI 라우터는 오히려 더 단순해집니다
이 시점의 라우터는 보통 이렇게 생깁니다.
app/api/rag_chat_router.py
from typing import Annotated
from fastapi import APIRouter, Depends
from app.core.config import Settings, get_settings
from app.schemas.rag_chat import RagChatRequest
from app.services.chat_history_service import InMemoryChatHistoryService
from app.services.context_builder_service import ContextBuilderService
from app.services.openai_rag_chat_service import OpenAIRagChatService
from app.services.retrieval_service import RetrievalService
router = APIRouter(prefix="/rag-chat", tags=["rag-chat"])
_history_service = InMemoryChatHistoryService()
def get_history_service() -> InMemoryChatHistoryService:
return _history_service
def get_retrieval_service(
settings: Annotated[Settings, Depends(get_settings)],
) -> RetrievalService:
return RetrievalService(settings)
def get_context_builder() -> ContextBuilderService:
return ContextBuilderService()
def get_rag_chat_service(
settings: Annotated[Settings, Depends(get_settings)],
) -> OpenAIRagChatService:
return OpenAIRagChatService(settings)
@router.post("")
async def rag_chat(
request: RagChatRequest,
history_service: Annotated[InMemoryChatHistoryService, Depends(get_history_service)],
retrieval_service: Annotated[RetrievalService, Depends(get_retrieval_service)],
context_builder: Annotated[ContextBuilderService, Depends(get_context_builder)],
rag_chat_service: Annotated[OpenAIRagChatService, Depends(get_rag_chat_service)],
):
history_service.add_user_message(request.session_id, request.message)
recent_messages = history_service.get_recent_messages(request.session_id, limit=8)
retrieved_chunks = retrieval_service.search(request.message, limit=4)
messages = context_builder.build_messages(
current_user_message=request.message,
recent_messages=recent_messages[:-1],
retrieved_chunks=retrieved_chunks,
session_summary=None,
)
answer = await rag_chat_service.answer(messages)
history_service.add_assistant_message(request.session_id, answer)
return {
"answer": answer,
"sources": [chunk.source for chunk in retrieved_chunks],
}
이 구조가 딱 좋습니다.
- 히스토리 관리
- 검색
- 컨텍스트 조립
- 생성
이 네 단계가 분리되어 있거든요.
12. “언제 검색해야 하나?”도 생각보다 중요합니다
이건 실무에서 되게 중요합니다.
모든 질문마다 무조건 검색하면 비효율적일 수 있어요.
예를 들면 이런 질문은 굳이 검색이 없어도 됩니다.
- “아까 설명한 걸 한 줄로 줄여줘”
- “좀 더 쉽게 말해줘”
- “방금 예제를 다시 보여줘”
반면 이런 질문은 검색이 강하게 필요합니다.
- “우리 회사 환불 정책이 뭐야?”
- “업로드한 PDF 기준으로 설명해줘”
- “사내 온보딩 문서 기준으로 배포 순서 알려줘”
그래서 실무에서는 보통 아래 둘 중 하나를 씁니다.
단순 규칙 방식
특정 키워드가 있으면 검색
- “문서”
- “정책”
- “PDF”
- “가이드”
- “FAQ”
라우팅 분류 방식
먼저 작은 분류 프롬프트로
“이 질문은 검색이 필요한가?”를 판별한 뒤 분기
처음엔 규칙 방식으로 시작해도 충분합니다.
괜히 처음부터 복잡한 router agent 만들 필요 없어요.
13. File Search tool 방식도 같이 알아두면 좋습니다
앞에서는 Retrieval을 “검색과 생성 분리” 구조로 설명했는데,
OpenAI는 Responses API에서 File Search tool을 직접 붙이는 방식도 제공합니다.
OpenAI File Search 가이드는 다음처럼 tools=[{"type":"file_search","vector_store_ids":[...]}]를 주고, 필요하면 max_num_results로 결과 수를 제한할 수 있다고 설명합니다. 또 기본적으로는 검색 결과 자체가 응답에 포함되지 않지만, include=["file_search_call.results"]를 넣으면 검색 결과를 함께 볼 수 있다고 안내합니다. (OpenAI 개발자)
예를 들면 이런 코드입니다.
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-4.1",
input="우리 환불 정책 문서를 기준으로 요약해줘",
tools=[{
"type": "file_search",
"vector_store_ids": ["<vector_store_id>"],
"max_num_results": 3
}],
include=["file_search_call.results"]
)
이 방식의 장점은 빨리 붙기 쉽다는 거고,
단점은 검색과 생성을 더 세밀하게 분리해서 제어하는 감각은 Retrieval 직접 호출보다 조금 약할 수 있다는 점입니다.
그래서 저는 보통 이렇게 권합니다.
- 구조를 배우는 단계 → Retrieval 직접 호출
- 빨리 데모 붙이는 단계 → File Search tool
- 나중에 둘 중 더 맞는 쪽으로 정착
14. 직접 임베딩 방식도 왜 알아야 하냐면
OpenAI Embeddings 가이드는 embeddings를 텍스트의 수치 벡터 표현으로 설명하고, 검색·클러스터링·추천 등 다양한 용도에 쓸 수 있다고 안내합니다. 또 Python 예제로 client.embeddings.create(input="...", model="text-embedding-3-small")를 보여줍니다. text-embedding-3-small의 기본 차원은 1536, text-embedding-3-large는 3072라고 설명합니다. (OpenAI 개발자)
즉, 나중에 OpenAI hosted vector store가 아니라 직접 벡터DB를 운영하고 싶다면 이런 흐름으로 갑니다.
from openai import OpenAI
client = OpenAI()
response = client.embeddings.create(
input="파이썬 데코레이터는 기존 함수를 감싸서 기능을 추가하는 문법이다.",
model="text-embedding-3-small"
)
embedding = response.data[0].embedding
print(len(embedding))
이렇게 만든 embedding을 pgvector 같은 곳에 넣고 cosine similarity로 검색하면,
직접 구현하는 RAG가 됩니다.
초기엔 Retrieval/File Search가 편하지만,
데이터 거버넌스나 검색 튜닝을 더 세밀하게 하려면 직접 임베딩 구조를 알아둘 필요가 있습니다.
15. 주니어가 RAG에서 가장 많이 하는 실수
실수 1. 대화 히스토리와 검색 결과를 같은 것으로 취급한다
아닙니다. 둘은 역할이 다릅니다.
히스토리는 대화 흐름, 검색 결과는 근거 문서입니다.
실수 2. 검색 결과를 너무 많이 넣는다
많이 넣는다고 항상 좋은 게 아닙니다.
오히려 핵심 chunk 몇 개만 잘 넣는 편이 더 나을 때가 많습니다. OpenAI File Search 가이드도 max_num_results를 줄이면 토큰 사용량과 지연을 낮출 수 있지만, 대신 품질 trade-off가 있을 수 있다고 설명합니다. (OpenAI 개발자)
실수 3. 검색 결과가 없는데도 아는 척하게 둔다
이건 위험합니다.
검색 기반 서비스라면 “문서에서 확인되지 않았다”는 식의 fallback 정책이 필요합니다.
실수 4. chunk 원문을 너무 지저분하게 넣는다
HTML 찌꺼기, 표 깨짐, 중복 텍스트, 머리말/꼬리말이 너무 많으면 검색 품질도 떨어집니다.
실수 5. RAG를 붙였다고 무조건 정확해졌다고 믿는다
RAG는 강력하지만, 검색이 잘못되면 생성도 흔들립니다.
결국 검색 품질, chunk 품질, prompt 설계가 같이 좋아야 합니다.
16. 지금 단계에서 추천하는 가장 현실적인 설계
주니어 개발자가 FastAPI 기반으로 첫 RAG 채팅을 만들 때는 이 정도가 제일 균형이 좋습니다.
- 채팅 히스토리 시스템은 그대로 유지
- 문서는 OpenAI vector store에 업로드
- 질문마다 Retrieval로 top 3~5 chunk 검색
- 히스토리 + 검색 결과 + 현재 질문을 함께 조립
- Responses API로 최종 답변 생성
- 응답에 source 파일명도 함께 반환
왜 source를 같이 반환하냐고요?
사람이 검증하기 쉬워지거든요.
실무에선 이게 꽤 중요합니다.
그리고 OpenAI File Search 가이드처럼 include=["file_search_call.results"]를 활용하면
검색 결과 자체를 응답에 포함시켜 디버깅하기도 좋습니다. (OpenAI 개발자)
17. 오늘 글의 핵심 요약
이번 글에서 꼭 가져가야 할 건 이것입니다.
RAG는 채팅 히스토리를 대체하는 게 아니라, 그 위에 “문서 근거”를 추가하는 구조다.
정리하면 흐름은 이렇습니다.
- 히스토리로 대화 맥락을 유지한다.
- Retrieval 또는 File Search로 관련 문서를 찾는다.
- 검색된 chunk를 정리해서 모델 입력에 넣는다.
- Responses API로 최종 답을 생성한다.
- 가능하면 source도 함께 반환해 검증 가능성을 높인다. (OpenAI 개발자)
이제부터는 진짜 “우리 문서를 아는 채팅 서비스”로 넘어가기 시작합니다.
다음 편 예고
다음 글에서는 여기서 한 단계 더 들어가겠습니다.
Python + FastAPI에서 RAG 품질을 높이기 위한 chunking, top-k, re-ranking 감각
이 주제로,
- 문서를 어떻게 나눠야 검색이 잘 되는지
- chunk 크기를 너무 크게 하면 왜 안 좋은지
- top-k를 몇 개로 시작하면 좋은지
- 검색 결과를 다시 정렬해야 하는 경우
- RAG가 “붙긴 했는데 답이 별로”일 때 어디를 먼저 봐야 하는지
까지 이어가보겠습니다.
출처
- OpenAI Retrieval 가이드 — semantic search, vector stores, Quickstart로 vector store 생성·파일 업로드·검색 예제 제공. (OpenAI 개발자)
- OpenAI File Search 가이드 — Responses API에서 file_search tool 사용, vector_store_ids, max_num_results, include=["file_search_call.results"] 지원. (OpenAI 개발자)
- OpenAI Embeddings 가이드 — embeddings의 개념, 활용 사례, text-embedding-3-small/text-embedding-3-large, Python 예제, 기본 차원 수 안내. (OpenAI 개발자)
- OpenAI Python SDK README — client.responses.create(...), AsyncOpenAI, 스트리밍 및 async 클라이언트 사용 흐름. (GitHub)
Python, OpenAI, FastAPI, RAG, Retrieval, File Search, Vector Store, Embeddings, OpenAI Python SDK, Responses API, AI 백엔드, 문서 검색, semantic search, 주니어 개발자, 사내 챗봇
'study > Python으로 시작하는 OpenAI 개발 입문' 카테고리의 다른 글
| Python으로 공부하는 OpenAI 8편 — 채팅 히스토리는 전부 저장하고, 모델에는 필요한 만큼만 보내야 합니다 (0) | 2026.03.31 |
|---|---|
| Python으로 공부하는 OpenAI 7편 — FastAPI에서 대화 히스토리를 이어가는 채팅 API 만들기 (0) | 2026.03.30 |
| Python으로 공부하는 OpenAI 6편 — FastAPI에서 OpenAI 스트리밍 응답 만들기 (0) | 2026.03.26 |
| Python으로 공부하는 OpenAI 5편 — FastAPI에서 OpenAI 서비스 계층을 제대로 나누는 방법 (0) | 2026.03.23 |
| Python으로 공부하는 OpenAI 4편 — Pydantic으로 응답을 검증해야 진짜 백엔드 코드가 됩니다 (0) | 2026.03.20 |
- Total
- Today
- Yesterday
- 백엔드개발
- JAX
- Next.js
- fastapi
- PostgreSQL
- llm
- flax
- rag
- 개발블로그
- node.js
- Docker
- Prisma
- DevOps
- NestJS
- ai철학
- REACT
- JWT
- 쿠버네티스
- CI/CD
- 딥러닝
- Python
- 웹개발
- nextJS
- Express
- seo 최적화 10개
- kotlin
- LangChain
- 압박면접
- SEO최적화
- Redis
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
