说实话,这个问题的答案在我电脑里躺了很久。我一直被一件事折磨:项目 Wiki 写了三十多页,代码仓里躺了几千个文件,可每次想查点东西,Wiki 是一套说法,代码是另一套写法,两个东西各说各话,谁也接不上谁。后来我搭了一个本地「知识助手」,把 Wiki 和代码放进同一个知识库,用语义检索加本地大模型来问答,总算把这条裂缝补上了。这篇文章就是把我的搭建过程、踩坑经验,还有各种参数选择的心得,一次性讲清楚。
这套方案适合谁?适合团队里维护文档的技术负责人,也适合一个人维护好几个仓库的独立开发者,更适合那些想把自己的 Obsidian 笔记、飞书 Wiki、代码注释整合成"第二大脑"的知识管理爱好者。只要你受够了在文档和代码之间反复横跳,这篇内容就能给你一套立刻能落地的做法。
1. Wiki 和代码为什么总是"各说各话"
1.1 三个典型割裂场景
第一个场景是文档界面和代码实现对不上。业务改了,功能下线了,架构调整了,但 Wiki 大概率还停留在三个月前的截图。我见过最离谱的,文档里写着"系统包含 A、B、C 三个模块",实际代码里 A 已经被重构掉了,B 和 C 合并成了 D,新人照着文档去对代码,第一反应是怀疑自己 clone 错了分支。
第二个场景是新人入职根本无从下手。Wiki 写的是"结果描述":登录模块支持 OAuth2.0、支持验证码、支持自动续期。可新人真正想知道的是:这段逻辑在哪个仓库、哪个目录、哪个文件、哪个函数里,方法之间怎么调用,数据怎么流转。这些关键信息,Wiki 里几乎永远找不到。于是新人只能靠 IDE 全局搜索加肉眼硬啃,一啃就是两三天。
第三个场景是老手写代码但不写文档。代码里全是auth_service、TokenManager、SessionGuard这种名字,变量命名倒是规范,可背后的业务决策没人记录。等这个人一离职,知识直接断层。后来的人看到代码只能推断"它在做什么",却永远不知道"它为什么这么做"。
这三个场景的共同点:文档是叙述性的,代码是执行性的,两者的组织方式和检索方式天然不一样。文档按主题组织,适合通篇阅读;代码按模块组织,适合精确定位。Wiki 靠关键词搜索结果,遇到"登录流程"这种业务描述,和auth_service这种技术符号之间根本没有关联,自然各说各话。
1.2 问题的根源:语境断裂
拆开来看,Wiki 和代码割裂的根源不只是"更新不及时"这么简单。更深一层是语境断裂:Wiki 里的每个概念,在代码里都有一串对应的符号;代码里的每个设计,在 Wiki 里都有一篇对应的说明。可这两者之间没有任何桥梁。Wiki 的"登录模块"是一段自然语言,代码里的login()是一段函数实现,它们之间缺少一层映射。人肉去维护这种映射,短期可行,项目一大人就崩了。
另一个根源是信息更新节奏完全不在一个频率。代码每次 commit 都在变,文档可能一个季度才动一次。代码是活的,文档是"按快门拍出来的照片",照片怎么可能跟得上活物?你不可能要求每个开发都在改代码的同一秒去改 Wiki,这不现实。所以必须有一种机制,能自动把代码和文档的变化汇集到一起,让知识随代码同步更新。
1.3 知识助手到底解决什么
知识助手解决的不是"写文档"这件事,而是检索和理解这件事。它做的事情本质上是:把 Wiki 文档、代码仓库、技术笔记全部切碎、向量化,塞进同一个向量数据库;你提问的时候,它先做语义检索,把最相关的文档片段和代码片段捞出来,再交给本地大模型组织成带出处、可验证的回答。
为什么语义检索能打通割裂?因为传统关键词搜索是"字面匹配",你搜"登录流程",它只找包含这几个字的页面;而语义检索是"意图匹配",它能把你的问题映射到auth_service相关的代码片段和文档段落上。同一个问题,关键词搜索可能一无所获,语义检索却能直接从代码仓库里捞出一段函数签名。这就是知识助手区别于传统 Wiki 搜索的核心价值。
2. 知识助手的设计思路与方案选型
2.1 整体架构:采集、切分、向量化、检索、生成
整套系统的架构,我用一句话概括:先囤书,再拆书,然后配一个随叫随到的图书管理员。囤书是把 Wiki、代码、笔记全部采集进来;拆书是把长文档按语义切成小块,每一小块做向量化存进数据库;图书管理员就是 RAG(检索增强生成)管道,它收到问题后,先到书库里翻出最相关的几页,再把这些内容连同问题一起交给大模型,由大模型组织回答。
RAG 这个词听起来玄乎,其实就是"先检索,后生成"。我不需要让大模型记住我全部的代码,这不现实,也浪费算力;我只需要它每次回答前,临时去知识库里翻相关资料,然后基于这些资料作答。这样一来,模型挂掉也不怕,知识库是独立的;知识库更新了,回答自然跟着更新,不需要重新训练模型。
具体到我用的架构,是五段式:数据采集层负责对接 Obsidian、飞书 Wiki、Git 仓库;预处理层负责格式清洗、敏感信息过滤、Markdown 结构解析;切分层负责把文档和代码切成检索友好的片段;向量层负责生成 embedding 和相似度检索;生成层负责把检索结果组装成 Prompt,交给本地大模型生成最终答案。每一层都要独立设计,哪一层出了问题都能单独替换。
2.2 关键选型:为什么优先考虑本地方案
关于本地还是在线,我自己的判断很明确:优先本地,数据不出内网是底线。团队代码和 Wiki 里往往有内部架构、业务策略、未公开功能的信息,这些内容扔给外部 API 服务做向量化或者问答,等于把底裤露给人家。就算公司允许,你也要考虑成本——文档量一大,按 token 计费的在线方案,每个月账单看得人心慌。
本地方案的好处不止是隐私。首先是可控:模型参数、切分参数、检索逻辑全在自己手里,想调就调。其次是可离线:出差、断网、内网隔离环境,知识助手照常可用。第三是可复用:同一个知识库可以接不同的模型,今天用 7B 模型,明天换了 14B 模型,向量库不用动,只替换生成层就行。
当然,本地方案有代价:需要一台内存够大的机器,CPU 推理慢,GPU 不是每个人都有。我的态度是,本地跑小模型+本地向量检索,组合起来的效果,对绝大多数"查代码、找文档、捋逻辑"的场景完全够用。真要追求顶级的代码理解能力,可以在本地 RAG 管道上再挂一个更大的模型做后审,但那是后话了。
2.3 工具选型参考表
我踩了一圈工具,把最终落在实处的方案整理成表格,每个环节都写了我选它的理由:
| 环节 | 我用的方案 | 选型理由 |
|---|---|---|
| 知识库 | Obsidian / 飞书 Wiki 导出 Markdown | 文档已有,重点是统一转成 Markdown 格式,方便后续切分 |
| 代码源 | 本地 Git 仓库、码云 / GitHub 镜像 | 直接 clone 到本地,按目录结构扫描,保留文件路径和语言信息 |
| 文档切分 | Markdown 标题结构切分 + 递归字符切分 | 按标题切能保住章节语义,按字符切兜底长段落 |
| 代码切分 | 按函数/类级别的语法感知切分 | 避免代码块被腰斩,保证每个片段都是一个完整语义单元 |
| 向量库 | Chroma | 本地单机足够,零配置,API 简单,持久化方便 |
| 嵌入模型 | BGE-M3 | 中文效果好,支持 1024 维向量,本地运行即可 |
| 生成模型 | Ollama + Qwen2.5 7B | 中文代码场景表现稳,16G 内存机器能跑,量化后体积可控 |
| 对话前端 | 自写 CLI + Open WebUI | 自己写的脚本用于调试,WebUI 用于日常问答,共用同一套 API |
这表里的每一项都可以替换。你如果团队已经上了 Confluence,就把 Confluence 导出成 HTML 再转 Markdown;如果机器有独显,生成模型换 14B 效果会明显上一个台阶。核心思路是:数据源多样化,管道标准化。
3. 落地实操:从零搭一个本地知识助手
3.1 第一步:盘点数据源与导出
动手之前先别急着写代码,先回答一个问题:你有哪些知识资产?我的建议是画一张清单,把 Wiki 页面、代码仓库、接口文档、FAQ、个人笔记全部列出来,每个数据源标清楚格式和大概体量。我当时的清单大概是:飞书 Wiki 导出 Markdown 约 80 篇,代码仓库 2 个,共 3000 多个文件,Obsidian 笔记零散几百条。
导出这一步有几个坑要注意。飞书 Wiki 可以按目录导出为 Markdown 或者 Word,导出之后要检查图片路径和代码块格式,很多导出工具会把代码块变成普通文本,缩进全没了。Git 仓库只需要 clone 到本地固定目录,但要注意排除.git目录、node_modules、dist这类无关文件,否则向量库里全是依赖包代码,检索时噪声极大。
导出完成后,建议做一次目录规范化。我的处理方式是建一个sources/目录,下面分docs/和code/两个子目录,docs/放所有 Wiki 和笔记,code/按仓库名分目录。这样做的好处是后续给每条知识生成 metadata 时,可以从路径上直接读出类型、模块、仓库名,检索时可以直接按 metadata 过滤。
3.2 第二步:文档切分与向量化
切分是整套系统里最影响效果的一环,比模型选型还关键。为什么不能把整篇文档当成一条向量存进去?因为向量检索返回的是一个"片段",如果你每个片段是一整篇三千字的文章,把整篇文章塞进大模型的上下文,既浪费 token,又会引入大量无关内容,回答精度反而下降。反过来说,如果切得太碎,比如每个句子一条向量,检索出来的内容就缺上下文,模型看到一句话根本不知道在讲什么。
我的经验是:按 Markdown 标题结构切,保持章节完整。用 Markdown 的#、##、###做分界,每一章作为基本片段;如果某章太长,再用递归字符切分按窗口继续切分,窗口设 500,重叠设 80。重叠的目的是让相邻片段之间保持上下文衔接,避免一个问题正好落在切口上导致内容缺失。
代码的切分思路和文档不一样。代码不能按字符切,必须按语法边界切。我试过用行数硬切,效果极差:一个函数被切成两半,前半部分有签名没逻辑,后半部分有逻辑没签名,模型根本无法判断这段代码在干什么。正确做法是按函数、类、方法作为切分单元:从 AST 里解析出每个函数/类的起止行号,按行号切片,把函数签名、注释、函数体一起保留在同一条向量里。
切分完成后就是向量化。嵌入模型我选了 BGE-M3,中文和代码混合场景下表现比很多通用模型好。每条数据存入向量库时,除了文本本身,我还会写入 metadata:来源文件、章节标题、文件路径、语言类型、函数名、标签。这些 metadata 在后续检索过滤时能发挥巨大作用,比如只搜某个模块的代码,或者只搜 Wiki 文档。
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter headers_to_split_on = [ ("#", "章节"), ("##", "子章节"), ] markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on) docs = markdown_splitter.split_text(markdown_text) recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", " ", ""] ) final_docs = recursive_splitter.split_documents(docs)3.3 第三步:把代码变成"可检索的上下文"
很多人搭知识助手只处理文档,代码文件直接当作纯文本切了入库,效果往往一般。原因很简单:代码是结构化的,纯文本切分破坏了它的结构。我后来摸索出一个效果明显的做法:代码元数据 + 模块摘要双管齐下。
具体做法是,对每个代码文件,除了存函数级片段之外,再生成一份"模块摘要"。摘要内容包括:这个文件是干什么的、涉及哪些核心类、对外暴露了哪些接口、关键依赖是什么。这份摘要可以人工写,也可以用本地模型批量生成。摘要随文件的函数片段一起入库,用户问"登录流程怎么走的"时,检索系统先命中模块摘要,再顺着摘要里的文件路径定位到具体函数,回答就精确多了。
一定要在每条代码向量里带上文件路径和函数签名,这不仅是溯源需要,更是"回答可验证"的关键。模型如果回答"登录逻辑位于auth/service.py中的login()",使用者就能直接跳过去看代码。事实上,"给出位置"这件事,比"给出结论"在代码场景里更有价值。
代码仓库还有个加分动作:把 commit message 和关键 PR 描述也作为语料入库。commit message 里经常写着"修正 token 过期判断"、"调整重试策略",这些可以回答"为什么这里要这样改"。我试过一次之后把所有历史 commit 都导入了,效果出奇好,尤其适合回答那些"当时为什么这么设计"的问题。
3.4 第四步:检索与生成串联
切分和向量化完成之后,知识库就建好了。接下来要做的,就是写一条 RAG 管道:拿到用户问题,转成向量,去向量库做相似度检索,取回 top_k 片段,拼装 Prompt,调用本地模型生成回答。
检索参数我在实战里常用的配置是top_k=6。太少,比如只取 2 条,容易漏掉关键信息;太多,比如取 20 条,又会塞入大量无关片段,模型容易跑偏。如果你的文档之间关联度很高,可以适当加大top_k,但 Prompt 的长度会涨,回答速度会慢。调参的时候记得看两件事:第一,检索回来的片段跟问题有没有直接关系;第二,追加到 Prompt 里的总字符数有没有超过窗口限制。
Prompt 设计是要单独花功夫的。我的 Prompt 核心约束有三条:只依据提供的上下文回答,不许编造;回答必须引用来源文件的路径;上下文不足以回答时直接说明不知道。这三条写不写,回答质量差距天壤之别。不写的模型经常自由发挥,张嘴就编一个接口名,你照着去找代码根本不存在。
def ask(question): docs = vectorstore.similarity_search(question, k=6) context = "\n\n---\n\n".join([ f"来源: {d.metadata.get('source', '未知')}\n{d.page_content}" for d in docs ]) prompt = f"""你是一名熟悉本项目代码与文档的技术助手。 请严格依据下面的上下文回答问题。 如果上下文内容不足,请直接说"知识库中没有找到相关信息"。 回答时引用相关文件的路径。 上下文: {context} 问题:{question} 回答:""" response = ollama.chat(model="qwen2.5:7b", messages=[{"role": "user", "content": prompt}]) return response["message"]["content"]这个脚本虽然简陋,但已经足够跑通一条完整链路。我建议你先跑通它,再去考虑 Web 界面、历史记录、多用户这些功能。管道通了,后面都是锦上添花。
3.5 第五步:接入本地模型与对话界面
生成模型我用 Ollama 跑 Qwen2.5 7B 的量化版,16G 内存的笔记本跑 CPU 推理确实慢,一个问题要等十几秒,但日常用可以接受。如果你的机器有 24G 以上内存或者一块 GPU,直接上 14B,回复质量和代码理解能力会明显提升。这一步注意:量化版本体积小,适合本地部署,但精度略有损失,代码细节敏感的场景建议用更高精度。
对话界面我留了两个入口。日常调试用命令行脚本,问题输入进去直接打印回答,方便快速验证检索效果;同事也要用的时候,就上 Open WebUI,配置好 Ollama 的 API 地址,再把向量检索结果通过 Prompt 方式接进去,浏览器里就能问答了。前端本质上只是入口,核心逻辑都在 RAG 那一层,换什么界面都不影响。
如果你完全不想写代码,也可以用 AnythingLLM 这种一体化工具,直接把文档和代码目录拖进去,自动完成切分和向量化,再连上 Ollama 就能用。这种工具的好处是零门槛,缺点是检索和切分的参数可控性差,出了问题不好排查。我的建议是:先用一体化工具验证思路到底有没有用,确定要长期用了,再迁移到自建管道。
4. 常见问题与排查技巧实录
4.1 检索结果不相关 / 答非所问
这是我被问得最多的一个问题:为什么我的知识助手回答得驴唇不对马嘴?按照我的排查顺序,第一件事不是调模型,而是直接看检索回来的原文片段。输出一下similarity_search的结果,看看向量库捞回来的到底是些什么内容。如果捞回来的内容本身就不相关,那问题一定出在切分或向量化环节。
切分粒度过大,一个片段混进了多个主题,检索命中一半扯出一半;切分粒度过小,片段之间失去上下文,检索只捞到一句没头没尾的话。这两种情况都把chunk_size往中间调,我常用的文档切分是 400 到 800 之间。如果嵌入模型对中文理解差,可以对比几个本地模型的效果再定;如果检索时混入太多代码或文档噪声,可以按 metadata 过滤,比如限制只检索某种类型的数据源。
4.2 代码片段经常"答非所问"怎么办
代码场景最常见的翻车现场是:模型回答的头头是道,引用的函数名却是编的,根本不存在。这类问题从根上说有两个来源:一是切分截断了代码结构,二是模型本身代码理解能力不足。先解决切分问题,确认每个片段是不是完整的函数或类;再确认 metadata 里有没有文件路径和函数签名,没有的话照样容易幻觉;最后再考虑换更大的模型。
还有一个很容易被忽略的点:代码注释和代码本身被切到了不同的片段。如果切分策略按字符硬切,函数上面的注释属于上一个片段,函数体属于下一个片段,模型只看到函数体,当然不知道这个函数是干什么的。解决办法是用语法感知切分,让注释、签名、函数体永远在同一条向量里。我在实操中发现,这个改动对回答质量的影响,比换一个更大的模型还明显。
4.3 知识库更新后回答仍是旧的
代码每天都在变,知识库向量如果不跟着变,回答就是过时的。我遇到过的坑主要有两个:一个是向量库里旧版本的向量没删除,新版本插入后,检索时新旧混杂,回答里新老接口各说一句;另一个是文档改了,但嵌入向量没重新生成,干脆返回的是缓存命中。
解决方案其实很简单:给每个数据源打一个doc_id,内容变化后计算 hash,hash 变了就把旧的向量删掉再插入新的。代码仓库更新频率高,我的习惯是每次 pull 之后,只对发生变化的文件重新做切分和向量化。如果数据量不大,比如几万条以内,每月做一次全量重建更省心,直接清空向量库重新生成,成本也就几分钟的事。
4.4 本地模型回答幻觉
所谓幻觉,就是模型一本正经地胡说八道。代码场景里,幻觉的杀伤力比文档场景大得多,因为代码是要拿去跑的。一个不存在的函数名,模型能把它说得极其自然,连参数类型都替你编好。要压制幻觉,单靠调 Prompt 是不够的,从管道上就要下功夫。
第一,temperature调到最低,比如 0.1 或 0,减少模型自由发挥的空间。第二,Prompt 里强调"只依据上下文回答",并强制要求引用来源路径,模型在必须给出处时会更谨慎。第三,top_k别太小,太小了关键上下文缺失,模型为了凑答案只能编;也别太大,太大噪声多。第四,对高价值的代码问答,建议增加一道人工抽检流程,特别是回答里包含 API 调用时,先验证再使用。
4.5 参数速查表
把常用的参数整理成速查表,方便配置时直接对照:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 文档 chunk_size | 400 ~ 800 | 按章节长短调整,中文场景建议 500 |
| 文档 chunk_overlap | 50 ~ 100 | 保留上下文衔接,别超过 chunk 的 20% |
| 代码切分单元 | 函数 / 类 | 用语法树解析,按起止行号切片 |
| 检索 top_k | 6 ~ 10 | 先试 6,上下文不够再加大 |
| temperature | 0 ~ 0.1 | 代码问答必须低温度 |
| 向量库 | Chroma | 单机够用,无需额外服务 |
| 本地模型参数量 | 7B 起步 | 有 GPU 直接上 14B |
4.6 几个让效果翻倍的独门小技巧
第一个技巧:把术语表放进知识库。给 Wiki 里出现的高频技术名词做一份解释表,比如"令牌""网关""灰度"各是什么,在这个项目里指什么,入库后检索命中率提升明显。原因是很多问题里的用词和文档里的用词不一致,术语表就像一座桥,天然把语义拉近了。
第二个技巧:commit message 和 PR 描述一定要收进语料。这部分内容以前是知识孤岛,没人会去翻,但恰恰记录了最真实的决策过程。有一次我让知识助手回答"为什么网关层要做两次重试",它直接引用了两年前的 commit:当时线上偶发超时,加了重试,后来发现幂等性问题又用了独特 ID 去重。这种历史上下文,翻代码翻一天都翻不出来。
第三个技巧:不要追求第一次就搭得完美。先接两条数据源跑通流程,再逐渐把其他仓库、文档加进来。知识助手的效果依赖数据质量和迭代次数,你用得越多,越知道该往库里加什么、Prompt 该怎么调。我现在的知识库已经跑了大半年,每周都有新语料加进来,效果比刚搭建时好了不止一个档次。
我自己现在处理一个陌生模块,流程是这样的:先打开知识助手问一句"这个模块的核心链路是什么",拿到它引用的文件路径,再用 IDE 跳过去看真实代码,两分钟就能定位。放在以前,先翻 Wiki 再看代码再搜 commit,半小时起步,还经常看不全。设置好之后,你会跟我一样,再也回不去了。