Практычная архітэктура RAG на Firebase і Vertex AI з вектарамі ў Firestore
RAG на Firebase з эмбедынгамі Vertex AI і вектарным пошукам Firestore: пакетная апрацоўка, фільтры па метаданых, ізаляцыя кліентаў, абгрунтаваныя промпты, кэш і метрыкі.
Крыніцы: How to Implement RAG (Retrieval-Augmented Generation) in Your Web Application
Метод Retrieval-Augmented Generation стаў популярным падходам для прыкладнасцей ШЫ, якія працуюць з прыватной, зміннаючайся або спецыялізаванай інформацыяй. У працы замест таго, каб адзін велікі мовны мадэль запамятаваў усё з часоў навчання, RAG дазволяе модэлю праглядаць зовнішніе крыніцы інформацыі пад час адпаведных запытанняў. Гэта дае можлівасць ствараць асістэнтов для корпаратыўных дакументаў, дакументаў кліянтаў, інформацыі пра продукты, історычных дадзеных аб внутрашняях процэсах.
RAG — гэта не проста векторная база дадзеных, падключаная да велікага мовнага модэля. Продакшн-гатовая рэалізацыя аб’еднвае апрацоўку дакументаў, стварэнне вектарных прэдставленняў, пошук па векторах, фільтрацыю метададзеных, складанне кантэксту, праектаванне запытанняў, генераванне тэксту, кэшаванне, карэспанцію з бядамі і монітарынг. Ніжчэй паказана практычная архітэктура на базе Firebase і Vertex AI, а таксама шаблоны рэалізацыі, якія маюць значэнне, калі пратотып пераходзіць у стадыю продакшну.
Тры слоі
Сістэма RAG складаецца з трох шароў: хранілніцы ведамаў, шара адзысквання і шара генеравання. Выхідныя дакументы або даныя прыложэння ператвараюцца на эмбедынгі і зберагаюцца разам з ўсім первісным тэкстам і метаданымі. Калі корыстнік ставіць запыт, ён таксама ператвараецца на эмбедынг, і пошук за сэмантычным супаданнем знаходзіць найбліжэйшыя фрагменты інфармацыі. Гэтыя фрагменты стаюць кантэкстам, які перэдаецца мовнаму модэлю, які піша адпаведны адказ на адзысканую інфармацыю.
Ключовая ідэя заключаецца у тым, што модэлю ніколі не трэба запамятаваць усю базу ведамаў. Адзысквання інфармацыі адбываецца занова для кожнага запыту, таму базовыя даны можаць змяніцца без падтрэнавання модэля.
User Question
|
v
Generate Query Embedding
|
v
Metadata Filtering
|
v
Vector Similarity Search
|
v
Top-K Relevant Documents
|
v
Context Construction
|
v
LLM / Gemini
|
v
Grounded Response + Sources
Такая структура таксама спрыяе тым, каб лёгкая была адміністрацыя. Стратэгія адзыскання можа змяніцца, калі мадэль генеравання застаецца незменнай, або можа быць заменена LLM без падзялу базы знанняў.
Эмбедынгі
Эмбедынгі ўтрамбовваюць сэмантычнае адзыскання. У працоўцы з тэкстам замест таго, каб спрыяць яму як з ключоўымі словамі, мадэль эмбедынга ператварае яго ў чысловы вектар, які фіксуе адносы на рэвэле значэнняў. У прыкладнай рэалізацыі вікорыстоўваецца gemini-embedding-001 з Vertex AI, пры чым для дакументаў і запытанняў выкарыстоўваюцца разныя тыпы задач, адтуды што збераганыя дакументы і прыходзячыя запытанні выпалююць разныя ролі.
const { PredictionServiceClient, helpers } =
require('@google-cloud/aiplatform');
const PROJECT_ID = process.env.PROJECT_ID;
const client = new PredictionServiceClient({
apiEndpoint: 'aiplatform.googleapis.com'
});
async function generateEmbedding(
text,
taskType = 'RETRIEVAL_DOCUMENT'
) {
const endpoint =
`projects/${PROJECT_ID}/locations/global/` +
`publishers/google/models/gemini-embedding-001`;
const instance = {
content: text,
task_type: taskType
};
const request = {
endpoint,
instances: [helpers.toValue(instance)]
};
const [response] = await client.predict(request);
return response.predictions[0].embeddings.values;
}
Індэксаванне базы знання выклікае выкарыстоўванне RETRIEVAL_DOCUMENT, а апрацоўка запытку корыстувальніка — RETRIEVAL_QUERY. Важна паставацься да гэтага адносова адносова, таму што якосьць аднаходжэння інфармацыі значна залежыць ад таго, як прэдставлены як RETRIEVAL_DOCUMENT, так і RETRIEVAL_QUERY.
Групаванне і паралельнае выкананне
У ранніх версіях коду RAG кожны дакумент часта адбываецца по аднаму. Гэта падходзіць для маленькага пратотыпа, але ёсць марнатрацыёю рэсурсаў, калі трэба апрацуе тыячы або мільйоны фрагментаў. Крашчэ групаваць тэксты ў пакеты, за межамі колькасці запыткаў і токенав.
У базовым кодзе колькасць токенав ацэнюецца проста, і встановліваецца максімум як для колькасці фрагментаў, так і для ацэнёванай колькасці токенав за запытком. Кроме таго, выкарыстоўваецца обмежанае паралельнае выкананне, таму калькі пакетаў можа выканавацца паралельна без перавантажэння API.
const EMBEDDING_CONFIG = {
maxSegmentsPerRequest: 100,
maxTokensPerRequest: 18000,
concurrency: 3,
tokenEstimateDivisor: 3
};
function estimateTokens(text) {
return Math.ceil(
text.length / EMBEDDING_CONFIG.tokenEstimateDivisor
);
}
function packIntoBatches(texts) {
const batches = [];
let currentBatch = [];
let currentTokens = 0;
for (const text of texts) {
const tokens = estimateTokens(text);
const exceedsCount =
currentBatch.length >=
EMBEDDING_CONFIG.maxSegmentsPerRequest;
const exceedsTokens =
currentTokens + tokens >
EMBEDDING_CONFIG.maxTokensPerRequest;
if (exceedsCount || exceedsTokens) {
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
currentBatch = [text];
currentTokens = tokens;
} else {
currentBatch.push(text);
currentTokens += tokens;
}
}
if (currentBatch.length > 0) {
batches.push(currentBatch);
}
return batches;
}
Ліміты павінны быць заданыя ў налаштаваннях, а не вбудованыя як прыняттыя за звычай. У працэйнай асяроддзі яны павінны разлічвацца залежна ад супакоўкі, а перад налаштаванням канкурантнасці трэба сочыць за затрымкамі API, лімітамі частоты, практыкамі бягучых аперацый і працэйной спроможнасцю.
Зберагчы вектары разам з дадзеннямі прыкладнага програмы
Практычны варыянт — зберагчы вектары разам з дадзеннямі прыкладнага програмы, замест таго каб адразу ствараць яшчэ адну базу дадзеных. У прыкладным кодзе тэкст дакументаў, вектарныя прэдставы і метададзеныя зберагаюцца ў Firestore, а падобныя запісі аднаходзяцца за дапамогою пошуку вектараў па найбліжэйшым суседу.
const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();
async function storeDocumentWithEmbedding(
collectionPath,
docId,
text,
embedding,
metadata
) {
const docRef =
firestore.doc(`${collectionPath}/${docId}`);
await docRef.set({
text,
embedding,
...metadata,
createdAt: Firestore.FieldValue.serverTimestamp()
});
}
async function findSimilarDocuments(
collectionPath,
queryEmbedding,
limit = 5
) {
const collectionRef =
firestore.collection(collectionPath);
const vectorQuery = collectionRef.findNearest({
vectorField: 'embedding',
queryVector: queryEmbedding,
limit,
distanceMeasure: 'DOT_PRODUCT'
});
const snapshot = await vectorQuery.get();
return snapshot.docs.map(doc => ({
id: doc.id,
data: doc.data()
}));
}
Стратэгія адзыскання інфармацыі трэба выбіраць адказальна. Схожасць вектара паказвае, якія дакументы сэмантычна блізкія да запіту, але блізкасць у значэнні не завжды означае ўзаемную рэлевантнасць. Калі дапыт працюе, метаданы стаюць чырвоным колерам важлівымі.
Фільтрацыя і ізоляцыя корыстнікаў
Чысты пошук на адзысканне вектара рэдка калі ўстаёт у працэ. Уявіце базу знанняя, якая охопляе калькія кліянтаў, аддзелы, продукты чы рэгіоны. Запіт можа адпавядаць дакументам ад калькіх корыстнікаў, але дапыт павінен вярнуць толькі тое, што можа бачыць чынны корыстнік.
Эфектывнейшы падход — выкарыстоўваць структураваныя фільтры яшчэ до чым сэмантычны пошук, чым разам з яным. Код-прамер фільтруе за тэмамі яшчэ перад вектарным запытом, чым звяршае прастор пошуку і павышае рэлевантнасць.
async function retrieveContext(
queryText,
selectedTopics = [],
limit = 5
) {
const queryEmbedding =
await generateEmbedding(
queryText,
'RETRIEVAL_QUERY'
);
let collectionRef =
firestore.collection('knowledge_base');
if (selectedTopics.length > 0) {
collectionRef = collectionRef.where(
'topics',
'array-contains-any',
selectedTopics
);
}
const vectorQuery =
collectionRef.findNearest({
vectorField: 'embedding',
queryVector: queryEmbedding,
limit,
distanceMeasure: 'DOT_PRODUCT'
});
const snapshot = await vectorQuery.get();
return snapshot.docs.map(doc => {
const data = doc.data();
return {
text: data.text,
source: data.source,
metadata: data.metadata || {}
};
});
}
Гэта мае вялікое значэнне ў дапытках для калькуляцыі плата за арэнду. Працэс адзыскання дакументаў не павінен ператварыцца на нехтачны бар’ер безпекі, які абнародуе даны іншага кліента. Автарызацыя і ізоляцыя калькуляцыі плата за арэнду павінны выкананы окрэма ад крытэрыяў сэмантычнай супадзімасці.
Стварэнне запіту
Адзыскання дакументаў рашае толькі палову проблемы. Адзысканы тэкст таксама павінен дасягнуць LLM у такім фармаце, які бы разлічваў выхідны матэрыял ад рэальнага запіту корыстніка. У прыкладным кодзе ствараецца спецыяльны раздзіл кантэксту, у якім указваюцься дэталі выхіднага матэрыялу, і модэлю чыста сказваецца, каб ён адпавядаў на запыт на адной з паданых інформацый, а таксама каб ён паведаміў пра гэта, калі кантэкст не мае неабходных дадзеных.
async function generateRAGResponse(
userQuery,
contextDocuments
) {
const contextSection =
contextDocuments
.map((doc, index) => {
return `
### Reference ${index + 1}
${doc.text}
Source: ${doc.metadata?.source || 'Unknown'}
`;
})
.join('\n');
const prompt = `
You are an AI assistant with access
to a knowledge base.
Use the provided context to answer
the user's question accurately.
CONTEXT:
${contextSection}
USER QUESTION:
${userQuery}
INSTRUCTIONS:
1. Answer using the provided context.
2. If the context is insufficient, say so clearly.
3. Cite the references used.
4. Do not invent information.
ANSWER:
`;
const result =
await genAI.models.generateContent({
model: 'gemini-2.5-flash-lite',
contents: prompt
});
return result.text.trim();
}
Якасць адзыначэння і якасць запытку з’яўляюцца ў зв’язку. Нават сільныя дакументы можу даўаць паслабленыя адпаведзі, калі кантэкст паказваецца паслабна арганізаваным, дуплікаваным, занадта вялікім або без чыстых інструкцый.
Затрымкі, косты і абыекты
Затрымкі і косты трэба вучыць з самага пачатку. Генераванне за дапамою embed-модэляў — гэта зовнішнія запыткі API, і той самы запыткі можа павтарыцца часта, таму кэшаванне популярных embed-модэляў скарочывае непатрэбныя запыткі. Тымчасовыя абыекты і ліміты частоты запыткаў вымагаюць павторных спроб, а не негайнага абыекту всего запытка. У прыкладным кодзе паказаны кэш embed-модэляў у памяці, а таксама экспанэнцыйны метод адкладэння павторных запыткаў у разы лімітавання частоты.
const embeddingCache = new Map();
async function getCachedEmbedding(text, taskType) {
const cacheKey = `${taskType}:${text}`;
if (embeddingCache.has(cacheKey)) {
return embeddingCache.get(cacheKey);
}
const embedding =
await generateEmbedding(text, taskType);
embeddingCache.set(cacheKey, embedding);
return embedding;
}
async function retryWithBackoff(
fn,
maxRetries = 3
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (
error.code === 429 ||
error.message.includes('rate limit')
) {
const delay =
Math.pow(2, attempt) * 1000;
await new Promise(resolve =>
setTimeout(resolve, delay)
);
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
Калі запускаецца калькі ляўканняў, процэс-спецыфічны Map больш не падходзіць, і распадзелены кэш забезпечвае лепшую адпаведнасць між рэплікамі. Прынцып мае перавагу над тэхналогіяй: не трэба пераскладваць дорогія, дэтэрміністычныя вычысленні, калі рэзультат можна без апасцярыбы перыякораваць.
Калькі джэрэла інфармацыі
Дзеяныя запыткі выклікаюць патрэбу болей чым аднаго тыпу інфармацыі. Чыннікавацелю, каб добра адпавясціць, можа знадобіцца апісанне продукту, практычны прыклад і частка історыі. Складзенне всіх дакументаў у адну загальную колекцыю зменшае точнасць пошуку.
У падходзе на аднойчыну раздзеляюцца апісанні, прыклады і історычныя даны, кожны з яных запрашваецца незалежна, а рэзультаты з’едынаюцца ў структураваны об’ект кантэксту.
async function retrieveMultiContext(
queryText,
options = {}
) {
const {
includeDefinitions = true,
includeExamples = true,
includeHistorical = false,
topics = []
} = options;
const queryEmbedding =
await generateEmbedding(
queryText,
'RETRIEVAL_QUERY'
);
const contextPromises = [];
if (includeDefinitions) {
contextPromises.push(
findSimilarDocuments(
'definitions',
queryEmbedding,
3
).then(documents => ({
type: 'definitions',
documents
}))
);
}
if (includeExamples) {
contextPromises.push(
findSimilarDocuments(
'examples',
queryEmbedding,
5
).then(documents => ({
type: 'examples',
documents
}))
);
}
const contexts =
await Promise.all(contextPromises);
return contexts;
}
Такім чынам прыкладнэнне атрымлее кантроль. У змену таму, што прасіць адну аперацыю пошуку вектара абы ўсё зрабіць, яно выбірае, якія джэрэлы маюць значэнне для кожнага запіту, і спяўнае іх перад стварэнням рэспансу.
Візуабельнасць
Візуабельнасць — галоўная асоблівасць, якая адрóżняе дэмы ад прыкладнэнняў RAG у працэсе выработкі. Рэспанс ад чатбота не ёсць доказам таго, што працэс адзыскання інфармацыі праходзіць правільна. Командам патрэбны паказнікі па часу адзыскання, колькісці вярнуцых дакументаў за запітам, ўзаемнасу ўзаемнасу іх з запітам, выкарыстоўванню кэша і частоце абыякавасцей.
У калектывам коду ёсць клас RAGMetrics, які фіксуе колькасць запытанняў, час адпаведзення, точнасць адзыскання, парадакс кэша і абыякавасці, а таксама вырахоўвае точнасць і вярнаванне, калі існуюць актуальныя, рэлевантныя дакументы.
class RAGMetrics {
constructor() {
this.metrics = {
totalQueries: 0,
averageLatency: 0,
retrievalAccuracy: [],
errors: []
};
}
logQuery(
query,
contextCount,
latency,
sources
) {
this.metrics.totalQueries++;
const previousLatency =
this.metrics.averageLatency *
(this.metrics.totalQueries - 1);
this.metrics.averageLatency =
(previousLatency + latency) /
this.metrics.totalQueries;
console.log({
query: query.substring(0, 100),
contextCount,
latency,
sourceCount: sources.length,
timestamp: new Date().toISOString()
});
}
logRetrievalAccuracy(
retrievedDocs,
relevantDocs
) {
const retrievedIds =
new Set(retrievedDocs.map(d => d.id));
const relevantIds =
new Set(relevantDocs.map(d => d.id));
const intersection =
new Set(
[...retrievedIds]
.filter(id => relevantIds.has(id))
);
const precision =
intersection.size / retrievedIds.size;
const recall =
intersection.size / relevantIds.size;
this.metrics.retrievalAccuracy.push({
precision,
recall
});
}
}
Калі такія чыслы існуе, падбор RAG ператвараецца ў інжынерную задачу. Можна прыменяць розныя параметры, такія як размах фрагментавання дакументаў, параметр top-K, фільтры метаданых, мадэль утворэння эмбедынгаў, структура запіта і падбор рэзультатаў, пры чым па памерам можна выявіць, чы справаўна падбіраюцца дакументы і чы стаюць крашчымі адпаведныя адпаведзі.
Усё канцэрнаванне
Сістэма RAG у працэўным рэжыме — гэта канцэрнаванне, а не проста адзін вызов мадэлі. Дакументы чысцяцца і фрагментуюцца, эмбедынгі ствараюцца эфектыва, метаданыя зберагаюцца адносова, працэс падбору дакументаў выконваецца з урахоўваннем прав на доступ, а шар генеравання адпаведзяў отрымае чыстыя інструкцыі. Практычная структура выглядае так:
DOCUMENT INGESTION
|
v
Clean + Split Documents
|
v
Generate Embeddings
|
v
Firestore + Metadata Storage
|
|
USER QUERY --------+
|
v
Query Embedding
|
v
Authorization + Filters
|
v
Vector Similarity Search
|
v
Relevant Context
|
v
Prompt Construction
|
v
Gemini / LLM
|
v
Answer + Sources + Metrics
Нічыта тут не фіксуе дызайн на адзінаковы спосаб. Пазнейшыя дадзеныя, такія як гібрыдны пошук за ключавымі словамі і вектарамі, прыстроі пераранкавання, перапісва запыткаў, памяць для размов, наборы для ацэнкі, выходны тэкст у рэальны час, спяльны кэш і фонавая обработка дадзэнняў, можаць быць даданыя без змены базовай ідэі RAG.
Урокі
LLM — толькі адна з складовых. Дажы найсупэльны мадэль не можа компенсаваць паслабленую разбівку дадзэнняў на часткі, слабы пошук, некарэктныя метаданы, відсутнае перакананне ў правах на доступ чы несутыячны кантэкст. У практыцы якосць пошуку часта вялікая мера таго, чы выконваець канечная сістэма свае функцыі эфектыва чы ні.
Найкрашча працаваць паэтапна: пачніце з прыемкі дадзэнняў, стварэння эмбедынгаў, вектарнага пошуку і простага запытка, а пасля пераканаецеся ў рэзультатах перш чы дадаце большую складнасць. Потым дадаце фільтрацыю метаданых, кэшаванне, павторныя спробы, монітарынг, ацэнку і прыглыблены пошук, адпаведна да рэальных прычын спамоці.
RAG змінюе спосаб будавання прыкладнасцей на асабліцэйных інтэлектах, адколькі мадэль можа працаваць з ведамасцю, якая ніколі не была часткай яе навучальных дадзеных. Умяшчэнні, сэмантычны пошук, структураваныя метададзены і генераванне з урахоўваннем кантэкста разам можу ператворыць звычную прыкладнасць у асістэнта, які розумее свою сабеўную базу ведамасці.
Найлепшыя сістэмы RAG не завжды ўсё самыя складныя. Гэта тые, дзе пошук можна вымерыць, кантэкст ёсць рэлевантным, доступ ёсць безпечным, неудачы адмаўляюцца, а адпаведзі вядуць да корыстных джэрэл. Пачаць проста і праграмаваць удосконаленні на адпаведных показніках лепшая, чым дадаць складнасць раней, чым будзе зрозумелы рэальны проблема.
Супаўязаныя матэрыялы
- Пошук і адміністрацыя даных для RAG: чаму практыка выкарыстоўвання даных патрабуе аддзельнага этапу перыяркі — Методы адбівання сэмабільнасці выявляюць кандыдатаў, але не можуць адмацаваць ўзаемнае адносленне. Пераканаецеся ў двуэтапным падходзе з RRF, крос-кодэрарамі, ColBERT і алгорытмамі перыяркі LLM.
- Практычныя нотаткі: Чым ёсцца практыка выкарыстоўвання даных у генераванні (RAG)? — Паследовны апіс практычных нотатак: Чым ёсцца практыка выкарыстоўвання даных у генераванні (RAG)?: контракты, перакананні і шаблоны коду для команд, якія впрыменяюць гэты падход.