11. ChatGPT 플러그인 만들기 ChefGPT

이 장에서는 지금까지와는 다른 새로운 유형의 애플리케이션, 바로 ChatGPT 플러그인을 만들어 봅니다. FastAPI라는 강력한 Python 웹 프레임워크를 사용하여 API 서버를 구축하고, 이를 ChatGPT와 연동하여 특정 기능을 수행하는 ‘GPT Action’을 구현하는 전체 과정을 배우게 됩니다. 사용자가 입력한 재료에 따라 레시피를 추천해주는 “ChefGPT”를 만들면서 API 개발, 인증, 그리고 벡터 데이터베이스를 활용한 시맨틱 검색까지 다양한 기술을 경험할 것입니다.

이 장에서 배울 내용

  • FastAPI 기초: 현대적인 고성능 웹 프레임워크 FastAPI를 사용하여 API 서버를 구축하는 방법을 배웁니다.
  • Pydantic 데이터 모델링: API의 요청 및 응답 데이터 구조를 명확하게 정의하고 검증하는 방법을 익힙니다.
  • ChatGPT Action 연동: 우리가 만든 API를 ChatGPT 플러그인으로 등록하고 연동하는 과정을 이해합니다.
  • OAuth2 인증: 사용자를 대신하여 안전하게 API를 호출할 수 있도록 OAuth2 인증 흐름을 구현합니다.
  • Pinecone 벡터 데이터베이스: 키워드 검색을 넘어, 문맥과 의미를 이해하는 시맨틱 검색을 위해 Pinecone 벡터 데이터베이스를 연동하고 활용하는 방법을 배웁니다.

FastAPI로 첫 API 서버 만들기

가장 먼저, 우리의 ChefGPT 플러그인의 핵심이 될 API 서버를 구축합니다. Python 웹 프레임워크 중에서도 간결함과 높은 성능, 그리고 자동 API 문서 생성 기능으로 주목받는 FastAPI를 사용합니다.

학습 목표

  • FastAPI가 무엇이며 왜 사용하는지 이해하기
  • 기본적인 FastAPI 애플리케이션을 생성하고 실행하는 방법 배우기
  • Pydantic BaseModel을 사용하여 데이터 모델을 정의하는 방법 익히기
  • @app.get 데코레이터를 사용하여 API 엔드포인트를 만드는 방법 배우기

단계 1: FastAPI와 Pydantic 소개

FastAPI는 이름에서 알 수 있듯 매우 빠른(Fast) API 개발을 지원하는 현대적인 웹 프레임워크입니다. Python 3.7+의 타입 힌트(Type Hint)를 적극적으로 활용하여 코드의 가독성과 안정성을 높입니다.

Pydantic은 FastAPI와 환상의 짝을 이루는 데이터 유효성 검사 라이브러리입니다. Pydantic의 BaseModel을 상속받아 클래스를 정의하면, API가 받아들이거나 반환할 데이터의 형태(스키마)를 명확하게 정의하고, 들어오는 데이터가 이 형태에 맞는지 자동으로 검증해줍니다.

단계 2: FastAPI 앱 초기화

main.py 파일을 생성하고 다음과 같이 FastAPI 애플리케이션을 초기화합니다.

1
2
3
4
5
6
7
8
9
# main.py

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
title="Nicolacus Maximus Quote Giver",
description="Get a real quote said by Nicolacus Maximus himself.",
)
  • FastAPI()를 호출하여 app이라는 FastAPI 인스턴스를 생성합니다.
  • titledescription 매개변수는 나중에 자동으로 생성될 API 문서에 표시될 정보입니다. 처음에는 간단한 “명언 제공기”로 시작합니다.

단계 3: 데이터 모델 정의 (Quote)

Pydantic을 사용하여 API가 반환할 데이터의 구조를 정의합니다.

1
2
3
4
5
6
7
8
9
# main.py (이어서)

class Quote(BaseModel):
quote: str = Field(
description="The quote that Nicolacus Maximus said.",
)
year: int = Field(
description="The year when Nicolacus Maximus said the quote.",
)
  • BaseModel을 상속받는 Quote 클래스를 만듭니다.
  • quote는 문자열(str), year는 정수(int) 타입임을 명시합니다.
  • Field를 사용하면 각 필드에 대한 추가적인 설명(description)을 달 수 있으며, 이 또한 API 문서에 반영됩니다.

