让 Agent 读得懂你的私有数据(简历问答机器人)

2 阅读8分钟

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通用问答简单、无需数据容易幻觉
传统搜索关键词匹配快速、准确无法理解语义

🎓 七、学习资源


✅ 总结

通过这个项目,我们学会了:

  1. RAG 核心原理(检索 + 生成)
  2. 构建 RAG 系统(文档加载、分割、向量化、检索)
  3. 实战项目(简历问答机器人)
  4. 进阶优化(混合检索、重排序、缓存)
  5. 部署方案(API + Web 界面)

RAG 不是「黑科技」,而是让 AI 真正读懂你数据的「桥梁」!

它让你的私有数据变成 AI 的「第二大脑」,回答更准确、更可信。


🚀 下一步

  1. 尝试更多数据源(数据库、API、网页)
  2. 优化检索效果(混合检索、重排序)
  3. 添加用户认证(多租户支持)
  4. 部署到生产环境(Docker + Kubernetes)
  5. 集成到现有系统(HR 系统、客服系统)

现在,轮到你动手实践了!🎉


如果觉得这篇文章对你有帮助,欢迎点赞、收藏、评论!也欢迎关注我,获取更多 RAG 和 AI 开发实战经验。

参考资料


作者:一名热爱 AI 应用开发的前端工程师
本文项目代码已开源,欢迎 Star 和 Fork!