系列: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 连接池,程序干净退出、零收尾报错。
两个值得盯的点:
- 「补水」命中了「保湿霜」。 query 说的是「补水」,产品库写的是「保湿」——字面上毫无交集,但「玻尿酸保湿霜」以 0.650 排到第一。这就是关键词检索的死穴、RAG 的核心价值。
- 前三名分数咬得很紧(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 学习笔记》
- ChatGPT 玩得很溜,却做不出一个 Agent?
- 多轮对话,让 Agent 记住你(in-token 实证记忆机制)
- 给 Agent 一张「人设卡」(五件套立规矩,AI 导购实测对比)
- 给 Agent 装上「手」——工具调用入门(Function Calling 从猜到查)
- 给 Agent 一库「知识」——RAG 检索入门(补水也能找到保湿)
- AI 答得飞快,一问细节就瞎编?Top-K 调优 + 幻觉测试(附代码)
- 双剑合璧——工具 + RAG,做一个会查会答的客服(从会说到会查会答)
- 一分为多——意图路由与多 Agent 协作(把客服拆成一队)
- 集思广益——多 Agent 投票与结果汇总(用多数人的智慧压住随机性)
- 给 Agent 打个分——评测体系入门(零成本四维质检,编价格一票否决)
- 让 AI 当裁判——LLM Judge 语义评测进阶(规则查不到的「答非所问」,交给 AI 二审)
- 从评测到进化——把评测结果喂回 Prompt(0.75 到 1.0,只差一句 Prompt)
- 从 Demo 到上线——把 Agent 部署成服务(四道闸,把「能跑的脚本」变成「敢上线的服务」)
- 把 Agent 放到云上——容器化部署与上线运营(六步运营闭环,让 Agent 越上线越好用)