프로젝트 개요서: AI 소믈리에 RAG 서비스


1. 프로젝트 개요

  • 프로젝트명:

    AI 소믈리에 RAG 서비스

  • 목적:

    음식 사진(이미지)와 한 줄 설명을 입력받아, AI가 음식의 특징을 분석하고, 대규모 와인 리뷰 데이터베이스(벡터DB, Pinecone)에서 음식과 어울리는 와인 정보를 검색(RAG, Retrieval-Augmented Generation)한 뒤 LLM(GPT-4o-mini)로 자연어 추천 결과를 생성해 사용자에게 제공하는 웹 서비스 구축

  • 기대효과:

    • 초개인화 와인 추천 경험 제공
    • LLM+RAG 기술(이미지→설명→벡터검색→생성) 전체 파이프라인 실습
    • 최신 생성형 AI/검색 증강/멀티모달 AI 실전 체험

2. 서비스 시나리오

1. 사용자는 음식 사진을 업로드하고 한 줄 설명을 입력
2. AI가 이미지를 분석(vision LLM: GPT-4o-mini)해 음식의 맛과 특징을 한 문장으로 요약
3. 음식 설명을 임베딩으로 변환해 Pinecone DB에서 유사한 와인 리뷰를 top-K로 검색
4. 검색된 리뷰와 음식 정보를 LLM(GPT-4o-mini)에 전달
5. LLM이 최적의 와인을 한글로 추천하고 그 이유를 자연어로 설명
6. UI에 추천 와인, 이유, 검색된 리뷰와 유사도 등 표시

3. 시스템 아키텍처

  • Frontend/UI:

    • Streamlit 기반 웹 UI
    • 이미지 업로드, 설명 입력, 결과 표시(리뷰, 유사도, 상세 추천)
  • Backend 핵심 모듈:

    • sommelier.py: 이미지 분석, 벡터DB 검색, LLM 프롬프트 처리
  • 외부 서비스:

    • OpenAI API: GPT-5-nano (LLM), text-embedding-3-small (임베딩)
    • Pinecone: 벡터 데이터베이스(와인 리뷰 임베딩 저장/검색)
  • 환경설정:

    • 환경 변수는 .env 파일로 관리

4. 주요 사용 기술 및 환경

  • 프로그래밍 언어:
    • Python 3.12+
  • 주요 라이브러리:

    • openai, langchain_openai, langchain_pinecone, pinecone, streamlit, python-dotenv, base64 등
  • 모델:

    • 텍스트 임베딩: text-embedding-3-small
    • LLM: gpt-4o-mini
  • 벡터DB:

    • Pinecone (us-east1-aws, cosine metric, dimension=1536)

5. 환경변수 및 설정 파일 예시

.env 파일 예시

OPENAI_API_KEY=sk-...        
OPENAI_LLM_MODEL=gpt-5-nano
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
PINECONE_API_KEY=pcsk_...
PINECONE_ENVIRONMENT=us-east1-aws
PINECONE_INDEX_REGION=us-east1
PINECONE_INDEX_CLOUD=aws
PINECONE_INDEX_NAME=wine-reviews
PINECONE_INDEX_DIMENSION=1536
PINECONE_INDEX_METRIC=cosine

※ 실제 환경에서는 API 키를 안전하게 보관하고, .env 파일은 절대 공개 저장소에 올리지 않습니다.


6. 핵심 코드 구조

(1) Streamlit app.py

  • 두 개 컬럼(입력/출력, 이미지 미리보기) 구성
  • 1단계: 이미지/설명 → 맛 분석(LLM Vision)
  • 2단계: 벡터DB에서 유사한 와인 리뷰(top-K, 유사도 점수 포함) 검색
  • 3단계: LLM을 통한 와인 상세 추천 결과 생성 및 출력

(2) sommelier.py

  • 환경 변수 로드 및 LLM/임베딩/벡터스토어 초기화
  • describe_dish_flavor:

    • 입력 이미지(base64)와 프롬프트로 음식 맛 설명(LLM Vision)
  • search_wine, search_wine_with_score:

    • 음식 설명으로 Pinecone에서 유사한 와인 리뷰 및 유사도 점수 검색
  • recommand_wine:

    • 음식 설명과 검색 리뷰를 context로 하여 GPT-4o-mini에 페어링 추천 프롬프트 작성 및 호출

7. 서비스 흐름 예시

  1. 사용자가 음식 이미지를 업로드하고 “이 요리에 어울리는 와인을 추천해주세요” 입력
  2. 시스템이 이미지를 Vision LLM 프롬프트에 포함시켜 맛/풍미 한 문장 생성
  3. 이 설명으로 Pinecone에 유사도 검색 → top-2 리뷰와 유사도 표시
  4. 이 결과와 함께 LLM에 “한글로 추천, 이유 설명” 프롬프트로 최종 추천 생성
  5. Streamlit UI에 분석 결과, 리뷰 목록, 상세 추천 결과 단계별 표시