단계 4: GET 엔드포인트 생성 (/quote)

이제 실제 API 요청을 처리할 엔드포인트(URL 경로)를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# main.py (이어서)

@app.get(
"/quote",
summary="Returns a random quote by Nicolacus Maximus",
description="Upon receiving a GET request this endpoint will return a real quiote said by Nicolacus Maximus himself.",
response_description="A Quote object that contains the quote said by Nicolacus Maximus and the date when the quote was said.",
response_model=Quote,
)
def get_quote():
return {
"quote": "Life is short so eat it all.",
"year": 1950,
}
  • @app.get("/quote"): 이 데코레이터는 get_quote 함수가 /quote 경로에 대한 GET 요청을 처리하도록 지정합니다.
  • summary, description, response_description: 모두 API 문서를 풍부하게 만들어주는 메타데이터입니다.
  • response_model=Quote: 매우 중요한 부분입니다. 이 매개변수는 이 엔드포인트의 응답이 Quote 모델의 구조를 따라야 함을 FastAPI에 알려줍니다. FastAPI는 이를 통해 응답 데이터의 유효성을 검사하고, API 문서에 정확한 응답 스키마를 명시할 수 있습니다.
  • get_quote 함수는 Quote 모델과 일치하는 딕셔너리를 반환합니다.

단계 5: API 실행 및 확인

API 서버를 실행하려면, 프로젝트 루트 디렉토리에서 터미널에 다음 명령어를 입력합니다.

1
uvicorn main:app --reload
  • uvicorn: FastAPI를 위한 고성능 ASGI(Asynchronous Server Gateway Interface) 서버입니다.
  • main:app: main.py 파일 안에 있는 app 인스턴스를 실행하라는 의미입니다.
  • --reload: 코드에 변경이 생길 때마다 서버를 자동으로 재시작해주는 편리한 옵션입니다.

서버가 실행되면 웹 브라우저에서 http://127.0.0.1:8000/docs로 접속해 보세요. FastAPI가 코드와 Pydantic 모델을 기반으로 생성한 멋진 대화형 API 문서(Swagger UI)를 확인할 수 있습니다. 여기서 직접 API를 테스트해볼 수도 있습니다.

체크리스트

  • FastAPI와 Pydantic의 역할을 이해했나요?
  • uvicorn을 사용하여 FastAPI 서버를 실행할 수 있나요?
  • Pydantic BaseModel로 데이터 구조를 정의할 수 있나요?
  • @app.get 데코레이터와 response_model의 의미를 이해했나요?
  • http://127.0.0.1:8000/docs에서 자동 생성된 API 문서를 확인했나요?

ChatGPT Action을 위한 서버 설정

로컬에서 실행되는 우리 API를 인터넷을 통해 ChatGPT가 접근할 수 있도록 만들어야 합니다. 이를 위해 API 문서에 우리 서버의 공개 주소를 알려주는 설정이 필요합니다.

학습 목표

  • ChatGPT 플러그인(Action)이 외부 API와 어떻게 통신하는지 이해하기
  • FastAPI에서 servers 설정을 추가하는 이유와 방법을 배우기
  • Cloudflare Tunnel과 같은 터널링 서비스의 역할을 이해하기

변경된 파일: main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# main.py

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
title="Nicolacus Maximus Quote Giver",
description="Get a real quote said by Nicolacus Maximus himself.",
servers=[
{
"url": "https://rage-adapter-gtk-wooden.trycloudflare.com",
},
],
)

# ... (이하 동일)

단계 1: ChatGPT Action과 API 연동의 원리

ChatGPT가 우리의 API를 ‘Action’으로 사용하려면, 인터넷에서 접속 가능한 공개 URL 주소가 필요합니다. 현재 우리가 uvicorn으로 실행한 서버 주소인 http://127.0.0.1:8000은 우리 컴퓨터 내부에서만 유효한 주소입니다.

단계 2: servers 설정 추가

FastAPI 앱을 초기화할 때 servers 매개변수를 추가하여 이 문제를 해결할 수 있습니다.

