你正在 进阶版 · 🛠️ 技能工坊 · ← 回到学院 · 进阶版主页 · 总入口

← 十二个项目

项目 01 · 领域专家(带 RAG)

用 Ollama 跑 Qwen 2.5 + Chroma 本地向量数据库,做一个能"读" 100 份你领域文档的专家系统。完全本地,零成本。

技术栈

怎么算"成"?

领域内问 10 个具体问题,至少 8 个回答里能引用具体文档片段,且回答准确。

步骤 1 · 准备语料

选一个你真正懂的领域 —— 你部落格的所有文章、你研究的某个话题的 100 篇论文、家族的菜谱、一个开源项目的全部文档…… 整理成一个文件夹,全部转成 .txt 或 .md。

步骤 2 · 装环境

ollama pull qwen2.5:7b

第 1 块 · 拉取本地大模型

qwen2.5:7b 是阿里的开源模型,7B 参数(相对轻量),在个人电脑上能跑。这一行会下载约 5GB 的模型文件到本地。

💡 本地 vs 云:用 Ollama 在本地跑大模型,意味着你的所有数据都不上云。隐私和成本都有保障。

ollama pull nomic-embed-text

第 2 块 · 拉取 embedding 模型

embedding 模型把文字转成向量(数学上的坐标)。nomic-embed-text 是开源的、轻量的、适合本地用。这样你的整个 RAG 系统(模型 + embedding + 向量库)都能在一台机器上跑。

👉 试改:ollama list 看已安装的模型,选更强或更轻量的版本。

pip install chromadb

第 3 块 · 安装 Python 向量库

chromadb 是开源向量数据库,Python 包。一条命令,它就在你的环境里了,可以本地存储向量和文档。

💡 工程简洁性:只需三行命令,就把"大模型"、"embedding"、"向量库"三个关键组件都装上了。没有云账户、没有 API 密钥、完全本地。

📋 看 / 复制完整代码
ollama pull qwen2.5:7b
ollama pull nomic-embed-text
pip install chromadb

步骤 3 · 索引文档

import chromadb
from ollama import embed

第 1 块 · 导入依赖

chromadb 是向量数据库库,ollamaembed 函数把文字变成向量。两个都是 Python 包。

💡 分工:Ollama 负责"大脑"(embedding 模型),Chroma 负责"记忆"(存储和检索向量)。

client = chromadb.PersistentClient(path="./db")
col = client.create_collection("docs")

第 2 块 · 建立数据库和集合

PersistentClient 意思是"数据会保存在硬盘上"(./db 文件夹)。create_collection 在数据库里创建一个叫 "docs" 的集合(类似数据库里的一张表)。

👉 试改:path="./db" 到其他地方,比如 path="/mnt/data/kb",这样向量库就存在那里了。

for i, doc in enumerate(docs):
    emb = embed(model="nomic-embed-text", input=doc)["embeddings"][0]
    col.add(ids=[str(i)], embeddings=[emb], documents=[doc])

第 3 块 · 逐文档索引

对每一个文档:① 用 embed(...) 把文字变成向量 ② 调用 col.add(...) 把向量和原文一起存进 Chroma。ids 是文档的编号,embeddings 是向量,documents 是原文本。

💡 这就是 RAG 的"记忆库"工作原理:文档 → 向量 → 存库。查询时反过来:查询 → 向量化 → 向量相似度搜索 → 返回相关文档。

👉 试改:在循环里加进度打印:if i % 10 == 0: print(f"已索引 {i} 个文档")

📋 看 / 复制完整代码
import chromadb
from ollama import embed

client = chromadb.PersistentClient(path="./db")
col = client.create_collection("docs")

for i, doc in enumerate(docs):
    emb = embed(model="nomic-embed-text", input=doc)["embeddings"][0]
    col.add(ids=[str(i)], embeddings=[emb], documents=[doc])

步骤 4 · 查询 + 生成

from ollama import chat

def ask(question):

第 1 块 · 导入和函数定义

导入 chat 函数(用于生成回答),定义 ask 函数接收一个问题。

💡 函数名 ask 很关键:这就是你的"智能体的嘴"——用户问问题,这个函数给出有根据的答案。

    q_emb = embed(model="nomic-embed-text", input=question)["embeddings"][0]
    results = col.query(query_embeddings=[q_emb], n_results=3)
    context = "\n\n".join(results["documents"][0])

