想象你正在参加一场开卷考试:你理论上拥有所有答案,但不知为何,这反而让你更难找到正确的那一个。

  • 你该从哪里开始搜索?
  • 你如何知道自己的来源是正确的?
  • 你如何优化时间,以免把全部精力都浪费在重复搜索上?

高效地找到正确的信息并没有那么简单。

把一个交给智能体,它也会陷入类似的困境:它的上下文窗口有限,所以和我们人类一样,它必须检索信息来完成某些任务(而不是把所有内容都记住)。在搜索时,智能体会遇到以下问题:

  • 低效搜索:每次搜索都要消耗 token 和时间。一个笨拙搜索的智能体(例如打开每个文件只为看看里面有什么,或者不断改写同一个查询)可能在找到任何有用内容之前,就已经用搜索结果填满了上下文。
  • 一次性上下文过多:一次搜索就倾倒出完整的数据导出或十几份文档,瞬间填满上下文窗口,让真正的工作没有余地。
  • 无关或过时的上下文:过时或跑题的信息在模型看来,可能和正确答案一样有说服力。一旦进入上下文窗口,智能体就可能在此基础上继续构建(而这个错误会贯穿之后的每一步)。

幸运的是,智能体可以使用多种技术来检索正确的信息。智能体框架的职责就是策略性地应用这些技术,让模型在正确的时间获得正确的上下文以完成给定任务。

__wf_reserved_inherit

这篇博客介绍了智能体检索(只查找并拉取当前任务所需的信息)面临的挑战、我们可以用来应对这些挑战的技术,以及在设计智能体框架时如何牢记这些要点(以LangChain 的 Deep Agents框架为例)。

关于上下文一旦进入上下文窗口后如何管理,请查看这篇关于上下文工程的博客。

我们的示例:一个竞品分析智能体

让我们看看这如何应用到一个真实用例中:一个竞品分析智能体。每周一,一个智能体研究数百个竞争对手,并按团队模板撰写报告。它需要:

  • 查找上周关于公司 A 的笔记
  • 在庞大的市场数据导出中查找某个具体的营收数字
  • 在新闻档案中搜索任何新内容

这些都是不同类型的检索,因此面临不同的问题。我们使用以下技术构建了我们的智能体Deep Agents harness因此有一些技术我们可以开箱即用地应用,以尽量减少这些问题。

1. 低效搜索

我们的智能体必须为每个竞争对手找到三样东西。如果每一样都要尝试十几次(打开文件只是为了看看里面有什么,或者因为来源用了不同的措辞而重新组织搜索词),那么在它写出报告的第一个字之前,上下文窗口就已经被失败的搜索填满了。

因此,最好的搜索是智能体不必重复的搜索,这意味着要为每项工作使用正确的工具。

从内置的文件系统工具开始

智能体搜索信息最常见的方式之一是通过文件系统。然而,如果我们的智能体打开每个文件来查看里面有什么,它的搜索效率就非常低:它读取的每个文件都会进入其上下文窗口,所以仅仅靠查看就能把窗口填满。

更好的方法是先检查名称和关键词(就像开卷考试时你会先扫一遍书的索引,而不是读每一页)。有两个常见的文件系统工具正是这么做的:

  • glob按名称模式查找文件。例如,**/company-a*.md查找上周关于公司 A 的笔记。
  • grep在文件内部搜索而不将它们加载到上下文窗口中。它匹配字面文本,可以返回匹配的文件、匹配的行或计数。

__wf_reserved_inherit

每个 deep agent 都内置了这些工具,以及ls、read_file、write_file和edit_file。无论文件位于何处(智能体状态、本地磁盘、持久化存储或沙箱),这些工具都能正常工作,因为它们都通过智能体的后端。

当关键词不够时,添加一个检索工具

