목차
0. 들어가며
1. 7개 에이전트(agent)의 협업 - Week 8에서 만들려는 에이전트 시스템
2. 파인튜닝 모델을 클라우드 GPU에서 실행 - Modal 이용
3. 잠깐 떴다 사라지는 앱에서 배포된 서비스로 - 파인튜닝 모델 서비스화
4. 첫 번째 에이전트 - SpecialistAgent, 그리고 전처리가 필요한 이유
5. 학습이 아니라 추론(inference)에서 승부 - 상품 80만 개를 벡터 DB로
6. 두 번째 에이전트 - GPT-5.1에게 RAG를 통해 비슷한 상품 5개를 쥐여주기
7. 세 번째 에이전트와 앙상블 에이전트 - 셋을 섞으면 정말 더 좋아질까
마무리 - 성적표가 또 뒤집혔다
0. 들어가며
지난 글(글 16)에서 나온 가격 예측 모델 성능 비교는 꽤 놀라웠다.
3B(30억) 파라미터짜리 오픈소스 모델인 Llama 3.2를 QLoRA로 파인튜닝했더니
오차 $39.85로 제로샷 프론티어 모델을 전부 제치고 1등을 했으니까.
하지만 그건 어디까지나 딱 모델.
마지막 주차인 Week8은 해당 모델을 활용해서, 풀고 싶은 문제를 푸는 자율화된 에이전트 워크플로우를 만든다.
Week8에서 하는 일을 간단히 요약하면 다음과 같다.
1) 인터넷에서 할인 상품 정보를 스스로 긁어와
2) 그 상품의 진짜 가치가 얼마인지 추정하고
3) 파는 가격이 충분히 싸면 내 휴대폰으로 알림을 보내는 Agent 시스템을 만든다.
여기서 "스스로"라는 말과, 에이전트(Agent) 라는 말이 핵심이다.
강사는 에이전트에 대해 3가지 정의를 안내해줬다.
- 샘 알트먼: AI systems that can do wokr for you independently
-> 사용자를 대신해 독립적으로 업무를 수행할 수 있는 AI 시스템 (원래 "오퍼레이터"라 부르던 것을 에이전트라 부르기 시작) - Anthropic / HuggingFace: AI systems where an llm controls the workflow
-> LLM이 워크플로를 통제하는 AI 시스템 - 2025년 후반의 통용 정의: An agent runs tools in a loop to acheive a goal
-> 목표를 달성하기 위해 도구(tool)를 루프로 돌리는 것
3가지 정의가 조금씩 다르지만 공통점은 있다.
사람이 매번 다음 단계를 지시하지 않아도 된다는 것.
이번 주에 만들 시스템은 이 3가지 정의를 전부 만족하게 된다.
Week 8의 블로그 글은 Week 7과 마찬가지로 두 편으로 나눠 쓰려 한다.
- 이번 글(Day 1~2)은 "가격을 맞히는 쪽" 이다. 상품 설명을 던지면 가격을 추정해주는 전문가 Agent 3종을 만들고, 그 3개를 하나로 묶어 가격 예측하는 시스템을 만든다.
- 다음 글(Day 3~5)은 "가격 예측을 제외한 나머지 영역". 상품딜을 찾아오고, 판단하고, 알림을 보내는 부분이다.
참고로 이번 주부터는 Colab을 떠나 내 맥북에서 직접 돌렸다.
GPU가 필요한 부분(만든 파인튜닝 모델을 돌리는 것)은 Modal이라는 서비스에 떼어 맡기는 구조라 가능했는데,
그 이야기를 2장에서 먼저 하려고 한다.


1. 7개 에이전트(agent)의 협업 - Week 8에서 만들려는 에이전트 시스템
"에이전트 시스템"이라고 하면 뭔가 거창해 보이는데, 이번 주에 만들 것을 뜯어보면 결국 역할이 다른 파이썬 클래스 7개다.
계층으로 그리면 이렇다.
UI (Gradio)
└─ 에이전트 프레임워크 ... 메모리(어떤 딜을 이미 봤는지)와 로깅 담당
└─ 플래닝 에이전트 ... 전체 흐름 조율
├─ 스캐너 에이전트 ... RSS에서 딜 긁어와 유망한 딜 선별
├─ 앙상블 에이전트 ... 가격 추정 (내부에 3개를 더 갖고 있음)
│ ├─ 스페셜리스트 에이전트 ... 파인튜닝한 Llama 3.2 (Modal에서 원격 실행)
│ ├─ 프론티어 에이전트 ... RAG + GPT-5.1
│ └─ 뉴럴 네트워크 에이전트 ... Week 6에서 만든 딥러닝 모델
└─ 메시징 에이전트 ... 휴대폰으로 푸시 알림 (나는 웹 푸시로 대체)
여기서 이번 글이 다루는 범위는 가운데에 있는 앙상블 에이전트와 그 안의 3개 하위 가격 예측 에이전트다.
이 7개 에이전트를 만드는 데 시중의 에이전트 프레임워크는 하나도 쓰지 않는다.
CrewAI, OpenAI Agents SDK, Google ADK, LangGraph 같은 에이전트 프레임워크가 시중에 나와있지만, 강의에서는 전부 순수한 파이썬 클래스와 LLM 호출만으로 만든다.
강사가 처음부터 유려한 에이저트 프레임워크를 이용할 필요는 없다고 말하는 게 인상적이었다.
문제 풀려고 할때 너무 빨리 에이전트 아키텍처로 넘길려는 사람이 있는데, 비즈니스 목표에 집중, 프롬프트 테스트, 평가 하는 게 더 중요하다고 강사는 말한다.
즉, 에이전트 아키텍처에 뛰어드는 게 아닌 비즈니스 문제에 집중해야하며
LLM 호출도, 에이전트 개수도 단순하게 시작해서 그 뒤에 정말 필요하다고 느낄때 넓히는게 중요하다고 말한다.
모든 에이전트의 부모 클래스
7개가 공유하는 것은 딱 하나, Agent라는 추상 클래스다. 내용도 짧다.
class Agent:
# 에이전트별 로그 색상
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
WHITE = '\033[37m'
BG_BLACK = '\033[40m'
RESET = '\033[0m'
name: str = ""
color: str = '\033[37m'
def log(self, message):
# [에이전트 이름] 메시지 형태로, 자기 색깔을 입혀서 로그를 남긴다
color_code = self.BG_BLACK + self.color
message = f"[{self.name}] {message}"
logging.info(color_code + message + self.RESET)
하는 일은 로그 찍기가 전부다. 그런데 왜 굳이 부모 클래스까지 만들어서 색을 입힐까?
=> 실제로 돌려보면 이유가 바로 납득된다. 가격 하나를 추정하는 동안 에이전트 4~5개가 동시에 로그를 쏟아내기 때문에, 누가 지금 무슨 말을 하는지 색으로 구분되지 않으면 읽을 수가 없다.
그리고 이 색깔은 나중에 Day 5에서 그대로 재활용된다.
터미널 색상 코드를 HTML <span style="color: ..."> 로 바꿔주는 매퍼가 있어서, Gradio 화면의 로그 패널에 똑같은 색으로 흘러나온다.

