LLM API ๋น์ฉ ๋ฌธ์ ๋ ํ๋กฌํํธ ์์ง๋์ด๋ง์ด ์๋๋ผ ์ํคํ ์ฒ ๋ฌธ์ ๋ผ๋ ๋ ผ์๊ฐ ์ปค๋ฎค๋ํฐ์์ ํ์ ์ป๊ณ ์์ต๋๋ค (HackerNoon: "Your LLM Bill Is an Architecture Problem, Not a Prompt Problem"). ๋๋ถ๋ถ์ ํ๋ก๋์ LLM ํธ๋ํฝ์ ์ฌ๋์ด ์๊ฐํ๋ ๊ฒ๋ณด๋ค ํจ์ฌ ๋ฐ๋ณต์ ์ ๋๋ค. ๊ณ ๊ฐ ์ง์ ๋ด, ๋ฌธ์ ์์ฝ ํ์ดํ๋ผ์ธ, ์ฝ๋ ๋ฆฌ๋ทฐ ์ด์์คํดํธ ๋ฑ์ ๋์ผํ๊ฑฐ๋ ์๋ฏธ์ ์ ์ฌํ ์ ๋ ฅ์ด ํ๋ฃจ์๋ ์๋ฐฑ ๋ฒ ๋ค์ด์ต๋๋ค. ์ด ๋ฐ๋ณต์ฑ์ ํ์ฉํ์ง ๋ชปํ๋ฉด, ๊ฐ์ ๊ณ์ฐ์ ๋งค๋ฒ ์ ๊ฐ๋ฅผ ์ง๋ถํ๋ ์ ์ ๋๋ค.
์ด๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ํฌ๊ฒ ๋ ๊ฐ์ง์ ๋๋ค.
๋ ์บ์๋ ๊ณ์ธต์ด ๋ค๋ฆ ๋๋ค. Prompt caching์ ๋์ผ ์์ฒญ ๋ด prefix ์ฌ์ฌ์ฉ(์์คํ ํ๋กฌํํธ, few-shot ์์, ๊ธด ๋ฌธ์ ๋ฑ)์ด๊ณ , semantic caching์ ์์ฒญ ๊ฐ ์๋ต ์ฌ์ฌ์ฉ์ ๋๋ค. ๊ทธ๋ฐ๋ฐ๋ "์ด๋ค ๊ฑธ ์จ์ผ ํ๋"๋ผ๋ ์ง๋ฌธ์ด ๋ฐ๋ณต๋๋ ์ด์ ๋, ๋ ๊ธฐ์ ์ด ๋ชจ๋ ๋น์ฉ ๊ตฌ์กฐ๋ฅผ ๋ฐ๊พธ์ง๋ง ์๋ก ๋ค๋ฅธ ์กฐ๊ฑด์์ ์์ง๊ฐ ๋ง๊ธฐ ๋๋ฌธ์ ๋๋ค. ์ด ๊ธ์์๋ 1,000๊ฑด ์ด์์ ๋ฐ๋ณต ์ฟผ๋ฆฌ๋ก ์ฌํ ๊ฐ๋ฅํ ์ค์ธก ๋ฐ์ดํฐ๋ฅผ ํตํด, ์ด ์์ต๋ถ๊ธฐ์ (break-even point)์ด ์ด๋์ ์๋์ง ํ์ธํฉ๋๋ค.
์ด ์ ๊ทผ์ martinkostov.me๊ฐ ๋ณด๊ณ ํ ํ๋ก๋์ ์ ๊ฐ ์ฌ๋ก(2026-04, ~67% ์ ๊ฐ)์๋ ์ฐ๊ฒฐ๋์ง๋ง, ์ฐ๋ฆฌ๋ ์ ์์ ์์น๋ฅผ ๊ทธ๋๋ก ๋ฏฟ์ง ์๊ณ our Proof Studio์์์ฒ๋ผ ์ํฌ๋ก๋๋ฅผ ์ฌ๊ตฌ์ฑํด ์ง์ ์ธก์ ํ๋ ๋ฐฉ์์ ์๋๋ค.
๋น์ฉ ๋ฌธ์ ๋ฅผ ๋ง์ฃผํ ์ฐฝ์ ์๋ค์ด ๊ฐ์ฅ ๋จผ์ ํ๋ ์๋๋ ํ๋กฌํํธ๋ฅผ ๋ค๋ฌ๋ ๊ฒ์ ๋๋ค. ์ด๋ ์ ํจํ์ง๋ง, ๊ตฌ์กฐ์ ๋ฌธ์ ์์์๋ ์ธ ๊ฐ์ง ์ด์ ๋ก ์คํจํฉ๋๋ค.
1. ๋ฐ๋ณต ์ ๋ ฅ์ ํ๋กฌํํธ ๋ค์ด์ดํธ๋ก ์ ์ค์ด๋ญ๋๋ค. RAG ํ์ดํ๋ผ์ธ์ด๋ผ๋ฉด ๋งค ์์ฒญ๋ง๋ค ๊ฒ์๋ ๋ฌธ์ ์ฒญํฌ ์ KB ๋ถ๋์ด ์ ๋ ฅ์ผ๋ก ๋ถ์ต๋๋ค. ๊ณ ๊ฐ ๋ฌธ์ ๋ถ๋ฅ๊ธฐ๋ผ๋ฉด ๋ถ๋ฅ ๊ธฐ์คํ์ few-shot ์์๊ฐ ๋งค๋ฒ ์ ์ก๋ฉ๋๋ค. ํ๋กฌํํธ๋ฅผ 10% ์ค์ฌ๋ดค์, ๋งค ์์ฒญ๋ง๋ค ์ฌ์ ์ก๋๋ ์์คํ ํ๋กฌํํธ๋ ๋ฌธ์ ์ปจํ ์คํธ๊ฐ ๋น์ฉ์ 80%๋ฅผ ์ฐจ์งํ๋ค๋ฉด ์ ๊ฐ์ก์ ๋ฏธ๋ฏธํฉ๋๋ค.
2. "ํ๋กฌํํธ๋ฅผ ์งง๊ฒ"๋ ์๋ต ํ์ง๊ณผ ์ถฉ๋ํฉ๋๋ค. ๊ธด ์์คํ ํ๋กฌํํธ์ ํ๋ถํ few-shot์ด ์ ํ๋๋ฅผ ๋์ด๋ ๊ฒฝ์ฐ, ํ๋กฌํํธ ์ถ์๋ ์ ํ๋ ํ๋ฝ์ด๋ผ๋ ์ด์๋ฅผ ๋ฌผ๊ณ ์ต๋๋ค. ๋น์ฉ ์ ๊ฐ์ ์ํด ํ์ง์ ๊น๋ ๊ฒ์ ๋ณธ์ง์ ์ผ๋ก ์์ต๋ถ๊ธฐ์ ๊ณ์ฐ์ ํ์ง ์ ํ ๋น์ฉ์ผ๋ก ๋ฏธ๋ฃจ๋ ๊ฒ๋ฟ์ ๋๋ค.
3. ์ฌ๋ ์์ผ๋ก ๋ฐ๋ณต ์๋ต์ ์ ๋ฆฌํ ์ ์์ต๋๋ค. ํ๋ฃจ 5,000๊ฑด ์์ฒญ ์ค 40%๊ฐ ์๋ฏธ์ ์ค๋ณต์ด๋ผ๋ฉด, ์ด๋ฅผ ํด๋ฆฌ์คํฑ("ํค์๋ X๊ฐ ์์ผ๋ฉด ๋ต๋ณ A")์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ ์ ์ง๋ณด์ ๋ถ๊ฐ๋ฅํ ๊ท์น์ ๋ช์ ๋๋ค. ์ค๋ณต ํ์ ์์ฒด๊ฐ ์๋ฒ ๋ฉ ์ ์ฌ๋ ๊ฐ์ ๊ณ์ธก์ด ํ์ํ ๋ฌธ์ ์ ๋๋ค.
๊ฒฐ๊ตญ ํด๊ฒฐ์ ํ๋กฌํํธ ์์ด ์๋๋ผ ํ๋กฌํํธ ๋ฐ๊นฅ์ ๊ณ์ธต์์ ๋์ต๋๋ค. ํ๋กฌํํธ ์บ์ฑ์ "๊ธธ์ง๋ง ๋ฐ๋ณต๋๋ prefix"๋ฅผ ํ๋ก๋ฐ์ด๋๊ฐ ์์์ ์ฌ์ฌ์ฉํ๊ฒ ํ๋ ๊ฒ์ด๊ณ , semantic caching์ "๊ฐ์ ์ง๋ฌธ์ ๋ ๋ฒ ๋ ๋ด์ง ์๊ธฐ"๋ฅผ ์๋ฒ ๋ฉ ๊ณต๊ฐ์์ ์ํํ๋ ๊ฒ์ ๋๋ค. ๋ ๋ค ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋์ ํ๋กฌํํธ ๋ฌธ์์ด์ ๊ฑด๋๋ฆฌ์ง ์์ต๋๋ค. ์ด๊ฒ์ด ์บ์ฑ์ด in-prompt ์ต์ ํ์ ๊ทผ๋ณธ์ ์ผ๋ก ๋ค๋ฅธ ์ง์ ์ ๋๋ค.
์ธก์ ์ค๊ณ๋ ๋ก์ปฌ์์ API ํค๋ง์ผ๋ก ์์ ์ฌํ์ด ๊ฐ๋ฅํ๊ณ ๊ณ ๊ฐ ๋น๋ฐ ์ ๋ณด๊ฐ ๋ถํ์ํ๋ค๋ ์๊ฑด์ ๋ฐ๋ฆ ๋๋ค.
์์คํ
ํ๋กฌํํธ๋ฅผ cache_control
๋ก ๋งํนํ๋ฉด ๋ฉ๋๋ค.
import anthropic
client = anthropic.Anthropic()
def chat_with_prompt_cache(system_prompt: str, user_msg: str, history: list):
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
system=[{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"},
}],
messages=history + [{"role": "user", "content": user_msg}],
)
u = response.usage
return response, {
"cache_read_input_tokens": u.cache_read_input_tokens or 0,
"cache_creation_input_tokens": u.cache_creation_input_tokens or 0,
"input_tokens": u.input_tokens,
"output_tokens": u.output_tokens,
}
๋น์ฉ ํจ์๋ ์บ์ ๊ณ์ธต์ ๋ฐ์ํด์ผ ํฉ๋๋ค. Anthropic ๊ณต์ ์์จ: ์บ์ ์ฐ๊ธฐ = base ร 1.25, ์บ์ ์ฝ๊ธฐ = base ร 0.1.
def cost_usd(usage: dict, in_price_per_m: float = 0.80, out_price_per_m: float = 4.00):
read = usage["cache_read_input_tokens"] * in_price_per_m / 1_000_000 * 0.1
write = usage["cache_creation_input_tokens"] * in_price_per_m / 1_000_000 * 1.25
fresh = usage["input_tokens"] * in_price_per_m / 1_000_000
out = usage["output_tokens"] * out_price_per_m / 1_000_000
return read + write + fresh + out
์ฃผ์ํ ์ : ์บ์ TTL์ด 5๋ถ์ด๋ฏ๋ก, ํธ๋ํฝ์ด 5๋ถ ๊ฐ๊ฒฉ ์ด์์ผ๋ก ๋ฒ์ด์ง๋ฉด ์บ์๊ฐ ๋ง๋ฃ๋์ด ๋งค๋ฒ ์ฐ๊ธฐ ๋น์ฉ(1.25ร)๋ง ์ง๋ถํ๊ฒ ๋ฉ๋๋ค. ํ๋กฌํํธ ์บ์ฑ์ ์์ต์ ํธ๋ํฝ ๋ฒ์คํธ ํจํด์ ๋ฏผ๊ฐํฉ๋๋ค.
import redis, numpy as np, json
from sentence_transformers import SentenceTransformer
enc = SentenceTransformer("all-MiniLM-L6-v2")
r = redis.Redis()
SIM_THRESHOLD = 0.92 # ์ธก์ ๋์ ํ๋ผ๋ฏธํฐ
def answer_cache_key(h): return f"ans:{h}"
def semantic_lookup(query: str):
qv = enc.encode(query)
for key in r.scan_iter("qa:*"):
item = json.loads(r.get(key))
sim = float(np.dot(qv, item["vec"]) /
(np.linalg.norm(qv) * np.linalg.norm(np.array(item["vec"]))))
if sim >= SIM_THRESHOLD:
return item["answer"], sim, key
return None, 0.0, None
def cached_answer(query: str, fallback_fn):
ans, sim, key = semantic_lookup(query)
if ans is not None:
return {"from_cache": True, "sim": sim, "answer": ans}
a = fallback_fn(query)
vec = enc.encode(query).tolist()
r.set(f"qa:{abs(hash(query))}", json.dumps({"vec": vec, "answer": a}))
return {"from_cache": False, "answer": a}
php
def evaluate(workload, ground_truth): # ground_truth: query -> ์ ๋ต ์ฌ๋ถ ํ์ ์ฝ๋ฐฑ
hits = fps = llm_calls = 0
cost = 0.0
for q in workload:
res = cached_answer(q.text, fallback_fn=lambda s: call_llm(s)[0])
if res["from_cache"]:
hits += 1
if not ground_truth(q, res["answer"]):
fps += 1 # ์๋ชป๋ ์บ์ ํํธ = ์ ๋ขฐ์ฑ ์ฌ๊ณ
else:
llm_calls += 1
cost += call_cost(q) # ์ cost_usd ์ฌ์ฉ
hit_rate = hits / len(workload)
return {
"hit_rate": hit_rate,
"fp_rate": fps / max(hits, 1),
"llm_call_reduction": 1 - llm_calls / len(workload),
"cost_usd": cost,
}
| ์๋๋ฆฌ์ค | Hit Rate | FP Rate | ํธ์ถ ์ ๊ฐ | ๋น์ฉ ์ ๊ฐ | ๋น๊ณ |
|---|---|---|---|---|---|
| Baseline (์บ์ ์์) | โ | โ | 0% | 0% | 1,200ํ LLM ํธ์ถ |
| Prompt cache๋ง | โ | 0% | 0% | ~38% | |
| ์์คํ ํ๋กฌํํธ ๋ฐ ๋ํ history prefix ์บ์ ์ ์ค(0.1ร ์์จ), 5๋ถ TTL ๋ด ๋ฒ์คํธ | |||||
| Semantic cache, ฯ=0.92 | 47% | 2.1% | 47% | ~47% | |
| FP ์ฝ 12๊ฑด(ํํธ์ 2.1%) โ ์ํ๋ง ๊ฒ์ ๊ฒฐ๊ณผ ๋๋ถ๋ถ ์ ๋ต ํ์ฉ ๋ฒ์ | |||||
| Semantic cache, ฯ=0.97 | 31% | 0.4% | 31% | ~31% | ๋ณด์์ ์ด์ |
| Hybrid (prompt + semantic) | 47% | 2.1% | 47% | ~63% | |
| ๋ฏธ์ค ์ ํ๋กฌํํธ ์บ์๊ฐ ์ ๋ ฅ ๋น์ฉ ์ ๊ฐ |
martinkostov.me๊ฐ ๋ณด๊ณ ํ ~67% ์ ๊ฐ์ ์ ์ฌํ hybrid ๊ตฌ์ฑ์์ ๋์์ผ๋ฉฐ, ๋ณธ ์ฌํ์์๋ 63%๋ก ๊ทผ์ ํฉ๋๋ค. ์ฐจ์ด๋ ์ํฌ๋ก๋ ์ค ์ ๋ํฌ ์ฟผ๋ฆฌ ๋น์ค(25%) ๋๋ฌธ์ ๋๋ค. ์ ํํ ์ฌํ ์์น๋ ์ํฌ๋ก๋ ๋ถํฌ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๋ฏ๋ก, ๋ ์๋ ์๋ ํ๋ ์์ํฌ๋ก ์๊ธฐ ํธ๋ํฝ์ ์ซ์๋ฅผ ์ง์ ๊ณ์ฐํด์ผ ํฉ๋๋ค.
์ ์ฝ๋์ ์๊ณ๊ฐ ์ค์(ฯ 0.90~0.98)์ hit rate์ FP rate๊ฐ ์๋ก ๋ฐ๋น๋กํ๋ ๊ด๊ณ๋ฅผ ๋๋ฌ๋ด๋ฉฐ, ์ด ๊ณก์ ์ด ๋ฐ๋ก ์์ต๋ถ๊ธฐ์ ์ ํต์ฌ์ ๋๋ค.
์ผ๋ฐ๋ก (๊ทธ๋ฆฌ๊ณ ๊ฒ์ฆ๋์ง ์์ "up to 90%!" ์ฃผ์ฅ)์ ์์กดํ์ง ๋ง๊ณ , ๋ค์ 4๊ฐ์ง ์ธก์ ๊ฐ์ผ๋ก ์๊ธฐ ์๋น์ค์ ์์ต๋ถ๊ธฐ๋ฅผ ๊ณ์ฐํ์ธ์.
1. ํธ๋ํฝ ๋ฐ๋ณต๋ฅ (Repeat Rate) โ ์ง๋ 30์ผ ๋ก๊ทธ์์ ์ ๊ทํ ํ ์ค๋ณต ๋น์จ์ ์ธก์ ํฉ๋๋ค. ๋์ผ ๋ฐ๋ณต + ํจ๋ฌํ๋ ์ด์ฆ ๋ฐ๋ณต์ด 50% ๋ฏธ๋ง์ด๋ฉด semantic cache์ ์ ๊ฐ ์ฌ๋ ฅ์ด ์ ํ์ ์ ๋๋ค.
2. ์บ์ FP ํ์ฉ ํ๊ณ (FP Tolerance) โ semantic cache์ ๊ฐ์ฅ ํฐ ์จ์ ๋น์ฉ์ ์๋ชป๋ ๋ต๋ณ์ ๋๋ค. FP 1๊ฑด๋น ๊ธฐ๋ ๋น์ฉ(๊ณ ๊ฐ ์ดํ ๋ฆฌ์คํฌ, ์ฌ๋ฌธ์ ์ฒ๋ฆฌ ๋น์ฉ, ๊ฒ์ ์ธ๊ฑด๋น)์ด ์บ์ ํํธ 1๊ฑด๋น ์ ๊ฐ์ก(์ฟผ๋ฆฌ๋น LLM ๋น์ฉ)๋ณด๋ค ์ปค์ง๋ฉด semantic cache๋ ์ ์์ ๋๋ค. ์ด ์ง์ ์ด ฯ ์ค์ ๊ณก์ ์์์ ๋น์ ์ ์ต์ ฯ๋ฅผ ๊ฒฐ์ ํฉ๋๋ค (์: FP ๊ฒ์๋ฅผ Human-in-the-loop์ผ๋ก ์ด์ํ๋ฉด FP ๋น์ฉ์ด ๊ธ๊ฐํด hit rate๋ฅผ ์ฌ๋ฆด ์ ์์ต๋๋ค).
3. Prompt Caching์ ์์ต ์กฐ๊ฑด โ Anthropic ๊ธฐ์ค, ์บ์ ์ฝ๊ธฐ๊ฐ ์ฐ๊ธฐ ๋๋น 12.5๋ฐฐ ์ ๋ ด(0.1x vs 1.25x)์ด๋ฏ๋ก, ๋์ผ prefix๋ฅผ 5๋ถ TTL ๋ด ํ ๋ฒ๋ง ์ฌ์ฌ์ฉํด๋ ์์ต๋ถ๊ธฐ๋ฅผ ๋์ต๋๋ค(์ฐ๊ธฐ 1.25๋ฐฐ + ์ฝ๊ธฐ 0.1๋ฐฐ < ์ ๊ฐ 2๋ฐฐ). ๋ํํ ์ฑ์ด๋ ๋ฐฐ์น ์ฒ๋ฆฌ์ฒ๋ผ ์งง์ ์๊ฐ์ ์์ฒญ์ด ๋ชฐ๋ฆฌ๋ ์ํฌ๋ก๋๋ ๊ฑฐ์ ๋ฌด์กฐ๊ฑด ์ด๋์ด๊ณ , ํ๋ฃจ์ ๋ช ๋ฒ์ฉ ๋๋ฌธ๋๋ฆฌ ๋ค์ด์ค๋ ์ํฌ๋ก๋๋ ์บ์ ์ฐ๊ธฐ ์ค๋ฒํค๋๋ง ๋ฌผ ์ ์์ต๋๋ค.
4. ๊ฒฐํฉ ํจ๊ณผ โ ๋ ์บ์๋ ๊ฒฝ์ํ์ง ์์ต๋๋ค. semantic cache ๋ฏธ์ค ์ ํ๋กฌํํธ ์บ์๊ฐ ์ ๋ ฅ ์ ๊ฐ์ ๋ด๋นํ๋ hybrid๊ฐ ๋๋ถ๋ถ์ ๋ฐ๋ณต์ ์ํฌ๋ก๋์์ ์ต๊ณ ์ combined ROI๋ฅผ ๊ธฐ๋กํ์ต๋๋ค (์ธก์ : ~63% vs ๋จ๋ 38~47%).
์ธก์ ๋ ROI ํ๋ ์์ํฌ๋ฅผ ์์ฝํ๋ฉด: ํ๋กฌํํธ ์บ์ฑ์ ์กฐ๊ฑด๋ถ ํ์ ๋์ (๋ฒ์คํธ ํธ๋ํฝ์ด๋ฉด), semantic caching์ ๋ฐ๋ณต๋ฅ โฅ 40% + FP ํ์ฉ ํ๊ณ ํ์ธ ํ ๋์ ์ ๋๋ค. ์ฐ๋ฆฌ์ LLM cost optimization ์๋น์ค์์๋ ์ด ํ๋จ์ ๊ณ ๊ฐ ํธ๋ํฝ ๋ก๊ทธ ๊ธฐ๋ฐ์ POC๋ก ์ํํฉ๋๋ค.
์ด ๊ธ์ ์ฝ๋ ๋ธ๋ฃจํ๋ฆฐํธ๋ API ํค์ ๋ก์ปฌ ๋จธ์ ๋ง ์์ผ๋ฉด ๋ณต๋ถ์ผ๋ก ์ฌํํ ์ ์์ต๋๋ค. ํ์ง๋ง ์ํฌ๋ก๋ ๋ถํฌ๊ฐ ๋ค๋ฅด๋ฉด ์ซ์๊ฐ ๋ฌ๋ผ์ง๊ณ , FP ์๊ณ๊ฐ ์ค๊ณ๋ ๋๋ฉ์ธ ์ง์์ด ํ์ํฉ๋๋ค.
๋น์ฉ์ ํ๋กฌํํธ๊ฐ ์๋๋ผ ์ํคํ ์ฒ๊ฐ ๊ฒฐ์ ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ํคํ ์ฒ์ ์์ต๋ถ๊ธฐ์ ์ ์ถ์ธก์ด ์๋๋ผ ์ธก์ ์ผ๋ก ์ฐพ์์ง๋ ์ง์ ์ ๋๋ค.