Grep在你明确知道要查找哪些词时非常有用,但它只匹配你提供的精确文本。这对我们示例中的第三项工作来说是个问题:在新闻档案中搜索任何新内容。如果我们的智能体用 grep 搜索“Company A funding”,它会漏掉一篇标题为“Company A closes $50M Series B”的文章,尽管那正是它要找的内容。

要按含义而非精确词语进行搜索(语义搜索),你需要添加一个检索工具由向量存储提供支持。检索工具是智能体可以调用来获取信息的任何工具:它接收一个查询,在某个数据源中查找,并返回找到的内容(最接近的匹配项)。

__wf_reserved_inherit

检索工具的匹配方式取决于其背后的数据源。由向量存储提供支持时,它按含义匹配,但检索工具也可以建立在其他数据源之上:

  • SQL 数据库,用于结构化查询(如销售数据或客户记录)
  • 搜索 API(用于从网络获取最新信息)
  • 一个MCP 服务器(用于他人已经构建好的工具,比如文档搜索)

__wf_reserved_inherit

要深入了解语义搜索的工作原理,请参阅LangChain 的指南。

让智能体决定何时搜索

许多检索方案在开始时搜索一次,然后作答。这对 FAQ 机器人有效(两步 RAG),但对我们的智能体不行。假设在研究公司 C 的过程中,它发现了一篇关于某款产品发布的文章,而它此前并不知道这款产品,现在它需要该产品的定价(这在开始时是不可能知道要去查找的)。如果只搜索一次,报告就会漏掉这部分内容。

研究型智能体需要智能体式检索(而不是两步 RAG):由智能体决定何时搜索、搜索什么,以及是否需要更多信息。使用 Deep Agents 时,它默认就是这样工作的:智能体在一个循环中运行,可以按需多次调用任何工具。

__wf_reserved_inherit

将你的搜索知识打包为技能

为了帮助智能体更好地搜索,你可以将搜索知识打包为一个技能:针对特定任务的一组指令(例如使用哪个索引、如何组织查询语句以及如何格式化引用)。

在任务需要之前,智能体只能看到每个技能的名称和简短描述;当任务需要时,它会加载完整指令,因此小众知识在需要时可用,其余时间不占用上下文(这种技术称为渐进式披露)。这里有一个视频讲解。

__wf_reserved_inherit

渐进式披露(动画)

2. 一次上下文过多

然而,尽管优化了搜索过程,即使是正确的搜索可能返回过多内容。我们的智能体只需要市场数据导出中的一个数字(Company B 的季度营收),但加载全部 40,000 行来找到它,会填满它的上下文窗口。我们的检索工具也有同样的问题:如果它把每篇新闻文章都直接返回给模型,那么五家公司每次搜索四条结果,很快就会累积起来。

一次一页地读取大文件

文件系统工具的设计目标就是尽可能少返回内容。read_file一次读取文件的一页(默认 100 行),智能体可以设置 offset(从哪一行开始)和 limit(读取多少行)来跳到它需要的部分。因此,要找到 Company B 的营收,智能体会先用 grep 搜索它,然后只读取匹配位置附近的几行。

__wf_reserved_inherit

让 harness 卸载过大的结果

有时工具返回的内容还是会超出预期。当工具结果过大(超过约 20,000 个 token)时,harness 会把它保存到 /large_tool_results/ 下的一个文件中,并把路径加上一段简短预览(开头和结尾的几行)交给智能体。智能体随后可以用read_file的 offset 和 limit 只读回它需要的部分。

__wf_reserved_inherit

让你的检索工具返回文件路径

你可以把同样的思路应用到自己的检索工具上,而不必等待触及大小限制:让它把每个结果写入一个文件,只返回路径。这是名为“retrieve, offload, delegate”的模式的第一部分(LangChain 的 RAG 指南对此有完整讲解)。

__wf_reserved_inherit

下面是一个简化版的检索工具。它执行搜索,把每个结果保存为智能体后端中的一个文件,并且只返回文件路径列表:

