AI 工具开发实战(2):开发一个本地 RAG 知识库——丢一个文件夹进去,直接问答

上一篇做了一个命令行翻译工具,这篇做一个更实用的:本地 RAG 知识库

把 PDF、Markdown、TXT 文件丢到一个文件夹里,运行一条命令,就能向这些文件提问,AI 会基于文件内容回答。全程本地运行,数据不出本机。

项目结构

localrag/
├── localrag.py          # CLI 主程序
├── indexer.py           # 文档索引
├── retriever.py         # 检索引擎
├── requirements.txt
└── docs/                # 放文档的文件夹

安装依赖

# requirements.txt
click==8.1.7
openai==1.50.0
python-dotenv==1.0.0
sentence-transformers==3.1.0
PyMuPDF==1.24.0
numpy==1.26.0

核心实现

文档索引器(indexer.py)

# indexer.py
import os
import json
import hashlib
from pathlib import Path
import fitz  # PyMuPDF

class DocumentIndexer:
    """文档索引:读取文件夹、切分文本、向量化。"""

    def __init__(self, chunk_size=512, chunk_overlap=128):
        self.chunk_size = chunk_size
        self.chunk_overlap = chunk_overlap

    def load_folder(self, folder_path: str) -> list:
        """加载文件夹中的所有文档。"""
        docs = []
        for f in sorted(Path(folder_path).iterdir()):
            if f.suffix.lower() == '.pdf':
                text = self._parse_pdf(str(f))
            elif f.suffix.lower() in ('.txt', '.md'):
                text = f.read_text(encoding='utf-8', errors='ignore')
            else:
                continue
            if text.strip():
                docs.append({"source": f.name, "content": text})
        return docs

    def _parse_pdf(self, path):
        doc = fitz.open(path)
        text = '\n'.join(p.get_text() for p in doc)
        doc.close()
        return text

    def chunk_documents(self, docs: list) -> list:
        """将文档切分为重叠的 chunk。"""
        chunks = []
        for doc in docs:
            text = doc["content"]
            # 按段落切
            paragraphs = text.split('\n\n')
            buffer = ""
            for para in paragraphs:
                para = para.strip()
                if not para:
                    continue
                if len(buffer) + len(para)  list:
        """检索最相关的 chunk。"""
        q_vec = self.model.encode([query], normalize_embeddings=True)[0]
        scores = np.dot(self.vectors, q_vec)  # 余弦相似度
        top_indices = np.argsort(scores)[-top_k:][::-1]

        results = []
        for idx in top_indices:
            if scores[idx] `")
        return

    # 检索
    results = retriever.search(question, top_k=top)
    if not results:
        click.echo("❌ 没有找到相关内容")
        return

    # 构建 Prompt
    context = "\n\n".join(
        f"[{r['source']}] {r['text'][:500]}" for r in results
    )

    # 调用 LLM
    response = client.chat.completions.create(
        model="deepseek-chat",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT.format(context=context)},
            {"role": "user", "content": question},
        ],
        temperature=0.3,
        stream=True,
    )

    # 流式输出
    for chunk in response:
        if chunk.choices[0].delta.content:
            click.echo(chunk.choices[0].delta.content, nl=False)
    click.echo()

    # 显示来源
    if show_sources:
        click.echo(f"\n📖 引用来源:")
        for i, r in enumerate(results):
            click.echo(f"  [{i+1}] {r['source']} (相关度: {r['score']:.2f})")


if __name__ == "__main__":
    cli()

使用方式

# 1. 索引文档
python localrag.py index ./docs
# ✅ 索引完成:5 个文档 → 42 个片段

# 2. 提问
python localrag.py ask "这个项目的架构是什么?"
# 这个项目采用微服务架构,包含 API 服务、数据库、缓存三层...

# 3. 带来源引用
python localrag.py ask "部署流程是怎样的?" --show-sources
# ...(回答内容)...
# 📖 引用来源:
#   [1] deploy.md (相关度: 0.87)
#   [2] architecture.md (相关度: 0.72)

性能特点

  • 全本地运行,数据不出本机
  • 支持 PDF、MD、TXT 三种格式
  • 首次索引后缓存,下次提问不用重新索引
  • 512 维 BGE 向量,500 个 chunk 检索 本文是 《AI 开发者工具链实战》 系列的第 2 篇。

    上一篇:命令行 AI 翻译工具
    下一篇:AI 代码审查 CLI
    本文由 Zyentor(智元界)原创发布