How to Build a RAG Pipeline with YouTube Transcripts
A working RAG pipeline over YouTube video transcripts: collect transcripts across a video list, chunk them, embed and store the vectors, retrieve the relevant chunks, and generate a cited answer - full code for every step, plus how to keep the index fresh.
00:09:00 · SEP 27, 2026
Trả lời nhanh
Thu thập bản chép lời
Lấy một khóa từ trang quản trị (100 tín dụng miễn phí, không cần thẻ) và lấy bản chép lời của từng video theo id. Tín dụng được tính theo mỗi request, không theo phút video, nên một bài giảng dài 3 giờ chỉ tốn 1 tín dụng như một clip 2 phút - điều này quan trọng khi bạn thu thập cả kho video của một kênh:
async function fetchTranscript(videoId: string) {
const res = await fetch(
`https://getyoutubetranscript.com/api/v1/transcript?v=${videoId}`,
{ headers: { Authorization: `Bearer ${process.env.GETYOUTUBETRANSCRIPT_API_KEY}` } }
);
const json = await res.json();
if (!json.success) throw new Error(json.message);
return json.data as { video_id: string; title: string; transcript: string; word_count: number };
}
const videoIds = ['dQw4w9WgXcQ', 'jNQXAC9IVRw' /* ... */];
const videos = await Promise.all(videoIds.map(fetchTranscript));video_id và title trả về cùng với văn bản chép lời - đó là thứ bước 5 dùng để trích dẫn câu trả lời.
Chia bản chép lời thành các phần
Chia mỗi bản chép lời thành các đoạn có chồng lấn trước khi embed - embed cả video là quá thô để truy xuất một câu trả lời cụ thể, và không cần thư viện nào cho việc này:
function chunkText(text: string, size = 1200, overlap = 200) {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + size, text.length);
chunks.push(text.slice(start, end));
start += size - overlap;
}
return chunks;
}
const records = videos.flatMap((video) =>
chunkText(video.transcript).map((text, chunkIndex) => ({
videoId: video.video_id,
title: video.title,
chunkIndex,
text,
}))
);Đoạn 1.200 ký tự với 200 ký tự chồng lấn là mặc định hợp lý cho bản chép lời dạng lời nói - giảm với clip ngắn, súc tích, tăng với nội dung dạng bài giảng dài nơi cần nhiều ngữ cảnh xung quanh hơn.
Embed và lưu trữ
Embed từng phần và giữ video id cùng title bên cạnh vector, vì đó là thứ biến một phần được truy xuất thành một trích dẫn sau này:
import OpenAI from 'openai';
const openai = new OpenAI();
async function embed(text: string) {
const res = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text });
return res.data[0].embedding;
}
const index = await Promise.all(
records.map(async (r) => ({ ...r, embedding: await embed(r.text) }))
);Ví dụ này giữ vector trong bộ nhớ để dễ hiểu. Khi vượt quá vài trăm phần, hãy thay mảng này bằng pgvector, Pinecone, hoặc một vector store khác - bước truy xuất bên dưới chỉ cần một hàm trả về top-k vector gần nhất, không cần đúng cấu trúc này.
Truy xuất các phần liên quan
Tại thời điểm truy vấn, embed câu hỏi bằng cùng model và xếp hạng các phần đã lưu theo cosine similarity:
function cosineSimilarity(a: number[], b: number[]) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function retrieve(query: string, k = 5) {
const queryEmbedding = await embed(query);
return index
.map((r) => ({ ...r, score: cosineSimilarity(r.embedding, queryEmbedding) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}k=5 là điểm khởi đầu hợp lý - quá ít phần thì model không đủ ngữ cảnh để trả lời đầy đủ, quá nhiều thì các phần không liên quan bắt đầu làm loãng câu trả lời.
Tạo câu trả lời có căn cứ
Đưa các phần đã truy xuất cho model dưới dạng ngữ cảnh đánh số và yêu cầu chỉ trả lời dựa trên đó cùng với trích dẫn nguồn - đây chính là điều giữ cho câu trả lời có thể truy vết về một video thật thay vì kiến thức chung của model:
async function answer(query: string) {
const matches = await retrieve(query);
const context = matches
.map((m, i) => `[${i + 1}] From "${m.title}":\n${m.text}`)
.join('\n\n');
const completion = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',
content:
"Answer only using the numbered transcript excerpts below. Cite sources as [1], [2], etc. " +
"If the excerpts don't contain the answer, say so.",
},
{ role: 'user', content: `${context}\n\nQuestion: ${query}` },
],
});
return completion.choices[0].message.content;
}Trích dẫn theo tiêu đề video, thay vì bịa ra một mốc thời gian, khớp với những gì API chép lời thực sự trả về - đừng để model bịa ra một mốc thời gian mà nó chưa từng được cung cấp.
Giữ cho chỉ mục luôn cập nhật
Với một kênh vẫn đang đăng video mới, hãy kiểm tra video mới theo lịch bằng endpoint miễn phí /channel/latest (0 tín dụng), bỏ qua các video id đã lập chỉ mục, và chỉ lấy, chia nhỏ, embed những video mới. Chi phí thu thập nhờ đó tỷ lệ với nội dung mới thay vì lập chỉ mục lại toàn bộ mỗi lần chạy.
Vì sao bản chép lời phù hợp với RAG
Video là một trong những định dạng khó xây dựng truy xuất nhất - bạn không thể grep một video. Bản chép lời biến nó trở lại thành văn bản mà stack embedding và truy xuất hiện có của bạn đã biết cách xử lý, mà không cần tự chạy một job speech-to-text. Và vì một API tính phí theo request không tính phí theo độ dài video, việc thu thập một kho nội dung dài (podcast, bài giảng, livestream nhiều giờ) không tự nhiên đắt hơn việc thu thập các clip ngắn - đúng loại kho lưu trữ mà một pipeline RAG thường được xây dựng để tìm kiếm.
Tài liệu tham khảo endpoint đầy đủ, xác thực, và giới hạn tốc độ có trong tài liệu API. Lấy một khóa với 100 tín dụng miễn phí từ trang quản trị để thử nghiệm toàn bộ quy trình.
Câu hỏi thường gặp về pipeline RAG
Tôi có cần một vector database thật sự để làm theo hướng dẫn này không?
Không - mảng trong bộ nhớ trong hướng dẫn này hoạt động tốt tới vài trăm phần và là cách nhanh nhất để kiểm chứng toàn bộ pipeline hoạt động. Chuyển sang pgvector hoặc một vector store được quản lý khi bạn lập chỉ mục nhiều hơn thế, hoặc cần chỉ mục tồn tại qua các lần khởi động lại.
Tôi nên dùng model embedding nào?
text-embedding-3-small là mặc định hợp lý - rẻ và đủ tốt cho hầu hết việc truy xuất bản chép lời. Chỉ chuyển sang text-embedding-3-large nếu bạn gặp vấn đề chất lượng truy xuất mà nó có khả năng khắc phục; nó tốn phí hơn mỗi lần gọi.
Điều gì xảy ra nếu một video hoàn toàn không có bản chép lời?
Endpoint chép lời trả về một mã lỗi rõ ràng (TRANSCRIPT_DISABLED hoặc TRANSCRIPT_NOT_FOUND) thay vì một chuỗi rỗng - hãy kiểm tra success: false và bỏ qua video đó thay vì embed một chuỗi rỗng như thể đó là nội dung thật.
Lập chỉ mục khoảng 200 video tốn bao nhiêu?
200 tín dụng cho các lần lấy bản chép lời, bất kể đó là clip 2 phút hay video 2 giờ - 100 tín dụng miễn phí khi đăng ký đã trang trải khoảng nửa kho 200 video đó. Chi phí embedding tách biệt và do nhà cung cấp embedding của bạn tính phí.
Tôi có thể trích dẫn một mốc thời gian chính xác thay vì chỉ tiêu đề video không?
Không thể với endpoint này - API chép lời công khai trả về văn bản thuần, không phải các đoạn có mốc thời gian, nên các ví dụ ở đây trích dẫn theo tiêu đề và id video. Nếu trích dẫn theo mốc thời gian quan trọng với trường hợp của bạn, hãy giữ các phần nhỏ hơn để trích dẫn theo video vẫn chính xác nhất có thể.
Liên quan
- YouTube API Quota Exceeded: Causes and Fixes
- YouTube Transcript API Rate Limit: What It Is and How to Handle 429s
- YouTube Transcript MCP Server: Setup Guide for Claude and Other AI Tools
- YouTube Transcripts in n8n: HTTP Request Workflow Guide
- Migrating from Supadata: A Field Guide
- How to Get YouTube Transcripts in Python
- Migrating from TranscriptAPI.com: A Field Guide
- Build an AI YouTube Video Summarizer with LangChain and Next.js
- The Best YouTube Transcript APIs in 2026
- GetYouTubeTranscript so với TranscriptAPI
- GetYouTubeTranscript so với youtubetotranscript.com
- GetYouTubeTranscript so với youtube-transcript.io
- GetYouTubeTranscript vs NoteGPT