LangChain 的 Jess Weng 以一个竞品分析 Agent 为例,说明怎样搜索、分页读取和筛选材料,避免检索结果挤满上下文。以下是全文翻译。
要点
-
Agent 的上下文窗口有限,而信息检索通常会在三个地方出问题:搜索效率低、一次塞入太多上下文,以及引入不相关或过期的信息。 -
Deep Agents 的运行框架默认就会控制搜索结果的体量。内置文件系统工具先搜索再读取: glob匹配文件名,grep找出匹配的行,read_file则按页读取。过大的工具结果会被卸载到文件中:完整内容留在文件里,Agent 只收到路径和预览。 -
对于 RAG 等自定义检索工具,可以采用“检索、卸载、委派”的方式:把结果保存为文件,返回路径,再让子 Agent 读取。主 Agent 只接收相关内容。
想象一下,你正在参加一场开卷考试:理论上,所有答案都在手边,但找到正确答案反而更难了。
-
应该从哪里开始找? -
什么时候才能确定,找到的资料是对的? -
怎样安排时间,才不会把精力都耗在重复搜索上?
高效找到正确的信息,并没有那么简单。
给 Agent 一个问题,它也会遇到类似的困境:上下文窗口有限,所以和人一样,它需要通过检索来完成某些任务,而不能把所有东西都记在脑子里。搜索时,Agent 常常遇到这些问题:
-
搜索效率低:每次搜索都要消耗 token 和时间。如果 Agent 搜索得很笨拙,比如挨个打开文件看里面有什么,或者不断改写同一个查询,那么还没找到有用信息,上下文就已经被搜索结果塞满了。 -
一次塞入太多上下文:一次搜索就返回整份数据导出,或者十几篇文档,会瞬间占满上下文窗口,真正的工作反而没有空间可用。 -
上下文不相关或已过期:对模型来说,过时或跑题的信息看起来可能和正确答案一样可信。一旦进入上下文窗口,Agent 就可能在它的基础上继续推理,让错误沿着后续步骤一路传下去。
好在 Agent 有多种办法可以检索到正确的信息。Agent 运行框架的职责,就是根据任务有策略地运用这些方法,在合适的时间,把合适的上下文交给模型。
本文将介绍 Agent 检索面临的挑战,也就是如何只找到并取回当前任务必需的信息;随后讨论可以采用的方法,以及设计 Agent 运行框架时应该怎样考虑这些问题。我们会以 LangChain 的 Deep Agents 为例。
如果想了解信息进入上下文窗口后该怎样管理,可以阅读另一篇关于上下文工程的文章。
用一个竞品分析 Agent 举例
我们来看一个实际场景:竞品分析 Agent。每周一,它都会调研数百家竞争对手,按照团队模板写出报告。它需要:
-
找到上周关于 A 公司的笔记。 -
从一份庞大的市场数据导出中,查出某个具体的营收数字。 -
搜索新闻档案,看看有没有新消息。
这三项工作对应不同的检索方式,也会遇到不同的问题。我们使用 Deep Agents 运行框架构建这个 Agent,因此可以直接利用几种内置方法来减少这些问题。
1. 搜索效率低
Agent 要为每家竞争对手找到三类信息。如果每一项都得尝试十几次,比如打开文件才知道里面是什么,或者因为资料用了不同的说法而反复改写查询,那么报告一个字都还没写,上下文窗口就已经装满了失败的搜索。
因此,最好的搜索是不必重复的搜索。这意味着,要为每项工作选对工具。
先用内置文件系统工具
文件系统是 Agent 查找信息最常用的途径之一。但如果 Agent 每次都打开文件看看里面有什么,效率就会很低:每读一个文件,其内容都会进入上下文窗口,光是找资料就足以把窗口塞满。
更好的办法是先检查文件名和关键词,就像开卷考试时先翻书后的索引,而不是逐页读完整本书。两种常见的文件系统工具正好可以完成这件事:
glob按文件名模式查找文件。例如, **/company-a*.md可以找到上周关于 A 公司的笔记。grep在文件内部搜索,而不把文件内容加载进上下文窗口。它匹配字面文本,可以返回匹配的文件、匹配的行,或者匹配次数。
每个 Deep Agent 都内置了这些工具,以及 ls、read_file、write_file 和 edit_file。无论文件存放在 Agent 状态、本地磁盘、持久化存储还是沙箱中,使用的都是同一套工具,因为它们都会经过 Agent 的 backend。
关键词不够用时再添加检索工具
知道要找哪些词时,grep 很好用,但它只能匹配你给出的确切文本。这会影响例子中的第三项工作:搜索新闻档案。如果 Agent 搜索“Company A funding”,就会漏掉标题为“Company A closes $50M Series B”的文章,尽管“A 公司完成 5000 万美元 B 轮融资”正是它要找的消息。
如果希望按含义而不是确切用词进行搜索,也就是语义搜索,就可以添加一个由向量存储支持的检索工具。凡是 Agent 可以调用来获取信息的工具,都可以算检索工具:它接收查询,在某个数据源中查找,再返回找到的内容,也就是最接近的匹配结果。
不过,转向语义搜索之前,可以先试试同义词。grep 只能匹配给定的词,但选择这些词的 Agent 已经知道,“funding”也可能写成“Series B”或“raised”。告诉它搜索无果时换个说法,它就能针对每个词分别运行一次 grep。对于只有团队内部才使用的词,比如内部产品名,可以把它们列在一个 skill 中。用词可预测时,这种方法很有效;无法预测时,才需要语义搜索。
检索工具怎样匹配,取决于背后的数据源。向量存储按含义匹配,但检索工具也可以连接其他数据源:
-
SQL 数据库:用于结构化查询,例如销售数据或客户记录。 -
搜索 API:用于获取网上的最新信息。 -
MCP 服务器:用于调用别人已经构建好的工具,例如文档搜索。
想进一步了解语义搜索的工作原理,可以阅读 LangChain 的相关指南。
让 Agent 决定何时搜索
很多检索系统会在开始时搜索一次,然后给出答案。这适用于 FAQ 机器人,也就是两步式 RAG,却不适合我们这个 Agent。假设它调研 C 公司到一半,发现了一篇此前不知道的产品发布报道,于是又需要查询该产品的价格;一开始它根本不可能预先知道要查这个。如果只允许搜索一次,最后的报告就会漏掉这部分信息。
研究型 Agent 需要的是 Agent 驱动的检索,而不是两步式 RAG:由 Agent 自己决定何时搜索、搜索什么,以及是否还需要更多信息。Deep Agents 默认就是这样工作的:Agent 在一个循环中运行,可以按需多次调用任何工具。
把搜索经验封装成 skill
为了帮助 Agent 更好地搜索,可以把搜索经验封装成一个 skill:一组针对特定任务的指引,例如该用哪个索引、怎样组织查询,以及引用采用什么格式。
Agent 起初只会看到每个 skill 的名称和简短描述,只有任务需要时,才加载完整指引。这样,专门知识在需要时随时可用,平时又不会占据上下文。这种方法叫渐进披露。相关视频解释也可以帮助理解。
渐进披露演示的静态预览 动画见原文
2. 一次引入太多上下文
然而,即使优化了搜索过程,正确的搜索也可能返回太多内容。Agent 只需要市场数据导出中的一个数字,比如 B 公司的季度营收,但为了找到它而加载全部 4 万行数据,就会占满上下文。自定义检索工具也有同样的问题:如果每次搜索都把所有新闻文章直接交给模型,五家公司、每次四篇文章,很快就会堆积起来。
大文件按页读取
文件系统工具的设计,就是尽量少返回内容。read_file 一次读取一页,默认是 100 行;Agent 可以设置 offset,也就是从哪一行开始,以及 limit,也就是读多少行,直接跳到需要的位置。因此,要找 B 公司的营收,可以先用 grep 定位,再只读取匹配位置附近的几行。
让运行框架卸载过大的结果
有时,工具返回的内容仍然会超出预期。当一个工具结果过大,超过约 2 万 token 时,运行框架会把它保存到 /large_tool_results/ 下的文件中,只把路径和一小段预览交给 Agent,预览包含开头和结尾的几行。随后,Agent 可以用 read_file 的 offset 和 limit,按需取回其中的部分内容。
让检索工具返回文件路径
你也可以在自定义检索工具中主动采用同样的方式,而不用等到触及大小限制:把每条结果写入文件,只返回路径。这是“检索、卸载、委派”模式的第一部分,LangChain 的 RAG 指南对完整流程有详细介绍。
下面是一个简化的检索工具。它执行搜索,把每条结果保存到 Agent 的 backend 中,然后只返回文件路径列表:
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)
现在,主 Agent 看到的是四条简短路径,而不是四篇完整文章。
3. 上下文不相关或已过期
如果结果是错的,体量再小也没用。这四个文件中,任何一个都可能讲的是另一家名字相近的公司,或者两年前的旧闻。对模型来说,跑题或过时的文本可能和正确答案一样可信,因此目标是根本不让它们进入主 Agent 的上下文。
让子 Agent 筛选结果
不过,主 Agent 仍然得读这些文件,才能知道哪些有用;而它打开的每篇无关文章,也都会进入上下文。这就轮到“检索、卸载、委派”中的“委派”了:让一个子 Agent 阅读每个检索到的文件,只返回关键事实。
-
定义子 Agent:description 告诉主 Agent 什么时候使用它,每次只交给它一个文件;system prompt 则告诉子 Agent 该怎样处理这个文件。
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.",
}
-
给主 Agent 配上检索工具和这个子 Agent。传入的 backend,要与 search_news使用的是同一个实例,这样子 Agent 才能读到工具保存的文件。
agent = create_deep_agent(
model=model,
tools=[search_news],
backend=backend, # same instance, so read_file can see /retrieved/
subagents=[chunk_analyst],
)
现在,流程变成了:
-
搜索:主 Agent 调用 search_news,拿到四条路径。 -
委派:它把每条路径交给一个 chunk-analyst 子 Agent,这些子 Agent 可以并行工作。 -
综合:每个分析子 Agent 返回一小段摘要及其来源。主 Agent 汇总这些内容,如果还有缺失,就继续搜索。
原始结果写入文件,容易夹杂噪声的阅读工作由拥有各自上下文窗口的子 Agent 完成,主 Agent 只接收结论。不相关的内容留在子 Agent 的上下文中,主 Agent 的上下文窗口就能保持清爽!
使用最新的数据源
筛选可以排除无关结果,却无法修复过期的结果。我们的竞品分析 Agent 每周一运行一次,上个月建立的索引捕捉不到本周的新消息。
一种办法是使用搜索 API 或 MCP 服务器,为 Agent 提供当前信息,而不是几个月前索引中的内容。如果数据带有日期,也可以在检索工具中按日期过滤,挡住旧结果。
有时,索引里同时存在某篇文档的新旧版本,而旧版往往更容易胜出,因为旧文档通常更长、更详细,排名也就更高。人看到“最后更新于公元前 12 年”就会跳过,但文档被切块放入向量存储时,这个日期往往会丢失。如果检索工具能拿到日期,就把它放进返回的文本,让模型看得到。随后,让子 Agent 把日期和关键事实一起返回,并标出可能过时的内容。
小心处理检索到的文本
另一方面,相关而且最新的结果也可能带来问题!检索文本是数据,不是指令,所以任何取回的内容都可能包含提示注入,而且没有任何提示词或分隔符策略能够完全防住它。一定要验证输出:检查答案是否引用了预期的来源,以及其中的主张是否与来源相符。
值得记住的检索策略
无论使用哪种运行框架,都可以关注以下几点:
-
使用能完成任务的最高效工具。知道具体用词时, grep比 embedding 查询更合适;真正需要时,再使用语义搜索。 -
分页读取,不要一次加载。把大文件拆成小块来读,而不是全部塞进上下文。 -
返回指针,不返回完整载荷。把检索内容写进文件,再把路径交给 Agent。 -
委派阅读。让子 Agent 阅读并归纳,主 Agent 只接收结论。 -
核验依据。对照检索到的内容检查答案,并把检索文本视为不可信数据。
动手试试
uv add deepagents
想跟着讲师现场学习更多内容吗?我将在 11 月 9 日讲解这些内容,可以通过原文中的链接报名!
检索只讲了故事的一半:信息怎样抵达 Agent。那么,随着信息不断积累,又该怎样管理 Agent 的上下文窗口?
想进一步了解,可以阅读:
-
关于 Deep Agents 上下文工程的博客。 -
LangChain Academy 的 Deep Agents 课程,免费、在线,而且可以按自己的节奏学习。
参考链接
-
原文:https://www.langchain.com/blog/context-engineering-starts-with-retrieval -
agent harness:https://www.langchain.com/blog/the-anatomy-of-an-agent-harness -
LangChain’s Deep Agents:https://www.langchain.com/deep-agents -
blog on Context Engineering:https://www.langchain.com/blog/context-engineering-deep-agents-remember -
Deep Agents harness:https://docs.langchain.com/oss/python/deepagents/overview -
these tools built in:https://docs.langchain.com/oss/python/deepagents/tools -
backend:https://docs.langchain.com/oss/python/deepagents/backends -
Grep:https://reference.langchain.com/python/deepagents/backends/protocol/BackendProtocol/grep -
retrieval tool:https://docs.langchain.com/oss/python/deepagents/retrieval -
vector store:https://docs.langchain.com/oss/python/integrations/vectorstores -
MCP server:https://docs.langchain.com/oss/python/deepagents/tools -
LangChain’s guide:https://docs.langchain.com/oss/python/langchain/knowledge-base -
2-step RAG:https://docs.langchain.com/oss/python/deepagents/retrieval -
agentic retrieval:https://docs.langchain.com/oss/python/deepagents/retrieval -
skill:https://docs.langchain.com/oss/python/deepagents/skills -
progressive disclosure:https://docs.langchain.com/oss/python/deepagents/skills -
video explanation:https://www.youtube.com/watch?v=bv1qgkbWhms -
LangChain’s RAG guide:https://docs.langchain.com/oss/python/deepagents/rag -
subagent:https://docs.langchain.com/oss/python/deepagents/subagents -
prompt injection:https://docs.langchain.com/oss/python/deepagents/rag -
HERE:https://events.langchain.com/register/agent-memory-102-context-retrieval-storage/ -
context engineering with Deep Agents:https://www.langchain.com/blog/context-engineering-deep-agents-remember -
Deep Agents course:https://academy.langchain.com/courses/foundation-introduction-to-deepagents

