Başlarken
Rehber: embedding'lerle RAG
Belgelerinizi parçalara bölüp embed ederek, arama, rerank ve kaynak göstererek yanıt veren bir RAG hattı kurun; Python ve Node kodu ile maliyet tahmini.
Retrieval-augmented generation (RAG), soruları kendi belgelerinizden yanıtlar. Belgeleri parçalara bölersiniz, her parçayı bir kez embed edersiniz; soru geldiğinde soruyu embed edip en yakın parçaları bulur, isteğe bağlı olarak yeniden sıralar ve en iyilerini, bunlara atıf yapma yönergesiyle bir sohbet modeline gönderirsiniz.
Aşağıdakilerin hepsi https://api14.com/api/v1 base URL'ini, sk-api14-... anahtarınızı ve resmi openai paketini kullanır. Maliyetler ön ödemeli USD bakiyenizden düşer ve her yanıtta usage.cost içinde döner.
import os
from openai import OpenAI
client = OpenAI(base_url="https://api14.com/api/v1", api_key=os.environ["API14_API_KEY"])
EMBED_MODEL = "openai/text-embedding-3-small"
CHAT_MODEL = "openai/gpt-5-mini"
1. Belgeleri parçalara bölün
Parçalar spesifik olacak kadar küçük, tek başlarına anlam taşıyacak kadar büyük olmalıdır. İyi bir başlangıç noktası, paragraf sınırlarında bölünmüş, biraz örtüşmeli 500 ila 1.000 karakterdir:
def chunk(text, size=800, overlap=150):
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
chunks, current = [], ""
for p in paragraphs:
if current and len(current) + len(p) > size:
chunks.append(current)
current = current[-overlap:] # carry context into the next chunk
current = f"{current}\n\n{p}".strip()
if current:
chunks.append(current)
return chunks
docs = {"handbook.md": open("handbook.md").read(), "faq.md": open("faq.md").read()}
chunks = [{"source": name, "text": c} for name, text in docs.items() for c in chunk(text)]
Her parçanın yanında kaynağını (dosya adı, URL, sayfa numarası) tutun; atıflar için gerekir.
2. POST /embeddings ile embed edin
İstek başına çok sayıda parça gönderin. 100 girdili tek istek, hız limitinize karşı tek istek sayılır:
def embed(texts):
vectors = []
for i in range(0, len(texts), 100):
r = client.embeddings.create(model=EMBED_MODEL, input=texts[i:i + 100])
vectors += [d.embedding for d in sorted(r.data, key=lambda d: d.index)]
return vectors
for c, v in zip(chunks, embed([c["text"] for c in chunks])):
c["vector"] = v
Vektörleri ve model kimliğini birlikte saklayın. Farklı modellerin vektörleri karşılaştırılamaz; bu yüzden EMBED_MODEL değerini değiştirirseniz veri kümesini yeniden embed etmelisiniz. Bkz. Embeddings.
3. Kosinüs benzerliğiyle arayın
Birkaç bin parça için bellekte doğrusal tarama yeterince hızlıdır. Ötesinde, vektörleri bir vektör veritabanına veya pgvector olan bir Postgres tablosuna koyun.
import math
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))
def search(question, k=20):
q = embed([question])[0]
return sorted(chunks, key=lambda c: cosine(q, c["vector"]), reverse=True)[:k]
4. POST /rerank ile yeniden sıralayın
Embedding araması geri çağırmada iyidir ama sıralamada kabadır. Bir reranker, soruyu ve her adayı birlikte okur ve alaka düzeyini daha hassas puanlar. 20 ila 100 aday getirin, yeniden sıralayın ve en iyi 3 ila 8 tanesini tutun. OpenAI SDK'sında rerank metodu yoktur; bu yüzden HTTP üzerinden çağırın (httpx, openai ile birlikte kurulur):
import httpx
def rerank(question, candidates, top_n=5):
r = httpx.post(
"https://api14.com/api/v1/rerank",
headers={"Authorization": f"Bearer {os.environ['API14_API_KEY']}"},
json={
"model": "cohere/rerank-v3.5",
"query": question,
"documents": [c["text"] for c in candidates],
"top_n": top_n,
},
timeout=30,
)
r.raise_for_status()
return [candidates[x["index"]] for x in r.json()["results"]]
index, gönderdiğiniz documents dizisindeki konumu gösterir. Bkz. Rerank.
5. Atıflarla yanıtlayın
Prompt'ta parçaları numaralandırın ve modelden bu numaralara atıf yapmasını isteyin. Bağlam yanıtı içermiyorsa bunu söylemesini belirtin:
def answer(question):
context = rerank(question, search(question))
sources = "\n\n".join(f"[{i + 1}] ({c['source']})\n{c['text']}" for i, c in enumerate(context))
r = client.chat.completions.create(
model=CHAT_MODEL,
messages=[
{"role": "system", "content": (
"Answer only from the numbered sources. Cite them like [1] or [2][3] after each claim. "
"If the sources do not contain the answer, say you don't know."
)},
{"role": "user", "content": f"Sources:\n\n{sources}\n\nQuestion: {question}"},
],
)
return r.choices[0].message.content, context, r.usage.cost
Uygulamanız atıfları veri olarak istiyorsa, köşeli parantezleri ayrıştırmak yerine { "answer": string, "citations": integer[] } gibi bir şemayla Structured outputs kullanın.
Aynı hat Node'da
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api14.com/api/v1", apiKey: process.env.API14_API_KEY });
const embed = async (input) =>
(await client.embeddings.create({ model: "openai/text-embedding-3-small", input })).data.map((d) => d.embedding);
const cosine = (a, b) => {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }
return dot / Math.sqrt(na * nb);
};
async function answer(question, chunks /* [{ source, text, vector }] */) {
const [q] = await embed([question]);
const candidates = chunks.map((c) => ({ ...c, score: cosine(q, c.vector) }))
.sort((a, b) => b.score - a.score).slice(0, 20);
const ranked = await client.post("/rerank", {
body: { model: "cohere/rerank-v3.5", query: question, documents: candidates.map((c) => c.text), top_n: 5 },
});
const context = ranked.results.map((r) => candidates[r.index]);
const sources = context.map((c, i) => `[${i + 1}] (${c.source})\n${c.text}`).join("\n\n");
const res = await client.chat.completions.create({
model: "openai/gpt-5-mini",
messages: [
{ role: "system", content: "Answer only from the numbered sources and cite them like [1]. If they do not contain the answer, say so." },
{ role: "user", content: `Sources:\n\n${sources}\n\nQuestion: ${question}` },
],
});
return res.choices[0].message.content;
}
Maliyet tahmini
Bir RAG sisteminin üç maliyet kalemi vardır; hepsi USD cinsindendir ve hepsi usage.cost içinde bildirilir:
| Adım | Ne zaman | Faturalama birimi |
|---|---|---|
| Veri kümesini embed etme | Bir kez, belgeler değiştiğinde yeniden | Girdi token'ı |
| Soruyu embed etme + rerank | Her soruda | Girdi token'ı / arama |
| Sohbet yanıtı | Her soruda | Girdi ve çıktı token'ları |
Çalıştırmadan önce tahmin etmek için modelin fiyatlarını GET /models/{id} çıktısından alın (pricing.prompt ve pricing.completion token başına USD'dir) ve İngilizce metin için token başına yaklaşık 4 karakter varsayın:
import httpx
def price(model_id):
return httpx.get(f"https://api14.com/api/v1/models/{model_id}").json()["pricing"]
corpus_tokens = sum(len(c["text"]) for c in chunks) / 4
print("One-off embedding:", corpus_tokens * float(price(EMBED_MODEL)["prompt"]), "USD")
p = price(CHAT_MODEL)
prompt_tokens, answer_tokens = 5 * 800 / 4 + 200, 300 # 5 chunks + instructions, short answer
print("Per answer:", prompt_tokens * float(p["prompt"]) + answer_tokens * float(p["completion"]), "USD")
Sohbet çağrısı genellikle en büyük kalemdir. Düşürmek için daha az ve daha kısa parça gönderin (reranking bunu güvenli kılar), prompt caching uygulanabilsin diye sistem prompt'unu başta sabit tutun ve max_tokens ayarlayın. Gerçek maliyetleri sonradan usage.cost veya Generation sorgulama ile kontrol edin.