느낀 점
- 강사가 이 장에서 한 경고가 인상 깊었다. "문제를 풀 때 너무 빨리 에이전트 아키텍처로 넘어가려는 사람이 많다"는 것. 중요한 건 비즈니스 목표에 집중하고, 프롬프트를 테스트하고, 평가하는 쪽이지 아키텍처가 아니라고 했다. LLM 1개로 시작해서, 정말 필요하다고 느껴질 때 넓혀가라는 조언.
- 특히 처음 실무에서 에이전트를 만들어갈 떄도 이런 부분이 생기는 거 같다. 에이전트 아키텍처적인 고민을 먼저 하고(ex. 이걸 어떤 서브에이전트로 나눌까, 어떻게 스킬화/tool을 나눠서 이용할까 등) 근데 이렇게 가다보면, 결국에 아키텍처 구조에 문제를 끼워맞추게 되는 상황이 생긴다.
- 이번 Week 8의 시스템도 처음부터 7개의 에이전트로 강사가 나눴던 건 아니라고 한다. "가격을 잘 추정한다"는 하나의 목표가 먼저 있었고, 그걸 잘하기 위해 역할을 쪼갠 결과가 에이전트 7개였다. 결국에 중요한 건 비즈니스 문제를 푸는 것이라는 점이 인상적이었다.
2. 파인튜닝 모델을 클라우드 GPU에서 실행 - Modal 이용
Week 7에서 만든 파인튜닝 모델은 GPU가 있어야 돌아간다.
그런데 내 맥북에는 해당 파인튜닝 모델을 지속적으로 돌리기엔 GPU가 빡셀 뿐더러 계속 맥북을 켜놓을 순 없다.
또한 그렇다고 Google Colab 노트북을 24시간 켜둘 수도 없다.
딜을 발견했을 때만 잠깐 가격을 물어보면 되는 시스템인데, GPU 서버를 계속 띄워두는 것도 낭비다.
=> 필요할 때만 몇 초 빌려 쓰는 GPU가 필요하다. 그게 이번에 쓰는 Modal이다.
Modal은 서버리스 컴퓨팅 플랫폼이다. 파이썬 함수 위에 데코레이터 한 줄만 붙이면 그 함수가 클라우드에서 실행된다. 서버를 띄우거나 Docker 파일을 쓰거나 배포 파이프라인을 만들 필요가 없고, 실행에 쓴 시간만큼만 과금된다. 가입하면 매달 30달러어치 크레딧이 무료로 주어져서 이번 주 실습은 이걸로 충분했다.

