在 Ubuntu 22.04 上搭建 TFTP 服务器和 NFS 服务器是嵌入式开发(如 Linux 内核调试、根文件系统挂载)的经典组合。以下是详细的搭建步骤:
TFTP(简单文件传输协议)常用于在开发板启动时下载 Linux 内核镜像(uImage/zImage)和设备树文件(.dtb)。
打开终端,执行以下命令安装 TFTP 服务端和客户端:
Bash
sudo apt update
sudo apt install tftpd-hpa tftp-hpa
创建一个用于存放传输文件的目录,并赋予最高权限,确保开发板可以正常读写:
Bash
sudo mkdir -p /home/tftpboot
sudo chmod 777 /home/tftpboot
编辑 TFTP 的配置文件 /etc/default/tftpd-hpa:
Bash
sudo nano /etc/default/tftpd-hpa
将内容修改为以下配置(主要修改 TFTP_DIRECTORY 和 TFTP_OPTIONS):
Plaintext
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/home/tftpboot"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure --create"
提示:
--create参数允许客户端在服务器上创建新文件(即支持上传)。
保存退出后,重启 TFTP 服务并将其设置为开机自启:
Bash
sudo systemctl restart tftpd-hpa
sudo systemctl enable tftpd-hpa
检查服务状态,确保显示 active (running):
Bash
sudo systemctl status tftpd-hpa
你可以通过本地回环测试来验证是否搭建成功:
Bash
echo "TFTP Test File" > /home/tftpboot/test.txt
cd /tmp
tftp 127.0.0.1
# 进入 tftp 命令行后输入:
get test.txt
quit
# 查看 /tmp 目录下是否成功获取文件
cat test.txt
NFS(网络文件系统)常用于将 Ubuntu 上的某个目录(如根文件系统 rootfs)挂载到开发板上,方便直接修改和调试代码而无需反复烧录。
Bash
sudo apt install nfs-kernel-server
创建一个用于挂载的文件目录:
Bash
sudo mkdir -p /home/nfsroot
sudo chmod 777 /home/nfsroot
编辑 /etc/exports 文件来指定允许访问的客户端和权限:
Bash
sudo nano /etc/exports
在文件末尾添加以下内容(根据你的网络环境调整):
Plaintext
/home/nfsroot *(rw,sync,no_root_squash,no_subtree_check)
参数说明:
*:代表允许所有的 IP 访问(在局域网开发环境中很方便。如果为了安全,可以写具体网段如 119.168.1.0/24)。
rw:读写权限。
sync:资料同步写入内存与硬盘。
no_root_squash:如果客户端使用的是 root 用户,则对该共享目录也拥有 root 权限(嵌入式开发必备)。
no_subtree_check:不检查子目录权限,提高性能。
避坑指南:Ubuntu 22.04 默认关闭了 NFS v2 和 v3,只开启了 NFS v4。但是很多老旧的开发板 U-Boot 或内核只支持 NFS v2 或 v3。如果你在挂载时遇到
Protocol not supported或一直连不上,请按以下步骤开启 v3 支持。
编辑 /etc/default/nfs-kernel-server:
Bash
sudo nano /etc/default/nfs-kernel-server
修改或添加以下内容,确保不禁用 v3:
Plaintext
RPCNFSDOPTS=""
编辑 /etc/nfs.conf:
Bash
sudo nano /etc/nfs.conf
找到 [nfsd] 标签,取消注释并修改 vers3 如下:
Plaintext
[nfsd]
vers3=y
配置完成后,重启相关服务并导出共享目录:
Bash
sudo systemctl restart rpcbind
sudo systemctl restart nfs-kernel-server
sudo systemctl enable nfs-kernel-server
使用以下命令查看当前生效的 NFS 共享目录:
Bash
showmount -e
如果输出显示 /home/nfsroot *,说明配置成功。
你可以尝试在本地挂载自己,验证服务是否正常:
Bash
sudo mount -t nfs -o nfsvers=3 127.0.0.1:/home/nfsroot /mnt
# 检查是否成功挂载,随后卸载
ls /mnt
sudo umount /mnt
防火墙问题:如果开发板依然无法连接,建议直接关闭 Ubuntu 的防火墙(仅限本地安全开发环境):
Bash
sudo ufw disable
IP 地址在同一网段:请确保你的 Ubuntu 主机、Windows 宿主机(如果是虚拟机的话)以及开发板的 IP 地址处于同一个网段,且能够互相 ping 通。
情况一:在终端中直接设置(生效)
export PATH=$PATH:/new/path
这是在当前 shell(父进程)中直接修改 PATH 后续所有命令都继承这个新 PATH
情况二:在脚本中设置(不生效)
# setpath.sh
export PATH=$PATH:/new/path
echo $PATH # 脚本内能看到新 PATH
./setpath.sh # 或 bash setpath.sh
echo $PATH # 终端中 PATH 未改变!
为什么?
当你运行 ./setpath.sh 时,系统会启动一个子 shell 来执行脚本 脚本中的 export 只修改了子 shell 的环境变量 脚本执行完毕后,子 shell 退出,修改不会回传给父 shell(你的终端)
📌 Unix/Linux 的设计原则:子进程不能修改父进程的环境变量(安全机制)
✅ 正确解决方案
source setpath.sh#
或简写
. setpath.sh
原理:
如果希望每次打开终端都生效,将 PATH 添加到配置文件:
对当前用户:
echo 'export PATH="$PATH:/opt/myapp/bin"' >> ~/.bashrc
# 或 ~/.zshrc(如果你用 zsh)
然后重载配置:
source ~/.bashrc
对所有用户(需 root 权限):
sudo echo 'export PATH="$PATH:/opt/myapp/bin"' >> /etc/environment
在“仅用于自己查询、不上传任何信息到云端”的前提下,构建本地知识库的核心目标是:完全离线、数据安全、高效检索。下面是一个系统化的构建方案,涵盖工具选择、数据处理、检索方式等关键环节:
[本地文档/数据源]
↓
[文档解析 & 向量化] → [本地向量数据库]
↓
[本地大模型(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:








![]()