第 2 块 · 检索相关文档

① 把问题向量化:embed(...) ② 在向量库里查询最相似的 3 个文档:col.query(..., n_results=3) ③ 把返回的文档拼成一段 context(用两个换行分隔,便于模型理解)。

👉 试改:n_results=3510,看更多上下文是否让答案更准。

    response = chat(model="qwen2.5:7b", messages=[
        {"role": "system", "content": f"基于以下资料回答:\n{context}"},
        {"role": "user", "content": question}
    ])
    return response["message"]["content"]

第 3 块 · 生成有根据的回答

调用 chat(Ollama 的大模型)。系统提示说"基于以下资料"(这就是 RAG 的关键——把检索到的文档作为上下文),然后是用户的问题。返回模型生成的内容。

💡 RAG 的核心就在这里:不是"模型凭记忆回答",而是"模型基于我给的文档回答"。这大大减少了幻觉(编造)。

📋 看 / 复制完整代码
from ollama import chat

def ask(question):
    q_emb = embed(model="nomic-embed-text", input=question)["embeddings"][0]
    results = col.query(query_embeddings=[q_emb], n_results=3)
    context = "\n\n".join(results["documents"][0])

    response = chat(model="qwen2.5:7b", messages=[
        {"role": "system", "content": f"基于以下资料回答:\n{context}"},
        {"role": "user", "content": question}
    ])
    return response["message"]["content"]

步骤 5 · 评测 10 个问题

列 10 个领域内的具体问题。跑一遍。逐条评估准确度。把不准的查一查 —— 是检索没找到?还是模型答错了?写一份 EVALUATION.md。

← 十二个项目 下一个 →

工程测验 什么时候用 RAG vs Skills 系统?
下面哪个说法最准确关于 RAG 和 Skills 的区别?
解释:这个问题触及了系统设计的核心:RAG 适合处理海量、不断变化的文本知识(如文档库);Skills 适合处理明确、边界清晰的操作(如数据库查询、API 调用)。在你的项目里,两者经常联合使用 —— RAG 检索信息,Skills 执行特定任务。
分步引导 设计一个小型 RAG 管道的 4 步
  1. 确定你要检索的「文档类型」。不是「文档」,是具体的 —— 比如「菜谱」、「GitHub issues」、「医学论文」。
    看参考

    例:你要建一个「python-requests 库的所有 GitHub issue 和讨论」的知识库。这决定了你的检索粒度 —— 全文 vs 段落 vs Q&A 对。

  2. 选择 embedding 模型。开源有 nomic-embed-text、bge-m3。测试一个真实查询,看检索质量。
    看参考

    例:用「如何处理超时错误?」这个问题,检索前 3 条,看是否都有用。如果不满意,换更强的模型或调整分块大小。

  3. 建立「回答规则」。RAG 很容易出现「我的知识库里没有」的情况 —— 你要明确告诉 LLM 什么时候该说「我找不到」,什么时候可以做常识推理。
    看参考

    例:系统提示可以说「只基于检索到的文档回答。如果检索结果里没有明确答案,先说 \"我的资料里没有...\" 再给推测。」

  4. 评测 10 个问题。混合三种:(1)在文档里明确有答案的;(2)需要综合多份文档的;(3)你的知识库完全没有的。看准确率和幻觉率。
    看参考

    例:10 个问题,8 个准确,1 个幻觉,1 个「找不到」。这样就及格了。记录下来放在 EVALUATION.md。

动手 写一个简单的 retrieve 函数
任务:写一个函数 `retrieve(query, docs)` —— 接收一个查询字符串和文档数组,返回最相关的 3 个文档(简单版:用词重叠来算相似度)。
参考实现

工程级参考答案(带完整注释):

