Cara Bina Pipeline RAG Sendiri di VPS
Panduan lengkap membina sistem RAG pada VPS menggunakan PostgreSQL dan pgvector. Pelajari teknik chunking, embedding tempatan dengan Ollama, serta konfigurasi HNSW.
Rupa bentuk pipeline RAG yang dihoskan sendiri
Pipeline RAG (retrieval augmented generation) mempunyai lima peringkat: memecahkan dokumen kepada cebisan (chunk), menukar cebisan kepada embedding, menyimpan vektor, mendapatkan semula cebisan yang paling relevan untuk sesuatu soalan, dan menghantar cebisan tersebut kepada model bahasa yang akan menulis jawapan. Pada VPS yang anda sewa, empat peringkat pertama dijalankan pada pelayan tersebut. PostgreSQL dengan sambungan pgvector menyimpan vektor, dan model embedding kecil yang dihoskan oleh Ollama menukarkan teks kepada vektor. Hanya peringkat terakhir yang perlu dihantar keluar dari pelayan.
Pembahagian ini adalah hujah utama panduan ini. Pemecahan dokumen (chunking) hanyalah tugas CPU biasa. Embedding ialah model dengan 137 juta parameter yang hanya menggunakan beberapa ratus megabait RAM. Storan pula ialah jadual Postgres yang saiznya boleh dikira dengan matematik sebelum anda menulis satu baris data pun. Bagi korpus yang mengandungi ratusan ribu cebisan, kesemuanya boleh dijalankan pada VPS biasa. Penjanaan jawapan (generation) adalah berbeza, kerana ia melibatkan kos bagi setiap soalan, untuk selama-lamanya.
Bahagian mana dalam pipeline RAG yang sebenarnya menelan kos
Tutorial RAG hujung-ke-hujung DigitalOcean (https://www.digitalocean.com/community/tutorials/end-to-end-rag-pipeline) menggunakan pangkalan data vektor terurus dan model embedding yang dihoskan, manakala bahagian kosnya bersifat kualitatif: cache pertanyaan berulang, pastikan bilangan chunk yang diambil adalah kecil, dan lakukan rerank sebelum penjanaan. Nasihat tersebut adalah tepat. Ia juga terlepas pilihan yang mengubah pengiraan kos, iaitu menjalankan model embedding pada pelayan yang anda sudah bayar.
Kira token dan bukannya dolar, kerana kiraan token tidak menjadi lapuk apabila senarai harga berubah. Ambil korpus sebanyak 100,000 chunk dengan 400 token setiap satu, 10,000 soalan yang ditanya kepadanya, 8 chunk dihantar ke model bagi setiap jawapan, blok soalan dan arahan sebanyak 100 token, serta jawapan sebanyak 400 token.
The data behind this chart
[
{
"label": "Embed the corpus (once)",
"tokens_millions": 40,
"tokens_per_question": "4,000"
},
{
"label": "Embed each question",
"tokens_millions": 0.2,
"tokens_per_question": "20"
},
{
"label": "Generation input",
"tokens_millions": 33,
"tokens_per_question": "3,300"
},
{
"label": "Generation output",
"tokens_millions": 4,
"tokens_per_question": "400"
}
]Melakukan embedding bagi keseluruhan korpus adalah sebanyak 40 juta token, dan ia hanya berlaku sekali. Jika dibahagikan dengan 10,000 soalan tersebut, ia menjadi 4,000 token bagi setiap soalan. Jika anda bertanya seratus ribu soalan, ia turun kepada 400. Penjanaan tidak pernah turun. Ia menelan kos 3,300 token masuk dan 400 token keluar bagi setiap soalan yang anda jawab.
Oleh itu, kos tertumpu pada peringkat yang berulang. Miliki langkah embedding, kerana anda membayarnya sekali sahaja dan VPS anda tetap berjalan. Beli langkah penjanaan, kerana di situlah model yang lebih baik memberikan nilai sebenar. Caching penting atas sebab yang sama: cache hit melangkau satu-satunya peringkat yang kosnya tidak pernah terhapus (amortised). Perbezaan antara KV cache dan prompt cache menentukan bahagian mana yang boleh anda guna semula, dan prompt RAG mempunyai blok arahan yang stabil diikuti oleh blok chunk yang berubah-ubah, iaitu bentuk yang paling mendapat manfaat.
Chunking: mengapa saiz tetap dengan pertindihan adalah lalai yang tepat
Chunk ialah unit yang anda peroleh, jadi saiznya menentukan segala-galanya di peringkat hiliran. Ia mestilah cukup kecil supaya embedding-nya hanya mengenai satu perkara, kerana embedding ialah satu titik tunggal dalam ruang, jadi chunk yang merangkumi empat topik akan berada di antara topik-topik tersebut dan tidak dekat dengan mana-mana daripadanya. Ia mestilah cukup besar untuk menjawab soalan dengan sendirinya, kerana model bahasa melihat chunk tersebut dan bukan dokumen di sekelilingnya.
Mulakan pada 300 perkataan dengan 50 perkataan pertindihan. Bahasa Inggeris berjalan pada kira-kira 1.3 token setiap perkataan, jadi 300 perkataan adalah kira-kira 400 token. Pertindihan wujud kerana ayat yang jatuh pada sempadan akan terbelah dua, dan kedua-dua bahagian tidak akan menjawab soalan tersebut.
Pecahkan mengikut struktur terlebih dahulu di mana dokumen mempunyai struktur. Pecahkan pada tajuk, kemudian pada perenggan, dan gunakan peraturan saiz tetap hanya di dalam bahagian yang masih terlalu panjang. Chunk yang bermula di tengah-tengah ayat akan dibaca dengan teruk dalam jawapan akhir, kerana model akan memetik semula apa yang anda berikan kepadanya.
Jangan laraskan chunking sebelum anda boleh mengukurnya. Saiz tetap dengan pertindihan adalah deterministik dan murah untuk dijalankan semula, yang menjadikannya garis dasar yang boleh anda atasi. Bina pertanyaan pemarkahan di peringkat lebih bawah dahulu, kemudian ubah satu perkara pada satu masa.
Penyematan pada pelayan yang sama, serta kos RAM dan kependaman
curl -fsSL https://ollama.com/install.sh | sh
ollama pull nomic-embed-textnomic-embed-text mempunyai 137 juta parameter dan saiz muat turun 274 MB setakat Ogos 2026. Semak output yang dikembalikan sebelum anda mereka bentuk jadual untuknya.
curl -s http://127.0.0.1:11434/api/embed \
-d '{"model": "nomic-embed-text", "input": "search_document: hello"}' |
python3 -c 'import json,sys; print(len(json.load(sys.stdin)["embeddings"][0]))'Perintah tersebut mencetak 768. Jenis lajur anda mestilah sepadan dengan nombor tersebut dengan tepat.
Dua tetapan pada model ini sering memerangkap pengguna.
Awalan tugasan (task prefix) adalah wajib. Kad model Nomic menyatakan bahawa input "mesti menyertakan awalan arahan tugasan". Dokumen disematkan dengan search_document: di hadapan, manakala soalan dengan search_query: . Jika anda meninggalkannya, tiada ralat akan berlaku: anda tetap mendapat vektor, namun kualiti carian akan merosot dan tiada log yang akan memberitahu puncanya.
Input yang panjang dipotong secara senyap. Endpoint /api/embed menerima medan truncate dengan nilai lalai true, dan model yang dibungkus oleh Ollama mengiklankan konteks 2K. Ketulan (chunk) yang lebih panjang daripada itu akan dipotong pada had tersebut dan tetap disematkan, menyebabkan bahagian akhirnya tidak boleh dicari. Hantar "truncate": false semasa anda membuat ujian, supaya ketulan yang terlalu besar akan menyebabkan kegagalan dan bukannya diteruskan secara senyap.
Lakukan permintaan secara berkelompok (batch) dan pastikan model kekal dalam memori.
curl -s http://127.0.0.1:11434/api/embed -d '{
"model": "nomic-embed-text",
"input": ["search_document: first chunk", "search_document: second chunk"],
"keep_alive": "30m"
}' > /dev/nullinput menerima senarai, dan satu permintaan yang membawa 32 ketulan adalah lebih baik daripada 32 permintaan berasingan, kerana pusingan HTTP dan carian model hanya berlaku sekali dan bukannya 32 kali. keep_alive mengawal tempoh model kekal dalam memori selepas sesuatu permintaan, dengan nilai lalai selama 5 minit. Apabila tempoh ini tamat, permintaan seterusnya akan menanggung masa pemuatan semula.
Ukur dua angka yang penting pada pelayan anda sendiri. Nilai ini bergantung pada jumlah vCPU anda, jadi tiada angka yang diterbitkan akan tepat dengan situasi anda.
ollama ps
time curl -s http://127.0.0.1:11434/api/embed \
-d '{"model":"nomic-embed-text","input":"search_document: ... one real chunk ..."}' > /dev/nullollama ps mencetak saiz model yang dimuatkan dalam memori, iaitu RAM yang telah anda peruntukkan selagi keep_alive mengekalkannya. Output time dibahagikan dengan saiz kelompok ialah masa dalam saat bagi setiap ketulan. Darabkan dengan jumlah ketulan untuk mendapatkan kos pengindeksan sekali sahaja. Pada pelan CPU sahaja, jangkakan korpus 100,000 ketulan mengambil masa berjam-jam dan bukannya beberapa minit. Ini tidak mengapa, kerana ia hanya berlaku sekali dan boleh dijalankan sepanjang malam di bawah nice -n 19. Jika tempoh berjam-jam tidak dapat diterima, persoalan sebenar ialah sama ada menyewa GPU berbaloi dari segi kos, dan itu adalah pengiraan titik pulang modal berbanding token API dan bukannya sekadar pilihan peribadi.
Jika pelayan tersebut sudah menjalankan model sembang, model penyematan akan menjadi model kedua yang menetap dalam memori dan penggunaan RAM akan bertambah. Menjalankan Ollama pada VPS merangkumi penentuan saiz untuk bahagian penjanaan, dan prestasi model yang dihoskan sendiri di bawah pengguna serentak merangkumi apa yang berlaku apabila beberapa orang bertanya pada masa yang sama. Model penyematan cukup kecil untuk diletakkan bersama mana-mana model tersebut.
Skrip pengindeksan, dari mula hingga akhir
Pada Ubuntu 24.04, pip install biasa di luar persekitaran maya akan terhenti dengan error: externally-managed-environment, kerana Python sistem adalah milik apt.
python3 -m venv ~/rag
~/rag/bin/pip install "psycopg[binary]" pgvectorimport json, urllib.request
import psycopg
from pgvector.psycopg import register_vector
from pgvector import Vector
OLLAMA = "http://127.0.0.1:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(texts, prefix="search_document: "):
payload = {"model": MODEL,
"input": [prefix + t for t in texts],
"truncate": False,
"keep_alive": "30m"}
req = urllib.request.Request(OLLAMA, data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
return json.load(resp)["embeddings"]
def split(text, size=300, overlap=50):
words = text.split()
step = size - overlap
return [" ".join(words[i:i + size]) for i in range(0, len(words), step)]
with psycopg.connect("dbname=rag user=rag") as conn:
register_vector(conn)
for doc_id, text in documents(): # your loader
pieces = split(text)
for start in range(0, len(pieces), 32):
batch = pieces[start:start + 32]
vectors = embed(batch)
with conn.cursor() as cur:
cur.executemany(
"INSERT INTO chunks (doc_id, seq, body, embedding)"
" VALUES (%s, %s, %s, %s)",
[(doc_id, start + i, body, Vector(vec))
for i, (body, vec) in enumerate(zip(batch, vectors))])
conn.commit()documents() adalah milik anda: apa sahaja yang melayari fail atau baris anda dan menghasilkan id dokumen serta teksnya. Selebihnya adalah saluran paip (pipeline) tersebut.
Storan: skema pgvector dan saiznya
Ubuntu 24.04 membekalkan postgresql-16-pgvector pada versi 0.6.0, iaitu lebih lama daripada jenis halfvec. Gunakan repositori rasmi projek PostgreSQL untuk binaan terkini.
sudo apt update && sudo apt install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh
sudo apt install -y postgresql-17 postgresql-17-pgvectorNombor dalam nama pakej mestilah sepadan dengan versi utama pelayan anda. Kemudian, cipta role, pangkalan data dan extension tersebut.
sudo -u postgres createuser --pwprompt rag
sudo -u postgres createdb --owner rag rag
sudo -u postgres psql -d rag -c 'CREATE EXTENSION vector;'CREATE TABLE chunks (
id bigserial PRIMARY KEY,
doc_id text NOT NULL,
seq int NOT NULL,
body text NOT NULL,
embedding vector(768) NOT NULL,
fts tsvector GENERATED ALWAYS AS (to_tsvector('english', body)) STORED
);
CREATE INDEX chunks_fts ON chunks USING gin (fts);vector(768) mestilah sepadan dengan output model. Masukkan vektor 1024 dimensi ke dalam lajur tersebut dan Postgres akan menolaknya dengan expected 768 dimensions, not 1024, yang merupakan mesej ralat paling jelas dalam keseluruhan talian paip ini. Lajur fts yang dijana tidak memerlukan kos penyelenggaraan dan membolehkan carian kata kunci dilakukan kemudian.
Storan adalah berdasarkan aritmetik. pgvector mendokumenkan vector sebagai 4 * dimensions + 8 bait dan halfvec sebagai 2 * dimensions + 8. Kiraan dimensi di bawah adalah saiz output yang diterbitkan bagi setiap model.
The data behind this chart
[
{
"label": "384 (all-minilm)",
"bytes_per_vector": "1,544",
"vector_mib_per_100k": 147,
"halfvec_mib_per_100k": 74
},
{
"label": "768 (nomic-embed-text)",
"bytes_per_vector": "3,080",
"vector_mib_per_100k": 294,
"halfvec_mib_per_100k": 147
},
{
"label": "1024 (mxbai-embed-large)",
"bytes_per_vector": "4,104",
"vector_mib_per_100k": 391,
"halfvec_mib_per_100k": 196
},
{
"label": "1536 (hosted API model)",
"bytes_per_vector": "6,152",
"vector_mib_per_100k": 587,
"halfvec_mib_per_100k": 294
}
]Pada 768 dimensi, setiap vektor adalah 3,080 bait, jadi 100,000 ketulan (chunks) menyimpan 294 MiB data vektor. Korpus yang sama yang dibenamkan oleh model hos 1536 dimensi memerlukan 587 MiB, dan indeks di atasnya berkembang secara berkadar. Ketepatan separuh (half precision) mengurangkan kedua-duanya kepada separuh: halfvec(768) menyimpan korpus tersebut dalam 147 MiB. Sama ada ia menjejaskan recall adalah soalan yang dijawab oleh pertanyaan pemarkahan di bawah dalam satu larian.
Angka-angka tersebut hanya merangkumi lajur vektor sahaja. Teks, overhead baris dan indeks adalah tambahan, jadi ukur jadual sebenar anda.
SELECT pg_size_pretty(pg_total_relation_size('chunks')) AS total,
pg_size_pretty(pg_relation_size('chunks')) AS heap,
count(*) AS n_rows
FROM chunks;Jika anda lebih suka extension yang sama dengan API dan akaun pengguna di sekelilingnya, stack Supabase yang dihoskan sendiri adalah Postgres dengan pgvector yang telah pun didayakan, dan setiap pertanyaan dalam panduan ini berfungsi di sana tanpa perubahan.
Pengindeksan: tetapan HNSW yang penting
Bagi data yang kurang daripada beberapa ribu baris, jangan gunakan indeks. Carian tepat membaca setiap baris, ia cukup pantas pada saiz tersebut, dan tahap ingatannya (recall) adalah sempurna. Tambahkan indeks apabila imbasan berjujukan (sequential scan) tidak lagi cukup pantas, dan fahami pertukarannya: indeks anggaran akan mengembalikan jiran yang hampir tepat.
SET maintenance_work_mem = '2GB';
SET max_parallel_maintenance_workers = 3;
CREATE INDEX chunks_embedding ON chunks
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);m = 16 dan ef_construction = 64 ialah tetapan lalai bagi pgvector. Meningkatkannya akan menambah baik tahap ingatan tetapi memerlukan masa binaan dan saiz indeks yang lebih besar. Gunakan vector_cosine_ops dengan operator <=> melainkan anda tahu model anda mengeluarkan vektor panjang unit, kerana jarak kosinus (cosine distance) mengabaikan panjang vektor manakala hasil darab dalam (inner product) tidak.
Pantau proses binaan. Apabila graf melebihi maintenance_work_mem, pgvector akan memaklumkannya:
NOTICE: hnsw graph no longer fits into maintenance_work_mem after 100000 tuples
DETAIL: Building will take significantly more time.Itu bukan ralat dan proses binaan tetap selesai, tetapi ia akan beralih ke laluan yang jauh lebih perlahan. Tingkatkan maintenance_work_mem dalam sesi yang membina indeks tersebut dan biarkan tetapan lalai pelayan tidak berubah, kerana tetapan itu adalah untuk setiap operasi penyelenggaraan dan nilai global yang tinggi boleh menyebabkan pelayan kehabisan memori. Pantau binaan yang panjang daripada sesi kedua.
SELECT phase, round(100.0 * blocks_done / nullif(blocks_total, 0), 1) AS "%"
FROM pg_stat_progress_create_index;Kemudian bandingkan indeks yang telah siap dengan memori yang ada pada pelayan.
SELECT pg_size_pretty(pg_relation_size('chunks_embedding'));
SHOW shared_buffers;Carian HNSW menyusuri graf, jadi ia menyentuh halaman yang bertaburan di seluruh indeks dan bukannya membaca julat. Indeks yang tidak dimuatkan ke dalam memori akan menukarkan setiap pertanyaan kepada bacaan cakera, dan kelambatan inilah yang akan disedari oleh pengguna. Itulah satu-satunya peraturan saiz untuk pelayan: indeks ditambah dengan baris yang anda sajikan sebenarnya harus dimuatkan ke dalam RAM. free -m dan saiz di atas adalah dua nombor yang perlu dibandingkan.
Pada masa pertanyaan, hnsw.ef_search ialah pelaras untuk tahap ingatan dan nilai lalainya ialah 40.
BEGIN;
SET LOCAL hnsw.ef_search = 100;
SELECT id, body FROM chunks ORDER BY embedding <=> $1 LIMIT 8;
COMMIT;Nilai yang lebih tinggi akan mencari lebih banyak bahagian graf, menemui jiran yang lebih baik, dan meningkatkan latensi. Ia merupakan tetapan sesi, jadi anda boleh meningkatkannya untuk satu pertanyaan tanpa mengubah indeks.
Jika sesuatu pertanyaan tidak menggunakan indeks langsung, pelan (plan) akan menunjukkannya.
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM chunks ORDER BY embedding <=> $1 LIMIT 8;Imbasan berjujukan di sini sering kali berpunca daripada storan. Vektor 768 dimensi ialah 3,080 bait, iaitu lebih besar daripada apa yang disimpan secara inline oleh Postgres, jadi nilai tersebut dipindahkan ke jadual TOAST (storan luar talian untuk nilai yang terlalu besar). Nota daripada pgvector sendiri menyatakan bahawa perancang (planner) tidak mengira storan luar talian dalam anggaran kosnya, yang boleh menyebabkan imbasan bersiri kelihatan lebih murah daripada yang sebenarnya. ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; mengekalkan vektor secara inline. Ia terpakai pada baris yang ditulis selepas perubahan dibuat, jadi baris sedia ada memerlukan penulisan semula jadual.
Pengambilan: satu pertanyaan, dua isyarat
Carian vektor mencari teks yang mempunyai maksud yang sama dengan soalan. Ia lemah dalam mencari rentetan tepat: nombor bahagian, kod ralat, atau nama keluarga. Carian kata kunci adalah sebaliknya, dan Postgres sudah pun menyokongnya. Gabungkan kedua-duanya dalam satu pertanyaan daripada menjalankan sistem kedua.
Reciprocal rank fusion ialah penggabung paling ringkas yang berkesan. Setiap hasil mengambil 1 / (60 + rank) daripada setiap senarai yang ia muncul, dan kedua-dua skor tersebut ditambah. Ia tidak memerlukan penormalan skor kerana ia membaca kedudukan dan bukannya jarak.
WITH semantic AS (
SELECT id, row_number() OVER (ORDER BY distance) AS rank
FROM (SELECT id, embedding <=> $1 AS distance
FROM chunks ORDER BY embedding <=> $1 LIMIT 40) s
),
keyword AS (
SELECT id, row_number() OVER (ORDER BY score DESC) AS rank
FROM (SELECT c.id, ts_rank_cd(c.fts, q) AS score
FROM chunks c, websearch_to_tsquery('english', $2) q
WHERE c.fts @@ q
ORDER BY score DESC LIMIT 40) k
)
SELECT c.id, c.body,
coalesce(1.0 / (60 + s.rank), 0) + coalesce(1.0 / (60 + k.rank), 0) AS rrf
FROM (SELECT id FROM semantic UNION SELECT id FROM keyword) u
JOIN chunks c ON c.id = u.id
LEFT JOIN semantic s ON s.id = u.id
LEFT JOIN keyword k ON k.id = u.id
ORDER BY rrf DESC
LIMIT 8;$1 ialah embedding soalan daripada model yang sama, dibina dengan awalan search_query: . $2 ialah soalan dalam bentuk teks. Kedua-duanya diikat daripada aplikasi anda. websearch_to_tsquery menerima soalan pengguna sebenar tanpa tergendala oleh tanda baca, yang mana to_tsquery tidak mampu melakukannya. Satu lagi perkara yang perlu diketahui: menambah penapis WHERE di atas imbasan HNSW boleh menghasilkan baris yang lebih sedikit daripada yang diminta, kerana indeks dicari terlebih dahulu dan penapis dijalankan selepasnya. SET hnsw.iterative_scan = relaxed_order; memastikan pgvector terus mengimbas sehingga ia mempunyai baris yang mencukupi.
Bagaimanakah anda menentukan sama ada perolehan (retrieval) itu berkualiti?
Ini adalah langkah yang hampir semua panduan RAG abaikan, dan ia merupakan satu-satunya langkah yang memberitahu anda sama ada pilihan lain yang dibuat benar-benar membantu. Ia tidak memerlukan kerangka kerja penilaian. Ia hanya memerlukan 30 soalan dan ID bagi chunk yang menjawab setiap soalan tersebut.
Tulis soalan-soalan tersebut secara manual. Ambil soalan yang benar-benar ditanya oleh pengguna mengenai korpus ini, jalankan setiap satu, baca hasil yang diperoleh, dan rekodkan ID bagi chunk yang sepatutnya terpilih. Tiga puluh soalan tidak akan menyelesaikan perbezaan kecil. Namun, ia akan mengesan perbezaan yang penting, kerana perbezaan tersebut biasanya ketara.
CREATE TABLE gold (
id bigserial PRIMARY KEY,
question text NOT NULL,
chunk_id bigint NOT NULL REFERENCES chunks(id),
embedding vector(768) NOT NULL
);Embed setiap soalan dengan awalan search_query: , simpan, kemudian skor keseluruhan set tersebut dalam satu kuiri.
WITH hits AS (
SELECT g.id,
min(r.rank) FILTER (WHERE r.id = g.chunk_id) AS hit_rank
FROM gold g
CROSS JOIN LATERAL (
SELECT top.id, row_number() OVER (ORDER BY top.distance) AS rank
FROM (SELECT c.id, c.embedding <=> g.embedding AS distance
FROM chunks c
ORDER BY c.embedding <=> g.embedding
LIMIT 10) top
) r
GROUP BY g.id
)
SELECT count(*) AS questions,
count(hit_rank) AS found_in_top_10,
round(avg(coalesce(1.0 / hit_rank, 0)), 3) AS mrr
FROM hits;found_in_top_10 dibahagikan dengan questions ialah recall at 10: kekerapan jawapan berada di dalam tetingkap yang anda hantar kepada model. MRR (mean reciprocal rank) mengira purata 1 dibahagikan dengan kedudukan chunk yang betul dan mengira kegagalan sebagai sifar, jadi ia memberi ganjaran kepada kedudukan jawapan di tempat pertama berbanding tempat kelapan. Kedua-dua nombor ini berubah apabila anda menukar saiz chunk, menukar model embedding, atau menambah carian kata kunci, dan kini anda boleh melihat ke arah mana ia berubah.
Utamakan recall at 10 melebihi segala-galanya, kerana penjana (generator) tidak boleh menggunakan chunk yang tidak pernah diterimanya. Apabila recall at 10 adalah 0.9 dan jawapan masih salah, puncanya terletak pada prompt atau model, bukan pada perolehan. Pemisahan itu menjimatkan masa berhari-hari daripada meneka-neka.
Periksa indeks secara berasingan. Carian anggaran (approximate search) mengorbankan recall, dan pgvector menunjukkan kepada anda sejauh mana: jalankan kuiri yang sama dengan carian tepat (exact search) dan bandingkan ID-ID tersebut.
BEGIN;
SET LOCAL enable_indexscan = off; -- use exact search
SELECT id FROM chunks ORDER BY embedding <=> $1 LIMIT 10;
COMMIT;Sembilan daripada sepuluh ID yang sama bermakna ef_search adalah memadai. Empat daripada sepuluh bermakna anda perlu meningkatkannya.
Reranking dan penjanaan: di mana API menjana keuntungan
Reranker ialah jenis model yang berbeza. Ia membaca soalan dan satu cebisan (chunk) secara serentak lalu memberikan skor kepada pasangan tersebut, yang lebih berkesan berbanding membandingkan dua embedding yang dikira secara bebas, dan ia terlalu perlahan untuk dijalankan ke atas keseluruhan korpus. Itulah sebabnya ia diletakkan di sini. Ia melihat 40 calon yang dikembalikan oleh proses retrieval, bukan 100,000 cebisan dalam jadual, jadi API reranking berhos mengenakan caj untuk 40 pasangan pendek bagi setiap soalan dan membuang false positive yang paling teruk sebelum ia sampai ke peringkat yang mahal.
Penjanaan ialah bil yang berulang, dan dua tuas mempengaruhinya. Hantar lebih sedikit cebisan, gunakan recall pada 10 untuk mengetahui berapa sedikit yang boleh anda hantar tanpa kehilangan jawapan. Pastikan bahagian hadapan prompt stabil bait demi bait supaya prompt cache pembekal boleh mencapainya, dan letakkan cebisan yang diperoleh selepas bahagian stabil tersebut. Cache jawapan yang telah siap mengikut soalan juga, kerana token yang paling murah untuk dijana ialah token yang anda jana pada minggu lepas.
Menentukan saiz pelayan, dan bila kapasiti ini tidak lagi mencukupi
Setiap peraturan penentuan saiz di sini adalah sesuatu yang anda ukur, bukannya anggarkan.
- RAM ialah kekangan utama: saiz model residen daripada
ollama ps, ditambah saiz indeks HNSW, ditambahshared_buffers, dengan ruang tambahan yang ditinggalkan untuk sambungan dan cache halaman. - Cakera memerlukan dua kali ganda
pg_total_relation_size('chunks'), kerana pembinaan semula indeks menyimpan kedua-dua salinan pada masa yang sama. - CPU menentukan masa pengindeksan semula, berdasarkan saat per chunk yang anda ukur didarab dengan jumlah chunk.
- Pengindeksan semula berlaku lebih kerap daripada yang anda jangka, kerana menukar model embedding akan membatalkan setiap vektor yang telah disimpan.
Reka bentuk ini tidak lagi mencukupi pada tahap yang boleh anda jangkakan. Apabila indeks HNSW tidak lagi muat dalam RAM yang mampu anda beli, kependaman pertanyaan akan bertukar menjadi carian cakera dan tiada tetapan yang dapat memulihkannya. Apabila satu jadual melayani ramai penyewa dan setiap pertanyaan menapis mengikut penyewa, pemetakan (partitioning) jadual menjadi penyelesaiannya, dan itu merupakan kerja yang sebenar. Apabila penulisan indeks dan pertanyaan pengguna bersaing pada pelayan yang sama, pindahkan pekerja embedding ke pelayan kedua sebelum anda memindahkan pangkalan data. Selagi perkara tersebut belum berlaku, Postgres dengan pgvector pada VPS yang anda sewa sekarang adalah jawapan untuk pengeluaran, dan angka di atas memberitahu anda sejauh mana had tersebut berada.
FAQ
Bolehkah saya menjalankan pipeline RAG pada satu VPS, atau adakah saya memerlukan pangkalan data vektor?
Satu VPS sudah memadai untuk korpus yang mengandungi ratusan ribu cebisan (chunks). Pada 768 dimensi, 100,000 cebisan adalah 294 MiB data vektor ditambah dengan teks dan indeks HNSW, yang muat dalam RAM pelan biasa. Hadnya adalah memori dan bukannya kiraan baris, kerana carian HNSW melompat di sekitar indeks, jadi kependaman (latency) akan merosot sebaik sahaja indeks tidak lagi muat dalam RAM. Bandingkan pg_relation_size pada indeks dengan free -m dan anda akan mengetahui kedudukan anda.
Adakah saya memerlukan GPU untuk melakukan embedding dokumen saya?
Tidak jika anda melakukan embedding sekali sahaja dan membuat pertanyaan selepas itu. Model 137 juta parameter seperti nomic-embed-text boleh dijalankan pada CPU, dan satu laluan penuh ke atas korpus yang besar mengambil masa beberapa jam yang boleh anda lakukan pada waktu malam. GPU mula menjadi penting apabila dokumen tiba secara berterusan, atau apabila anda ingin menjalankan penjanaan pada mesin yang sama. Ukur masa satu kelompok (batch) terhadap /api/embed pada pelayan anda sendiri dan darabkan dengan kiraan cebisan anda, kerana kiraan vCPU terlalu berbeza untuk angka yang diterbitkan dapat membantu.
Mengapa pertanyaan vektor saya menggunakan imbasan berjujukan (sequential scan) dan bukannya indeks HNSW?
Baca pelan tersebut dengan EXPLAIN (ANALYZE, BUFFERS). Punca biasa ialah storan: pgvector menyatakan bahawa perancang (planner) tidak mengira storan luar talian (out of line storage) dalam anggaran kosnya, yang menjadikan imbasan bersiri kelihatan lebih murah daripada yang sebenarnya, dan vektor 768 dimensi adalah 3,080 bait jadi ia disimpan dalam jadual TOAST secara lalai. ALTER TABLE chunks ALTER COLUMN embedding SET STORAGE PLAIN; mengekalkan baris baharu secara dalam talian (inline). Dua punca lain ialah operator yang tidak sepadan dengan indeks, memandangkan indeks yang dibina dengan vector_cosine_ops hanya digunakan oleh <=>, dan pertanyaan tanpa ORDER BY ... LIMIT, memandangkan indeks anggaran hanya berfungsi untuk pertanyaan jiran terdekat yang tertib (ordered nearest neighbour).
Bagaimanakah saya tahu sama ada pengambilan (retrieval) saya berkualiti?
Bina set emas (gold set) yang mengandungi 30 soalan, setiap satunya dipasangkan dengan id cebisan yang menjawabnya, dan simpan embedding soalan tersebut bersama-samanya. Kemudian ukur recall pada 10, iaitu kekerapan cebisan yang betul muncul dalam 10 teratas, dan MRR, yang memberi ganjaran jika ia diletakkan di kedudukan pertama. Kedua-dua angka itulah yang memberitahu anda sama ada perubahan pada saiz cebisan, model embedding atau rank fusion membantu. Tanpanya, anda hanya menukar tetapan dan mempercayai tanggapan anda terhadap segelintir jawapan sahaja.