시작은 토큰 등록이다.
uv run modal token set --token-id ak-... --token-secret as-...
혹은 .env에 MODAL_TOKEN_ID, MODAL_TOKEN_SECRET 두 줄을 직접 넣어도 된다.
인프라를 코드로 정의한다
가장 단순한 예제인 hello.py를 보면 Modal의 사고방식이 그대로 드러난다.
import modal
from modal import Image
app = modal.App("hello")
# 실행 환경(컨테이너 이미지)도 파이썬 코드로 정의한다
# 데비안 슬림 이미지에 requests 하나만 설치
image = Image.debian_slim().pip_install("requests")
@app.function(image=image)
def hello() -> str:
import requests
# 지금 이 코드가 어느 지역에서 돌고 있는지 확인
response = requests.get("https://ipinfo.io/json")
data = response.json()
city, region, country = data["city"], data["region"], data["country"]
return f"Hello from {city}, {region}, {country}!!"
눈여겨볼 점 두 가지.
- 컨테이너 이미지를 Dockerfile이 아니라 파이썬 한 줄(Image.debian_slim().pip_install("requests"))로 정의한다.
- import requests가 함수 바깥이 아니라 안쪽에 있다. 이 함수는 내 맥북이 아니라 클라우드 컨테이너에서 실행되므로, 필요한 import도 그 안에서 일어나야 한다.
local과 remote, 한 글자 차이
같은 함수를 두 가지 방식으로 부를 수 있다.
with app.run():
reply = hello.local() # 내 컴퓨터에서 실행
reply
-> 'Hello from Seongnam-si, Gyeonggi-do, KR!!'
with app.run():
reply = hello.remote() # Modal 클라우드에서 실행
reply
-> 'Hello from Ashburn, Virginia, US!!'
코드는 똑같은데 결과가 성남에서 미국 버지니아 애쉬번으로 바뀐다.
.local()과 .remote(), 딱 이 차이 하나로 실행 위치가 바뀐다는 게 Modal의 핵심 아이디어다.
지역을 지정할 수도 있다. 데코레이터에 region만 추가하면 된다.
@app.function(image=image, region="eu")
def hello_europe() -> str:
...
-> 'Hello from Brussels, Brussels Capital, BE!!'
여기까지는 CPU 함수였고, 이제 진짜 목적인 GPU를 붙일 차례다.
llama.py에서는 데코레이터에 gpu 한 줄이 추가된다.
app = modal.App("llama")
image = Image.debian_slim().pip_install("torch", "transformers", "accelerate")
# Modal 대시보드에 등록해둔 시크릿을 이름으로 가져온다
secrets = [modal.Secret.from_name("huggingface-secret")]
GPU = "T4"
MODEL_NAME = "meta-llama/Llama-3.2-3B"
@app.function(image=image, secrets=secrets, gpu=GPU, timeout=1800)
def generate(prompt: str) -> str:
from transformers import AutoTokenizer, AutoModelForCausalLM, set_seed
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto")
set_seed(42)
inputs = tokenizer.encode(prompt, return_tensors="pt").to("cuda")
outputs = model.generate(inputs, max_new_tokens=5)
return tokenizer.decode(outputs[0])
Llama 3.2는 HuggingFace에서 토큰이 있어야 내려받을 수 있는 모델이라 HF_TOKEN이 필요한데,
이걸 코드에 적지 않고 Modal 대시보드에 huggingface-secret이라는 이름으로 등록해두고 이름으로만 참조한다.
=> 내 노트북에는 토큰이 없어도 되고, 클라우드 컨테이너 안에서만 환경변수로 풀린다.
실행 결과는 이렇다.
with modal.enable_output():
with app.run():
result = generate.remote("Never gonna give you up, never gonna")
result
'<|begin_of_text|>Never gonna give you up, never gonna let you down, never'
modal.enable_output()으로 감싸면 클라우드 쪽 진행 상황이 그대로 흘러나오는데, 첫 실행에서는 이런 로그가 한참 찍힌다.
=> Step 0: FROM base
=> Step 1: RUN python -m pip install accelerate torch transformers
Collecting accelerate ...
Collecting torch ...
내 컴퓨터에서 pip install을 하는 게 아니라, 클라우드에서 컨테이너 이미지를 처음 굽고 있는 것이다. 한 번 구워두면 다음부터는 이 단계가 사라진다.
느낀 점
- 모델을 만드는 것을 넘어 모델을 서비스로 굴리기 위해 필요한 작업을 해본게 의미가 있었다. 이런 Modal을 이용해서, 오픈소스 모델이나 파인튜닝한 모델도 서비스화 할 수 있는 지점이 신기했다.
- 특히 "안 쓸 때는 0원"이 될 수 있는 부분이 Modal이 가지는 큰 장점이라고 느꼈다.
3. 잠깐 떴다 사라지는 앱에서 배포된 서비스로 - 파인튜닝 모델 서비스화
앞 장의 hello와 llama는 with app.run(): 블록 안에서만 살아 있는 앱이었다.
블록을 빠져나오면 앱도 사라진다. Modal은 이걸 ephemeral app(임시 앱)이라고 부른다.
실험할 때는 편하지만, 에이전트 시스템이 아무 때나 가격을 물어보려면 늘 그 자리에 있는 서비스여야 한다.
이 장은 임시 앱 → 배포된 앱으로 넘어가는 과정이다.
먼저 임시 앱 버전.
pricer_ephemeral.py는 Week 7의 파인튜닝 모델을 그대로 GPU에 올려 가격을 뱉는 함수다.
GPU = "T4"
BASE_MODEL = "meta-llama/Llama-3.2-3B"
HF_USER = "ed-donner"
RUN_NAME = "2025-11-28_18.47.07"
FINETUNED_MODEL = f"{HF_USER}/price-{RUN_NAME}"
REVISION = "b19c8bfea3b6ff62237fbb0a8da9779fc12cefbd"
@app.function(image=image, secrets=secrets, gpu=GPU, timeout=1800)
def price(description: str) -> float:
...
# Week 7과 똑같은 4bit 양자화 설정
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
)
# 베이스 모델을 올리고 그 위에 LoRA 어댑터를 붙인다
base_model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL, quantization_config=quant_config, device_map="auto")
fine_tuned_model = PeftModel.from_pretrained(base_model, FINETUNED_MODEL, revision=REVISION)
# Week 7에서 학습할 때 쓴 프롬프트 형식을 그대로 재현
prompt = f"{QUESTION}\n\n{description}\n\n{PREFIX}" # ... 마지막이 "Price is $"
outputs = fine_tuned_model.generate(inputs, max_new_tokens=5)
result = tokenizer.decode(outputs[0])
contents = result.split("Price is $")[1] # $ 뒤 숫자만 잘라낸다
...
호출은 앞 장과 똑같다.
with modal.enable_output():
with app.run():
result = price.remote("Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio")
result
-> (출력 결과) 90.0
배포된 앱으로 바꾸기
터미널에서 deploy 한 번이면 임시 앱이 상주 서비스가 된다.
!uv run modal deploy -m pricer_service
✓ App deployed in 1.761s! 🎉
View Deployment: https://modal.com/apps/kdb1248/main/deployed/pricer-service
이제 with app.run(): 없이, 이름만 알면 어디서든 부를 수 있다.
pricer = modal.Function.from_name("pricer-service", "price")
pricer.remote(text)
-> 130.0
노트북이든 다른 스크립트든, 나중에 만들 SpecialistAgent든 전부 이 이름으로 접근한다.
사실상 REST API를 만든 것과 같은 효과인데 엔드포인트 코드를 한 줄도 쓰지 않았다.
느린 이유와 그 해결 - @modal.enter()
그런데 이 방식에는 큰 낭비가 있다.
price()가 호출될 때마다 함수 안에서 3B 모델을 새로 내려받고 4bit로 양자화하고 어댑터를 붙인다.
가격 하나 물어보는 데 그걸 매번 다시 한다는 뜻이다.
Modal 대시보드에서 실제 실행 시간을 보면 체감이 된다.

해결책이 pricer_service2.py다. 함수 대신 클래스로 바꾼다.
@app.cls(
image=image.env({"HF_HUB_CACHE": CACHE_DIR}),
secrets=secrets,
gpu=GPU,
timeout=1800,
min_containers=MIN_CONTAINERS,
volumes={CACHE_DIR: hf_cache_volume}, # 모델 가중치를 캐시해두는 저장소
)
class Pricer:
@modal.enter()
def setup(self):
# 컨테이너가 살아날 때 딱 한 번 실행된다
# 모델 로드/양자화/어댑터 부착을 여기서 끝내둔다
...
self.fine_tuned_model = PeftModel.from_pretrained(self.base_model, FINETUNED_MODEL, revision=REVISION)
@modal.method()
def price(self, description: str) -> float:
# 이제 이 메서드는 추론만 한다
...
@modal.enter()가 붙은 메서드는 컨테이너가 뜰 때 한 번만 실행된다.
무거운 준비는 전부 setup()에 몰아넣고, price()는 이미 메모리에 올라간 모델로 추론만 한다.
거기에 Volume(hf-hub-cache)을 붙여서 모델 가중치 자체도 매번 새로 받지 않게 캐시한다.
호출 방식만 살짝 바뀐다. Function 대신 Cls다.
Pricer = modal.Cls.from_name("pricer-service", "Pricer")
pricer = Pricer()
reply = pricer.price.remote(text)
print(reply)
-> 130.0
같은 결과인데 걸린 시간이 다르다.

