普通 RAG 和 Agentic RAG:微软 AI Agent 入门课里谁决定检索

2026-08-18

如果你手上已经有一套「查向量库 → 把命中结果塞进 prompt → 让模型作答」的问答服务,那么「要不要改成 Agentic RAG」这个问题,多半不是被架构图说服的,而是被某几类问不出结果的提问逼出来的。ai-agents-for-beginners 的第五课 05-agentic-rag/ 把这两种形态的代码都摆在了同一个目录下,正好可以拿来对照——注意,两边的依据都来自这个课程仓自己,不是我们从别处补的。

分歧点一:检索是代码里的一步,还是模型手里的一个工具

课程仓 05-agentic-rag/code_samples/05-python-agent-framework.ipynb 的说明单元里,对传统做法只给了一句定性:它是一条固定管线,先取文档再生成回答。而这一课的做法是把知识库包成一个工具:

@tool(approval_mode="never_require")
def search_travel_knowledge(
    query: Annotated[str, "The search query about a travel destination"]
) -> str:
    """Search the travel knowledge base for destination information."""

这段是仓库原文。要理解它为什么改变了触发方式,得回头看 04-tool-use/code_samples/04-python-agent-framework.ipynb 里对 @tool 的说明:函数的 docstring 会成为模型看到的工具描述,类型标注(包括 Annotated 上挂的那句文字)用来生成工具 schema。也就是说,「这个函数是干什么的」是写给模型读的,模型据此决定这一轮要不要调它。

固定管线里,检索发生在你的代码路径上,无条件执行;改成工具之后,检索变成模型可选的一个动作。这个改变的代价很直白:它有可能不检索。 第五课自己也承认这一点——它在 agent 的 instructions 第一条写的是「ALWAYS search the travel knowledge base first」。要求必检索这件事,在 agentic 结构里只能用自然语言指令表达,不再是代码保证。

顺带一提,approval_mode 在这门课里出现过两个取值:never_requirealways_require,第四课用表格写明前者自动执行、后者每次调用都要人确认。检索类工具用前者是这一课的写法;如果你的「检索」实际上会拉取受限数据、或者顺带产生副作用,这里就是加人工闸门的位置。

分歧点二:多轮检索不在参数里,在指令里

第五课的 notebook 建了两个 agent,这是全课最值得看的一处。它们的构造代码是这样的:

agent = client.as_agent(
    tools=[search_travel_knowledge],
    name="TravelRAGAgent",
    instructions="""...1. ALWAYS search the travel knowledge base first ...""",
)

checker_agent = client.as_agent(
    tools=[search_travel_knowledge],
    name="TravelRAGCheckerAgent",
    instructions="""...2. For each destination found, search again with the destination name to get full details
... 5. If any detail seems incomplete, search once more to confirm before responding.""",
)

(上面为节选,完整指令见仓库原文。)

把两次 as_agent 调用摆在一起看:client 是同一个,tools 是同一个列表,连模型部署都没换。两者在代码上的差别只有 nameinstructions 所谓 maker-checker 式的多轮检索,在这份示例里完全由指令里那两句「search again」「search once more」表达出来。

这一点对做工程的人不太舒服,因为它意味着轮数没有一个可以设死的旋钮。我们在第五课的代码与 README 里没有找到任何检索轮数上限的配置项。作为对照,同一个仓库里 17-creating-local-ai-agents/code_samples/17-local-agent-foundry-local.ipynb 是手写的循环,函数签名是 run_agent(user_query: str, max_iterations: int = 5)——那个默认值是仓库当前代码里的,随版本可能变动,而且它之所以存在,恰恰是因为那一课自己写了 loop。第五课把循环交给了框架和模型,于是这个数就消失了。

课程 README 在讲可观测性时提到 Azure AI Tracing 这类工具(这是仓库文档自述),思路是一致的:轮数不可预设,就得靠事后能看清它到底查了几次、查了什么。

分歧点三:同一课的 Python 与 .NET,检索实现落在两个地方

第五课的 code_samples/ 下同时有 Python notebook 和 05-dotnet-agent-framework.cs,两份的代码结构差得很远,写代码前要看清自己在哪条路上。

Python 那份,知识库就是 notebook 里的一个进程内字典 TRAVEL_KNOWLEDGE_BASE,检索函数体是普通的 Python 字符串匹配。notebook 开头的 Setup 单元自述:这里用内存知识库是为了让你专注在模式本身,不需要 Azure AI Search 资源,并在同一处给出了可选的 00-course-setup/AzureSearch.md 作为换成真实索引的配套指引;结尾的小结单元也写明生产环境应当把这个内存字典换成真实的 Azure AI Search 索引。

.NET 那份走的是另一条结构。它先把 ./document.md 读成流,调 Files.UploadFileAsync 上传,再用 VectorStores.CreateVectorStoreAsync 建向量库,然后在建 agent 时把检索能力挂上去:

