尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
AI知识库系统实战:Python+FAISS+text2vec实现语义问答
简介这份人工智能知识库系统源码包面向需要搭建知识管理或信息检索应用的开发者提供了基于网页的完整实现可解决知识条目的存储、分类和快速查询等问题。压缩包共22个文件其中10个HTML页面负责界面展示6个Python脚本承担后端逻辑4个缓存文件由解释器生成另外还有1个数据库和1个说明文档整体仅53KB结构紧凑适合学习轻量级知识管理系统的设计思路。资源中包含界面模板、数据库连接、配置初始化、系统入口等后端模块并且涵盖用户注册登录、知识文档管理、问答检索等典型功能页面支持本地部署后直接运行用户可通过网页完成知识内容的录入与管理。目前已有66人学习下载可作为入门参考。对于想了解Web应用或知识库检索机制的读者该源码提供了清晰的目录划分和基础代码便于二次开发也可作为课程设计或毕业设计的起步素材。1. AI知识库系统是干什么的先把你手头文档变成能问答的资产「AI知识库系统」这个名字看起来很大但拆开看就是三件事把散落的文档集中管理用语义而不是关键词去检索内容再把检索到的内容交给大模型组织成答案。这个源码包就是用 Python 把这三件事完整实现了一遍跑通它只需要一台普通电脑、一套 Python 环境和几行命令不需要 GPU 服务器。适合谁呢你手头有一堆 Markdown/TXT 文档想给团队搭一个内部问答工具或者正在做检索增强生成RAG入门想找一份不是玩具代码、能直接改的项目——这个源码包刚好在你想要的区间。下面我从技术选型、核心实现到运行改造把整个项目拆开讲一遍。2. 技术选型与目录结构为什么是 FAISS text2vec 而不是别的组合2.1 技术选型四个关键组件背后的取舍一个 AI 知识库系统真正拆开要决定的事很多文档存哪里、怎么分块、用什么模型转向量、向量存哪、问答用哪个大模型接口。我先说结论再逐个解释。嵌入模型选的是 text2vec-base-chinese。这个模型参数规模不大CPU 上就能跑输出向量是 768 维中文语义质量在同类开源小模型里算稳的。如果你换成英文模型或者别的中文模型向量维度会变后面存储层的维度配置必须跟着改这是整个项目里最容易翻车的一个耦合点。向量存储用的是 FAISS 的 IndexFlatIP没有上 Chroma 这类带独立服务的向量数据库。原因很直接知识库的量级还没大到需要独立服务IndexFlatIP 做全量暴力检索在几万条 chunk 的规模下毫秒级返回。IndexFlatIP 是内积索引配合归一化后的向量算出来就是余弦相似度——这是关键词检索做不到的语义匹配。后端用 FastAPI Uvicorn。异步友好上传文件、并发请求处理起来顺手而且 FastAPI 自带的 /docs 页面能在浏览器里直接调试接口省了不少事。文档格式只支持 Markdown/TXT。不要小看这个限制它替我们省掉了解析 PDF 的复杂度。PDF 在真实场景里问题很多表格、扫描件、页眉页脚都会干扰切分质量等核心链路跑通之后再按需接入就行。组件选择理由嵌入模型text2vec-base-chinese中文语义质量好CPU 可跑768 维向量库FAISS IndexFlatIP轻量无服务小规模知识库毫秒级返回后端框架FastAPI异步友好自带调试文档文档格式Markdown/TXT切分干净避免 PDF 解析复杂2.2 目录结构每个文件对应什么职责代码组织遵循的是一个很常见的分层API 层 - 服务层 - 存储层。这样拆的好处是你换一个向量库或者换一个嵌入模型改动范围被限制在某一个目录里不会牵一发动全身。ai_knowledge_base/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 应用入口 │ ├── api/ │ │ └── routes.py # /upload /ask /health 路由 │ ├── services/ │ │ ├── loader.py # 文档读取 │ │ ├── splitter.py # 文本分块 │ │ ├── embedder.py # 向量化 │ │ ├── retriever.py # 检索 │ │ ├── chat.py # 问答组装 │ │ └── llm_client.py # 大模型 API 通用客户端 │ ├── storage/ │ │ └── vector_store.py # FAISS 封装的增删查存 │ └── core/ │ ├── config.py # 全局配置 │ └── models.py # Pydantic 数据模型 ├── data/ │ ├── docs/ # 存放原始文档 │ └── index/ # 索引文件输出目录 ├── scripts/ │ └── build_index.py # 一次性构建索引脚本 ├── requirements.txt ├── run.py └── README.md跑到最后你会发现真正决定知识库好不好用的不是大模型那个环节而是 services 里 loader、splitter、retriever 这三个文件。它们决定了喂给大模型的上下文干不干净、准不准。core/config.py 是整个项目的开关中枢。我把嵌入模型名、向量维度、分块大小、重叠长度、TopK、大模型 API 地址和密钥全放在这里用环境变量做覆盖。你拿到源码后80% 的定制需求都是改这个文件。3. 核心实现拆解从加载文档到检索出答案的完整链路3.1 文档加载与分块把整本手册拆成可检索的卡片第一步是把磁盘上的文档读进来。loader 的做法很朴素遍历 data/docs 目录读取所有 .md 和 .txt每条文档记录下文件名和正文内容。# app/services/loader.py from pathlib import Path def load_documents(docs_dir: str) - list[dict]: 读取 docs 目录下的 .md / .txt 文件返回文档列表 docs [] for ext in (*.md, *.txt): for file in Path(docs_dir).glob(ext): content file.read_text(encodingutf-8) if len(content.strip()) 20: # 过滤掉空文件和只有几个字的占位文件 continue docs.append({source: file.name, content: content}) return docs逻辑上注意两点一是强制 UTF-8 读取Windows 环境下容易出现编码问题二是过滤短文档避免后面把无意义的碎片也向量化。返回的 source 字段会一路带进检索结果展示答案时能告诉用户「这段内容来自哪个文件」。分块是知识库质量的第一道关口。我采用的是按空行分段再合并的策略这样能尽量保住语义的完整性如果不分段直接按字符硬切很容易把一句话劈成两半。# app/services/splitter.py import re def split_by_paragraph(content: str, chunk_size: int 512, overlap: int 64) - list[str]: 按空行分段再把段落拼成不超过 chunk_size 的块块之间保留 overlap 字符重叠 paragraphs [p.strip() for p in re.split(r\n\s*\n, content) if p.strip()] chunks [] current for para in paragraphs: if len(current) len(para) chunk_size and current: chunks.append(current) # 取当前块末尾 overlap 个字符作为下一块的上下文衔接 current current[-overlap:] if overlap len(current) else current current (current \n para).strip() if current: chunks.append(current) return chunks这里 chunk_size 默认 512 字符overlap 默认 64。overlap 的作用是让相邻两个块之间有一小段重复内容检索时不会因为切分边界把完整上下文丢掉。为什么默认 512太大了 embedding 质量会下降太小了单个块信息量不够512 是中文场景下比较折中的值。3.2 向量化与存储让语义变成能计算的距离分块完成之后每一段文本要转成向量。Embedder 是对 sentence-transformers 库的薄封装核心参数是 normalize_embeddingsTrue。# app/services/embedder.py from sentence_transformers import SentenceTransformer class Embedder: def __init__(self, model_name: str): self.model SentenceTransformer(model_name) def embed(self, texts: list[str]) - list[list[float]]: 批量向量化返回 list[list[float]] vectors self.model.encode(texts, normalize_embeddingsTrue) return vectors.tolist()normalize_embeddingsTrue 这行很关键。它把向量归一化成单位向量配合 FAISS 的 IndexFlatIP 内积索引计算结果就等于余弦相似度分数越接近 1 表示语义越接近。VectorStore 负责 FAISS 索引的读写。这里有一个容易忽略的设计FAISS 只存向量不存原始文本所以必须单独把 chunks 和 sources 序列化到磁盘。# app/storage/vector_store.py import pickle import faiss import numpy as np class VectorStore: def __init__(self, dim: int): self.dim dim self.index faiss.IndexFlatIP(dim) self.chunks [] self.sources [] def add(self, vectors: list[list[float]], chunks: list[str], sources: list[str]): if len(vectors) 0: return self.index.add(np.array(vectors, dtypefloat32)) self.chunks.extend(chunks) self.sources.extend(sources) def search(self, query_vec: list[float], top_k: int 5) - list[dict]: scores, indices self.index.search( np.array([query_vec], dtypefloat32), top_k ) results [] for j, idx in enumerate(indices[0]): if idx 0: continue results.append({ chunk: self.chunks[idx], source: self.sources[idx], score: float(scores[0][j]), }) return results def save(self, index_path: str): faiss.write_index(self.index, index_path) meta_path index_path .meta with open(meta_path, wb) as f: pickle.dump({chunks: self.chunks, sources: self.sources}, f) def load(self, index_path: str): self.index faiss.read_index(index_path) meta_path index_path .meta with open(meta_path, rb) as f: meta pickle.load(f) self.chunks meta[chunks] self.sources meta[sources]看出来了吗save 的时候 FAISS 索引写进 .index 文件raw 文本写进 .meta 文件。加载的时候两个都要读回来缺一个服务就起不来。这是跑这个项目最容易踩的坑之一——很多人以为只要 .index 就够了。3.3 检索与问答从问题到答案的完整组装Retriever 的逻辑很简单把问题向量化拿去 FAISS 里搜 top_k返回带来源的结果。# app/services/retriever.py class Retriever: def __init__(self, store: VectorStore, embedder: Embedder): self.store store self.embedder embedder def search(self, question: str, top_k: int 5) - list[dict]: vec self.embedder.embed([question])[0] hits self.store.search(vec, top_k) return hitstop_k 默认 5这个值在小知识库上表现均衡如果你的文档量大可以考虑调到 8 甚至 10但大模型能吃到的上下文有限太多了反而会引入噪音。问答组装是决定输出质量的一环。ChatService 把检索结果拼进 Prompt明确要求模型只依据参考资料回答# app/services/chat.py class ChatService: def __init__(self, retriever: Retriever, llm_client): self.retriever retriever self.llm llm_client def answer(self, question: str, top_k: int 5) - str: hits self.retriever.search(question, top_k) if not hits: return 知识库里暂时没有相关内容 context \n\n.join( f【来源{h[source]}】{h[chunk]} for h in hits ) prompt ( 你是企业内部知识库助手。请只根据下面参考资料回答 不要编造。参考资料\n context \n\n问题 question ) return self.llm.chat(prompt)Prompt 结构是「角色设定 限定范围 参考资料 问题」四段式。为什么这么写不限定参考范围模型就会开始自由发挥不标来源你无法追溯答案可信度。最后是 LLMClient。这个客户端走的是当前大多数大模型厂商通用的 /chat/completions 接口格式换服务商只需要改三个配置# app/services/llm_client.py import requests class LLMClient: def __init__(self, api_base: str, api_key: str, model: str): self.api_base api_base.rstrip(/) self.api_key api_key self.model model def chat(self, prompt: str) - str: resp requests.post( f{self.api_base}/chat/completions, headers{Authorization: fBearer {self.api_key}}, json{ model: self.model, messages: [{role: user, content: prompt}], temperature: 0.2, }, timeout60, ) resp.raise_for_status() return resp.json()[choices][0][message][content]temperature 设成 0.2是刻意压低随机性——知识库问答要的是稳定和可复现不是创意写作。timeout60 是给长回答留的余量如果你用的是本地模型可以适当缩小。4. 本地跑通全流程依赖、索引、服务三步到位4.1 环境准备与依赖安装先准备 Python 3.10 以上的虚拟环境然后安装依赖。requirements.txt 内容如下fastapi uvicorn[standard] sentence-transformers faiss-cpu numpy requests python-multipart版本号我故意没锁。原因很简单AI 生态更新太快锁死了反而容易装不上。建议你在 x86_64 的 Linux 或 macOS 上先装 faiss-cpu再装 sentence-transformers这两个包的顺序有讲究——如果先装 sentence-transformers 再装 faiss-cpu有时会因为 torch 的 numpy 版本冲突导致报错。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt装完可以用一行命令验证python -c from sentence_transformers import SentenceTransformer; m SentenceTransformer(shibing624/text2vec-base-chinese); print(m.encode([测试]).shape)能输出 [1, 768] 就说明基础环境没问题。如果卡在这一步多半是网络问题导致模型下载不了下面避坑章节会细说。4.2 构建知识库索引先往 data/docs 里放几篇测试文档然后运行构建脚本# scripts/build_index.py from app.core.config import settings from app.services.loader import load_documents from app.services.splitter import split_by_paragraph from app.services.embedder import Embedder from app.storage.vector_store import VectorStore def main(): docs load_documents(settings.DOCS_DIR) embedder Embedder(settings.EMBED_MODEL) store VectorStore(settings.VECTOR_DIM) all_chunks, all_sources, all_vectors [], [], [] for doc in docs: chunks split_by_paragraph( doc[content], settings.CHUNK_SIZE, settings.CHUNK_OVERLAP, ) if not chunks: continue vectors embedder.embed(chunks) all_chunks.extend(chunks) all_sources.extend([doc[source]] * len(chunks)) all_vectors.extend(vectors) print(f{doc[source]}: {len(chunks)} chunks) store.add(all_vectors, all_chunks, all_sources) store.save(settings.INDEX_PATH) print(f索引完成共 {len(all_chunks)} 个 chunk) if __name__ __main__: main()执行python scripts/build_index.py整个过程耗时的瓶颈在 embedding 阶段。text2vec 在纯 CPU 上大约每分钟处理几百到一千个文本块你如果有十几篇长文档等个几分钟很正常。第一次跑会更慢因为要下载模型文件到本地缓存。跑完之后 data/index 目录下会出现两个文件faiss.index 和 faiss.index.meta。记住它们是成对存在的拷贝目录时两个都要带上。4.3 启动服务并验证run.py 做了两个命令入口构建索引和启动服务。# run.py import argparse def main(): parser argparse.ArgumentParser() parser.add_argument(--build-index, actionstore_true, help构建索引) parser.add_argument(--serve, actionstore_true, help启动服务) args parser.parse_args() if args.build_index: from scripts.build_index import main as build build() elif args.serve: import uvicorn uvicorn.run(app.main:app, host0.0.0.0, port8000) if __name__ __main__: main()启动服务python run.py --serve在 app/api/routes.py 里模块级初始化了 VectorStore、Embedder、Retriever 和 ChatService加载已有索引后对外暴露三个接口。# app/api/routes.py from fastapi import APIRouter, UploadFile from app.core.config import settings from app.services.chat import ChatService from app.services.retriever import Retriever from app.services.embedder import Embedder from app.services.splitter import split_by_paragraph from app.storage.vector_store import VectorStore router APIRouter() store VectorStore(settings.VECTOR_DIM) store.load(settings.INDEX_PATH) embedder Embedder(settings.EMBED_MODEL) retriever Retriever(store, embedder) chat_service ChatService(retriever, get_llm_client()) router.post(/upload) async def upload(file: UploadFile): content (await file.read()).decode(utf-8) chunks split_by_paragraph(content, settings.CHUNK_SIZE, settings.CHUNK_OVERLAP) vectors embedder.embed(chunks) store.add(vectors, chunks, [file.filename] * len(chunks)) store.save(settings.INDEX_PATH) return {chunks: len(chunks)} router.post(/ask) async def ask(question: str): return {answer: chat_service.answer(question)} router.get(/health) async def health(): return {status: ok}启动后直接在浏览器打开 http://localhost:8000/docs 用 Swagger 页面调接口最方便。也可以用 curl 验证curl -X POST http://localhost:8000/api/ask \ -H Content-Type: application/json \ -d {question: 数据库连接超时怎么排查}如果能返回一段带来源的答案说明整条链路已经通了。如果 /ask 返回的是「知识库里暂时没有相关内容」不要急着怪代码先检查索引里到底有没有数据再检查问题表述和文档内容是不是对得上。5. 避坑排查跑 AI 知识库容易翻车的五个地方5.1 现象启动时报 Index dimension mismatch构建索引一切正常但启动服务时 FAISS 直接报错说查询向量维度和索引维度不一致。原因config.py 里的 VECTOR_DIM 默认是 768对应 text2vec-base-chinese 的输出维度。如果你换了嵌入模型比如换成一个输出 512 维的模型但没改 VECTOR_DIMFAISS 创建索引时用的是旧维度检索时就炸了。解决换模型之前先确认新模型的输出维度。最简单的方法是在 Python 里跑一次 embedding打印 shape。然后在 config 里同步修改。还有一个小细节改维度之后旧的 .index 文件也不能用了需要删掉重建。5.2 现象检索结果和问题完全对不上TOP5 返回的文本块看起来和你问的问题八竿子打不着或者像随机抽取的。原因八成出在分块策略上。如果你图省事直接按字符长度切块语义单元被切断的概率极高另外overlap 要是设成 0块与块之间没有任何承接检索时容易漏掉跨边界的信息。解决用 split_by_paragraph 这种按段落聚合的策略并把 overlap 至少设到 50 字符。中文不像英文按空格分词字符切分天然容易把词和短语砍断overlap 是补偿这种断裂最直接的手段。5.3 现象首次构建索引卡在模型加载运行 build_index 看起来像死机了终端停在一行日志上十几分钟没动静。原因sentence-transformers 首次使用需要下载模型权重到本地缓存这一步受网络环境影响很大。卡住不一定是死了是下载慢。解决最稳妥的办法是在网络好的机器上先跑一次 embedding让模型下载完成然后把缓存目录整个拷贝到目标机器或者在传参时直接用绝对路径指向本地模型目录跳过在线拉取。从那以后我不管在哪个环境跑都会先检查本地缓存不会再傻等。5.4 现象答案全来自同一篇文档问题问得很具体结果返回的 TOP5 全是同一篇长文档的不同切片其他文档的信息完全没被召回。原因长文档被切成多个 chunk 后彼此语义相似度高全部挤占了 top_k 的名额。知识库里要是有几篇超长文档这种现象尤其明显。解决retrieve 之后对结果按 source 做去重限制同一个来源最多出现两个 chunk。还有一种思路是用 MMR最大边际相关性对候选结果做多样性排序兼顾相关性和信息冗余。对大部分场景简单的按 source 限流已经够用了。5.5 现象服务重启后新上传的知识丢失通过 /upload 接口传了文档当时问答正常重启服务后再问新文档相关的问题全部答不上来。原因/upload 只往内存里的 FAISS 索引加了向量没有触发持久化。虽然路由里调了 store.save但如果你改了代码或者没走这个分支重启后索引就会回到构建时的状态。解决确认上传逻辑里必须调用 store.save我在 routes.py 里保留了这行。更稳妥的做法是每次上传后检查一下 data/index 下 .meta 文件的修改时间确认真的写进去了。另外正式使用时建议把 upload 接口做成写一个临时目录再触发重建索引避免并发上传时内存索引和磁盘文件不同步。6. 二次开发实战三个方向把知识库接进自己的业务6.1 替换成大模型 API源码默认的 LLMClient 直接可用你只需要在 config.py 里填上 api_base、api_key、model 三项。如果你用的是本地开源模型也可以部署一个兼容 /chat/completions 的服务然后把地址指向 localhost 端口业务代码一行都不用动。6.2 给知识库加业务权限过滤在 VectorStore 里给每个 chunk 增加 metadata 字段比如部门、权限级别。检索时先按问题携带的上下文过滤 metadata再进 FAISS 计算相似度。这块我有一个习惯metadata 过滤必须发生在向量检索之前否则结果集会掺杂大量无权限内容。6.3 按 Markdown 标题自适应分块默认分块策略按空行聚合已经能覆盖大多数场景。如果你的文档结构很强比如操作手册、故障手册可以改 splitter 按#标题层级切分保证一个块就是一段完整的主题。实现思路是先用正则匹配标题位置再决定段落归属哪个块。我刚做这个项目第二版的时候整个服务上线第二天就被吐槽检索不准查到最后就是分块太粗暴、metadata 也没过滤干净。从那以后我每次改知识库都会先准备一组真实问题和对应文档离线跑一遍 TOP5 召回率确认准了再放出去用。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