8. 한계 및 확장 가능성

  • 실제 리뷰DB, LLM 모델, 임베딩 품질에 따라 추천 품질이 좌우됨
  • 확장 가능:

    • 추천 결과에 와인 라벨 이미지 추가
    • 사용자의 피드백/선호 학습 반영
    • 다양한 음식/와인 DB 추가 등

프로젝트 : AI 소믈리에 RAG 서비스


setp1

.env 파일

OPENAI_API_KEY = sk-p****
OPENAI_LLM_MODEL = gpt-5-nano
OPENAI_EMBEDDING_MODEL = text-embedding-3-small
PINECONE_API_KEY = pcsk_*******
PINECONE_ENVIRONMENT = us-east-1-aws
PINECONE_INDEX_REGION=us-east-1
PINECONE_INDEX_CLOUD=aws
PINECONE_INDEX_NAME=wine-reviews
PINECONE_INDEX_DIMENSION=1536
PINECONE_INDEX_METRIC=cosine

환경 변수 로드

from dotenv import load_dotenv

load_dotenv()

환경 변수 값 상수로 저장

import os

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_LLM_MODEL = os.getenv("OPENAI_LLM_MODEL")
OPENAI_EMBEDDING_MODEL = os.getenv("OPENAI_EMBEDDING_MODEL")
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")
PINECONE_INDEX_REGION = os.getenv("PINECONE_INDEX_REGION")
PINECONE_INDEX_CLOUD = os.getenv("PINECONE_INDEX_CLOUD")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")
PINECONE_INDEX_DIMENSION = os.getenv("PINECONE_INDEX_DIMENSION")
PINECONE_INDEX_METRIC = os.getenv("PINECONE_INDEX_METRIC")

pinecone 연결하고 index 생성

from pinecone import Pinecone, ServerlessSpec

pc = Pinecone(api_key=PINECONE_API_KEY)

if not pc.has_index(PINECONE_INDEX_NAME):
    pc.create_index(
        name=PINECONE_INDEX_NAME,
        dimension=1536,
        metric=PINECONE_INDEX_METRIC,
        spec=ServerlessSpec(
            region=PINECONE_INDEX_REGION,
            cloud=PINECONE_INDEX_CLOUD
        )
    )

생성한 인덱스 로드하고 정보 확인

wine_index = pc.Index(PINECONE_INDEX_NAME)
wine_index.describe_index_stats()

출력 결과

{'dimension': 1536,
 'index_fullness': 0.0,
 'metric': 'cosine',
 'namespaces': {},
 'total_vector_count': 0,
 'vector_type': 'dense'}

와인 데이터파일(csv) 로드

winemag-data-130k-v2.csv

from langchain_community.document_loaders import CSVLoader
loader = CSVLoader(file_path="winemag-data-130k-v2.csv",encoding="utf-8")
docs = loader.load()

print(docs[0])

전체 데이터 건수 및 데이터 중 가장 긴 길이 값 확인

print('전체 데이터 건수 :' ,len(docs))
print('데이터 중 가장 긴 길이 값 : ',max(len(doc.page_content) for doc in docs))
전체 데이터 건수 : 129971
데이터 중 가장 긴 길이 값 :  1115

OpenAI에서 제공하는 tiktoken 패키지를 사용해 토큰 수를 계산

tiktoken은 OpenAI가 만든 토크나이저(tokenizer) 라이브러리로, 텍스트를 LLM이 실제로 처리할 수 있는 “토큰 단위”로 변환하거나 반대로 토큰을 다시 문자열로 복원할 때 사용합니다.
GPT 모델의 토큰 계산, 비용 계산, 입력 제한 확인 등에 꼭 필요한 도구입니다.

import tiktoken

encoding = tiktoken.encoding_for_model("text-embedding-3-small")
text = "이 문장이 몇 개의 토큰으로 인코딩되는지 확인해보세요."
tokens = encoding.encode(text)
print('전체 토큰 : ',tokens)
print('토큰 수 : ',len(tokens))  # 토큰 수 출력
print('복원 : ' ,encoding.decode(tokens))

# 각 토큰이 실제로 어떤 문자열에 해당하는지 보기
# 한글은 글자단위로 토큰처리되지 않기 때문에 한글은 깨질수 있음.
for token in tokens:
    print(token, "→", encoding.decode([token]))

Embedding 모델 로드

from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
    model=OPENAI_EMBEDDING_MODEL,
    openai_api_key=OPENAI_API_KEY
)

129971건의 데이터를 300건씩 처리
문서의 길이가 가장 긴게 1115, 한번에 처리할 수 있는 단위(8192 tokens)보다 작으므로 문서를 쪼개지 않고 진행

from langchain_pinecone import PineconeVectorStore

BATCH_SIZE = 300

