Vi använder cookies. Cookies och webbläsarlagring håller dig inloggad och kommer ihåg dina preferenser. Med ditt tillstånd mäter vi också hur webbplatsen används. Du kan när som helst. Läs vår cookiepolicy.

Guide: RAG med embeddings

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:

StegNärFaktureras per
Embeddings för materialetEn gång, och igen när dokument ändrasIndata-token
Embedding av frågan + rerankVarje frågaIndata-token / sökning
ChattsvarVarje frågaIndata- 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.