Zhanbo's Blog
Back to home

从 @tool 到 BM25:smolagents 中 Tool 与 RAG 的四种认知纠偏

Learning Notes
2026-07-28
5 min read
0 reads

在学习 Hugging Face Agents Course Unit 2.1 时,我以为自己懂了 Tool 和 RAG,直到看到源码和 notebook 才发现好几处理解偏差。这篇笔记记录了从 `@tool` 装饰器到 `BM25` 稀疏检索的四个关键问题。

smolagentsRAGBM25Agentic-RAGHuggingFace学习笔记稀疏检索

1. Tool 的 forward 到底扮演什么角色?

smolagents 中,Tool 有两种定义方式:用 @tool 装饰器包装函数,或者继承 Tool 基类。当你选择后者时,必须实现一个 forward 方法。

文档里对 forward 的描述非常直白:

forward: The method containing the inference logic to execute. —— Hugging Face Agents Course — Tools

来看 Hugging Face Agents Course — Tools 中的示例:

# HF Agents Course — Tools 示例代码
class SuperheroPartyThemeTool(Tool):
    name = "superhero_party_theme_generator"
    description = """
    This tool suggests creative superhero-themed party ideas based on a category.
    It returns a unique party theme idea."""

    inputs = {
        "category": {
            "type": "string",
            "description": "The type of superhero party ...",
        }
    }

    output_type = "string"

    def forward(self, category: str):
        themes = {
            "classic heroes": "Justice League Gala...",
            "villain masquerade": "Gotham Rogues' Ball...",
            "futuristic Gotham": "Neo-Gotham Night..."
        }
        return themes.get(category.lower(), "Themed party idea not found...")

如果把 Tool 类比成 PyTorch 的 nn.Module,那么 forward 就是实际执行业务逻辑的前向入口。Agent 在决定调用某个 Tool 时,会把解析好的参数传给 forward,由它完成计算、查询或 API 调用,再把结果返回给 Agent。

简言之:forward 就是这个 Tool 真正“干活”的地方。


2. @tool 的 docstring 与 class Tooldescription 是一回事吗?

本质上是同一个东西,只是来源不同。

维度@tool 装饰器class Tool 子类
描述来源函数的 docstring类属性 description
参数描述docstring 中的 Args: 段落inputs 字典
最终用途填充到 Agent 的 system prompt同样填充到 system prompt

@tool 本质上是一个语法糖。当你用它装饰一个函数时,smolagents 会在底层:

  1. 解析函数的 docstring,提取描述和参数;
  2. 自动生成一个 Tool 的子类;
  3. 把 docstring 的内容映射到新类的 descriptioninputs 属性上。

所以,两种方式最终提供给 LLM 的接口描述格式完全一致——都是让模型知道:这个工具叫什么、干什么、需要什么参数、返回什么类型。

  • 简单工具 → 推荐 @tool + docstring(写起来快)
  • 复杂工具 → 推荐继承 Tool + 手写属性(更灵活,适合需要精细控制或复用逻辑的场景)

3. RAG 的 Retrieve 必须从本地向量数据库查吗?

不是。RAG 的核心是“Retrieve + Generate”,至于信息从哪 Retrieve,并没有严格的限定。

Hugging Face Agents Course — Retrieval Agents 中,第一个例子直接用了 DuckDuckGoSearchTool

# HF Agents Course — Retrieval Agents 示例代码
from smolagents import CodeAgent, DuckDuckGoSearchTool

search_tool = DuckDuckGoSearchTool()
agent = CodeAgent(model=model, tools=[search_tool])

response = agent.run(
    "Search for luxury superhero-themed party ideas..."
)

这里 DuckDuckGoSearchTool 承担的就是 Retriever 的角色:接收 query,去外部搜索引擎抓回相关文本,再交给 LLM 生成回答。整个 pipeline 和传统 RAG 完全一致,只是数据源从本地向量库换成了互联网。

常见的 Retrieve 来源包括:

来源例子
本地向量数据库Pinecone, Weaviate, Chroma, FAISS
搜索引擎DuckDuckGo, Google, Bing
传统数据库PostgreSQL + pgvector, Elasticsearch, BM25
知识图谱Neo4j 等
混合来源本地知识库 + 网络搜索同时检索

