Skip to content
A · AI 应用开发入门第 5 课⏱ 20 分钟

Embedding 和 RAG 问答

学完你能
  • 说清楚 RAG 的流程:切块、向量化、检索、生成
  • 用向量相似度找出和问题最相关的资料
  • 写出「只根据资料回答、答不上就说不知道」的提示词
资料被检索后交给模型回答
资料被检索后交给模型回答AI 生成配图

模型的知识停在训练的那一天,也不知道你公司的规章、你的产品文档。把这些资料整段塞进提示词?资料一多就超出上下文,而且很贵。RAG(检索增强生成) 的做法是:先从资料里找出和问题相关的几段,只把这几段交给模型回答。

整体流程 ​

图表加载中…

分两个阶段:

  1. 建索引(资料变了才需要重做):把文档切成小段,每段算一个向量,存起来。
  2. 问答(每次提问):把问题也算成向量,找出最相近的几段,连同问题一起发给模型。

Embedding:把文字变成向量 ​

相近意思在向量空间靠近
相近意思在向量空间靠近AI 生成配图

Embedding 模型把一段文字变成一串数字(向量),意思相近的文字,向量也相近。比如「营业时间」和「几点开门」字面不同,向量却很接近,这是关键词搜索做不到的。

两个向量有多「近」,常用余弦相似度衡量:越接近 1 越相似。

向量这一步用什么模型

HiveGPT 的分组目前以对话模型为主。下面的例子用开源的 BAAI/bge-small-zh-v1.5 在本地算向量:中文效果不错、模型不到 100MB、不花钱,CPU 也能跑。生成回答这一步再调 HiveGPT。

切块 ​

资料按段切开并保留重叠
资料按段切开并保留重叠AI 生成配图

切块直接决定检索效果:

  • 块太大:一块里混着好几个主题,检索不准,还浪费 token。
  • 块太小:一句话缺上下文,模型看不懂。
  • 常见做法:按段落或标题切,每块 200–500 字,相邻块重叠 50 字左右,避免一句话被切断。
  • 每块保留来源(文件名、标题),回答时才能注明出处。

完整代码 ​

资料用一家虚构咖啡店的几条说明:

python
# pip install sentence-transformers openai numpy
import os
import numpy as np
from openai import OpenAI
from sentence_transformers import SentenceTransformer

DOCS = [
    {"source": "门店须知", "text": "小蜂咖啡营业时间:周一到周五 8:00–21:00,周末 9:00–22:00,法定节假日照常营业。"},
    {"source": "会员规则", "text": "会员每消费 1 元积 1 分,每 100 分可抵扣 5 元,积分有效期 12 个月。"},
    {"source": "菜单", "text": "拿铁 28 元,美式 22 元,燕麦拿铁 32 元;大杯加 4 元。"},
    {"source": "门店须知", "text": "店内提供免费 Wi-Fi,可以带宠物,但请勿让宠物上桌。"},
]

embedder = SentenceTransformer("BAAI/bge-small-zh-v1.5")
doc_vecs = embedder.encode([d["text"] for d in DOCS], normalize_embeddings=True)

def search(question, k=2):
    q = embedder.encode([question], normalize_embeddings=True)[0]
    scores = doc_vecs @ q                      # 向量已归一化,点积就是余弦相似度
    top = np.argsort(-scores)[:k]
    return [(DOCS[i], float(scores[i])) for i in top]

client = OpenAI(base_url="https://hivegpt.cn/v1", api_key=os.environ["HIVEGPT_API_KEY"])

def answer(question):
    hits = search(question)
    context = "\n".join(f"[{i + 1}]({d['source']}){d['text']}" for i, (d, _) in enumerate(hits))
    resp = client.chat.completions.create(
        model="gpt-5.5",
        messages=[
            {"role": "system", "content": "只根据「资料」回答问题,并在句末用 [1]、[2] 标出依据。资料里没有的内容,直接说「资料里没有提到」,不要猜。"},
            {"role": "user", "content": f"资料:\n{context}\n\n问题:{question}"},
        ],
    )
    return resp.choices[0].message.content

print(answer("周六几点开门?"))
print(answer("店里能带狗吗?"))
print(answer("你们有外卖吗?"))   # 资料里没有,应当说不知道

资料多了以后,把 doc_vecs 换成向量数据库(如 pgvector、Milvus、Qdrant),检索写法类似。

动手:只根据资料回答 ​

下面已经把「检索到的资料」放进系统提示词,你只需要提问。试试资料里有的(「拿铁大杯多少钱?」)和没有的(「有没有蛋糕?」),看模型会不会乱编:

▶ 动手试试
系统提示词(这次请求一起发送,点开查看)
你是小蜂咖啡的客服。只根据下面的资料回答,句末用 [1]、[2] 标出依据;资料里没有的内容,直接说「资料里没有提到」,不要猜。 资料: [1](门店须知)营业时间:周一到周五 8:00–21:00,周末 9:00–22:00,法定节假日照常营业。 [2](会员规则)会员每消费 1 元积 1 分,每 100 分可抵扣 5 元,积分有效期 12 个月。 [3](菜单)拿铁 28 元,美式 22 元,燕麦拿铁 32 元;大杯加 4 元。 [4](门店须知)店内提供免费 Wi-Fi,可以带宠物,但请勿让宠物上桌。
登录后运行登录 HiveGPT 后每天有免费运行次数
示例输出(之前运行的结果)
周末营业到 22:00 [1]。可以带宠物,但请不要让宠物上桌 [4]。

效果不好时查哪里 ​

现象常见原因怎么改
资料里明明有,却答「没提到」没检索到:块切得不好,或 k 太小先打印检索结果看看;调整切块,k 改成 3–5
回答里有资料之外的内容提示词约束不够明确「只根据资料」,并要求标注出处
专有名词、型号搜不到向量对精确字面不敏感加上关键词检索(BM25),两种结果合并
检索到一堆相似但没用的段落只看相似度排序多取一些,再用重排(rerank)模型精选

排查 RAG 的第一步永远是先看检索到了什么:检索错了,模型再聪明也答不对。

小结 ​

  • RAG = 检索相关资料 + 让模型只根据资料回答,适合知识库、客服、文档问答。
  • 向量让「意思相近」的文字能被找到;切块大小和重叠会直接影响效果。
  • 要求标注出处、允许说「不知道」,可以大幅减少编造。

下一课:让模型自己决定先查什么、再算什么——Agent 循环。

代码示例在页面里运行时使用 HiveGPT 的模型接口。延伸阅读来自 JavaGuide(Apache-2.0),版权归原作者。