许多开发者或许已经察觉到,AgentScope 2.0 在核心架构上实现了重大转向。尤其在知识检索领域,广为人知的 SimpleRAGKnowledge、BgeRAGKnowledge 等 1.x API,在最新的 RC2 版本中已正式被标记为“过去式”。
为什么框架要放弃看似成熟且易用的“文档分块→嵌入→检索”黑盒管道?简而言之,设计理念发生了根本变化。1.x 版本将其固化为一个只需配置参数的固定“体质”,而 2.0 的思路更加激进:将检索的决策权真正交还给大模型(LLM)本身。
新方案将“检索”拆解为“subagent + 文件检索 + Skill 仓库”的组合。你的文档无需再经过切片、向量化处理,直接放置在 ./docs 目录下即可。Subagent 可以像人类一样,先用 grep_files 搜索关键词定位文件,再通过 read_file 读取原文。搜索什么词语、读取哪段内容,都由 LLM 自主权衡,这比硬编码的检索管道灵活得多。
为了帮助大家平滑承接与迁移,本章我们先回顾 1.x 旧 API 作为参考(主要面向维护老项目的同学),随后详细展开 2.0 推荐的新实践。
16.1 1.x RAG 旧 API 回顾
首先展示一个 1.x 的典型用法,算是留个纪念:
import io.agentscope.core.rag.SimpleRAGKnowledge;
import io.agentscope.core.rag.Knowledge;
import io.agentscope.core.rag.loader.LocalFileLoader;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.DashScopeChatModel;
public class Chapter16_LegacyRag {
public static void main(String[] args) {
// 1. 准备知识库
Knowledge knowledge = new SimpleRAGKnowledge(
new LocalFileLoader("./docs"),
new DashScopeEmbeddingModel());
// 2. agent 装上 knowledge
ReActAgent agent = ReActAgent.builder()
.name("doc_qa")
.sysPrompt("你是文档问答助手。")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build())
.knowledge(knowledge) // 1.x 的套路
.build();
// 3. 调用时 agent 内部会先 retrieve 再回答
}
}
即便在 2.0 中,这段代码依然可以编译通过。只是 ReActAgent.knowledge(...) 方法上已被标记 @Deprecated(forRemoval = true),编译时会产生告警。因此,新项目无需再走回头路。
16.2 2.0 推荐的“subagent + 文件检索”
为什么不再使用 1.x 的 RAG?
我们来深入剖析 1.x 那条**固定管道**究竟有哪些局限性:
文档 → 切片 → 嵌入(向量化) → 存向量库
↓
用户提问 → 同样嵌入 → 余弦相似度搜索 → 取 top-k 切片 → 拼进 prompt → LLM 回答
这条流程至少存在三大痛点:
**痛点一:黑盒检索,查无对症。** 向量相似度较高并不代表语义真正相关。搜索出的片段可能“形似”,但与实际问题可能牛头不对马嘴。更棘手的是,你无法有效调试为何选中了这段而非那一段。
**痛点二:管道路径固定,模型沦为“局外人”。** 切片、嵌入、搜索等环节均被管道固化。LLM 在整个检索过程中完全无法参与决策,它只能被动接受喂入 prompt 的内容。
**痛点三:外部依赖过多。** 需要依赖 Embedding 模型(如 DashScope / Qwen Embedding),还需搭建向量数据库(如 Milvus、Pinecone),凭空增加两个微服务需维护,整体复杂度显著上升。
2.0 如何实现?
核心思路是将“选择权”交还给 LLM。Agent 内置了 grep_files(关键词搜索)和 read_file(文件读取)两大工具,让 LLM 自主决定搜索哪些关键词、读取哪个文件以及读多少内容。
整个工作流演变为:
用户提问 → LLM 判断应使用什么关键词进行 grep → 打开并读取相关文件 → 综合信息给出答案
对比之下,差异十分明显:
| 比较维度 | 1.x RAG | 2.0 subagent 文件检索 | | :--- | :--- | :--- | | **检索决策者** | 管道(程序) | LLM(agent) | | **检索方式** | 向量相似度 | `grep_files` 关键词搜索 + `read_file` 精读 | | **外部依赖** | Embedding 模型 + 向量库 | 无,全依赖 agent 内置工具 | | **可调试性** | 差(无法了解管道为何选取某段) | 好(agent 日志会记录 grep 了哪些词、读了哪个文件) | | **灵活性** | 固定管道,一成不变 | LLM 可自主分步:先 grep 找文件名 → 再 read 读内容 → 线索不足时再次 grep 扩大范围 |
这一思路的本质是:与其让程序猜测“哪段文本与用户问题最相似”,不如让 LLM 主动思考“我应该搜索什么关键词、读取哪个文件”。
在代码实现上,HarnessAgent 已自带 read_file、grep_files、glob_files 三个内置工具。Subagent 可直接使用它们:
import io.agentscope.harness.agent.subagent.SubagentDeclaration;
import io.agentscope.harness.HarnessAgent;
public class Chapter16_NewRag {
public static void main(String[] args) {
// 文档检索 subagent:用 grep_files + read_file 代替传统向量检索
SubagentDeclaration docReader = SubagentDeclaration.builder()
.name("doc_reader")
.description("""
文档检索 subagent。
拿到问题后:
1. 先用 grep_files 在 ./docs 找关键词
2. 用 read_file 读最相关的 1-2 个文件
3. 综合内容回答
""") // LLM 根据这个 description 决定什么时候 spawn 它
.inlineAgentsBody("你是一个文档检索员," +
"只用 read_file / grep_files 找答案。") // subagent 自己的系统提示词
.build();
HarnessAgent host = HarnessAgent.builder()
.name("qa")
.sysPrompt("你是问答助理,需要查文档时 spawn doc_reader。")
.model(model())
.workspace(Path.of("./workspace"))
.subagent(docReader) // 注册 subagent
.build();
}
}
当你调用 host.call(...) 时,LLM 一旦发现用户问题中包含“文档”等字眼,便会主动 spawn 之前注册的 doc_reader subagent,后者再通过 grep 和 read 工具自主决定如何进行检索。
16.3 进阶:用 Skill 仓库实现“结构化 RAG”
若你的文档数量庞大,且希望按主题进行“切分与管理”,将每份文档直接制作成一个 **Skill** 是更加明智的做法(具体可参考第 18 章)。你的目录结构可设计如下:
workspace/
└── skills/
├── product-faq/
│ └── SKILL.md
├── engineering-handbook/
│ └── SKILL.md
└── legal-policies/
└── SKILL.md
每个 SKILL.md 好比一份说明书,描述了“这个 skill 负责什么”:
# product-faq/SKILL.md
name: product-faq
description: |
产品 FAQ:当用户询问"如何退款 / 如何开发片 / 如何修改地址"时优先使用。
allowed-tools: [read_file, grep_files]
主 Agent 在 prompt 中被告知“遇到问题先查看 SKILL.md 来决定选用哪个 skill”。LLM 会根据 description 将问题路由至对应的 Skill,进而读取 SKILL.md 中**人工维护的文档链接**。
这种做法的优势切实可见:
- **路由策略透明,产品经理也能轻松理解。** 需要调整路由?只需修改 SKILL.md 中的描述文本即可。
- **节省 token。** 每次仅载入相关 Skill 的元信息,无需将所有文档一次性塞入 prompt。
- **管理成本低。** 文档更新时,只需改动对应 Skill 的内容即可。
16.4 何时仍应使用真正的“嵌入 + 向量检索”
话说回来,新方案并非万能。如果你的业务场景符合以下任一条件,传统 RAG 仍然有它的应用价值:
- **文档量级巨大**,例如超过 10 万条。此时 subagent 使用 grep_files 进行关键词检索效率会显著下降,难以满足性能要求。
- **检索核心是“语义相似”**。比如用户抱怨“心情不好”,你希望系统能找到关于“沮丧”、“低落”的文档片段。关键词检索难以胜任此类需求。
- **需要 Hybrid Search(混合搜索)**。即同时运行 BM25(关键词)和向量搜索,然后按照权重合并两种结果,以获得更优排序。
在这些情况下,2.0 框架推荐的做法是:**自行编写一个 @Tool**。
@Tool(name = "vector_search", description = "向量检索")
public String vectorSearch(
@ToolParam(name = "query") String query,
@ToolParam(name = "topK", required = false) Integer topK) {
// 调你自己的向量库(Milvus / Elasticsearch / PGVector)
return vectorStore.search(query, topK == null ? 5 : topK);
}
写好这个普通的 Ja va 工具后,注册到你的 Agent 或 Subagent 中即可。这正是 2.0 框架所推崇的核心理念:**该用什么工具,就用什么工具**,无需再受限于名为 RAGKnowledge 的抽象层。
16.5 最小迁移清单(1.x RAG → 2.0)
最后,若你正从 1.x 进行迁移,下表可助你快速找到“新路径”:
| 1.x 你做了什么 | 2.0 怎么做 |
| :--- | :--- |
| 调用 RAGKnowledge.retrieve(...) 自动检索 | Subagent 使用内置 grep_files + read_file 进行检索 |
| 使用 SimpleRAGKnowledge 等内置分块、嵌入功能 | 框架已移除内置管道。如需嵌入,自行编写 @Tool 调用向量库 |
| 配置分块策略、嵌入模型 | 在自定义 @Tool 中自由实现,框架不再限制 |
| agent.knowledge(knowledge) | .subagent(retriever) 或 .toolkit(toolkit) |
| agent.call(..., retriever=knowledge) | 拆分为 subagent + 工具,LLM 自主决定何时检索 |
总结所有变化的根本:1.x 的 RAG 是框架内置的固定管道,你只需配置参数;而 2.0 框架不再内置具体实现,但赋予了极大的自由度。你可以通过 @Tool 实现任意你想要的检索逻辑。检索的决策权,也从管道转移到了 LLM 手中。
16.6 完整可运行示例
为了让你获得更直观的感受,下面展示一个完整的示例。这个例子要说明什么?
一个 QA agent 配备了**两种检索方式**,LLM 会像“专家系统”一样,根据问题的类型自主决定使用哪一种:
- **doc_reader subagent**:使用内置的 grep_files 和 read_file,在 ./docs 目录下进行文件级关键词检索。适合“退货政策是什么?”“怎么开发片?”这类事实性、精准的问答,零外部依赖。
- **vector_search 工具**:调用后端 Milvus/ES 服务,进行语义级向量检索。适合用户输入“心情不好”需要找“沮丧相关文档”这类模糊、需联想的问题。
这并非功能冗余,而是互补。Subagent 查文件快但仅能精确匹配,向量检索虽慢但能理解语义。LLM 就像一个聪明的调度员,遇到事实性问题则 spawn subagent,遇到模糊问题则调 vector_search 工具。两者可以在同一个 agent 中和谐共存。
public class Chapter16_Full {
public static void main(String[] args) {
// 文件检索 subagent:使用内置工具,无需额外编写 Ja va 代码
SubagentDeclaration docReader = SubagentDeclaration.builder()
.name("doc_reader")
.description("文档检索;输入问题,输出从 ./docs 找出的相关段落")
.inlineAgentsBody("""
你是一个文档检索员。
1. 用 grep_files 在 ./docs 下找关键词
2. 用 read_file 读最相关 2 份文件
3. 把内容整理成 200 字以内回答
""")
.build();
// 语义检索工具:业务方自行对接向量库(可选)
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new VectorSearchTool("https://localhost:19530"));
HarnessAgent host = HarnessAgent.builder()
.name("qa")
.sysPrompt("""
你是问答助理。
优先 spawn doc_reader 查本地文档;若用户提出模糊的语义类问题,则调 vector_search 工具。
""")
.model(model())
.workspace(Path.of("./workspace"))
.subagent(docReader) // 文件检索(关键词)
.toolkit(toolkit) // 向量检索(语义)
.build();
// 事实性问题 → LLM 会 spawn doc_reader
host.call(
List.of(new UserMessage("user", "退款政策是什么?")),
RuntimeContext.empty())
.block();
// 模糊问题 → LLM 会调 vector_search
host.call(
List.of(new UserMessage("user", "有哪些和用户不满意相关的政策?")),
RuntimeContext.empty())
.block();
}
}
16.7 本章小结
- 1.x 的 RAGKnowledge 在 2.0 中已被标记为弃用,并将在未来版本中移除。
- 2.0 推荐的做法是 **“subagent + 文件检索”** 或是 **“业务方手写向量检索 @Tool”**。
- 对于大量结构化文档,可设计为 Skill 仓库,利用其 description 实现智能路由,管理更为便捷。
- 当确实需要嵌入和向量数据库时,完全可以通过 @Tool 自由实现,框架不再束缚你的实现方式。

