LLM 엔지니어링17장 · Project 02. LLM 추론 API 서빙11 / 14← 이전목차다음 →MetaContents

17장. Project 02. LLM 추론 API 서빙

프로젝트 2 — 문의 분류 API 서비스

솔빛생활 고객센터에는 하루에도 수백 건의 문의가 들어옵니다. 문의를 다섯 분류(배송, 교환반품, 결제, 회원, 기타)로 나눠 담당자에게 보내는 API 를 만들어 보세요. 모든 문의를 LLM 에 보내면 정확하지만 느리고, 빠른 분류기만 쓰면 처음 보는 말투에 약합니다. 두 모델을 나눠 쓰는 두 단계 분류로 정확도와 속도를 함께 챙깁니다. 문의는 모두 지어낸 연습용 데이터입니다.

파일생김새
inquiry_train.csv학습용 문의 200개(분류마다 40개). 문의, 분류. 틀에 상품 이름을 넣어 만들었다
inquiries.csv10장의 문의 40개. 손으로 쓴 문장이라 학습용과 말투가 다르다. 이 장에서는 40개를 모두 시험에 쓴다

요구사항은 다섯 가지입니다.

  1. 임베딩(intfloat/multilingual-e5-small, CPU)과 로지스틱 회귀로 빠른 분류기를 학습하고, 시험 문의 40개의 정확도와 확신도를 봅니다.
  2. LLM(qwen2.5:3b)이 다섯 분류 가운데 하나만 답하게 해 정확도와 시간을 잽니다.
  3. 기준값마다 LLM 에 넘기는 수와 정확도를 표로 만들어 기준값을 고릅니다.
  4. POST /classify(문의 하나)와 POST /classify/batch(최대 50개)를 만듭니다. 답마다 분류, 확신도, 누가 판단했는지, 판을 적습니다. LLM 이 실패하면 분류기의 답으로 돌아갑니다.
  5. 시험 문의 40개를 묶음으로 보내 정확도, LLM 에 넘긴 수, 걸린 시간을 잽니다.

단계별 힌트입니다.

  1. e5 모델은 문장 앞에 query: 를 붙여 임베딩합니다(7장).
  2. Ollama 의 format 에 JSON 스키마를 주고 "enum": ["배송", "교환반품", "결제", "회원", "기타"] 로 답을 묶습니다.
  3. 프롬프트에는 분류마다 무엇이 들어가는지 한 줄씩 적습니다. 이름만 주면 작은 모델은 헷갈립니다.
  4. 분류기와 임베딩 모델 이름, 판은 joblib 파일 하나에 담아 API 가 lifespan 에서 읽게 합니다.
  5. 묶음 요청은 임베딩을 한 번에 계산하고, 확신이 낮은 문의만 하나씩 LLM 에 보냅니다.

확인할 것: 다 풀었으면 항목마다 스스로 확인해 체크해 보세요.

실제 데이터로 해 보려면

실제 문의에는 이름, 전화번호, 주소가 섞여 있습니다. 개인정보를 가린 뒤 쓰고, 자료마다 이용 약관을 확인하세요.

자료구하는 곳내용
민원(콜센터) 질의응답 데이터AI Hub(aihub.or.kr)여러 분야의 상담 대화와 분류
우리 고객센터의 문의 기록상담 시스템담당자가 붙인 분류를 정답으로 쓴다. 분류 기준이 사람마다 다르면 먼저 기준표를 맞춘다
공개된 상품 문의쇼핑몰 게시판약관상 수집이 허용되는지 확인한다. 크롤링보다 제공되는 내려받기나 API 를 먼저 찾는다

코드의 흐름

예시 풀이의 코드가 어떤 차례로 흘러가는지 한 장으로 그렸습니다. 풀기 전에 큰 줄기를 먼저 보고, 풀다가 막히면 지금 어느 상자에 있는지 확인해 보세요.

분류기, LLM 분류, 기준값, API 파일, 묶음 재기의 다섯 상자를 화살표로 이은 문의 분류 API 프로젝트의 코드 흐름도