PCS储能双向变流器Simulink仿真功率控制环调试经验与避坑指南

PCS储能双向变流器Simulink仿真功率控制环调试经验与避坑指南

这篇文章不会去复述课本上的原理,而是从我实际调试PCS储能双向变流器Simulink仿真模型的经历出发,拆开功率控制环的每一层,把那些让人头秃的配合问题摊开来看。如果你也在为功率环响应、电流内环发散、模式切换冲击这类问题挠头,这…

📅 2026/10/21 8:42:10
国标GB28181+EasyCVR+视频质量诊断:构建智慧园区全域可视体系

国标GB28181+EasyCVR+视频质量诊断:构建智慧园区全域可视体系

很多年前第一次接触智慧园区项目时,我觉得视频监控系统是整个工程里最"简单"的部分——无非就是布点、装摄像头、上平台、能看录像。直到某园区项目进入运维期,业主那边每天接到一堆"某某摄像头黑屏""画面模糊看不清"&quo…

📅 2026/10/21 8:37:00
agent-skills:智能体能力的工程化封装与调度协议

agent-skills:智能体能力的工程化封装与调度协议

1. 项目概述:一个被严重低估的“技能容器”概念“agent-skills”这个词组乍看像技术文档里的缩写,实则藏着当前智能体开发中最关键却最常被跳过的底层设计逻辑。它不是某个具体工具、框架或开源库的名字,而是一套面向智能体(Agent…

📅 2026/10/21 8:37:00
MORE NEWS

更多资讯

📰

关联规则与协同过滤:从原理到选型的推荐系统实战指南

1. 先搞清楚两件事:一个算“一起买”,一个算“像你的人”1.1 关联规则是找“购物篮里的规律”关联规则要做的事情,说人话就是:从一堆历史订单里找出“哪些东西经常被同时买”。这个思路最早来自线下零售的购物篮分析。顾客结账时收…

📰

分布式系统中进程组织的四大核心设计原则

1. 项目概述:为什么“进程组织”是分布式系统里最被低估的硬功夫 “分布式系统的进程组织”——这八个字乍看平平无奇,像教科书目录里一个不起眼的小节,但在我带过十几个中大型后端系统落地的实操经历里,它从来不是理论题&#xf…

📰

关联规则 vs 协同过滤:本质区别、场景选择与实战避坑指南

推荐系统里有个问题经常被拿出来讨论:关联规则和协同过滤,到底有什么区别?很多人做了几年推荐,用协同过滤做过不少项目,但让他解释关联规则和协同过滤的本质区别,却说不清楚。 我在实践中也栽过跟头。早期…

📰

分布式系统进程组织:五层实战架构与生产级落地

1. 项目概述:为什么“分布式系统的进程组织”不是个技术名词,而是一道生存考题 你打开一个电商App下单,后台可能同时跑着库存服务、订单服务、支付网关、风控引擎、物流跟踪、用户画像计算……这些服务未必在一台机器上,甚至未必在…

📰

从技术方案到视觉呈现:系统架构图设计方法论

写技术方案的人都知道,图和字往往是两套语言。很多团队里,文字方案写得清清楚楚,一到画系统架构图、业务流程图、数据流图,就开始凭感觉发挥:有人喜欢把整面墙的信息塞进一张图,有人用二十种颜色标"重…

📰

Gazebo仿真环境搭建实战:从物理引擎到多机器人协同调优

2. 为什么第三篇才聊仿真环境搭建 说实话,前两篇我主要讲了模型导入、关节配置、传感器插件这些基础操作。但越往后做越发现,很多问题根本不出在模型本身,而是出在仿真环境上。环境没搭好,后面调控制器、跑导航、做视觉识别&#…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