用 AI SDK 从 0 实现一个 PDF RAG 知识库(demo)
在做 AI 知识库 / ChatPDF / 企业文档问答 时,最核心的技术就是 RAG(Retrieval Augmented Generation) 。
本文通过一个 完整可运行的 Node.js 示例,一步一步实现:
- 解析 PDF
- 文本切分
- 生成向量(Embedding)
- 向量检索
- 使用 AI SDK 进行回答
最终实现一个 简单版 ChatPDF。
一、最终实现效果
流程如下:
PDF
↓
解析文本
↓
chunk切分
↓
embedding
↓
vector store
↓
用户提问
↓
向量检索
↓
LLM回答
用户提问:
Node.js 的 event loop 是什么?
系统会:
1️⃣ 从 PDF 中找到最相关段落
2️⃣ 将段落提供给 LLM
3️⃣ 生成答案
二、创建项目
创建项目目录:
rag-demo
初始化 Node 项目:
npm init -y
安装依赖:
npm install ai @ai-sdk/openai pdfjs-dist
依赖说明:
| 依赖 | 作用 |
|---|---|
| ai | AI SDK |
| @ai-sdk/openai | LLM Provider |
| pdfjs-dist | 解析 PDF |
三、项目结构
项目结构如下:
rag-demo
│
│─ test.pdf
├─ ingest.js
├─ embedding.js
├─ rag.js
├─ vectorStore.json
└─ package.json
说明:
| 文件 | 作用 |
|---|---|
| ingest.js | 文档入库 |
| embedding.js | embedding API |
| rag.js | 问答 |
| vectorStore.json | 向量数据 |
四、解析 PDF
首先我们需要把 PDF 转成文本。
创建文件:
ingest.js
代码:
import fs from "fs";
import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
async function extractTextFromPDF(path) {
const data = new Uint8Array(fs.readFileSync(path));
const pdf = await pdfjs.getDocument({ data }).promise;
let text = "";
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const pageText = content.items.map(item => item.str).join(" ");
text += pageText + "\n";
}
return text;
}
const text = await extractTextFromPDF("./test.pdf");
console.log(text);
运行:
node ingest.js
输出:
PDF完整文本
五、文本切分(Chunk)
在 RAG 中,不能把整篇文档做 embedding。
原因:
整篇文档 → 一个向量
检索效果会很差。
正确方式是:
文档
↓
chunk1
chunk2
chunk3
添加 chunk 函数:
function chunkText(text, size = 500) {
const chunks = [];
for (let i = 0; i < text.length; i += size) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
示例:
chunk1: Node.js 简介
chunk2: Node.js Event Loop
chunk3: Node.js Stream
六、生成 Embedding
向量是 RAG 的核心。
这里要用到embeddings模型,推荐 ai.gitee.com/serverless-…
示例
const openai = createOpenAI({
apiKey: 'your key',
baseURL: "https://ai.gitee.com/v1"
});
for (const chunk of chunks) {
const embedding = await embed({
model: openai.embedding("Qwen3-Embedding-8B"),
value: chunk
});
vectorStore.push({
text: chunk,
embedding: embedding.embedding
});
}
}
七、文档入库(Embedding Pipeline)
修改 ingest.js:
import fs from "fs";
import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs";
import { embed } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
async function extractTextFromPDF(path) {
const data = new Uint8Array(fs.readFileSync(path));
const pdf = await pdfjs.getDocument({ data }).promise;
let text = "";
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
const pageText = content.items.map(item => item.str).join(" ");
text += pageText + "\n";
}
return text;
}
function chunkText(text, size = 500) {
const chunks = [];
for (let i = 0; i < text.length; i += size) {
chunks.push(text.slice(i, i + size));
}
return chunks;
}
const text = await extractTextFromPDF("./test.pdf");
const chunks = chunkText(text);
const vectorStore = [];
const openai = createOpenAI({
apiKey: 'your key',
baseURL: "https://ai.gitee.com/v1"
});
for (const chunk of chunks) {
const embedding = await embed({
model: openai.embedding("Qwen3-Embedding-8B"),
value: chunk
});
vectorStore.push({
text: chunk,
embedding: embedding.embedding
});
}
fs.writeFileSync("vectorStore.json", JSON.stringify(vectorStore, null, 2));
console.log("Embedding完成");
运行:
node ingest.js
生成:
vectorStore.json
数据示例:
{
"text": "Node.js 是一个基于 V8 的 JavaScript runtime",
"embedding": [0.123, -0.234, ...]
}
八、实现向量检索
创建文件:
rag.js
首先实现余弦相似度:
function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
然后实现搜索:
async function search(query) {
const embedding = await embed({
model: openai.embedding("Qwen3-Embedding-8B"),
value: query
});
const queryVector = embedding.embedding;
const scored = vectorStore.map(doc => ({
text: doc.text,
score: cosineSimilarity(queryVector, doc.embedding)
}));
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, 3);
}
九、结合 AI SDK 生成回答
完整 rag.js:
import fs from "fs";
import { embed, streamText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
const vectorStore = JSON.parse(fs.readFileSync("./vectorStore.json"));
const openai = createOpenAI({
apiKey: 'your key',
baseURL: "https://ai.gitee.com/v1"
});
function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function search(query) {
const embedding = await embed({
model: openai.embedding("Qwen3-Embedding-8B"),
value: query
});
const queryVector = embedding.embedding;
const scored = vectorStore.map(doc => ({
text: doc.text,
score: cosineSimilarity(queryVector, doc.embedding)
}));
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, 3);
}
async function ask(question) {
const docs = await search(question);
const context = docs.map(d => d.text).join("\n");
const result = await streamText({
model: openai.chat("Qwen3-8B"),
system: `Answer using this context:\n${context}`,
prompt: question
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
}
ask("这个PDF主要讲什么?");
十、运行 RAG
先执行:
node ingest.js
生成向量库。
再执行:
node rag.js
提问:
这个 PDF 讲什么?
系统会:
问题
↓
embedding
↓
vector search
↓
Top3 文档
↓
LLM
↓
回答
十一、完整架构总结
最终系统流程:
PDF
↓
pdfjs 解析
↓
chunk
↓
embedding
↓
vector store
↓
query embedding
↓
vector search
↓
top K context
↓
LLM回答
总结
本文实现了一个 完整的 PDF RAG 系统:
- PDF 解析
- 文档切分
- Embedding
- 向量检索
- AI SDK 问答
理解这一套流程,就掌握了 AI 知识库系统的核心技术。