for i in range(0, len(docs), BATCH_SIZE):
    batch = docs[i:i + BATCH_SIZE]
    try:
        PineconeVectorStore.from_documents(
            documents=batch,
            index_name=PINECONE_INDEX_NAME,
            embedding=embeddings
        )

        print(f"Indexed documents {i} to {i + BATCH_SIZE - 1}")
    except Exception as e:
        print(f"Error indexing documents {i} to {i + BATCH_SIZE - 1}: {e}")
        

벡터DB에 질의 테스트

vector_store = PineconeVectorStore(
    index_name=PINECONE_INDEX_NAME,
    embedding=embeddings
)
query = "달콤한 맛을 가진 와인"
results = vector_store.similarity_search(query, k=5)
for result in results:
    print(f"Wine: \nDescription: {result.page_content}\n")

step2

LLM/EMBEDDING/VectorStore 객체 생성

from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore

import langchain
langchain.debug = False
langchain.llm_cache = None 

llm = ChatOpenAI( name=OPENAI_LLM_MODEL,verbose=True)

embeddings = OpenAIEmbeddings(model=OPENAI_EMBEDDING_MODEL)

vector_store = PineconeVectorStore(
  index_name=PINECONE_INDEX_NAME,
  embedding=embeddings,
  pinecone_api_key=PINECONE_API_KEY
)

와인(이미지)에 따른 어울리는 음식 추천 함수 작성

from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser

def recommand_dishs(query):
    prompt = ChatPromptTemplate.from_messages([
        ("system", '''
            Persona:
            소믈리에로서 저는 포도 품종, 산지, 테이스팅 노트, 음식 페어링을 포함한 와인에 대한 방대한 지식을 보유하고 있습니다. 개인의 취향, 특별한 행사, 특정 요리에 맞춰 와인을 추천하는 데 탁월한 능력을 갖추고 있습니다. 와인 생산 방법, 풍미 프로필, 그리고 다양한 음식과의 상호작용을 이해하는 것이 제 전문성입니다. 또한 와인계의 최신 트렌드를 꾸준히 파악하며 전통적이면서도 모험적인 와인을 제안할 수 있습니다. 식사 경험을 한층 풍요롭게 하기 위해 세심하고 맞춤형 추천을 제공하기 위해 노력합니다.

            Role:
            1. 와인 & 음식 페어링: 특정 요리와 조화를 이루며 풍미를 균형 있게 조율하고 전체적인 식사 경험을 향상시키는 상세한 와인 추천을 제공합니다. 간단한 안주든 정성스러운 식사든, 음식의 질감, 맛, 스타일을 보완하는 와인을 제안합니다.
            2. 와인 선택 안내: 다양한 행사(축하 자리, 공식 만찬, 캐주얼 모임)에 맞춰 행사 분위기와 참석자들의 취향에 부합하는 와인을 선정하는 데 도움을 드립니다.
            3. 와인 테이스팅 전문성: 산도, 타닌 함량, 단맛, 바디감과 같은 테이스팅 노트에 기반해 와인을 식별하는 데 도움을 드리며, 와인의 독특한 특징을 분석해 드립니다.
            4. 와인 용어 설명: 복잡한 와인 용어를 쉽게 풀어 설명하여 누구나 포도 품종, 산지, 테이스팅 프로필을 이해할 수 있도록 합니다.
            5. 교육적 역할: 다양한 와인 산지, 생산 기술, 와인 스타일에 대한 정보를 제공하고 교육함으로써, 시중에 나와 있는 와인의 다양성에 대한 이해를 높입니다.

            Examples:
            - 와인 페어링 예시 (요리 우선):
버터 갈릭 새우 구이 요리의 경우, 버터의 진한 풍미를 중화시키고 해산물 본연의 맛을 살려줄 상큼한 산미의 소비뇽 블랑이나 샤르도네를 추천합니다.
            - 와인 페어링 예시 (와인 우선):  
            카베르네 소비뇽을 즐기고 계시다면, 그 강렬한 타닌과 진한 과일 풍미가 그릴에 구운 스테이크나 양고기와 환상적인 조화를 이룹니다. 고기의 풍부한 맛이 와인의 강렬함을 더욱 돋보이게 합니다.
            - 와인 페어링 예시 (와인 먼저):
            가벼운 바디감과 붉은 베리의 은은한 풍미로 유명한 피노 누아는 구운 오리나 버섯 리조또와 함께 즐기기에 완벽합니다. 그 흙내음이 요리의 풍미를 더욱 돋보이게 하기 때문입니다.
            - 행사별 추천:
            로맨틱한 기념일 저녁을 축하하신다면, 특별한 친밀한 저녁에 어울리는 클래식한 샴페인이나 우아한 피노 누아를 추천합니다.
            - 취향에 따른 추천:
            강렬한 풍미와 진한 타닌을 가진 와인을 선호하신다면, 나파 밸리산 카베르네 소비뇽이 입맛에 딱 맞을 것입니다. 더 가볍고 과일 향이 풍부한 것을 원하신다면, 리슬링이 훌륭한 대안이 될 수 있으며 매콤한 요리나 신선한 샐러드와 잘 어울립니다.
        ''')
    ])
    template = [{"text": query["text"]}]
    if query["image_urls"]:
        template += [{"image_url": url} for url in query["image_urls"]]
    prompt += HumanMessagePromptTemplate.from_template(template)

    chain = prompt | llm | StrOutputParser()
    return chain