// 参考答案(生产级注释)
function retrieve(query, docs) {
  // Step 1: 分词 - 简单版就是按空格/标点分
  const queryWords = query.toLowerCase()
    .split(/[\s\p{P}]+/u)  // 中文标点也分
    .filter(w => w.length > 0);

第 1 块 · 查询分词

把查询问题转成词数组。.split(/[\s\p{P}]+/u) 用正则表达式分割空格和标点(\p{P} 匹配所有标点,u 让它支持 Unicode,包括中文标点)。.filter(w => w.length > 0) 删除空词。

💡 分词是关键:好的分词决定了相似度计算的准确率。实际项目通常用 jieba(中文)或 NLTK(英文)。

  // Step 2: 为每个文档计算相似度
  const scored = docs.map((doc, idx) => {
    const docWords = doc.toLowerCase()
      .split(/[\s\p{P}]+/u)
      .filter(w => w.length > 0);

    // Jaccard 相似度:交集 / 并集
    const intersection = queryWords.filter(w => docWords.includes(w)).length;
    const union = new Set([...queryWords, ...docWords]).size;
    const similarity = intersection / union || 0;

    return { doc, idx, similarity };
  });

第 2 块 · 计算 Jaccard 相似度

对每个文档,计算"查询词和文档词的重叠程度"。Jaccard = 交集大小 / 并集大小。例如:查询词 [a, b],文档词 [a, c],交集 = 1(只有a),并集 = 3(a,b,c),相似度 = 1/3。

👉 试改:改用 Cosine 相似度(需要计算词频向量),会更精准。

  // Step 3: 按相似度降序排列,返回前 3 个
  return scored.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
}

第 3 块 · 排序和返回

从高到低排序,取前 3 个最相关的文档。这 3 个就是要送给大模型做 RAG 上下文的。

💡 为什么 3 个?足够提供上下文,不至于太长(大模型会分心)。可以根据实际调整。

📋 看 / 复制完整代码
// 参考答案(生产级注释)
function retrieve(query, docs) {
  // Step 1: 分词 - 简单版就是按空格/标点分
  const queryWords = query.toLowerCase()
    .split(/[\s\p{P}]+/u)  // 中文标点也分
    .filter(w => w.length > 0);

  // Step 2: 为每个文档计算相似度
  const scored = docs.map((doc, idx) => {
    const docWords = doc.toLowerCase()
      .split(/[\s\p{P}]+/u)
      .filter(w => w.length > 0);

    // Jaccard 相似度:交集 / 并集
    const intersection = queryWords.filter(w => docWords.includes(w)).length;
    const union = new Set([...queryWords, ...docWords]).size;
    const similarity = intersection / union || 0;

    return { doc, idx, similarity };
  });

  // Step 3: 按相似度降序排列,返回前 3 个
  return scored.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
}
动手 为你的领域设计 RAG 的 system prompt
任务:你要为一个「公司内部文档 RAG」系统写一个 system prompt。考虑:(1) 该检索什么类型的信息;(2) 什么情况下应该说『我的文库里没有』;(3) 如何避免编造信息。

在下面框里写你自己的 prompt(可以用中文):

→ 打开通义千问粘贴试 已复制 ✓
看参考 prompt

参考 prompt(这是一个模板,你可以改细节):

你是一个领域专家。请基于以下规则回答问题:

1. 只基于你的专业知识和常见做法回答,不编造。

第 1 块 · 身份和基本原则

清晰地说出 AI 的身份("领域专家")和第一原则:"不编造"。这是防止幻觉最重要的一句。

💡 防幻觉的第一防线:在 system prompt 里明确说"不要编造"比任何其他技术手段都有效。

2. 如果问题超出你的领域,明确说「这不在我的专业范围内」。
3. 给出的建议应该包括「为什么」和「什么时候不应该这样做」。

第 2-3 块 · 边界和深度

第 2 条教 AI "知道自己的边界"(像项目 06 的红线)。第 3 条要求不只给答案,还要给"为什么"和"反面"——这是成熟思考的标志。

👉 试改:加第 4 条:"如果这个领域有新进展,说『我的知识可能不是最新的』"。

4. 对于有争议的做法,列出不同观点。

现在,开始回答用户的问题。

第 4 块 · 平衡和行动信号

第 4 条说明 AI 应该展示"这个领域不是非黑即白的"。最后一句是行动信号:"现在开始",告诉 AI 准备好了。

💡 对比策略:这个 prompt 强调"审慎"(不编造、知道边界、承认争议)而不是"全知"。这样的 prompt 出来的答案更可信。

📋 看 / 复制完整代码
你是一个领域专家。请基于以下规则回答问题:

1. 只基于你的专业知识和常见做法回答,不编造。
2. 如果问题超出你的领域,明确说「这不在我的专业范围内」。
3. 给出的建议应该包括「为什么」和「什么时候不应该这样做」。
4. 对于有争议的做法,列出不同观点。

现在,开始回答用户的问题。