1
2
3
4
5
6
7
8
app = FastAPI(
# ...
servers=[
{
"url": "https://rage-adapter-gtk-wooden.trycloudflare.com",
},
],
)
  • servers 매개변수는 생성될 OpenAPI 명세에 API 서버의 공개 주소를 명시하는 역할을 합니다.
  • ChatGPT와 같은 클라이언트는 이 정보를 보고 실제 API를 호출할 주소를 알게 됩니다.

단계 3: Cloudflare Tunnel 소개

https://rage-adapter-gtk-wooden.trycloudflare.com와 같은 주소는 어떻게 얻을 수 있을까요? 바로 Cloudflare Tunnel이나 ngrok과 같은 터널링 서비스를 사용하면 됩니다. 이 서비스들은 로컬 컴퓨터에서 실행 중인 서버(예: localhost:8000)와 인터넷상의 공개 URL을 안전하게 연결해주는 ‘터널’을 만들어 줍니다.

이를 통해 우리는 복잡한 배포 과정 없이도 로컬 환경에서 개발 중인 API를 ChatGPT Action으로 테스트해 볼 수 있습니다.

체크리스트

  • 로컬 서버를 외부 인터넷에 노출시켜야 하는 이유를 이해했나요?
  • FastAPI의 servers 설정이 OpenAPI 명세에 어떤 영향을 미치는지 알고 있나요?
  • Cloudflare Tunnel이나 ngrok과 같은 터널링 서비스의 기본 개념을 이해했나요?

OpenAI를 위한 OpenAPI 확장

OpenAPI는 훌륭한 표준 명세이지만, 특정 플랫폼(여기서는 OpenAI)은 그들만의 추가적인 정보를 요구할 때가 있습니다. FastAPI는 openapi_extra 매개변수를 통해 이러한 플랫폼별 확장을 손쉽게 추가할 수 있도록 지원합니다.

학습 목표

  • OpenAPI 스키마를 확장하여 특정 플랫폼에 맞는 정보를 추가하는 방법 배우기
  • openapi_extra 매개변수의 사용법 이해하기
  • x-openai-isConsequential 필드의 의미와 중요성 파악하기

변경된 파일: main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# main.py

# ... (상단 생략)

@app.get(
"/quote",
summary="Returns a random quote by Nicolacus Maximus",
description="Upon receiving a GET request this endpoint will return a real quiote said by Nicolacus Maximus himself.",
response_description="A Quote object that contains the quote said by Nicolacus Maximus and the date when the quote was said.",
response_model=Quote,
openapi_extra={
"x-openai-isConsequential": False,
},
)
def get_quote():
# ... (함수 내용 동일)

단계 1: OpenAPI와 확장성

OpenAPI 명세는 x- 접두사를 사용하여 표준에 없는 사용자 정의 필드를 추가할 수 있도록 허용합니다. OpenAI는 이를 활용하여 GPT Action의 동작 방식을 제어하기 위한 몇 가지 필드를 정의했으며, x-openai-isConsequential이 그중 하나입니다.

단계 2: openapi_extra 사용하기

FastAPI에서는 각 엔드포인트 데코레이터( @app.get, @app.post 등)에 openapi_extra 매개변수를 전달하여, 생성될 OpenAPI 문서에 손쉽게 사용자 정의 필드를 주입할 수 있습니다.

단계 3: x-openai-isConsequential 필드의 의미

x-openai-isConsequential 필드는 해당 API 호출이 시스템 상태를 변경하는 등 중요한 결과를 초래하는지 여부를 OpenAI에 알려주는 중요한 플래그입니다.

  • False (기본값): 이 작업은 ‘안전하다’는 의미입니다. 주로 데이터를 읽기만 하는 GET 요청에 해당합니다. ChatGPT는 이 경우 사용자에게 매번 명시적인 확인을 받지 않고 자유롭게 API를 호출할 수 있습니다.
  • True: 이 작업은 중요한 결과를 초래할 수 있다는 의미입니다. 예를 들어, 데이터베이스에 새로운 데이터를 생성(Create), 수정(Update), 삭제(Delete)하거나 이메일을 보내는 등의 작업이 해당됩니다. ChatGPT는 이런 API를 호출하기 전에 반드시 사용자에게 “이 작업을 실행할까요?”라고 확인을 요청합니다.