backend = StateBackend()@tooldefsearch_news(query:str) ->str:"""搜索新闻档案,并将每个结果保存到一个文件。"""docs = vector_store.similarity_search(query, k=4)files = [(f"/retrieved/result_{i}.md",doc.page_content.encode())fori, docinenumerate(docs)]backend.upload_files(files)return"已将结果保存到:"+", ".join(pathforpath, _infiles)

现在主智能体看到的是四个短路径,而不是四篇完整文章。

3. 无关或过时的上下文

如果结果是错误的结果。这四个文件中的任何一个都可能是关于另一家名称相似的公司,或者是两年前的报道。跑题或过时的文本对模型来说,看起来和正确答案一样有说服力,所以目标就是把它完全挡在主智能体的上下文之外。

让子智能体过滤结果

然而,主智能体仍然必须读取这些文件才能找出哪些是相关的(而且它打开的每一篇跑题文章都会进入它的上下文)。这就是“检索、卸载、委派”模式中“委派”部分的用武之地:一个子智能体读取每个检索到的文件,只返回关键事实。

  1. 定义子智能体:description 告诉主智能体何时使用它(每次调用处理一个文件),system prompt 告诉子智能体该如何处理该文件。
chunk_analyst = {"name":"chunk-analyst","description":"分析一个检索到的文件。传入问题和 /retrieved/ 下的单个路径。","system_prompt":"使用 read_file 读取指定的文件。返回关键事实和来源 URL。",}
  1. 把检索工具和子智能体交给主智能体。传入你在search_news中使用过的同一个 backend,这样子智能体就能读取该工具保存的文件。
agent = create_deep_agent(model=model,tools=[search_news],backend=backend,# 同一个实例,因此 read_file 能看到 /retrieved/subagents=[chunk_analyst],)

现在的流程是:

  1. 搜索:主智能体调用search_news并拿回四个路径。
  2. 委派:它把每个路径发送给一个 chunk-analyst 子智能体,它们可以并行运行。
  3. 综合:每个分析员返回一段带来源的简短摘要。主智能体将它们合并,如果有缺失就再次搜索。

__wf_reserved_inherit

原始结果写入文件,嘈杂的阅读发生在拥有各自上下文窗口的子智能体中,而主智能体只看到结论。任何跑题内容都留在子智能体的上下文中,主智能体的上下文窗口保持干净!

从最新的来源获取

过滤确实能把跑题结果挡在外面,但它无法修正过时的结果。我们的竞品分析智能体每周一运行,所以上个月的索引抓不到本周的新闻。

一个解决方案:搜索 API 或 MCP 服务器可以为智能体提供当前信息(而不是几个月前索引的任何内容)。如果你的数据带有日期,在检索工具中按日期过滤也能把旧结果排除在外。

小心检索到的文本

反过来,相关的当前结果也可能带来问题!由于检索到的文本是数据而非指令,你检索到的任何内容都可能包含提示注入(而且没有任何提示词或分隔符策略能完全防止这一点)。务必验证输出:检查答案是否引用了你预期的来源,以及论断是否与来源相符。

需要留意的检索策略

以下是你需要留意的地方(无论你基于什么框架构建):

  • 使用能奏效的最高效工具。如果你知道确切的词,grep 胜过嵌入查找。把语义搜索留到你真正需要它的时候。
  • 分页加载而非一次性加载。分块读取大文件,而不是一次性全部读入。
  • 返回指针而非完整内容。把检索到的内容写入文件,并把路径交给智能体。
  • 委托阅读。让子智能体去阅读和总结,这样主智能体只看到结论。
  • 验证依据。对照检索到的内容检查答案,并将检索到的文本视为不可信。

试一试

uv add deepagents

想向现场讲师学习更多?我将在 11 月 9 日讲解这些内容:注册此处!

‍

检索只是故事的一半:信息如何找到通往智能体的路径。当智能体随着时间推移开始积累信息时,我们如何管理它的上下文窗口?

了解更多请查看: