์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿค– LangChain์œผ๋กœ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ๋ด‡ ๋งŒ๋“ค๊ธฐ

๐Ÿค– LangChain์œผ๋กœ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ๋ด‡ ๋งŒ๋“ค๊ธฐ

ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค๋ฅผ ํ•œ ๋ฒˆ์—! ๋˜‘๋˜‘ํ•œ AI ๋ด‡ ์ œ์ž‘ ์™„์ „์ •๋ณต

๐ŸŽฏ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI, ์™œ ์ง€๊ธˆ ์ฃผ๋ชฉ๋ฐ›์„๊นŒ?

์•ˆ๋…•, ์นœ๊ตฌ! ๐Ÿ˜Š ์š”์ฆ˜ AI ๊ฐœ๋ฐœ ํŠธ๋ Œ๋“œ๋ฅผ ๋ณด๋ฉด ์ •๋ง ์‹ ๊ธฐํ•œ ๊ฒŒ ๋งŽ์•„. ์˜ˆ์ „์—๋Š” ํ…์ŠคํŠธ๋งŒ ์ดํ•ดํ•˜๋Š” ์ฑ—๋ด‡์ด ๋Œ€๋ถ€๋ถ„์ด์—ˆ๋Š”๋ฐ, ์ด์ œ๋Š” ์‚ฌ์ง„์„ ๋ณด์—ฌ์ฃผ๋ฉด ๊ทธ ์•ˆ์— ๋ญ๊ฐ€ ์žˆ๋Š”์ง€ ์„ค๋ช…ํ•ด์ฃผ๊ณ , ์Œ์„ฑ์„ ๋“ค๋ ค์ฃผ๋ฉด ๊ฐ์ •๊นŒ์ง€ ๋ถ„์„ํ•˜๋Š” ์‹œ๋Œ€๊ฐ€ ์™”๊ฑฐ๋“ !

์ด๊ฒŒ ๋ฐ”๋กœ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ(Multimodal) AI์•ผ. ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ธ๊ณต์ง€๋Šฅ์„ ๋งํ•˜๋Š” ๊ฑฐ์ง€. ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค, ๋น„๋””์˜ค๊นŒ์ง€ ํ•œ๊บผ๋ฒˆ์— ์ดํ•ดํ•˜๊ณ  ๋ถ„์„ํ•  ์ˆ˜ ์žˆ๋‹ค๋‹ˆ, ์ •๋ง ๋Œ€๋‹จํ•˜์ง€ ์•Š์•„? ๐Ÿš€

๐Ÿ’ก ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI์˜ ์‹ค์ƒํ™œ ํ™œ์šฉ ์˜ˆ์‹œ

โ€ข ์˜๋ฃŒ ๋ถ„์•ผ: X-ray ์ด๋ฏธ์ง€์™€ ํ™˜์ž ์ฆ์ƒ ํ…์ŠคํŠธ๋ฅผ ๋™์‹œ์— ๋ถ„์„ํ•ด ์ง„๋‹จ ๋ณด์กฐ
โ€ข ์‡ผํ•‘: ์ œํ’ˆ ์‚ฌ์ง„์„ ์ฐ์œผ๋ฉด ์œ ์‚ฌ ์ƒํ’ˆ์„ ์ฐพ์•„์ฃผ๊ณ  ๋ฆฌ๋ทฐ๊นŒ์ง€ ์š”์•ฝ
โ€ข ๊ต์œก: ํ•™์ƒ์ด ์ œ์ถœํ•œ ๊ณผ์ œ(ํ…์ŠคํŠธ+์ด๋ฏธ์ง€)๋ฅผ ์ข…ํ•ฉ์ ์œผ๋กœ ํ‰๊ฐ€
โ€ข ์ฝ˜ํ…์ธ  ์ œ์ž‘: ์˜์ƒ์˜ ์žฅ๋ฉด๊ณผ ๋Œ€์‚ฌ๋ฅผ ๋ถ„์„ํ•ด ์ž๋™์œผ๋กœ ํ•˜์ด๋ผ์ดํŠธ ์ƒ์„ฑ

๊ทธ๋Ÿฐ๋ฐ ์ด๋Ÿฐ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค๋ ค๋ฉด ์ •๋ง ๋ณต์žกํ•ด. ๊ฐ๊ฐ์˜ ๋ชจ๋ธ์„ ๋”ฐ๋กœ ํ•™์Šต์‹œํ‚ค๊ณ , ๊ฒฐ๊ณผ๋ฅผ ํ†ตํ•ฉํ•˜๊ณ , ํ”„๋กฌํ”„ํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ ... ์ƒ๊ฐ๋งŒ ํ•ด๋„ ๋จธ๋ฆฌ๊ฐ€ ์•„ํ”„์ง€? ๐Ÿ˜ต

๋ฐ”๋กœ ์—ฌ๊ธฐ์„œ LangChain์ด ๋“ฑ์žฅํ•ด! LangChain์€ ์ด๋Ÿฐ ๋ณต์žกํ•œ ๊ณผ์ •์„ ํ›จ์”ฌ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋งˆ์น˜ ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์กฐ๋ฆฝํ•ด์„œ ๋‚˜๋งŒ์˜ AI ๋ด‡์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฑฐ๋“ . ๐Ÿงฉ

๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI ์ž‘๋™ ์›๋ฆฌ ๐Ÿ“ ํ…์ŠคํŠธ ์‚ฌ์šฉ์ž ์งˆ๋ฌธ ์ž์—ฐ์–ด ์ฒ˜๋ฆฌ ๐Ÿ–ผ๏ธ ์ด๋ฏธ์ง€ ์‹œ๊ฐ ๋ฐ์ดํ„ฐ ์ปดํ“จํ„ฐ ๋น„์ „ ๐ŸŽต ์˜ค๋””์˜ค ์Œ์„ฑ ๋ฐ์ดํ„ฐ ์Œ์„ฑ ์ธ์‹ LangChain ํ†ตํ•ฉ ์ฒ˜๋ฆฌ ๐ŸŽฏ ํ†ตํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ ๋งฅ๋ฝ์„ ์ดํ•ดํ•œ ์ •ํ™•ํ•œ ๋‹ต๋ณ€ ์ƒ์„ฑ โœจ ๋˜‘๋˜‘ํ•œ AI ๋ด‡

๐Ÿ”ง LangChain์ด ๋ญ๊ธธ๋ž˜?

LangChain์€ 2022๋…„์— ๋“ฑ์žฅํ•œ ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ์•ผ. ๋Œ€๊ทœ๋ชจ ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ํ™œ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ๋„๊ตฌ์ธ๋ฐ, ํŠนํžˆ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ฒ˜๋ฆฌ์— ์ •๋ง ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด. ๐Ÿ› ๏ธ

์ƒ๊ฐํ•ด๋ด. GPT-4, Claude, Gemini ๊ฐ™์€ ์ตœ์‹  AI ๋ชจ๋ธ๋“ค์€ ์ด๋ฏธ ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ž–์•„? ๊ทผ๋ฐ ์ด๊ฑธ ์‹ค์ œ ์„œ๋น„์Šค์— ์ ์šฉํ•˜๋ ค๋ฉด API ํ˜ธ์ถœ, ๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ, ๊ฒฐ๊ณผ ํ›„์ฒ˜๋ฆฌ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๋“ฑ๋“ฑ... ์‹ ๊ฒฝ ์“ธ ๊ฒŒ ํ•œ๋‘ ๊ฐ€์ง€๊ฐ€ ์•„๋‹ˆ์•ผ.

๐ŸŽ“ LangChain์˜ ํ•ต์‹ฌ ์žฅ์ 

1. ๋ชจ๋“ˆํ™”๋œ ๊ตฌ์กฐ: ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ๊ณจ๋ผ์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
2. ๋‹ค์–‘ํ•œ ๋ชจ๋ธ ์ง€์›: OpenAI, Anthropic, Google, HuggingFace ๋“ฑ ์ฃผ์š” AI ๋ชจ๋ธ ํ†ตํ•ฉ
3. ์ฒด์ธ ๊ตฌ์„ฑ: ์—ฌ๋Ÿฌ ์ž‘์—…์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์—ฐ๊ฒฐํ•ด ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„
4. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ: ๋Œ€ํ™” ๋งฅ๋ฝ์„ ๊ธฐ์–ตํ•˜๊ณ  ์œ ์ง€ํ•˜๋Š” ๊ธฐ๋Šฅ
5. ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ: ์Šค์Šค๋กœ ํŒ๋‹จํ•˜๊ณ  ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ž์œจ AI ๊ตฌํ˜„

ํŠนํžˆ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ๋ด‡์„ ๋งŒ๋“ค ๋•Œ LangChain์ด ๋น›์„ ๋ฐœํ•˜๋Š” ์ด์œ ๋Š” ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ํ˜•์‹์„ ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ด๋ฉด LangChain์ด ์•Œ์•„์„œ ์ ์ ˆํ•œ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ด์„œ AI ๋ชจ๋ธ์— ์ „๋‹ฌํ•ด์ฃผ๊ฑฐ๋“ . ๐Ÿ˜Ž

๐Ÿš€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ค€๋น„ํ•˜๊ธฐ

์ž, ์ด์ œ ๋ณธ๊ฒฉ์ ์œผ๋กœ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ๋ด‡์„ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๋ถ€ํ„ฐ ์„ธํŒ…ํ•ด์•ผ ํ•ด. ๊ฑฑ์ • ๋งˆ, ์ƒ๊ฐ๋ณด๋‹ค ์–ด๋ ต์ง€ ์•Š์•„! ์ฐจ๊ทผ์ฐจ๊ทผ ๋”ฐ๋ผ์˜ค๋ฉด ๋ผ. โ˜•

1
Python ์„ค์น˜ ํ™•์ธ

LangChain์€ Python ๊ธฐ๋ฐ˜์ด์•ผ. Python 3.8 ์ด์ƒ ๋ฒ„์ „์ด ํ•„์š”ํ•ด. ํ„ฐ๋ฏธ๋„์—์„œ ํ™•์ธํ•ด๋ณด์ž:
python --version
# ๋˜๋Š”
python3 --version
๋งŒ์•ฝ ์„ค์น˜๋˜์–ด ์žˆ์ง€ ์•Š๋‹ค๋ฉด python.org์—์„œ ๋‹ค์šด๋กœ๋“œํ•ด์„œ ์„ค์น˜ํ•˜๋ฉด ๋ผ.
2
๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ

ํ”„๋กœ์ ํŠธ๋ณ„๋กœ ๋…๋ฆฝ์ ์ธ ํ™˜๊ฒฝ์„ ๋งŒ๋“œ๋Š” ๊ฒŒ ์ข‹์•„. ํŒจํ‚ค์ง€ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๊ฑฐ๋“ !
# ๊ฐ€์ƒํ™˜๊ฒฝ ์ƒ์„ฑ
python -m venv multimodal_bot

# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (Windows)
multimodal_bot\Scripts\activate

# ๊ฐ€์ƒํ™˜๊ฒฝ ํ™œ์„ฑํ™” (Mac/Linux)
source multimodal_bot/bin/activate
3
ํ•„์ˆ˜ ํŒจํ‚ค์ง€ ์„ค์น˜

์ด์ œ ํ•„์š”ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ์„ค์น˜ํ•  ์ฐจ๋ก€์•ผ. ํ•œ ๋ฒˆ์— ์„ค์น˜ํ•˜๋ฉด ํŽธํ•ด:
pip install langchain langchain-openai langchain-anthropic
pip install pillow python-dotenv
pip install openai anthropic
pip install langchain-community
๊ฐ ํŒจํ‚ค์ง€์˜ ์—ญํ• ์„ ๊ฐ„๋‹จํžˆ ์„ค๋ช…ํ•˜๋ฉด:
โ€ข langchain: ํ•ต์‹ฌ ํ”„๋ ˆ์ž„์›Œํฌ
โ€ข langchain-openai: OpenAI ๋ชจ๋ธ ์—ฐ๋™
โ€ข pillow: ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ
โ€ข python-dotenv: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ
4
API ํ‚ค ์„ค์ •

AI ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด API ํ‚ค๊ฐ€ ํ•„์š”ํ•ด. OpenAI๋‚˜ Anthropic ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์žˆ์–ด.

