ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零搭建个人AI知识库:RAG+向量数据库+Ollama实战

从零搭建个人AI知识库:RAG+向量数据库+Ollama实战 说实话我第一次在网上搜“ai知识库”的时候看到的全是那种企业级、RAG检索增强生成架构、向量数据库、千亿参数模型部署动辄就是“一站式解决方案”。看完我的感受是太远了跟我没关系。但我想搞知识库的初衷很简单——我手上积了一堆Markdown笔记、产品分析、竞品调研、需求文档散落在各个文件夹里。每次想找某个结论都要翻半天每次想写新文档又总觉得自己以前写过一个类似的东西就是搜不到。我需要的是一个能让我用大白话提问然后把我自己写过的内容原原本本回答给我的东西。说白了我要给我的资料配一个“会帮我翻笔记的助理”。所以我花了一个周末用最粗糙、最不高级、连代码都写得抠抠搜搜的方式搭了一个属于我自己的“AI产品经理知识库”。它性能不极致、界面也丑、内核甚至称不上“智能”但它非常管用。这篇文章不聊高大上的架构就聊聊我是怎么从零折腾出来这个“粗糙版”的以及中途踩了哪些坑、哪些地方我劝你们直接抄作业就行。1. 知识库到底解决了我什么问题1.1 散落资料的集中检索我以前找资料是这个状态先在本地文件夹里一层层点开记不清目录就按名字猜猜不中就全盘搜索。搜索出来一堆同名文件之后再逐个打开看措辞才能确认“哦这个才是当时那个结论”。一套下来二十分钟过去了灵感早没了。现在我的用法是直接在对话界面问“我上次分析过视频号和小红书的推荐机制差异结论是什么”知识库会把我写过的相关段落原样捞出来标清楚出处我扫一眼就能把资料定位到具体内容。这比任何目录归类都直观因为我真正记得的不是“文件路径”而是“我当时想表达的观点”。1.2 新写作时的素材复用作为经常要写分析文档的产品经理最痛苦的不是没有素材而是素材太多没法快速知道哪些能整合。有了知识库之后我会在动手前先问几个具体问题比如“我有没有总结过登录注册流程的改进方案”让系统把相关段落全部调出来。然后我就在这些旧观点的基础上做增量修改而不是每次从零开始重新组织语言。这个习惯形成之后写作效率提升非常明显而且文档之间的关联性也自然增强了。1.3 解决“我记得我写过但我找不到”的焦虑这是一件小事但对产品经理来说很要命。你在评审会上被问到一个功能细节你脑子里闪过“这个我之前踩过坑”却拿不出证据。有了知识库之后我已经不止一次在讨论现场打开手机搜索然后直接把当时的笔记原文亮出来。那一刻的感觉怎么说呢就像出门总是多带了一把伞平时觉得重下雨时才知道多重要。2. 到底选什么技术方案我的纠结过程2.1 为啥PASS掉大而全的框架一开始我也看了那些成熟的RAG框架像什么Dify、FastGPT、LangChain全家桶甚至考虑过直接用阿里云/腾讯云的向量数据库服务。它们确实强大功能丰富界面也好看对非技术背景的人特别友好。但我纠结之后还是放弃了学习成本高。 这些平台有自己的概念体系要弄懂工作流、节点、应用编排我得先学一套新知识而不是先把我的知识库用起来。环境部署重。 哪怕是最轻量的Docker化部署也要一台一直开着的服务器或NAS。我手头只有一台普通Windows笔记本跑大一点的模型已经很吃力了再拖一个后台服务散热风扇怕是要起飞。和我的数据隐私诉求冲突。 把资料传到云端知识库服务上虽然方便但我的笔记里可能有未公开的产品策略放在第三方平台总感觉怪怪的。我当时给自己的定位是先跑通一条最简链路让我的笔记能被“检索”到哪怕环节粗糙一点都行。等链路通了再去研究花哨的优化。2.2 我选的“粗糙”组合是什么既然要本地、免费、轻量我把思路分成了三个环节文本抽取与切块用Python读入我所有的Markdown和TXT文件简单清洗后按标题和段落切成小块。向量化与存储用一个本地的小型向量库把每块文本转成向量并存储。我选了Chroma因为它就是一个Python包pip装完就能用数据存在本地文件夹零额外部署成本。检索与问答本地装一个量化版的大语言模型Qwen2.5-7B-Instruct的GGUF量化版本用Ollama加载然后写一小段Python把用户提问转成向量先检索出最相关的若干块文本再连同问题一起扔给大模型做总结回答。这套组合里没有FastAPI、没有Docker、没有Milvus、没有企业级架构就是三个环节各用一个“土办法”串起来。提示如果你对编程完全零基础这篇文章后面的代码思路可能还是要找人帮你稍微调一下但整体逻辑和参数选择照着抄基本能跑。2.3 为什么向量检索必须靠自己实现你可能会问为什么不直接用Ollama聊天把全部资料塞进上下文让它总结因为大模型的上下文窗口有限且还有“大海捞针”效应——资料太多的时候模型会忽略掉中间部分的关键信息。向量检索做的事就是帮你“大海捞针”的缩小范围先找出跟问题最相关的2~3千字内容再让模型只看这些内容。模型看得少反而答得准。这个思路理解之后你会发现整个知识库的搭建逻辑其实就三步找得到检索、看得进上下文限制、答得出生成回答。任何花哨的框架最后也是干这三件事只是人家把界面和流程做得更完整而已。3. 数据准备与文本切块这是被我低估的环节3.1 我的笔记格式统一化我的原始笔记格式五花八门有些是# 一级标题有些是## 二级标题有些直接把日期当标题还有些干脆是纯文本。为了让知识库能识别语义边界我花了一个周末把散落的几百篇文档整理成统一的命名规则主目录按年份分每个文档内部保留原有Markdown标题结构并在文件开头加一行描述本文主题的元信息。这步虽然枯燥但它决定了后续检索的上限。3.2 切块参数的经验值文本切块是RAG流程里最影响检索效果的关键参数没有之一。切块太大向量中包含的冗余信息太多检索到的内容不够聚焦切块太小语义不完整模型看到的上下文支离破碎答非所问。我试了几组参数后把块大小定为512个字符重叠overlap设为64个字符。为什么这么设512字符大约相当于中文250字左右刚好能覆盖一个比较完整的论述段落又不会混杂多个主题。64字符的重叠是让相邻切块之间保留语义过渡地带避免因为截断导致某个结论被切开后失去上下文。具体操作用了RecursiveCharacterTextSplitter它有个好处能按Markdown结构如\n\n、#等符号依次尝试切分优先保持段落结构的完整性。我实测下来这个切块器比纯按固定长度硬切要好得多检索出来的片段读起来完整很多。3.3 清洗规则必须做如果直接把笔记原文扔进去会遇到很多干扰项比如代码块、表格、URL、图片链接。这些内容被切进向量后会污染语义。我写了一个简单的清洗函数删除HTML标签、图片语法![](链接)、普通超链接删除代码块外的行内代码符号保留代码块本身把Markdown标题符号#转化为纯文本“标题”便于模型理解层级关系删除连续空行和多余空格。这里再强调一句知识库的质量上限由原始文本决定向量化只是“搬运工”。我的清洗脚本跑了之后整体检索精度提升了至少三成建议你也别省这一步。4. 动手干活跑通第一个版本的完整流程4.1 环境安装记录我是在Windows 11笔记本上操作的Python版本3.10。全程用到的依赖库很克制pip install chromadb sentence-transformers ollama还有一个额外需要的库pip install langchain-text-splitters这里有个细节为了减少包之间的版本冲突我并没有装全套LangChain而是只装了langchain-text-splitters这个独立拆出来文本切分包的库只用到它的RecursiveCharacterTextSplitter。如果你任由pip把整个LangChain全家桶装进来会遇到一堆依赖警告虽然也不影响用但看着烦。另外我用到的Embedding模型是sentence-transformers里的paraphrase-multilingual-MiniLM-L12-v2一个只有122M的参数的多语言模型能在CPU上跑速度还算能接受。4.2 代码实现讲解:索引构建我写了一个build_index.py脚本作用是把整个笔记目录里的.md和.txt文件读取、切块、向量化并存入Chroma。核心代码如下import os from pathlib import Path from chromadb import PersistentClient from langchain_text_splitters import RecursiveCharacterTextSplitter from sentence_transformers import SentenceTransformer def load_markdown_files(folder_path): 递归读取文件夹下所有 .md 和 .txt 文件返回(文件路径, 文本)列表 docs [] for ext in (*.md, *.txt): for file_path in Path(folder_path).rglob(ext): with open(file_path, r, encodingutf-8) as f: content f.read() # 简单清洗 content clean_text(content) docs.append((str(file_path), content)) return docs def clean_text(text): 去掉图片链接、URL、HTML标签、连续空行并把标题符号转为文字 import re # 去掉图片 ![alt](url) text re.sub(r!\[[^\]]*\]\([^\)]*\), , text) # 去掉普通链接 [text](url) text re.sub(r\[([^\]]*)\]\([^\)]*\), r\1, text) # 去掉行内URL text re.sub(rhttps?://\S, , text) # 去掉html标签 text re.sub(r[^], , text) # 标题符号转文本 text re.sub(r^#\s, 标题, text, flagsre.MULTILINE) text re.sub(r^##\s, 二级标题, text, flagsre.MULTILINE) text re.sub(r^###\s, 三级标题, text, flagsre.MULTILINE) # 去掉连续空行 text re.sub(r\n\s*\n, \n, text) return text.strip() def build_index(folder_path, db_path./kb_db): docs load_markdown_files(folder_path) splitter RecursiveCharacterTextSplitter( chunk_size512, chunk_overlap64, separators[\n\n, \n, 。, , , , ] ) chunks [] metadatas [] for file_path, content in docs: # 按文件切块 split_docs splitter.split_text(content) for chunk in split_docs: if len(chunk.strip()) 30: # 过滤太短的碎片 continue chunks.append(chunk.strip()) metadatas.append({source: file_path}) embed_model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) embeddings embed_model.encode(chunks, show_progress_barTrue).tolist() client PersistentClient(pathdb_path) collection client.get_or_create_collection( namemy_kb, metadata{hnsw:space: cosine} # 用余弦距离衡量语义相似度 ) # 给每条记录生成一个唯一ID ids [str(i) for i in range(len(chunks))] collection.add( embeddingsembeddings, documentschunks, metadatasmetadatas, idsids ) print(f索引完成共 {len(chunks)} 个文本块) if __name__ __main__: build_index(./notes)解释几个关键细节hnsw:space: cosineChroma默认用平方L2距离但语义检索中用余弦相似度更符合我们的直觉——关注方向而非绝对数值。我在建collection时显式指定了余弦距离。metadatas里存了source字段这个太有用了。检索结果里能直接看到文本块来自哪个文件这样我拿着答案反查原文非常快。切块时把过短的碎片过滤掉比如只有“结论可行”这种片段的向量检索时意义不大还会造成噪声我直接丢掉。这个脚本对我的几百篇笔记运行耗时约3分钟CPU或40秒GPU属于可以接受的范围。第一次跑完Chroma会在kb_db目录下生成一套持久化文件之后就不需要重复构建了除非笔记内容有更新。4.3 代码实现讲解:检索与问答索引建好之后接下来是核心的使用环节。我写了一个query_kb.py脚本实现“问题→向量→检索→组装Prompt→调用大模型回答”的流程。核心代码如下from chromadb import PersistentClient from sentence_transformers import SentenceTransformer import ollama db_path ./kb_db collection_name my_kb embed_model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) client PersistentClient(pathdb_path) collection client.get_collection(collection_name) def retrieve_chunks(query, top_k6): query_emb embed_model.encode([query]).tolist() results collection.query( query_embeddingsquery_emb, n_resultstop_k, include[documents, metadatas, distances] ) chunks [] sources [] for doc, meta, dist in zip(results[documents][0], results[metadatas][0], results[distances][0]): chunks.append(f[来源: {meta.get(source, 未知)}]\n{doc}) sources.append(meta.get(source, 未知)) return \n\n---分隔线---\n\n.join(chunks), sources def ask_with_kb(question): context, sources retrieve_chunks(question) prompt f你是一个熟悉我的笔记内容的产品经理助手。请基于以下从我个人知识库中检索到的资料回答用户的问题。 要求 1. 优先使用资料原文中的表述 2. 如果资料中没有相关内容明确回答“我笔记里没有直接记录” 3. 不用额外发挥不要编造数据 4. 回答中适当标注资料来源。 资料内容 {context} 用户问题{question} 请回答 response ollama.chat( modelqwen2.5:7b-instruct-q4_K_M, messages[{role: user, content: prompt}] ) print(回答, response[message][content]) print(\n参考来源) for s in set(sources): print( -, s) if __name__ __main__: q input(请输入你的问题) ask_with_kb(q)这个脚本每次提问都会实时生成一次问题的embedding然后去Chroma里做一次向量相似度检索召回6个最相关的文本块。召回数量设为6是因为根据我的经验6块大约能覆盖1500~2000字这些内容对大模型来说既不会信息不足也不会因为太多而稀释注意力。4.4 Ollama部署本地模型的细节为了让所有数据都留在本机我用Ollama来加载推理模型。安装Ollama之后执行如下命令拉取Qwen2.5 7B的4bit量化版本ollama pull qwen2.5:7b-instruct-q4_K_M这个模型文件大小大概在4.7GB左右下载完毕之后如果你不主动启动服务Ollama会常驻后台。我的笔记本是16GB内存加RTX 3060显卡6GB显存把模型加载进显存之后推理速度大概每秒15~20个token做问答够用了。如果你没有独立显卡纯CPU也能跑就是速度会降到每秒3~5个token一个问题等上半天体验会差不少。这种场景下我建议换用更小参数的模型比如qwen2.5:3b虽然精准度略有退化但速度能上来很多。说实话用在自己个人知识库场景3B模型勉强也能用了。5. 使用效果实测与调优5.1 初次提问表现我建完库之后问了第一个问题“我考虑过哪些视频结构化产品的功能设计”检索模块先从几千个文本块里找出了含“视频”“结构化”“功能设计”等语义相关的6个块其中有几条来自我当时分析竞品时写的笔记有一块来自某次周报总结。大模型阅读完这些资料之后给出的回答基本还原了我当时的核心思考框架还按资料里的原文指出了来源文件。这个回答质量超过我的预期因为它回答出来的不是我随口说的而是我当时认真分析过的内容相当于把我三个月前的记忆精准唤醒。5.2 回答质量不行的几次调优第一次实测后我发现检索出的文本块里有几个明显跑偏了导致大模型回答里混入了无关内容。于是我做了三件事第一调整切块大小。把512字符改成480字符后每个块包含的主题更加单一检索精度稍微提高了一点。但这个改变并非决定性核心还是文本本身的信息密度。第二top_k从8改成6。起初我为了“不漏资料”召回8块结果上下文里塞进不少弱相关片段。反而改成6块之后模型专注度提升编造感明显减少。第三给清洗规则加了一条折叠“引用块”。我的笔记经常用引用别人的观点这在普通笔记里很加分但到了知识库里如果不标明出处模型容易把别人的观点当成我的观点。我在清洗时把引用块转成“引用资料xxxx”开头效果就明显好了。5.3 增量更新的处理策略知识库不像搜索引擎不能一劳永逸。我现在的维护策略很简单平时写笔记照旧不打断。每周末跑一次build_index.py脚本但这次会先删除旧的collection再重建。因为我的笔记总量很小也就几十MB重建耗时3分钟比增量更新逻辑简单太多了。如果你们资料量大重建Index时间很长那才考虑做增量更新。我的经验是个人本地知识库就别折腾复杂的增量同步了脚本定时重建反而更可靠。6. 踩坑汇总与避坑建议6.1 Chroma的路径版本坑Chroma更新迭代很快我用的是0.4.x版本。如果你在别处看到老教程用chromadb.Client()而不是PersistentClient说明版本已经很旧了。新版本直接用PersistentClient(path...)简单直接。另外Chroma的collection.add如果传了重复ids不会报错但会覆盖我建议每次重建前先调用collection.delete()清理旧数据避免残留旧向量干扰新索引。6.2 中文Embedding模型的选择我最初用过开源的BAAI/bge-small-zh-v1.5它在中文语义理解上更强但那个模型版本对硬件的要求略高一些。后来换成了paraphrase-multilingual-MiniLM-L12-v2主打多语言在中文和英文混排的笔记里表现很均衡。如果你的资料是纯中文我更推荐bge-small-zh-v1.5如果中英文混杂用multilingual版更省心。两种模型都只要两三百MBCPU都能跑。6.3 大模型“一本正经地瞎编”怎么破本地7B模型有个通病资料不够时会硬凑答案。我在prompt里明确给它下了命令“如果笔记里没有相关信息直接明说‘没有直接记录’不要猜。”这个约束很有效。另外我要求模型在回答时尽可能引用资料原文而不是过度转述减少编造空间。6.4 多轮对话的缺失我这个粗糙版是单轮问答每次提问都是独立上下文没办法接着聊“那还有呢”这类追问。想支持多轮对话代码里得维护一个历史消息列表并且把上一轮的回答也拼进prompt。这个做起来不难但会明显增加token消耗和推理时间。对我个人来说单轮检索已经覆盖了大多数使用场景我就没加。6.5 权限与隐私方面的考虑如果你要分享这个查询脚本给同事用建议别直接暴露整个知识库的Chroma目录。可以做成一个简单的Web界面或者把检索结果限制在必要的字段。我们本地搭建的优势之一就是隐私可控没必要因为图省事把整个数据库敞开来。7. 这套“粗糙版”后续还能怎么扩展7.1 接上对话记忆做成真正的“第二大脑”目前的单轮问答是够用但不够自然。下一步我计划在脚本里加一个简单的历史记录缓存让用户追问“刚才说的那点再展开讲讲”时系统能结合上一轮检索到的上下文进行回答。这个改动核心就是维护一个history列表拼进prompt即可。代码量不大但体验提升显著。7.2 接入多格式文件我现在只处理了Markdown和TXT但我的资料里还有PDF和Word文档。要让知识库支持这些格式就需要引入pypdf和python-docx来抽取文本。这一步并不难难的是PDF里的表格和图表抽取后会很乱可能需要做OCR。但是如果你能忍受抽取不完整接入仍是值得的。7.3 自动归类与标签推荐现在我已经有了向量化数据完全可以拿来做自动聚类把相似内容的笔记归组然后自动打上标签。尤其是长期积累之后你会发现自己写笔记时在反复讨论某些主题聚类结果可以帮你建立自己的思维地图。这个后续想清楚了再折腾。7.4 做成本地Web应用如果你不想只在命令行里问问题可以把query_kb.py包一个最简单的Flask接口浏览器里打开一个聊天框。我暂时没做是因为命令行用习惯了反而速度最快但等我空了肯定要包装一下给手机端也留个入口。8. 最终的经验沉淀折腾这个知识库最大的体会是别让完美主义阻止你迈出第一步。我知道Dify的界面更好看知道LangChain的Agent能力更强知道需要引入重排模型、混合检索、父子切块等等高级手段。但如果我一开始就追求“完整企业级”大概率到现在还在看教程而不是已经用上了自己亲手搭的知识库。现在这个“粗糙版”每日陪伴我帮我在会议前临时补资料、写作时快速引用旧结论、偶尔复盘时翻出前人思路。过程中踩的那些坑其实也成了我对RAG技术最直观的理解素材。最后送上一个踩坑后总结的小建议在搭建之前先花半小时想清楚你的知识库“输入”是什么“输出”是什么。输入如果是杂乱的凑合型笔记任意花哨的框架都救不了输出如果只是用来查找那甚至不一定需要大模型单纯向量检索就够。我这个版本加了生成回答是为了更接近“对话式回忆”的感觉。从性能看它不是最快的从精度看它不是最好的。但用一句我常跟同事说的话来总结管用的工具从来不用看起来高级。你那堆快要吃灰的笔记也完全可以搞成这么一个“粗糙”却真能帮上忙的小东西四十行代码周末半天值得一试。
返回列表