본문 바로가기
AI System/OpenAI API와 바이브 코딩으로 배우는 AI 서비스 개발

d02 - 07. embeddings firesearch retrieval - 04. responses rag v2

by Toddler_AD 2026. 6. 8.

04. Responses File Search RAG 직접 호출 v2

이 노트북은 Responses API의 file_search 도구를 직접 사용해 검색과 답변 생성을 한 번에 수행합니다. 검색 결과 include와 previous_response_id 기반 후속 질문도 함께 확인합니다.

# Colab 또는 로컬 노트북 실행 환경을 구분하기 위해 sys를 가져옵니다.
import sys
# 패키지 설치 명령을 현재 Python 커널에서 실행하기 위해 subprocess를 가져옵니다.
import subprocess
# 패키지 설치 여부를 확인하기 위해 importlib.util을 가져옵니다.
import importlib.util
# 환경 변수에서 API 키를 읽고 설정하기 위해 os를 가져옵니다.
import os
# API 키를 화면에 노출하지 않고 입력받기 위해 getpass를 가져옵니다.
import getpass
# .env 파일 위치를 다루기 위해 pathlib의 Path를 가져옵니다.
from pathlib import Path

# google.colab 모듈이 있으면 현재 런타임이 Google Colab이라고 판단합니다.
IN_COLAB = "google.colab" in sys.modules
# 노트북에서 사용하는 import 이름과 pip 패키지 이름을 짝지어 둡니다.
REQUIRED_PACKAGES = {
    "openai": "openai>=2.26.0",
    "dotenv": "python-dotenv>=1.2.2",
    "pydantic": "pydantic>=2.11.0",
    "PIL": "pillow>=12.1.1",
    "requests": "requests>=2.32.5",
    "numpy": "numpy>=2.3.3",
    "websockets": "websockets>=15.0.1",
    "websocket": "websocket-client>=1.8.0",
    "nest_asyncio": "nest-asyncio>=1.6.0",
}

def ensure_package(import_name: str, package_name: str) -> None:
    if importlib.util.find_spec(import_name) is not None:
        return
    subprocess.check_call(
        [sys.executable, "-m", "pip", "install", "-q", package_name]
    )

if IN_COLAB:
    for import_name, package_name in REQUIRED_PACKAGES.items():
        ensure_package(import_name, package_name)

from dotenv import load_dotenv

load_dotenv(Path.cwd() / ".env")
if not os.getenv("OPENAI_API_KEY"):
    secret_key = None
    try:
        from google.colab import userdata

        secret_key = userdata.get("OPENAI_API_KEY")
    except Exception:
        secret_key = None
    if secret_key:
        os.environ["OPENAI_API_KEY"] = secret_key
    else:
        entered_key = getpass.getpass("OPENAI_API_KEY를 입력하세요: ").strip()
        if entered_key:
            os.environ["OPENAI_API_KEY"] = entered_key

if not os.getenv("OPENAI_API_KEY"):
    raise RuntimeError(
        "OPENAI_API_KEY가 없습니다. Colab Secrets, 수동 입력, 또는 .env 파일로 설정해 주세요."
    )

print(f"개발환경: {'Colab' if IN_COLAB else '로컬'}")
print("OPENAI_API_KEY 준비 완료")
개발환경: 로컬
OPENAI_API_KEY 준비 완료
 
 

실습 전에 보는 핵심 개념과 API

 

핵심 개념

  • RAG (Retrieval-Augmented Generation, 검색 증강 생성)는 먼저 관련 문서를 검색하고, 그 결과를 근거로 답변을 만드는 방식입니다.
  • 이 노트북은 검색 API를 따로 조합하지 않고, Responses API 안에서 file_search 도구를 직접 호출합니다.
  • previous_response_id를 사용하면 이전 응답 맥락을 이어 받아 후속 질문을 자연스럽게 처리할 수 있습니다.
  • include=["file_search_call.results"]를 사용하면 모델이 참고한 검색 근거를 함께 확인할 수 있습니다.

 

API 소개

  • client.responses.create(...)는 일반 응답 생성뿐 아니라 도구 호출도 함께 처리하는 통합 API입니다.
  • tools=[{"type": "file_search"}]를 넣으면 응답 생성 과정에서 파일 검색을 수행할 수 있습니다.
  • 이 노트북은 기본 file_search 답변, 검색 결과 포함 응답, 멀티턴 follow-up(후속 질문)을 다룹니다.

 

