个人 AI 知识库问答助手构建全流程
项目目标与需求场景
平时积累了上百篇 Markdown 笔记和开发记录,但经常遇到“记得曾经解决过某个依赖冲突,却想不起具体在哪个工程哪个文件”的尴尬情况。传统的基于关键词全文搜索对同义词、意图泛化检索支持较弱。
目标:构建一个完全本地可运行、轻量级的 RAG (Retrieval-Augmented Generation) 问答助手。
- 输入自然语言问题(如:“Mac 下端口被占用怎么排查?”)
- 自动命中知识库相关章节
- 结合大模型生成精准回复并附带原文引用来源。
整体架构设计
text
┌──────────────────────┐
│ 本地 Markdown 笔记库 │
└──────────┬───────────┘
│ 1. 递归扫描与分块 (Recursive Chunking)
▼
┌──────────────────────┐
│ Text Chunks (片段) │
└──────────┬───────────┘
│ 2. 生成 Embedding 向量
▼
┌──────────────────────┐
│ Local Vector Store │ ◄─── 3. 用户提问自然语言 Query
└──────────┬───────────┘
│ 4. Cosine 相似度 Top-3 召回
▼
┌──────────────────────┐
│ Context Augmented │ (将召回片段拼入 Prompt)
└──────────┬───────────┘
│ 5. 模型生成带引用的回答
▼
┌──────────────────────┐
│ 前端 Vue 3 对话界面 │
└──────────────────────┘关键技术实现
1. 语义感知的 Markdown 分块策略
普通的固定长度切割(如每 500 字一刀切)很容易破坏代码块或切断段落上下文。 我们采用按标题层级(H1/H2/H3)拆分的语义分块策略:
typescript
export interface DocumentChunk {
id: string
filePath: string
heading: string
content: string
}
export function chunkMarkdown(filePath: string, rawText: string): DocumentChunk[] {
const sections = rawText.split(/(?=^#{1,3}\s)/m)
return sections
.map((sec, idx) => {
const match = sec.match(/^#{1,3}\s+(.+)$/m)
const heading = match ? match[1] : '前言'
return {
id: `${filePath}#${idx}`,
filePath,
heading,
content: sec.trim()
}
})
.filter(chunk => chunk.content.length > 50)
}2. 向量生成与轻量本地索引
针对个人几百篇文档规模(数千个 chunk),无需部署重型的 Milvus 或 Pinecone 服务,使用纯前端内存/本地 JSON 文件索引搭配余弦相似度即可实现毫秒级召回:
typescript
function cosineSimilarity(vecA: number[], vecB: number[]): number {
let dotProduct = 0
let normA = 0
let normB = 0
for (let i = 0; i < vecA.length; i++) {
dotProduct += vecA[i] * vecB[i]
normA += vecA[i] ** 2
normB += vecB[i] ** 2
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB) || 1)
}3. Prompt 模板与防幻觉指令
text
你是一个专业技术知识库助手。请根据下方提供的【参考文档片段】准确回答用户的问题。
回答必须严格遵循:
1. 仅依据提供的参考内容作答,不得编造任何未提及的事实、参数或命令。
2. 给出结论时,在段落末尾标注对应引用文件路径。
3. 若参考内容中没有相关信息,请明确回答“本地知识库中未检索到相关记录”。
【参考文档片段】:
---
{{retrieved_context}}
---
【用户问题】:
{{user_query}}踩坑与性能调优
- 代码块被截断问题:如果分块落在代码块内部,会导致缺少语言声明和闭合反引号,影响召回后模型阅读。通过正则先标记代码块范围,确保分块边界不跨越未闭合代码块。
- 多余空行与图片噪音:Markdown 中的 Base64 图片或长外链链接会白白消耗 Token 且稀释文本语义。在向量化前通过预处理器将其替换为占位符
[图片: caption]。 - 混合检索加权:单纯的语义向量检索在搜索特定错误码(如
EADDRINUSE、0x80070005)时,精确度有时不如传统的 BM25 / 精确词频匹配。将 BM25 关键词评分 + 向量余弦相似度 按照 0.4 : 0.6 线性加权,准确率大幅提升。
验证与效果
- 索引 120 篇个人笔记,生成约 900 个 chunk,本地初始化向量构建耗时 18 秒。
- 检索耗时稳定在 12ms 以内。
- 经过 50 个真实开发问题测试,Top-3 召回率达到 92%,有效解决了日常快速定位历史排查方案的难题。