함수 호출 테스트

import base64
import requests

# ✅ 이미지 URL → Base64 변환 함수
def image_to_base64_url(image_url: str):
    response = requests.get(image_url)
    mime_type = response.headers.get("Content-Type", "image/png")
    base64_str = base64.b64encode(response.content).decode("utf-8")
    return f"data:{mime_type};base64,{base64_str}"

# ✅ 테스트 이미지 변환
img_url = "https://images.vivino.com/thumbs/Z90I3--JRKWlpMA8wdLY-Q_pb_x600.png"
b64_url = image_to_base64_url(img_url)

from langchain_core.runnables import RunnableLambda

runnable = RunnableLambda(recommand_dishs)
response = runnable.invoke({
    "text": "이 와인에 어울리는 요리에는 어떤 것들이 있을까?",
    "image_urls": [b64_url]
})

print(response)

요리(이미지)에 대한 풍미를 분석하는 함수

def describe_dish_flavor(query):
    prompt = ChatPromptTemplate.from_messages([
        ("system", '''
        Persona:
            맛 분석 시스템으로서 저는 식품 재료, 조리 방법, 그리고 맛, 식감, 향과 같은 감각적 특성에 대한 깊은 이해를 갖추고 있습니다. 주요 맛(단맛, 신맛, 짠맛, 쓴맛, 감칠맛)은 물론 매운 정도, 진한 맛, 신선함, 뒷맛과 같은 미묘한 요소까지 식별하여 요리의 풍미 프로필을 평가하고 분석할 수 있습니다. 재료와 조리법을 기반으로 다양한 음식을 비교할 수 있으며, 문화적 영향과 대표적인 조합도 고려합니다. 제 목표는 요리의 풍미 프로필을 상세히 분석하여 사용자가 그 독특함을 이해하거나 어울리는 음식과 음료를 선택하는 데 도움을 드리는 것입니다.

            Role:
            1. 풍미 분석: 요리의 주된 풍미와 보조 풍미를 분석하며, 단맛, 신맛, 쓴맛, 짠맛, 감칠맛, 향신료나 허브의 존재 여부 등 주요 맛 요소를 강조합니다.
            2. 질감과 향 분석: 맛을 넘어, 나는 요리의 입안에서의 느낌과 향을 평가하며, 질감(예: 크리미함, 바삭함)과 향(예: 스모키함, 꽃향기)이 전체적인 경험에 어떻게 기여하는지 고려합니다.
            3. 재료 분석: 각 재료가 요리의 풍미에 미치는 역할을 평가하며, 요리의 균형감, 풍성함 또는 강도에 미치는 영향도 포함합니다.
            4. 요리적 영향: 요리를 형성하는 문화적 또는 지역적 영향을 고려하며, 전통적인 조리법이나 독특한 재료가 전체적인 맛에 어떻게 영향을 미치는지 이해합니다.
            5. 음식과 음료 페어링: 요리의 풍미 프로필을 바탕으로, 요리의 특성을 강화하거나 균형을 잡아주는 보완적인 음식 또는 음료 페어링을 제안합니다.

            Examples:
            - 요리 풍미 분석 : 버터 갈릭 새우 요리의 경우, 버터의 진한 풍미, 마늘의 강렬한 향, 그리고 새우의 은은한 단맛을 느낄 수 있습니다. 이 요리는 진한 풍미와 살짝 느껴지는 짠맛이 조화를 이루며, 부드럽고 연한 새우의 식감은 그릴에 구워낸 약간의 바삭함으로 더욱 돋보입니다.
            - 질감과 향 분석 : 크리미한 버섯 리조또에는 크림 같은 육수와 버터 덕분에 부드럽고 벨벳 같은 질감이 느껴집니다. 버섯의 흙내음은 감칠맛을 더해주며, 파르메산 치즈를 살짝 뿌리면 은은한 짭짤함과 함께 풍미가 더해집니다.
            - 재료 역할 분석 : 매콤한 태국 카레에서 코코넛 밀크는 풍부하고 크리미한 베이스 역할을 하며, 레몬그라스와 라임은 신선함과 시트러스 풍미를 더합니다. 고추는 매운맛을 내고, 단맛, 신맛, 매운맛의 균형이 역동적인 풍미 프로필을 만들어냅니다.
            - 문화적 영향 : 전통적인 이탈리아식 마르게리타 피자는 신선한 토마토, 모짜렐라 치즈, 바질의 고전적인 조합을 바탕으로 합니다. 재료의 단순함은 각 재료의 풍미를 돋보이게 하며, 토마토 소스의 새콤함이 치즈의 진한 풍미와 바질의 신선함을 균형 있게 조화시킵니다.
            - 음식 페어링 예시 : 진한 초콜릿 케이크에는 초콜릿의 쓴맛을 보완해 줄 포트 와인과 같은 달콤한 디저트 와인을 추천합니다. 또는 가벼운 에스프레소를 곁들여 단맛과 대비를 이루고 디저트의 풍미를 한층 높여보세요.
''')
    ])
    template = [{"text": query["text"]}]
    if query["image_urls"]:
        template += [{"image_url": url} for url in query["image_urls"]]
    prompt += HumanMessagePromptTemplate.from_template(template)

    chain = prompt | llm | StrOutputParser()
    return chain