API 호출부와 주요 매개변수

response = client.responses.create(
    model=MODEL_NAME,
    instructions="검색 결과에 근거해 답변하고, 모르면 모른다고 말하세요.",
    input="재택근무 신청 절차를 요약해 주세요.",
    tools=[{"type": "file_search", "vector_store_ids": [vector_store_id]}],
    tool_choice="required",
    include=["file_search_call.results"],
    previous_response_id=previous_response_id,
)
  • model
    • 보통 gpt-5, gpt-5-mini, gpt-5.5 같은 Responses 모델을 넣습니다.
    • 기본값: 실질적인 기본값이 없으므로 명시하는 것이 안전합니다.
  • instructions
    • 검색 근거를 어떻게 사용해 답변할지 지시합니다.
    • 예를 들어 근거 중심 요약, 모르면 모른다고 답하기, 표 형식 정리 같은 정책을 넣을 수 있습니다.
    • 기본값: 없습니다. 생략하면 모델 기본 답변 스타일에 맡기게 됩니다.
  • input
    • 사용자 질문입니다.
    • 문자열 하나로 줄 수도 있고, 메시지 배열 형태로 확장할 수도 있습니다.
    • 기본값: 없습니다.
  • tools
    • 사용할 도구 목록입니다.
    • file_search 도구에는 보통 vector_store_ids를 함께 넣어 어느 인덱스를 검색할지 정합니다.
    • 기본값: 생략하면 도구를 사용하지 않는 응답으로 처리됩니다.
  • tool_choice
    • 일반적으로 auto, required, none 같은 흐름을 생각할 수 있습니다.
    • required는 반드시 검색을 사용하게 만들고, auto는 모델 판단에 맡깁니다.
    • 기본값: 생략하면 자동 선택 흐름으로 이해하는 편이 안전합니다.
  • include
    • 응답에 포함할 부가 정보를 지정합니다.
    • 이 노트북에서는 file_search_call.results를 넣어 검색 근거를 함께 저장합니다.
    • 기본값: 생략하면 추가 상세 정보 없이 기본 응답 본문만 받습니다.
  • previous_response_id
    • 후속 질문에서 이전 문맥을 잇는 값입니다.
    • 기본값: 없습니다. 첫 턴에서는 넣지 않고, follow-up에서만 사용합니다.
  • max_output_tokens
    • 답변 길이 상한을 정하는 정수입니다.
    • 짧은 요약이면 작게, 근거 포함 답변이면 조금 더 크게 잡습니다.
    • 기본값: 생략하면 모델 기본 동작을 따르므로, 학습용 비교에서는 명시하는 편이 안전합니다.

 

요청/응답 필드에서 볼 것

  • RAG 실습에서는 먼저 요청의 input, tools, tool_choice, include, previous_response_id를 읽고, 이번 턴이 기본 검색 답변인지 근거 포함 답변인지 후속 질문인지 구분해 보세요.
  • 기본 결과를 확인할 때는 response.output_text를 먼저 읽고, 바로 아래에 response.id를 기록해 다음 후속 질문에 재사용할 준비를 해 두는 것이 좋습니다.
  • include=["file_search_call.results"]를 넣은 실습에서는 답변만 읽지 말고 file_search_call.results 안에 실제로 어떤 문서 조각이 들어왔는지 함께 열어 보는 것이 핵심입니다.
  • 답변이 이상하면 가장 먼저 검색 결과가 충분했는지, 그리고 tool_choice가 정말 검색을 강제했는지 확인하면 원인을 좁히기 쉽습니다.
  • 실습 로그에는 질문, 답변, response.id, 검색 근거 JSON, usage를 한 파일에 같이 저장해 두면 RAG 흐름을 나중에 설명하거나 디버깅하기 편합니다.
 

1단계. 실행 환경 준비

RAG는 검색 인덱스와 생성 모델을 함께 사용합니다. 먼저 vector_store_id를 읽어 file_search 도구가 어느 인덱스를 사용할지 정합니다.