之所以很多人把 RAG 和“本地向量库”绑定,是因为早期场景主要是用私域文档补偿 LLM 的知识盲区,所以默认走“文档向量化 → 存入向量数据库”这条路。但从框架设计的角度看,只要一个 tool 能根据 query 找到相关信息并返回,它就可以作为 RAG 的 Retrieval 环节。


4. BM25 为什么不需要 Embedding 就能检索?

这是我在读 Hugging Face Agents Course — Retrieval Agents 时最大的疑惑。代码里明明做了文档切块,但最后只是传给了 BM25Retriever.from_documents(docs),没有调用任何 Embedding 模型,也没有存入向量数据库,为什么检索还能工作?

4.1 BM25 的本质:稀疏检索(Sparse Retrieval)

BM25 是一种基于词频统计的稀疏检索算法,和 Embedding 的密集检索(Dense Retrieval)是完全不同的两条路线。

在 notebook 中:

# HF Agents Course — Retrieval Agents notebook 示例代码
class PartyPlanningRetrieverTool(Tool):
    def __init__(self, docs, **kwargs):
        super().__init__(**kwargs)
        self.retriever = BM25Retriever.from_documents(
            docs, k=5
        )

BM25Retriever.from_documents(docs) 内部实际执行了:

  1. 分词(Tokenization):把每段文本切成词;
  2. 构建倒排索引(Inverted Index):记录每个词出现在哪些文档中;
  3. 统计文档信息:计算文档长度、词的文档频率(DF)等;
  4. 全部保存在内存中

当调用 retriever.invoke(query) 时,它的流程是:

  1. 把 query 分词;
  2. 查倒排索引,找到包含这些词的文档;
  3. BM25 公式 打分(综合考虑词频 TF、逆文档频率 IDF、文档长度归一化);
  4. 返回分数最高的 Top-K 篇文档。

整个过程没有神经网络参与,没有向量相似度计算,纯粹是数学统计。

4.2 为什么这个例子能用 BM25 搞定?

因为这个知识库非常小(只有 5 条文本),而且查询词和文档用词高度重叠。比如查 "luxury superhero party",文档里就有 "superhero-themed masquerade ball with luxury decor",BM25 的词匹配足以应对。

维度BM25(本例)Embedding 向量检索
核心原理词频统计(TF-IDF 改进)神经网络语义编码
是否需要模型❌ 不需要✅ 需要 Embedding 模型
怎么“存”倒排索引(词 → 文档列表)向量数据库(高维空间坐标)
怎么“查”词重叠程度打分向量相似度(余弦距离等)
优势快、轻量、零依赖、精确匹配强能理解同义词、语义、跨语言
劣势不理解同义词(搜“汽车”找不到“轿车”)需要计算资源、维护向量库

4.3 一个值得注意的细节

notebook 里给这个 tool 写的 description 是:

description = "Uses semantic search to retrieve relevant party planning ideas..."

严格来说,BM25 并不属于语义检索(Semantic Search),它只是词汇匹配(Lexical Matching)。如果用户搜 "costumed hero",而文档里只有 "superhero",BM25 是无法理解二者同义的。这个描述算是一个不太严谨的说法。


总结

这次学习纠正了我之前对 smolagents 和 RAG 的四个直觉性误解:

  1. forward 是 Tool 的执行入口,不是某个装饰器魔法,而是类定义中真正承载业务逻辑的方法。
  2. @tool 的 docstring 和 class Tooldescription 最终服务于同一个目的——让 LLM 理解工具接口。前者是后者的语法糖。
  3. RAG 的 Retrieve 不限于本地向量库。搜索引擎、传统数据库、甚至 SQL 查询,只要能根据问题找信息,都可以是 RAG 的一部分。
  4. BM25 是一种无需 Embedding 的稀疏检索方案。它通过倒排索引和词频统计完成检索,轻量且快,但在语义理解能力上不如向量检索。小规模和术语一致的场景下,它是更简单的选择。
ZB

Zhanbo Chen

Java Backend & AI Agent Developer

Back to home
Comments