우리의 /quote 엔드포인트는 단순히 하드코딩된 명언을 반환하는 읽기 전용 작업이므로, isConsequential 값을 False로 설정하는 것이 올바릅니다.

체크리스트

  • OpenAPI 명세에 플랫폼별 확장 필드를 추가해야 하는 이유를 이해했나요?
  • openapi_extra를 사용하여 특정 엔드포인트의 OpenAPI 문서를 수정할 수 있나요?
  • x-openai-isConsequential 플래그가 True일 때와 False일 때 ChatGPT의 동작이 어떻게 달라지는지 이해했나요?

API 요청 정보 확인하기: API Key 인증 준비

이제 API를 보호하기 위한 첫 단계로, API Key 기반 인증을 준비합니다. 이를 위해 먼저 클라이언트(ChatGPT)가 API를 호출할 때 어떤 정보를 보내는지 확인해야 합니다. FastAPI의 Request 객체를 사용하면 들어오는 모든 HTTP 요청의 세부 정보를 쉽게 들여다볼 수 있습니다.

학습 목표

  • FastAPI의 Request 객체를 사용하여 들어오는 HTTP 요청 정보에 접근하는 방법 배우기
  • API Key 인증의 기본 개념과 헤더의 역할 이해하기
  • request.headers를 통해 클라이언트가 보낸 헤더 정보를 확인하는 방법 익히기

변경된 파일: main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# main.py

from fastapi import FastAPI, Request # Request 추가
from pydantic import BaseModel, Field

# ... (app 초기화 부분 생략)
# ... (Quote 모델 생략)

@app.get(
# ... (데코레이터 매개변수 생략)
)
def get_quote(request: Request): # request: Request 인자 추가
print(request.headers) # 헤더 정보 출력
return {
"quote": "Life is short so eat it all.",
"year": 1950,
}

# ... (이하 생략)

단계 1: API 인증의 필요성

우리가 만든 API를 아무나 호출할 수 있다면 곤란하겠죠? 가장 간단하고 널리 쓰이는 방법 중 하나는 API Key를 발급하고, 이 키를 가진 클라이언트의 요청만 허용하는 것입니다.

단계 2: FastAPI의 Request 객체

FastAPI에서 들어오는 요청에 대한 모든 정보는 Request 객체에 담겨 있습니다. 엔드포인트 함수의 인자로 request: Request를 선언하면, FastAPI가 자동으로 해당 요청 객체를 주입해 줍니다.

단계 3: 요청 헤더(Request Headers) 확인

API Key는 보통 HTTP 요청의 **헤더(Header)**에 담겨 전달됩니다. Authorization 헤더나 X-API-Key 같은 사용자 정의 헤더가 주로 사용됩니다.

print(request.headers) 코드를 추가함으로써, 우리 API가 호출될 때 클라이언트가 보낸 모든 헤더 정보를 서버 로그에서 확인할 수 있습니다. 이 정보를 바탕으로 “ChatGPT가 API Key를 어떤 헤더에 담아 보내는구나”를 파악하고, 다음 단계에서 해당 헤더의 값을 읽어 인증 로직을 구현할 수 있습니다.

체크리스트

  • API를 보호하기 위해 인증이 왜 필요한지 이해했나요?
  • FastAPI 엔드포인트 함수에서 Request 객체를 어떻게 사용하는지 알고 있나요?
  • request.headers를 통해 요청 헤더를 확인할 수 있나요?
  • API Key가 주로 요청 헤더를 통해 전달된다는 사실을 이해했나요?

OAuth2를 이용한 사용자 인증 구현

API Key가 서비스 전체에 대한 접근을 제어한다면, OAuth2는 ‘사용자’를 대신하여 특정 작업을 수행할 수 있도록 허가(Authorization)를 받는 표준 방식입니다. 이를 통해 우리는 사용자별로 다른 데이터에 접근하거나 작업을 수행하는 등 훨씬 정교한 기능을 구현할 수 있습니다.

학습 목표

  • OAuth2 인증 흐름(Authorization Code Grant)의 기본 개념 이해하기
  • FastAPI를 사용하여 /authorize/token 엔드포인트를 구현하는 방법 배우기
  • ChatGPT Action에서 사용자 인증이 어떻게 동작하는지 파악하기