콜드 스타트를 어떻게 다룰까
물론 컨테이너가 자고 있으면 깨우는 시간이 필요하다. 강사가 정리해준 감각은 이렇다.
- 맨 처음 이미지를 굽는 데는 최대 10분
- 그 뒤로는 컨테이너가 잠들어 있다가 깨어나는 데 30초 정도
- 컨테이너가 살아 있으면 2초
이걸 조절하는 방식이 두 개 있다.
1) 하나는 MIN_CONTAINERS = 1로 두어 컨테이너를 항상 살려두는 것.
- 다만 그만큼 크레딧이 계속 나간다.
2) 다른 하나는 컨테이너가 잠들기까지의 대기 시간을 늘리는 것.
pricer = modal.Cls.from_name("pricer-service", "Pricer")()
pricer.update_autoscaler(scaledown_window=1200) # 20분간 깨어 있기 (기본값은 120초)
=> 실습하는 동안만 20분으로 늘려두고, 끝나면 다시 120초로 돌려놓는 식으로 쓰면 된다.
느낀 점
- "모델을 올리는 시간"과 "추론하는 시간"을 분리하는 게 서빙의 핵심이라는 걸 숫자로 보니 확 와닿았다. 1분 22초 중 실제 추론은 1초 남짓이고 나머지는 전부 준비 과정이었다. (실제 모델을 서비스 화 하는 과정에서 신경 써야 할 포인트를 배울 수 있었다. 모델 인프라 담당하시는 엔지니어들은 이런 부분에 대한 고민들을 엄청 해나가는 사람들이겠지?)
- 결국 서빙 설계는 콜드 스타트와 비용 사이의 트레이드오프를 어디에 둘 것인가의 문제인 것 같다. 5분에 한 번 도는 딜 알림 시스템이라면 30초 콜드 스타트를 감수하는 쪽이 맞고, 사용자가 화면 앞에서 기다리는 서비스라면 컨테이너를 살려두는 비용을 내야 한다. 엔지니어링 세계에서의 최적화에 대한 관점을 작게나마 느껴볼 수 있던 순간이었다.
4. 첫 번째 에이전트 - SpecialistAgent, 그리고 전처리가 필요한 이유
이제 Modal에 올린 서비스를 에이전트로 감쌀 차례다. 아래는 첫 번째 에이전트인 SpecialistAgent 이다.
class SpecialistAgent(Agent):
"""
Modal에서 원격으로 돌고 있는 파인튜닝 모델을 호출하는 에이전트
"""
name = "Specialist Agent"
color = Agent.RED
def __init__(self):
self.log("Specialist Agent is initializing - connecting to modal")
Pricer = modal.Cls.from_name("pricer-service", "Pricer")
self.pricer = Pricer()
def price(self, description: str) -> float:
self.log("Specialist Agent is calling remote fine-tuned model")
result = self.pricer.price.remote(description)
self.log(f"Specialist Agent completed - predicting ${result:.2f}")
return result
앞 장에서 만든 배포 서비스를 이름으로 붙잡아서(modal.Cls.from_name), price()로 감싸고, 앞뒤로 로그를 찍는 게 전부다.
무거운 모델 로딩, GPU, 양자화는 전부 Modal 쪽에 있다.
agent = SpecialistAgent()
agent.price("iPhone 10")
INFO:root:[Specialist Agent] Specialist Agent is initializing - connecting to modal
INFO:root:[Specialist Agent] Specialist Agent is calling remote fine-tuned model
INFO:root:[Specialist Agent] Specialist Agent completed - predicting $350.00
-> 350.0
"iPhone 10" 네 글자만 던졌는데 350달러가 나온다. 잘 되는 것처럼 보인다. 그런데 여기서 강사가 제동을 건다.
학습할 때의 입력과 추론(Inference)할 때의 입력이 다르다
Week 7에서 이 모델을 학습(파인튜닝)시킬 때 넣었던 데이터는 이런 모양이었다.
Title: Excess V2 Distortion/Modulation Pedal
Category: Music Pedals
Brand: Old Blood Noise
Description: A versatile pedal offering distortion and three modulation modes...
Details: Features include separate gain, tone, and volume controls...
제목, 카테고리, 브랜드, 설명, 상세가 정해진 순서로 들어간 정형화된 텍스트다. 그런데 실제 서비스에서 들어오는 입력은 "iPhone 10" 같은 단어이거나, RSS에서 긁어온 홍보 문구다.
=> 모델은 학습 때 본 형식으로 물어볼 때 가장 잘한다. 답이 나오니까 되는 것 같아 보이지만, 이상적인 상태는 아니라는 것.
그래서 모델에 보내기 전에 입력을 학습 때의 형식으로 다시 써주는 단계를 하나 둔다. 그게 Preprocessor다.
전처리기는 작은 LLM이 맡는다.
SYSTEM_PROMPT = """Create a concise description of a product. Respond only in this format. Do not include part numbers.
Title: Rewritten short precise title
Category: eg Electronics
Brand: Brand name
Description: 1 sentence description
Details: 1 sentence on features"""
# 기본값은 내 맥에서 도는 로컬 Llama 3.2
DEFAULT_MODEL_NAME = os.getenv("PRICER_PREPROCESSOR_MODEL", "ollama/llama3.2")
class Preprocessor:
def preprocess(self, text: str) -> str:
response = completion(
messages=self.messages_for(text),
model=self.model_name,
api_base=self.base_url, # ollama라면 http://localhost:11434
)
return response.choices[0].message.content
재밌는 건 이 전처리를 굳이 좋은 모델에 맡기지 않는다는 점이다. 기본값은 내 맥에서 Ollama로 도는 Llama 3.2. 형식만 맞춰 다시 쓰는 일이라 작은 모델로 충분하고, 무료이고, 데이터도 밖으로 나가지 않는다.
실제로 돌려보면 이렇다.
preprocessor = Preprocessor()
text = preprocessor.preprocess("Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio")
print(text)
### System:
**Title:** High-Quality Microphone
**Category:** Electronics
**Brand:** HyperX
**Description:** The Quadcast HyperX condenser mic delivers exceptional sound quality and connectivity.
**Details:** Equipped with USB-C, this microphone features plug-and-play functionality.
형식은 맞췄는데 아쉬운 구석이 보인다. ### System: 같은 군더더기가 붙었고, 제목이 "High-Quality Microphone"으로 뭉개져서 정작 중요한 제품명이 사라졌다.
모델을 바꿔보면 결과가 달라진다. .env에 한 줄만 추가하면 된다.
PRICER_PREPROCESSOR_MODEL=groq/openai/gpt-oss-20b
preprocessor = Preprocessor(model_name="groq/openai/gpt-oss-20b")
text = preprocessor.preprocess("Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio")
print(text)
Title: HyperX Quadcast Mic
Category: Audio Equipment
Brand: HyperX
Description: Crystal-clear USB-C condenser mic.
Details: Built-in cardioid pickup, tap-to-mute button, four-channel switching, USB-C powered.
제품명이 살아 있고 군더더기도 없다.
전처리가 실제로 예측을 바꾸는가
같은 상품을 두 가지 입력으로 물어봤다. 원문 그대로 던졌을 때는 90달러, 전처리를 거친 텍스트로 던졌을 때는 130달러가 나왔다.
이 마이크(HyperX QuadCast)의 실제 가격은 140달러대다.
=> 모델도, 가중치도, 프롬프트 템플릿도 전부 같은데 입력 형식 하나만 바꿔서 90 → 130으로 바뀌었다.
느낀 점
- 학습 때의 입력 분포와 서비스에서 들어오는 입력 분포가 다르면 성능이 조용히 새어나간다는 걸 눈으로 봤다. 모델을 더 잘 학습시키는 것보다, inference 과정에서 들어오는 입력을 학습 때 형식으로 맞춰주는 게 훨씬 싸게 먹히는 개선일 수 있다는 것도.
- 이를 통해 inference 과정에서의 전처리 테크닉 등, 인퍼런스 테크닉의 중요성을 많이 느꼈던 거 같다.
- 그리고 전처리 같은 "형식 맞추기" 작업에 굳이 비싼 모델을 쓸 필요가 없다는 점도 실용적이었다. 공짜 로컬 모델로도 어느정도 작업이 가능하다는 점이 인상적이었다 . 태스크 난이도에 따라 모델 체급을 나눠 쓰는 게 시스템 설계의 기본이 되어가는 느낌이다.
5. 학습이 아니라 추론(inference)에서 승부 - 상품 80만 개를 벡터 DB로
Week 6~7에서 성능을 올리는 방법은 전부 학습(training) 쪽이었다.
데이터를 모으고, 모델을 고르고, 파인튜닝을 했다.
Day 2에서 시도하는 건 정반대다. 학습은 전혀 하지 않는다.
대신 AI에게 질문을 던지는 순간에 참고 자료를 함께 넣어준다. 추론 시점 기법(inference-time technique), 즉 RAG다.
재밌는 건 재료가 새로 필요하지 않다는 점이다. Week 6에서 학습 데이터로 썼던 아마존 상품 80만 개를, 이번에는 검색용 자료로 다시 쓴다.
=> 같은 데이터를 학습에 한 번, 추론에 또 한 번 쓰는 셈이다.
상품 설명 문장을 384개의 숫자로
먼저 텍스트를 벡터로 바꿔줄 임베딩 모델이 필요하다. 여기서는 HuggingFace에 있는 오픈소스 모델인 all-MiniLM-L6-v2를 쓴다.
encoder = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
vector = encoder.encode(["A proficient AI engineer who has almost reached the finale of AI Engineering Core Track!"])[0]
print(vector.shape)
-> (384,)
문장 하나가 384개의 실수로 바뀐다.
OpenAI의 임베딩 API를 써도 되지만 이 모델을 쓰는 이유는 명확하다. 무료이고, 빠르고, 내 컴퓨터에서 돌기 때문에 데이터가 밖으로 나가지 않는다.
벡터 데이터베이스에 80만 개 상품 정보 밀어 넣기
벡터 DB는 오픈소스인 Chroma를 쓴다. 로컬 폴더에 파일로 저장되는 방식이다.
client = chromadb.PersistentClient(path="products_vectorstore")
collection = client.create_collection("products")
for i in tqdm(range(0, len(train), 1000)):
documents = [item.summary for item in train[i: i+1000]] # 상품 설명 텍스트
vectors = encoder.encode(documents).astype(float).tolist() # 384차원 벡터로 변환
metadatas = [{"category": item.category, "price": item.price} # 정답 가격을 메타데이터로 함께 저장
for item in train[i: i+1000]]
ids = [f"doc_{j}" for j in range(i, i+1000)]
collection.add(ids=ids, documents=documents, embeddings=vectors, metadatas=metadatas)
여기서 핵심은 metadatas에 price를 같이 넣는다는 점이다.
나중에 비슷한 상품을 찾았을 때 그 상품들의 실제 가격을 바로 꺼내 쓰기 위해서다.
문제는 시간이다. 강사 GPU로도 30분이 걸린다고 했고, 내 맥에서는 더 걸렸다.
그러다 중간에 한 번 끊겨서, 이미 들어간 개수를 확인하고 이어서 채우는 코드를 따로 짜야 했다.
# 중단된 지점부터 이어서 채우기
START = collection.count() # 이미 들어간 개수 = 다음에 채울 시작 인덱스
print(f"Resuming from doc_{START}, {len(train) - START:,} items remaining")
for i in tqdm(range(START, len(train), 1000)):
...
print(f"Done. Total count: {collection.count():,}")
Resuming from doc_289000, 511,000 items remaining
Done. Total count: 800,000
28만 9천 개까지 들어가 있었고, 남은 51만 1천 개를 마저 채워서 80만 개를 완성했다.
임베딩이 의미를 담고 있다는 시각적 증거
80만 개 상품정보를 다 넣고 나면 궁금해진다. 이 384차원 숫자 뭉치가 정말 의미를 담고 있을까?
확인하는 방법이 t-SNE다. 384차원을 사람이 볼 수 있는 2차원이나 3차원으로 눌러서 그려보는 기법이다.
전부 그리면 컴퓨터가 버티지 못해서 1만 개만 뽑고, 8개 카테고리를 색으로 구분했다.
MAXIMUM_DATAPOINTS = 10_000
CATEGORIES = ['Appliances', 'Automotive', 'Cell_Phones_and_Accessories', 'Electronics',
'Musical_Instruments', 'Office_Products', 'Tools_and_Home_Improvement', 'Toys_and_Games']
COLORS = ['cyan', 'blue', 'brown', 'orange', 'yellow', 'green', 'purple', 'red']
# 384차원 -> 2차원
tsne = TSNE(n_components=2, random_state=42)
reduced_vectors = tsne.fit_transform(vectors)