주황 화살표가 실행 차례이고, 상자의 번호는 예시 풀이의 단계 번호와 같습니다. 1단계(청록)는 빠른 분류기를 학습해 joblib.dump 로 out_inquiry_clf.joblib 에 저장하고, 2단계(마젠타)는 ask_llm() 으로 문의 40개를 LLM 으로 나눕니다. 3단계(다홍)는 확신도가 기준값보다 낮으면(conf < th) LLM 에 넘기는 규칙을 기준값 0.4~0.9 로 견주어 기준값을 고릅니다. 4단계(빨강)는 /classify 와 /classify/batch 를 담은 API 파일 out_inquiry_api.py 를 쓰고, 5단계(짙은 청록)는 TestClient 로 묶음 요청을 보내 시간과 정확도를 잽니다.

결과 보기 (예시 풀이)

1단계. 빠른 분류기 학습하기

Python
import json, sys, time, urllib.request
import joblib
import numpy as np
import pandas as pd
from sentence_transformers import SentenceTransformer
from sklearn.linear_model import LogisticRegression

train = pd.read_csv("inquiry_train.csv")
test = pd.read_csv("inquiries.csv")
print("학습", len(train), "개 · 시험", len(test), "개 · 학습 예:", train["문의"].head(3).tolist())

# 임베딩은 CPU 에 둔다. GPU 는 Ollama 가 쓴다(7차시)
emb = SentenceTransformer("intfloat/multilingual-e5-small", device="cpu")
encode = lambda texts: emb.encode(["query: " + t for t in texts], normalize_embeddings=True)
clf = LogisticRegression(max_iter=1000, C=10).fit(encode(train["문의"].tolist()), train["분류"])

t = time.perf_counter()
proba = clf.predict_proba(encode(test["문의"].tolist()))          # 문의마다 다섯 분류의 확률
fast_s = time.perf_counter() - t
pred = clf.classes_[proba.argmax(axis=1)]
conf = proba.max(axis=1)                                          # 확신도
print(f"분류기 정확도 {(pred == test['분류']).mean():.3f} · 40개에 {fast_s * 1000:.0f}ms · "
      f"확신도 p10/p50/p90 {np.round(np.percentile(conf, [10, 50, 90]), 3).tolist()}")

# 분류기와 판을 한 파일로 저장한다. API 가 켜질 때 읽는다
joblib.dump({"분류기": clf, "임베딩": "intfloat/multilingual-e5-small", "판": "inquiry-lr-1"}, "out_inquiry_clf.joblib")
출력
학습 200 개 · 시험 40 개 · 학습 예: ['블라우스 배송이 너무 늦어요.', '택배 회사가 어디예요?', '프라이팬 언제 다시 들어와요?']
분류기 정확도 0.700 · 40개에 353ms · 확신도 p10/p50/p90 [0.329, 0.518, 0.838]

2단계. LLM 으로 나누기

Python
LABELS = ["배송", "교환반품", "결제", "회원", "기타"]
SCHEMA = {"type": "object", "properties": {"분류": {"type": "string", "enum": LABELS}}, "required": ["분류"]}
SYSTEM = ("쇼핑몰 고객 문의를 다섯 분류 가운데 하나로 나누세요.\n"
          "- 배송: 배송 상태, 주소, 택배\n- 교환반품: 교환, 반품, 불량, 잘못 온 상품\n"
          "- 결제: 결제, 할부, 영수증, 환불 금액, 쿠폰 할인\n- 회원: 로그인, 계정, 등급, 적립금, 회원 쿠폰\n"
          "- 기타: 재입고, 포장, 매장, 리뷰, 고객센터 같은 그 밖의 문의\nJSON 으로 답하세요.")
retried = {"수": 0}


def post(path, body, timeout=60):
    req = urllib.request.Request("http://127.0.0.1:11434" + path, data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())


def ask_llm(text):
    """format 에 준 JSON 스키마의 enum 때문에 다섯 분류 가운데 하나만 나온다. 오류면 모델을 내렸다 다시 묻는다"""
    body = {"model": "qwen2.5:3b", "stream": False, "format": SCHEMA,
            "messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": text}],
            "options": {"temperature": 0, "seed": 42, "num_predict": 30}}
    for _ in range(3):
        try:
            return json.loads(post("/api/chat", body)["message"]["content"])["분류"]
        except (OSError, ValueError, KeyError):
            retried["수"] += 1
            post("/api/generate", {"model": "qwen2.5:3b", "keep_alive": 0})
            time.sleep(2)
    return None