변경된 파일: main.py

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
31
32
33
34
35
36
37
38
39
40
41
42
43
# main.py

from typing import Any, Dict
from fastapi import Body, FastAPI, Form, Request
from fastapi.responses import HTMLResponse
# ... (기존 import 생략)

# ... (app, Quote 모델 생략)

@app.get(
# ... get_quote 엔드포인트 생략
)
def get_quote(request: Request):
# ...

# --- 신규 추가된 코드 ---

user_token_db = {"ABCDEF": "nico"}


@app.get(
"/authorize",
response_class=HTMLResponse,
)
def handle_authorize(client_id: str, redirect_uri: str, state: str):
return f"""
<html>
<head>
<title>Nicolacus Maximus Log In</title>
</head>
<body>
<h1>Log Into Nicolacus Maximus</h1>
<a href="{redirect_uri}?code=ABCDEF&state={state}">Authorize Nicolacus Maximus GPT</a>
</body>
</html>
"""


@app.post("/token")
def handle_token(code=Form(...)):
return {
"access_token": user_token_db[code],
}

단계 1: OAuth2 인증 흐름(Authorization Code Grant) 소개

ChatGPT Action에서 OAuth2는 다음과 같은 순서로 동작합니다.

  1. 사용자 인증 요청: ChatGPT가 사용자를 우리 서버의 /authorize URL로 보냅니다.
  2. 사용자 동의: 사용자는 우리 서버가 제공하는 페이지에서 로그인하고 “이 GPT가 내 계정 정보에 접근하는 것을 허용합니다”라고 동의 버튼을 클릭합니다.
  3. 임시 코드 발급: 우리 서버는 사용자를 ChatGPT가 지정한 redirect_uri로 다시 보내면서, 일회용 **임시 코드(Authorization Code)**를 함께 전달합니다.
  4. 토큰 교환 요청: ChatGPT는 백그라운드에서 이 임시 코드를 가지고 우리 서버의 /token 엔드포인트에 POST 요청을 보냅니다.
  5. 액세스 토큰 발급: 우리 서버는 임시 코드가 유효한지 확인하고, 해당 사용자를 위한 **액세스 토큰(Access Token)**을 발급하여 ChatGPT에 전달합니다.
  6. API 호출: 이후 ChatGPT는 이 액세스 토큰을 모든 API 요청 헤더에 포함하여 보냅니다. 우리 서버는 이 토큰을 보고 “아, 이 요청은 ‘nico’ 사용자를 대신해서 온 것이구나”라고 인식하고 처리합니다.

단계 2: 사용자 동의 화면 구현 (/authorize)

/authorize 엔드포인트는 사용자가 권한을 부여하는 화면을 보여줍니다.

  • response_class=HTMLResponse: 이 엔드포인트가 HTML을 반환함을 명시합니다.
  • client_id, redirect_uri, state: OAuth2 표준에 따라 ChatGPT가 전달하는 매개변수들입니다.
  • 함수는 동의 링크가 포함된 간단한 HTML 페이지를 반환합니다.
  • 가장 중요한 부분은 이 링크의 href 속성입니다. 사용자가 링크를 클릭하면, ChatGPT가 알려준 redirect_uri로 이동하면서, 우리 서버가 임시로 발급한 code=ABCDEF와 CSRF 공격 방지를 위한 state 값을 전달합니다.

단계 3: 액세스 토큰 교환 구현 (/token)

/token 엔드포인트는 ChatGPT가 임시 코드를 액세스 토큰으로 교환하기 위해 호출합니다.

  • @app.post("/token"): POST 요청을 처리합니다.
  • code=Form(...): 요청의 본문(body)에 form 데이터로 code가 포함되어 올 것을 기대합니다.
  • user_token_db: 실제로는 데이터베이스를 사용해야 하지만, 여기서는 간단한 딕셔너리를 사용하여 code “ABCDEF”가 오면 “nico”라는 액세스 토큰을 반환하도록 시뮬레이션합니다.

