Kom igång
Guide: RAG med embeddings
Dela upp dokument i delar, skapa embeddings, sök med cosinuslikhet, rerank och svara med källhänvisningar, med kod för Python och Node.
Retrieval-augmented generation (RAG) besvarar frågor utifrån dina egna dokument. Du delar upp dokumenten i delar, bäddar in varje del en gång, och vid frågetillfället bäddar du in frågan, hittar de närmaste delarna, rankar eventuellt om dem och skickar de bästa till en chattmodell med instruktionen att hänvisa till dem.
Allt nedan använder base URL https://api14.com/api/v1, din sk-api14-...-nyckel och det officiella paketet openai. Kostnader tas från ditt förbetalda USD-saldo och returneras i usage.cost i varje svar.
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. Dela upp dokumenten
Delarna ska vara tillräckligt små för att vara specifika och tillräckligt stora för att ge mening på egen hand. Ett bra utgångsläge är 500–1 000 tecken med viss överlappning, uppdelat vid styckegränser:
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:] # för med kontext till nästa del
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)]
Behåll källan (filnamn, URL, sidnummer) för varje del, du behöver den för källhänvisningar.
2. Skapa embeddings med POST /embeddings
Skicka många delar per anrop. Ett anrop med 100 indata räknas som ett anrop mot din hastighetsgräns:
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
Spara vektorerna tillsammans med modell-id:t. Vektorer från olika modeller går inte att jämföra, så om du ändrar EMBED_MODEL måste du bädda in materialet på nytt. Se Embeddings.
3. Sök med cosinuslikhet
För några tusen delar räcker en linjär genomsökning i minnet gott och väl. Därutöver, lägg vektorerna i en vektordatabas eller en Postgres-tabell med pgvector.
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. Rerank med POST /rerank
Embeddingsökning är bra på täckning men grov på ordning. En reranker läser frågan och varje kandidat tillsammans och poängsätter relevansen mer exakt. Hämta 20–100 kandidater, rerank och behåll de 3–8 bästa. OpenAI SDK har ingen rerank-metod, så anropa den över HTTP (httpx installeras tillsammans med openai):
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 pekar in i documents-arrayen som du skickade. Se Rerank.
5. Svara med källhänvisningar
Numrera delarna i prompten och be modellen hänvisa till de numren. Säg åt den att säga ifrån när kontexten inte innehåller svaret:
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
Om din app behöver källhänvisningarna som data, använd Strukturerade utdata med ett schema som { "answer": string, "citations": integer[] } i stället för att tolka hakparenteser.
Samma pipeline i Node
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;
}
Uppskatta kostnaden
Ett RAG-system har tre kostnadsposter, alla i USD och alla rapporterade i usage.cost:
| Steg | När | Faktureras per |
|---|---|---|
| Embeddings för materialet | En gång, och igen när dokument ändras | Indata-token |
| Embedding av frågan + rerank | Varje fråga | Indata-token / sökning |
| Chattsvar | Varje fråga | Indata- och utdata-tokens |
För att uppskatta innan du kör, ta modellens priser från GET /models/{id} (pricing.prompt och pricing.completion är USD per token) och anta ungefär 4 tecken per token för engelsk text:
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 delar + instruktioner, kort svar
print("Per answer:", prompt_tokens * float(p["prompt"]) + answer_tokens * float(p["completion"]), "USD")
Chattanropet är oftast den största posten. För att sänka den, skicka färre och kortare delar (rerank gör det säkert), håll systemprompten fast i början så att prompt-cachning kan tillämpas, och ange max_tokens. Kontrollera verkliga kostnader efteråt med usage.cost eller Generationsuppslag.