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:
| Langkah | Kapan | Ditagih per |
|---|---|---|
| Meng-embed korpus | Sekali, dan lagi saat dokumen berubah | Token input |
| Meng-embed pertanyaan + rerank | Setiap pertanyaan | Token input / pencarian |
| Jawaban chat | Setiap pertanyaan | Token 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.