İçeriğe geç

Performans ve maliyet

7 yıllık bir çalışanın yıllık izni kaç iş günü?
İngilizce, logların yazdığı gibi: “How many working days of annual leave does an employee with 7 years of service get?”
→ “An employee with 7 years of service gets 24 working days of annual leave [1].” ✓
chroma · section · rerank on · sürüm 2026

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.

Prompt’a giren token (rerank hariç)RAG: 3 chunk · 976 token · 22 ✓hepsi: 8 doküman, maaş tablosu dahil · 9954 token · 22 ✓bizim num_ctx ayarımız: 8192 tokenGünün sorusu, tek koşu: süre nerede geçiyor?rerank · 5 LLM çağrısı · 1500 mscevap · 678 ms↑ arama adımı 22 ms (vector DB 8 ms · soruyu embed 0 ms, cache’ten)Ingest, ikinci kez: 60 vektörün 60’ı cache’ten, 4-embed.ts = 55 ms2025 · 60 kayıt · dokunulmadı2026 · 60 kayıt · yeni yazıldıdepoda 120 kayıt, 2 sürüm: yalnızca değişen sürüm yazılır
  1. 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.

  2. …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.

  3. 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.
  4. 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.ts 55 ms. 2026’yı eklemek yalnızca onun 60 kaydını yazdı; 2025 yerinde kaldı.

  5. 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 10 ile sorun: rerank kaç chunk puanladı, süre nasıl değişti?
    src/steps/7-rerank.ts içinde rerank’ı kapatıp soruyu tekrar sorun: süre ve cevap ne oldu? Sonra rerank’ı geri açın.

Günün sorusu için arama, rerank ve cevap; süreler her logun TIME satırında:

npm run step -- 6
$ 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
$ 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
$ 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
… aynı komut, son satır
TIME 678 ms → data/8-answer.json

Hepsini 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.

her doküman tek prompt'ta (retrieval yok)
$ 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:

src/cli/capture.ts
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}`);
});

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:

src/lib/ollama.ts
/**
* 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 (ikinci kez)
$ 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.json

Bu 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):

src/lib/store.ts
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>;
}
npm run ingest -- --edition 2026 (adım 4 ve 5)
━━ 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.json
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ı

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.
  1. 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.
  2. Aynı soruyu --k 10 ile çalıştırın. Rerank kaç chunk puanladı, süresi nasıl değişti? Cevap değişti mi?
  3. src/steps/7-rerank.ts içindeki TOGGLE rerank bloğunda true; // default satırını yoruma alın, false; // alternative satırını açın; soruyu tekrar sorun. Kazandığınız süreyi ve cevabı karşılaştırın, sonra toggle’ı geri alın.
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.

Süre ve maliyet aramada değil, modele giden her çağrıda ve her token’da.