체크리스트

  • OAuth2의 목적이 ‘사용자를 대신하여’ API를 호출할 권한을 얻는 것임을 이해했나요?
  • /authorize 엔드포인트의 역할(사용자 동의 및 임시 코드 전달)을 이해했나요?
  • /token 엔드포인트의 역할(임시 코드를 액세스 토큰으로 교환)을 이해했나요?
  • 액세스 토큰이 이후의 API 호출에서 사용자를 식별하는 데 사용된다는 것을 알고 있나요?

OpenAPI 스키마 정리하기

우리가 만든 API의 기능 중에는 ChatGPT가 직접 사용할 ‘도구’도 있지만, OAuth2 인증처럼 그 도구를 사용하기 위한 ‘기반 인프라’도 있습니다. API 명세(스키마)에는 ‘도구’에 해당하는 엔드포인트만 노출하여 ChatGPT가 혼동하지 않도록 하는 것이 좋습니다.

학습 목표

  • API 스키마에 포함할 엔드포인트와 포함하지 않을 엔드포인트를 구분하는 이유 이해하기
  • FastAPI에서 include_in_schema=False를 사용하여 특정 엔드포인트를 OpenAPI 문서에서 숨기는 방법 배우기

변경된 파일: main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# main.py

# ... (상단 생략)

@app.get(
"/authorize",
response_class=HTMLResponse,
include_in_schema=False, # 스키마에서 제외
)
def handle_authorize(client_id: str, redirect_uri: str, state: str):
# ...

@app.post(
"/token",
include_in_schema=False, # 스키마에서 제외
)
def handle_token(code=Form(...)):
# ...

단계 1: 스키마의 역할

OpenAPI 스키마(openapi.json)는 ChatGPT에게 “네가 사용할 수 있는 도구(API) 목록은 이것들이야”라고 알려주는 메뉴판과 같습니다.

단계 2: 인증 인프라와 액션 엔드포인트의 구분

  • 액션 엔드포인트: AI가 실제 기능을 수행하기 위해 호출하는 API입니다. (예: /recipes) 이들은 스키마에 반드시 포함되어야 합니다.
  • 인증 인프라 엔드포인트: OAuth2 인증 과정 자체를 위해 시스템 간에 통신하는 API입니다. (예: /authorize, /token) 이들은 AI가 직접 사용하는 도구가 아니므로 스키마에서 제외하는 것이 좋습니다.

단계 3: include_in_schema=False 사용

FastAPI에서는 각 엔드포인트 데코레이터에 include_in_schema=False 옵션을 추가하는 것만으로 간단하게 해당 엔드포인트를 OpenAPI 스키마에서 제외할 수 있습니다.

이렇게 하면 ChatGPT는 오직 자신이 직접 호출해야 할 기능적인 API 엔드포인트 정보만 받게 되어, 더 깔끔하고 명확한 연동이 가능해집니다.

체크리스트

  • 액션 엔드포인트와 인증 인프라 엔드포인트의 차이를 이해했나요?
  • include_in_schema=False 옵션의 역할을 알고 있나요?
  • API 스키마를 깔끔하게 유지하는 것이 왜 중요한지 이해했나요?

레시피 검색을 위한 벡터 데이터베이스 연동

지금까지는 API가 고정된 값을 반환했지만, 이제부터는 실제 데이터를 다루어 봅니다. 특히, 단순한 키워드 검색을 넘어 문맥과 의미를 이해하여 검색하는 ‘시맨틱 검색’을 구현하기 위해 Pinecone이라는 벡터 데이터베이스를 사용합니다.

학습 목표

  • 벡터 데이터베이스와 시맨틱 검색(semantic search)의 기본 개념 이해하기
  • Pinecone을 사용하여 벡터 인덱스에 연결하는 방법 배우기
  • LangChain의 OpenAIEmbeddings를 사용하여 텍스트를 벡터로 변환하는 방법 익히기
  • similarity_search를 사용하여 벡터 인덱스에서 관련 문서를 찾는 방법 배우기

변경된 파일: notebook.ipynb

이번 단계의 작업은 API 코드를 직접 수정하기보다는, Jupyter Notebook(notebook.ipynb) 환경에서 새로운 데이터 백엔드를 어떻게 연동하고 테스트하는지 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# notebook.ipynb (새로운 내용)

import pinecone
import os
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone

pinecone.init(
api_key=os.getenv("PINECONE_API_KEY"),
environment="gcp-starter",
)

embeddings = OpenAIEmbeddings()

vector_store = Pinecone.from_existing_index(
"recipes",
embeddings,
)

docs = vector_store.similarity_search("tofu")

docs

단계 1: 시맨틱 검색(Semantic Search) 소개

사용자가 “두부”를 검색했을 때, 단순히 텍스트에 ‘두부’라는 단어가 포함된 결과만 보여주는 것은 한계가 있습니다. 시맨틱 검색은 ‘두부’라는 단어의 의미와 문맥을 이해하여, ‘순두부 찌개’나 ‘마파두부’처럼 직접적으로 ‘두부’라는 단어가 없더라도 관련된 레시피를 찾아주는 기술입니다.

  • 임베딩(Embeddings): 텍스트(단어, 문장)를 의미를 함축한 숫자 벡터(Vector)로 변환하는 기술입니다.
  • 벡터 데이터베이스(Vector Database): 이렇게 만들어진 벡터들을 저장하고, 특정 벡터와 ‘의미적으로 유사한’ 다른 벡터들을 매우 빠르게 찾아주는 특화된 데이터베이스입니다. Pinecone이 바로 여기에 해당합니다.

단계 2: Pinecone과 LangChain 설정

  • pinecone.init(...): 환경 변수(PINECONE_API_KEY)를 읽어 Pinecone 서비스에 연결합니다.
  • OpenAIEmbeddings(): LangChain 라이브러리가 제공하는 기능으로, OpenAI의 강력한 모델을 사용하여 텍스트를 고차원 벡터로 변환하는 역할을 합니다.

단계 3: 기존 인덱스에 연결

  • Pinecone.from_existing_index("recipes", embeddings): Pinecone에 미리 생성해 둔 “recipes”라는 이름의 벡터 인덱스에 연결합니다. 이 인덱스에는 recipes.csv 파일의 모든 레시피 정보가 이미 벡터 형태로 변환되어 저장되어 있다고 가정합니다. (실제 인덱스를 생성하고 데이터를 넣는 과정은 이 프로젝트의 범위를 벗어나지만, LangChain을 사용하면 비교적 쉽게 수행할 수 있습니다.)

단계 4: 유사도 검색 실행

  • vector_store.similarity_search("tofu"): 이 코드가 바로 시맨틱 검색의 핵심입니다.
    1. OpenAIEmbeddings 모델이 “tofu”라는 검색어를 벡터로 변환합니다.
    2. Pinecone 벡터 스토어는 이 검색어 벡터와 인덱스 안에 있는 모든 레시피 벡터들 간의 ‘유사도’를 계산합니다.
    3. 가장 유사도가 높은, 즉 의미적으로 가장 관련 있는 레시피 문서(docs)들을 찾아 반환합니다.

이 노트북 코드를 통해, 우리는 API의 백엔드에서 어떻게 시맨틱 검색이 이루어질 것인지 프로토타입을 만들고 테스트한 것입니다.

체크리스트

  • 시맨틱 검색이 키워드 검색과 어떻게 다른지 이해했나요?
  • 텍스트를 벡터로 변환하는 ‘임베딩’의 역할을 알고 있나요?
  • Pinecone과 같은 벡터 데이터베이스가 왜 필요한지 이해했나요?
  • similarity_search가 어떻게 동작하는지 설명할 수 있나요?

ChefGPT API 완성: 시맨틱 검색 기능 통합

이제 모든 조각을 맞출 시간입니다. 이전에서 프로토타이핑했던 Pinecone 벡터 검색 기능을 실제 FastAPI 애플리케이션에 통합하여, 사용자가 요청한 재료에 따라 동적으로 레시피를 찾아주는 완전한 “ChefGPT” API를 완성합니다.

학습 목표

  • Jupyter Notebook에서 프로토타이핑한 코드를 실제 FastAPI 애플리케이션에 통합하는 방법 배우기
  • API 엔드포인트가 사용자 입력을 받아 LangChain과 벡터 데이터베이스를 사용하여 동적 응답을 생성하는 과정 이해하기
  • API의 목적에 맞게 문서, 데이터 모델, 엔드포인트 로직을 전체적으로 수정하는 방법 익히기

변경된 파일: main.py

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# main.py

