给 Agent 一库「知识」——RAG 检索入门(补水也能找到保湿)

26 阅读14分钟

系列:AgentScope 2.0 学习笔记 · 第 005 篇

难度:进阶

适合谁:想把产品文档/FAQ/知识库接进 Agent 的开发者,被「搜不到」「答不准」困扰的人

前置:建议先读 004《给 Agent 装上「手」——工具调用入门》

阅读收获:理解 RAG 四步,让 Agent 基于「知识库」回答,从根上治幻觉

运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x


你有没有遇到过这种事:自家产品库明明有保湿霜,客户问「有没有补水的」,你的 Agent 却答不上来——因为产品描述里写的是「保湿」,客户说的是「补水」,字面上一个字都对不上。

这是所有「靠关键词搜」的系统的通病。搜索引擎也这样,你在站内搜「补水」,出来的结果可能空空如也。但人脑不会犯这个错——因为人都知道「补水」和「保湿」说的是同一件事。怎么让机器也懂这层意思,就是这一篇的主题:RAG。


一、004 篇的工具,够用吗?

004 篇我们给 Agent 装上了「手」——工具能查价格、查库存。但工具查的是结构化数据:字段是死的(价格就是价格、库存就是库存),你得先知道「查什么字段」。

可真实的客服,用户问的是这种话:

  • 「我脸很干,冬天起皮,用什么好?」
  • 「有没有适合敏感肌、还能提亮的?」

这些话里没有产品名、没有字段,全是自然语言。工具无从下手——因为它只会「精确匹配」,用户说「补水」,它就找不到写着「保湿」的产品。

要接住这种问题,Agent 需要的不是「手」,而是「知识库」。这就是这一篇的主角:RAG(Retrieval-Augmented Generation,检索增强生成)。


二、关键词检索的死穴

先看一个最朴素的方案:把产品库当字典,用户问什么,就用关键词去产品描述里「搜字面」。

这个方案有个死穴——字不同,就搜不到。用户说「补水」,产品描述写的是「保湿」,字面上毫无交集,关键词检索直接扑空。可「补水」和「保湿」明明说的是同一件事。

这就是关键词检索和语义检索的分水岭:

拼什么「补水」vs「保湿」
关键词检索拼字面❌ 找不到
语义检索拼含义✅ 能匹配

RAG 用的就是后者。而实现「语义检索」的钥匙,是一个叫 Embedding(嵌入) 的技术。


三、Embedding:给文字做个「数字指纹」

Embedding 干的事,一句话就能说清:把一段文字,变成一串数字(向量)。

关键在第二个特性:语义相近的文字,向量也相近。 「补水」和「保湿」虽然字不同,但它们在「向量空间」里的位置挨得很近;而「补水」和「祛痘」则离得很远。

所以,只要把「用户问题」和「产品知识」都转成向量,再算它们之间的「距离」,就能找到语义最接近的产品——不用管字面上是否一样。

算「距离」的工具,叫余弦相似度:两个向量夹角越小,余弦越接近 1,表示越相似。公式是 cos = (a·b) / (|a|·|b|)——点乘除以两个模长,几行代码就能实现。

你还可以这样理解整个过程:Embedding 是「翻译」,把人类语言翻译成机器能比大小的数字;余弦相似度是「尺子」,量出两段话有多像。

可能你会好奇:凭什么「补水」和「保湿」的向量会相近?因为 Embedding 模型是在海量文本上训练出来的,它见过无数「补水」和「保湿」出现在相似上下文里的例子(比如「这款面霜很补水/很保湿」),于是学着把语义相近的词,映射到向量空间里相近的位置。这个「向量空间」就是模型对语言含义的理解——我们只是借它的眼睛,来看「两段话像不像」。


四、RAG 四步走

把上面这些串起来,RAG 就是四步:

① 向量化   —— 产品知识库逐条转成向量
② 检索     —— 用户问题也转成向量,算余弦相似度,取最相关的 Top-K
③ 拼接     —— 把检索到的知识 + 用户问题,一起塞进 prompt
④ 生成     —— Agent 基于「喂进来的知识」回答,而不是凭空编

前两步解决「找到相关知识」,后两步解决「基于知识回答」。四步合起来,Agent 就从「靠记忆胡猜」变成了「查完知识库再答」。

RAG 的价值,对应三个老大难问题:

  • 幻觉:模型不再凭训练记忆编,而是基于检索到的真实知识回答;
  • 时效:模型训练有截止日期,新产品它不知道;知识库却能随时增删改;
  • 私有知识:敏辰的产品库,模型训练时压根没见过,RAG 让它「现场现查」。