주목할 점은 임베딩 모델에게 카테고리를 알려준 적이 없다는 것이다.
그냥 상품 설명 텍스트만 넣었는데, 자동차 용품은 자동차 용품끼리, 악기는 악기끼리 모였다.
=> 벡터 사이의 거리가 곧 의미의 유사도라는 걸 눈으로 확인하는 순간이다.
n_components=3으로 바꾸면 3차원으로도 볼 수 있고, 이 3D 플롯은 나중에 Day 5의 Gradio 화면 오른쪽에 그대로 박히게 된다.

느낀 점
- 같은 데이터를 학습용과 검색용으로 두 번 쓴다는 발상이 좋았다. 이를 통해 inference-time technique으로 성능을 높일 수 있다는 점이 인상적이었다.
- 벡터 DB를 채우는 일이 생각보다 지루하고 잘 끊긴다는 것도 실감했다. 80만 건짜리 배치 작업에서 실패를 했을 때, 재시작 지점을 어떻게 잡을지 고민해보는 것도 좋은 경험이었다.
6. 두 번째 에이전트 - GPT-5.1에게 RAG를 통해 비슷한 상품 5개를 쥐여주기
벡터 DB가 준비됐으니 이제 진짜 RAG를 해볼 차례다. 흐름은 세 단계다.
- 가격을 모르는 상품에 대한 설명이 들어온다
- 벡터 DB에서 그와 비슷한 상품 5개와 그 실제 가격을 찾는다
- "이런 비슷한 상품들이 이 가격이더라"를 프롬프트에 붙여서 GPT-5.1에게 물어본다
테스트에 쓸 상품은 이거다.
test[0]
-> <Old Blood Noise Excess V2 Distortion Chorus/Delay Pedal = $219.0>
기타 이펙터 페달이고 정답은 219달러다.
비슷한 상품 5개 찾기
def vector(item):
return encoder.encode(item.summary)
def find_similars(item):
vec = vector(item)
# 벡터 DB에서 가장 가까운 5개를 꺼낸다
results = collection.query(query_embeddings=vec.astype(float).tolist(), n_results=5)
documents = results['documents'][0][:]
prices = [m['price'] for m in results['metadatas'][0][:]] # 아까 넣어둔 정답 가격
return documents, prices
실제로 돌려보면 이런 게 잡힌다.
Old Blood Noise Endeavors Procession Reverb $209.00
Boss MD-2 Mega Distortion Modulation Pedal $109.99
Old Blood Noise Endeavors Mondegreen Delay Pedal $209.00
Aural Dream Bold Distortion Pedal $24.49
Bit Commander V2 Monophonic Analog Guitar Synth $199.00
같은 브랜드(Old Blood Noise)의 다른 페달 2개가 상위에 올라온 게 눈에 띈다. 브랜드명으로 검색한 게 아니라 설명 문장의 의미로만 찾은 결과다.
가격대도 흥미롭다. 209, 109.99, 209, 24.49, 199. 비슷해 보이는 페달인데 24달러짜리부터 209달러짜리까지 섞여 있다.
=> 그래서 단순 평균을 내는 게 아니라, 어떤 게 우리 상품과 더 가까운지 판단하는 일을 LLM에게 맡기는 것이다.
컨텍스트를 프롬프트에 붙이기
def make_context(similars, prices):
message = "For context, here are some other items that might be similar to the item you need to estimate.\n\n"
for similar, price in zip(similars, prices):
message += f"Potentially related product:\n{similar}\nPrice is ${price:.2f}\n\n"
return message
def messages_for(item, similars, prices):
message = f"Estimate the price of this product. Respond with the price, no explanation\n\n{item.summary}\n\n"
message += make_context(similars, prices)
return [{"role": "user", "content": message}]
완성된 프롬프트는 이런 모양이 된다.
Estimate the price of this product. Respond with the price, no explanation
Title: Excess V2 Distortion/Modulation Pedal
Category: Music Pedals
Brand: Old Blood Noise
...
For context, here are some other items that might be similar to the item you need to estimate.
Potentially related product:
Title: Old Blood Noise Endeavors Procession Reverb
...
Price is $209.00
Potentially related product:
Title: Boss MD-2 Mega Distortion Modulation Multi-Effects Pedal
...
Price is $109.99
(이하 3개 더)
호출 부분은 짧다.
def gpt_5__1_rag(item):
documents, prices = find_similars(item)
response = completion(
model="gpt-5.1",
messages=messages_for(item, documents, prices),
reasoning_effort="none", # 추론 모드를 끄고 빠르게
seed=42, # 같은 입력이면 같은 답이 나오도록
)
return response.choices[0].message.content
gpt_5__1_rag(test[0])
-> '$229.00'
정답 219달러에 예측 229달러. 10달러 차이다.
200건으로 평가해보면
한 건 잘 맞힌 건 우연일 수 있으니 테스트셋 200건으로 돌려봤다.
evaluate(gpt_5__1_rag, test)
내 실행 결과는 평균 오차 30.97달러였다. (MSE 2,978, r² 86.4%)