# 운영체제 환경 변수를 읽기 위해 os 모듈을 가져옵니다.
import os
# JSON 설정과 결과를 저장하기 위해 json 모듈을 가져옵니다.
import json
# 실행 시간과 인덱싱 대기 시간을 다루기 위해 time 모듈을 가져옵니다.
import time
# 벡터 유사도 계산에 사용할 수학 함수를 가져옵니다.
import math
# 파일 경로를 안전하게 다루기 위해 Path를 가져옵니다.
from pathlib import Path
# 타입 힌트에서 유연한 응답 구조를 표현하기 위해 Any를 가져옵니다.
from typing import Any
# .env 파일의 API 키를 로드하기 위해 load_dotenv를 가져옵니다.
from dotenv import load_dotenv
# OpenAI API를 직접 호출하기 위해 OpenAI 클라이언트를 가져옵니다.
from openai import OpenAI
# 현재 실행 위치를 저장합니다.
CURRENT_DIR = Path.cwd()
# 루트에서 실행하는 경우 retrieval 트랙 폴더를 찾습니다.
if (CURRENT_DIR / "07.embeddings_firesearch_retrieval").exists():
    # 워크스페이스 루트에서 실행 중이면 해당 폴더를 실습 루트로 사용합니다.
    RETRIEVAL_ROOT = CURRENT_DIR / "07.embeddings_firesearch_retrieval"
# 하위 폴더에서 실행하는 경우 경로 조각에서 트랙 폴더를 찾습니다.
elif "07.embeddings_firesearch_retrieval" in [part for part in CURRENT_DIR.parts]:
    # 트랙 폴더까지의 경로를 복원합니다.
    RETRIEVAL_ROOT = Path(*CURRENT_DIR.parts[: CURRENT_DIR.parts.index("07.embeddings_firesearch_retrieval") + 1])
# 그 밖의 경우 상대 경로를 사용합니다.
else:
    # 현재 위치 기준 트랙 폴더를 실습 루트로 사용합니다.
    RETRIEVAL_ROOT = CURRENT_DIR / "07.embeddings_firesearch_retrieval"
# 워크스페이스 루트는 retrieval 트랙의 상위 폴더입니다.
WORKSPACE_ROOT = RETRIEVAL_ROOT.parent
# 결과 저장 폴더를 정합니다.
OUTPUT_DIR = RETRIEVAL_ROOT / "output" / "v2"
# 결과 저장 폴더를 생성합니다.
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 샘플 문서 폴더를 정합니다.
SAMPLE_DIR = RETRIEVAL_ROOT / "samples" / "v2"
# 샘플 문서 폴더를 생성합니다.
SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
# 벡터 스토어 설정 파일 경로를 정합니다.
CONFIG_PATH = OUTPUT_DIR / "02-04_vector_store_config.json"
# .env 파일을 읽습니다.
load_dotenv(WORKSPACE_ROOT / ".env")
# API 키가 없으면 명확한 오류로 중단합니다.
if not os.getenv("OPENAI_API_KEY"):
    # 필요한 환경 변수 이름을 포함해 안내합니다.
    raise RuntimeError("OPENAI_API_KEY가 없습니다. 워크스페이스 루트의 .env 파일을 확인해 주세요.")
# OpenAI API 클라이언트를 생성합니다.
client = OpenAI()
# 임베딩 모델명을 환경 변수에서 읽습니다.
EMBEDDING_MODEL = os.getenv("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small")
# Responses API 모델명을 환경 변수에서 읽습니다.
RESPONSES_MODEL = os.getenv("OPENAI_RESPONSES_MODEL", "gpt-5-mini")
# 워크스페이스 기준 상대 경로를 반환하는 함수를 정의합니다.
def workspace_path(path: Path) -> str:
    # 절대 경로를 워크스페이스 기준 / 구분 문자열로 변환합니다.
    return path.resolve().relative_to(WORKSPACE_ROOT.resolve()).as_posix()
# 코사인 유사도를 계산하는 함수를 정의합니다.
def cosine_similarity(left: list[float], right: list[float]) -> float:
    # 두 벡터의 내적을 계산합니다.
    dot = sum(a * b for a, b in zip(left, right))
    # 왼쪽 벡터의 길이를 계산합니다.
    left_norm = math.sqrt(sum(a * a for a in left))
    # 오른쪽 벡터의 길이를 계산합니다.
    right_norm = math.sqrt(sum(b * b for b in right))
    # 0으로 나누는 상황을 피하기 위해 길이를 확인합니다.
    if left_norm == 0 or right_norm == 0:
        # 길이가 0이면 유사도를 0으로 처리합니다.
        return 0.0
    # 내적을 두 벡터 길이의 곱으로 나누어 코사인 유사도를 반환합니다.
    return dot / (left_norm * right_norm)
