Kami menggunakan cookie. Cookie dan penyimpanan browser menjaga Anda tetap masuk dan mengingat preferensi Anda. Dengan izin Anda, kami juga mengukur cara situs digunakan. Anda dapat kapan saja. Lihat kebijakan cookie kami.

Panduan: RAG dengan embeddings

Memulai

Panduan: RAG dengan embeddings

Pecah dokumen menjadi chunk, embed, cari dengan cosine similarity, rerank, dan jawab dengan sitasi memakai api14 di Python dan Node.

Retrieval-augmented generation (RAG) menjawab pertanyaan dari dokumen Anda sendiri. Anda memecah dokumen menjadi chunk, meng-embed setiap chunk satu kali, dan saat ada pertanyaan Anda meng-embed pertanyaan itu, mencari chunk terdekat, secara opsional me-rerank-nya, lalu mengirim yang terbaik ke model chat dengan instruksi untuk mengutipnya.

Semua yang ada di bawah memakai base URL https://api14.com/api/v1, key sk-api14-... Anda, dan paket openai resmi. Biaya diambil dari saldo USD prabayar Anda dan dikembalikan di usage.cost pada setiap respons.

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. Memecah dokumen menjadi chunk

Chunk harus cukup kecil agar spesifik dan cukup besar agar masuk akal berdiri sendiri. Titik awal yang baik adalah 500–1.000 karakter dengan sedikit overlap, dipecah pada batas paragraf:

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)]

Simpan sumber (nama file, URL, nomor halaman) bersama setiap chunk, Anda memerlukannya untuk sitasi.

2. Embed dengan POST /embeddings

Kirim banyak chunk per request. Satu request dengan 100 input dihitung sebagai satu request terhadap batas laju Anda:

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

Simpan vektor dan ID model bersamaan. Vektor dari model yang berbeda tidak dapat dibandingkan, jadi jika Anda mengubah EMBED_MODEL Anda harus meng-embed ulang korpus. Lihat Embeddings.

3. Mencari dengan cosine similarity

Untuk beberapa ribu chunk, pemindaian linear di memori sudah cukup cepat. Di luar itu, taruh vektor di database vektor atau tabel Postgres dengan 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 dengan POST /rerank

Pencarian embedding bagus dalam recall tetapi kasar dalam pengurutan. Reranker membaca pertanyaan dan setiap kandidat bersamaan dan menilai relevansi dengan lebih presisi. Ambil 20–100 kandidat, rerank, dan simpan 3–8 teratas. OpenAI SDK tidak memiliki method rerank, jadi panggil lewat HTTP (httpx terpasang bersama 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 menunjuk ke array documents yang Anda kirim. Lihat Rerank.

5. Menjawab dengan sitasi

Beri nomor pada chunk di prompt dan minta model mengutip nomor tersebut. Suruh ia mengatakannya ketika konteks tidak memuat jawabannya:

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

Jika aplikasi Anda memerlukan sitasi sebagai data, gunakan Structured outputs dengan skema seperti { "answer": string, "citations": integer[] } alih-alih mem-parse tanda kurung siku.

Pipeline yang sama di 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;
}

Memperkirakan biaya

Sistem RAG memiliki tiga pos biaya, semuanya dalam USD dan semuanya dilaporkan di usage.cost:

LangkahKapanDitagih per
Meng-embed korpusSekali, dan lagi saat dokumen berubahToken input
Meng-embed pertanyaan + rerankSetiap pertanyaanToken input / pencarian
Jawaban chatSetiap pertanyaanToken input dan output

Untuk memperkirakan sebelum menjalankan, ambil harga model dari GET /models/{id} (pricing.prompt dan pricing.completion adalah USD per token) dan asumsikan sekitar 4 karakter per token untuk teks bahasa Inggris:

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")

Panggilan chat biasanya pos terbesar. Untuk menurunkannya, kirim chunk yang lebih sedikit dan lebih pendek (reranking membuat ini aman), jaga system prompt tetap di awal agar prompt caching dapat berlaku, dan atur max_tokens. Periksa biaya sebenarnya setelahnya dengan usage.cost atau Pencarian generasi.