이 숫자가 왜 중요한지는 앞선 글들의 기록과 나란히 놓으면 보인다.
- GPT-5.1 제로샷 (Week 6): $44.74
- Llama 3.2 3B 파인튜닝 Full (Week 7): $39.85
- GPT-5.1 + RAG (지금): $30.97
지난 글에서 몇 시간 넘게 파인튜닝을 통해 만든 1등 기록을, 학습을 한 번도 하지 않은 방법이 넘어섰다.
바뀐 것은 프롬프트에 비슷한 상품 5개를 붙여준 것뿐이다.
에이전트로 감싸기
이 로직을 FrontierAgent 클래스로 옮긴다. 구조는 SpecialistAgent와 똑같다.
class FrontierAgent(Agent):
name = "Frontier Agent"
color = Agent.BLUE
def __init__(self, collection):
self.log("Initializing Frontier Agent")
self.client = OpenAI()
self.MODEL = "gpt-5.1"
self.collection = collection # 벡터 DB
self.model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
self.log("Frontier Agent is ready")
def get_price(self, s) -> float:
# "$229.00" 같은 문자열에서 숫자만 뽑아낸다
s = s.replace("$", "").replace(",", "")
match = re.search(r"[-+]?\d*\.\d+|\d+", s)
return float(match.group()) if match else 0.0
def price(self, description: str) -> float:
documents, prices = self.find_similars(description)
...
돌려보면 이렇다.
agent = FrontierAgent(collection)
agent.price("Quadcast HyperX condenser mic, connects via usb-c to your computer for crystal clear audio")
INFO:root:[Frontier Agent] Frontier Agent is performing a RAG search of the Chroma datastore to find 5 similar products
INFO:root:[Frontier Agent] Frontier Agent has found similar products
INFO:root:[Frontier Agent] Frontier Agent is about to call gpt-5.1 with context including 5 similar products
INFO:root:[Frontier Agent] Frontier Agent completed - predicting $135.00
-> 135.0 (실제 가격 140달러대)
agent.price("Shure MV7+ professional podcaster microphone with usb-c and XLR outputs")
-> 279.0 (실제 가격 299달러)
느낀 점
- 파인튜닝없이 프론티어 모델에 RAG만을 붙였는데 성능이 꽤 높아지는게 인상적이었다.
- 에이전트 개발하는 과정에서도 적절한 지식을 에이전트에 주는 것만으로도 원하는 성능을 꽤 끌어올릴 수 있다는 반증을 보여주는 예시라고 생각한다.
- 다만 파인튜닝 모델은 한번 특화되어서 만들어놓으면 작은 모델을 API 비용없이 쓸 수 있다는 점에서 의미자체가 없는 건 아니라고 생각한다.
- RAG의 또하나의 장점은 정보의 갱신이라고도 생각한다. 새 상품이 나오면 벡터 DB에 한 건 추가해서 inference에서 참고하게 하면 되지만, 파인튜닝 모델은 다시 학습해야 한다. 가격처럼 계속 변하는 정보를 다룰수록 이 차이가 커질 것 같다.
7. 세 번째 에이전트와 앙상블 에이전트 - 셋을 섞으면 정말 더 좋아질까
가격을 추정하는 전문가가 지금 둘이다. 파인튜닝 모델(SpecialistAgent)과 RAG(FrontierAgent).
여기에 하나를 더 붙인다. Week 6에서 만들었던 가격예측용 딥러닝 모델이다.
Week 6에서 학습해둔 가중치 파일(deep_neural_network.pth)을 내려받아서 그대로 불러온다.
class NeuralNetworkAgent(Agent):
name = "Neural Network Agent"
color = Agent.MAGENTA
def __init__(self):
self.log("Neural Network Agent is initializing")
self.neural_network = DeepNeuralNetworkInference()
self.neural_network.setup()
self.neural_network.load("deep_neural_network.pth") # Week 6에서 학습한 가중치
self.log("Neural Network Agent is ready and weights are loaded")
def price(self, description: str) -> float:
self.log("Neural Network Agent is starting a prediction")
result = self.neural_network.inference(description)
self.log(f"Neural Network Agent completed - predicting ${result:.2f}")
return result
agent = NeuralNetworkAgent()
agent.price("Shure MV7+ professional podcaster microphone with usb-c and XLR outputs")
-> 162.3922576904297 (실제 가격 299달러)
혼자서는 좀 아쉬운 성적이다(Week 6 기록으로 평균 오차 $46.49). 그런데 앙상블에서는 이런 모델도 쓸모가 있다는 게 이 장의 이야기다.
앙상블이 왜 통하는가
강사가 든 예시가 직관적이었다.
모든 상품의 실제 가격이 100달러라고 하자. 모델 A는 항상 10달러씩 빗나간다. 110이나 90을 낸다. 평균 오차는 10.
모델 B도 마찬가지로 평균 오차가 10이다.
이 둘의 평균을 내면 오차는 어떻게 될까?
- 최악의 경우: A와 B가 둘 다 +10으로 틀린다 -> 평균도 110, 오차 그대로 10
- 하지만 A는 +10, B는 -10으로 틀리는 경우가 생기면 -> 평균은 100, 오차 0
=> 두 모델이 서로 다른 방향으로 틀리는 만큼, 평균 오차는 10보다 작아진다. 서로의 노이즈를 상쇄시키는 것이 앙상블의 원리다.
이건 새로운 아이디어가 아니다. 넷플릭스 프라이즈 우승팀도 대규모 앙상블로 이겼고, Week 6에서 썼던 랜덤 포레스트 자체가 결정 트리 여러 개를 묶은 앙상블 모델이다.
그럼 3개 에이전트를 어떤 비율로 섞을까
강사가 원래 소개한 방식은 검증 데이터에 선형 회귀를 돌려서 최적 가중치를 찾는 것이다.
Week 6에서 여러 모델을 묶을 때 썼던 그 방법이다.
다만 이번 코드에서 실제로 들어간 건 고정 가중치다.
def ensemble(item):
price1 = get_price(gpt_5__1_rag(item)) # RAG
price2 = specialist(item) # 파인튜닝 모델
price3 = deep_neural_network(item) # 딥러닝 모델
return price1 * 0.8 + price2 * 0.1 + price3 * 0.1
RAG에 0.8, 나머지 둘에 0.1씩. 성능이 가장 좋았던 모델에 몰아주고 나머지를 가볍게만 얹은 구성이다.
결과는 어땠나
evaluate(ensemble, test)
내 실행 결과는 평균 오차 30.70달러. (MSE 3,011, r² 86.3%)

