跳到正文
LangChain Blog·· 2 小时前精选AI 评分60

LangChain 详解 AI 智能体的上下文工程为何从检索开始

Context Engineering for AI Agents Starts with Retrieval

AI 导读

LangChain 发布博客,提出 AI 智能体的上下文工程应从检索入手,并以一个每周运行的竞品分析智能体为例说明三类检索难题。

推荐理由

LangChain 官方以竞品分析智能体为例,拆解检索环节的三类上下文问题与对应工具组合,可迁移到自建 harness 的设计。

正文 · AI 翻译

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

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

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

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

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

幸运的是,智能体可以使用多种技术来检索正确的信息。agent harness 的职责就是策略性地应用这些技术,为给定任务在正确的时间把正确的上下文交给模型。

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

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

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

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

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

以上每一项都是不同类型的检索,因此面临不同的问题。我们使用 Deep Agents harness 构建了我们的智能体,因此可以直接应用一些技术来尽量减少这些问题。

1. 低效搜索

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

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

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

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

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

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

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

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

Grep 在你明确知道要查找哪些词时非常有用,但它只匹配你提供的精确文本。这对我们示例中的第三项任务来说是个问题:在新闻档案中搜索任何新内容。如果我们的智能体用 grep 搜索“A 公司融资”,就会漏掉一篇标题为“A 公司完成 5000 万美元 B 轮融资”的文章,尽管那正是它要找的内容。

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

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

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

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

让智能体决定何时搜索

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

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

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

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

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

渐进式披露(动画)

2. 一次性上下文过多

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

一次一页地读取大文件

文件系统工具的设计目标是尽可能少地返回内容。read_file一次读取文件的一页(默认100行),智能体可以设置偏移量(从哪一行开始)和限制(读取多少行)来跳转到它需要的部分。因此,要找到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. 无关或过时的上下文

如果结果是错误的,那么结果小也无济于事。这四个文件中的任何一个都可能涉及另一家名称相似的公司,或者是两年前的报道。离题或过时的文本对模型来说看起来和正确答案一样有说服力,因此目标是将它完全排除在主智能体的上下文之外。

让子智能体过滤结果

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

  1. 定义子智能体:description告诉主智能体何时使用它(每次调用处理一个文件),system prompt告诉子智能体如何处理该文件。‍
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.",
}
  1. 把检索工具和子智能体交给主智能体。传入你在search_news中使用的同一个后端,这样子智能体就能读取该工具保存的文件。
agent = create_deep_agent(
		model=model,
		tools=[search_news],
		backend=backend,  # same instance, so read_file can see /retrieved/
		subagents=[chunk_analyst],
)

‍现在的流程是:

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

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

从最新来源获取

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

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

小心检索到的文本

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

需要留意的检索策略

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

  • 使用最有效且可行的工具。如果你知道确切的词,grep 比嵌入查找更胜一筹。把语义搜索留到真正需要时再用。
  • 分页读取,而非一次性加载。把大文件分块读取,而不是一次全部读入。
  • 返回指针,而非内容本身。把检索到的内容写入文件,把路径交给 agent。
  • 把阅读工作委派出去。让子 agent 去阅读和总结,这样主 agent 只看到结论。
  • 验证依据。对照检索到的内容检查答案,并把检索到的文本视为不可信。

试一试

uv add deepagents

想跟着真人讲师深入学习?我将在 11 月 9 日讲解这些内容:点此处注册!

‍

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

了解更多请查看:

来源:LangChain Blog · langchain.com