# 실습 루트와 모델 정보를 출력합니다.
print(f"검색 실습 폴더: {RETRIEVAL_ROOT}")
# 결과 저장 위치를 출력합니다.
print(f"결과 저장 폴더: {OUTPUT_DIR}")
# 임베딩 모델을 출력합니다.
print(f"임베딩 모델: {EMBEDDING_MODEL}")
# Responses 모델을 출력합니다.
print(f"Responses 모델: {RESPONSES_MODEL}")

# 설정 파일이 없으면 선행 노트북 실행이 필요합니다.
if not CONFIG_PATH.exists():
    # 필요한 선행 노트북을 안내합니다.
    raise RuntimeError("02-04_vector_store_config.json이 없습니다. 02.files_vector_stores/files_vector_stores_v2.ipynb를 먼저 실행해 주세요.")
# 벡터 스토어 설정을 읽습니다.
config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
# file_search 도구에 전달할 벡터 스토어 ID를 꺼냅니다.
vector_store_id = config["vector_store_id"]
# 벡터 스토어 ID를 출력합니다.
print(f"vector_store_id: {vector_store_id}")
검색 실습 폴더: c:\Users\ai4nu\2026-ai-tech\AS.1.1_0601\07.embeddings_firesearch_retrieval
결과 저장 폴더: c:\Users\ai4nu\2026-ai-tech\AS.1.1_0601\07.embeddings_firesearch_retrieval\output\v2
임베딩 모델: text-embedding-3-small
Responses 모델: gpt-5-mini
vector_store_id: vs_6a1dcfe656dc8191a36c70645fd11414
 
 

2단계. file_search 도구로 답변 생성

Responses API에 tools=[{type: file_search, vector_store_ids: [...]}]를 넣으면 모델이 필요한 검색을 수행하고 답변을 생성합니다. 이 요청 구조를 직접 확인합니다.

# RAG 질문을 준비합니다.
question = "재택근무를 신청하려는 직원에게 필요한 조건과 제한을 한국어로 설명해 주세요."
# Responses API를 file_search 도구와 함께 직접 호출합니다.
rag_response = client.responses.create(
    # 답변 생성 모델을 지정합니다.
    model=RESPONSES_MODEL,
    # 모델의 답변 스타일을 지정합니다.
    instructions=(
        "당신은 사내 정책을 설명하는 교육용 어시스턴트입니다. "
        "검색한 문서에 근거해 짧고 명확하게 답하세요."
    ),
    # 사용자 질문을 입력합니다.
    input=[{"role": "user", "content": question}],
    # file_search 도구와 벡터 스토어 ID를 직접 전달합니다.
    tools=[{"type": "file_search", "vector_store_ids": [vector_store_id]}],
    # 반드시 검색 도구를 사용하게 합니다.
    tool_choice="required",
    # 답변이 잘리지 않도록 출력 토큰 한도를 둡니다.
    max_output_tokens=1500,
)
# 최종 답변 텍스트를 읽습니다.
answer = rag_response.output_text
# 답변을 텍스트 파일로 저장합니다.
(OUTPUT_DIR / "04-02_rag_basic_answer.txt").write_text(answer + "\n", encoding="utf-8")
# 답변을 출력합니다.
print(answer)
요청하신 재택근무 신청 관련 주요 조건과 제한은 다음과 같습니다.

- 자격(필수): 보안 교육을 이수하고 VPN 접근 권한을 받은 직원만 신청할 수 있습니다.   
- 이용 기간 제한: 재택근무는 월 최대 8일까지 사용할 수 있습니다.   
- 신청/승인 절차(참고): 사내 연차는 사내 포털에서 최소 3일 전에 신청해야 하며, 긴급한 경우 팀장 승인 후 당일 신청이 가능합니다. (재택근무 절차가 별도 규정되어 있지 않다면 유사한 절차를 따를 수 있습니다.) 

추가로 회사 규정(보안·장비·업무보고 등)이 별도로 있을 수 있으니 재택근무 신청 전 관련 부서(IT/인사) 안내문을 확인하거나 팀장에게 문의하시기 바랍니다.
 

 

3단계. file_search 결과 include

검색 결과를 답변과 함께 확인하려면 include=['file_search_call.results']를 사용합니다. 이 정보는 모델이 어떤 근거를 보고 답했는지 검토할 때 중요합니다.