솔직히 말하면 기대보다 훨씬 작은 개선이다. RAG 단독이 30.97이었으니 0.27달러 좋아진 셈이다.
강사의 실행에서도 30.19에서 29.90으로, 0.29달러 내려갔다.
왜 이것밖에 안 올랐을까 생각해보면 이유는 가중치에 있는 것 같다. RAG가 30.97인데 파인튜닝 모델은 39.85, 딥러닝 모델은 46.49다. 나머지 둘의 오차가 확실히 크니 0.1씩밖에 실을 수 없고, 그러면 상쇄 효과도 그만큼 작아진다.
=> 앙상블은 성능이 비슷한 모델들이 서로 다른 실수를 할 때 효과가 크다.
앙상블 에이전트로 묶기
마지막으로 세 에이전트를 EnsembleAgent 하나로 묶는다. 앞에서 만든 Preprocessor도 여기에 들어간다.
class EnsembleAgent(Agent):
name = "Ensemble Agent"
color = Agent.YELLOW
def __init__(self, collection):
self.log("Initializing Ensemble Agent")
self.specialist = SpecialistAgent()
self.frontier = FrontierAgent(collection)
self.neural_network = NeuralNetworkAgent()
self.preprocessor = Preprocessor()
self.log("Ensemble Agent is ready")
def price(self, description: str) -> float:
# 1) 들어온 텍스트를 학습 때의 형식으로 다시 쓴다
rewrite = self.preprocessor.preprocess(description)
# 2) 세 에이전트에게 각각 물어본다
specialist = self.specialist.price(rewrite)
frontier = self.frontier.price(rewrite)
neural_network = self.neural_network.price(rewrite)
# 3) 가중 평균
combined = frontier * 0.8 + specialist * 0.1 + neural_network * 0.1
self.log(f"Ensemble Agent complete - returning ${combined:.2f}")
return combined
실제로 돌려보면 1장에서 이야기했던 색깔 로그의 쓸모가 드러난다.
agent = EnsembleAgent(collection)
agent.price("Shure MV7+ professional podcaster microphone with usb-c and XLR outputs")

