在“仅用于自己查询、不上传任何信息到云端”的前提下,构建本地知识库的核心目标是:完全离线、数据安全、高效检索。下面是一个系统化的构建方案,涵盖工具选择、数据处理、检索方式等关键环节:
[本地文档/数据源]
↓
[文档解析 & 向量化] → [本地向量数据库]
↓
[本地大模型(LLM)] ←→ [本地检索系统]
↓
[用户界面 / 查询入口]
所有组件均运行在你的个人电脑或局域网服务器上,无需联网。
Unstructured(Python 库):智能解析多种文档。PyPDF2 / pdfplumber:处理 PDF。BeautifulSoup:解析 HTML。RecursiveCharacterTextSplitter 或自定义逻辑。BAAI/bge-small-zh-v1.5(中文优化,轻量高效)m3e-small(中文社区常用)all-MiniLM-L6-v2(英文/多语言,极小)sentence-transformers 库本地加载模型。✅ 所有数据库均可配置为仅保存在本地磁盘,不联网。
Qwen1.5-4B-Chat、Yi-1.5-6B-Chat、DeepSeek-Coder(若含代码)Mistral-7B、Phi-3-mini(微软,性能强且小)Ollama(最简单,一键运行模型)LM Studio(图形界面,支持聊天和 API)llama.cpp(CPU 友好,量化后可在普通笔记本运行)LangChain 或 LlamaIndex 构建本地 RAG 流程(设置 offline=True,禁用网络)。Gradio 或 Streamlit 快速搭建本地 UI)Smart Connections + Local LLM 集成)~/my_knowledge_base/)。ollama serve 或 lmstudio 时,确保禁用远程访问(默认仅 localhost)。pip install langchain chromadb sentence-transformers unstructured
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
from langchain_chroma import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
embeddings = HuggingFaceBgeEmbeddings(model_name="BAAI/bge-small-zh-v1.5")
vectorstore = Chroma.from_documents(
documents=your_docs,
embedding=embeddings,
persist_directory="./chroma_db"
)
results = vectorstore.similarity_search("你的问题", k=3)
print(results)
ollama pull qwen:4b
ollama run qwen:4b
再通过 API 或 LangChain 调用。
✅ 完全本地
✅ 无数据上传
✅ 中文友好
✅ 开源免费
✅ 可在普通电脑运行(尤其使用 4B 以下模型 + Chroma/FAISS)
只要你有一台个人电脑(最好是 16GB+ 内存,如有独立 GPU 更佳),就可以搭建一个私密、安全、高效的个人知识库系统。
以下是一个完全本地化、无需联网、不上传任何数据到云端的中文个人知识库构建教程,使用开源工具和模型。我们将使用:
BAAI/bge-small-zh-v1.5(中文优化,本地运行)从 python.org 下载并安装。
mkdir ~/local_knowledge_base
cd ~/local_knowledge_base
python -m venv venv
source venv/bin/activate # macOS/Linux
# 或
venv\Scripts\activate # Windows
pip install langchain \
langchain-community \
langchain-chroma \
chromadb \
sentence-transformers \
unstructured \
unstructured[local-inference] \
pdf2image \
PyPDF2 \
ollama \
python-dotenv
⚠️ 如果你不需要 PDF 解析,可跳过
pdf2image和PyPDF2。⚠️ 首次运行
sentence-transformers会自动下载模型到本地缓存(约 130MB),但不需要联网之后也能用。如果你完全不能联网,请提前在有网环境下载好模型(见附录)。
在项目目录下创建一个 docs/ 文件夹,放入你的文档,例如:
local_knowledge_base/
├── docs/
│ ├── note1.md
│ ├── note2.md
│ └── my_book_summary.txt
支持格式:
.txt,.md,
创建文件:ingest.py
# ingest.py
import os
from langchain_community.document_loaders import (
DirectoryLoader,
TextLoader,
UnstructuredMarkdownLoader,
PyPDFLoader
)
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 配置路径
DOCS_DIR = "./docs"
PERSIST_DIR = "./chroma_db"
def load_documents():
"""加载所有支持的文档"""
documents = []
# 加载 .txt 文件
txt_loader = DirectoryLoader(DOCS_DIR, glob="*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"})
documents.extend(txt_loader.load())
# 加载 .md 文件
md_loader = DirectoryLoader(DOCS_DIR, glob="*.md", loader_cls=UnstructuredMarkdownLoader)
documents.extend(md_loader.load())
# 加载 .pdf 文件(可选)
# pdf_loader = DirectoryLoader(DOCS_DIR, glob="*.pdf", loader_cls=PyPDFLoader)
# documents.extend(pdf_loader.load())
print(f"✅ 共加载 {len(documents)} 个文档片段")
return documents
def split_documents(documents):
"""将文档切分为小块"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len,
is_separator_regex=False,
)
chunks = text_splitter.split_documents(documents)
print(f"✅ 切分为 {len(chunks)} 个文本块")
return chunks
if __name__ == "__main__":
docs = load_documents()
chunks = split_documents(docs)
# 保存到临时文件(用于下一步嵌入)
os.makedirs(PERSIST_DIR, exist_ok=True)
with open(os.path.join(PERSIST_DIR, "chunks.txt"), "w", encoding="utf-8") as f:
for i, chunk in enumerate(chunks):
f.write(f"--- Chunk {i} ---\n")
f.write(chunk.page_content + "\n\n")
print("✅ 文本块已保存(供调试)")
运行:
python ingest.py
✅ 此时会在
chroma_db/chunks.txt中看到切分后的文本(可选,仅调试用)
创建文件:build_vector_db.py
# build_vector_db.py
from langchain_chroma import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from ingest import load_documents, split_documents
import os
# 嵌入模型配置(完全本地)
embedding_model_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {'device': 'cpu'} # 若有 GPU 可改为 'cuda'
encode_kwargs = {'normalize_embeddings': True} # BGE 推荐设置
embeddings = HuggingFaceBgeEmbeddings(
model_name=embedding_model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
query_instruction="为这个句子生成表示以用于检索相关文章:" # BGE 中文模型推荐 query 指令
)
PERSIST_DIR = "./chroma_db"
def build_vector_db():
print("🔍 正在加载文档...")
docs = load_documents()
chunks = split_documents(docs)
print("🧠 正在生成向量并构建数据库(首次运行会下载模型到本地缓存)...")
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=PERSIST_DIR,
collection_name="my_knowledge"
)
print(f"✅ 向量数据库已保存到 {PERSIST_DIR}")
if __name__ == "__main__":
build_vector_db()
运行:
python build_vector_db.py
⏳ 首次运行会自动下载
BAAI/bge-small-zh-v1.5模型(约 130MB)到~/.cache/huggingface/,之后完全离线可用。✅ 数据库文件将保存在
./chroma_db/,仅在本地磁盘。
创建文件:query.py
# query.py
from langchain_chroma import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
embedding_model_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
embeddings = HuggingFaceBgeEmbeddings(
model_name=embedding_model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
query_instruction="为这个句子生成表示以用于检索相关文章:"
)
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="my_knowledge"
)
def search(query: str, k: int = 3):
docs = vectorstore.similarity_search(query, k=k)
print(f"\n🔍 问题:{query}\n")
for i, doc in enumerate(docs):
print(f"【结果 {i+1}】(来源: {doc.metadata.get('source', '未知')})")
print(doc.page_content[:500] + "...\n")
return docs
if __name__ == "__main__":
while True:
question = input("\n请输入你的问题(输入 'quit' 退出):")
if question.lower() == 'quit':
break
search(question)
运行:
python query.py
✅ 此时你可以直接提问,系统会返回最相关的原文片段,完全本地、无网络请求。
ollama pull qwen:4b
模型大小约 2.5GB(4-bit 量化版),可在 16GB 内存笔记本 CPU 运行(稍慢)。
rag_query.py# rag_query.py
from langchain_chroma import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from langchain_ollama import OllamaLLM
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
# === 配置 ===
embedding_model_name = "BAAI/bge-small-zh-v1.5"
llm_model = "qwen:4b" # Ollama 中的模型名
# === 嵌入与向量库 ===
embeddings = HuggingFaceBgeEmbeddings(
model_name=embedding_model_name,
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True},
query_instruction="为这个句子生成表示以用于检索相关文章:"
)
vectorstore = Chroma(
persist_directory="./chroma_db",
embedding_function=embeddings,
collection_name="my_knowledge"
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# === LLM 与 Prompt ===
llm = OllamaLLM(model=llm_model, temperature=0.3)
template = """
你是一个知识助手,请根据以下提供的上下文信息回答问题。
只使用上下文中的内容,不要编造。如果上下文没有相关信息,请回答“根据现有资料无法回答”。
上下文:
{context}
问题:{question}
答案:
"""
prompt = ChatPromptTemplate.from_template(template)
# === RAG 链 ===
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# === 查询 ===
if __name__ == "__main__":
while True:
question = input("\n🧠 请输入你的问题('quit' 退出):")
if question.lower() == 'quit':
break
print("\n🤔 正在思考...\n")
answer = rag_chain.invoke(question)
print(f"✅ 答案:{answer}\n")
⚠️ 确保 Ollama 正在运行(终端输入
ollama serve,或后台已启动)
运行:
python rag_query.py
✅ 此时系统会:
- 用本地嵌入模型检索相关段落
- 将段落 + 问题交给本地 Qwen 模型
- 生成自然语言答案 全程无任何数据上传到网络
./chroma_db/~/.cache/huggingface/hub/~/.ollama/models/C:\Users\<user>\.ollama\models\~/.ollama/models/local_knowledge_base 文件夹放入加密磁盘(如 VeraCrypt)进一步保护。如果你完全不能联网,请在有网络的机器上提前下载:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
model.save('./bge-small-zh') # 保存到本地
然后修改代码中 model_name='./bge-small-zh'
Ollama 模型可直接复制 ~/.ollama/models/ 到另一台机器(需相同架构)。
你现在已经拥有一个:
的私人知识库系统!
在已有完全本地、离线、隐私安全的知识库基础上,我们可以进一步:
watchdog 监听 docs/ 目录变化,自动增量更新向量库✅ 依然 不联网、不上传任何数据,所有组件运行在本地。
[docs/ 目录] ←─(watchdog 监控)─┐
↓
[用户通过浏览器访问 http://localhost:8501]
↓
[Streamlit GUI] → [检索/问答逻辑] → [ChromaDB + 本地 LLM]
↑
(文档变更时自动触发 rebuild 向量库)
pip install streamlit watchdog
注意:ChromaDB 本身不支持高效增量删除,但我们可以:
- 简单方案:监控到变化 → 重新构建整个向量库(适合 <1000 文档)
- 进阶方案:记录文档 hash,只更新变更/新增的 chunk(本文采用简单方案,清晰可靠)
auto_updater.py# auto_updater.py
import os
import time
import hashlib
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from build_vector_db import build_vector_db # 复用之前写的构建函数
DOCS_DIR = "./docs"
STATE_FILE = "./chroma_db/update_state.txt"
def compute_docs_hash():
"""计算 docs/ 目录下所有文件的 hash,用于判断是否变化"""
hash_md5 = hashlib.md5()
paths = sorted(Path(DOCS_DIR).rglob("*"))
for path in paths:
if path.is_file():
stat = path.stat()
hash_md5.update(str(stat.st_mtime).encode())
hash_md5.update(str(stat.st_size).encode())
return hash_md5.hexdigest()
def load_last_hash():
if os.path.exists(STATE_FILE):
with open(STATE_FILE, "r") as f:
return f.read().strip()
return None
def save_current_hash():
current_hash = compute_docs_hash()
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
f.write(current_hash)
class DocUpdateHandler(FileSystemEventHandler):
def __init__(self, callback):
self.callback = callback
self.last_trigger = 0
def on_any_event(self, event):
# 防抖:5 秒内只触发一次
if time.time() - self.last_trigger > 5:
if event.src_path.endswith(('.txt', '.md', '.pdf')):
print(f"\n📁 检测到文档变更: {event.src_path}")
self.last_trigger = time.time()
self.callback()
def start_watcher():
"""启动文件监控"""
observer = Observer()
event_handler = DocUpdateHandler(on_docs_changed)
observer.schedule(event_handler, DOCS_DIR, recursive=True)
observer.start()
print(f"👀 正在监控 {os.path.abspath(DOCS_DIR)} 目录...")
# 初始检查是否需要首次构建
if not os.path.exists("./chroma_db/chroma.sqlite3"): # ChromaDB 默认文件
print("🆕 首次运行:正在构建向量库...")
build_vector_db()
save_current_hash()
else:
current_hash = compute_docs_hash()
last_hash = load_last_hash()
if current_hash != last_hash:
print("🔄 检测到历史变更:正在重建向量库...")
build_vector_db()
save_current_hash()
return observer
def on_docs_changed():
"""文档变更时的回调"""
build_vector_db()
save_current_hash()
print("✅ 向量库已更新!")
build_vector_db.py(支持被调用)修改 build_vector_db.py,使其可被其他模块调用而不重复初始化模型:
# build_vector_db.py(修改版)
from langchain_chroma import Chroma
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
from ingest import load_documents, split_documents
import os
import shutil
PERSIST_DIR = "./chroma_db"
embedding_model_name = "BAAI/bge-small-zh-v1.5"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}
query_instruction = "为这个句子生成表示以用于检索相关文章:"
# 全局嵌入模型(避免重复加载)
_EMBEDDING_MODEL = None
def get_embedding_model():
global _EMBEDDING_MODEL
if _EMBEDDING_MODEL is None:
_EMBEDDING_MODEL = HuggingFaceBgeEmbeddings(
model_name=embedding_model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs,
query_instruction=query_instruction
)
return _EMBEDDING_MODEL
def build_vector_db():
"""构建向量数据库(覆盖写入)"""
# 删除旧数据库(Chroma 不支持高效更新)
if os.path.exists(PERSIST_DIR):
shutil.rmtree(PERSIST_DIR)
os.makedirs(PERSIST_DIR)
docs = load_documents()
chunks = split_documents(docs)
embeddings = get_embedding_model()
print("🧠 正在生成向量...")
Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=PERSIST_DIR,
collection_name="my_knowledge"
)
print("✅ 向量库重建完成")
app.py# app.py
import streamlit as st
from langchain_chroma import Chroma
from build_vector_db import get_embedding_model
from langchain_ollama import OllamaLLM
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
import os
# === 配置 ===
PERSIST_DIR = "./chroma_db"
USE_RAG = True # 设为 False 则只返回检索结果
# === 初始化 ===
@st.cache_resource
def get_retriever():
embeddings = get_embedding_model()
vectorstore = Chroma(
persist_directory=PERSIST_DIR,
embedding_function=embeddings,
collection_name="my_knowledge"
)
return vectorstore.as_retriever(search_kwargs={"k": 3})
@st.cache_resource
def get_rag_chain():
retriever = get_retriever()
llm = OllamaLLM(model="qwen:4b", temperature=0.3)
template = """
你是一个知识助手,请根据以下提供的上下文信息回答问题。
只使用上下文中的内容,不要编造。如果上下文没有相关信息,请回答“根据现有资料无法回答”。
上下文:
{context}
问题:{question}
答案:
"""
prompt = ChatPromptTemplate.from_template(template)
return (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
# === Streamlit UI ===
st.set_page_config(page_title="我的本地知识库", layout="wide")
st.title("🧠 我的本地知识库(完全离线)")
# 输入框
query = st.text_input("请输入你的问题:", placeholder="例如:项目的关键时间节点是什么?")
if query:
with st.spinner("正在检索和思考..."):
if USE_RAG:
try:
answer = get_rag_chain().invoke(query)
st.subheader("✅ 答案")
st.write(answer)
except Exception as e:
st.error(f"模型调用失败(请确认 Ollama 正在运行):{str(e)}")
USE_RAG = False # 回退到仅检索
# 显示检索到的原文(始终显示)
retriever = get_retriever()
docs = retriever.invoke(query)
st.subheader("📚 相关原文片段")
for i, doc in enumerate(docs):
source = doc.metadata.get("source", "未知")
st.markdown(f"**来源 {i+1}:** `{os.path.basename(source)}`")
st.text_area("", doc.page_content, height=150, key=f"doc_{i}")
# 侧边栏
with st.sidebar:
st.header("⚙️ 系统状态")
db_exists = os.path.exists(os.path.join(PERSIST_DIR, "chroma.sqlite3"))
st.success("🟢 向量库已加载") if db_exists else st.warning("🔴 向量库未构建")
ollama_status = "🟢 Ollama 可用(若启用 RAG)" if USE_RAG else "⚪ 仅检索模式"
st.write(ollama_status)
st.info("📁 文档目录: `./docs`")
st.info("🔒 所有数据仅保存在本地")
start.sh(Linux/macOS)或 start.bat(Windows)start.sh#!/bin/bash
source venv/bin/activate
# 启动监控
python -c "
from auto_updater import start_watcher
import time
observer = start_watcher()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
" &
WATCHER_PID=$!
# 启动 Streamlit
streamlit run app.py --server.port=8501 --server.address=127.0.0.1
# 停止监控
kill $WATCHER_PID
start.bat)@echo off
call venv\Scripts\activate
start /B python -c "from auto_updater import start_watcher; import time; o=start_watcher(); time.sleep(100000)" ^
streamlit run app.py --server.port=8501 --server.address=127.0.0.1
💡 更简单的做法:开两个终端分别运行:
# 终端 1:启动监控 python -c "from auto_updater import start_watcher; start_watcher(); import time; time.sleep(100000)" # 终端 2:启动 Web 界面 streamlit run app.py
chmod +x start.sh # Linux/macOS
./start.sh
浏览器会自动打开 http://localhost:8501
./docs/ 下的 .md / .txt 文件127.0.0.1(仅本机访问)| 功能 | 实现方式 |
|---|---|
| 支持 PDF/Word | 在 ingest.py 中启用 Unstructured 的 PDF/DOCX loader |
| 显示文档修改时间 | 在 auto_updater.py 中记录变更日志 |
| 多用户(局域网) | 修改 Streamlit 启动参数 --server.address=0.0.0.0(⚠️ 仅限可信内网) |
| 加密存储 | 将整个项目目录放入 VeraCrypt 加密卷 |
你现在拥有一个:
的完整个人知识库系统!
💡 提示:首次运行会较慢(下载模型 + 构建向量库),之后所有操作均在本地快速完成。
要在 ingest.py 中启用 Unstructured 的 PDF 和 DOCX(Word)文档加载功能,你需要:
ingest.py 中添加对应的加载器Unstructured 使用本地模型(避免联网)由于你要求完全本地、不上传任何信息到云端,我们将配置 Unstructured 使用 本地 OCR(如 Tesseract) 和 本地解析器,禁用其默认可能调用的远程 API(如 unstructured-api)。
# 已有基础
pip install unstructured[local-inference]
# 新增:PDF 和 DOCX 支持
pip install unstructured[pdf] # 包含 pdf2image, PyPDF2
pip install unstructured[docx] # 包含 python-docx
# 可选但推荐(提高 PDF 表格/图文解析质量)
pip install pymupdf # 更快的 PDF 解析(替代 pdf2image)
pip install poppler-utils # 用于 pdf2image(需系统安装)
⚠️ 注意:
unstructured[pdf]默认会尝试使用pdf2image,它依赖系统级的 Poppler。
chi_sim(简体中文)和 chi_tra(繁体)C:\Program Files\Tesseract-OCR)加入系统 PATH 环境变量pdf2image)C:\popplerC:\poppler\Library\bin 加入系统 PATH✅ 验证安装:
tesseract --version pdftoppm -h
ingest.py 启用 PDF/DOCX 加载器更新你的 ingest.py 文件如下(关键:禁用远程 API,强制本地解析):
# ingest.py (更新版:支持 PDF / DOCX / TXT / MD,完全本地)
import os
from langchain_community.document_loaders import (
DirectoryLoader,
TextLoader,
UnstructuredMarkdownLoader,
)
from langchain_community.document_loaders.unstructured import UnstructuredFileLoader
from typing import List
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="unstructured")
DOCS_DIR = "./docs"
PERSIST_DIR = "./chroma_db"
def load_documents() -> List:
"""加载所有支持的文档(TXT, MD, PDF, DOCX),完全本地解析"""
documents = []
# === .txt 文件 ===
txt_loader = DirectoryLoader(
DOCS_DIR,
glob="*.txt",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
documents.extend(txt_loader.load())
# === .md 文件 ===
md_loader = DirectoryLoader(
DOCS_DIR,
glob="*.md",
loader_cls=UnstructuredMarkdownLoader,
loader_kwargs={"encoding": "utf-8"}
)
documents.extend(md_loader.load())
# === .pdf 文件 ===
pdf_files = [str(f) for f in Path(DOCS_DIR).glob("*.pdf")]
for pdf_file in pdf_files:
print(f"📄 正在解析 PDF: {os.path.basename(pdf_file)}")
loader = UnstructuredFileLoader(
pdf_file,
mode="single", # 或 "elements"(更细粒度)
strategy="fast", # "hi_res" 更准但慢,需 OCR
# 关键:强制本地,不调用 API
api_url=None, # 禁用远程 API
# 启用本地 OCR(如果 PDF 是扫描件)
ocr_languages=["chi_sim", "eng"], # 中英混合
)
documents.extend(loader.load())
# === .docx 文件 ===
docx_files = [str(f) for f in Path(DOCS_DIR).glob("*.docx")]
for docx_file in docx_files:
print(f"📝 正在解析 DOCX: {os.path.basename(docx_file)}")
loader = UnstructuredFileLoader(
docx_file,
mode="single",
strategy="fast",
api_url=None, # 禁用远程 API
)
documents.extend(loader.load())
print(f"✅ 共加载 {len(documents)} 个文档片段")
return documents
# --- 保留原有的 split_documents 函数 ---
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pathlib import Path
def split_documents(documents):
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
length_function=len,
is_separator_regex=False,
)
chunks = text_splitter.split_documents(documents)
print(f"✅ 切分为 {len(chunks)} 个文本块")
return chunks
if __name__ == "__main__":
docs = load_documents()
chunks = split_documents(docs)
os.makedirs(PERSIST_DIR, exist_ok=True)
with open(os.path.join(PERSIST_DIR, "chunks.txt"), "w", encoding="utf-8") as f:
for i, chunk in enumerate(chunks):
f.write(f"--- Chunk {i} ---\n")
f.write(chunk.page_content + "\n\n")
print("✅ 文本块已保存(供调试)")
UnstructuredFileLoader 默认在 api_url=None 时会使用 本地解析(partition 函数),不会连接任何远程服务。
你可以通过以下方式确认:
python ingest.py🔒 安全提示:
Unstructured的UnstructuredAPIFileLoader才会联网,我们用的是UnstructuredFileLoader(本地版)。
如果你的 PDF 是扫描件(图片型),确保 Tesseract 已正确安装并支持中文:
# 在 UnstructuredFileLoader 中显式指定 OCR 选项
loader = UnstructuredFileLoader(
pdf_file,
strategy="hi_res", # 启用高级布局分析
ocr_languages="chi_sim+eng", # 中英混合
include_page_breaks=False,
api_url=None,
)
💡
strategy="hi_res"会调用layoutparser和Tesseract,需要额外安装:pip install unstructured[local-inference] # 已包含 layoutparser, detectron2(但 detectron2 在 Windows 安装复杂)如果安装失败,可降级使用
strategy="ocr_only"或"fast"。
local_knowledge_base/
└── docs/
├── notes.md
├── summary.txt
├── report.pdf ← 支持!
└── meeting.docx ← 支持!
放入这些文件后,运行:
python ingest.py
python build_vector_db.py
即可将 PDF/DOCX 内容纳入知识库。
| 问题 | 解决方案 |
|---|---|
TesseractNotFoundError |
安装 Tesseract 并加入 PATH |
poppler not installed |
安装 Poppler 并加入 PATH |
| 中文 OCR 识别乱码 | 安装 chi_sim 语言包(Tesseract 安装时勾选) |
| DOCX 表格丢失 | unstructured 对复杂表格支持有限,可考虑转为 PDF 或 Markdown 预处理 |
你现在已经成功在 ingest.py 中启用了:
所有文档(包括 PDF/DOCX)都会被切块、向量化,并纳入你的本地知识库,可通过图形界面查询。
在图像处理、视频编码和内存存储中,Interleaved(交错)、Planar(平面)和 Semi-planar(半平面)是描述多通道数据(如 RGB、YUV)在内存中布局方式的三种常见格式。它们直接影响内存访问效率、硬件解码兼容性、图像处理性能。
下面以最常见的 YUV 色彩空间(视频常用)为例进行说明(RGB 原理类似):
所有 Y 像素连续存储,然后 所有 U 像素连续存储,最后 所有 V 像素连续存储。
内存地址 → [YYYY YYYY YYYY YYYY UU UU VV VV]
↑ ↑ ↑ ↑ ↑ ↑
Y平面 | | | U平面 V平面
全部Y数据 全部U数据 全部V数据
I420(YUV420 Planar)YV12(Y, V, U 顺序,Planar)📌 典型应用:FFmpeg、OpenCV(
cv::COLOR_YUV2BGR_I420)、部分摄像头原始输出。
Y、U、V 像素按像素或宏块交错存储。常见于 RGB,YUV 中较少(但有)。
[R0 G0 B0] [R1 G1 B1] [R2 G2 B2] ...
[Y0 U0 Y1 V0] [Y2 U1 Y3 V1] ...
RGB24(R-G-B 交错)YUYV / UYVY(YUV422 Interleaved)📌 典型应用:显示器帧缓冲、USB 摄像头(UVC)、DirectShow。
[YYYY YYYY YYYY YYYY UV UV UV UV]
↑ ↑ ↑ ↑ ↑
Y平面(全部Y) UV平面(U和V交替)
NV12:Y + UV 交错(U 在前)NV21:Y + VU 交错(V 在前,Android camera 默认)📌 典型应用:
- Android Camera 输出格式:
NV21- Intel/AMD/NVIDIA GPU 视频解码器:默认输出
NV12- H.264/H.265 解码:硬件解码器常输出 Semi-planar
| 特性 | Planar (I420) | Semi-planar (NV12) | Interleaved (YUYV) |
|---|---|---|---|
| Y 存储 | 连续 | 连续 | 与 U/V 交错 |
| U/V 存储 | U 连续,V 连续 | UV 交替(UVUV…) | 与 Y 交错 |
| 内存块数 | 3 块(Y, U, V) | 2 块(Y, UV) | 1 块 |
| 硬件支持 | 一般 | ⭐⭐⭐(GPU/编解码器首选) | 中等 |
| Android Camera | ❌ | ✅ NV21 |
❌ |
| OpenCV 支持 | ✅ I420 |
✅ NV12 |
✅ YUYV |
NV21(Semi-planar)// Java (Android)
Mat yuvMat = new Mat(height + height/2, width, CvType.CV_8UC1);
yuvMat.put(0, 0, nv21ByteArray);
Mat bgrMat = new Mat();
Imgproc.cvtColor(yuvMat, bgrMat, Imgproc.COLOR_YUV2BGR_NV21);
NV12| 格式 | 中文 | 存储方式 | 典型用途 |
|---|---|---|---|
| Planar | 平面格式 | Y / U / V 分三个连续区域 | 软件处理、FFmpeg |
| Semi-planar | 半平面格式 | Y 连续 + UV 交错 | Android、GPU、硬件编解码 |
| Interleaved | 交错格式 | 每像素/宏块内 YUV 交错 | 显示器、RGB 图像、摄像头 |
💡 记住:
- Planar = 三通道分离
- Semi-planar = Y 分离,UV 合并交错
- Interleaved = 所有通道混合交错
理解这三种布局,对优化图像处理性能、调试视频 pipeline、跨平台数据转换至关重要。
Video Signal Process technology lists:








![]()





wav是一种无损的音频文件格式,WAV符合 PIFF(Resource Interchange File Format)规范。 所有的WAV都有一个文件头,这个文件头音频流的编码参数。 WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。 缺点:体积十分大!
PCM(Pulse Code Modulation—-脉码调制录音)。 所谓PCM录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。 PCM信号是由“1”、“0”等符号构成的数字信号,而未经过任何编码和压缩处理。 与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。
简单来说:wav是一种无损的音频文件格式,pcm是没有压缩的编码方式。
WAV可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中。 基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互转换之中,例如MP3转换成WMA。
简单来说:pcm是无损wav文件中音频数据的一种编码方式,但wav还可以用其它方式编码。
在文件的前44字节放置标头(header),使播放器或编辑器能够简单掌握文件的基本信息,其内容以区块(chunk)为最小单位,每一区块长度为4字节。

// 将录音文件保存为wav格式,这需要手动填充wav的文件头信息
private RandomAccessFile fopen(String path) throws IOException {
File f = new File(path);
if (f.exists()) {
f.delete();
} else {
File parentDir = f.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
}
RandomAccessFile file = new RandomAccessFile(f, "rw");
// 16K、16bit、单声道
/* RIFF header */
file.writeBytes("RIFF"); // riff id
file.writeInt(0); // riff chunk size *PLACEHOLDER*
file.writeBytes("WAVE"); // wave type
/* fmt chunk */
file.writeBytes("fmt "); // fmt id
file.writeInt(Integer.reverseBytes(16)); // fmt chunk size
file.writeShort(Short.reverseBytes((short) 1)); // format: 1(PCM)
file.writeShort(Short.reverseBytes((short) 1)); // channels: 1
file.writeInt(Integer.reverseBytes(16000)); // samples per second
file.writeInt(Integer.reverseBytes((int) (1 * 16000 * 16 / 8))); // BPSecond
file.writeShort(Short.reverseBytes((short) (1 * 16 / 8))); // BPSample
file.writeShort(Short.reverseBytes((short) (1 * 16))); // bPSample
/* data chunk */
file.writeBytes("data"); // data id
file.writeInt(0); // data chunk size *PLACEHOLDER*
Log.d(TAG, "wav path: " + path);
return file;
}
private void fwrite(RandomAccessFile file, byte[] data, int offset, int size) throws IOException {
file.write(data, offset, size);
Log.d(TAG, "fwrite: " + size);
}
private void fclose(RandomAccessFile file) throws IOException {
try {
file.seek(4); // riff chunk size
file.writeInt(Integer.reverseBytes((int) (file.length() - 8)));
file.seek(40); // data chunk size
file.writeInt(Integer.reverseBytes((int) (file.length() - 44)));
Log.d(TAG, "wav size: " + file.length());
} finally {
file.close();
}
}
# 将文件头去掉,数据转成int型即可
import numpy as np
def wav2pcm(wavfile, pcmfile, data_type=np.int16):
f = open(wavfile, "rb")
f.seek(0)
f.read(44)
data = np.fromfile(f, dtype= data_type)
data.tofile(pcmfile)
# 利用wave库,添加通道信息、采样位数、采样率等信息作为文件头,pcm数据直接写入即可
import wave
def pcm2wav(pcm_file, wav_file, channels=1, bits=16, sample_rate=16000):
pcmf = open(pcm_file, 'rb')
pcmdata = pcmf.read()
pcmf.close()
if bits % 8 != 0:
raise ValueError("bits % 8 must == 0. now bits:" + str(bits))
wavfile = wave.open(wav_file, 'wb')
wavfile.setnchannels(channels)
wavfile.setsampwidth(bits // 8)
wavfile.setframerate(sample_rate)
wavfile.writeframes(pcmdata)
wavfile.close()
AAC-LC 是一个音频编码标准,它是 Advanced Audio Coding (AAC) 的一个配置文件(Profile),全称为 AAC Low Complexity,意为 低复杂度AAC。
AAC 标准还有其他变体,它们在压缩效率、复杂度和延迟方面有所不同:
简单来说,AAC-LC 是目前最主流和兼容性最好的 AAC 格式,是高品质音频压缩的基石。
在音频系统中,进行采样率转换(SRC)的主要原因是为了确保不同来源的音频信号能够在统一的采样率下进行处理和输出。这一过程对于音频系统的兼容性、同步性和音质保持至关重要。 以下是对这一问题的详细分析:
统一采样率以实现兼容性 在音频系统中,不同设备或音频源可能使用不同的采样率。例如,CD音频通常使用44.1kHz的采样率,而某些设备可能使用48kHz的采样率。 如果这些音频信号直接混合或处理,可能会导致信号失真或无法正确播放。因此,通过采样率转换(SRC)将所有音频信号转换为统一的采样率(如48kHz),可以确保 所有音频信号在混合和处理过程中保持一致。
防止音调失真和保持同步 不同采样率的音频信号在混合时可能会导致音调失真或相位失真。通过采样率转换,可以确保所有音频信号在相同的采样率下进行处理,从而避免因采样率不一致导致的音质问题。此外,采样率转换还可以帮助系统保持同步,特别是在多设备或多音频源共存的环境中。
提高音频处理的灵活性和兼容性 在音频制作和播放过程中,不同设备和平台可能使用不同的采样率。例如,数字音频工作站(DAW)可能使用高采样率(如96kHz或192kHz)进行录制和编辑,但最终输出到CD或视频时可能需要转换为44.1kHz或48 kHz的采样率。采样率转换使得音频文件可以在不同设备和平台之间进行转换,从而提高音频处理的灵活性和兼容性。
减少混叠伪影和提高音质 在采样率转换过程中,适当的采样率转换算法可以减少混叠伪影和信号失真。例如,通过插值或降采样技术,可以保持音频信号的完整性,同时减少高频失真和噪声。高质量的采样率转换器(SRC)可以提供高保真度的音频输出,从而提高整体音质。
支持异步采样率转换 在某些情况下,音频系统可能需要在不同采样率之间进行异步转换。例如,异步采样率转换器(SRC)可以在不同采样率之间进行转换,而无需同步时钟信号,从而提高系统的灵活性和稳定性。这种异步转换方式可以减少抖动和时钟漂移的影响,从而提高音频信号的稳定性和可靠性。
硬件和软件实现的考虑 采样率转换可以在硬件或软件中实现。
硬件采样率转换器(SRC)通常具有更高的处理速度和更低的延迟,适合实时音频处理。
而软件采样率转换虽然灵活性高,但可能在实时处理中存在性能瓶颈。
因此,选择合适的采样率转换方法和算法对于音频系统的性能和音质至关重要。
在音频系统中,进行采样率转换(SRC)的主要目的是为了确保不同来源的音频信号能够在统一的采样率下进行处理和输出,从而提高音频系统的兼容性、同步性和音质。
通过采样率转换,可以避免因采样率不一致导致的音质问题,并提高音频处理的灵活性和兼容性。
因此,采样率转换在现代音频系统中具有重要的应用价值。