함수 테스트

runnable = RunnableLambda(describe_dish_flavor)
response = runnable.invoke({
    "text": "이 요리의 이름과 맛을 한 문장으로 요약해주세요",
    "image_urls": ["https://www.stockfood.com/Sites/StockFood/Documents/Homepage/News//en/16.jpg"]
})
print(respnse)

와인 맛에 따른 와인 검색

def search_wine(dish_flavor):
  results = vector_store.similarity_search(
    dish_flavor,
    k=2
  )
  return {
    'dish_flavor': dish_flavor,
    'wine_reviews': "\n\n".join(doc.page_content for doc in results)
  }

함수 테스트

runnables = RunnableLambda(search_wine)
response = runnables.invoke("달콤한 맛을 가진 와인")
print(response["dish_flavor"])
print(response["wine_reviews"])

요리의 풍미를 설명하고 그 풍미에 맞는 와인을 검색해서 추천

runnable1 = RunnableLambda(describe_dish_flavor)
runnable2 = RunnableLambda(search_wine)
chain = runnable1 | runnable2
response = chain.invoke({
    "text": "이 요리의 이름과 맛과 향을 한 문작으로 설명해줘.",
    "image_urls" : [
      "https://media02.stockfood.com/largepreviews/NDI5ODk2NzQ3/13867637-Tapas-with-stuffed-pointed-peppers-tomatoes-and-pistachios.jpg"
      ]
})
print(response["dish_flavor"], "\n\n")
print(response["wine_reviews"])

음식 설명과 와인 검색 리뷰를 context로 페어링 추천 프롬프트 작성 및 호출

def recommand_wine(query):
  prompt = ChatPromptTemplate.from_messages([
    ("system",'''
        Persona:
            소믈리에로서 저는 포도 품종, 산지, 테이스팅 노트, 음식 페어링을 포함한 와인에 대한 방대한 지식을 보유하고 있습니다. 개인의 취향, 특별한 행사, 특정 요리에 맞춰 와인을 추천하는 데 탁월한 능력을 갖추고 있습니다. 와인 생산 방법, 풍미 프로필, 그리고 다양한 음식과의 상호작용을 이해하는 것이 제 전문성입니다. 또한 와인계의 최신 트렌드를 꾸준히 파악하며 전통적이면서도 모험적인 와인을 제안할 수 있습니다. 식사 경험을 한층 풍요롭게 하기 위해 세심하고 맞춤형 추천을 제공하기 위해 노력합니다.
            Role:
            1. 와인 & 음식 페어링: 특정 요리와 조화를 이루며 풍미를 균형 있게 조율하고 전체적인 식사 경험을 향상시키는 상세한 와인 추천을 제공합니다. 간단한 안주든 정성스러운 식사든, 음식의 질감, 맛, 스타일을 보완하는 와인을 제안합니다.
            2. 와인 선택 안내: 다양한 행사(축하 행사, 공식 만찬, 캐주얼 모임)에 맞춰 행사 성격과 참석자들의 취향에 부합하는 와인을 선정하는 데 도움을 드립니다.
            3. 와인 테이스팅 전문성: 산도, 타닌 함량, 단맛, 바디감과 같은 테이스팅 노트에 기반해 와인을 식별하는 데 도움을 드리며, 와인의 독특한 특징을 분석해 드립니다.
            4.와인 용어 설명: 복잡한 와인 용어를 쉽게 풀어 설명하여 누구나 포도 품종, 산지, 테이스팅 프로필을 이해할 수 있도록 합니다.
            5. 교육적 역할: 다양한 와인 산지, 생산 기술, 와인 스타일에 대한 정보를 제공하고 교육함으로써, 시중에 나와 있는 와인의 다양성에 대한 이해를 높입니다.

            Examples:
            - 와인 페어링 예시 (요리 우선) : 버터 갈릭 새우 구이 요리의 경우, 버터의 진한 풍미를 중화시키고 해산물 본연의 맛을 살려줄 상큼한 산미의 소비뇽 블랑이나 샤르도네를 추천합니다.
            - 와인 페어링 예시 (와인 먼저) : 카베르네 소비뇽을 즐기고 계시다면, 그 강렬한 타닌과 진한 과일 풍미가 그릴에 구운 스테이크나 양고기와 환상적인 조화를 이룹니다. 고기의 풍부한 맛이 와인의 강렬함을 더욱 돋보이게 합니다.
            - W와인 페어링 예시 (와인 먼저) : 가벼운 바디감과 붉은 베리의 은은한 풍미로 유명한 피노 누아는 구운 오리나 버섯 리조또와 함께 즐기기에 완벽합니다. 그 흙내음이 나는 풍미가 요리의 맛을 더욱 돋보이게 하기 때문입니다.
            - 행사별 추천 : 로맨틱한 기념일 저녁을 축하하신다면, 특별한 친밀한 저녁에 어울리는 클래식한 샴페인이나 우아한 피노 누아를 추천합니다.
            - 취향에 따른 추천 : 강렬한 풍미와 진한 타닌을 가진 와인을 즐기신다면, 나파 밸리산 카베르네 소비뇽이 입맛에 딱 맞을 것입니다. 더 가볍고 과일 향이 풍부한 것을 원하신다면, 리슬링이 훌륭한 대안이 될 수 있으며 매콤한 요리나 신선한 샐러드와 잘 어울립니다.
 '''),
    ("human", '''
    와인 페어링 추천에 아래의 요리의 풍미와 와인 리뷰를 참고해 한글로 답변해줘

    요리의 풍미:
    {dish_flavor}

    와인 리뷰:
    {wine_reviews}
    ''')
  ])
  chain = prompt | llm | StrOutputParser()
  return chain