ask_llm("몸풀기 문의입니다")
t = time.perf_counter()
llm = np.array([ask_llm(q) for q in test["문의"]])
llm_s = time.perf_counter() - t
print(f"LLM 정확도 {(llm == test['분류']).mean():.3f} · 40개에 {llm_s:.1f}초(하나에 {llm_s / 40:.2f}초) · 다시 물음 {retried['수']}")
for q, y, a, b in zip(test["문의"], test["분류"], pred, llm):
    if b != y:
        print(f"  LLM 이 틀림: {q} 정답 {y} · LLM {b} · 분류기 {a}")
출력
LLM 정확도 0.900 · 40개에 15.2초(하나에 0.38초) · 다시 물음 0
  LLM 이 틀림: 택배 송장 번호는 어디서 봐요? 정답 배송 · LLM 결제 · 분류기 배송
  LLM 이 틀림: 대량 구매 견적을 받고 싶어요. 정답 기타 · LLM 결제 · 분류기 교환반품
  LLM 이 틀림: 문 앞에 두고 가 달라고 할 수 있어요? 정답 배송 · LLM 교환반품 · 분류기 기타
  LLM 이 틀림: 생일 쿠폰이 안 왔어요. 정답 회원 · LLM 교환반품 · 분류기 회원

3단계. 기준값 고르기

