Skip to content

Ollama RAG:本地私人知识库问答

本章节我们把前面学到的嵌入、检索、生成能力组合成一个真正可用的应用:把你的文档喂给本地模型,让它只根据你的资料回答问题,并注明出处。

完整代码不到 100 行,零外部向量库依赖,复制即可运行。


需求拆解与技术选型

先明确问题:大模型不知道你硬盘里的资料,直接问只会得到编造的答案。RAG(检索增强生成)的思路是"开卷考试"——先从资料里检索相关片段,把片段塞进提示词,让模型照着作答。

rag-pipeline.svg

技术选型遵循"最小依赖"原则:

环节选型理由
嵌入模型embeddinggemma体积小、本地快;备选 qwen3-embedding
生成模型qwen3.5:4b中文好、16GB 内存可跑
向量存储Python 列表 + 余弦相似度零依赖,教学清晰;量大换 Chroma 等专业库
文档格式txt / md纯文本即可讲清原理,PDF 见文末扩展

需要拉取两个模型:

bash
ollama pull embeddinggemma
ollama pull qwen3.5:4b

EmbeddingGemma 是 Google 开源的轻量多语言文本嵌入(Embedding)模型,专门用于把文本转为向量,非常适合本地设备离线部署,Ollama 已经对该模型做适配封装,可以直接拉取使用。


第一步:文档解析与切片

模型上下文有限,整本文档塞不进去,需要切成小片段。切片策略用最朴素的"定长 + 重叠":每片 300 字,相邻片段重叠 50 字,避免关键句被切断。

实例

python

# 文件路径:rag.py

import os

def load_chunks(folder, size=300, overlap=50):

    """读取文件夹中所有 txt/md 文件,切成带来源标记的片段"""

    chunks = []

    for name in sorted(os.listdir(folder)):

        if not name.endswith(('.txt', '.md')):

            continue

        path = os.path.join(folder, name)

        text = open(path, encoding='utf-8').read().strip()

        # 滑动窗口切片:步长 = size - overlap

        step = size - overlap

        for i in range(0, len(text), step):

            piece = text[i:i + size].strip()

            if len(piece) > 20:  # 过滤太短的碎片

                chunks.append({'file': name, 'text': piece})

    return chunks

第二步:向量化与检索

把所有片段和用户问题都转成向量,用余弦相似度找出与问题最相关的 top-k 片段。

实例

python

import ollama

EMBED_MODEL = 'embeddinggemma'

def embed(texts):

    """批量生成向量(输入列表,输出向量列表)"""

    result = ollama.embed(model=EMBED_MODEL, input=texts)

    return result['embeddings']

def cosine(a, b):

    """余弦相似度:越接近 1 越相似"""

    dot = sum(x * y for x, y in zip(a, b))

    na = sum(x * x for x in a) ** 0.5

    nb = sum(x * x for x in b) ** 0.5

    return dot / (na * nb)

def search(question, chunks, vectors, top_k=3):

    """检索:返回最相关的 k 个片段及其相似度"""

    q_vec = embed([question])[0]

    scored = [

        (cosine(q_vec, v), c)

        for v, c in zip(vectors, chunks)

    ]

    scored.sort(key=lambda x: -x[0])

    return scored[:top_k]

embeddinggemma 输出的是 L2 归一化向量,此处仍保留完整余弦公式,让原理一目了然;换成点积结果相同。


第三步:组装提示词并生成带引用的回答

把检索到的片段连同来源编号一起写进提示词,明确要求模型"仅依据资料回答"并标注引用。

实例

python

from ollama import chat

GEN_MODEL = 'qwen3.5:4b'

def answer(question, chunks, vectors):

    hits = search(question, chunks, vectors, top_k=3)

    # 把片段与来源编号拼进上下文

    context = '\n\n'.join(

        f'[资料{idx}] (来自 {c["file"]}\n{c["text"]}'

        for idx, (score, c) in enumerate(hits, 1)

    )

    prompt = f'''仅根据以下资料回答问题,并标注使用的资料编号。

如果资料不足以回答,请直接说明不知道。

资料:

{context}

问题:{question}'''

    response = chat(

        model=GEN_MODEL,

        messages=[{'role': 'user', 'content': prompt}],

        options={'temperature': 0.2},  # 低温度,减少发挥

    )

    print(response.message.content)

    print('\n参考来源:')

    for idx, (score, c) in enumerate(hits, 1):

        print(f'  [资料{idx}] {c["file"]}(相似度 {score:.3f})')

完整运行:主程序入口

实例

python

# 追加到 rag.py 末尾

if __name__ == '__main__':

    folder = 'docs'  # 把你的 txt/md 文档放进 docs 文件夹

    chunks = load_chunks(folder)

    # 离线索引:一次性向量化全部片段

    print(f'正在索引 {len(chunks)} 个片段...')

    vectors = embed([c['text'] for c in chunks])

    print('索引完成。\n')

    # 在线问答循环

    while True:

        q = input('问题(exit 退出):').strip()

        if q.lower() == 'exit':

            break

        if q:

            answer(q, chunks, vectors)

            print()

准备资料并运行:

bash
mkdir docs
cp RUNOOB-python-notes.md docs/
python rag.py

输出:

bash
$ python rag.py
正在索引 42 个片段...
索引完成。

问题(exit 退出):Python 的切片是什么?
Python 切片是用 [start:stop:step] 从序列截取子序列的语法 [资料1]。
例如 s[1:4] 取索引 1 3 的字符 [资料2]。

参考来源:
  [资料1] RUNOOB-python-notes.md(相似度 0.712)
  [资料2] RUNOOB-python-notes.md(相似度 0.668)

注意回答末尾的来源标注:每句话都能追溯到你 docs 目录里的具体文件,这正是 RAG 相比"直接问模型"的核心价值。


效果调优与常见问题

RAG 效果不好,八成出在检索环节而不是生成环节。调优杠杆按影响力排序:

杠杆默认值调整方向
切片大小 size300 字答得太碎就调大;太大则检索不准,经验区间 200~500
重叠 overlap50 字句子频繁被切断时调大
检索条数 top_k3资料密集时加大,但会占用更多上下文
相似度阈值未启用过滤相似度过低的片段,减少不相关干扰
生成温度0.2保持低温,知识库不需要创造性

高频问题速查:

现象原因对策
回答与资料无关检索没命中检查切片是否太小/太大,确认问题语言与资料一致
模型仍编造答案提示词约束不足强化"仅根据资料"表述,检索不到时直接回答"不知道"
索引特别慢片段过多逐条嵌入使用批量 embed;文档量大时改用专业向量库与增量索引
长文档回答变差关键信息被切碎按标题/段落等语义边界切片代替纯定长

生产化路线图:文档量上万或需要 PDF 解析时,把"列表向量库"替换为 Chroma 一类专业向量库(提供持久化与高效近邻检索),用 pypdf 抽取 PDF 文本,其余代码骨架不变——这正是本篇把各环节解耦的意义。

AI 思考中...

Ollama 安全与合规

Ollama 本地 AI 编程助手

基于 VitePress 构建,部署于 GitHub Pages