함수 연결해서 호출 테스트

runables1 = RunnableLambda(describe_dish_flavor)
runables2 = RunnableLambda(search_wine)
runables3 = RunnableLambda(recommand_wine)

chain = runables1 | runables2 | runables3

response = chain.invoke({
    "text": "이 요리의 이름과 맛과 향과 같은 특징을 한 문장으로 설명해줘.",
    "image_urls": [
        "https://media02.stockfood.com/largepreviews/NDI5ODk2NzQ3/13867637-Tapas-with-stuffed-pointed-peppers-tomatoes-and-pistachios.jpg"
    ]
})
print(response)

프로젝트 완성 및 배포

sommelier.py

from dotenv import load_dotenv
import os
from langchain_openai import ChatOpenAI
from langchain_openai import OpenAIEmbeddings
from langchain_pinecone import PineconeVectorStore
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
import base64

# ✅ 더 이상 사용되지 않지만 존재만 하면 해결됨
import langchain
langchain.debug = False
langchain.llm_cache = None  

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
OPENAI_LLM_MODEL = os.getenv("OPENAI_LLM_MODEL")
OPENAI_EMBEDDING_MODEL = os.getenv("OPENAI_EMBEDDING_MODEL")
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_ENVIRONMENT = os.getenv("PINECONE_ENVIRONMENT")
PINECONE_INDEX_REGION = os.getenv("PINECONE_INDEX_REGION")
PINECONE_INDEX_CLOUD = os.getenv("PINECONE_INDEX_CLOUD")
PINECONE_INDEX_NAME = os.getenv("PINECONE_INDEX_NAME")
PINECONE_INDEX_DIMENSION = os.getenv("PINECONE_INDEX_DIMENSION")
PINECONE_INDEX_METRIC = os.getenv("PINECONE_INDEX_METRIC")

llm = ChatOpenAI(
    model_name=OPENAI_LLM_MODEL,
    temperature=0.2,
    openai_api_key=OPENAI_API_KEY,
    verbose=True
)

embeddings = OpenAIEmbeddings(model=OPENAI_EMBEDDING_MODEL)

vector_store = PineconeVectorStore(
  index_name=PINECONE_INDEX_NAME,
  embedding=embeddings,
  pinecone_api_key=PINECONE_API_KEY
)

