(1)RAG 接入本地大模型:用 Ollama 实现真实语义检索与生成
·
系列导读:本系列共 6 篇,带你从零到一构建完整的 RAG + LangGraph + MCP 项目。
- 第 1 篇:最小 RAG 实现,纯 numpy,无任何 AI 框架
- 第 2 篇(本文):接入 Ollama 本地大模型,实现真实语义检索
- 第 3 篇:接入 ChromaDB 持久化向量数据库
- 第 4 篇:用 LangChain 重构 + 多轮对话
- 第 5 篇:LangGraph 多步推理工作流
- 第 6 篇:MCP 工具调用协议集成
一、上一篇的问题
第 1 篇用随机向量模拟 Embedding,导致检索结果完全错误——“年假有多少天"检索不到"年假政策”。
根本原因:随机向量没有语义,无法表达文字之间的相似关系。
本文只改动两个函数,其余代码完全不变:
| 组件 | 第 1 篇 | 第 2 篇 |
|---|---|---|
embed() | 随机向量(无语义) | Ollama nomic-embed-text(真实语义) |
generate() | 返回模板字符串 | Ollama qwen2.5:7b 真实生成 |
VectorStore | numpy 数组(内存) | numpy 数组(内存,不变) |
二、Ollama 环境准备
# 1. 安装 Ollama(macOS)
brew install ollama
# 2. 启动服务
ollama serve
# 3. 拉取模型
ollama pull nomic-embed-text # embedding 模型,82MB,专门做向量化
ollama pull qwen2.5:7b # 生成模型,4.7GB,中文效果好
# 4. 安装 Python 依赖
pip install ollama numpy
为什么选 nomic-embed-text?
- 体积小(82MB),专门做 embedding,比通用模型快
- 输出 768 维向量,语义质量好
- 开源,本地运行,数据不出本机
三、替换点 1:真实 Embedding
import ollama
import numpy as np
EMBED_MODEL = "nomic-embed-text"
def embed(text: str) -> np.ndarray:
"""
调用 Ollama embedding 模型,把文本变成语义向量。
nomic-embed-text 输出 768 维向量。
对比第 1 篇:
- 第 1 篇:hash(text) → 随机数 → 无语义
- 第 2 篇:text → 神经网络 → 768维语义向量
"""
response = ollama.embeddings(model=EMBED_MODEL, prompt=text)
vec = np.array(response["embedding"], dtype=np.float32)
vec = vec / np.linalg.norm(vec) # 归一化
return vec
现在"年假"和"年假政策"的向量距离会很近,"年假"和"报销流程"的向量距离会很远。
语义向量的直觉
# 语义相似 → 向量方向相近 → 余弦相似度高
embed("年假有多少天") ≈ embed("年假政策:员工入职满一年...")
相似度 ≈ 0.85
# 语义不同 → 向量方向不同 → 余弦相似度低
embed("年假有多少天") ≈ embed("报销流程:填写费用报销单...")
相似度 ≈ 0.12
四、替换点 2:真实大模型生成
CHAT_MODEL = "qwen2.5:7b"
def generate(query: str, contexts: list) -> str:
context_text = "\n".join(f"- {c}" for c in contexts)
prompt = f"""你是一个企业内部知识库助手。请根据以下资料回答用户问题。
如果资料中没有相关信息,请说"知识库中没有找到相关信息"。
【参考资料】
{context_text}
【用户问题】
{query}
【回答】"""
# 调用 Ollama,stream=True 实时打印,用户体验更好
print("\n🤖 模型回答(流式输出):")
full_response = ""
for chunk in ollama.generate(model=CHAT_MODEL, prompt=prompt, stream=True):
token = chunk["response"]
print(token, end="", flush=True) # 实时输出每个 token
full_response += token
print() # 换行
return full_response
流式输出(stream=True)的好处:模型边生成边输出,用户不用等待全部生成完再看到结果,体验更好。
五、完整代码
import numpy as np
import ollama
EMBED_MODEL = "nomic-embed-text"
CHAT_MODEL = "qwen2.5:7b"
DOCUMENTS = [
"年假政策:员工入职满一年后享有15天年假,满三年后20天。",
"病假政策:病假需提供医院证明,当天请假需电话通知直属上级。",
"请假流程:登录OA系统,填写请假申请单,提前3个工作日提交。",
"加班规定:工作日加班按1.5倍计算,周末加班按2倍计算。",
"报销流程:填写费用报销单,附发票原件,提交财务部审核。",
]
def embed(text: str) -> np.ndarray:
response = ollama.embeddings(model=EMBED_MODEL, prompt=text)
vec = np.array(response["embedding"], dtype=np.float32)
return vec / np.linalg.norm(vec)
class VectorStore: # 和第 1 篇完全一样
def __init__(self):
self.texts = []
self.vectors = []
def add(self, text: str):
self.texts.append(text)
self.vectors.append(embed(text))
print(f" ✓ 已向量化:{text[:20]}...")
def search(self, query: str, top_k: int = 3) -> list:
query_vec = embed(query)
matrix = np.array(self.vectors)
scores = matrix @ query_vec
top_indices = np.argsort(scores)[::-1][:top_k]
return [(self.texts[i], float(scores[i])) for i in top_indices]
def generate(query: str, contexts: list) -> str:
context_text = "\n".join(f"- {c}" for c in contexts)
prompt = f"""你是一个企业内部知识库助手。请根据以下资料回答用户问题。
如果资料中没有相关信息,请说"知识库中没有找到相关信息"。
【参考资料】
{context_text}
【用户问题】
{query}
【回答】"""
full_response = ""
for chunk in ollama.generate(model=CHAT_MODEL, prompt=prompt, stream=True):
token = chunk["response"]
print(token, end="", flush=True)
full_response += token
print()
return full_response
class SimpleRAG:
def __init__(self):
self.store = VectorStore()
def build_index(self, documents: list):
print(f"📚 建立索引,共 {len(documents)} 个文档片段...")
for doc in documents:
self.store.add(doc)
print("✅ 索引建立完成\n")
def query(self, question: str, top_k: int = 2) -> str:
print(f"\n🔍 问题:{question}")
results = self.store.search(question, top_k=top_k)
print(f"\n📋 检索到 {top_k} 个相关片段:")
for i, (text, score) in enumerate(results, 1):
print(f" [{i}] 相似度={score:.3f} {text}")
contexts = [text for text, _ in results]
return generate(question, contexts)
def check_models():
try:
models = ollama.list()
available = [m.model for m in models.models]
print(f"✅ Ollama 运行正常,可用模型:{available}")
return True
except Exception as e:
print(f"❌ Ollama 未运行:{e}\n请先执行:ollama serve")
return False
if __name__ == "__main__":
if not check_models():
exit()
rag = SimpleRAG()
rag.build_index(DOCUMENTS)
rag.query("我想请假,怎么申请?")
rag.query("年假有多少天?")
rag.query("周末加班怎么算工资?")
运行:
ollama serve # 终端1
python3 step2_ollama_rag.py # 终端2
六、对比第 1 篇的变化
第 1 篇的问题:"年假有多少天?“检索到的是"绩效考核”(错误)。
第 2 篇的效果:"年假有多少天?“正确检索到"年假政策:员工入职满一年后享有15天年假…”(相似度 0.85+)。
只改了 embed() 和 generate() 两个函数,检索质量天壤之别。 这说明一件事:Embedding 模型的质量决定了 RAG 的上限。
七、第 2 篇的局限
目前还有一个问题:每次程序重启都要重新 embed。
8 个文档 × 768 维 = 6144 次浮点运算,虽然很快,但如果是几万个文档,每次启动都要花几分钟重新 embed,无法接受。
解决方案:向量持久化。这正是第 3 篇 ChromaDB 要解决的问题。
八、常用 Embedding 模型对比
| 模型 | 维度 | 大小 | 中文效果 | 适用场景 |
|---|---|---|---|---|
| nomic-embed-text | 768 | 82MB | 一般 | 快速原型 |
| bge-m3 | 1024 | 1.2GB | 优秀 | 中文生产环境 |
| text-embedding-ada-002 | 1536 | 云API | 优秀 | OpenAI 生态 |
| mxbai-embed-large | 1024 | 670MB | 良好 | 多语言 |
总结
本文只改动了两个函数,但效果质的飞跃:
embed():随机向量 → Ollama 语义向量,检索准确率大幅提升generate():模板字符串 → 真实大模型,回答质量质的提升
这验证了第 1 篇的设计思想:好的代码分层,让替换组件变得极其简单。
下一篇引入 ChromaDB,把向量持久化到磁盘,同时增加文档来源追踪(metadata)。
更多推荐


所有评论(0)