ํ”„๋กœ์ ํŠธ ํด๋”์— .env ํŒŒ์ผ์„ ๋งŒ๋“ค๊ณ :
OPENAI_API_KEY=your_openai_api_key_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
โš ๏ธ ์ฃผ์˜: API ํ‚ค๋Š” ์ ˆ๋Œ€ GitHub ๊ฐ™์€ ๊ณต๊ฐœ ์ €์žฅ์†Œ์— ์˜ฌ๋ฆฌ๋ฉด ์•ˆ ๋ผ! .gitignore ํŒŒ์ผ์— .env๋ฅผ ์ถ”๊ฐ€ํ•ด๋‘์ž.

๐Ÿ’ป ๊ธฐ๋ณธ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ด‡ ๊ตฌํ˜„ํ•˜๊ธฐ

ํ™˜๊ฒฝ ์„ค์ •์ด ๋๋‚ฌ์œผ๋‹ˆ ์ด์ œ ์ง„์งœ ์ฝ”๋”ฉ์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ๋จผ์ € ๊ฐ„๋‹จํ•œ ํ…์ŠคํŠธ+์ด๋ฏธ์ง€ ๋ถ„์„ ๋ด‡๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด๋ณด์ž. ์ด๊ฒŒ ๊ธฐ๋ณธ์ด ๋˜๋ฉด ๋‚˜์ค‘์— ์˜ค๋””์˜ค๋‚˜ ๋น„๋””์˜ค๋„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽจ

๐Ÿ“‹ 1๋‹จ๊ณ„: ๊ธฐ๋ณธ ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ

๋จผ์ € ํ”„๋กœ์ ํŠธ์˜ ๋ผˆ๋Œ€๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด. multimodal_bot.py ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด๋ณด์ž:

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.schema.messages import HumanMessage, SystemMessage
from langchain.prompts import ChatPromptTemplate
import base64
from pathlib import Path

# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋กœ๋“œ
load_dotenv()