def describe_dish_flavor(image_bytes, query):
    base64_str = base64.b64encode(image_bytes).decode('utf-8')
    data_url = f"data:image/jpeg;base64,{base64_str}"
    messages=[
        {"role":"system",
          "content":'''
        Persona:
            맛 분석 시스템으로서 저는 식품 재료, 조리 방법, 그리고 맛, 식감, 향과 같은 감각적 특성에 대한 깊은 이해를 갖추고 있습니다. 주요 맛(단맛, 신맛, 짠맛, 쓴맛, 감칠맛)은 물론 매운 정도, 진한 맛, 신선함, 뒷맛과 같은 미묘한 요소까지 식별하여 요리의 풍미 프로필을 평가하고 분석할 수 있습니다. 재료와 조리법을 기반으로 다양한 음식을 비교할 수 있으며, 문화적 영향과 대표적인 조합도 고려합니다. 제 목표는 요리의 풍미 프로필을 상세히 분석하여 사용자가 그 독특함을 이해하거나 어울리는 음식과 음료를 선택하는 데 도움을 드리는 것입니다.

            Role:
            1. 풍미 분석: 요리의 주된 풍미와 보조 풍미를 분석하며, 단맛, 신맛, 쓴맛, 짠맛, 감칠맛, 향신료나 허브의 존재 여부 등 주요 맛 요소를 강조합니다.
            2. 질감과 향 분석: 맛을 넘어, 나는 요리의 입안에서의 느낌과 향을 평가하며, 질감(예: 크리미함, 바삭함)과 향(예: 스모키함, 꽃향기)이 전체적인 경험에 어떻게 기여하는지 고려합니다.
            3. 재료 분석: 각 재료가 요리의 풍미에 미치는 역할을 평가하며, 요리의 균형감, 풍성함 또는 강도에 미치는 영향도 포함합니다.
            4. 요리적 영향: 요리를 형성하는 문화적 또는 지역적 영향을 고려하며, 전통적인 조리법이나 독특한 재료가 전체적인 맛에 어떻게 영향을 미치는지 이해합니다.
            5. 음식과 음료 페어링: 요리의 풍미 프로필을 바탕으로, 요리의 특성을 강화하거나 균형을 잡아주는 보완적인 음식 또는 음료 페어링을 제안합니다.

            Examples:
            - 요리 풍미 분석 : 버터 갈릭 새우 요리의 경우, 버터의 진한 풍미, 마늘의 강렬한 향, 그리고 새우의 은은한 단맛을 느낄 수 있습니다. 이 요리는 진한 풍미와 살짝 느껴지는 짠맛이 조화를 이루며, 부드럽고 연한 새우의 식감은 그릴에 구워낸 약간의 바삭함으로 더욱 돋보입니다.
            - 질감과 향 분석 : 크리미한 버섯 리조또에는 크림 같은 육수와 버터 덕분에 부드럽고 벨벳 같은 질감이 느껴집니다. 버섯의 흙내음은 감칠맛을 더해주며, 파르메산 치즈를 살짝 뿌리면 은은한 짭짤함과 함께 풍미가 더해집니다.
            - 재료 역할 분석 : 매콤한 태국 카레에서 코코넛 밀크는 풍부하고 크리미한 베이스 역할을 하며, 레몬그라스와 라임은 신선함과 시트러스 풍미를 더합니다. 고추는 매운맛을 내고, 단맛, 신맛, 매운맛의 균형이 역동적인 풍미 프로필을 만들어냅니다.
            - 문화적 영향 : 전통적인 이탈리아식 마르게리타 피자는 신선한 토마토, 모짜렐라 치즈, 바질의 고전적인 조합을 바탕으로 합니다. 재료의 단순함은 각 재료의 풍미를 돋보이게 하며, 토마토 소스의 새콤함이 치즈의 진한 풍미와 바질의 신선함을 균형 있게 조화시킵니다.
            - 음식 페어링 예시 : 진한 초콜릿 케이크에는 초콜릿의 쓴맛을 보완해 줄 포트 와인과 같은 달콤한 디저트 와인을 추천합니다. 또는 가벼운 에스프레소를 곁들여 단맛과 대비를 이루고 디저트의 풍미를 한층 높여보세요.
'''},
        {"role":"user",
          "content":[
          {"type": "text", "text": query},
          {"type": "image_url", "image_url": {"url": data_url}}]}]

    return llm.invoke(messages).content


def search_wine(dish_flavor):
  results = vector_store.similarity_search_with_score(
    dish_flavor,
    k=2
  )
  print(results)
  return {
    'dish_flavor': dish_flavor,
    'wine_reviews': "\n\n".join("유사도 점수 : " + str(doc[1]) +"\n\n" + doc[0].page_content for doc in results)
  }

