目录

Ollama 文本嵌入与 RAG 教程:让模型根据你的文档回答

语言模型不了解你公司的规章制度、产品手册或个人笔记。RAG(Retrieval-Augmented Generation,检索增强生成) 的做法是:先从你的文档中找出与问题相关的段落,再把这些段落和问题一起交给模型,让它根据资料作答。

问题 ──▶ 找出最相关的文档段落 ──▶ “根据以下资料回答:……” ──▶ 模型 ──▶ 答案

找出“相关段落”的关键技术就是文本嵌入(embedding)。

什么是文本嵌入

嵌入模型会把一段文本转换为一串数字(向量)。意思相近的文本,向量也会相近,即使用词完全不同:

  • “如何申请请假?”
  • “休假要怎么申请?”

这两句话的向量会很接近,因此可以实现关键词搜索做不到的“语义搜索”。

下载嵌入模型

ollama pull bge-m3            # 多语言,中文表现好
ollama pull nomic-embed-text  # 轻量,主要面向英文

嵌入模型不能用于聊天,只能生成向量。

生成嵌入

curl http://localhost:11434/api/embed -d '{
  "model": "bge-m3",
  "input": ["如何申请请假?", "休假要怎么申请?"]
}'
import ollama

result = ollama.embed(model="bge-m3", input=["如何申请请假?", "休假要怎么申请?"])
vectors = result.embeddings
print(len(vectors), len(vectors[0]))  # 2 条,每条 1024 维

input 可以是单个字符串或字符串列表,批量处理比逐条发送快很多。

计算相似度

最常用的是余弦相似度,取值范围为 -1 到 1,越接近 1 越相似:

import math


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


print(cosine(vectors[0], vectors[1]))

实战:本地知识库问答

假设有一份公司规章文件 handbook.txt。完整程序如下:

import math
import ollama

EMBED_MODEL = "bge-m3"
CHAT_MODEL = "qwen3:8b"


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    return dot / (math.sqrt(sum(x * x for x in a)) * math.sqrt(sum(y * y for y in b)))


# 1. 读取文档并切分成段落(按空行分段)
text = open("handbook.txt", encoding="utf-8").read()
chunks = [c.strip() for c in text.split("\n\n") if c.strip()]

# 2. 为每个段落生成嵌入(只需做一次,可以保存起来重复使用)
chunk_vectors = ollama.embed(model=EMBED_MODEL, input=chunks).embeddings


def ask(question: str, top_k: int = 3) -> str:
    # 3. 找出与问题最相似的段落
    q_vec = ollama.embed(model=EMBED_MODEL, input=question).embeddings[0]
    ranked = sorted(zip(chunks, chunk_vectors), key=lambda p: cosine(q_vec, p[1]), reverse=True)
    context = "\n\n".join(c for c, _ in ranked[:top_k])

    # 4. 把段落和问题一起交给模型
    response = ollama.chat(
        model=CHAT_MODEL,
        messages=[
            {
                "role": "system",
                "content": "你是公司的内部助理。只根据提供的资料,用简体中文回答。"
                "如果资料中没有答案,请回答‘资料中没有相关信息’,不要自行推测。",
            },
            {"role": "user", "content": f"资料:\n{context}\n\n问题:{question}"},
        ],
        options={"temperature": 0.2},
    )
    return response.message.content


while True:
    q = input("\n问题:").strip()
    if not q:
        break
    print(ask(q))

这就是一个最基础但完整可用的 RAG 系统,所有数据都保留在本地。

改进方向

切块(chunking)

切得太大,会混入不相关的内容;切得太小,会丢失上下文。常见做法:

  • 每块约 300~800 个字,相邻两块重叠 50~100 字。
  • 按照文档结构(标题、段落)切分,比按固定字数切分更好。
  • 在每块前面加上所属标题,例如“第三章 请假规定 > 年假”。

使用向量数据库

段落很多时,每次都与所有向量比对会很慢,而且每次重启都要重新计算。可以改用向量数据库来存储和搜索:

工具特点
ChromaPython 中最容易上手,可存储在本地文件夹中
SQLite + sqlite-vec单个文件,适合小型项目
PostgreSQL + pgvector已在使用 PostgreSQL 时的首选
Qdrant、Milvus海量数据、独立服务

混合搜索

语义搜索不擅长精确匹配(例如产品型号、人名)。结合关键词搜索(BM25)和语义搜索,通常效果更好。

注意上下文长度

把多个段落放进提示词时,总长度不能超过模型的上下文长度,否则前面的内容会被截断。必要时调高 num_ctx,参见环境变量与设置。

不想自己写程序?

Open WebUI 内置了文档上传和知识库功能,可以直接上传 PDF、Word 文件,让模型根据文档作答。

更新于 2026年10月12日