Python+AI 实战:搭建本地智能问答机器人
介绍如何使用 Python 和开源 AI 技术构建本地智能问答系统。采用 RAG 架构,结合 Sentence-BERT 进行语义检索,FAISS 作为向量数据库,Phi-3 模型生成答案。内容涵盖技术选型、系统架构设计、核心代码实现(知识库构建、向量化、LLM 集成)及效果演示。方案完全开源免费,支持离线运行,保护数据隐私,适合开发者快速上手并应用于实际项目。

介绍如何使用 Python 和开源 AI 技术构建本地智能问答系统。采用 RAG 架构,结合 Sentence-BERT 进行语义检索,FAISS 作为向量数据库,Phi-3 模型生成答案。内容涵盖技术选型、系统架构设计、核心代码实现(知识库构建、向量化、LLM 集成)及效果演示。方案完全开源免费,支持离线运行,保护数据隐私,适合开发者快速上手并应用于实际项目。

欢迎文末添加好友交流,共同进步!
'俺はモンキー・D・ルフィ。海贼王になる男だ!'

在本项目中,我们选用了以下技术栈:
sentence-transformers/all-MiniLM-L6-v2。这是一个轻量级的语义编码器,能够将文本转换为 384 维的向量表示,在速度和效果之间取得了良好平衡。FAISS(Facebook AI Similarity Search)。这是 Meta 开发的高效相似度搜索库,支持海量向量的快速检索。microsoft/Phi-3-mini-4k-instruct。Phi-3 是微软推出的轻量级开源模型,参数量约 38 亿,在消费级 GPU 甚至 CPU 上即可流畅运行,同时具备出色的指令遵循能力。LangChain 和 Transformers。LangChain 提供了便捷的 RAG 管道抽象,Transformers 则负责模型的加载和推理。为什么不直接调用 OpenAI、文心一言等云端 API?原因有三:首先,本地部署确保数据完全私密,适合处理敏感信息;其次,一次投入即可无限使用,没有按 Token 计费的成本压力;最后,离线环境下的可用性对于某些行业(如军工、金融、医疗)至关重要。当然,本地模型的性能在复杂推理任务上可能略逊于 GPT-4 等顶级模型,但对于知识库问答这类相对封闭的场景,已经能够取得令人满意的效果。
整个系统的数据流动可以概括为以下流程:
用户输入问题 → 文本预处理(分词、清洗) → 嵌入模型编码(Sentence-BERT) → 向量检索(FAISS 相似度搜索) → Top-K 文本块提取 → 上下文拼接(问题 + 相关知识) → LLM 生成答案(Phi-3/Mistral) → 返回回答给用户
知识库文档(Markdown/PDF) → 文档切分(文本块) → 批量嵌入编码 → 构建 FAISS 索引
这个架构设计充分考虑了系统的可扩展性。例如,可以轻松替换不同的嵌入模型或生成模型以适应特定需求;FAISS 索引支持增量更新,新增知识时无需重建整个索引;通过调整 top-k 值可以平衡召回率和精确度。在实际部署中,还可以添加查询改写、结果重排序、缓存机制等模块进一步提升性能。
现在让我们开始编写代码。整个项目将被组织为清晰的模块,包括知识库构建、向量检索、LLM 推理和主循环控制。所有代码均在 Python 3.10+ 环境中测试通过。
首先,我们需要安装必要的 Python 库。创建一个 requirements.txt 文件:
transformers>=4.35.0 sentence-transformers>=2.2.2 faiss-cpu>=1.7.4 langchain>=0.1.0 langchain-community>=0.0.10 torch>=2.0.0 accelerate>=0.25.0 pypdf>=3.17.0 python-dotenv>=1.0.0
然后执行安装命令:
pip install -r requirements.txt
如果你的机器有 NVIDIA GPU,建议安装 faiss-gpu 替代 faiss-cpu 以获得更快的检索速度。
假设我们有一份 Markdown 格式的产品手册作为知识库。以下代码展示了如何加载文档并进行切分:
# knowledge_base.py
import os
from typing import List, Dict
import re
class KnowledgeBase:
"""本地知识库管理类"""
def __init__(self, chunk_size: int = 500, chunk_overlap: int = 50):
"""初始化知识库
Args:
chunk_size: 文本块的最大字符数
chunk_overlap: 相邻文本块之间的重叠字符数
"""
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
self.documents = [] # 存储所有文本块
def load_markdown_file(self, file_path: str) -> str:
"""加载 Markdown 文件内容
Args:
file_path: 文件路径
Returns:
文件文本内容
"""
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
def split_text_into_chunks(self, text: str, source: str = "") -> List[Dict]:
"""将长文本切分为小块
Args:
text: 待切分的文本
source: 文本来源标识(如文件名)
Returns:
文本块列表,每个块包含内容和元数据
"""
chunks = []
start = 0
text_length = len(text)
while start < text_length:
end = start + .chunk_size
end < text_length:
delimiter [, , , , , , ]:
pos = text.rfind(delimiter, start, end)
pos != -:
end = pos + (delimiter)
chunk_text = text[start:end].strip()
chunk_text:
chunks.append({: chunk_text,
: {: source, : start, : end}})
start = end - .chunk_overlap
chunks
() -> :
filename os.listdir(directory):
filename.endswith(extension):
file_path = os.path.join(directory, filename)
()
content = .load_markdown_file(file_path)
chunks = .split_text_into_chunks(content, source=filename)
.documents.extend(chunks)
()
() -> []:
.documents
接下来,我们使用 Sentence-BERT 将文本块转换为向量,并构建 FAISS 索引:
# vector_store.py
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from typing import List, Dict, Tuple
class VectorStore:
"""向量存储和检索类"""
def __init__(self, model_name: str = 'sentence-transformers/all-MiniLM-L6-v2'):
"""初始化向量存储
Args:
model_name: 嵌入模型名称
"""
print(f"正在加载嵌入模型:{model_name}")
self.embedding_model = SentenceTransformer(model_name)
self.dimension = self.embedding_model.get_sentence_embedding_dimension()
self.index = None
self.documents = [] # 存储原始文档块
def build_index(self, documents: List[Dict]) -> None:
"""为文档构建 FAISS 索引
Args:
documents: 文档块列表
"""
self.documents = documents
print(f"正在为 {len(documents)} 个文档块生成嵌入向量...")
# 批量生成嵌入向量
texts = [doc['content'] for doc in documents]
embeddings = self.embedding_model.encode(
texts, show_progress_bar=, convert_to_numpy=)
.index = faiss.IndexFlatL2(.dimension)
.index.add(embeddings.astype())
()
() -> :
.index :
ValueError()
faiss.write_index(.index, index_path)
np.save(docs_path, .documents)
()
()
() -> :
.index = faiss.read_index(index_path)
.documents = np.load(docs_path, allow_pickle=).tolist()
()
() -> []:
.index :
ValueError()
query_vector = .embedding_model.encode([query], convert_to_numpy=).astype()
distances, indices = .index.search(query_vector, top_k)
results = []
i, (distance, idx) ((distances[], indices[])):
idx < (.documents):
result = .documents[idx].copy()
result[] = / ( + distance)
result[] = i +
results.append(result)
results
现在我们使用 Transformers 库加载 Phi-3 模型进行答案生成:
# llm_generator.py
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from typing import List, Dict
class LLMGenerator:
"""LLM 答案生成器"""
def __init__(self, model_name: str = 'microsoft/Phi-3-mini-4k-instruct', device: str = 'auto'):
"""初始化 LLM 生成器
Args:
model_name: 模型名称或本地路径
device: 运行设备 ('cuda', 'cpu', 'auto')
"""
if device == 'auto':
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
else:
self.device = device
print(f"正在加载模型:{model_name}")
print(f"使用设备:{self.device}")
self.tokenizer = AutoTokenizer.from_pretrained(
model_name, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(
model_name, torch_dtype=torch.float16 if self.device == 'cuda' else torch.float32,
device_map='auto' if self.device == 'cuda' else None,
trust_remote_code=)
.device == :
.model = .model.to(.device)
()
() -> :
context_text = .join([ i, doc (context_docs)])
prompt =
inputs = .tokenizer(prompt, return_tensors=, truncation=, max_length=).to(.device)
torch.no_grad():
outputs = .model.generate(**inputs, max_new_tokens=max_new_tokens, temperature=temperature, do_sample= temperature > , top_p=, repetition_penalty=, pad_token_id=.tokenizer.eos_token_id)
answer = .tokenizer.decode(outputs[][inputs[].shape[]:], skip_special_tokens=)
answer.strip()
最后,我们将所有组件组合起来,构建完整的问答系统:
# main.py
import os
from dotenv import load_dotenv
from knowledge_base import KnowledgeBase
from vector_store import VectorStore
from llm_generator import LLMGenerator
class QASystem:
"""智能问答系统主类"""
def __init__(self, knowledge_dir: str = './knowledge', index_path: str = './faiss_index.bin', docs_path: str = './documents.npy', rebuild_index: bool = False):
"""初始化问答系统
Args:
knowledge_dir: 知识库目录
index_path: FAISS 索引路径
docs_path: 文档存储路径
rebuild_index: 是否重建索引
"""
# 初始化组件
self.kb = KnowledgeBase(chunk_size=500, chunk_overlap=50)
self.vector_store = VectorStore()
self.llm = LLMGenerator()
# 加载或构建索引
if rebuild_index or not os.path.exists(index_path):
print("正在构建新索引...")
self.kb.load_directory(knowledge_dir)
documents = self.kb.get_documents()
self.vector_store.build_index(documents)
self.vector_store.save_index(index_path, docs_path)
else:
print("正在加载已有索引...")
.vector_store.load_index(index_path, docs_path)
( * )
()
( * )
() -> :
()
()
retrieved_docs = .vector_store.search(query, top_k=top_k)
()
()
answer = .llm.generate_answer(query, retrieved_docs)
{: query, : answer, : [{: doc[][:] + , : doc[], : doc[][]} doc retrieved_docs]}
():
( + * )
()
()
( * + )
:
:
query = ().strip()
query:
query.lower() [, , ]:
()
result = .ask(query)
()
()
()
()
i, source (result[], ):
()
()
()
KeyboardInterrupt:
()
Exception e:
()
()
__name__ == :
os.makedirs(, exist_ok=)
sample_md =
sample_path =
os.path.exists(sample_path):
(sample_path, , encoding=) f:
f.write(sample_md)
()
qa_system = QASystem(rebuild_index=)
qa_system.run_interactive()
完成上述代码后,运行 python main.py 即可启动问答系统。以下是几个实际问答示例:
示例 1:密码重置
您的问题:我忘记密码了怎么办?
答案:您可以通过访问公司内部系统登录页面,点击'忘记密码'链接来重置密码。输入您的员工邮箱地址,查收邮箱中的重置链接,点击链接后设置新密码即可。新密码需包含大小写字母、数字和特殊字符。如遇到问题,请联系 IT 部门热线:400-123-4567。
示例 2:年假申请
您的问题:年假怎么申请?
答案:员工申请年假需提前至少 3 个工作日提交申请。请登录 HR 系统的'假期管理'模块,选择年假类型并填写起止日期和工作交接安排,提交后等待直属上级审批。注意年假最小单位为 0.5 天,需在当年 12 月 31 日前使用完毕,且法定节假日前后 3 天内不得申请。
示例 3:超出知识库范围
您的问题:公司有股票期权计划吗?
答案:抱歉,我无法从现有资料中找到关于公司股票期权计划的信息。建议您咨询 HR 部门或查阅公司内部的相关政策文档。您可以通过内线 6666 或发送邮件至 hr@company.com 联系 HR 部门。
从示例中可以看出,系统不仅能够准确检索到相关信息,还能生成自然流畅的回答。当问题超出知识库范围时,系统也能优雅地处理,不会胡编乱造。
1. 模型幻觉
虽然 RAG 架构大大降低了幻觉风险,但在某些情况下仍可能出现。解决方法包括:
2. 上下文截断
当检索到的文档总长度超过模型上下文窗口时,需要截断内容。优化策略:
3. 检索精度不足
某些复杂问题可能无法准确检索到相关文档。改进方案:
bge-large-zh-v1.5)缓存机制
对于重复问题,可以缓存答案以加快响应速度。使用 LRU 缓存策略,存储问题 - 答案对,当相同问题再次出现时直接返回缓存结果。
前端集成
将系统封装为 Web API,使用 Flask 或 FastAPI 提供 RESTful 接口,然后构建简洁的前端界面。这样可以让更多用户同时使用,也便于集成到现有系统中。
多模态支持
扩展系统以支持图片、表格等多模态内容。例如,可以使用 OCR 技术提取 PDF 中的图片文字,或使用表格解析器处理结构化数据。
持续学习
实现反馈机制,允许用户标记答案的正确性。收集这些反馈数据,可以用于知识库的更新和模型的微调,形成持续改进的闭环。
下一步行动建议:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online