第三个「私有知识」值得多说两句。这是企业里最普遍、也最刚需的场景:你的产品手册、售后 FAQ、内部流程,网上根本没有,模型训练时更不可能见过。想让它回答这类问题,唯一的路就是 RAG——把「只属于你的知识」变成模型能查的库。这也是 RAG 在企业落地里排第一的原因:它不改变模型,只给它配了一个「你说了算」的资料库。


五、运行环境(重点:多一个 Embedding 的 Key)

  • 系统:WSL Ubuntu-24.04
  • Python:3.11+,虚拟环境 venv
  • 框架:AgentScope 2.x
  • API Key:DEEPSEEK_API_KEY(对话)+ BAILIAN_API_KEY(Embedding 走百炼)
wsl -d Ubuntu-24.04
cd /2026_Study/agentscope/
source venv/bin/activate

export DEEPSEEK_API_KEY="sk-xxxx"   # 对话
export BAILIAN_API_KEY="sk-xxxx"    # Embedding(百炼 text-embedding-v4)

python 005_rag.py

三个模型的 Embedding 支持情况:百炼 ✅、混元 ✅、DeepSeek ❌(未开放,404)。所以 Embedding 这里统一走百炼。


六、完整代码(单文件自包含)

保存为 005_rag.py。核心是 RAG 四步:向量化 → 检索 → 拼接 → 生成。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AgentScope 2.0 · 给 Agent 一库「知识」——RAG 检索入门
演示:把产品知识库「向量化」,用语义检索找到最相关的知识,再注入给 Agent 回答。
  与 004 工具调用的区别:工具查结构化数据(字段精确),RAG 查非结构化知识(语义匹配)。