Python
rows = []
for th in [0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:
    to_llm = conf < th                                       # 확신도가 기준값보다 낮으면 LLM 에 넘긴다
    final = np.where(to_llm, llm, pred)
    rows.append({"기준값": th, "LLM 에 넘김": int(to_llm.sum()), "정확도": round(float((final == test["분류"]).mean()), 3),
                 "분류기가 끝낸 것의 정확도": round(float((pred[~to_llm] == test["분류"].values[~to_llm]).mean()), 3) if (~to_llm).any() else None,
                 "40개 예상 초": round(fast_s + to_llm.sum() * llm_s / 40, 1)})
print(pd.DataFrame(rows).to_string(index=False))
print(f"(견주기) 분류기만 {(pred == test['분류']).mean():.3f}, {fast_s:.2f}초 · LLM 만 {(llm == test['분류']).mean():.3f}, {llm_s:.1f}초")
출력
 기준값  LLM 에 넘김   정확도  분류기가 끝낸 것의 정확도  40개 예상 초
 0.4        10 0.825           0.833       4.1
 0.5        18 0.850           0.909       7.2
 0.6        28 0.900           1.000      11.0
 0.7        33 0.900           1.000      12.9
 0.8        35 0.900           1.000      13.6
 0.9        40 0.900             NaN      15.5
(견주기) 분류기만 0.700, 0.35초 · LLM 만 0.900, 15.2초

4단계. API 파일 쓰기

Python
INQUIRY_API = r'''
"""솔빛생활 고객 문의 분류 API (17차시 프로젝트 2)"""
import json, os, time, urllib.request
from contextlib import asynccontextmanager
from typing import Annotated

import joblib
from fastapi import FastAPI
from pydantic import BaseModel, Field
from sentence_transformers import SentenceTransformer

OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.environ.get("INQUIRY_MODEL", "qwen2.5:3b")
THRESHOLD = float(os.environ.get("INQUIRY_THRESHOLD", "0.6"))     # 확신도가 이보다 낮으면 LLM 에 넘긴다
CLF_PATH = os.environ.get("INQUIRY_CLF", "out_inquiry_clf.joblib")
LABELS = ["배송", "교환반품", "결제", "회원", "기타"]
SCHEMA = {"type": "object", "properties": {"분류": {"type": "string", "enum": LABELS}}, "required": ["분류"]}
SYSTEM = ("쇼핑몰 고객 문의를 다섯 분류 가운데 하나로 나누세요.\n"
          "- 배송: 배송 상태, 주소, 택배\n- 교환반품: 교환, 반품, 불량, 잘못 온 상품\n"
          "- 결제: 결제, 할부, 영수증, 환불 금액, 쿠폰 할인\n- 회원: 로그인, 계정, 등급, 적립금, 회원 쿠폰\n"
          "- 기타: 재입고, 포장, 매장, 리뷰, 고객센터 같은 그 밖의 문의\nJSON 으로 답하세요.")
state = {"준비": False, "데우기실패": False, "LLM호출": 0, "LLM실패": 0}


def post(path, body, timeout):
    req = urllib.request.Request(OLLAMA_URL + path, data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())


def ask_llm(text, timeout=30):
    body = {"model": MODEL, "stream": False, "keep_alive": "30m", "format": SCHEMA,
            "messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": text}],
            "options": {"temperature": 0, "seed": 42, "num_predict": 30}}
    return json.loads(post("/api/chat", body, timeout)["message"]["content"])["분류"]


@asynccontextmanager
async def lifespan(app):
    saved = joblib.load(CLF_PATH)                       # 분류기, 임베딩 모델 이름, 판
    state["clf"], state["판"] = saved["분류기"], saved["판"]
    state["emb"] = SentenceTransformer(saved["임베딩"], device="cpu")
    try:
        ask_llm("데우기 문의입니다", timeout=300)         # LLM 도 미리 올려 둔다
    except (OSError, ValueError, KeyError):            # 데우기에 실패해도 서버는 뜬다(분류기는 쓸 수 있다)
        state["데우기실패"] = True
    state["준비"] = True
    yield
    state.clear()


app = FastAPI(title="고객 문의 분류 API", version="1.0", lifespan=lifespan)


def classify_many(texts):
    """임베딩은 한 번에 계산하고, 확신도가 낮은 문의만 LLM 에 넘긴다. LLM 이 두 번 실패하면 분류기의 답을 쓴다"""
    X = state["emb"].encode(["query: " + t for t in texts], normalize_embeddings=True)
    out = []
    for text, p in zip(texts, state["clf"].predict_proba(X)):
        label, conf, by = str(state["clf"].classes_[p.argmax()]), float(p.max()), "분류기"
        if conf < THRESHOLD:
            for _ in range(2):
                try:
                    label, by = ask_llm(text), "LLM"
                    state["LLM호출"] += 1
                    break
                except (OSError, ValueError, KeyError):
                    state["LLM실패"] += 1
                    by = "분류기(LLM 실패)"
                    post("/api/generate", {"model": MODEL, "keep_alive": 0}, 30)
        out.append({"문의": text, "분류": label, "확신도": round(conf, 3), "판단": by})
    return out


Text = Annotated[str, Field(min_length=2, max_length=300)]      # 문의 하나는 2~300자


class One(BaseModel):
    문의: Text


class Many(BaseModel):
    문의들: list[Text] = Field(min_length=1, max_length=50)       # 한 번에 1~50개


@app.get("/health")
def health():
    return {"준비": state.get("준비", False), "판": state.get("판"), "기준값": THRESHOLD, "모델": MODEL, "데우기실패": state.get("데우기실패"),
            "LLM호출": state.get("LLM호출"), "LLM실패": state.get("LLM실패")}


@app.post("/classify")
def classify(req: One):
    t = time.perf_counter()
    return {**classify_many([req.문의])[0], "판": state["판"], "걸린초": round(time.perf_counter() - t, 3)}


@app.post("/classify/batch")
def classify_batch(req: Many):
    t = time.perf_counter()
    results = classify_many(req.문의들)
    return {"결과": results, "LLM호출": sum(r["판단"] == "LLM" for r in results), "판": state["판"],
            "걸린초": round(time.perf_counter() - t, 3)}
'''
from pathlib import Path
Path("out_inquiry_api.py").write_text(INQUIRY_API.lstrip(), encoding="utf-8")
print("out_inquiry_api.py ·", len(INQUIRY_API.splitlines()), "줄")
출력
out_inquiry_api.py · 103 줄

5단계. 묶음 요청으로 재기

Python
sys.path.insert(0, ".")
from fastapi.testclient import TestClient
import out_inquiry_api as api

with TestClient(api.app) as client:
    print("health:", client.get("/health").json())
    one = client.post("/classify", json={"문의": "주문한 지 나흘이 지났는데 아직 안 왔어요."}).json()
    print("하나:", one)
    batch = client.post("/classify/batch", json={"문의들": test["문의"].tolist()}).json()
    got = np.array([r["분류"] for r in batch["결과"]])
    print(f"묶음 40개: 정확도 {(got == test['분류']).mean():.3f} · LLM 호출 {batch['LLM호출']} · {batch['걸린초']}초 · 판 {batch['판']}")
    print("판단별:", pd.Series([r["판단"] for r in batch["결과"]]).value_counts().to_dict())
    print("빈 글:", client.post("/classify", json={"문의": ""}).status_code,
          "· 51개:", client.post("/classify/batch", json={"문의들": ["배송 언제 와요?"] * 51}).status_code,
          "· 빈 목록:", client.post("/classify/batch", json={"문의들": []}).status_code)
    for r, y in zip(batch["결과"], test["분류"]):
        if r["분류"] != y:
            print(f"  틀림: {r['문의']} 정답 {y} · {r['분류']}({r['판단']}, 확신도 {r['확신도']})")
출력
health: {'준비': True, '판': 'inquiry-lr-1', '기준값': 0.6, '모델': 'qwen2.5:3b', '데우기실패': False, 'LLM호출': 0, 'LLM실패': 0}
하나: {'문의': '주문한 지 나흘이 지났는데 아직 안 왔어요.', '분류': '배송', '확신도': 0.736, '판단': '분류기', '판': 'inquiry-lr-1', '걸린초': 0.024}
묶음 40개: 정확도 0.900 · LLM 호출 28 · 10.957초 · 판 inquiry-lr-1
판단별: {'LLM': 28, '분류기': 12}
빈 글: 422 · 51개: 422 · 빈 목록: 422
  틀림: 택배 송장 번호는 어디서 봐요? 정답 배송 · 결제(LLM, 확신도 0.451)
  틀림: 대량 구매 견적을 받고 싶어요. 정답 기타 · 결제(LLM, 확신도 0.374)
  틀림: 문 앞에 두고 가 달라고 할 수 있어요? 정답 배송 · 교환반품(LLM, 확신도 0.343)
  틀림: 생일 쿠폰이 안 왔어요. 정답 회원 · 교환반품(LLM, 확신도 0.48)

손으로 쓴 시험 문의 40개에서 빠른 분류기는 0.700, LLM 은 0.900 이었습니다. 시간은 분류기가 40개에 0.35초, LLM 이 15.2초(하나에 0.38초)였습니다. 교안을 만들며 따로 돌렸을 때 LLM 은 0.950 이었으니 실행마다 한두 개가 달라집니다. 기준값 표를 보면 0.6 에서 28개만 LLM 에 넘기고도 LLM 만 쓸 때와 같은 0.900 이 나왔습니다. 예상 시간은 11.0초로 LLM 만 쓸 때의 15.5초보다 짧습니다. 그래서 API 의 기준값을 0.6 으로 두었습니다. 기준값 0.6 에서 분류기가 끝낸 12개는 모두 맞았습니다. 기준값을 0.5 로 내리면 LLM 호출이 18개로 줄지만 정확도가 0.850 으로 떨어집니다. API 로 40개를 묶어 보내자 정확도 0.900, LLM 호출 28개, 10.96초로 표의 예상과 맞았습니다. 빈 글, 51개 묶음, 빈 목록은 모두 422 였습니다. 틀린 넷은 모두 확신도가 낮아 LLM 에 넘긴 문의였습니다. LLM 이 "택배 송장 번호"를 결제로, "생일 쿠폰"을 교환반품으로 나눴는데, 둘 다 분류기는 맞힌 문의였습니다. 두 단계 분류도 LLM 의 실수까지 막아 주지는 못합니다. 문의 수가 많아지면 틀린 문의를 학습 데이터에 더해 분류기를 다시 학습하고, LLM 에 넘기는 수를 줄여 갑니다.

이 프로젝트가 뜻하는 것

이 프로젝트는 우편물 분류실에서 손이 빠른 신입이 먼저 나누고, 자신 없는 우편물만 꼼꼼한 선임에게 넘기는 일과 같습니다. 신입을 교육하고 시험 우편물 40통으로 실력을 봅니다(1단계). 같은 40통을 선임에게도 모두 맡겨 봅니다(2단계). "얼마나 자신 없으면 선임에게 넘길지" 선을 여러 개 그어 보고 하나를 고릅니다(3단계). 분류실 규칙서를 쓰고(4단계), 40통을 한 묶음으로 보내 실제로 처리해 봅니다(5단계). 여기서 신입이 임베딩과 로지스틱 회귀로 만든 빠른 분류기, 선임이 LLM(Large Language Model, 대규모 언어 모델)인 qwen2.5:3b, 신입의 자신감이 확신도, 넘기는 선이 기준값, 이 방식 전체가 두 단계 분류입니다. 비유와 다른 점은, 선임도 틀릴 수 있어서 넘긴 우편물 가운데 신입이 맞혔을 것을 선임이 틀리기도 한다는 것입니다.

단계출력뜻
1. 데이터학습 200 개 · 시험 40 개틀로 만든 학습용 문의 200개로 배우고, 말투가 다른 손으로 쓴 문의 40개로 시험합니다. 모두 지어낸 연습용 데이터입니다
1. 분류기정확도 0.700 · 40개에 353ms빠른 분류기는 40개를 0.35초(ms 는 millisecond, 1,000분의 1초)에 나누지만 10개 가운데 7개만 맞힙니다
1. 확신도p10/p50/p90 0.329, 0.518, 0.838시험 문의의 절반은 분류기의 확신도가 0.518 아래입니다. 처음 보는 말투라 자신 없는 문의가 많습니다
2. LLM정확도 0.900 · 40개에 15.2초(하나에 0.38초) · 다시 물음 0LLM 은 10개 가운데 9개를 맞히지만, 분류기보다 40배 넘게 오래 걸립니다
2. LLM 이 틀린 넷송장 번호 → 결제 · 견적 → 결제 · 문 앞 → 교환반품 · 생일 쿠폰 → 교환반품이 가운데 송장 번호와 생일 쿠폰은 분류기가 맞힌 문의입니다. LLM 도 만능은 아닙니다
3. 기준값 0.6LLM 에 넘김 28 · 정확도 0.900 · 분류기가 끝낸 것 1.000 · 예상 11.0초28개만 넘기고도 LLM 만 쓸 때와 같은 정확도가 나옵니다. 분류기가 스스로 끝낸 12개는 모두 맞았습니다
3. 기준값 0.4, 0.5넘김 10, 18 · 정확도 0.825, 0.850선을 낮추면 LLM 을 덜 부르지만 정확도가 떨어집니다
3. 기준값 0.9넘김 40 · 분류기가 끝낸 것 NaN · 15.5초모두 넘기면 LLM 만 쓰는 것과 같습니다. NaN 은 분류기가 끝낸 문의가 없어 비율을 낼 수 없다는 뜻입니다
4. API 파일out_inquiry_api.py · 103 줄기준값 0.6 을 기본값으로 둔 분류 API 를 파일로 썼습니다
5. 하나배송 · 확신도 0.736 · 판단 분류기 · 걸린초 0.024확신도가 기준값 0.6 보다 높아 LLM 을 부르지 않고 0.02초 만에 답했습니다
5. 묶음 40개정확도 0.900 · LLM 호출 28 · 10.957초3단계 표의 예상(28개, 11.0초)과 거의 같게 나왔습니다
5. 판단별LLM 28 · 분류기 1240개 가운데 12개, 곧 30% 는 LLM 을 부르지 않고 끝냈습니다
5. 잘못된 입력빈 글 422 · 51개 422 · 빈 목록 422약속한 범위(2~300자, 한 번에 1~50개) 밖의 요청은 막혔습니다
5. 틀린 넷모두 LLM 판단, 확신도 0.343~0.48틀린 것은 모두 분류기가 자신 없어 LLM 에 넘긴 문의이고, 2단계에서 LLM 이 틀린 넷과 같습니다

한 줄로 줄이면, 빠른 분류기와 정확한 LLM 을 나눠 쓰면 LLM 만 쓸 때와 같은 정확도를 LLM 을 덜 부르며 더 짧은 시간에 얻을 수 있다는 것을 보인 프로젝트입니다. 다만 시험 문의가 40개뿐이고, 교안을 만들며 따로 돌렸을 때 LLM 정확도가 0.950 이었던 것처럼 실행마다 한두 개가 달라집니다. 아직 40개 가운데 28개를 LLM 에 넘기므로, 틀린 문의를 학습 데이터에 더해 분류기를 키우는 것이 다음 할 일입니다. 이 API 는 프로젝트 3에서 요약 API 와 함께 배포 묶음으로 들어갑니다.

← 이전 절17장 · 프로젝트 1 — 요약 API 서비스다음 절 →17장 · 프로젝트 3 — 두 서비스를 한 묶음으로 배포하기