목차
- 왜 Assistants API를 선택했는가
- Assistants API의 구조 — Thread, Run, Tool
- File Search 설정 — 벡터스토어와 파일 업로드
- 질문하고 답변 받기
- Function Calling — RAG를 넘어서는 확장
- 비용 구조 — 생각보다 비싼 벡터스토어
- 실전 팁 — 공식 문서에 안 나오는 것들
- Assistants API를 쓸 때와 쓰지 말아야 할 때
$ python rag_assistant.py
Assistant created: asst_abc123
Vector store created: vs_xyz789
Uploading 12 files... done (43.2s)
File search ready. Asking question...
> 2024년 3분기 매출 데이터를 알려줘
매출 총액은 24억 3천만 원이며, 전분기 대비 12% 증가했습니다.
【출처: 2024_Q3_report.pdf, page 7】
위 터미널 출력이 OpenAI Assistants API로 만든 사내 문서 QA 봇의 실행 결과다. PDF 12개를 업로드하고, 질문하면 출처까지 달아서 답변한다. 여기까지 도달하는 데 걸린 코드가 80줄이 안 된다. 같은 기능을 LangChain + ChromaDB로 구현했을 때 250줄 넘게 썼던 걸 생각하면 체감 차이가 크다.
OpenAI Assistants API는 2024년 4월 v2로 업데이트되면서 File Search(구 Retrieval)와 Vector Store 개념이 도입됐다 (출처: OpenAI API Reference, Assistants v2 Migration Guide). 청킹, 임베딩, 벡터 인덱싱을 API가 알아서 처리해준다. RAG 파이프라인을 직접 구성할 필요가 없어진 거다.
왜 Assistants API를 선택했는가
원래 LangChain 0.2 + ChromaDB 조합으로 사내 문서 검색 시스템을 운영하고 있었다. 동작은 했지만 문제가 쌓였다. 문서가 추가될 때마다 임베딩 파이프라인을 돌려야 했고, 청킹 사이즈 조정할 때마다 전체 재인덱싱이 필요했다. ChromaDB 인스턴스 관리도 신경 써야 했다. 소규모 팀에서 이걸 유지보수하기엔 비용이 과했다.
대안으로 세 가지를 검토했다.
| 항목 | LangChain + ChromaDB | LlamaIndex | OpenAI Assistants API |
|---|---|---|---|
| 초기 구축 시간 | 2~3일 | 1~2일 | 반나절 |
| 벡터DB 관리 | 직접 (ChromaDB 인스턴스) | 직접 또는 managed | API가 자동 관리 |
| 청킹 커스터마이징 | 완전 자유 | 높음 | 제한적 (800토큰 고정) |
| 코드 복잡도 | 높음 (250줄+) | 중간 (150줄) | 낮음 (80줄) |
| 종속성 | openai, langchain, chromadb 등 | openai, llama-index 등 | openai 하나 |
| 월 비용 (50MB 문서 기준) | 서버비 + API 비용 | 서버비 + API 비용 | API 비용 + 스토리지 $0.15/일 |
LlamaIndex도 나쁘지 않았지만, 결국 벡터DB를 어딘가에 올려야 하는 건 마찬가지였다. "잘 되면 안 건드린다"가 원칙인데, 관리 포인트가 하나라도 줄어드는 쪽이 나았다. Assistants API는 openai 패키지 하나로 끝난다.
Assistants API의 구조 — Thread, Run, Tool
Assistants API의 핵심 개념은 세 가지다. Assistant, Thread, Run.
Assistant
Assistant는 설정 덩어리다. 어떤 모델을 쓸지(gpt-4o, gpt-4o-mini 등), 어떤 도구를 쓸지(File Search, Function Calling, Code Interpreter), 시스템 프롬프트는 뭔지를 정의한다. 한 번 만들면 여러 Thread에서 재사용 가능하다.
Thread와 Run
Thread는 대화 세션이다. 메시지를 Thread에 추가하고, Run을 실행하면 Assistant가 응답을 생성한다. Run은 상태 머신처럼 동작한다. queued → in_progress → completed 순서로 진행되는데, 함수 호출이 필요하면 중간에 requires_action 상태에서 멈춘다. 이 부분이 처음에 헷갈렸는데 뒤에서 자세히 다룬다.
도구 세 가지
Assistants API가 제공하는 도구는 File Search, Code Interpreter, Function Calling이다. 이 중 RAG에 핵심인 건 File Search와 Function Calling이다. Code Interpreter는 데이터 분석이나 차트 생성 용도라 이번 글에서는 다루지 않는다.
File Search 설정 — 벡터스토어와 파일 업로드
File Search가 Assistants API에서 RAG를 담당하는 도구다. 내부적으로 OpenAI가 벡터스토어를 생성하고, 업로드된 파일을 자동으로 청킹 → 임베딩 → 인덱싱한다.
from openai import OpenAI
client = OpenAI()
# 벡터스토어 생성
vector_store = client.vector_stores.create(
name="company-docs"
)
# 파일 업로드 및 벡터스토어에 연결
file_paths = ["docs/q3_report.pdf", "docs/hr_policy.pdf", "docs/api_spec.md"]
file_streams = [open(path, "rb") for path in file_paths]
# batch 업로드 — 파일이 많으면 이쪽이 빠르다
file_batch = client.vector_stores.file_batches.upload_and_poll(
vector_store_id=vector_store.id,
files=file_streams
)
print(file_batch.status) # completed
print(file_batch.file_counts) # FileCounts(completed=3, failed=0, ...)
upload_and_poll이 업로드와 인덱싱 완료까지 동기적으로 기다려준다. 파일 3개 기준 체감 20~30초 정도 걸렸다. PDF 12개일 때는 약 40초.
# Assistant 생성 — File Search 도구 연결
assistant = client.beta.assistants.create(
name="문서 검색 봇",
instructions="사내 문서를 검색해서 정확한 답변을 제공한다. 출처를 반드시 명시할 것.",
model="gpt-4o",
tools=[{"type": "file_search"}],
tool_resources={
"file_search": {
"vector_store_ids": [vector_store.id]
}
}
)
여기까지가 File Search 세팅의 전부다. 청킹 전략을 고를 수 없다는 게 단점이긴 한데, 대부분의 사내 문서 QA 용도에서는 기본 설정으로 충분했다. OpenAI 문서에 따르면 내부적으로 800토큰 단위 청킹에 앞뒤 400토큰 오버랩을 적용한다 (출처: OpenAI Assistants API 문서, "How it works" 섹션).
질문하고 답변 받기
Thread를 만들고 메시지를 보내면 된다.
# Thread 생성 및 질문
thread = client.beta.threads.create()
client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="2024년 3분기 매출이 얼마야?"
)
# Run 실행 및 대기
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=assistant.id
)
if run.status == "completed":
messages = client.beta.threads.messages.list(thread_id=thread.id)
# 가장 최근 메시지가 Assistant 응답
answer = messages.data[0].content[0].text.value
print(answer)
응답에 【4:0†source】 같은 출처 마커가 자동으로 붙는다. messages.data[0].content[0].text.annotations를 파싱하면 파일명과 인용 위치를 추출할 수 있다. 이게 생각보다 편하다. LangChain에서 RetrievalQA 쓸 때 출처 추적하려고 별도 로직 짰던 기억이 나는데, 여기선 기본 제공이다.
Function Calling — RAG를 넘어서는 확장
File Search만으로 해결 안 되는 경우가 있다. "오늘 환율 기준으로 매출을 달러로 환산해줘" 같은 질문이다. 이때 Function Calling을 쓴다. Assistant에게 "이런 함수를 호출할 수 있다"고 알려주면, 필요할 때 함수 호출을 요청한다.
함수 정의
tools = [
{"type": "file_search"},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "현재 환율을 조회한다",
"parameters": {
"type": "object",
"properties": {
"base_currency": {
"type": "string",
"description": "기준 통화 (예: KRW)"
},
"target_currency": {
"type": "string",
"description": "대상 통화 (예: USD)"
}
},
"required": ["base_currency", "target_currency"]
}
}
}
]
# Assistant 업데이트
assistant = client.beta.assistants.update(
assistant_id=assistant.id,
tools=tools
)
requires_action 처리 — 여기서 헤맸다
Function Calling을 붙이면 Run의 상태 흐름이 달라진다. in_progress에서 바로 completed로 가는 게 아니라, requires_action에서 멈춘다. 개발자가 함수를 실행하고 결과를 돌려줘야 Run이 계속 진행된다.
처음에 이 구조를 모르고 create_and_poll만 호출했더니 Run이 requires_action에서 영원히 안 끝났다. 상태 체크 로직을 직접 짜야 한다.
import json
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=assistant.id
)
# requires_action 상태 처리
if run.status == "requires_action":
tool_calls = run.required_action.submit_tool_outputs.tool_calls
tool_outputs = []
for tool_call in tool_calls:
if tool_call.function.name == "get_exchange_rate":
args = json.loads(tool_call.function.arguments)
# 실제 환율 API 호출 (여기선 예시)
rate = fetch_exchange_rate(args["base_currency"], args["target_currency"])
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": json.dumps({"rate": rate})
})
# 함수 실행 결과를 Run에 제출
run = client.beta.threads.runs.submit_tool_outputs_and_poll(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
한 가지 주의점이 있다. submit_tool_outputs를 호출할 때 Run이 requires_action 상태가 아니면 "Run is not in a state where tool outputs can be submitted" 에러가 난다. 배포 직후 이 에러를 만났는데, 원인은 타임아웃 설정 때문이었다. Run이 expired 상태로 넘어간 뒤에 submit을 시도한 거다. create_and_poll의 poll_interval_ms와 timeout 파라미터를 적절히 설정하면 해결된다.
비용 구조 — 생각보다 비싼 벡터스토어
Assistants API의 비용은 크게 세 가지다. 모델 사용료, 도구 사용료, 벡터스토어 저장 비용.
모델 사용료는 일반 Chat Completions API와 동일하다. gpt-4o 기준 input $2.50 / output $10.00 per 1M tokens (2025년 기준, 출처: OpenAI Pricing 페이지). 여기까진 예상 범위다.
문제는 벡터스토어 비용이다. GB당 하루 $0.10이 청구된다. 첫 1GB는 무료지만, 문서가 쌓이면 금방 넘는다. PDF 50MB 정도를 올려놓고 한 달 운영하면 스토리지 비용만 월 $3 정도 나온다. 금액 자체는 크지 않지만, 쓰지 않는 벡터스토어를 방치하면 계속 과금되니까 vector_stores.delete()로 정리하는 습관이 필요하다. 처음 한 달은 이걸 몰라서 테스트용 벡터스토어 5개가 그냥 돌아가고 있었다.
File Search 도구 호출 자체도 별도 비용이 있다. 호출당 $0.025인데, 이건 Thread당 누적이 아니라 Run당 과금이다. 자주 호출하는 서비스라면 무시 못 할 수준으로 올라갈 수 있다.
실전 팁 — 공식 문서에 안 나오는 것들
instructions가 생각보다 중요하다
File Search의 정확도는 임베딩 품질뿐 아니라 instructions에도 크게 좌우된다. "사내 문서를 검색해서 답변한다"처럼 뭉뚱그리면 관련 없는 청크를 끌어오는 경우가 잦았다. "재무 보고서와 인사 정책 문서에서 검색한다. 수치 데이터는 반드시 원문 그대로 인용한다"처럼 구체적으로 쓰니까 체감 정확도가 올라갔다.
Thread 재사용 vs 매번 생성
대화형 서비스라면 Thread를 유지하는 게 맞지만, 단발성 질문 시스템이라면 매번 새 Thread를 만드는 게 낫다. Thread에 메시지가 쌓이면 컨텍스트 윈도우를 소모해서 비용이 올라간다. 모든 이전 메시지가 모델에 들어가기 때문이다.
파일 형식별 차이
PDF, DOCX, MD, TXT 등을 지원하는데, 경험상 마크다운 파일의 검색 정확도가 가장 높았다. 구조화된 헤딩이 있으면 청킹 품질이 좋아지는 것으로 보인다. 반면 스캔된 PDF(이미지 기반)는 텍스트 추출이 안 돼서 검색이 거의 안 된다. OCR 처리를 먼저 해야 한다.
Assistants API를 쓸 때와 쓰지 말아야 할 때
Assistants API가 만능은 아니다. 구축 편의성과 커스터마이징 자유도는 트레이드오프 관계에 있다.
청킹 전략을 직접 제어해야 하는 경우, 예를 들어 코드 파일을 AST 기반으로 청킹한다거나, 테이블 구조를 보존하면서 청킹해야 하는 경우에는 LangChain이나 LlamaIndex가 맞다. Assistants API의 800토큰 고정 청킹은 이런 요구사항을 수용하지 못한다.
반대로, 50MB 이하의 일반 문서(보고서, 매뉴얼, 정책 문서)를 대상으로 빠르게 QA 시스템을 만들어야 한다면 Assistants API가 확실히 빠르다. 벡터DB 인프라를 신경 쓸 필요가 없고, 코드도 단순하다.
판단 기준을 정리하면 이렇다.
Assistants API를 쓰는 게 맞는 상황:
- 문서 규모가 작고(수십~수백 MB), 빠르게 프로토타입을 만들어야 할 때
- 팀에 ML 엔지니어가 없고, 백엔드 개발자가 RAG를 구현해야 할 때
- File Search + Function Calling 조합으로 충분한 요구사항일 때
다른 도구가 나은 상황:
- 청킹 전략을 세밀하게 제어해야 할 때
- 멀티모달 검색(이미지, 테이블)이 필요할 때
- 벡터DB를 자체 인프라에서 운영해야 하는 보안 요건이 있을 때
현재 사내 문서 QA 봇은 Assistants API v2 + gpt-4o-mini로 운영 중이다. gpt-4o 대신 gpt-4o-mini로 바꾼 건 비용 때문인데, 사내 문서 검색 정도의 난이도에서는 답변 품질 차이를 거의 못 느꼈다. 다음에는 Responses API(2025년 3월 출시)와의 비교를 해볼 생각이다. OpenAI가 Assistants API를 Responses API로 점진적 마이그레이션할 계획이라고 밝힌 만큼 (출처: OpenAI Blog, "New tools for building agents", 2025-03-11), 신규 프로젝트라면 Responses API도 검토 대상에 넣는 게 맞다.
관련 글
- ChatGPT API 비용 72% 줄인 실전 최적화 — 토큰 절약과 캐싱 전략 – GPT-4o와 gpt-4o-mini의 토큰당 단가 차이는 16배다. 이 차이를 활용한 모델 라우팅, 프롬프트 압축, 시멘틱 캐싱 세 가지 …
- ChatGPT GPTs 만들기 — Actions부터 배포까지 삽질 전 과정 – ChatGPT GPTs를 처음 만들 때 가장 많이 막히는 부분이 Actions 설정이다. OpenAPI 스키마 에러부터 CORS 문제까지,…
- Gemini 2.0 프로토타입 가이드 — Google AI Studio에서 데모 빠르게 만들기 – GPU 서버 없이 무료 티어만으로 Gemini 2.0 Flash 기반 문서 요약 데모를 3일 만에 만든 과정이다. Google AI Stu…