tools: [new FileSearchToolDefinition()],
toolResources: new()
{
    FileSearch = new()
    {
        VectorStoreIds = { fileStore.Id },
    }
},

(以上为 05-dotnet-agent-framework.cs 里创建 agent 那段调用的节选,非完整调用,以仓库最新代码为准。)

差别在于:Python 那侧检索的函数体在你的代码里,想改召回逻辑、加过滤、换成混合检索,改的是你自己的函数;.NET 那侧用的是服务端提供的 FileSearchToolDefinition,你的代码里没有检索函数体,这一课也没有给出定制检索行为的写法。这是「什么时候会咬到你」的典型场景——需求一旦从「能查到」变成「按我的规则查」,两条路的改动成本不在一个量级上。

另一处结构差异藏在指令里,值得单独拎出来。.NET 那份给 agent 的 instructions 花了整段篇幅约束「查不到怎么办」:要求只用检索到的文档内容作答,不许用通用知识或推理补全,不许给未被文档支撑的定义与解释,并且直接把拒答话术写死成一句固定的英文——大意是「抱歉,上传的文档里没有回答该问题所需的信息」。Python 那份把同一件事压缩成了指令里的一条:知识库里没有就明说。

两边落笔轻重不同,但方向一致:「检索为空时的行为」在这两份示例里都是写在指令里的,不是写在代码里的。 固定管线可以在代码里判断命中数为零然后直接返回兜底文案,这条路上没有这个位置。与之对应的是,这一课的冒烟测试里确实有一条专门针对虚构地名的用例(仓库没有写它为什么这么设计,这里只是把两处放在一起看)——指令能不能兜住,最终只能靠测试反过来验。

还有两处细节容易踩:05-dotnet-agent-framework.cs 文件头的 #:package 里,Microsoft.Agents.AIMicrosoft.Agents.AI.AzureAI 引的是 preview 版本,Azure.AI.Agents.Persistent 引的是 beta 版本,接口随时可能变;凭据也不一样,Python 用的是 DefaultAzureCredential(),.NET 那份用的是 AzureCliCredential()

Windows 侧特别注意:.NET 示例里是 Env.Load("../../../.env"),这是相对路径,取决于你的当前工作目录。在 Windows 上从资源管理器或别的目录起 shell 直接跑,很容易加载不到 .env。Python notebook 那侧用的是 dotenv.load_dotenv(),前置条件写的是 AZURE_AI_PROJECT_ENDPOINTAZURE_AI_MODEL_DEPLOYMENT_NAME 两个环境变量加上 az login;在 Windows 上把这两项写进项目根的 .env 比在 PowerShell 里用 $env: 临时设更省心,因为后者只在当前会话有效——这一句是通用做法,不是课程原文。

那么怎么选:三个问句

你的检索是不是永远都该做,且只有一个数据源? 是的话,固定管线更省事。把检索留在代码里,你至少不用担心模型某一轮跳过它。第五课那句 ALWAYS search ... first 恰恰说明,在 agentic 结构里,你要花指令去买回本来免费的确定性。

你是不是需要「第一次没查到就换个查法再来一次」? 这正是 TravelRAGCheckerAgent 那份指令干的事。固定管线要做到这个,得你自己写重试与改写逻辑;agentic 这条路上,它被表达成了指令的一部分。

你的工具有没有副作用或访问边界? 有就把 approval_modenever_require 换成 always_require,第四课写明这会让每次调用都需要人确认。

至于两者的答案质量、延迟、调用成本谁高谁低——这一点我们没有依据,不比。 我们没有跑过这一课的任何代码,课程仓里也没有给出对比数据。

改完之后拿什么验

这一课配了 tests/lesson-05-smoke-tests.json,里面最值得抄的思路是那条 no-fabrication 用例:拿一个知识库里根本不存在的虚构城市去问,断言写的是 contains_none,不允许输出里出现 Average daily cost: $ 这个模式(这串是仓库当前测试文件里的断言字符串,它对应的是内存知识库里那几条记录的写法,随课程更新可能变动)。它把「检索没命中时不许编」变成了一条可执行的判定,而不是靠人读一遍觉得还行。

需要说明的是,README 写明这个冒烟测试要在你学完第十六课、把 agent 部署到 Microsoft Foundry 并命名为 TravelRAGAgent 之后才能跑,运行方式见 tests/README.md。这门课持续更新,上面涉及的文件路径、包版本与接口写法都可能变动,以仓库最新内容为准。


本文依据 github.com/microsoft/ai-agents-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与 Agent 实际表现的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

本文对照的是同一项目内的两种用法,依据均为上述仓库内容,不对两种用法做优劣排名, 选型结论只在仓库文档写明的能力边界内成立。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。