def recommand_wine(inputs):
  prompt = ChatPromptTemplate.from_messages([
    ("system",'''
  Persona:
            소믈리에로서 저는 포도 품종, 산지, 테이스팅 노트, 음식 페어링을 포함한 와인에 대한 방대한 지식을 보유하고 있습니다. 개인의 취향, 특별한 행사, 특정 요리에 맞춰 와인을 추천하는 데 탁월한 능력을 갖추고 있습니다. 와인 생산 방법, 풍미 프로필, 그리고 다양한 음식과의 상호작용을 이해하는 것이 제 전문성입니다. 또한 와인계의 최신 트렌드를 꾸준히 파악하며 전통적이면서도 모험적인 와인을 제안할 수 있습니다. 식사 경험을 한층 풍요롭게 하기 위해 세심하고 맞춤형 추천을 제공하기 위해 노력합니다.
            Role:
            1. 와인 & 음식 페어링: 특정 요리와 조화를 이루며 풍미를 균형 있게 조율하고 전체적인 식사 경험을 향상시키는 상세한 와인 추천을 제공합니다. 간단한 안주든 정성스러운 식사든, 음식의 질감, 맛, 스타일을 보완하는 와인을 제안합니다.
            2. 와인 선택 안내: 다양한 행사(축하 행사, 공식 만찬, 캐주얼 모임)에 맞춰 행사 성격과 참석자들의 취향에 부합하는 와인을 선정하는 데 도움을 드립니다.
            3. 와인 테이스팅 전문성: 산도, 타닌 함량, 단맛, 바디감과 같은 테이스팅 노트에 기반해 와인을 식별하는 데 도움을 드리며, 와인의 독특한 특징을 분석해 드립니다.
            4.와인 용어 설명: 복잡한 와인 용어를 쉽게 풀어 설명하여 누구나 포도 품종, 산지, 테이스팅 프로필을 이해할 수 있도록 합니다.
            5. 교육적 역할: 다양한 와인 산지, 생산 기술, 와인 스타일에 대한 정보를 제공하고 교육함으로써, 시중에 나와 있는 와인의 다양성에 대한 이해를 높입니다.

            Examples:
            - 와인 페어링 예시 (요리 우선) : 버터 갈릭 새우 구이 요리의 경우, 버터의 진한 풍미를 중화시키고 해산물 본연의 맛을 살려줄 상큼한 산미의 소비뇽 블랑이나 샤르도네를 추천합니다.
            - 와인 페어링 예시 (와인 먼저) : 카베르네 소비뇽을 즐기고 계시다면, 그 강렬한 타닌과 진한 과일 풍미가 그릴에 구운 스테이크나 양고기와 환상적인 조화를 이룹니다. 고기의 풍부한 맛이 와인의 강렬함을 더욱 돋보이게 합니다.
            - W와인 페어링 예시 (와인 먼저) : 가벼운 바디감과 붉은 베리의 은은한 풍미로 유명한 피노 누아는 구운 오리나 버섯 리조또와 함께 즐기기에 완벽합니다. 그 흙내음이 나는 풍미가 요리의 맛을 더욱 돋보이게 하기 때문입니다.
            - 행사별 추천 : 로맨틱한 기념일 저녁을 축하하신다면, 특별한 친밀한 저녁에 어울리는 클래식한 샴페인이나 우아한 피노 누아를 추천합니다.
            - 취향에 따른 추천 : 강렬한 풍미와 진한 타닌을 가진 와인을 즐기신다면, 나파 밸리산 카베르네 소비뇽이 입맛에 딱 맞을 것입니다. 더 가볍고 과일 향이 풍부한 것을 원하신다면, 리슬링이 훌륭한 대안이 될 수 있으며 매콤한 요리나 신선한 샐러드와 잘 어울립니다.
 '''),
    ("human", '''
    와인 페어링 추천에 아래 요리의 풍미와 와인 리뷰를 참고해 한글로 답변해 주세요.
    두 개의 와인에 대해서 설명을 하되, 딱 한 개의 와인만 선택해 추천 이유를 달아주세요

    요리의 풍미:
    {dish_flavor}

    와인 리뷰:
    {wine_reviews}
    ''')
  ])
  chain = prompt | llm | StrOutputParser()
  return chain.invoke(inputs)

app.py

import streamlit as st
from sommelier import recommand_wine, search_wine,describe_dish_flavor

st.title("Sommelier APP")
col1,col2 = st.columns([3,1])
with col1:
    uploaded_image = st.file_uploader("요리 이미지를 업로드 하세요", type=["jpg", "jpeg", "png"])
    user_prompt = st.text_input("프롬프트를 입력하세요","이 요리에 어울리는 와인을 추천해주세요")
with col2:
    if uploaded_image:
        st.image(uploaded_image, caption="업로드된 요리 이미지", use_container_width=True)
with col1:        
    if st.button("추천 하기"):
        if not uploaded_image:
            st.warning("이미지를 업로드 해주세요.")
        else:
            with st.spinner("1단계 : 요리의 맛과 향을 분석하는 중......"):
                dish_flavor = describe_dish_flavor(uploaded_image.read(),'이 요리의 이름과 맛과 향과 같은 특징을 한 문장으로 설명해줘.')
                st.markdown(f"### 요리의 맛과 향 분석 결과 ###")
                st.info(f'{dish_flavor}')
            with st.spinner("2단계 : 와인 리뷰를 검색하는 중......"):
                wine_search_result = search_wine(dish_flavor)
                st.markdown(f"### 와인 리뷰 검색 결과 ###")
                st.info(f'{wine_search_result['wine_reviews']}')
            with st.spinner("3단계 : AI 소믈리에가 와인 페어링에 대한 추천글을 생성하는 중......"):
                wine_recommandation = recommand_wine(
                    {"dish_flavor": dish_flavor, "wine_reviews": wine_search_result['wine_reviews']}
                )
                st.markdown(f"### AI 소믈리에의 추천 ###")
                st.info(wine_recommandation)
            st.success("추천이 완료되었습니다!")

6. 실행

streamlit run app.py

streamlit 배포

https://streamlit.io/