class MultimodalBot:
    def __init__(self, model_name="gpt-4-vision-preview"):
        """
        ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ๋ด‡ ์ดˆ๊ธฐํ™”
        
        Args:
            model_name: ์‚ฌ์šฉํ•  AI ๋ชจ๋ธ ์ด๋ฆ„
        """
        self.llm = ChatOpenAI(
            model=model_name,
            temperature=0.7,
            max_tokens=1000
        )
        
    def encode_image(self, image_path):
        """
        ์ด๋ฏธ์ง€๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉ
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ
            
        Returns:
            base64๋กœ ์ธ์ฝ”๋”ฉ๋œ ์ด๋ฏธ์ง€ ๋ฌธ์ž์—ด
        """
        with open(image_path, "rb") as image_file:
            return base64.b64encode(image_file.read()).decode('utf-8')
    
    def analyze_image_with_text(self, image_path, user_question):
        """
        ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ๋ฅผ ํ•จ๊ป˜ ๋ถ„์„
        
        Args:
            image_path: ๋ถ„์„ํ•  ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            user_question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
            
        Returns:
            AI์˜ ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        # ์ด๋ฏธ์ง€ ์ธ์ฝ”๋”ฉ
        base64_image = self.encode_image(image_path)
        
        # ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ
        messages = [
            SystemMessage(content="๋‹น์‹ ์€ ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(
                content=[
                    {"type": "text", "text": user_question},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        # AI ๋ชจ๋ธ ํ˜ธ์ถœ
        response = self.llm.invoke(messages)
        return response.content

# ์‚ฌ์šฉ ์˜ˆ์‹œ
if __name__ == "__main__":
    bot = MultimodalBot()
    
    # ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ์™€ ์งˆ๋ฌธ ์„ค์ •
    image_path = "sample_image.jpg"
    question = "์ด ์ด๋ฏธ์ง€์—์„œ ๋ฌด์—‡์„ ๋ณผ ์ˆ˜ ์žˆ๋‚˜์š”? ์ž์„ธํžˆ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”."
    
    # ๋ถ„์„ ์‹คํ–‰
    result = bot.analyze_image_with_text(image_path, question)
    print(f"๋ถ„์„ ๊ฒฐ๊ณผ:\n{result}")
๐Ÿ” ์ฝ”๋“œ ํ•ด์„ค

1. ํด๋ž˜์Šค ๊ตฌ์กฐ: MultimodalBot ํด๋ž˜์Šค๋กœ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์บก์Аํ™”ํ–ˆ์–ด. ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ํŽธํ•˜์ง€?

2. ์ด๋ฏธ์ง€ ์ธ์ฝ”๋”ฉ: AI ๋ชจ๋ธ์€ ์ด๋ฏธ์ง€๋ฅผ ์ง์ ‘ ๋ฐ›์„ ์ˆ˜ ์—†์–ด์„œ base64 ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•ด.

3. ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ: LangChain์˜ ๋ฉ”์‹œ์ง€ ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•ด์„œ ํ…์ŠคํŠธ์™€ ์ด๋ฏธ์ง€๋ฅผ ํ•จ๊ป˜ ์ „๋‹ฌํ•ด.

4. ์œ ์—ฐํ•œ ์„ค๊ณ„: ๋‚˜์ค‘์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ ์‰ฝ๋„๋ก ๋ฉ”์„œ๋“œ๋ฅผ ๋ถ„๋ฆฌํ–ˆ์–ด.

๐ŸŽฏ 2๋‹จ๊ณ„: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

๊ธฐ๋ณธ ๋ด‡์ด ์ž‘๋™ํ•˜๋Š” ๊ฑธ ํ™•์ธํ–ˆ๋‹ค๋ฉด, ์ด์ œ ๋” ๋˜‘๋˜‘ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณด์ž! ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ๋ถ„์„ํ•˜๊ฑฐ๋‚˜, ๋Œ€ํ™” ๋งฅ๋ฝ์„ ๊ธฐ์–ตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด. ๐Ÿง 

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from typing import List, Dict

class AdvancedMultimodalBot(MultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview"):
        super().__init__(model_name)
        # ๋Œ€ํ™” ๊ธฐ๋ก์„ ์ €์žฅํ•  ๋ฉ”๋ชจ๋ฆฌ
        self.memory = ConversationBufferMemory(
            return_messages=True,
            memory_key="chat_history"
        )
        
    def analyze_multiple_images(self, image_paths: List[str], question: str):
        """
        ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ๋ถ„์„
        
        Args:
            image_paths: ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ ๋ฆฌ์ŠคํŠธ
            question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
            
        Returns:
            ์ข…ํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        # ์ด๋ฏธ์ง€๋“ค์„ ์ธ์ฝ”๋”ฉ
        encoded_images = [self.encode_image(path) for path in image_paths]
        
        # ๋ฉ”์‹œ์ง€ ์ปจํ…์ธ  ๊ตฌ์„ฑ
        content = [{"type": "text", "text": question}]
        
        for idx, encoded_img in enumerate(encoded_images):
            content.append({
                "type": "image_url",
                "image_url": {
                    "url": f"data:image/jpeg;base64,{encoded_img}",
                    "detail": "high"  # ๊ณ ํ•ด์ƒ๋„ ๋ถ„์„
                }
            })
        
        messages = [
            SystemMessage(content="์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋น„๊ต ๋ถ„์„ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=content)
        ]
        
        response = self.llm.invoke(messages)
        return response.content
    
    def analyze_with_context(self, image_path: str, question: str):
        """
        ์ด์ „ ๋Œ€ํ™” ๋งฅ๋ฝ์„ ๊ณ ๋ คํ•œ ๋ถ„์„
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
            
        Returns:
            ๋งฅ๋ฝ์„ ๊ณ ๋ คํ•œ ๋‹ต๋ณ€
        """
        # ์ด์ „ ๋Œ€ํ™” ๊ธฐ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
        chat_history = self.memory.load_memory_variables({})
        
        base64_image = self.encode_image(image_path)
        
        # ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์— ๋งฅ๋ฝ ์ •๋ณด ์ถ”๊ฐ€
        context_prompt = "์ด์ „ ๋Œ€ํ™” ๋‚ด์šฉ์„ ์ฐธ๊ณ ํ•˜์—ฌ ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”."
        if chat_history.get("chat_history"):
            context_prompt += f"\n์ด์ „ ๋Œ€ํ™”: {chat_history['chat_history']}"
        
        messages = [
            SystemMessage(content=context_prompt),
            HumanMessage(
                content=[
                    {"type": "text", "text": question},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        response = self.llm.invoke(messages)
        
        # ๋Œ€ํ™” ๊ธฐ๋ก ์ €์žฅ
        self.memory.save_context(
            {"input": question},
            {"output": response.content}
        )
        
        return response.content
    
    def extract_structured_data(self, image_path: str, schema: Dict):
        """
        ์ด๋ฏธ์ง€์—์„œ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            schema: ์ถ”์ถœํ•  ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ
            
        Returns:
            ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ
        """
        base64_image = self.encode_image(image_path)
        
        # ์Šคํ‚ค๋งˆ๋ฅผ ํ”„๋กฌํ”„ํŠธ๋กœ ๋ณ€ํ™˜
        schema_text = "๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•ด์ฃผ์„ธ์š”:\n"
        for key, description in schema.items():
            schema_text += f"- {key}: {description}\n"
        
        messages = [
            SystemMessage(content="์ด๋ฏธ์ง€์—์„œ ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜์—ฌ JSON ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."),
            HumanMessage(
                content=[
                    {"type": "text", "text": schema_text},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        response = self.llm.invoke(messages)
        return response.content

# ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์˜ˆ์‹œ
if __name__ == "__main__":
    bot = AdvancedMultimodalBot()
    
    # ์˜ˆ์‹œ 1: ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€ ๋น„๊ต ๋ถ„์„
    images = ["image1.jpg", "image2.jpg", "image3.jpg"]
    comparison = bot.analyze_multiple_images(
        images,
        "์ด ์„ธ ์ด๋ฏธ์ง€์˜ ๊ณตํ†ต์ ๊ณผ ์ฐจ์ด์ ์„ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”."
    )
    print(f"๋น„๊ต ๋ถ„์„:\n{comparison}\n")
    
    # ์˜ˆ์‹œ 2: ๋งฅ๋ฝ ๊ธฐ๋ฐ˜ ๋Œ€ํ™”
    result1 = bot.analyze_with_context(
        "product.jpg",
        "์ด ์ œํ’ˆ์˜ ํŠน์ง•์„ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”."
    )
    print(f"์ฒซ ๋ฒˆ์งธ ๋‹ต๋ณ€:\n{result1}\n")
    
    result2 = bot.analyze_with_context(
        "product.jpg",
        "๊ทธ๋ ‡๋‹ค๋ฉด ์ด ์ œํ’ˆ์˜ ๊ฐ€๊ฒฉ๋Œ€๋Š” ์–ด๋А ์ •๋„์ผ๊นŒ์š”?"
    )
    print(f"๋‘ ๋ฒˆ์งธ ๋‹ต๋ณ€ (๋งฅ๋ฝ ๊ณ ๋ ค):\n{result2}\n")
    
    # ์˜ˆ์‹œ 3: ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
    schema = {
        "์ œํ’ˆ๋ช…": "์ด๋ฏธ์ง€์— ํ‘œ์‹œ๋œ ์ œํ’ˆ์˜ ์ด๋ฆ„",
        "์ƒ‰์ƒ": "์ œํ’ˆ์˜ ์ฃผ์š” ์ƒ‰์ƒ",
        "ํŠน์ง•": "๋ˆˆ์— ๋„๋Š” ํŠน์ง•๋“ค",
        "์šฉ๋„": "์˜ˆ์ƒ๋˜๋Š” ์‚ฌ์šฉ ์šฉ๋„"
    }
    structured_data = bot.extract_structured_data("product.jpg", schema)
    print(f"์ถ”์ถœ๋œ ๋ฐ์ดํ„ฐ:\n{structured_data}")
๐Ÿ’ก ์‹ค์ „ ํŒ

๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ: ๋Œ€ํ™”๊ฐ€ ๊ธธ์–ด์ง€๋ฉด ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ณ„์† ์Œ“์—ฌ. ์ฃผ๊ธฐ์ ์œผ๋กœ memory.clear()๋ฅผ ํ˜ธ์ถœํ•ด์„œ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

์ด๋ฏธ์ง€ ํฌ๊ธฐ: ๋„ˆ๋ฌด ํฐ ์ด๋ฏธ์ง€๋Š” API ๋น„์šฉ์ด ๋งŽ์ด ๋“ค์–ด. ์ ์ ˆํžˆ ๋ฆฌ์‚ฌ์ด์ง•ํ•˜๋Š” ๊ฒŒ ๊ฒฝ์ œ์ ์ด์•ผ.

์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์‹ค์ œ ์„œ๋น„์Šค์—์„œ๋Š” try-except ๋ธ”๋ก์œผ๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๊ผญ ํ•ด์•ผ ํ•ด!

๐ŸŽต ์˜ค๋””์˜ค ๋ถ„์„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ํ•˜๊ธฐ

์ด๋ฏธ์ง€ ๋ถ„์„์€ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋‹ˆ, ์ด์ œ ์˜ค๋””์˜ค๋„ ๋‹ค๋ค„๋ณผ๊นŒ? ์Œ์„ฑ ํŒŒ์ผ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , ๊ฐ์ •์„ ๋ถ„์„ํ•˜๊ณ , ๋‚ด์šฉ์„ ์š”์•ฝํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด์ž! ๐ŸŽง

์˜ค๋””์˜ค ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด์„œ๋Š” ์ถ”๊ฐ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ํ•„์š”ํ•ด:

pip install openai-whisper pydub
# ๋˜๋Š” OpenAI API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ
pip install openai

Whisper๋Š” OpenAI๊ฐ€ ๋งŒ๋“  ์Œ์„ฑ ์ธ์‹ ๋ชจ๋ธ์ด์•ผ. ์ •ํ™•๋„๊ฐ€ ์ •๋ง ๋›ฐ์–ด๋‚˜๊ณ , ๋‹ค์–‘ํ•œ ์–ธ์–ด๋ฅผ ์ง€์›ํ•ด. ๐ŸŒ

import whisper
from pydub import AudioSegment
import tempfile
import os

class AudioMultimodalBot(AdvancedMultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview", whisper_model="base"):
        super().__init__(model_name)
        # Whisper ๋ชจ๋ธ ๋กœ๋“œ
        self.whisper = whisper.load_model(whisper_model)
        
    def transcribe_audio(self, audio_path: str):
        """
        ์˜ค๋””์˜ค ํŒŒ์ผ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
        
        Args:
            audio_path: ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
            
        Returns:
            ๋ณ€ํ™˜๋œ ํ…์ŠคํŠธ
        """
        result = self.whisper.transcribe(audio_path)
        return result["text"]
    
    def analyze_audio_sentiment(self, audio_path: str):
        """
        ์˜ค๋””์˜ค์˜ ๊ฐ์ • ๋ถ„์„
        
        Args:
            audio_path: ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
            
        Returns:
            ๊ฐ์ • ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        # ๋จผ์ € ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
        transcript = self.transcribe_audio(audio_path)
        
        # ๊ฐ์ • ๋ถ„์„ ํ”„๋กฌํ”„ํŠธ
        messages = [
            SystemMessage(content="์Œ์„ฑ ๋‚ด์šฉ์˜ ๊ฐ์ •์„ ๋ถ„์„ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=f"""
๋‹ค์Œ ์Œ์„ฑ ๋‚ด์šฉ์˜ ๊ฐ์ •์„ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:

"{transcript}"

๋‹ค์Œ ํ•ญ๋ชฉ์„ ํฌํ•จํ•ด์ฃผ์„ธ์š”:
1. ์ „๋ฐ˜์ ์ธ ๊ฐ์ • (๊ธ์ •/๋ถ€์ •/์ค‘๋ฆฝ)
2. ๊ตฌ์ฒด์ ์ธ ๊ฐ์ • (๊ธฐ์จ, ์Šฌํ””, ๋ถ„๋…ธ, ๋†€๋žŒ ๋“ฑ)
3. ๊ฐ์ •์˜ ๊ฐ•๋„ (1-10)
4. ์ฃผ์š” ํ‚ค์›Œ๋“œ
            """)
        ]
        
        response = self.llm.invoke(messages)
        return {
            "transcript": transcript,
            "sentiment_analysis": response.content
        }
    
    def summarize_audio(self, audio_path: str, summary_length="medium"):
        """
        ์˜ค๋””์˜ค ๋‚ด์šฉ ์š”์•ฝ
        
        Args:
            audio_path: ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
            summary_length: ์š”์•ฝ ๊ธธ์ด (short/medium/long)
            
        Returns:
            ์š”์•ฝ๋œ ๋‚ด์šฉ
        """
        transcript = self.transcribe_audio(audio_path)
        
        length_instructions = {
            "short": "3-5๋ฌธ์žฅ์œผ๋กœ ํ•ต์‹ฌ๋งŒ ๊ฐ„๋‹จํžˆ",
            "medium": "1-2 ๋‹จ๋ฝ์œผ๋กœ ์ฃผ์š” ๋‚ด์šฉ์„",
            "long": "์ƒ์„ธํ•˜๊ฒŒ ๋ชจ๋“  ์ค‘์š” ํฌ์ธํŠธ๋ฅผ"
        }
        
        messages = [
            SystemMessage(content="์Œ์„ฑ ๋‚ด์šฉ์„ ์š”์•ฝํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=f"""
๋‹ค์Œ ์Œ์„ฑ ๋‚ด์šฉ์„ {length_instructions.get(summary_length, length_instructions['medium'])} ์š”์•ฝํ•ด์ฃผ์„ธ์š”:

"{transcript}"
            """)
        ]
        
        response = self.llm.invoke(messages)
        return {
            "original_transcript": transcript,
            "summary": response.content
        }
    
    def multimodal_analysis(self, image_path: str, audio_path: str, question: str):
        """
        ์ด๋ฏธ์ง€์™€ ์˜ค๋””์˜ค๋ฅผ ํ•จ๊ป˜ ๋ถ„์„
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ
            audio_path: ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
            question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
            
        Returns:
            ํ†ตํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        # ์˜ค๋””์˜ค๋ฅผ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
        audio_transcript = self.transcribe_audio(audio_path)
        
        # ์ด๋ฏธ์ง€ ์ธ์ฝ”๋”ฉ
        base64_image = self.encode_image(image_path)
        
        # ํ†ตํ•ฉ ๋ถ„์„ ๋ฉ”์‹œ์ง€ ๊ตฌ์„ฑ
        messages = [
            SystemMessage(content="์ด๋ฏธ์ง€์™€ ์Œ์„ฑ ๋‚ด์šฉ์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๋ถ„์„ํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(
                content=[
                    {
                        "type": "text",
                        "text": f"""
์‚ฌ์šฉ์ž ์งˆ๋ฌธ: {question}

์Œ์„ฑ ๋‚ด์šฉ: "{audio_transcript}"

์œ„ ์Œ์„ฑ ๋‚ด์šฉ๊ณผ ์•„๋ž˜ ์ด๋ฏธ์ง€๋ฅผ ํ•จ๊ป˜ ๊ณ ๋ คํ•˜์—ฌ ๋‹ต๋ณ€ํ•ด์ฃผ์„ธ์š”.
                        """
                    },
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        response = self.llm.invoke(messages)
        return {
            "audio_transcript": audio_transcript,
            "analysis": response.content
        }

# ์˜ค๋””์˜ค ๊ธฐ๋Šฅ ์‚ฌ์šฉ ์˜ˆ์‹œ
if __name__ == "__main__":
    bot = AudioMultimodalBot()
    
    # ์˜ˆ์‹œ 1: ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„
    sentiment = bot.analyze_audio_sentiment("speech.mp3")
    print(f"์Œ์„ฑ ๋‚ด์šฉ: {sentiment['transcript']}")
    print(f"๊ฐ์ • ๋ถ„์„: {sentiment['sentiment_analysis']}\n")
    
    # ์˜ˆ์‹œ 2: ์Œ์„ฑ ์š”์•ฝ
    summary = bot.summarize_audio("lecture.mp3", summary_length="medium")
    print(f"์š”์•ฝ: {summary['summary']}\n")
    
    # ์˜ˆ์‹œ 3: ์ด๋ฏธ์ง€ + ์˜ค๋””์˜ค ํ†ตํ•ฉ ๋ถ„์„
    result = bot.multimodal_analysis(
        "presentation_slide.jpg",
        "presentation_audio.mp3",
        "์ด ํ”„๋ ˆ์  ํ…Œ์ด์…˜์˜ ํ•ต์‹ฌ ๋ฉ”์‹œ์ง€๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?"
    )
    print(f"ํ†ตํ•ฉ ๋ถ„์„: {result['analysis']}")
โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ

Whisper ๋ชจ๋ธ ํฌ๊ธฐ: "base" ๋ชจ๋ธ์€ ๋น ๋ฅด์ง€๋งŒ ์ •ํ™•๋„๊ฐ€ ๋‚ฎ์•„. "medium"์ด๋‚˜ "large" ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋” ์ •ํ™•ํ•˜์ง€๋งŒ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ๊ธธ์–ด์ ธ.

์˜ค๋””์˜ค ํ˜•์‹: Whisper๋Š” ๋Œ€๋ถ€๋ถ„์˜ ์˜ค๋””์˜ค ํ˜•์‹์„ ์ง€์›ํ•˜์ง€๋งŒ, WAV๋‚˜ MP3๊ฐ€ ๊ฐ€์žฅ ์•ˆ์ •์ ์ด์•ผ.

ํŒŒ์ผ ํฌ๊ธฐ: ๊ธด ์˜ค๋””์˜ค๋Š” ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ ค. ํ•„์š”ํ•˜๋ฉด ์ฒญํฌ๋กœ ๋‚˜๋ˆ ์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„.
๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ด‡ ์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ ๐Ÿ“ฅ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ํ…์ŠคํŠธ / ์ด๋ฏธ์ง€ / ์˜ค๋””์˜ค ๐Ÿ”„ ์ „์ฒ˜๋ฆฌ ์ธ์ฝ”๋”ฉ / ๋ณ€ํ™˜ ๐Ÿค– AI ๋ชจ๋ธ LangChain + LLM ๐Ÿ“ค ๊ฒฐ๊ณผ ์ถœ๋ ฅ ๋ถ„์„ / ์š”์•ฝ / ๋‹ต๋ณ€ ์ฒ˜๋ฆฌ ๋‹จ๊ณ„ ์ƒ์„ธ ์ด๋ฏธ์ง€ Base64 ์ธ์ฝ”๋”ฉ ์˜ค๋””์˜ค Whisper ๋ณ€ํ™˜ ํ”„๋กฌํ”„ํŠธ ๊ตฌ์„ฑ ๋ฉ”์‹œ์ง€ ํฌ๋งทํŒ… LLM API ํ˜ธ์ถœ ์‘๋‹ต ํŒŒ์‹ฑ ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ๐Ÿ’ก ํ•ต์‹ฌ ๊ธฐ๋Šฅ โœ… ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ํ†ตํ•ฉ ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ๋™์‹œ ์ฒ˜๋ฆฌ โœ… ๋งฅ๋ฝ ์ดํ•ด ๋Œ€ํ™” ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ โœ… ์œ ์—ฐํ•œ ์ถœ๋ ฅ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ โœ… ํ™•์žฅ ๊ฐ€๋Šฅ ์ƒˆ๋กœ์šด ๋ชจ๋‹ฌ๋ฆฌํ‹ฐ ์ถ”๊ฐ€ LangChain์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ์ง€๋Šฅํ˜• ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์‹œ์Šคํ…œ

๐Ÿ› ๏ธ ์‹ค์ „ ํ™œ์šฉ ์‚ฌ๋ก€

์ด๋ก ๊ณผ ์ฝ”๋“œ๋Š” ์ถฉ๋ถ„ํžˆ ๋ดค์œผ๋‹ˆ, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ตฌ์ฒด์ ์ธ ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณด์ž! ๋‹ค์–‘ํ•œ ์‚ฐ์—… ๋ถ„์•ผ์—์„œ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ด‡์ด ์–ด๋–ป๊ฒŒ ์“ฐ์ด๋Š”์ง€ ์•Œ์•„๋ณด๋ฉด ์•„์ด๋””์–ด๊ฐ€ ์ƒ˜์†Ÿ์„ ๊ฑฐ์•ผ. ๐Ÿ’ก

๐Ÿฅ ์˜๋ฃŒ ๋ถ„์•ผ: ์˜๋ฃŒ ์˜์ƒ ๋ถ„์„ ๋ณด์กฐ

๐Ÿ“‹ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

์˜์‚ฌ๊ฐ€ X-ray๋‚˜ CT ์Šค์บ” ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•˜๊ณ , ํ™˜์ž์˜ ์ฆ์ƒ์„ ํ…์ŠคํŠธ๋กœ ์ž…๋ ฅํ•˜๋ฉด AI๊ฐ€ ์ดˆ๊ธฐ ๋ถ„์„์„ ์ œ๊ณตํ•ด. ๋ฌผ๋ก  ์ตœ์ข… ์ง„๋‹จ์€ ์˜์‚ฌ๊ฐ€ ํ•˜์ง€๋งŒ, ๋†“์น  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์„ ์ฒดํฌํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋ผ.
class MedicalAnalysisBot(AudioMultimodalBot):
    def analyze_medical_image(self, image_path: str, patient_info: dict):
        """
        ์˜๋ฃŒ ์˜์ƒ ๋ถ„์„
        
        Args:
            image_path: ์˜๋ฃŒ ์˜์ƒ ๊ฒฝ๋กœ
            patient_info: ํ™˜์ž ์ •๋ณด (๋‚˜์ด, ์„ฑ๋ณ„, ์ฆ์ƒ ๋“ฑ)
            
        Returns:
            ๋ถ„์„ ๊ฒฐ๊ณผ ๋ฐ ์ฃผ์˜์‚ฌํ•ญ
        """
        base64_image = self.encode_image(image_path)
        
        patient_context = f"""
ํ™˜์ž ์ •๋ณด:
- ๋‚˜์ด: {patient_info.get('age', '๋ฏธ์ƒ')}
- ์„ฑ๋ณ„: {patient_info.get('gender', '๋ฏธ์ƒ')}
- ์ฃผ์š” ์ฆ์ƒ: {patient_info.get('symptoms', '๋ฏธ์ƒ')}
- ๋ณ‘๋ ฅ: {patient_info.get('history', '์—†์Œ')}
        """
        
        messages = [
            SystemMessage(content="""
๋‹น์‹ ์€ ์˜๋ฃŒ ์˜์ƒ ๋ถ„์„์„ ๋ณด์กฐํ•˜๋Š” AI์ž…๋‹ˆ๋‹ค.
์ฃผ์˜: ์ด ๋ถ„์„์€ ์ฐธ๊ณ ์šฉ์ด๋ฉฐ, ์ตœ์ข… ์ง„๋‹จ์€ ๋ฐ˜๋“œ์‹œ ์˜๋ฃŒ ์ „๋ฌธ๊ฐ€๊ฐ€ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
            """),
            HumanMessage(
                content=[
                    {"type": "text", "text": f"""
{patient_context}

์œ„ ํ™˜์ž์˜ ์˜๋ฃŒ ์˜์ƒ์„ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”:
1. ๊ด€์ฐฐ๋˜๋Š” ์ฃผ์š” ์†Œ๊ฒฌ
2. ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•œ ๋ถ€๋ถ„
3. ์ถ”๊ฐ€ ๊ฒ€์‚ฌ ๊ถŒ์žฅ์‚ฌํ•ญ
4. ์ผ๋ฐ˜์ ์ธ ๊ฐ๋ณ„ ์ง„๋‹จ ๋ชฉ๋ก
                    """},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        response = self.llm.invoke(messages)
        return response.content

# ์‚ฌ์šฉ ์˜ˆ์‹œ
medical_bot = MedicalAnalysisBot()
patient = {
    'age': 45,
    'gender': '๋‚จ์„ฑ',
    'symptoms': 'ํ‰ํ†ต, ํ˜ธํก๊ณค๋ž€',
    'history': '๊ณ ํ˜ˆ์••'
}
analysis = medical_bot.analyze_medical_image("chest_xray.jpg", patient)
print(analysis)

๐Ÿ›๏ธ ์ด์ปค๋จธ์Šค: ์Šค๋งˆํŠธ ์ƒํ’ˆ ์ถ”์ฒœ

๐ŸŽ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

๊ณ ๊ฐ์ด ์›ํ•˜๋Š” ์Šคํƒ€์ผ์˜ ์˜ท ์‚ฌ์ง„์„ ์ฐ์–ด์„œ ์˜ฌ๋ฆฌ๊ณ , "์ด๋Ÿฐ ๋А๋‚Œ์˜ ์˜ท์„ ์ฐพ๊ณ  ์žˆ์–ด์š”"๋ผ๊ณ  ๋งํ•˜๋ฉด AI๊ฐ€ ๋น„์Šทํ•œ ์ƒํ’ˆ์„ ์ฐพ์•„์ฃผ๊ณ  ์ฝ”๋”” ํŒ๊นŒ์ง€ ์ œ๊ณตํ•ด. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ์ด๋Ÿฐ ๊ธฐ์ˆ ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋”ฑ ๋งž๋Š” ์žฌ๋Šฅ์„ ์ถ”์ฒœํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€? ๐ŸŽจ
class EcommerceBot(AudioMultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview"):
        super().__init__(model_name)
        # ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค (์‹ค์ œ๋กœ๋Š” DB ์—ฐ๋™)
        self.product_db = []
    
    def analyze_style_preference(self, reference_images: List[str], 
                                 user_description: str):
        """
        ์‚ฌ์šฉ์ž์˜ ์Šคํƒ€์ผ ์„ ํ˜ธ๋„ ๋ถ„์„
        
        Args:
            reference_images: ์ฐธ๊ณ  ์ด๋ฏธ์ง€๋“ค
            user_description: ์‚ฌ์šฉ์ž ์„ค๋ช…
            
        Returns:
            ์Šคํƒ€์ผ ํ”„๋กœํ•„ ๋ฐ ์ถ”์ฒœ
        """
        encoded_images = [self.encode_image(img) for img in reference_images]
        
        content = [
            {"type": "text", "text": f"""
์‚ฌ์šฉ์ž ์„ค๋ช…: "{user_description}"

์œ„ ์„ค๋ช…๊ณผ ์•„๋ž˜ ์ด๋ฏธ์ง€๋“ค์„ ๋ถ„์„ํ•˜์—ฌ:
1. ์„ ํ˜ธํ•˜๋Š” ์Šคํƒ€์ผ (์บ์ฃผ์–ผ, ํฌ๋ฉ€, ์ŠคํŠธ๋ฆฟ ๋“ฑ)
2. ์„ ํ˜ธ ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ
3. ์ฃผ์š” ๋””์ž์ธ ์š”์†Œ
4. ์ถ”์ฒœ ์•„์ดํ…œ ์นดํ…Œ๊ณ ๋ฆฌ
5. ์ฝ”๋”” ํŒ

์„ ์ œ๊ณตํ•ด์ฃผ์„ธ์š”.
            """}
        ]
        
        for encoded_img in encoded_images:
            content.append({
                "type": "image_url",
                "image_url": {
                    "url": f"data:image/jpeg;base64,{encoded_img}"
                }
            })
        
        messages = [
            SystemMessage(content="ํŒจ์…˜ ์Šคํƒ€์ผ๋ฆฌ์ŠคํŠธ AI์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=content)
        ]
        
        response = self.llm.invoke(messages)
        return response.content
    
    def compare_products(self, product_images: List[str], criteria: str):
        """
        ์—ฌ๋Ÿฌ ์ƒํ’ˆ ๋น„๊ต
        
        Args:
            product_images: ๋น„๊ตํ•  ์ƒํ’ˆ ์ด๋ฏธ์ง€๋“ค
            criteria: ๋น„๊ต ๊ธฐ์ค€
            
        Returns:
            ์ƒ์„ธ ๋น„๊ต ๋ถ„์„
        """
        encoded_images = [self.encode_image(img) for img in product_images]
        
        content = [
            {"type": "text", "text": f"""
๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ์ƒํ’ˆ๋“ค์„ ๋น„๊ตํ•ด์ฃผ์„ธ์š”: {criteria}

๊ฐ ์ƒํ’ˆ์˜:
1. ์žฅ๋‹จ์ 
2. ๊ฐ€๊ฒฉ ๋Œ€๋น„ ๊ฐ€์น˜ (์ด๋ฏธ์ง€์—์„œ ํ’ˆ์งˆ ์ถ”์ •)
3. ์–ด์šธ๋ฆฌ๋Š” ์ƒํ™ฉ/์Šคํƒ€์ผ
4. ์ตœ์ข… ์ถ”์ฒœ ์ˆœ์œ„

๋ฅผ ์ œ๊ณตํ•ด์ฃผ์„ธ์š”.
            """}
        ]
        
        for idx, encoded_img in enumerate(encoded_images, 1):
            content.append({
                "type": "image_url",
                "image_url": {
                    "url": f"data:image/jpeg;base64,{encoded_img}"
                }
            })
        
        messages = [
            SystemMessage(content="์ƒํ’ˆ ๋น„๊ต ์ „๋ฌธ๊ฐ€ AI์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=content)
        ]
        
        response = self.llm.invoke(messages)
        return response.content

# ์‚ฌ์šฉ ์˜ˆ์‹œ
ecommerce_bot = EcommerceBot()

# ์Šคํƒ€์ผ ๋ถ„์„
style_analysis = ecommerce_bot.analyze_style_preference(
    ["style1.jpg", "style2.jpg"],
    "ํŽธ์•ˆํ•˜๋ฉด์„œ๋„ ์„ธ๋ จ๋œ ๋А๋‚Œ์˜ ์˜ท์„ ์ข‹์•„ํ•ด์š”"
)
print(f"์Šคํƒ€์ผ ๋ถ„์„:\n{style_analysis}\n")

# ์ƒํ’ˆ ๋น„๊ต
comparison = ecommerce_bot.compare_products(
    ["product1.jpg", "product2.jpg", "product3.jpg"],
    "๋””์ž์ธ, ํ’ˆ์งˆ, ํ™œ์šฉ๋„"
)
print(f"์ƒํ’ˆ ๋น„๊ต:\n{comparison}")

๐Ÿ“š ๊ต์œก: ํ•™์Šต ์ž๋ฃŒ ๋ถ„์„ ๋ฐ ํ”ผ๋“œ๋ฐฑ

๐ŸŽ“ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

ํ•™์ƒ์ด ์†์œผ๋กœ ์“ด ์ˆ˜ํ•™ ๋ฌธ์ œ ํ’€์ด๋ฅผ ์‚ฌ์ง„์œผ๋กœ ์ฐ์–ด ์˜ฌ๋ฆฌ๊ณ , ํ’€์ด ๊ณผ์ •์„ ์„ค๋ช…ํ•˜๋Š” ์Œ์„ฑ์„ ๋…น์Œํ•˜๋ฉด AI๊ฐ€ ์ •๋‹ต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  ์ƒ์„ธํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ด. ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด ์–ด๋””์„œ ์‹ค์ˆ˜ํ–ˆ๋Š”์ง€ ์ •ํ™•ํžˆ ์งš์–ด์ค˜.
class EducationBot(AudioMultimodalBot):
    def evaluate_homework(self, image_path: str, audio_path: str = None, 
                         subject: str = "์ˆ˜ํ•™"):
        """
        ๊ณผ์ œ ํ‰๊ฐ€ ๋ฐ ํ”ผ๋“œ๋ฐฑ
        
        Args:
            image_path: ๊ณผ์ œ ์ด๋ฏธ์ง€ (์†๊ธ€์”จ, ๊ทธ๋ฆผ ๋“ฑ)
            audio_path: ํ•™์ƒ์˜ ์„ค๋ช… ์Œ์„ฑ (์„ ํƒ)
            subject: ๊ณผ๋ชฉ
            
        Returns:
            ํ‰๊ฐ€ ๊ฒฐ๊ณผ ๋ฐ ํ”ผ๋“œ๋ฐฑ
        """
        base64_image = self.encode_image(image_path)
        
        # ์Œ์„ฑ์ด ์žˆ์œผ๋ฉด ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜
        audio_transcript = ""
        if audio_path:
            audio_transcript = self.transcribe_audio(audio_path)
        
        prompt_text = f"""
๊ณผ๋ชฉ: {subject}

์•„๋ž˜ ์ด๋ฏธ์ง€์˜ ๊ณผ์ œ๋ฅผ ํ‰๊ฐ€ํ•ด์ฃผ์„ธ์š”:

1. ์ •๋‹ต ์—ฌ๋ถ€ ๋ฐ ์ ์ˆ˜ (100์  ๋งŒ์ )
2. ์ž˜ํ•œ ์ 
3. ๊ฐœ์„ ์ด ํ•„์š”ํ•œ ์ 
4. ์ƒ์„ธํ•œ ํ•ด์„ค
5. ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ ์ถ”์ฒœ
        """
        
        if audio_transcript:
            prompt_text += f"\n\nํ•™์ƒ์˜ ์„ค๋ช…: \"{audio_transcript}\"\n(์„ค๋ช…๋„ ํ•จ๊ป˜ ๊ณ ๋ คํ•ด์ฃผ์„ธ์š”)"
        
        messages = [
            SystemMessage(content=f"{subject} ๊ต์œก ์ „๋ฌธ๊ฐ€ AI์ž…๋‹ˆ๋‹ค."),
            HumanMessage(
                content=[
                    {"type": "text", "text": prompt_text},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        response = self.llm.invoke(messages)
        return response.content
    
    def create_personalized_quiz(self, weak_areas: List[str], difficulty: str):
        """
        ๋งž์ถคํ˜• ํ€ด์ฆˆ ์ƒ์„ฑ
        
        Args:
            weak_areas: ์ทจ์•ฝํ•œ ์˜์—ญ ๋ฆฌ์ŠคํŠธ
            difficulty: ๋‚œ์ด๋„ (easy/medium/hard)
            
        Returns:
            ์ƒ์„ฑ๋œ ํ€ด์ฆˆ
        """
        messages = [
            SystemMessage(content="๊ต์œก ์ฝ˜ํ…์ธ  ์ œ์ž‘ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(content=f"""
๋‹ค์Œ ์˜์—ญ์— ๋Œ€ํ•œ {difficulty} ๋‚œ์ด๋„์˜ ํ€ด์ฆˆ๋ฅผ 5๋ฌธ์ œ ๋งŒ๋“ค์–ด์ฃผ์„ธ์š”:
{', '.join(weak_areas)}

๊ฐ ๋ฌธ์ œ๋Š”:
1. ๋ฌธ์ œ
2. ์„ ํƒ์ง€ (4๊ฐœ)
3. ์ •๋‹ต
4. ํ•ด์„ค

ํ˜•์‹์œผ๋กœ ์ œ๊ณตํ•ด์ฃผ์„ธ์š”.
            """)
        ]
        
        response = self.llm.invoke(messages)
        return response.content

# ์‚ฌ์šฉ ์˜ˆ์‹œ
edu_bot = EducationBot()

# ๊ณผ์ œ ํ‰๊ฐ€
feedback = edu_bot.evaluate_homework(
    "math_homework.jpg",
    "explanation.mp3",
    subject="์ˆ˜ํ•™"
)
print(f"ํ‰๊ฐ€ ๊ฒฐ๊ณผ:\n{feedback}\n")

# ๋งž์ถคํ˜• ํ€ด์ฆˆ
quiz = edu_bot.create_personalized_quiz(
    ["์ด์ฐจ๋ฐฉ์ •์‹", "์ธ์ˆ˜๋ถ„ํ•ด"],
    difficulty="medium"
)
print(f"์ƒ์„ฑ๋œ ํ€ด์ฆˆ:\n{quiz}")
๐ŸŒŸ ์žฌ๋Šฅ๋„ท์—์„œ์˜ ํ™œ์šฉ ์•„์ด๋””์–ด

์žฌ๋Šฅ๋„ท ํ”Œ๋žซํผ์—์„œ ์ด๋Ÿฐ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI๋ฅผ ํ™œ์šฉํ•˜๋ฉด ์ •๋ง ๋ฉ‹์ง„ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด:

โ€ข ํฌํŠธํด๋ฆฌ์˜ค ์ž๋™ ๋ถ„์„: ๋””์ž์ด๋„ˆ๋‚˜ ์•„ํ‹ฐ์ŠคํŠธ์˜ ์ž‘ํ’ˆ ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•ด์„œ ์Šคํƒ€์ผ๊ณผ ๊ฐ•์ ์„ ์ž๋™์œผ๋กœ ์š”์•ฝ
โ€ข ์žฌ๋Šฅ ๋งค์นญ: ์˜๋ขฐ์ธ์ด ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฌผ ์ƒ˜ํ”Œ๊ณผ ์„ค๋ช…์„ ์˜ฌ๋ฆฌ๋ฉด ๊ฐ€์žฅ ์ ํ•ฉํ•œ ์žฌ๋Šฅ ์ œ๊ณต์ž๋ฅผ ์ถ”์ฒœ
โ€ข ์ž‘์—…๋ฌผ ํ’ˆ์งˆ ๊ฒ€์ˆ˜: ์™„์„ฑ๋œ ์ž‘์—…๋ฌผ์„ AI๊ฐ€ ๋จผ์ € ๊ฒ€ํ† ํ•ด์„œ ๊ธฐ๋ณธ์ ์ธ ํ’ˆ์งˆ ์ฒดํฌ
โ€ข ํ•™์Šต ์ฝ˜ํ…์ธ  ์ถ”์ฒœ: ์‚ฌ์šฉ์ž์˜ ๊ด€์‹ฌ์‚ฌ์™€ ์ˆ˜์ค€์„ ๋ถ„์„ํ•ด์„œ ๋งž์ถคํ˜• ๊ฐ•์˜ ์ถ”์ฒœ

โšก ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ฐ ๋น„์šฉ ์ ˆ๊ฐ

๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI๋Š” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ, ์ž˜๋ชป ์‚ฌ์šฉํ•˜๋ฉด API ๋น„์šฉ์ด ์—„์ฒญ๋‚˜๊ฒŒ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์ด๋ฏธ์ง€์™€ ์˜ค๋””์˜ค๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ๋Š” ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰์ด ๋งŽ์•„์ง€๊ฑฐ๋“ . ๋˜‘๋˜‘ํ•˜๊ฒŒ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ’ฐ

๐ŸŽฏ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”

from PIL import Image
import io

class OptimizedMultimodalBot(AudioMultimodalBot):
    def optimize_image(self, image_path: str, max_size=(1024, 1024), 
                      quality=85):
        """
        ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (ํฌ๊ธฐ ์กฐ์ • ๋ฐ ์••์ถ•)
        
        Args:
            image_path: ์›๋ณธ ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            max_size: ์ตœ๋Œ€ ํฌ๊ธฐ (width, height)
            quality: JPEG ํ’ˆ์งˆ (1-100)
            
        Returns:
            ์ตœ์ ํ™”๋œ ์ด๋ฏธ์ง€์˜ base64 ๋ฌธ์ž์—ด
        """
        # ์ด๋ฏธ์ง€ ์—ด๊ธฐ
        img = Image.open(image_path)
        
        # RGBA๋ฅผ RGB๋กœ ๋ณ€ํ™˜ (JPEG๋Š” ํˆฌ๋ช…๋„ ๋ฏธ์ง€์›)
        if img.mode in ('RGBA', 'LA', 'P'):
            background = Image.new('RGB', img.size, (255, 255, 255))
            if img.mode == 'P':
                img = img.convert('RGBA')
            background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
            img = background
        
        # ๋น„์œจ ์œ ์ง€ํ•˜๋ฉฐ ๋ฆฌ์‚ฌ์ด์ง•
        img.thumbnail(max_size, Image.Resampling.LANCZOS)
        
        # ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅ
        buffer = io.BytesIO()
        img.save(buffer, format='JPEG', quality=quality, optimize=True)
        buffer.seek(0)
        
        # base64 ์ธ์ฝ”๋”ฉ
        import base64
        return base64.b64encode(buffer.read()).decode('utf-8')
    
    def batch_process_images(self, image_paths: List[str], question: str,
                            batch_size: int = 3):
        """
        ์ด๋ฏธ์ง€๋ฅผ ๋ฐฐ์น˜๋กœ ๋‚˜๋ˆ ์„œ ์ฒ˜๋ฆฌ (๋น„์šฉ ์ ˆ๊ฐ)
        
        Args:
            image_paths: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ ๋ฆฌ์ŠคํŠธ
            question: ์งˆ๋ฌธ
            batch_size: ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ์ด๋ฏธ์ง€ ์ˆ˜
            
        Returns:
            ๋ฐฐ์น˜๋ณ„ ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        results = []
        
        for i in range(0, len(image_paths), batch_size):
            batch = image_paths[i:i+batch_size]
            
            # ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
            encoded_images = [self.optimize_image(img) for img in batch]
            
            content = [
                {"type": "text", "text": f"{question} (์ด๋ฏธ์ง€ {i+1}-{i+len(batch)})"}
            ]
            
            for encoded_img in encoded_images:
                content.append({
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/jpeg;base64,{encoded_img}",
                        "detail": "low"  # ์ €ํ•ด์ƒ๋„ ๋ชจ๋“œ๋กœ ๋น„์šฉ ์ ˆ๊ฐ
                    }
                })
            
            messages = [
                SystemMessage(content="์ด๋ฏธ์ง€ ๋ฐฐ์น˜ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
                HumanMessage(content=content)
            ]
            
            response = self.llm.invoke(messages)
            results.append({
                'batch': i // batch_size + 1,
                'images': batch,
                'analysis': response.content
            })
        
        return results

๐Ÿ’พ ์บ์‹ฑ ์ „๋žต

import hashlib
import json
from functools import lru_cache
import pickle

class CachedMultimodalBot(OptimizedMultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview", cache_dir="./cache"):
        super().__init__(model_name)
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)
    
    def get_cache_key(self, *args):
        """
        ์บ์‹œ ํ‚ค ์ƒ์„ฑ
        """
        # ์ธ์ž๋“ค์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ•ด์‹œ ์ƒ์„ฑ
        key_string = json.dumps(args, sort_keys=True)
        return hashlib.md5(key_string.encode()).hexdigest()
    
    def get_cached_result(self, cache_key: str):
        """
        ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
        """
        cache_file = os.path.join(self.cache_dir, f"{cache_key}.pkl")
        if os.path.exists(cache_file):
            with open(cache_file, 'rb') as f:
                return pickle.load(f)
        return None
    
    def save_to_cache(self, cache_key: str, result):
        """
        ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅ
        """
        cache_file = os.path.join(self.cache_dir, f"{cache_key}.pkl")
        with open(cache_file, 'wb') as f:
            pickle.dump(result, f)
    
    def analyze_with_cache(self, image_path: str, question: str):
        """
        ์บ์‹ฑ์„ ํ™œ์šฉํ•œ ๋ถ„์„ (๋™์ผํ•œ ์š”์ฒญ์€ ์žฌ์‚ฌ์šฉ)
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            question: ์งˆ๋ฌธ
            
        Returns:
            ๋ถ„์„ ๊ฒฐ๊ณผ (์บ์‹œ ๋˜๋Š” ์ƒˆ๋กœ์šด ๋ถ„์„)
        """
        # ์ด๋ฏธ์ง€ ํ•ด์‹œ ๊ณ„์‚ฐ
        with open(image_path, 'rb') as f:
            image_hash = hashlib.md5(f.read()).hexdigest()
        
        # ์บ์‹œ ํ‚ค ์ƒ์„ฑ
        cache_key = self.get_cache_key(image_hash, question, self.llm.model_name)
        
        # ์บ์‹œ ํ™•์ธ
        cached_result = self.get_cached_result(cache_key)
        if cached_result:
            print("โœ… ์บ์‹œ์—์„œ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค!")
            return cached_result
        
        # ์ƒˆ๋กœ์šด ๋ถ„์„ ์ˆ˜ํ–‰
        print("๐Ÿ”„ ์ƒˆ๋กœ์šด ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค...")
        result = self.analyze_image_with_text(image_path, question)
        
        # ์บ์‹œ์— ์ €์žฅ
        self.save_to_cache(cache_key, result)
        
        return result

# ์‚ฌ์šฉ ์˜ˆ์‹œ
cached_bot = CachedMultimodalBot()

# ์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ - ์‹ค์ œ API ํ˜ธ์ถœ
result1 = cached_bot.analyze_with_cache("product.jpg", "์ด ์ œํ’ˆ์˜ ํŠน์ง•์€?")
print(result1)

# ๋‘ ๋ฒˆ์งธ ํ˜ธ์ถœ - ์บ์‹œ์—์„œ ๊ฐ€์ ธ์˜ด (๋น„์šฉ 0)
result2 = cached_bot.analyze_with_cache("product.jpg", "์ด ์ œํ’ˆ์˜ ํŠน์ง•์€?")
print(result2)
๐Ÿ’ก ๋น„์šฉ ์ ˆ๊ฐ ํŒ ๋ชจ์Œ

1. ์ด๋ฏธ์ง€ ํ•ด์ƒ๋„ ์กฐ์ ˆ: "detail": "low" ์˜ต์…˜ ์‚ฌ์šฉ ์‹œ ๋น„์šฉ์ด ์•ฝ 1/3๋กœ ๊ฐ์†Œ
2. ํ”„๋กฌํ”„ํŠธ ์ตœ์ ํ™”: ๋ถˆํ•„์š”ํ•œ ์„ค๋ช… ์ œ๊ฑฐ, ํ•ต์‹ฌ๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ
3. ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ: ์—ฌ๋Ÿฌ ์š”์ฒญ์„ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ์ฒ˜๋ฆฌ
4. ์บ์‹ฑ: ๋™์ผํ•œ ์š”์ฒญ์€ ์žฌ์‚ฌ์šฉ (ํŠนํžˆ ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ)
5. ๋ชจ๋ธ ์„ ํƒ: ๊ฐ„๋‹จํ•œ ์ž‘์—…์€ GPT-3.5 ๊ฐ™์€ ์ €๋ ดํ•œ ๋ชจ๋ธ ์‚ฌ์šฉ
6. ์ŠคํŠธ๋ฆฌ๋ฐ: ๊ธด ์‘๋‹ต์€ ์ŠคํŠธ๋ฆฌ๋ฐ์œผ๋กœ ๋ฐ›์•„์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

โš™๏ธ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋กœ ์†๋„ ํ–ฅ์ƒ

import asyncio
from langchain_openai import ChatOpenAI

class AsyncMultimodalBot(CachedMultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview"):
        super().__init__(model_name)
        # ๋น„๋™๊ธฐ LLM ์ดˆ๊ธฐํ™”
        self.async_llm = ChatOpenAI(
            model=model_name,
            temperature=0.7,
            max_tokens=1000
        )
    
    async def analyze_async(self, image_path: str, question: str):
        """
        ๋น„๋™๊ธฐ ์ด๋ฏธ์ง€ ๋ถ„์„
        """
        base64_image = self.optimize_image(image_path)
        
        messages = [
            SystemMessage(content="์ด๋ฏธ์ง€ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค."),
            HumanMessage(
                content=[
                    {"type": "text", "text": question},
                    {
                        "type": "image_url",
                        "image_url": {
                            "url": f"data:image/jpeg;base64,{base64_image}"
                        }
                    }
                ]
            )
        ]
        
        # ๋น„๋™๊ธฐ ํ˜ธ์ถœ
        response = await self.async_llm.ainvoke(messages)
        return response.content
    
    async def analyze_multiple_async(self, tasks: List[tuple]):
        """
        ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
        
        Args:
            tasks: [(image_path, question), ...] ํ˜•ํƒœ์˜ ๋ฆฌ์ŠคํŠธ
            
        Returns:
            ๋ชจ๋“  ๋ถ„์„ ๊ฒฐ๊ณผ
        """
        # ๋ชจ๋“  ์ž‘์—…์„ ๋™์‹œ์— ์‹คํ–‰
        results = await asyncio.gather(*[
            self.analyze_async(img, q) for img, q in tasks
        ])
        
        return results

# ์‚ฌ์šฉ ์˜ˆ์‹œ
async def main():
    bot = AsyncMultimodalBot()
    
    # ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ๋™์‹œ์— ์ฒ˜๋ฆฌ
    tasks = [
        ("image1.jpg", "์ด ์ด๋ฏธ์ง€์˜ ์ฃผ์š” ๊ฐ์ฒด๋Š”?"),
        ("image2.jpg", "์ด ์ด๋ฏธ์ง€์˜ ์ƒ‰์ƒ ํ†ค์€?"),
        ("image3.jpg", "์ด ์ด๋ฏธ์ง€์˜ ๋ถ„์œ„๊ธฐ๋Š”?")
    ]
    
    import time
    start = time.time()
    results = await bot.analyze_multiple_async(tasks)
    end = time.time()
    
    print(f"โฑ๏ธ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {end - start:.2f}์ดˆ")
    for i, result in enumerate(results, 1):
        print(f"\n๊ฒฐ๊ณผ {i}:\n{result}")

# ์‹คํ–‰
# asyncio.run(main())
์ฒ˜๋ฆฌ ๋ฐฉ์‹ 3๊ฐœ ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์žฅ์  ๋‹จ์ 
์ˆœ์ฐจ ์ฒ˜๋ฆฌ ~30์ดˆ ๊ตฌํ˜„ ๊ฐ„๋‹จ, ์•ˆ์ •์  ๋А๋ฆผ, ๋น„ํšจ์œจ์ 
๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ~10์ดˆ ๋น ๋ฆ„, ํšจ์œจ์  ๋ณต์žก๋„ ์ฆ๊ฐ€
๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ~12์ดˆ ๋น„์šฉ ์ ˆ๊ฐ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌ ์ œํ•œ
์บ์‹ฑ ํ™œ์šฉ ~0.1์ดˆ (์บ์‹œ ํžˆํŠธ) ๋งค์šฐ ๋น ๋ฆ„, ๋ฌด๋ฃŒ ์ €์žฅ ๊ณต๊ฐ„ ํ•„์š”

๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ์—๋Ÿฌ ์ฒ˜๋ฆฌ

์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ๋•Œ๋Š” ๋ณด์•ˆ๊ณผ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ์ •๋ง ์ค‘์š”ํ•ด. ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธํ•˜๊ณ , ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ƒํ™ฉ์—๋„ ์„œ๋น„์Šค๊ฐ€ ์ค‘๋‹จ๋˜์ง€ ์•Š๋„๋ก ํ•ด์•ผ ํ•˜๊ฑฐ๋“ . ๐Ÿ›ก๏ธ

๐Ÿ” ๋ณด์•ˆ ๊ฐ•ํ™”

import os
from typing import Optional
import logging
from datetime import datetime, timedelta
import jwt

class SecureMultimodalBot(AsyncMultimodalBot):
    def __init__(self, model_name="gpt-4-vision-preview", 
                 max_file_size_mb=10,
                 allowed_extensions={'.jpg', '.jpeg', '.png', '.mp3', '.wav'}):
        super().__init__(model_name)
        self.max_file_size = max_file_size_mb * 1024 * 1024  # MB to bytes
        self.allowed_extensions = allowed_extensions
        
        # ๋กœ๊น… ์„ค์ •
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
            filename='multimodal_bot.log'
        )
        self.logger = logging.getLogger(__name__)
    
    def validate_file(self, file_path: str) -> tuple[bool, Optional[str]]:
        """
        ํŒŒ์ผ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
        
        Returns:
            (์œ ํšจ ์—ฌ๋ถ€, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€)
        """
        # ํŒŒ์ผ ์กด์žฌ ํ™•์ธ
        if not os.path.exists(file_path):
            return False, "ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."
        
        # ํ™•์žฅ์ž ํ™•์ธ
        _, ext = os.path.splitext(file_path)
        if ext.lower() not in self.allowed_extensions:
            return False, f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {self.allowed_extensions}"
        
        # ํŒŒ์ผ ํฌ๊ธฐ ํ™•์ธ
        file_size = os.path.getsize(file_path)
        if file_size > self.max_file_size:
            return False, f"ํŒŒ์ผ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค. (์ตœ๋Œ€: {self.max_file_size / 1024 / 1024}MB)"
        
        # ํŒŒ์ผ ๋‚ด์šฉ ๊ฒ€์ฆ (์ด๋ฏธ์ง€์ธ ๊ฒฝ์šฐ)
        if ext.lower() in {'.jpg', '.jpeg', '.png'}:
            try:
                from PIL import Image
                img = Image.open(file_path)
                img.verify()  # ์†์ƒ๋œ ์ด๋ฏธ์ง€ ์ฒดํฌ
            except Exception as e:
                return False, f"์†์ƒ๋œ ์ด๋ฏธ์ง€ ํŒŒ์ผ์ž…๋‹ˆ๋‹ค: {str(e)}"
        
        return True, None
    
    def sanitize_input(self, text: str, max_length: int = 5000) -> str:
        """
        ์ž…๋ ฅ ํ…์ŠคํŠธ ์ •์ œ
        
        Args:
            text: ์›๋ณธ ํ…์ŠคํŠธ
            max_length: ์ตœ๋Œ€ ๊ธธ์ด
            
        Returns:
            ์ •์ œ๋œ ํ…์ŠคํŠธ
        """
        # ๊ธธ์ด ์ œํ•œ
        text = text[:max_length]
        
        # ์œ„ํ—˜ํ•œ ๋ฌธ์ž ์ œ๊ฑฐ (SQL injection, XSS ๋“ฑ ๋ฐฉ์ง€)
        dangerous_chars = ['<script>', '</script>', 'javascript:', 'onerror=']
        for char in dangerous_chars:
            text = text.replace(char, '')
        
        # ์—ฐ์†๋œ ๊ณต๋ฐฑ ์ œ๊ฑฐ
        text = ' '.join(text.split())
        
        return text.strip()
    
    def generate_access_token(self, user_id: str, expiry_hours: int = 24) -> str:
        """
        ์ ‘๊ทผ ํ† ํฐ ์ƒ์„ฑ (API ์ธ์ฆ์šฉ)
        
        Args:
            user_id: ์‚ฌ์šฉ์ž ID
            expiry_hours: ๋งŒ๋ฃŒ ์‹œ๊ฐ„ (์‹œ๊ฐ„)
            
        Returns:
            JWT ํ† ํฐ
        """
        secret_key = os.getenv('JWT_SECRET_KEY', 'your-secret-key')
        
        payload = {
            'user_id': user_id,
            'exp': datetime.utcnow() + timedelta(hours=expiry_hours),
            'iat': datetime.utcnow()
        }
        
        token = jwt.encode(payload, secret_key, algorithm='HS256')
        return token
    
    def verify_access_token(self, token: str) -> tuple[bool, Optional[dict]]:
        """
        ์ ‘๊ทผ ํ† ํฐ ๊ฒ€์ฆ
        
        Returns:
            (์œ ํšจ ์—ฌ๋ถ€, ํŽ˜์ด๋กœ๋“œ)
        """
        try:
            secret_key = os.getenv('JWT_SECRET_KEY', 'your-secret-key')
            payload = jwt.decode(token, secret_key, algorithms=['HS256'])
            return True, payload
        except jwt.ExpiredSignatureError:
            return False, None
        except jwt.InvalidTokenError:
            return False, None
    
    def safe_analyze(self, image_path: str, question: str, 
                    user_token: Optional[str] = None):
        """
        ์•ˆ์ „ํ•œ ๋ถ„์„ (๋ชจ๋“  ๊ฒ€์ฆ ํฌํ•จ)
        
        Args:
            image_path: ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ
            question: ์งˆ๋ฌธ
            user_token: ์‚ฌ์šฉ์ž ์ธ์ฆ ํ† ํฐ
            
        Returns:
            ๋ถ„์„ ๊ฒฐ๊ณผ ๋˜๋Š” ์—๋Ÿฌ
        """
        try:
            # ํ† ํฐ ๊ฒ€์ฆ (์„ ํƒ์ )
            if user_token:
                is_valid, payload = self.verify_access_token(user_token)
                if not is_valid:
                    self.logger.warning(f"Invalid token attempt")
                    return {"error": "์ธ์ฆ ์‹คํŒจ", "code": 401}
                user_id = payload.get('user_id')
            else:
                user_id = "anonymous"
            
            # ํŒŒ์ผ ๊ฒ€์ฆ
            is_valid, error_msg = self.validate_file(image_path)
            if not is_valid:
                self.logger.warning(f"File validation failed: {error_msg}")
                return {"error": error_msg, "code": 400}
            
            # ์ž…๋ ฅ ์ •์ œ
            clean_question = self.sanitize_input(question)
            
            # ๋กœ๊น…
            self.logger.info(f"User {user_id} analyzing image: {image_path}")
            
            # ๋ถ„์„ ์ˆ˜ํ–‰
            result = self.analyze_image_with_text(image_path, clean_question)
            
            self.logger.info(f"Analysis completed for user {user_id}")
            
            return {
                "success": True,
                "result": result,
                "timestamp": datetime.utcnow().isoformat()
            }
            
        except Exception as e:
            self.logger.error(f"Error in safe_analyze: {str(e)}", exc_info=True)
            return {
                "error": "๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.",
                "code": 500,
                "details": str(e) if os.getenv('DEBUG') == 'True' else None
            }

# ์‚ฌ์šฉ ์˜ˆ์‹œ
secure_bot = SecureMultimodalBot()

# ํ† ํฐ ์ƒ์„ฑ
token = secure_bot.generate_access_token("user123")

# ์•ˆ์ „ํ•œ ๋ถ„์„
result = secure_bot.safe_analyze(
    "user_upload.jpg",
    "์ด ์ด๋ฏธ์ง€๋ฅผ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”",
    user_token=token
)

if result.get("success"):
    print(f"๋ถ„์„ ๊ฒฐ๊ณผ: {result['result']}")
else:
    print(f"์—๋Ÿฌ: {result['error']}")

๐Ÿšจ ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง

import time
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from requests.exceptions import RequestException, Timeout

class RobustMultimodalBot(SecureMultimodalBot):
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        retry=retry_if_exception_type((RequestException, Timeout))
    )
    def analyze_with_retry(self, image_path: str, question: str):
        """
        ์žฌ์‹œ๋„ ๋กœ์ง์ด ํฌํ•จ๋œ ๋ถ„์„
        
        ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๋‚˜ ์ผ์‹œ์  ์žฅ์•  ์‹œ ์ž๋™์œผ๋กœ ์žฌ์‹œ๋„
        """
        try:
            return self.analyze_image_with_text(image_path, question)
        except Exception as e:
            self.logger.error(f"Analysis failed: {str(e)}")
            raise
    
    def analyze_with_fallback(self, image_path: str, question: str,
                             fallback_model: str = "gpt-3.5-turbo"):
        """
        ๋Œ€์ฒด ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•œ ํด๋ฐฑ ์ฒ˜๋ฆฌ
        
        ์ฃผ ๋ชจ๋ธ ์‹คํŒจ ์‹œ ๋” ์ €๋ ดํ•œ ๋ชจ๋ธ๋กœ ์ž๋™ ์ „ํ™˜
        """
        try:
            # ์ฃผ ๋ชจ๋ธ ์‹œ๋„
            return self.analyze_with_retry(image_path, question)
        except Exception as e:
            self.logger.warning(f"Primary model failed, trying fallback: {str(e)}")
            
            try:
                # ํด๋ฐฑ ๋ชจ๋ธ๋กœ ์ „ํ™˜
                original_model = self.llm.model_name
                self.llm.model_name = fallback_model
                
                result = self.analyze_image_with_text(image_path, question)
                
                # ์›๋ž˜ ๋ชจ๋ธ๋กœ ๋ณต๊ตฌ
                self.llm.model_name = original_model
                
                return {
                    "result": result,
                    "fallback_used": True,
                    "fallback_model": fallback_model
                }
            except Exception as fallback_error:
                self.logger.error(f"Fallback also failed: {str(fallback_error)}")
                return {
                    "error": "๋ชจ๋“  ๋ถ„์„ ์‹œ๋„๊ฐ€ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.",
                    "details": str(fallback_error)
                }
    
    def batch_analyze_with_error_handling(self, tasks: List[tuple]):
        """
        ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ ๊ฐœ๋ณ„ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
        
        ์ผ๋ถ€ ์ž‘์—…์ด ์‹คํŒจํ•ด๋„ ๋‚˜๋จธ์ง€๋Š” ๊ณ„์† ์ง„ํ–‰
        """
        results = []
        
        for idx, (image_path, question) in enumerate(tasks):
            try:
                result = self.analyze_with_fallback(image_path, question)
                results.append({
                    "index": idx,
                    "success": True,
                    "data": result
                })
            except Exception as e:
                self.logger.error(f"Task {idx} failed: {str(e)}")
                results.append({
                    "index": idx,
                    "success": False,
                    "error": str(e)
                })
        
        # ํ†ต๊ณ„ ์ •๋ณด ์ถ”๊ฐ€
        success_count = sum(1 for r in results if r["success"])
        
        return {
            "results": results,
            "statistics": {
                "total": len(tasks),
                "success": success_count,
                "failed": len(tasks) - success_count,
                "success_rate": f"{success_count / len(tasks) * 100:.1f}%"
            }
        }

# ์‚ฌ์šฉ ์˜ˆ์‹œ
robust_bot = RobustMultimodalBot()

# ์žฌ์‹œ๋„ ๋กœ์ง ํ…Œ์ŠคํŠธ
result = robust_bot.analyze_with_retry("image.jpg", "๋ถ„์„ํ•ด์ฃผ์„ธ์š”")

# ํด๋ฐฑ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
result = robust_bot.analyze_with_fallback("image.jpg", "๋ถ„์„ํ•ด์ฃผ์„ธ์š”")

# ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ
tasks = [
    ("image1.jpg", "์งˆ๋ฌธ1"),
    ("image2.jpg", "์งˆ๋ฌธ2"),
    ("invalid.jpg", "์งˆ๋ฌธ3"),  # ์‹คํŒจํ•  ์ž‘์—…
]
batch_result = robust_bot.batch_analyze_with_error_handling(tasks)
print(f"์„ฑ๊ณต๋ฅ : {batch_result['statistics']['success_rate']}")
โš ๏ธ ํ”„๋กœ๋•์…˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ
  • โœ… API ํ‚ค๋ฅผ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ (.env ํŒŒ์ผ ์‚ฌ์šฉ)
  • โœ… ํŒŒ์ผ ์—…๋กœ๋“œ ํฌ๊ธฐ ์ œํ•œ ์„ค์ •
  • โœ… ํ—ˆ์šฉ๋œ ํŒŒ์ผ ํ˜•์‹๋งŒ ์ฒ˜๋ฆฌ
  • โœ… ์‚ฌ์šฉ์ž ์ž…๋ ฅ ๊ฒ€์ฆ ๋ฐ ์ •์ œ
  • โœ… ์—๋Ÿฌ ๋กœ๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง
  • โœ… ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„
  • โœ… ํด๋ฐฑ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ์ค€๋น„
  • โœ… ์†๋„ ์ œํ•œ(Rate Limiting) ์ ์šฉ
  • โœ… HTTPS ์‚ฌ์šฉ
  • โœ… ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ์—…๋ฐ์ดํŠธ

๐Ÿš€ ๋ฐฐํฌ ๋ฐ ์„œ๋น„์Šคํ™”

์ด์ œ ๋งŒ๋“  ๋ด‡์„ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ฐฐํฌํ•ด๋ณด์ž! FastAPI๋ฅผ ์‚ฌ์šฉํ•ด์„œ REST API๋กœ ๋งŒ๋“ค๋ฉด ์›น์ด๋‚˜ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด. ๐ŸŒ

๐Ÿ”Œ FastAPI๋กœ API ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ

๋จผ์ € ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•˜์ž:

pip install fastapi uvicorn python-multipart
from fastapi import FastAPI, File, UploadFile, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import shutil
from pathlib import Path
import uuid

app = FastAPI(
    title="๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ API",
    description="์ด๋ฏธ์ง€์™€ ์˜ค๋””์˜ค๋ฅผ ๋ถ„์„ํ•˜๋Š” AI API",
    version="1.0.0"
)

# CORS ์„ค์ • (ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋„๋ก)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํŠน์ • ๋„๋ฉ”์ธ๋งŒ ํ—ˆ์šฉ
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ๋ด‡ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
bot = RobustMultimodalBot()

# ์ž„์‹œ ํŒŒ์ผ ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ
UPLOAD_DIR = Path("./uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

@app.get("/")
async def root():
    """
    API ์ƒํƒœ ํ™•์ธ
    """
    return {
        "status": "running",
        "message": "๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ API๊ฐ€ ์ •์ƒ ์ž‘๋™ ์ค‘์ž…๋‹ˆ๋‹ค.",
        "version": "1.0.0"
    }

@app.post("/analyze/image")
async def analyze_image(
    file: UploadFile = File(...),
    question: str = Form(...),
    token: str = Form(None)
):
    """
    ์ด๋ฏธ์ง€ ๋ถ„์„ ์—”๋“œํฌ์ธํŠธ
    
    Parameters:
    - file: ๋ถ„์„ํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ
    - question: ์‚ฌ์šฉ์ž ์งˆ๋ฌธ
    - token: ์ธ์ฆ ํ† ํฐ (์„ ํƒ)
    
    Returns:
    - ๋ถ„์„ ๊ฒฐ๊ณผ
    """
    # ๊ณ ์œ  ํŒŒ์ผ๋ช… ์ƒ์„ฑ
    file_id = str(uuid.uuid4())
    file_extension = Path(file.filename).suffix
    file_path = UPLOAD_DIR / f"{file_id}{file_extension}"
    
    try:
        # ํŒŒ์ผ ์ €์žฅ
        with file_path.open("wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        
        # ๋ถ„์„ ์ˆ˜ํ–‰
        result = bot.safe_analyze(str(file_path), question, token)
        
        # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
        file_path.unlink()
        
        if result.get("error"):
            raise HTTPException(
                status_code=result.get("code", 500),
                detail=result["error"]
            )
        
        return JSONResponse(content=result)
        
    except Exception as e:
        # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ํŒŒ์ผ ์ •๋ฆฌ
        if file_path.exists():
            file_path.unlink()
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/analyze/audio")
async def analyze_audio(
    file: UploadFile = File(...),
    analysis_type: str = Form("transcribe")  # transcribe, sentiment, summary
):
    """
    ์˜ค๋””์˜ค ๋ถ„์„ ์—”๋“œํฌ์ธํŠธ
    
    Parameters:
    - file: ์˜ค๋””์˜ค ํŒŒ์ผ
    - analysis_type: ๋ถ„์„ ์œ ํ˜• (transcribe/sentiment/summary)
    
    Returns:
    - ๋ถ„์„ ๊ฒฐ๊ณผ
    """
    file_id = str(uuid.uuid4())
    file_extension = Path(file.filename).suffix
    file_path = UPLOAD_DIR / f"{file_id}{file_extension}"
    
    try:
        with file_path.open("wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
        
        if analysis_type == "transcribe":
            result = bot.transcribe_audio(str(file_path))
        elif analysis_type == "sentiment":
            result = bot.analyze_audio_sentiment(str(file_path))
        elif analysis_type == "summary":
            result = bot.summarize_audio(str(file_path))
        else:
            raise HTTPException(status_code=400, detail="Invalid analysis type")
        
        file_path.unlink()
        
        return JSONResponse(content={"success": True, "result": result})
        
    except Exception as e:
        if file_path.exists():
            file_path.unlink()
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/analyze/multimodal")
async def analyze_multimodal(
    image: UploadFile = File(...),
    audio: UploadFile = File(None),
    question: str = Form(...)
):
    """
    ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ถ„์„ ์—”๋“œํฌ์ธํŠธ (์ด๋ฏธ์ง€ + ์˜ค๋””์˜ค)
    
    Parameters:
    - image: ์ด๋ฏธ์ง€ ํŒŒ์ผ
    - audio: ์˜ค๋””์˜ค ํŒŒ์ผ (์„ ํƒ)
    - question: ์งˆ๋ฌธ
    
    Returns:
    - ํ†ตํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ
    """
    image_id = str(uuid.uuid4())
    image_path = UPLOAD_DIR / f"{image_id}{Path(image.filename).suffix}"
    
    audio_path = None
    if audio:
        audio_id = str(uuid.uuid4())
        audio_path = UPLOAD_DIR / f"{audio_id}{Path(audio.filename).suffix}"
    
    try:
        # ์ด๋ฏธ์ง€ ์ €์žฅ
        with image_path.open("wb") as buffer:
            shutil.copyfileobj(image.file, buffer)
        
        # ์˜ค๋””์˜ค ์ €์žฅ (์žˆ๋Š” ๊ฒฝ์šฐ)
        if audio and audio_path:
            with audio_path.open("wb") as buffer:
                shutil.copyfileobj(audio.file, buffer)
            
            result = bot.multimodal_analysis(
                str(image_path),
                str(audio_path),
                question
            )
        else:
            result = bot.analyze_image_with_text(str(image_path), question)
        
        # ํŒŒ์ผ ์ •๋ฆฌ
        image_path.unlink()
        if audio_path and audio_path.exists():
            audio_path.unlink()
        
        return JSONResponse(content={"success": True, "result": result})
        
    except Exception as e:
        # ์—๋Ÿฌ ์‹œ ํŒŒ์ผ ์ •๋ฆฌ
        if image_path.exists():
            image_path.unlink()
        if audio_path and audio_path.exists():
            audio_path.unlink()
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/auth/token")
async def create_token(user_id: str = Form(...)):
    """
    ์ธ์ฆ ํ† ํฐ ์ƒ์„ฑ
    
    Parameters:
    - user_id: ์‚ฌ์šฉ์ž ID
    
    Returns:
    - JWT ํ† ํฐ
    """
    token = bot.generate_access_token(user_id)
    return {"token": token, "expires_in": "24h"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด:

python api_server.py

# ๋˜๋Š”
uvicorn api_server:app --reload --host 0.0.0.0 --port 8000

์ด์ œ ๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:8000/docs๋กœ ์ ‘์†ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ API ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด! FastAPI์˜ Swagger UI๊ฐ€ ์ •๋ง ํŽธ๋ฆฌํ•˜์ง€? ๐Ÿ“š

๐Ÿณ Docker๋กœ ์ปจํ…Œ์ด๋„ˆํ™”

๋ฐฐํฌ๋ฅผ ์‰ฝ๊ฒŒ ํ•˜๋ ค๋ฉด Docker๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒŒ ์ข‹์•„. Dockerfile์„ ๋งŒ๋“ค์–ด๋ณด์ž:

# Dockerfile
FROM python:3.10-slim

WORKDIR /app

# ์‹œ์Šคํ…œ ํŒจํ‚ค์ง€ ์„ค์น˜
RUN apt-get update && apt-get install -y \
    ffmpeg \
    libsm6 \
    libxext6 \
    && rm -rf /var/lib/apt/lists/*

# Python ํŒจํ‚ค์ง€ ์„ค์น˜
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ณต์‚ฌ
COPY . .

# ํฌํŠธ ๋…ธ์ถœ
EXPOSE 8000

# ์„œ๋ฒ„ ์‹คํ–‰
CMD ["uvicorn", "api_server:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt ํŒŒ์ผ๋„ ๋งŒ๋“ค์–ด์•ผ ํ•ด:

langchain==0.1.0
langchain-openai==0.0.5
langchain-anthropic==0.1.0
fastapi==0.109.0
uvicorn==0.27.0
python-multipart==0.0.6
pillow==10.2.0
python-dotenv==1.0.0
openai-whisper==20231117
pydub==0.25.1
tenacity==8.2.3
pyjwt==2.8.0

Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ ๋ฐ ์‹คํ–‰:

# ์ด๋ฏธ์ง€ ๋นŒ๋“œ
docker build -t multimodal-bot:latest .

# ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
docker run -d \
  --name multimodal-bot \
  -p 8000:8000 \
  -e OPENAI_API_KEY=your_key_here \
  -v $(pwd)/uploads:/app/uploads \
  multimodal-bot:latest

# ๋กœ๊ทธ ํ™•์ธ
docker logs -f multimodal-bot

โ˜๏ธ ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ (AWS ์˜ˆ์‹œ)

๐ŸŒฉ๏ธ AWS EC2์— ๋ฐฐํฌํ•˜๊ธฐ

1๋‹จ๊ณ„: EC2 ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ (Ubuntu 22.04, t3.medium ์ด์ƒ ๊ถŒ์žฅ)
2๋‹จ๊ณ„: ๋ณด์•ˆ ๊ทธ๋ฃน์—์„œ 8000๋ฒˆ ํฌํŠธ ๊ฐœ๋ฐฉ
3๋‹จ๊ณ„: SSH๋กœ ์ ‘์† ํ›„ Docker ์„ค์น˜
4๋‹จ๊ณ„: ์ฝ”๋“œ ์—…๋กœ๋“œ ๋ฐ Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ
5๋‹จ๊ณ„: ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰
6๋‹จ๊ณ„: Nginx๋กœ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์„ค์ • (HTTPS ์ ์šฉ)
# Nginx ์„ค์ • ์˜ˆ์‹œ (/etc/nginx/sites-available/multimodal-bot)
server {
    listen 80;
    server_name your-domain.com;
    
    location / {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # ํŒŒ์ผ ์—…๋กœ๋“œ ํฌ๊ธฐ ์ œํ•œ
        client_max_body_size 50M;
    }
}

# HTTPS ์„ค์ • (Let's Encrypt)
# sudo certbot --nginx -d your-domain.com

๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋ถ„์„

์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๋‹ค ๋ณด๋ฉด ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์‚ฌ์šฉ ํŒจํ„ด ๋ถ„์„์ด ํ•„์š”ํ•ด. ๊ฐ„๋‹จํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์„ ์ถ”๊ฐ€ํ•ด๋ณด์ž! ๐Ÿ“ˆ

from datetime import datetime
import json
from collections import defaultdict
from fastapi import Request
import time

class AnalyticsMiddleware:
    def __init__(self):
        self.stats = defaultdict(lambda: {
            'count': 0,
            'total_time': 0,
            'errors': 0
        })
    
    async def __call__(self, request: Request, call_next):
        start_time = time.time()
        
        try:
            response = await call_next(request)
            
            # ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
            endpoint = request.url.path
            process_time = time.time() - start_time
            
            self.stats[endpoint]['count'] += 1
            self.stats[endpoint]['total_time'] += process_time
            
            # ์‘๋‹ต ํ—ค๋”์— ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์ถ”๊ฐ€
            response.headers["X-Process-Time"] = str(process_time)
            
            return response
            
        except Exception as e:
            endpoint = request.url.path
            self.stats[endpoint]['errors'] += 1
            raise
    
    def get_stats(self):
        """
        ํ†ต๊ณ„ ์ •๋ณด ๋ฐ˜ํ™˜
        """
        result = {}
        for endpoint, data in self.stats.items():
            avg_time = data['total_time'] / data['count'] if data['count'] > 0 else 0
            result[endpoint] = {
                'requests': data['count'],
                'avg_response_time': f"{avg_time:.3f}s",
                'errors': data['errors'],
                'error_rate': f"{data['errors'] / data['count'] * 100:.1f}%" if data['count'] > 0 else "0%"
            }
        return result

# FastAPI ์•ฑ์— ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€
analytics = AnalyticsMiddleware()
app.middleware("http")(analytics)

@app.get("/stats")
async def get_statistics():
    """
    API ์‚ฌ์šฉ ํ†ต๊ณ„ ์กฐํšŒ
    """
    return analytics.get_stats()

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์™€! ์—ฌ๊ธฐ๊นŒ์ง€ ์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ์–ด. ์šฐ๋ฆฌ๋Š” LangChain์„ ์‚ฌ์šฉํ•ด์„œ ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI ๋ด‡์„ ๋งŒ๋“ค์—ˆ์–ด. ๊ธฐ๋ณธ์ ์ธ ๊ตฌํ˜„๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ๋ณด์•ˆ, ์ตœ์ ํ™”, ๋ฐฐํฌ๊นŒ์ง€ ๋ชจ๋“  ๊ณผ์ •์„ ๋‹ค๋ค˜์ง€. ๐ŸŽ‰

์ด์ œ ๋„ˆ๋งŒ์˜ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ๋ด‡์„ ๋งŒ๋“ค ์ค€๋น„๊ฐ€ ๋์–ด! ์˜๋ฃŒ, ์ด์ปค๋จธ์Šค, ๊ต์œก ๋“ฑ ๋‹ค์–‘ํ•œ ๋ถ„์•ผ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ณ , ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ๋„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒ ์ง€? ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ๋” ๋‚˜์€ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ๋˜‘๋˜‘ํ•œ AI ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž! ๐Ÿ’ช

๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ ์ถ”์ฒœ

1. ์‹คํ—˜ํ•ด๋ณด๊ธฐ: ๋‹ค์–‘ํ•œ ํ”„๋กฌํ”„ํŠธ์™€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ฉฐ ์ตœ์ ์˜ ์„ค์ • ์ฐพ๊ธฐ
2. ํ™•์žฅํ•˜๊ธฐ: ๋น„๋””์˜ค ๋ถ„์„, ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ ๋“ฑ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€
3. ์ตœ์ ํ™”ํ•˜๊ธฐ: ์บ์‹ฑ, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋“ฑ์œผ๋กœ ๋น„์šฉ๊ณผ ์†๋„ ๊ฐœ์„ 
4. ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: LangChain GitHub, Discord์—์„œ ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๋“ค๊ณผ ๊ต๋ฅ˜
5. ํ”„๋กœ์ ํŠธ ๊ณต์œ : ์žฌ๋Šฅ๋„ท์—์„œ ๋„ˆ์˜ AI ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ณ  ์ˆ˜์ตํ™”ํ•˜๊ธฐ! ๐Ÿ’ฐ

๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. GPT-5, Gemini 2.0 ๊ฐ™์€ ์ƒˆ๋กœ์šด ๋ชจ๋ธ๋“ค์ด ๋‚˜์˜ค๋ฉด์„œ ๋” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ๋“ค์ด ์ถ”๊ฐ€๋˜๊ณ  ์žˆ๊ฑฐ๋“ . ์ง€๊ธˆ ๋ฐฐ์šด ๊ธฐ์ดˆ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ๊ณ„์† ํ•™์Šตํ•˜๊ณ  ์‹คํ—˜ํ•˜๋ฉด, ์ •๋ง ๋ฉ‹์ง„ AI ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์„ ๊ฑฐ์•ผ! ๐ŸŒŸ

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๊ฑฐ๋‚˜ ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์œผ๋ฉด LangChain ๊ณต์‹ ๋ฌธ์„œ๋‚˜ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ํ™œ์šฉํ•ด๋ด. ๊ทธ๋ฆฌ๊ณ  ์žฌ๋Šฅ๋„ท์—์„œ AI ๊ฐœ๋ฐœ ๊ด€๋ จ ์žฌ๋Šฅ์„ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ. ํ•จ๊ป˜ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•˜๋Š” ๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅธ ๊ธธ์ด๋‹ˆ๊นŒ! ๐Ÿ˜Š

์ž, ์ด์ œ ์ฝ”๋”ฉ์„ ์‹œ์ž‘ํ•ด๋ณผ๊นŒ? ํ™”์ดํŒ…! ๐Ÿ”ฅ

๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI์˜ ๋ฏธ๋ž˜ ๐Ÿค– ์ง€๋Šฅํ˜• ๋ด‡ ๐Ÿš€ ๋น ๋ฅธ ์ฒ˜๋ฆฌ ๐ŸŽฏ ์ •ํ™•ํ•œ ๋ถ„์„ ๐Ÿ’ก ๋ฌดํ•œํ•œ ๊ฐ€๋Šฅ์„ฑ LangChain์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” AI ํ˜์‹ 

๐ŸŽ‰ ์ถ•ํ•˜ํ•ด์š”! ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI ๋ด‡ ๋งˆ์Šคํ„ฐ๊ฐ€ ๋˜์—ˆ์–ด์š”!

์ด์ œ ๋‹น์‹ ๋งŒ์˜ ๋˜‘๋˜‘ํ•œ AI ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”.

์žฌ๋Šฅ๋„ท์—์„œ ์—ฌ๋Ÿฌ๋ถ„์˜ AI ๊ฐœ๋ฐœ ์žฌ๋Šฅ์„ ๊ณต์œ ํ•˜๊ณ  ์„ฑ์žฅํ•˜์„ธ์š”! ๐Ÿš€

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0