12. OpenAI Assistants API로 AI 비서 만들기
이 장에서는 OpenAI의 강력한 Assistants API를 사용하여 한 단계 더 발전된 AI 애플리케이션을 구축합니다. 단순한 일회성 질의응답을 넘어, 대화의 맥락을 기억하고, 외부 도구를 사용하여 정보를 가져오거나 작업을 수행하며, 심지어 사용자가 제공한 파일을 읽고 그 내용을 기반으로 답변하는 ‘AI 비서’를 만드는 전체 과정을 학습합니다.
이 장에서 배울 내용
- Assistants API의 핵심 개념: Assistant, Thread, Message, Run 등 Assistants API를 구성하는 핵심 객체들의 역할과 관계를 이해합니다.
- Tool-Using Assistant: AI 비서가 웹 검색, 데이터베이스 조회 등 외부 세계와 상호작용할 수 있도록 ‘도구(Tool)’를 정의하고 연결하는 방법을 배웁니다.
- 상태 관리(Stateful Conversation):
Thread를 사용하여 여러 차례의 질문과 답변에 걸쳐 대화의 맥락을 유지하는 방법을 익힙니다. - 비동기 처리 및 Function Calling: AI가 도구 사용을 요청할 때(
requires_action), 해당 함수를 실행하고 그 결과를 다시 AI에게 전달하는 비동기 상호작용 루프를 구현합니다. - RAG (검색 증강 생성): AI 비서에게 파일을 업로드하여 그 내용을 학습시키고, 파일 기반의 질문에 대해 출처를 인용하며 답변하는 RAG(Retrieval-Augmented Generation) 기능을 구현합니다.
첫 번째 AI 비서(Assistant) 생성하기
가장 먼저, AI 비서의 정체성, 지침, 그리고 능력을 정의하는 Assistant 객체를 생성합니다. 이는 우리가 만들 AI 에이전트의 청사진과 같습니다.
학습 목표
- OpenAI Assistants API의 기본 개념과 구성 요소(Assistant, Thread, Message, Run) 이해하기
- AI 비서가 사용할 수 있는 ‘도구(Tool)’를 JSON 스키마 형식으로 정의하는 방법 배우기
openai라이브러리를 사용하여 새로운 Assistant를 생성하고 설정하는 방법 익히기
변경된 파일: notebook.ipynb
이번 장의 모든 개발 과정은 notebook.ipynb 파일 내에서 진행됩니다.
단계 1: OpenAI Assistants API 소개
Assistants API는 대화형 AI를 더 쉽고 강력하게 만들 수 있도록 설계되었습니다. 주요 구성 요소는 다음과 같습니다.
- Assistant: 특정 목적(예: 투자 분석)을 위해 설정된 AI. 지침, 모델, 사용할 도구 등을 가집니다.
- Thread: 사용자와 Assistant 간의 대화 세션입니다. 하나의 대화와 관련된 모든 메시지를 담습니다.
- Message: Thread에 포함된 각 대화 내용입니다. 사용자 또는 Assistant가 생성할 수 있습니다.
- Run: Assistant가 Thread를 읽고 대화를 진행시키는 작업 단위입니다. Assistant는 메시지를 읽고, 도구를 사용할지, 아니면 텍스트로 답변할지 결정합니다.
단계 2: 도구(Tool) 정의하기
Assistant가 웹 검색이나 데이터 조회 같은 특정 작업을 수행하게 하려면, 먼저 어떤 도구를 사용할 수 있는지 알려줘야 합니다. 이는 두 부분으로 구성됩니다.
실제 함수 구현: 작업을 수행하는 Python 함수를 만듭니다. 여기서는 DuckDuckGo 검색을 통해 회사의 티커 심볼을 찾는
get_ticker함수를 예시로 정의합니다.1
2
3
4
5
6from langchain.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper
def get_ticker(inputs):
ddg = DuckDuckGoSearchAPIWrapper()
company_name = inputs["company_name"]
return ddg.run(f"Ticker symbol of {company_name}")함수 스키마 정의: AI 모델이 함수를 이해할 수 있도록, 함수의 이름, 설명, 필요한 인자 등을 정해진 JSON 형식으로 설명해 줍니다. 아직 구현되지 않은 함수들도 미리 스키마로 정의하여 Assistant에게 능력을 알려줄 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20functions = [
{
"type": "function",
"function": {
"name": "get_ticker",
"description": "Given the name of a company returns its ticker symbol",
"parameters": {
"type": "object",
"properties": {
"company_name": {
"type": "string",
"description": "The name of the company",
}
},
"required": ["company_name"],
},
},
},
# get_income_statement, get_balance_sheet 등 다른 함수 스키마들...
]
단계 3: Assistant 생성하기
이제 openai 클라이언트를 사용하여 위에서 정의한 내용을 바탕으로 Assistant를 생성합니다.
1 | import openai as client |
name: Assistant의 이름입니다.instructions: Assistant의 역할과 행동 지침을 정의하는 가장 중요한 부분입니다. 일종의 시스템 프롬프트입니다.model: 사용할 GPT 모델을 지정합니다.tools: 위에서 정의한 함수 스키마 리스트를 전달하여 Assistant가 사용할 수 있는 도구들을 알려줍니다.
단계 4: 생성 결과
이 코드를 실행하면 OpenAI 서버에 Investor Assistant라는 설정값을 가진 Assistant 객체가 영구적으로 생성됩니다. 이 Assistant는 고유한 ID를 가지게 되며, 우리는 앞으로 이 ID를 사용하여 Assistant를 호출하고 대화를 나눌 수 있습니다.
체크리스트
- Assistant, Thread, Message, Run의 기본 개념을 이해했나요?
- Assistant에게 ‘도구’를 제공하는 것이 어떤 의미인지 이해했나요?
- 함수의 기능을 설명하는 JSON 스키마를 작성할 수 있나요?
-
client.beta.assistants.create를 사용하여 새로운 Assistant를 만들 수 있나요?
AI 비서의 도구 상자 채우기
이전 단계에서 AI 비서가 무엇을 할 수 있는지 ‘설명’했다면, 이제는 그 설명을 실제로 수행할 Python 코드를 구현할 차례입니다. yfinance 라이브러리를 사용하여 주식 시장 데이터를 가져오는 실제 도구 함수들을 만듭니다.
학습 목표
yfinance라이브러리를 사용하여 주식 티커로 실제 재무 데이터를 가져오는 방법 배우기- 도구 스키마에 정의된 함수들을 실제 Python 코드로 구현하는 방법 익히기
- Pandas DataFrame을 AI가 처리할 수 있는 JSON 문자열로 변환하는 이유와 방법 이해하기
변경된 파일: notebook.ipynb
1 | # notebook.ipynb |
단계 1: yfinance 라이브러리 소개
yfinance는 Yahoo! Finance에서 제공하는 주식 시장 데이터를 파이썬으로 쉽게 다운로드할 수 있게 해주는 매우 유용한 라이브러리입니다.
단계 2: 재무 데이터 조회 함수 구현
이전 단계에서 스키마로만 정의했던 함수들의 실제 내용을 yfinance를 사용하여 구현합니다.
get_income_statement(inputs):yfinance.Ticker(ticker)로 특정 주식 객체를 가져온 뒤,.income_stmt속성을 통해 손익계산서 정보를 가져옵니다.get_balance_sheet(inputs): 같은 방식으로.balance_sheet속성을 통해 대차대조표 정보를 가져옵니다.get_daily_stock_performance(inputs):.history(period="3mo")메소드를 호출하여 최근 3개월간의 주가 데이터를 가져옵니다.
단계 3: 데이터를 문자열로 반환하기
Assistant API의 도구 함수가 반환하는 값은 반드시 **문자열(string)**이어야 합니다. yfinance가 반환하는 데이터는 주로 Pandas DataFrame이라는 복잡한 테이블 형태의 객체이므로, 이를 그대로 반환할 수 없습니다.
따라서 .to_json() 메소드로 DataFrame을 JSON 형태의 문자열로 변환하고, json.dumps()를 한 번 더 사용하여 전체를 완전한 문자열로 만들어 반환합니다. 이렇게 해야 AI Assistant가 그 결과를 읽고 해석할 수 있습니다.
단계 4: 함수 맵핑
functions_map 딕셔너리는 나중에 AI가 “get_income_statement 함수를 호출해줘” 라고 요청했을 때, 문자열 이름("get_income_statement")을 실제 파이썬 함수(get_income_statement)와 연결해주는 역할을 합니다.
체크리스트
-
yfinance라이브러리로 주식 정보를 가져올 수 있나요? - 도구 함수의 반환값이 왜 반드시 문자열이어야 하는지 이해했나요?
- Pandas DataFrame을
.to_json()을 사용해 문자열로 변환할 수 있나요?
AI 비서와 대화 시작하기: Thread와 Run
Assistant를 만들었으니, 이제 실제로 대화를 시작해 봅니다. Assistants API에서는 하나의 대화 단위를 ‘Thread’라고 부르며, Assistant가 이 Thread를 읽고 응답을 생성하는 행위를 ‘Run’이라고 합니다.
학습 목표
- Assistants API의 핵심 객체인
Thread와Run의 개념과 역할을 이해하기 - 사용자 메시지를 포함하여 새로운 대화(
Thread)를 시작하는 방법 배우기 - 특정
Assistant가Thread를 처리하도록Run을 생성하고 실행하는 방법 익히기 Run의 비동기적 특성과 상태를 확인해야 하는 이유 이해하기
변경된 파일: notebook.ipynb
1 | # notebook.ipynb |
단계 1: 대화의 시작, Thread
Thread는 사용자와 Assistant 간의 대화 그 자체입니다. client.beta.threads.create()를 호출하여 새로운 대화를 시작할 수 있으며, messages 인자를 통해 사용자의 첫 질문을 포함시킬 수 있습니다.
단계 2: Assistant 실행, Run
Run은 특정 Assistant에게 특정 Thread를 처리하라고 지시하는 작업입니다. client.beta.threads.runs.create()를 호출하며, 어떤 Assistant(assistant_id)가 어떤 대화(thread_id)를 읽고 응답해야 하는지 알려줍니다.
단계 3: 비동기적 실행과 상태 확인
Run을 생성하면 API는 즉시 Run 객체를 반환하지만, Assistant가 응답을 생성하는 작업은 백그라운드에서 비동기적으로 수행됩니다. 따라서 우리는 Run의 상태를 주기적으로 확인하여 작업이 완료되었는지, 아니면 도구 사용과 같은 추가 조치가 필요한지 알아내야 합니다. get_run 함수는 바로 이 Run의 현재 상태를 가져오는 역할을 합니다.
단계 4: 대화 내용 가져오기
Run이 완료되면, Assistant는 자신의 응답을 Message 객체로 만들어 해당 Thread에 추가합니다. get_messages 함수는 특정 Thread의 모든 메시지 기록을 시간 순으로 가져와 보여줍니다.
노트북의 실행 결과를 보면, Assistant가 사용자의 질문을 이해하고, 자신이 가진 도구들을 활용하여 어떻게 답변을 할 것인지 체계적인 계획을 세워 사용자에게 먼저 제안하는 것을 볼 수 있습니다. 이는 Assistant가 자신의 instructions와 tools 정보를 바탕으로 스스로 추론한 결과입니다.
체크리스트
-
Thread가 대화 세션을 의미한다는 것을 이해했나요? -
Run이 Assistant를 특정 Thread에 대해 실행시키는 작업임을 이해했나요? -
Run이 비동기적으로 처리된다는 것의 의미를 알고 있나요? -
get_messages를 통해 대화의 전체 내용을 확인할 수 있나요?
AI 비서의 액션 실행하기: 도구 호출과 결과 제출
Assistant가 도구를 사용하겠다고 결정하면, Run의 상태는 requires_action으로 바뀝니다. 이는 우리 애플리케이션에게 “이제 네가 나설 차례야. 내가 요청한 함수를 실행하고 결과를 알려줘”라고 신호를 보내는 것과 같습니다. 이 단계에서는 그 신호를 받아 실제 도구를 실행하고 결과를 다시 Assistant에게 전달하는 과정을 구현합니다.
학습 목표
Run객체의requires_action상태의 의미를 이해하기- AI가 요청한 도구 호출(Tool Call) 정보를 파싱하는 방법 배우기
- 요청된 함수를 동적으로 실행하고, 그 결과를 다시 AI 비서에게 제출하는 전체 흐름 이해하기
변경된 파일: notebook.ipynb
1 | # notebook.ipynb |
단계 1: requires_action 상태
사용자가 “계속 진행해줘”라고 답하고 새로운 Run을 생성하면, Assistant는 계획에 따라 도구를 사용해야 한다고 판단합니다. 이때 Run의 상태는 completed가 아닌 requires_action이 됩니다. 이는 Assistant가 우리 쪽의 응답을 기다리며 일시정지했음을 의미합니다.
단계 2: 도구 호출(Tool Call) 정보 파싱
run.required_action.submit_tool_outputs.tool_calls 객체에는 Assistant가 호출하려는 함수들의 목록이 들어있습니다. get_tool_outputs 함수는 이 목록을 순회하며 각 함수 호출(action)에 필요한 정보를 추출합니다.
action.id: 각 도구 호출을 식별하는 고유 ID. 나중에 결과를 제출할 때 이 ID를 함께 보내야 합니다.action.function.name: 호출할 함수의 이름 (예:"get_ticker")action.function.arguments: 함수에 전달할 인자를 담은 JSON 문자열 (예:'{"company_name": "Salesforce"}')
단계 3: 도구 실행 및 결과 제출
get_tool_outputs 함수는 functions_map 딕셔너리를 사용하여 함수 이름에 해당하는 실제 Python 함수를 찾습니다. json.loads()를 사용해 인자 문자열을 Python 딕셔너리로 변환한 뒤, 함수를 실행합니다.
submit_tool_outputs 함수는 이렇게 실행된 모든 도구의 결과(outputs)를 모아, client.beta.threads.runs.submit_tool_outputs API를 호출하여 Assistant에게 다시 전달합니다. 이로써 상호작용의 한 사이클이 마무리됩니다.
결과를 제출받은 Assistant는 Run을 재개하고, 이제 도구로부터 얻은 실제 데이터를 바탕으로 사용자에게 최종 답변을 생성하여 Thread에 추가합니다.
단계 4: 전체 대화 흐름
이 커밋의 노트북 실행 결과는 이 모든 과정을 보여줍니다.
사용자 질문 → Assistant의 분석 계획 제시 → 사용자의 진행 승인 → Assistant의 도구 사용 요청 (requires_action) → 우리 코드의 도구 실행 및 결과 제출 → Assistant의 최종 답변 생성 (completed)
체크리스트
-
Run의requires_action상태가 무엇을 의미하는지 설명할 수 있나요? -
tool_calls객체에서 함수 이름과 인자 문자열을 어떻게 추출하는지 알고 있나요? -
json.loads()를 사용하여 JSON 문자열을 Python 딕셔너리로 변환할 수 있나요? -
submit_tool_outputs를 사용하여 도구 실행 결과를 Assistant에게 어떻게 전달하는지 이해했나요?
대화 이어가기 및 리팩토링
이번 단계에서는 하나의 대화(Thread) 안에서 여러 번 질문과 답변을 주고받으며 대화의 맥락을 이어가는 방법을 배우고, 코드의 가독성과 안정성을 높이기 위한 작은 리팩토링을 진행합니다.
학습 목표
- 하나의
Thread내에서 여러 차례 질문과 답변을 주고받으며 대화를 이어가는 방법 배우기 Run->requires_action->submit_tool_outputs->Run으로 이어지는 상호작용 루프를 반복하는 방법 이해하기- 코드의 작은 버그를 수정하고 워크플로우를 명확하게 만드는 리팩토링의 중요성 파악하기
변경된 파일: notebook.ipynb
단계 1: 버그 수정 및 코드 개선
이전 코드의 get_tool_outputs 함수 안에는 작은 버그가 있었습니다.
- 기존:
run = get_run(run_id, thread.id) - 수정:
run = get_run(run_id, thread_id)
함수 외부의 전역 변수 thread에 의존하던 것을, 함수에 인자로 전달된 thread_id를 사용하도록 수정했습니다. 이렇게 해야 이 함수를 다른 스레드에서도 재사용할 수 있는 독립적이고 안정적인 코드가 됩니다.
단계 2: 대화 이어가기
Assistant와의 대화를 이어가려면 어떻게 해야 할까요? 새로운 Thread를 만드는 대신, 기존 Thread에 새로운 사용자 메시지를 추가하면 됩니다.
1 | # 새로운 사용자 메시지를 기존 스레드에 추가 |
새로운 메시지를 추가한 뒤, 다시 client.beta.threads.runs.create를 호출하면 Assistant는 이전 대화 내용을 모두 기억한 상태에서 새로운 질문에 대한 응답을 시작합니다.
단계 3: 전체 대화 분석
노트북의 최종 실행 결과를 보면, “Salesforce”에 대한 분석이 끝난 뒤 사용자가 “Cloudflare는 어때?”라고 묻자, Assistant가 이전과 동일한 분석 프로세스(티커 검색, 재무제표 조회 등)를 Cloudflare에 대해 다시 수행하고 상세한 분석 결과를 제공하는 것을 볼 수 있습니다. 이는 Assistant가 Thread 내의 전체 대화 맥락을 이해하고 있음을 보여줍니다.
체크리스트
- 함수를 작성할 때 전역 변수 대신 인자를 사용해야 하는 이유를 이해했나요?
- 기존 대화에 새로운 메시지를 추가하는
send_message함수의 역할을 이해했나요? - 하나의
Thread에서 대화를 이어가기 위해 새로운Run을 생성해야 함을 알고 있나요?
RAG 비서 만들기: 파일 기반 지식으로 답변하기
지금까지 Assistant는 외부 함수(도구)를 호출하여 정보를 얻었습니다. 이번에는 완전히 다른 접근법인 **검색 증강 생성(Retrieval-Augmented Generation, RAG)**을 사용합니다. Assistant에게 파일을 업로드하여 새로운 지식을 학습시키고, 그 파일의 내용을 기반으로 질문에 답변하도록 만들어 봅니다.
학습 목표
- 검색 증강 생성(RAG)의 개념과 AI 비서에 파일을 업로드하여 지식을 확장하는 방법 이해하기
- Assistants API의 내장
retrieval도구를 활성화하는 방법 배우기 client.files.create를 사용하여 파일을 업로드하고, 메시지에file_ids를 포함하여 스레드에 연결하는 방법 익히기- AI가 파일 내용을 참조하여 답변하고, 그 출처(citation)를 표시하는 과정 확인하기
변경된 파일: notebook.ipynb
Investor Assistant 관련 코드는 모두 삭제되고, 파일 기반 질의응답을 위한 새로운 코드로 대체됩니다.
단계 1: RAG(Retrieval-Augmented Generation) 소개
RAG는 AI 모델이 답변을 생성할 때, 외부의 신뢰할 수 있는 지식 소스(예: 우리가 업로드한 문서)를 먼저 ‘검색(Retrieval)’하고, 검색된 관련 정보를 ‘근거’로 삼아 답변을 ‘생성(Generation)’하는 기술입니다. 이를 통해 모델의 지식에만 의존할 때 발생할 수 있는 환각(Hallucination) 현상을 줄이고, 특정 도메인에 대한 정확하고 상세한 답변을 제공할 수 있습니다.
단계 2: retrieval 도구 활성화
Assistant에 RAG 기능을 활성화하는 것은 매우 간단합니다. Assistant를 생성하거나 수정할 때, tools 목록에 {"type": "retrieval"}을 추가하기만 하면 됩니다.
1 | # Assistant 생성 시 retrieval 도구 활성화 |
단계 3: 파일 업로드 및 스레드에 연결
Assistant가 파일을 사용하게 하려면 두 단계가 필요합니다.
파일 업로드:
client.files.create를 사용하여 로컬 파일을 OpenAI 서버에 업로드합니다.purpose를"assistants"로 지정해야 합니다.1
2
3
4file = client.files.create(
file=client.file_from_path("./files/chapter_one.txt"),
purpose="assistants"
)스레드에 파일 연결: 사용자 메시지를 생성할 때
file_ids인자에 업로드된 파일의 ID를 리스트 형태로 전달합니다. 이렇게 하면 해당Thread내에서 Assistant가 이 파일을 참조할 수 있게 됩니다.1
2
3
4
5
6client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="", # 파일 첨부 시 content는 비워둘 수 있음
file_ids=[file.id]
)
단계 4: 답변 출처 확인 (Citations)
RAG의 가장 강력한 기능 중 하나는 답변의 근거가 된 원본 소스를 명확히 밝혀준다는 것입니다. Assistant가 retrieval 도구를 사용하여 답변을 생성하면, 응답 메시지의 annotations 필드에 인용(Citation) 정보가 포함됩니다.
get_messages 함수를 약간 수정하여 이 인용 정보를 함께 출력하도록 만들 수 있습니다.
1 | def get_messages(thread_id): |
노트북의 실행 결과를 보면, “윈스턴은 어디에 사나?”라는 질문에 Assistant가 업로드된 chapter_one.txt 파일의 내용을 정확히 인용(【12†source】)하며 답변하는 것을 볼 수 있습니다. 또한 get_messages를 통해 출력된 Source 정보를 보면, 어떤 파일(file_id)의 어떤 구절(quote)을 근거로 답변했는지 투명하게 확인할 수 있습니다.
체크리스트
- RAG가 무엇이며 왜 유용한지 설명할 수 있나요?
- Assistant에
retrieval도구를 어떻게 활성화하는지 알고 있나요? - 파일을 업로드하고 특정 대화(Thread)에 연결하는 과정을 이해했나요?
- Assistant의 답변에서 출처(Citation)를 확인하는 것이 왜 중요한지 이해했나요?