# 검색 결과까지 포함해서 받을 질문을 준비합니다.
question_with_results = "휴가 신청은 언제까지 해야 하나요? 관련 근거도 함께 요약해 주세요."
# 검색 결과 include 옵션을 넣어 Responses API를 호출합니다.
result_response = client.responses.create(
    # 답변 생성 모델을 지정합니다.
    model=RESPONSES_MODEL,
    # 정책 근거 중심 답변을 지시합니다.
    instructions="검색 결과를 바탕으로 근거와 결론을 분리해 답하세요.",
    # 사용자 질문을 전달합니다.
    input=[{"role": "user", "content": question_with_results}],
    # file_search 도구를 직접 전달합니다.
    tools=[{"type": "file_search", "vector_store_ids": [vector_store_id]}],
    # 반드시 검색하게 합니다.
    tool_choice="required",
    # 검색 결과 세부 내용을 응답에 포함합니다.
    include=["file_search_call.results"],
    # 출력 토큰 한도를 둡니다.
    max_output_tokens=1100,
)
# file_search 호출 정보를 담을 리스트를 만듭니다.
file_search_calls = []
# 응답 output 배열을 순회합니다.
for item in result_response.output:
    # output item의 type을 확인합니다.
    if getattr(item, "type", None) == "file_search_call":
        # SDK 객체를 dict로 변환해 저장합니다.
        file_search_calls.append(
            item.model_dump() if hasattr(item, "model_dump") else dict(item)
        )
# 답변과 검색 호출 정보를 저장합니다.
(OUTPUT_DIR / "04-03_rag_with_results.json").write_text(
    json.dumps(
        {
            "answer": result_response.output_text,
            "file_search_calls": file_search_calls,
        },
        ensure_ascii=False,
        indent=2,
    )
    + "\n",
    encoding="utf-8",
)
# 답변을 출력합니다.
print(result_response.output_text)
# 검색 호출 수를 출력합니다.
print(f"file_search_call 수: {len(file_search_calls)}")
결론
- 연차 휴가는 사내 포털을 통해 최소 3일 전까지 신청해야 합니다.
- 긴급 휴가는 팀장 승인 후 당일 신청이 가능합니다.

근거 요약
- HR 휴가 규정 문서에는 "연차 휴가는 사내 포털에서 최소 3일 전에 신청합니다"라고 명시되어 있으며, 긴급한 경우에는 "팀장 승인 후 당일 신청할 수 있습니다"라고 되어 있습니다 .

원하시면 포털에서 신청하는 구체적 절차(메뉴 경로, 양식 기재 항목 등)나 예외 처리 방법도 알려드릴까요?
file_search_call 수: 1
 

 

4단계. previous_response_id로 후속 질문

후속 질문은 이전 응답 ID를 넘겨 같은 대화 흐름을 이어갈 수 있습니다. 검색 기반 답변에서도 맥락을 이어가는 방식이 어떻게 보이는지 확인합니다.

# 후속 질문을 준비합니다.
followup = "그 조건을 직원 안내문 형식으로 3줄로 다시 정리해 주세요."
# 이전 RAG 응답 ID를 사용해 후속 질문을 보냅니다.
followup_response = client.responses.create(
    # 같은 답변 생성 모델을 사용합니다.
    model=RESPONSES_MODEL,
    # 이전 응답의 맥락을 이어받습니다.
    previous_response_id=rag_response.id,
    # 후속 질문 텍스트를 전달합니다.
    input=followup,
    # 필요하면 다시 검색할 수 있도록 같은 file_search 도구를 전달합니다.
    tools=[{"type": "file_search", "vector_store_ids": [vector_store_id]}],
    # 후속 질문의 추론 노력을 낮게 설정합니다.
    reasoning={"effort": "low"},
    # 후속 답변의 토큰 한도를 둡니다.
    max_output_tokens=800,
)
# 후속 답변을 읽습니다.
followup_answer = followup_response.output_text
# 후속 답변을 저장합니다.
(OUTPUT_DIR / "04-04_rag_followup_answer.txt").write_text(
    followup_answer + "\n",
    encoding="utf-8",
)
# 후속 답변을 출력합니다.
print(followup_answer)
재택근무 신청은 보안 교육 이수 및 VPN 접근 권한이 있는 직원만 가능합니다.   
재택근무 사용은 월 최대 8일로 제한됩니다.   
신청은 사내 포털을 통해 사전 신청(긴급 시 팀장 승인으로 당일 가능)을 권장합니다.