要点
- 智能体(Agent)在有限的上下文窗口内工作,而信息检索会以三种方式失效:搜索效率低下、一次获取过多上下文、以及上下文不相关或过时。
- Deep Agents 框架开箱即用地将搜索保持在较小的规模。内置的文件系统工具在读取之前先搜索:glob 匹配文件名,grep 查找匹配的行,read_file 一次读取一页。过大的工具结果会被卸载:完整结果保存到文件中,但智能体只会收到路径和预览。
- 对于自定义搜索工具(如 RAG),请使用“检索、卸载、委派”:将结果保存到文件中,返回路径,并让子智能体去读取它们。主智能体只看到相关的内容。
想象一下你正在参加一场开卷考试:理论上你拥有所有答案,但不知为何,这反而让你更难找到 正确 的那一个。
- 你该从哪里开始搜索?
- 你如何知道你的资料来源是正确的?
- 你如何优化时间,以免把全部精力浪费在重复的搜索上?
高效地找到正确的信息并非易事。
给智能体一个问题时,它会面临类似的困境:它的上下文窗口有限,所以和我们人类一样,它必须 检索 信息来完成某些任务(而不是把一切都记住)。在搜索时,智能体会遇到以下问题:
- 搜索效率低下: 每次搜索都要消耗 token 和时间。一个搜索方式笨拙的智能体(例如打开每个文件只是为了看看里面有什么,或者不断改写同一个查询)可能在找到任何有用内容之前就用搜索结果填满了上下文。
- 一次获取过多上下文: 一次搜索就倾倒完整的数据导出或十几份文档,会一下子填满上下文窗口,不给工作本身留下任何空间。
- 上下文不相关或过时: 过时或离题的信息在模型看来可能与正确答案一样有说服力。一旦它进入上下文窗口,智能体可能会基于它进行构建(而这个错误会在之后的每一步中延续)。
幸运的是,智能体可以使用多种技术来检索 正确的 信息。 智能体框架 的工作就是策略性地应用这些技术,为模型在给定任务时提供 在正确的时间获得正确的上下文 。
这篇博客介绍了智能体检索的挑战(仅查找和拉取手头任务所需的信息)、我们可以用来应对这些挑战的技术,以及在设计智能体框架时如何牢记这些要点(以 LangChain 的 Deep Agents 框架为例)。
关于在上下文 英寸 上下文窗口,请查看这个 关于上下文工程的博客.
我们的示例:一个竞争分析智能体
让我们看看如何在一个真实的使用场景中应用这一点:一个竞争分析智能体。每个星期一,该智能体会研究数百个竞争对手,并按照团队的模板撰写一份报告。它需要:
- 查找上周关于 Company A 的笔记
- 在庞大的市场数据导出文件中查找特定的营收数字
- 在新闻档案中搜索任何新内容
这些各自都是一种不同类型的检索,因此面临不同的问题。我们使用以下方式构建了我们的智能体: 深度智能体(Deep Agents)利用 所以我们可以直接应用一些现成的技巧来尽量减少这些问题。
1. 低效的搜索
我们的智能体需要为每个竞争对手找到三样东西。如果每样都要尝试十几次(打开文件只为看看里面有什么,或者因为源文件用词不同而重新措辞搜索),那么在它还没写出一个字的报告之前,上下文窗口就已经被失败的搜索填满了。
因此,最好的搜索是智能体不必重复的那种,这意味着要为每项工作使用合适的工具。
从内置的文件系统工具开始
智能体搜索信息最常见的方式之一是使用文件系统。然而,如果我们的智能体打开每个文件来查看里面的内容,它的搜索效率就非常低:它读取的每个文件都会进入其上下文窗口,所以光靠浏览就可能把窗口填满。
更好的方法是先检查 名称 和 关键词 (就像开卷考试时先翻书的索引而不是逐页阅读一样)。有两个常用的文件系统工具正是这样做的:
glob按名称模式查找文件。例如,**/company-a*.md可以找到上周关于 Company A 的笔记。grep在不将文件加载到上下文窗口的情况下搜索文件内部内容。它匹配字面文本,并可以返回匹配的文件、匹配的行或计数。
每个深度智能体都内置了 这些工具,同时还有 ls, read_file, write_file 和 edit_file。无论文件存放在哪里(智能体状态、本地磁盘、持久存储或沙箱),这些工具都能正常工作,因为它们都通过智能体的 后端.
当关键词不够用时,添加一个检索工具
Grep 非常适合当你知道要查找哪些词的时候,但它只能匹配你给出的 精确 文本。这对我们示例中的第三项任务来说是个问题:在新闻档案库中搜索任何新内容。如果我们的智能体用 grep 搜索 "Company A funding",它会漏掉一篇题为 "Company A closes $50M Series B" 的文章,尽管那正是它要找的内容。
要按含义而非精确词汇进行搜索(语义搜索),你需要添加一个由 向量存储 支持的 检索工具。检索工具是智能体可以调用来获取信息的任何工具:它接收一个查询,在某个数据源中查找,并返回找到的结果(最接近的匹配)。
检索工具如何匹配取决于其背后的数据源。由向量存储支持时,它按含义匹配,但检索工具也可以构建在其他数据源之上:
- SQL 数据库,用于结构化查询(如销售数据或客户记录)
- 搜索 API(用于从网络获取最新信息)
- 一个 MCP 服务器 (用于他人已经构建好的工具,如文档搜索)
要更深入地了解语义搜索的工作原理,请参阅 LangChain 的指南.
让智能体自行决定何时搜索
许多检索设置只在开始时搜索一次,然后给出答案。这对 FAQ 机器人(2-step RAG)有效,但对我们的智能体不适用。假设在研究 Company C 的过程中,它发现了一篇关于自己不知道的产品发布的文章,现在它需要该产品的定价(它在开始时不可能知道要去查找这一点)。如果只有一次搜索机会,报告就会在没有定价的情况下发出。
研究型智能体需要 智能体检索 (而不是 2-step RAG):由智能体决定何时搜索、搜索什么,以及是否需要更多搜索。使用 Deep Agents 时,默认情况下就是这样工作的:智能体在循环中运行,可以根据需要任意多次调用任何工具。
将你的搜索经验打包为一项技能
为了帮助代理更好地搜索,你可以把你的搜索经验封装为一个 技能:一组针对特定任务的指令(例如使用哪个索引、如何构造查询语句以及如何格式化引用)。
代理平时只能看到每个技能的名称和简短描述,直到某个任务需要时才加载完整指令,这样小众知识在需要时可用,而其余时间不会占用上下文(这种技术称为 渐进式披露)。这里有一个 视频讲解.
2. 一次性上下文过多
然而,即使优化了搜索流程,即便是 正确的 搜索也可能返回过多内容。我们的代理只需要市场数据导出中的一个数字(Company B 的季度营收),但加载全部 40,000 行来找到它会占满其上下文窗口。我们的检索工具也有同样的问题:如果每次搜索都把每篇新闻文章直接返回给模型,五家公司每家四个结果会迅速累积。
一次一页地读取大文件
文件系统工具的设计目标是尽可能少地返回内容。 read_file 每次读取文件一页(默认 100 行),代理可以设置偏移量(从哪一行开始)和限制(读取多少行)来跳转到它需要的部分。因此要找到 Company B 的营收,代理先用 grep 搜索,然后只读取匹配处附近的几行。
让运行框架卸载过大的结果
有时工具仍然会返回超出预期的内容。当工具结果过大(超过约 20,000 个 token)时,运行框架会将其保存到 /large_tool_results/ 目录下的文件中,并给代理提供路径和简短预览(开头和结尾的几行)。然后代理可以用 read_file的偏移量和限制只读取它需要的部分。
让你的检索工具返回文件路径
你可以将同样的思路应用于自己的检索工具,而不必等待大小限制触发:让它把每个结果写入文件并只返回路径。这是名为“检索、卸载、委派”模式的第一部分(LangChain 的 RAG 指南 有完整介绍)。
这是一个简化的检索工具。它会执行搜索,并将每个结果保存为文件,存放在智能体的 后端,并且只交回文件路径列表:
backend = StateBackend()
@tool
def search_news(query: str) -> str:
"""Search the news archive and save each result to a file."""
docs = vector_store.similarity_search(query, k=4)
files = [(f"/retrieved/result_{i}.md", doc.page_content.encode()) for i, doc in enumerate(docs)]
backend.upload_files(files)
return "Saved results to: " + ", ".join(path for path, _ in files)现在主智能体看到的是四条简短路径,而不是四篇完整的文章。
3. 无关或过时的上下文
如果只是零星的小成果,那也无济于事,即使它们是 错误 results. 那四个文件中的任何一个都可能是关于名字相似的另一家公司,或是两年前的报道。离题或过时的文本对模型来说看起来和正确答案一样有说服力,因此目标是完全不让它进入主代理的上下文。
让子代理来筛选结果
然而,主要代理 仍然 必须读取那些文件以找出哪些是重要的(而它打开的每一篇离题文章都会进入其上下文)。这正是“检索、卸载、委派”模式中“委派”部分的用武之地:a 子代理 读取每个检索到的文件,并仅返回关键事实。
- 定义子代理:描述告诉主代理何时使用它(每次调用处理一个文件),而系统提示则告诉子代理如何处理该文件。
chunk_analyst = {
"name": "chunk-analyst",
"description": "Analyze one retrieved file. Pass the question and a single path under /retrieved/.",
"system_prompt": "Use read_file to read the assigned file. Return the key facts and the source URL.",
}- 将检索工具交给主代理和子代理。传入你在前面用过的同一个后端。
search_news,以便子代理可以读取该工具保存的文件。
agent = create_deep_agent(
model=model,
tools=[search_news],
backend=backend, # same instance, so read_file can see /retrieved/
subagents=[chunk_analyst],
)现在的流程是:
- 搜索:主代理调用
search_news并返回四条路径。 - 代表:它将每条路径发送给一个分块分析子智能体,这些子智能体可以并行运行。
- 合成: 每位分析师返回一份附有其来源的简短摘要。主智能体将它们整合起来,并在缺少信息时再次进行搜索。
原始结果写入文件,嘈杂的读取过程在拥有各自上下文窗口的子代理中进行,而主代理只会看到结论。任何偏离主题的内容都留在子代理的上下文中,主代理的上下文窗口保持清爽!
从最新来源获取数据
过滤确实能把偏离主题的结果挡在门外,但它无法修复过时的结果。我们的竞争分析智能体每周一运行一次,因此上个月的索引无法捕捉本周的新闻。
一个解决方案:搜索 API 或 MCP 服务器可以为智能体提供当前的信息(而不是几个月前索引的内容)。如果你的数据带有日期,在检索工具中按日期进行筛选也能把旧结果排除在外。
小心处理检索到的文本
反过来说,相关的当前结果也可能带来问题!由于检索到的文本是数据而非指令,你检索到的任何内容都可能包含一个 提示注入 (并且没有任何提示词或分隔符策略能完全防止这种情况)。务必验证输出:检查答案引用的来源是否符合预期,以及论断是否与来源一致。
需要牢记的检索策略
以下是需要注意的事项(无论你使用什么框架来构建):
- 使用能奏效的最高效工具。 如果你知道关键词,grep 优于向量嵌入查询。把语义搜索留到你真正需要的时候再用。
- 分页读取而不是全部加载。分块读取大文件,而不是一次全部读取。
- 返回指针而不是负载数据。 把检索到的内容写入文件,并将文件路径交给智能体。
- 委托阅读。让子智能体去阅读和总结,使主智能体只看到结论。
- 验证依据。对照检索到的内容检查答案,并将检索到的文本视为不可信。
试一试
uv add deepagents想跟着讲师实时学习更多内容?我将于11月9日进行讲解:在此注册 HERE!
检索只是故事的一半:信息如何到达智能体。当智能体随时间积累信息时,我们该如何管理其上下文窗口?
想了解更多请查看:
- 博客: 使用 Deep Agents 进行上下文工程
- LangChain Academy 上的 Deep Agents 课程 (免费、在线、可自学自定进度)。