from typing import Any, Dict
from fastapi import Body, FastAPI, Form, Request
from fastapi.responses import HTMLResponse
from pydantic import BaseModel, Field
from dotenv import load_dotenv
import pinecone
import os
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone

load_dotenv()

pinecone.init(
api_key=os.getenv("PINECONE_API_KEY"),
environment="gcp-starter",
)

embeddings = OpenAIEmbeddings()

vector_store = Pinecone.from_existing_index(
"recipes",
embeddings,
)

app = FastAPI(
title="CheftGPT. The best provider of Indian Recipes in the world.",
description="Give ChefGPT the name of an ingredient and it will give you multiple recipes to use that ingredient on in return.",
servers=[
{
"url": "https://occupations-partition-governments-analyzed.trycloudflare.com",
},
],
)

class Document(BaseModel):
page_content: str

@app.get(
"/recipes",
summary="Returns a list of recipes.",
description="Upon receiving an ingredient, this endpoint will return a list of recipes that contain that ingredient.",
response_description="A Document object that contains the recipe and preparation instructions",
response_model=list[Document],
openapi_extra={
"x-openai-isConsequential": False,
},
)
def get_recipe(ingredient: str):
docs = vector_store.similarity_search(ingredient)
return docs

# ... (OAuth 관련 엔드포인트 생략)

단계 1: 프로토타입에서 실제 앱으로

이전 단계의 notebook.ipynb에서 테스트했던 Pinecone 및 LangChain 관련 코드를 main.py 파일 상단으로 그대로 옮겨옵니다. 이제 API 서버가 시작될 때 벡터 데이터베이스에 연결됩니다.

단계 2: 환경 설정 및 초기화

  • from dotenv import load_dotenvload_dotenv()를 추가하여 .env 파일에 저장된 PINECONE_API_KEY와 같은 비밀 키들을 안전하게 불러옵니다. 코- 드에 API 키를 직접 하드코딩하는 것은 매우 위험한 습관이므로 절대 피해야 합니다.

단계 3: API 엔드포인트 재설계

기존의 /quote 엔드포인트를 레시피 검색 기능에 맞게 완전히 새롭게 설계합니다.

  • 경로 변경: @app.get("/quote") -> @app.get("/recipes")
  • 메타데이터 업데이트: title, description 등 API 문서 관련 내용을 모두 ChefGPT에 맞게 수정합니다.
  • 입력(Input): 함수 인자로 ingredient: str를 선언합니다. FastAPI는 이를 보고 /recipes?ingredient=두부 와 같이 쿼리 파라미터로 ingredient를 받아야 함을 자동으로 인식합니다.
  • 로직(Logic): 함수 본문이 docs = vector_store.similarity_search(ingredient)로 변경되었습니다. 이제 함수는 사용자로부터 받은 ingredient를 사용하여 직접 시맨틱 검색을 수행합니다.
  • 출력(Output):
    • response_modellist[Document]로 변경되었습니다. 이는 LangChain의 similarity_search가 반환하는 문서 리스트의 형식과 일치합니다.
    • Document 모델은 page_content라는 하나의 필드만 가지며, LangChain 문서 객체의 핵심 내용을 담습니다.
    • 함수는 검색 결과인 docs를 그대로 반환합니다. FastAPI는 response_model 정의에 따라 이 객체 리스트를 올바른 JSON 형식으로 변환하여 클라이언트에게 전달합니다.

단계 4: 완성된 ChefGPT

이제 우리의 API는 사용자가 재료 이름을 주면, Pinecone과 LangChain을 통해 의미적으로 관련된 레시피 목록을 찾아 동적으로 반환하는 완전한 기능을 갖춘 ChefGPT가 되었습니다.

체크리스트

  • .env 파일을 사용하여 API 키를 안전하게 관리하는 방법을 이해했나요?
  • 노트북에서 작성한 코드를 실제 애플리케이션에 어떻게 통합하는지 파악했나요?
  • API 엔드포인트가 쿼리 파라미터를 통해 사용자 입력을 어떻게 받는지 알고 있나요?
  • 사용자 입력을 받아 벡터 검색을 수행하고, 그 결과를 동적으로 반환하는 전체 흐름을 설명할 수 있나요?

출처 : https://nomadcoders.co/fullstack-gpt