Performans ve maliyet
Cevap doğru ve kaynaklı. Peki bir soru kaç saniye sürüyor, API’li bir modelde fatura neye göre büyür? Tahmine gerek yok: her adım kendi süresini ve token sayısını yazıyor.
Hepsini prompt'a koysak?
8 dokümanın tamamı (40709 karakter) tek prompt’ta: 9954 prompt token. RAG 3 chunk gönderir: 976 token, yaklaşık 10 kat az (rerank hariç). 9954 token, 8192’lik penceremize (num_ctx) sığmaz; bu tek koşu için 32768’e açtık.
…ve maaş tablosu herkese
Cevap doğru: 22. Ama 8 dokümanın biri hr-only maaş tablosu. Retrieval yok, erişim filtresi yok: maaşlar herkese gider. 27149 ms sürdü; RAG cevapları 674–3462 ms. Tek koşudan kat hesaplamıyoruz.
Bir sorunun süresi nereye gidiyor?
Günün sorusu: arama 22 ms (vector DB 8 ms). Rerank 1500 ms, cevap 678 ms. Rerank toplamın (2200 ms) yaklaşık %68’i: 5 LLM çağrısı, her chunk için bir tane. Sizde süreler farklı olur.
npm run ask -- "I am going to Frankfurt for a two-day meeting. I am a specialist. How much is my daily per diem?": arama, rerank ve cevap sürelerini not edin.Ingest'te: cache ve artımlı yükleme
Her embedding
.cache/altına yazılır; anahtar, model ve metnin hash’i. İkinci koşu: 60 vektörün 60’ı cache’ten,4-embed.ts55 ms. 2026’yı eklemek yalnızca onun 60 kaydını yazdı; 2025 yerinde kaldı.Süreyi ve faturayı ne büyütür?
Modele yapılan çağrı sayısı ve her çağrıdaki metin. Bir soru 6 çağrı: 5 rerank, 1 cevap. N kişi aynı anda: N × 6. Aşağıdaki ayarlar süreyi değiştirir; çoğu doğruluğu da. Sonra eval’ı koşun.
Aynı soruyu--k 10ile sorun: rerank kaç chunk puanladı, süre nasıl değişti?src/steps/7-rerank.tsiçinde rerank’ı kapatıp soruyu tekrar sorun: süre ve cevap ne oldu? Sonra rerank’ı geri açın.
Ölçümler, adım adım
Bölüm başlığı “Ölçümler, adım adım”Günün sorusu için arama, rerank ve cevap; süreler her logun TIME satırında:
$ npm run step -- 6
━━ STEP 6/8 · RETRIEVE IN How many working days of annual leave does an employee with 7 years of service get? WHAT embed the question (bge-m3, 0 ms) · ask chroma for the 5 closest · filter {"edition":"2025","access":["all"]} OUT 5 chunks handed on · search 8 ms 1 0.614 hr-leave@2025#3 Annual Leave Policy › §3 Part-time staff get the same number as … 2 0.604 hr-leave@2025#2 Annual Leave Policy › §2 One full year of work, counted from the… 3 0.602 hr-leave@2025#7 Annual Leave Policy › §7 Some unused leave can move to the next … 4 0.598 hr-leave@2025#6 Annual Leave Policy › §6 Unpaid leave is for serious personal re… 5 0.598 hr-leave@2025#4 Annual Leave Policy › §4 Each unit enters its yearly leave plan … ─ ─ ─ cut: k = 5 ─ ─ ─ 6 0.585 benefits@2025#6 Benefits › §6 Each employee gets 1 working day of bir… 7 0.583 hr-leave@2025#5 Annual Leave Policy › §5 In the cases below, an employee gets pa… 8 0.569 hr-leave@2025#8 Annual Leave Policy › §8 When a contract ends for any reason, un… TIME 22 ms → data/6-retrieved.json$ npm run step -- 7
━━ STEP 7/8 · RERANK IN data/6-retrieved.json (5 chunks for "How many working days of annual leave does an employee with 7 years of service get?") WHAT gemma3:4b scores each (question, chunk) pair 0–10 · re-sort · keep the top 3 OUT 5 chunks scored in 1498 ms · ties: 2 of 5 1 was 1 rel 10 vec 0.614 hr-leave@2025#3 Annual Leave Policy › §3 2 ▲ was 3 rel 6 vec 0.602 hr-leave@2025#7 Annual Leave Policy › §7 3 ▲ was 5 rel 6 vec 0.598 hr-leave@2025#4 Annual Leave Policy › §4 ─ ─ ─ into the prompt: top 3 ─ ─ ─ 4 ▼ was 2 rel 3 vec 0.604 hr-leave@2025#2 Annual Leave Policy › §2 5 ▼ was 4 rel 2 vec 0.598 hr-leave@2025#6 Annual Leave Policy › §6 TIME 1500 ms → data/7-reranked.json$ npm run step -- 8 --prompt
━━ STEP 8/8 · ANSWER IN data/7-reranked.json (top 3 of 5 chunks) WHAT rules (7) + numbered sources + question → gemma3:4b (add --prompt to see it) OUT 976 prompt tokens · 21 output tokens · 674 ms TIME 678 ms → data/8-answer.jsonHepsini prompt’a koymak. Retrieval yok: 8 dokümanın temiz metni art arda tek prompt’a girer; kurallar ve soru aynı.
| RAG (3 chunk) | hepsi (8 doküman) | |
|---|---|---|
| prompt token (rerank hariç) | 976 | 9954 |
| prompt’taki hr-only doküman | yok (filtre) | maaş tablosu |
| cevap süresi | 678 ms | 27149 ms |
| cevap | 22 iş günü ✓ | 22 iş günü ✓ |
Fark, her soruda ödenen bedelde; el kitabı büyüdükçe bu yol kapanır. Daha büyük pencere için Ollama modeli yeniden yükleyebilir; bunun 27149 ms’nin ne kadarı olduğunu ölçmedik; asıl maliyet 9954 token’ı okumak.
$ every document in one prompt (no retrieval) documents 8 (40709 characters) prompt tokens 9954 time 27149 ms
An employee with 7 years of service gets 22 working days of annual paid leave. [1]Ölçüm kodu; pencereyi 32768 token’a açan kısım numCtx:
await record("stuff-everything", "every document in one prompt (no retrieval)", async () => { const docs = JSON.parse(await readFile("data/2-clean.json", "utf8")).data as { id: string; text: string }[]; const all = docs.map((d) => `### ${d.id}\n${d.text}`).join("\n\n"); const g = await generate(`SOURCES:\n\n${all}\n\nQUESTION: ${config.dayQuestion}\nANSWER:`, { system: steps.s8.system(), numCtx: 32768 }); console.log(` documents ${docs.length} (${all.length} characters)`); console.log(` prompt tokens ${g.promptTokens}`); console.log(` time ${g.ms} ms`); console.log(`\n${g.text}`);});Ingest tarafı: cache ve artımlı yükleme
Bölüm başlığı “Ingest tarafı: cache ve artımlı yükleme”Aynı metin, aynı modelle her zaman aynı vektörü verir. Cache .cache/embeddings.json içinde; anahtar, model adı ile metnin birlikte alınmış hash’i:
/** * What it does: turns texts into vectors with the embedding model, and reuses cached vectors. * * Turn texts into vectors with the embedding model — one vector per text, same order. * Sends them in batches of `config.embedBatch`; texts seen before come from the cache. * @param texts the chunks (at ingest) or the question (at query time) * @returns the vectors and how many were freshly computed * @example const { vectors } = await embed(["annual leave"]); vectors[0].length // 1024 */export async function embed(texts: string[], model = config.embedModel): Promise<EmbedResult> { const store = await loadCache(); const missing = [...new Set(texts.filter((t) => !store[cacheKey(model, t)]))]; for (let i = 0; i < missing.length; i += config.embedBatch) { const batch = missing.slice(i, i + config.embedBatch); const data = await post<{ embeddings: number[][] }>("/api/embed", { model, input: batch }); batch.forEach((t, j) => (store[cacheKey(model, t)] = data.embeddings[j]!)); } if (missing.length) await saveCache(); return { vectors: texts.map((t) => store[cacheKey(model, t)]!), computed: missing.length };}$ npm run step -- 4
━━ STEP 4/8 · EMBED IN data/3-chunks.json (60 chunks) WHAT send each chunk's text to bge-m3 (Ollama /api/embed, 24 per call) OUT 60 vectors · 1024 numbers each · 0 computed, 60 from cache announcement-meal-card@2025#1 → [-0.0356, 0.0001, -0.0278, -0.0149, -0.0330, -0.0674, … ] length (norm) of that vector: 1.000 TIME 55 ms → data/4-vectors.jsonBu loglarda bütün vektörler cache’ten; ilk embed süresini göstermiyorlar. npm run doctor tek bir cümleyi 150 ms’de embed etti. Model bellekte değilse ilk çağrıya modelin yüklenme süresi de eklenir. Kendi makinenizde görmek için .cache/embeddings.json dosyasını silip npm run step -- 4 çalıştırın; log chunk başına milisaniyeyi yazar.
Depo sürüm başına yazar (replaceEdition):
export interface VectorStore { kind: StoreKind; /** Replace every record of one edition with these. Other editions stay untouched. */ replaceEdition(edition: string, records: VectorChunk[]): Promise<void>; /** The k records closest to `vector` that pass `filter`, best first. */ query(vector: number[], k: number, filter?: Filter): Promise<Hit[]>; count(): Promise<number>; /** Editions present in the store, sorted — "latest" is the last one. */ editions(): Promise<string[]>; /** Delete everything. */ reset(): Promise<void>;}━━ STEP 4/8 · EMBED IN data/3-chunks.json (60 chunks) WHAT send each chunk's text to bge-m3 (Ollama /api/embed, 24 per call) OUT 60 vectors · 1024 numbers each · 0 computed, 60 from cache announcement-meal-card@2026#1 → [-0.0356, 0.0001, -0.0278, -0.0149, -0.0330, -0.0674, … ] length (norm) of that vector: 1.000 TIME 49 ms → data/4-vectors.json
━━ STEP 5/8 · STORE IN data/4-vectors.json (60 vectors, edition 2026) WHAT replace edition 2026 in Chroma collection "kraken-policies" (HNSW index, cosine) OUT 60 records added · 120 in the store · editions: 2025, 2026
the day's question, top-5 ids: JSON (brute force) hr-leave@2026#3 hr-leave@2026#2 hr-leave@2026#7 hr-leave@2026#6 hr-leave@2026#4 chroma hr-leave@2026#3 hr-leave@2026#2 hr-leave@2026#7 hr-leave@2026#6 hr-leave@2026#4 identical ✓ — the database changes the scale, not the ranking TIME 197 ms → data/5-store.jsonAyarlar
Bölüm başlığı “Ayarlar”| Ayar | Bizde | Artırınca | Burada ölçtük mü? |
|---|---|---|---|
| Chunk boyu | section: ortalama 695 karakter, 60 chunk · fixed: 300 karakter, 138 chunk | prompt’a giren token artar, vektör sayısı azalır | Evet: hit@1 fixed ile 0.526 (19 sorunun 10’u), section ile 0.842 (16’sı) (Kaliteyi ölçmek) |
| k (retriever’ın getirdiği aday) | 5 | rerank’ta her aday bir LLM çağrısı daha; prompt’a yine 3 chunk girer | Hayır: --k 10 ile siz deneyin |
| Prompt’a giren chunk | 3 | prompt token artar | 3 chunk = 976 prompt token (günün sorusu) |
| Rerank | açık | kapatınca 5 çağrı gider, sıra vektör skoruna kalır | Kısmen: günün sorusunda cevap iki durumda da 22; q02’de doğru bölüm vektör sırasında 5.; rerank açıkken cevap doğru, kapalıyken burada ölçmedik. Kapanış sayfasındaki ilk ödev bu. |
| Model boyu | gemma3:4b | RAM ve süre artar | Hayır: süreyi yalnızca gemma3:4b ile ölçtük |
| Embedding cache | açık | — | Evet: 60 vektörün 60’ı cache’ten, 4-embed.ts = 55 ms |
| Artımlı ingest | sürüm başına replaceEdition |
— | Evet: 2026 eklendi, depoda 120 kayıt, 2025’e dokunulmadı |
Fatura: fiyatsız bir formül
Bölüm başlığı “Fatura: fiyatsız bir formül”Fiyatlar sağlayıcıya göre değişir; rakam yerine formül. API’li bir modelde:
günlük maliyet ≈ sorgu/gün × (cevap prompt’u + 5 × rerank prompt’u + çıktı token’ı) × birim fiyat + ingest’te bir kerelik embedding (yalnızca değişen sürüm; cache aynı metni yeniden embed etmez)
- Girdi ve çıktı token’ının fiyatı çoğu sağlayıcıda farklıdır; formülü ikiye bölerek hesaplayın.
- Bizim ölçtüğümüz terimler, günün sorusunda: cevap prompt’u 976 token, çıktı 21 token. Rerank prompt’larının token’ını loglamıyoruz; her biri soruyu ve tek bir chunk’ı taşır.
- Kendi sunucunuzda (self-host) birim fiyatın yerini GPU saati alır: aynı anda gelen N soru × 6 çağrıyı bekletmeden karşılayacak kapasite.
npm run ask -- "I am going to Frankfurt for a two-day meeting. I am a specialist. How much is my daily per diem?"çalıştırın. Arama, rerank ve cevap sürelerini not edin.- Aynı soruyu
--k 10ile çalıştırın. Rerank kaç chunk puanladı, süresi nasıl değişti? Cevap değişti mi? src/steps/7-rerank.tsiçindekiTOGGLE rerankbloğundatrue; // defaultsatırını yoruma alın,false; // alternativesatırını açın; soruyu tekrar sorun. Kazandığınız süreyi ve cevabı karşılaştırın, sonra toggle’ı geri alın.
Sık sorulan
Bölüm başlığı “Sık sorulan”Daha hızlı bir vector DB alsak?
Bu ölçümde vector DB araması 8 ms; rerank ile cevap birlikte 2178 ms (1500 + 678). Darboğaz modele yapılan çağrılar. Milyonlarca vektörde arama da pahalılaşır; D sayfasındaki indeks bunun için.
Rerank'ı kapatıp süreyi kısaltalım mı?
Yalnızca eval öyle derse. Kapanış sayfasındaki ilk ödev tam bunu ölçer. Süre ve doğruluk aynı ayara bağlı (yukarıdaki tabloda Rerank satırı). Daha hızlı bir seçenek, bu iş için eğitilmiş küçük bir reranker modeli (cross-encoder); onu ölçmedik.
Cevapları da cache'lesek?
Evet. Temperature 0 ve sabit seed ile aynı soru ve kaynaklar aynı cevabı verir. Bu, aynı makinede geçerli. Cevabı saklamak mümkün. Anahtarda soru, sürüm ve erişim kapsamı olmalı. Model ve prompt sürümü de olmalı. Kapsam anahtarda yoksa İK’nın aldığı maaş cevabı önbellekten bir çalışana gider. Yeni bir sürüm ingest edilince o sürümün cache’i silinir. Bu repoda cevap cache’i yok.
Daha büyük bir model daha mı iyi?
Daha yavaş ve daha çok RAM ister; daha iyi olup olmadığını eval söyler. Bu sitede süreler yalnızca gemma3:4b ile ölçüldü. Model değiştirmek için başka bir model adıyla bir CHAT_MODEL= satırını .env dosyasına ekleyin. .env yok mu? Önce env.example dosyasını .env adıyla kopyalayın. O modeli önce ollama pull ile indirin. Rerank da aynı modeli kullanır, yani onun süresi de değişir. Sonra npm run eval -- --answers.