-> 261.21
이 한 건에서는 파인튜닝 모델이 299달러로 정답을 정확히 맞혔는데, 앙상블은 다른 둘에 끌려 내려가 261.21달러가 됐다. 평균적으로는 앙상블이 낫지만, 개별 건에서는 잘 맞힌 모델의 답을 깎아먹기도 한다는 걸 보여주는 사례다.
그리고 비용도 같이 봐야 한다. 가격 하나를 답하는 데 LLM 호출이 4번(전처리 llama3.2, 파인튜닝 모델, 임베딩, GPT-5.1), 신경망 추론이 1번 일어난다.
=> 0.27달러의 정확도 개선을 위해 호출 수가 몇 배로 늘어난 구조다.
느낀 점
- 앙상블이 왜 좋은가에 대해 강사의 예시를 통해서 이해해볼 수 있어서 의미 있었다.
- 또한, 앙상블이 항상 좋지는 않을 수 있고 조건이 붙는다는 걸 숫자로 봤다. 모델들의 성능이 비슷하고 서로 다른 실수를 할 때만 상쇄가 일어난다. 한 모델이 압도적이면 나머지는 노이즈만 더할 수도 있다.
- 정확도 0.27달러 개선에 호출 4번을 쓰는 게 맞는 선택인지도 생각해볼 지점이었다. 실무였다면 앙상블을 빼고 RAG 단독으로 가는 쪽이 더 합리적일 수 있다. 성능표의 1등이 곧 시스템의 정답은 아니라는 것. 실무에선 여러번 호출로 인해 발생하는 레이턴시와 비용을 고려했을 때에도, 정말로 이 여러번 호출을 하는게 성능상의 유의미한 차이를 내는지가 중요했을 거 같다.
- 그럼에도 이 구조를 만들어본 의미는 있다고 느꼈다. 서로 다른 성질의 모델(파인튜닝 / 검색 기반 / 딥러닝)을 한 인터페이스(price(description) -> float)로 통일해두면, 나중에 모델을 갈아끼우거나 가중치를 바꾸는 게 쉬워진다.
마무리 - 성적표가 또 뒤집혔다
Week 6부터 이어온 가격 예측 성적표에 이번 글의 결과 두 줄이 추가됐다. 평균 절대 오차이고, 낮을수록 좋다.
Base Llama 3.2 (4bit) $185.52 ← 파인튜닝 전 (내 실행 기준)
Constant (평균값) $106.18
Linear Regression $101.56
Human (강사 Ed 본인) $87.62
NLP + Linear Regression $76.81
GPT-4.1 Nano (파인튜닝) $75.91
Random Forest $72.28
XGBoost $68.23
Fine-tuned Lite $65.40
Neural Network $63.97
GPT-4.1 Nano (제로샷) $62.51
Grok 4.1 Fast $57.62
Gemini 3 Pro $50.54
Claude 4.5 Sonnet $47.10
Deep Neural Network $46.49
GPT-5.1 (제로샷) $44.74
Fine-tuned Full $39.85 ← Week 7의 1등
GPT-5.1 + RAG $30.97 ← 이번 글 (내 실행)
Ensemble $30.70 ← 이번 글 (내 실행), 최종 1위
지난 글의 결말은 "작은 오픈소스 모델을 파인튜닝했더니 프론티어 모델을 이겼다"였다. 이번 글의 결말은 그 위에 하나가 더 얹힌다.
=> 그 파인튜닝 모델을, 학습을 한 번도 하지 않은 방법이 다시 넘어섰다.
두 글을 나란히 놓고 보면 결론이 이렇게 정리된다.
- 파인튜닝(학습 시점)은 작고 싼 모델을 특정 태스크의 전문가로 만든다. 한 번 만들어두면 호출이 싸다.
- RAG(추론 시점)는 모델은 그대로 두고 필요한 지식을 그때그때 넣어준다. 갱신이 쉽고, 학습 비용이 0이다.
- 둘은 대체재가 아니라 다른 축이고, 이번 시스템처럼 같이 쓸 수도 있다.
그리고 앙상블에서 배운 게 하나 더 있다. 성적표 1등이 곧 시스템의 정답은 아니라는 것.
Ensemble이 최종 1위지만 RAG 단독보다 0.27달러 나은 대가로 호출 수가 몇 배가 됐다.
실제 서비스를 만든다면 나는 RAG 단독을 골랐을 것 같다.
여기까지가 "가격을 맞히는 쪽"이다.
Day 1에서 파인튜닝 모델을 Modal에 올려 서비스로 만들었고, Day 2에서 벡터 DB를 채우고 RAG 에이전트와 앙상블 에이전트를 만들었다.
이제 상품 설명을 던지면 가격이 나온다.
다음 글에서 다룰 것
그런데 아직 빠진 게 있다. 지금은 내가 "이 상품 가격 얼마야?"라고 물어봐야만 시스템이 움직인다.
무엇의 가격을 물어볼지는 여전히 사람이 정하고 있다는 뜻이다.
다음 글(Day 3~5)에서는 그 부분을 채운다.
- 인터넷에서 딜을 긁어와 유망한 것만 골라내는 스캐너 에이전트 (그리고 Structured Outputs)
- 알림 문구를 작성해 휴대폰으로 보내는 메시징 에이전트
- 어떤 도구를 언제 부를지 LLM이 스스로 정하는 자율 플래닝 에이전트 (툴 콜)
- 전부를 묶는 에이전트 프레임워크(메모리 등)와 Gradio UI
다음 글은 이 LLM 엔지니어링 강의의 마지막 파트로,
Day 3~5를 통해 최종적인 에이전트를 구축한 내용을 소개하고,
간단히 이 전체 강의를 들으면서의 소감 및 회고를 적어보려 한다.
해당 내용으로 다시 찾아오겠습니다.
(꾸준히 공부하고 적을테니 많은 관심 부탁드립니다.)
Profile:
Linkedin
'AI 공부' 카테고리의 다른 글
| 16. 3B 오픈소스 모델(Llama 3.2)로 GPT-5.1 이겨보기 — QLoRA 파인튜닝 실전 (0) | 2026.07.11 |
|---|---|
| 15. LoRA·QLoRA 원리 파악 — Llama 3.2 파인튜닝 준비하기 (0) | 2026.06.28 |
| 14. 내 데이터로 GPT를 파인튜닝하면 더 똑똑해질까 - 가격 예측 모델 실험기 (0) | 2026.06.06 |
| 13. RAG를 넘어 파인튜닝으로 - 그 첫단계, 데이터 큐레이션 및 전처리 (0) | 2026.05.31 |
| 12. RAG의 한계를 어떻게 깰까 — Advanced RAG (Semantic Chunking·Reranking·Query Rewriting 등) (1) | 2026.05.09 |