1. 학습 주제
RAG 흐름에서 검색 결과를 실제 LLM 답변으로 연결하는 구조와, FastAPI로 API 서버를 만드는 기본 흐름을 학습했다.
핵심 내용은 다음과 같다.
- Chroma를 사용한 영구 VectorStore 구성
- Retriever로 관련 문서 검색
- 검색된 문서를 Context로 만들어 LLM에 전달
- RAG 답변에 출처 포함하기
- FastAPI의 Path, Query, Body 처리
- Pydantic을 활용한 요청 검증
- response_model과 예외 처리
2. Chroma VectorStore
이전에는 InMemoryVectorStore를 사용해 메모리 안에서만 문서를 저장하고 검색했다.
이번에는 Chroma를 사용해 벡터 데이터를 디스크에 저장하고 재사용하는 구조를 학습했다.
store = Chroma(
collection_name="class_knowledge",
embedding_function=create_embeddings(),
persist_directory=str(CHROMA_PERSIST_DIRECTORY),
)
InMemoryVectorStore는 프로그램을 종료하면 데이터가 사라진다.
반면 Chroma는 persist_directory에 벡터 DB를 저장하므로, 다시 실행해도 기존 색인을 재사용할 수 있다.
흐름은 다음과 같다.
문서 로드
-> Document 변환
-> Embedding 생성
-> Chroma에 저장
-> 기존 색인이 있으면 재사용
-> 질문과 유사한 문서 검색
3. 색인과 재사용
VectorStore에 문서를 넣는 과정을 색인(indexing)이라고 볼 수 있다.
문서를 Embedding Vector로 바꾸고, 검색 가능한 형태로 저장하는 과정이다.
existing_ids = store.get()["ids"]
indexed_now = not existing_ids
if indexed_now:
store.add_documents(docs, ids=[str(document.id) for document in docs])
이미 저장된 문서 ID가 있으면 다시 Embedding하지 않고 기존 색인을 재사용한다.
--rebuild-index 옵션을 사용하면 기존 collection을 초기화하고 다시 색인할 수 있다.
if rebuild_index:
store.reset_collection()
정리하면 다음과 같다.
| 상황 | 처리 |
| 처음 실행 | 문서를 Embedding 후 Chroma에 저장 |
| 다시 실행 | 기존 Chroma 색인 재사용 |
| --rebuild-index 사용 | 기존 색인 삭제 후 다시 생성 |
4. Retriever
Retriever는 VectorStore 위에서 검색 정책을 적용하는 객체이다.
retriever = store.as_retriever(
search_kwargs={"k": top_k}
)
VectorStore가 벡터와 문서를 저장하는 역할이라면, Retriever는 “질문이 들어왔을 때 어떤 문서를 몇 개 가져올지” 정하는 역할이다.
질문
-> Retriever
-> VectorStore에서 유사 문서 검색
-> 상위 k개 Document 반환
top_k는 검색 결과를 몇 개 가져올지 정하는 값이다.
retrieved_docs = retriever.invoke(question)
5. Metadata Filter
문서에는 본문뿐 아니라 metadata도 함께 들어간다.
metadata={
"source": path.name,
"line": line_number,
"topic": topic_for_text(text),
}
metadata는 검색 결과의 출처를 표시하거나, 특정 주제만 검색할 때 사용한다.
예를 들어 memory 관련 문서만 검색하고 싶다면 다음처럼 filter를 적용할 수 있다.
filter=lambda document: document.metadata.get("topic") == "memory"
이렇게 하면 전체 문서 중 질문과 비슷한 문서를 찾되, topic이 memory인 문서만 대상으로 검색한다.
6. RAG 답변 생성 흐름
RAG는 검색된 문서를 LLM에게 근거로 제공하고 답변을 생성하는 방식이다.
이번 구조는 다음과 같다.
사용자 질문
-> Retriever가 관련 Document 검색
-> 검색 결과를 Context 문자열로 변환
-> Prompt에 Context와 질문 삽입
-> LLM 호출
-> 문서 기반 답변 생성
검색된 문서는 다음처럼 Context로 변환한다.
def format_context(documents):
return "\n\n".join(
f"[출처: {doc.metadata.get('source')}:{doc.metadata.get('line')}]\n"
f"{doc.page_content}"
for doc in documents
)
이 Context를 Prompt에 넣는다.
너는 AI 수업 도우미다.
아래 Context만 근거로 답하라.
근거가 없으면 문서에서 찾을 수 없다고 답하라.
답변 마지막에 사용한 출처를 표시하라.
[Context]
...
핵심은 LLM이 마음대로 답하지 않도록 “검색된 문서만 근거로 답하라”는 제약을 주는 것이다.
7. FastAPI 기본 구조
FastAPI는 Python으로 API 서버를 만들 수 있는 웹 프레임워크이다.
기본 객체를 만들고, decorator로 URL과 함수를 연결한다.
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello FastAPI"}
이 구조는 다음 의미를 가진다.
GET /
-> root 함수 실행
-> dict 반환
-> FastAPI가 JSON 응답으로 변환
FastAPI는 기본적으로 /docs에서 Swagger 문서를 자동 제공한다.
API를 만들면서 바로 테스트할 수 있다는 장점이 있다.
8. Path Parameter와 Query Parameter
Path Parameter는 URL 경로 안에 들어가는 값이다.
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
예시:
GET /items/10
Query Parameter는 URL 뒤에 ?key=value 형태로 붙는 값이다.
@app.get("/items")
async def list_items(skip: int = 0, limit: int = 3):
...
예시:
GET /items?skip=0&limit=3
정리하면 다음과 같다.
| 구분 | 예시 | 사용 목적 |
| Path Parameter | /items/10 | 특정 자원 식별 |
| Query Parameter | /items?limit=3 | 검색, 필터, 페이징 조건 |
9. Request Body와 Pydantic
POST나 PUT 요청에서는 JSON Body를 받을 수 있다.
FastAPI에서는 Pydantic 모델을 사용해 Body 구조를 정의한다.
class ItemCreate(BaseModel):
name: str = Field(min_length=1, max_length=50)
price: float = Field(gt=0)
@app.post("/items")
async def create_item(item: ItemCreate):
return item.model_dump()
Pydantic 모델을 쓰면 좋은 점은 다음과 같다.
- 필수값 검증
- 타입 검증
- 문자열 길이 검증
- 숫자 범위 검증
- Swagger 문서 자동 생성
단순히 dict로 받으면 내부 필드 검증이 약해진다.
그래서 API 요청 Body는 가능하면 Pydantic 모델로 정의하는 것이 좋다.
10. Validation
FastAPI와 Pydantic은 요청값 검증을 자동으로 처리해준다.
name: str = Field(min_length=2, max_length=50)
price: float = Field(gt=0, le=1_000_000)
category: Literal["book", "course", "tool"]
검증 조건을 만족하지 않으면 FastAPI가 자동으로 422 Unprocessable Entity 응답을 반환한다.
직접 검증 로직도 추가할 수 있다.
@field_validator("name")
@classmethod
def normalize_name(cls, value: str) -> str:
normalized = value.strip()
if not normalized:
raise ValueError("상품명은 공백만 입력할 수 없습니다.")
return normalized
이런 방식으로 API 입구에서 잘못된 데이터를 막을 수 있다.
11. Response Model과 예외 처리
response_model은 응답으로 내보낼 데이터 구조를 제한한다.
class UserResponse(BaseModel):
id: int
username: str
display_name: str
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
return find_user_or_404(user_id)
DB나 내부 객체에 password 같은 값이 있어도, response_model에 없는 필드는 응답에서 제외된다.
사용자를 찾지 못한 경우에는 HTTPException으로 명확한 에러를 반환한다.
raise HTTPException(status_code=404, detail="사용자를 찾을 수 없습니다.")
API에서는 정상 응답뿐 아니라 실패 상황도 명확히 설계해야 한다.
12. 핵심 정리
- Chroma는 벡터 데이터를 디스크에 저장하고 재사용할 수 있는 VectorStore이다.
- 색인은 문서를 Embedding Vector로 변환해 검색 가능한 상태로 저장하는 과정이다.
- Retriever는 VectorStore에서 질문과 관련 있는 문서를 가져오는 검색 담당 객체이다.
- top_k는 검색 결과를 몇 개 가져올지 정하는 값이다.
- metadata는 출처 표시와 필터 검색에 사용된다.
- RAG는 검색된 문서를 Context로 만들고, 질문과 함께 LLM에 전달해 답변을 생성한다.
- FastAPI는 decorator로 HTTP Method와 URL을 함수에 연결한다.
- Path Parameter는 특정 자원을 식별하고, Query Parameter는 검색이나 페이징 조건에 자주 사용된다.
- Request Body는 Pydantic 모델로 정의하면 타입과 값 검증을 자동화할 수 있다.
- response_model은 응답 데이터 구조를 제한해 민감한 필드 노출을 막는 데 도움이 된다.
- HTTPException을 사용하면 API 실패 상황을 명확한 상태 코드와 메시지로 처리할 수 있다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]FastAPI 파일 브릿지와 LangGraph 기초 (0) | 2026.07.28 |
|---|---|
| [TIL]FastAPI REST API, Router 분리, File Upload, Spring 연동 (0) | 2026.07.27 |
| [TIL]Embedding과 VectorStore (1) | 2026.07.23 |
| [TIL]Chatbot Memory와 이전 대화 기억 구조 (0) | 2026.07.23 |
| [TIL]RAG와 OpenAI Embeddings 기반 문서 검색 흐름 (0) | 2026.07.21 |