运行环境:WSL Ubuntu-24.04 + Python 3.11+ + AgentScope 2.x
运行命令:python 005_rag.py
"""

import os
import gc
import math
import asyncio
import httpx
import warnings

# 压制 dashscope 包导入时的 DeprecationWarning(非本项目代码、非致命、仅噪音)
warnings.filterwarnings("ignore", category=DeprecationWarning)

from agentscope.agent import Agent
from agentscope.message import Msg
from agentscope.model import DeepSeekChatModel
from agentscope.credential import DeepSeekCredential, DashScopeCredential
from agentscope.embedding import DashScopeEmbeddingModel


# 1. 敏辰产品知识库(非结构化知识:描述 + 肤质)
PRODUCTS = [
    {"name": "玻尿酸保湿霜", "price": 268, "skin": "干性", "desc": "深层补水,含玻尿酸,适合秋冬干皮"},
    {"name": "积雪草舒缓精华", "price": 398, "skin": "敏感", "desc": "积雪草成分,舒缓泛红,修复屏障"},
    {"name": "净颜清透洁面乳", "price": 198, "skin": "油性", "desc": "氨基酸洁面,控油不紧绷"},
    {"name": "美白淡斑精华", "price": 458, "skin": "色斑", "desc": "烟酰胺提亮,淡化色斑暗沉"},
    {"name": "清爽控油凝露", "price": 238, "skin": "油性", "desc": "控油收敛毛孔,适合夏天油皮"},
]


# 2. 向量计算辅助
def cosine_sim(a: list, b: list) -> float:
    """余弦相似度:两个向量的夹角余弦,越接近 1 越相似。
    公式:cos(a,b) = (a·b) / (|a|·|b|)"""
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(y * y for y in b))
    return dot / (na * nb) if na and nb else 0.0


def _to_vector(emb) -> list[float]:
    """兼容不同版本的 Embedding 形态,统一取出底层 float 向量。"""
    if isinstance(emb, list):
        return emb
    if hasattr(emb, "vector"):
        return emb.vector
    return list(emb)


async def embed_vectors(model, texts: list[str]) -> list[list[float]]:
    """批量文本转向量。
    ⚠️ 关键:AgentScope 2.x 的 Embedding 模型是「可调用对象」,
    不是 .embed() 方法。调用返回 EmbeddingResponse,向量在 .embeddings 里。
    """
    resp = await model(texts)
    return [_to_vector(e) for e in resp.embeddings]


# 3. 模型构建
def build_embedding_model():
    """百炼文本嵌入模型(RAG 的「向量引擎」)。
    ⚠️ 关键:AgentScope 2.x 把 dimensions 设为必填参数,
    text-embedding-v4 标准 1024 维,必须显式传 dimensions=1024。
    """
    return DashScopeEmbeddingModel(
        credential=DashScopeCredential(api_key=os.environ["BAILIAN_API_KEY"]),
        model="text-embedding-v4",
        dimensions=1024,
    )


def build_chat_model():
    """对话模型(DeepSeek 主力,stream=False 干净退出)。"""
    return DeepSeekChatModel(
        credential=DeepSeekCredential(api_key=os.environ["DEEPSEEK_API_KEY"]),
        model="deepseek-v4-flash",
        stream=False,
    )


def extract_text(response) -> str:
    """从 AgentScope response 提取纯文本。"""
    reply_text = ""
    for block in response.content:
        if hasattr(block, "type") and block.type == "text":
            reply_text = block.text
    return reply_text


async def cleanup(chat_model):
    """收尾:关闭对话模型连接池 + 兜底清理 embedding 残留。"""
    closed = 0
    client = getattr(chat_model, "client", None)
    if client is not None:
        try:
            await client.close()
            closed += 1
        except Exception:
            pass
    for obj in gc.get_objects():
        if isinstance(obj, httpx.AsyncClient) and not getattr(obj, "is_closed", False):
            try:
                await obj.aclose()
                closed += 1
            except Exception:
                pass
    if closed:
        print(f"\n🧹 已关闭 {closed} 个 HTTP 连接池")


# 4. RAG 主流程
async def part_rag(chat_model) -> None:
    """RAG 四步:向量化 → 检索 → 拼接 → 生成。"""
    print("=" * 60)
    print("  RAG 检索入门(Embedding + 语义检索)")
    print("=" * 60)

    emb = build_embedding_model()

    # ① 向量化:把每条产品知识转成 1024 维向量
    product_texts = [f"{p['name']}:{p['desc']},适合{p['skin']}" for p in PRODUCTS]
    product_vecs = await embed_vectors(emb, product_texts)
    print(f"  ✅ 产品库已向量化:{len(product_vecs)} 条,维度 {len(product_vecs[0])}")

    # ② 检索:query 也向量化,算余弦相似度,取最相关的
    query = "我想找一款补水的产品"   # 注意「补水」字面上不在任何产品名里
    query_vec = (await embed_vectors(emb, [query]))[0]

    scores = [(cosine_sim(query_vec, pv), PRODUCTS[i]) for i, pv in enumerate(product_vecs)]
    scores.sort(key=lambda x: x[0], reverse=True)

    print(f"\n  🔍 语义检索(query「{query}」):")
    for sim, p in scores[:3]:
        print(f"     {p['name']}:相似度 {sim:.3f}")

    # ③ 拼接 + ④ 生成:把最相关产品注入 system_prompt,Agent 基于它回答
    top = scores[0][1]
    agent = Agent(
        name="小帮",
        model=chat_model,
        system_prompt=(
            f"你是「敏辰」护肤顾问。知识库检索到最相关产品:{top['name']}"
            f"({top['price']}元,适合{top['skin']},{top['desc']})。请基于此回答,不要编造。"
        ),
    )
    msg = Msg(name="user", content=[{"type": "text", "text": query}], role="user")
    response = await agent.reply(msg)
    print(f"\n  🤖 小帮:{extract_text(response)}")

    print("\n  💡 关键:query「补水」字面上不含「保湿」,但语义检索找到了「玻尿酸保湿霜」——")
    print("     这是关键词检索做不到的语义匹配,也是 RAG 的核心价值。")


def check_keys():
    need = ["DEEPSEEK_API_KEY", "BAILIAN_API_KEY"]
    missing = [k for k in need if not os.environ.get(k)]
    if missing:
        print("❌ 缺少环境变量:" + ", ".join(missing))
        print("   请先 export 对应的 API Key(对话用 DEEPSEEK,Embedding 用 BAILIAN)")
        return False
    return True


async def main():
    if not check_keys():
        return

    chat_model = build_chat_model()

    try:
        await part_rag(chat_model)
        print("\n" + "=" * 60)
        print("  ✅ RAG 检索入门完成")
        print("  🎯 后续:TopK / Rerank / Prompt 拼接策略 / 多模型 RAG 横评")
        print("=" * 60)
    finally:
        await cleanup(chat_model)
        del chat_model
        gc.collect()


if __name__ == "__main__":
    # catch_warnings 上下文包裹整个运行期(含 dashscope 实际被导入的时段)
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", DeprecationWarning)
        warnings.simplefilter("ignore", FutureWarning)
        warnings.simplefilter("ignore", PendingDeprecationWarning)
        asyncio.run(main())

七、代码拆解:三个关键点

① Embedding 模型是「可调用对象」,不是 .embed()。 这是 AgentScope 2.x 最容易踩的坑——await model(texts) 才是正确姿势,返回的 EmbeddingResponse 里,向量在 .embeddings 字段。另外,DashScopeEmbeddingModel 的 dimensions 在 2.x 是必填参数,text-embedding-v4 标准 1024 维,漏传会直接 TypeError。

② 余弦相似度是 RAG 的「发动机」。 它本质就是初中几何的「向量夹角」:夹角越小、余弦越接近 1,两段话越像。就这么一个公式,撑起了整个语义检索——不用上任何复杂的深度学习推理。

③ 检索结果要「喂回」prompt。 第四步里,最相关的产品被拼进 system_prompt,Agent 再回答。这一步是「检索」和「生成」的桥梁:没有它,检索到的知识就是死的;有了它,Agent 的回答才被「锚定」在真实知识上。


八、运行结果(真实输出)

下面是本文件在 WSL Ubuntu-24.04 里的真实运行结果。

① 向量化 —— 5 条产品知识转成 1024 维向量:

✅ 产品库已向量化:5 条,维度 1024

② 语义检索 —— query「我想找一款补水的产品」,Top-3 结果:

🔍 语义检索(query「我想找一款补水的产品」):
   玻尿酸保湿霜:相似度 0.650
   积雪草舒缓精华:相似度 0.597
   清爽控油凝露:相似度 0.592

③④ 拼接 + 生成 —— Agent 基于检索到的「玻尿酸保湿霜」回答,而不是凭空编:

🤖 小帮:您好,根据您的需求,我为您推荐玻尿酸保湿霜(268元)。这款产品主打
深层补水,核心成分是玻尿酸,非常适合干性肌肤,尤其是在秋冬季节或干燥环境下使用...

最后一行 🧹 已关闭 1 个 HTTP 连接池,程序干净退出、零收尾报错。

两个值得盯的点:

  1. 「补水」命中了「保湿霜」。 query 说的是「补水」,产品库写的是「保湿」——字面上毫无交集,但「玻尿酸保湿霜」以 0.650 排到第一。这就是关键词检索的死穴、RAG 的核心价值。
  2. 前三名分数咬得很紧(0.650 / 0.597 / 0.592)。 说明「补水」这个 query 同时和好几款产品语义相关,光取第一未必够、也可能误选。这埋了一个新问题:Top-K 到底取几个、要不要重排——正是下一篇《RAG 进阶》要解决的。

九、总结与下一步

这一篇,你给 Agent 建起了第一座「知识库」:

工具(004)= 手,查结构化字段
RAG(本篇)= 脑外记忆,查非结构化知识

核心就三步:Embedding 把文字变成可比大小的向量,余弦相似度量出语义远近,检索结果喂回 prompt 锚定回答。 有了它,Agent 面对「我脸很干用什么好」这种没有产品名的自然语言问题,终于不再抓瞎。

下一篇《AgentScope 2.0 学习笔记:RAG 进阶——Top-K 调优与幻觉测试》,我们把这一篇的「入门版」打磨成「能用版」:K 取几最合适、检索结果要不要重排、怎么验证 Agent 有没有在编。敬请期待。


十、写在最后:把「你的知识」变成 Agent 能查的库

写到这一篇,系列的主线已经很清楚了:编排是骨架、记忆是神经、人设是性格、工具是手、RAG 是知识库。而 RAG 这一步,对很多企业来说,恰恰是从「演示玩具」跨到「生产工具」的关键一跃——因为真正要上线的 Agent,回答的大多是「只属于你自己的问题」:产品手册、售后 FAQ、内部流程、历史订单。

这些知识有一个共同点:网上没有,模型也没见过,只有你有。 而 RAG 的价值,就是把这份「只有你有」的知识,变成模型随时能查的库。

如果你正在做类似的事——不管是给客服接产品知识、给内部做个文档问答、还是想把手里的资料库变成 AI 能用的资产——知识库怎么整理、Embedding 怎么选、检索效果怎么调,每一步都有不少讲究。卡住了,或者想聊聊怎么落地,欢迎来评论区讨论。下一篇「RAG 进阶」见。


本文为「AgentScope 2.0 学习笔记」系列第 005 篇,代码已通过 py_compile 语法校验,运行环境见第五节。

GitHub 仓库

系列 : 《AgentScope 2.0 学习笔记》