RAG 构建知识大脑:让 Agent 读得懂你的私有数据(实战:简历问答机器人)
别再让 AI 瞎编乱造了!用 RAG 技术把你的私有数据变成 AI 的「第二大脑」,30 分钟打造一个能精准回答简历问题的智能机器人
🎯 前言:为什么你需要 RAG?
想象一下这个场景:
你有一个智能招聘助手,HR 问它:
"这个候选人的技术栈是什么?有没有做过大型项目?"
没有 RAG 时:
AI:[瞎编一通]
"根据我的知识,这位候选人精通 Java、Python、Go,曾参与过阿里巴巴、腾讯等大厂项目..."
HR:❌ 等等,简历上根本没写这些!
有 RAG 时:
AI:[基于简历回答]
"根据简历,这位候选人的技术栈包括:
- 前端:Vue 3, React, TypeScript
- 后端:Node.js, Python
- 数据库:MySQL, MongoDB
曾参与的项目:
1. 电商后台管理系统(日活 10 万+)
2. AI 智能客服平台(QPS 5000+)
..."
HR:✅ 准确!
RAG 是什么?
RAG (Retrieval-Augmented Generation) = 检索增强生成
简单说:先检索私有数据,再生成答案
流程:
用户问题 → 检索相关文档 → 拼接上下文 → LLM 生成答案
(从你的数据) (给 AI 看) (精准回答)
核心价值:
- ✅ 基于真实数据回答,不瞎编
- ✅ 保护私有数据,不泄露给模型训练
- ✅ 实时更新,数据变了答案就变
- ✅ 可追溯,知道答案来自哪份文档
📚 一、RAG 核心概念(10 分钟速成)
1. RAG 工作原理
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户问题 │ ──→ │ 检索系统 │ ──→ │ 拼接上下文 │ ──→ │ LLM 生成 │
│ "候选人 │ │ (向量搜索) │ │ (Prompt) │ │ 最终答案 │
│ 经验?" │ │ │ │ │ │ │
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
↓
┌──────────────┐
│ 向量数据库 │
│ (简历数据) │
└──────────────┘
2. 核心组件
| 组件 | 作用 | 常用工具 |
|---|---|---|
| 文档加载器 | 读取各种格式文件 | LangChain Loaders |
| 文本分割器 | 将长文档切分成小块 | RecursiveTextSplitter |
| Embedding 模型 | 将文本转为向量 | OpenAI, Sentence-BERT |
| 向量数据库 | 存储和检索向量 | Chroma, Pinecone, FAISS |
| 检索器 | 根据问题找相关文档 | SimilaritySearch |
| LLM | 生成最终答案 | GPT-4, Claude, Llama |
3. RAG vs 传统方案
| 方案 | 数据准确性 | 数据隐私 | 更新成本 | 适用场景 |
|---|---|---|---|---|
| RAG | ✅ 基于真实数据 | ✅ 私有数据不泄露 | ✅ 实时更新 | 企业知识库、客服 |
| Fine-tuning | ⚠️ 可能幻觉 | ❌ 数据需训练 | ❌ 重新训练 | 特定领域模型 |
| 纯 LLM | ❌ 容易瞎编 | ❌ 数据需公开 | ❌ 无法更新 | 通用问答 |
🚀 二、环境准备(5 分钟)
1. 安装依赖
# 创建项目
mkdir rag-resume-bot
cd rag-resume-bot
npm init -y
# 安装核心库
npm install langchain @langchain/openai @langchain/chroma
npm install pdf-parse mammoth axios dotenv
npm install -D @types/node
2. 配置环境变量
创建 .env 文件:
OPENAI_API_KEY=sk-your-api-key-here
CHROMA_PATH=./chroma_db
3. 准备简历数据
创建 data/resumes/ 目录,放入简历文件(支持 PDF、DOCX、TXT):
data/
└── resumes/
├── 张三_前端工程师.pdf
├── 李四_全栈工程师.docx
└── 王五_数据科学家.txt
示例简历内容(张三):
姓名:张三
职位:高级前端工程师
工作年限:5 年
技术栈:
- 前端框架:Vue 3, React, Angular
- 语言:TypeScript, JavaScript, Python
- 工具:Webpack, Vite, Docker
- 数据库:MySQL, MongoDB, Redis
项目经验:
1. 电商后台管理系统(2022-2023)
- 技术栈:Vue 3 + TypeScript + Node.js
- 用户规模:日活 10 万+
- 核心功能:商品管理、订单处理、数据分析
- 性能优化:首屏加载从 3s 优化到 0.8s
2. AI 智能客服平台(2021-2022)
- 技术栈:React + Python + WebSocket
- QPS:5000+
- 核心功能:智能对话、工单系统、数据分析
教育背景:
- 北京大学,计算机科学与技术,本科(2015-2019)
技能证书:
- AWS 解决方案架构师认证
- Kubernetes 管理员认证
🎮 三、实战项目:简历问答机器人
项目结构
rag-resume-bot/
├── src/
│ ├── index.js # 入口文件
│ ├── config.js # 配置
│ ├── loader.js # 文档加载器
│ ├── rag.js # RAG 核心逻辑
│ └── utils.js # 工具函数
├── data/
│ └── resumes/ # 简历文件
├── chroma_db/ # 向量数据库(自动生成)
├── .env
└── package.json
1. 文档加载器
// src/loader.js
import fs from 'fs';
import path from 'path';
import pdf from 'pdf-parse';
import mammoth from 'mammoth';
import { DirectoryLoader } from 'langchain/document_loaders/fs/directory';
import { PDFLoader } from 'langchain/document_loaders/fs/pdf';
import { TextLoader } from 'langchain/document_loaders/fs/text';
import { DocxLoader } from 'langchain/document_loaders/fs/docx';
class ResumeLoader {
constructor(resumeDir) {
this.resumeDir = resumeDir;
}
// 加载所有简历
async loadAllResumes() {
const files = fs.readdirSync(this.resumeDir);
const documents = [];
for (const file of files) {
const filePath = path.join(this.resumeDir, file);
const ext = path.extname(file).toLowerCase();
let doc;
switch (ext) {
case '.pdf':
doc = await this.loadPDF(filePath);
break;
case '.docx':
doc = await this.loadDOCX(filePath);
break;
case '.txt':
doc = await this.loadTXT(filePath);
break;
default:
console.warn(`不支持的文件格式:${ext}`);
continue;
}
// 添加元数据(文件名、候选人姓名)
doc.metadata = {
source: file,
candidate: this.extractCandidateName(file)
};
documents.push(doc);
}
return documents;
}
// 加载 PDF
async loadPDF(filePath) {
const data = await pdf(fs.readFileSync(filePath));
return {
pageContent: data.text,
metadata: { source: filePath }
};
}
// 加载 DOCX
async loadDOCX(filePath) {
const result = await mammoth.extractRawText({
path: filePath
});
return {
pageContent: result.value,
metadata: { source: filePath }
};
}
// 加载 TXT
async loadTXT(filePath) {
const text = fs.readFileSync(filePath, 'utf-8');
return {
pageContent: text,
metadata: { source: filePath }
};
}
// 从文件名提取候选人姓名
extractCandidateName(filename) {
const name = filename.replace(/_[^_]+$/, '').replace(/\..*$/, '');
return name;
}
}
export default ResumeLoader;
2. RAG 核心逻辑
// src/rag.js
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';
import { OpenAIEmbeddings } from 'langchain/embeddings/openai';
import { Chroma } from 'langchain/vectorstores/chroma';
import { ChatOpenAI } from '@langchain/openai';
import { PromptTemplate } from 'langchain/prompts';
import { RetrievalQAChain } from 'langchain/chains';
import ResumeLoader from './loader.js';
class ResumeRAG {
constructor(resumeDir) {
this.resumeDir = resumeDir;
this.loader = new ResumeLoader(resumeDir);
this.vectorStore = null;
this.llm = new ChatOpenAI({
temperature: 0.3, // 低随机性,确保准确性
model: 'gpt-4'
});
this.embeddings = new OpenAIEmbeddings();
}
// 初始化 RAG 系统
async init() {
console.log('📚 正在加载简历数据...');
const documents = await this.loader.loadAllResumes();
console.log(`✅ 加载了 ${documents.length} 份简历`);
// 文本分割
console.log('🔪 正在分割文本...');
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50
});
const chunks = await splitter.splitDocuments(documents);
console.log(`✅ 分割为 ${chunks.length} 个片段`);
// 创建向量数据库
console.log('🗄️ 正在构建向量索引...');
this.vectorStore = await Chroma.fromDocuments(
chunks,
this.embeddings,
{
path: process.env.CHROMA_PATH || './chroma_db'
}
);
console.log('✅ 向量索引构建完成');
}
// 创建问答 Prompt
createPromptTemplate() {
return new PromptTemplate({
template: `你是一名专业的招聘助手,需要根据候选人的简历信息回答问题。
简历信息:
{context}
用户问题:
{question}
回答要求:
1. 只基于简历信息回答,不要编造
2. 如果简历中没有相关信息,明确说明"简历中未提及"
3. 回答要简洁、准确、专业
4. 如果有多个候选人,分别列出
如果问题涉及多个候选人,请先说明你找到了哪些候选人的相关信息。
回答:`,
inputVariables: ['context', 'question']
});
}
// 回答问题
async ask(question, candidateName = null) {
if (!this.vectorStore) {
await this.init();
}
// 如果有指定候选人,添加过滤条件
let retriever;
if (candidateName) {
// 过滤特定候选人(简化版,实际可用 metadata filter)
retriever = this.vectorStore.asRetriever(3);
} else {
retriever = this.vectorStore.asRetriever(5);
}
// 创建 RAG 链
const chain = RetrievalQAChain.fromLLM(
this.llm,
retriever,
{
returnSourceDocuments: true,
prompt: this.createPromptTemplate()
}
);
// 调用链
const result = await chain.call({
query: question
});
return {
answer: result.result,
sourceDocuments: result.sourceDocuments
};
}
// 搜索相关简历片段
async search(query, k = 3) {
if (!this.vectorStore) {
await this.init();
}
const docs = await this.vectorStore.similaritySearch(query, k);
return docs.map(doc => ({
content: doc.pageContent,
source: doc.metadata.source,
candidate: doc.metadata.candidate
}));
}
// 获取所有候选人列表
async getCandidates() {
if (!this.vectorStore) {
await this.init();
}
const files = await this.loader.loadAllResumes();
return [...new Set(files.map(f => f.metadata.candidate))];
}
}
export default ResumeRAG;
3. 主程序入口
// src/index.js
import ResumeRAG from './rag.js';
import readline from 'readline';
const resumeDir = './data/resumes';
const rag = new ResumeRAG(resumeDir);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('🤖 欢迎使用简历问答机器人!');
console.log('💡 输入"退出"结束,输入"候选人"查看候选人列表\n');
async function main() {
await rag.init();
while (true) {
const question = await new Promise(resolve => {
rl.question('👤 你:', resolve);
});
if (question.toLowerCase() === '退出') {
console.log('\n👋 感谢使用,再见!');
rl.close();
process.exit(0);
}
if (question.toLowerCase() === '候选人') {
const candidates = await rag.getCandidates();
console.log('\n📋 候选人列表:');
candidates.forEach((c, i) => console.log(` ${i + 1}. ${c}`));
console.log('');
continue;
}
try {
console.log('\n🤖 AI:正在分析简历...\n');
const result = await rag.ask(question);
console.log('✅ 回答:');
console.log(result.answer);
if (result.sourceDocuments && result.sourceDocuments.length > 0) {
console.log('\n📚 信息来源:');
result.sourceDocuments.forEach((doc, i) => {
console.log(` ${i + 1}. ${doc.metadata.candidate} - ${doc.metadata.source}`);
});
}
console.log('\n');
} catch (error) {
console.error('❌ 出错了:', error.message);
}
}
}
main().catch(error => {
console.error('启动失败:', error);
process.exit(1);
});
4. 运行项目
# package.json
{
"type": "module",
"scripts": {
"start": "node src/index.js"
}
}
# 启动
npm start
使用示例:
👤 你:张三的技术栈是什么?
🤖 AI:正在分析简历...
✅ 回答:
根据张三的简历,他的技术栈包括:
前端框架:
- Vue 3, React, Angular
编程语言:
- TypeScript, JavaScript, Python
工具:
- Webpack, Vite, Docker
数据库:
- MySQL, MongoDB, Redis
📚 信息来源:
1. 张三 - 张三_前端工程师.pdf
👤 你:谁有做过大型项目?
🤖 AI:正在分析简历...
✅ 回答:
根据简历,以下候选人有大型项目经验:
张三:
- 电商后台管理系统(日活 10 万+)
- AI 智能客服平台(QPS 5000+)
李四:
- 金融交易平台(日交易金额 1 亿+)
📚 信息来源:
1. 张三 - 张三_前端工程师.pdf
2. 李四 - 李四_全栈工程师.docx
🎯 四、进阶功能扩展
1. 支持多候选人对比
// 修改 ask 方法,支持对比
async compareCandidates(question, candidates) {
const results = [];
for (const candidate of candidates) {
const result = await this.ask(question, candidate);
results.push({
candidate,
answer: result.answer
});
}
return this.generateComparison(results);
}
async generateComparison(results) {
const prompt = `
请对比以下候选人的回答:
${results.map(r => `${r.candidate}:\n${r.answer}`).join('\n\n')}
请给出对比分析和建议。
`;
return this.llm.call(prompt);
}
2. 添加 Web 界面
// 使用 Express 提供 API
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors());
app.use(express.json());
app.post('/api/ask', async (req, res) => {
const { question, candidate } = req.body;
const result = await rag.ask(question, candidate);
res.json(result);
});
app.get('/api/candidates', async (req, res) => {
const candidates = await rag.getCandidates();
res.json({ candidates });
});
app.listen(3000, () => {
console.log('API 服务运行在 http://localhost:3000');
});
3. 前端界面(Vue 3)
<!-- ResumeBot.vue -->
<script setup>
import { ref } from 'vue';
const question = ref('');
const answer = ref('');
const loading = ref(false);
const candidates = ref([]);
const selectedCandidate = ref('');
// 获取候选人列表
async function loadCandidates() {
const res = await fetch('http://localhost:3000/api/candidates');
candidates.value = await res.json();
}
// 提问
async function ask() {
if (!question.value) return;
loading.value = true;
answer.value = '';
try {
const res = await fetch('http://localhost:3000/api/ask', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: question.value,
candidate: selectedCandidate.value || null
})
});
const data = await res.json();
answer.value = data.answer;
} catch (error) {
answer.value = '请求失败:' + error.message;
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="resume-bot">
<h1>🤖 简历问答机器人</h1>
<div class="controls">
<select v-model="selectedCandidate">
<option value="">所有候选人</option>
<option v-for="c in candidates" :key="c" :value="c">
{{ c }}
</option>
</select>
</div>
<div class="input-area">
<input
v-model="question"
@keyup.enter="ask"
placeholder="输入问题,例如:张三的技术栈是什么?"
:disabled="loading"
/>
<button @click="ask" :disabled="loading">
{{ loading ? '思考中...' : '提问' }}
</button>
</div>
<div v-if="answer" class="answer">
<h3>✅ 回答:</h3>
<p>{{ answer }}</p>
</div>
</div>
</template>
4. 支持更多文件格式
// 添加 Excel 支持
import * as XLSX from 'xlsx';
async loadExcel(filePath) {
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet, { header: 1 });
const text = data.map(row => row.join(' ')).join('\n');
return {
pageContent: text,
metadata: { source: filePath }
};
}
📊 五、RAG 优化技巧
1. 文本分割优化
// 按语义分割(而不是固定长度)
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500,
chunkOverlap: 50,
separators: [
'\n\n', // 段落
'\n', // 行
'. ', // 句子
' ' // 单词
]
});
2. 混合检索
// 结合关键词检索 + 向量检索
const hybridRetriever = await MultiQueryRetriever.fromLLM({
retriever: vectorStore.asRetriever(),
llm: this.llm
});
3. 重排序(Re-ranking)
// 对检索结果重新排序,提高相关性
import { CohereRerank } from 'langchain/retrievers/cohere';
const reranker = new CohereRerank({
model: 'rerank-english-v2.0',
topN: 3
});
const docs = await retriever.getRelevantDocuments(question);
const reranked = await reranker.rerank(docs, question);
4. 缓存优化
// 缓存常见问题答案
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 3600 }); // 1 小时缓存
async ask(question) {
const cached = cache.get(question);
if (cached) return cached;
const result = await this.performAsk(question);
cache.set(question, result);
return result;
}
📊 六、RAG vs 其他方案对比
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| RAG | 私有数据问答 | 准确、可追溯、实时更新 | 需要向量数据库 |
| Fine-tuning | 特定领域模型 | 模型更懂领域知识 | 成本高、更新慢 |
| 纯 LLM | 通用问答 | 简单、无需数据 | 容易幻觉 |
| 传统搜索 | 关键词匹配 | 快速、准确 | 无法理解语义 |
🎓 七、学习资源
- 📖 LangChain RAG 文档
- 📚 RAG 技术详解
- 💻 Chroma 向量数据库
- 🎥 RAG 实战教程
✅ 总结
通过这个项目,我们学会了:
- ✅ RAG 核心原理(检索 + 生成)
- ✅ 构建 RAG 系统(文档加载、分割、向量化、检索)
- ✅ 实战项目(简历问答机器人)
- ✅ 进阶优化(混合检索、重排序、缓存)
- ✅ 部署方案(API + Web 界面)
RAG 不是「黑科技」,而是让 AI 真正读懂你数据的「桥梁」!
它让你的私有数据变成 AI 的「第二大脑」,回答更准确、更可信。
🚀 下一步
- 尝试更多数据源(数据库、API、网页)
- 优化检索效果(混合检索、重排序)
- 添加用户认证(多租户支持)
- 部署到生产环境(Docker + Kubernetes)
- 集成到现有系统(HR 系统、客服系统)
现在,轮到你动手实践了!🎉
如果觉得这篇文章对你有帮助,欢迎点赞、收藏、评论!也欢迎关注我,获取更多 RAG 和 AI 开发实战经验。
参考资料
作者:一名热爱 AI 应用开发的前端工程师
本文项目代码已开源,欢迎 Star 和 Fork!