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

LangChain 详解 Deep Agents 的上下文工程:如何记住而不塞满上下文

Context Engineering with Deep Agents: How To Remember Without Keeping Everything In Context

AI 导读

LangChain 发布博客,介绍其开源 harness Deep Agents 如何用上下文工程缓解长任务中的上下文腐化。文中给出五种手段:工具结果与超长用户消息卸载到文件系统(阈值约 20,000 与 50,000 token)、接近 85% 上下文窗口时压缩旧消息且摘要可回读、技能按需加载、子智能体隔离嘈杂工作、长期记忆写入 markdown 文件跨会话保留。

推荐理由

LangChain 官方拆解 Deep Agents 的五种上下文工程手段与 middleware 挂载点,可迁移到自建 harness。

正文 · AI 翻译

如果你曾在考试前一晚临时抱佛脚,你就知道那种感觉:你学得越多,前面几章的内容就越开始从记忆中溜走。 

智能体也有同样的问题。当它们承担更长的任务时,会接收越来越多的信息(指令、工具结果、文件等);最终这会变成远超模型上下文窗口舒适容纳范围的上下文。模型携带的历史记录、噪声和工具输出越多,其准确性和推理能力往往就越差。

在智能体工程领域,我们称之为上下文腐化。本博客介绍管理智能体上下文窗口的技术,以及 Deep Agents 框架如何使用一种称为中间件的概念将它们注入到智能体的运行中。

‍

理解上下文工程技术

简单来说,智能体就是一个被框架包裹的模型。 

框架 = 包裹模型(LLM)

智能体可用来应对上下文腐化的上下文工程技术主要分为两类:

  • 防止不必要的信息进入上下文窗口
  • 管理并过滤已经在上下文窗口中的内容

框架会策略性地应用这些方法,以便针对给定任务让模型在正确的时间获得正确的上下文。

现在,要搭建一个能出色完成这项工作的框架,说起来容易做起来难,需要大量工作(如果你是那种考试前一晚临时抱佛脚的人,可能工作量太大了)。 

Deep Agents 是 LangChain 的开源框架。它用各种技术包裹你选择的模型,将上下文移出活动窗口(转移到文件、摘要或单独的智能体中),这样主智能体只携带它需要的内容(并且可以在之后需要时恢复原始数据):

  1. 卸载:大型工具结果和用户消息被保存到文件系统,模型只看到预览。
  2. 摘要:当对话接近上下文限制时,较早的消息被压缩成摘要。
  3. 技能:任务知识只在模型认为相关时才加载。
  4. 子智能体:嘈杂的工作在单独的上下文窗口中运行,只返回结果。
  5. 长期记忆:智能体学到的东西被保存到文件中,并延续到下一次对话。

‍

将上下文工程应用于常见用例

让我们看看上述技术如何应用于一个真实用例:一个竞争分析智能体。每周一,这个智能体研究五个竞争对手,并按照团队模板撰写报告。单次运行大致如下:

  1. 拉取公司 A 的市场数据导出(数万行)
  2. 搜索新闻,阅读几篇文章,并做笔记
  3. 对公司 B 到 E 重复上述步骤
  4. 起草报告,根据你的反馈修改,并格式化

到第 4 步时,智能体在每次模型调用中都携带了五份原始导出数据、数十条搜索结果和两份过时的草稿。其中大部分内容与格式化最终报告无关。

检索方法(RAG 或文件系统搜索)能帮助智能体只拉取它需要的内容,但这仍然只解决了一半问题。即使是一个精确的智能体也会积累不必要的消息、工具结果和中间工作,因此框架还必须管理已经存在于上下文窗口内及其周围的内容。

‍

如何实现这些技术:中间件

Agent 在一个循环中运行:模型决定要做什么、调用工具、读取结果,然后重复,直到任务完成。中间件是接入该循环中特定位置的代码:例如在 agent 启动之前、每次模型调用前后,或每次工具调用前后。

  • 在每个位置,它都可以读取上下文,并在下一步运行之前修改上下文。 
  • Every context engineering technique in this post is a piece of middleware hooked into one of those points. 
    • 其中大部分都是内置的,因此你无需编写任何中间件即可使用这些技术(除非你想自定义某些特定内容)。

‍

卸载:将其排除在上下文窗口之外

我们的 agent 并不总是需要看到每次工具调用或人类消息返回的所有数据,尤其是在数据量很大的情况下。

例如,每次 pull_market_report 工具调用都会返回完整的数据导出(数千行);然而,agent 只需要其中的几个数字,因此可以限制模型只看到这些行。

工具结果卸载过程(动画)

FilesystemMiddleware 在每次调用工具时运行:

  1. 当工具结果超过约 20,000 个 token 时,完整结果会在进入对话之前写入 /large_tool_results/{tool_call_id}。 
  2. 模型只能看到一段简短预览(开头和结尾的几行)以及文件路径。 
  3. 当 agent 稍后需要公司 B 的季度收入时,它会使用 read_file 分页读取该文件。

过大的用户/人类消息,例如粘贴的通话记录,会以更高的阈值(50,000 个 token)得到同样的处理,并保存到 /conversation_history/。

‍

摘要:压缩已有内容

尽管我们尽力卸载冗长的工具调用,但在长时间运行中,上下文仍会逐渐堆积(例如五家公司的搜索、笔记和草稿)。摘要可用于将我们 agent 的整个上下文窗口浓缩为迄今为止所发生情况的摘要,使其能够继续执行研究任务。

卸载阻止了小的尖峰,而摘要则“重置”了整体的攀升
  • 尖峰是大型工具结果/人类消息到达;卸载会在模型看到之前将每一个替换为简短预览,因此上下文只会增长一点点。 
  • 摘要处理缓慢攀升:一旦使用量达到 85%,较旧的消息会被浓缩为摘要,上下文随之回落。

SummarizationMiddleware 在每次模型调用之前检查 token 使用量:

  1. 一旦使用量超过阈值(默认是模型上下文窗口的 85%),就会进行一次单独的 LLM 调用,将较旧的消息浓缩为单个摘要
  2. 较旧的消息会从上下文窗口中移除,而最近的消息则逐字保留。 
  3. 模型现在会先看到摘要,然后看到最近的消息(因此最近的内容不会被改写)。

两个重要细节:

  • 它是可恢复的。被摘要的消息会写入 /conversation_history/{session_id}.md。如果 agent 在起草时需要公司 A 研究中的某个细节,它可以将其读回。
  • 它会循环。随着运行继续,使用量会再次攀升,而下一个摘要会包含前一个摘要。

你也可以在主题变化时进行摘要,而不仅仅是在某个 token 阈值时。自定义摘要提示词可能会有所帮助。

‍

卸载 摘要
在输入进入上下文之前拦截输入 作用于已在上下文窗口中的内容

我们关于Deep Agents 上下文管理的文章更详细地介绍了这两者。

技能:仅在需要时加载信息

假设你的销售团队对季度业务回顾有特定的格式。智能体每季度只需要这些指令一次,因此把它们放在系统提示中意味着每次调用都要为它们付费(以 token 和响应时间计)。相反,你可以将这些信息打包成一个技能。

技能是一个指令文件夹(可选包含脚本和参考文档),为智能体提供完成某项专门任务所需的内容。

技能文件夹的剖析

智能体一开始只会看到一个简短的描述,只有当 QBR 出现时才会加载 qbr-report 的完整指令(其他任务永远不会携带它们)。 

技能分层加载上下文,因此窗口会一直保持很小,直到某些内容真正相关。这被称为渐进式披露:模型一开始只看到一个“菜单”,在需要时才拉入完整的配方,因此十个技能消耗的上下文大约相当于十条单行说明。

渐进式披露(动画)

你将智能体指向你的技能,SkillsMiddleware 使用两个钩子:

  1. 在智能体启动之前,它会扫描技能文件夹,只读取每个 SKILL.md 的头部。
  2. 在每次模型调用时,它会将这些名称和描述添加到系统提示中。

只有当模型判断某个技能相关时,完整指令才会进入上下文(并通过 read_file 工具读取),因此我们的智能体在没有模板的情况下进行研究,只有在需要格式化 QBR 时才加载它。

‍

子智能体:隔离嘈杂的工作

卸载可以有效应对过大的结果,但对于你已知会很嘈杂的工作(比如通过数十次搜索和页面研究一家公司),子智能体是一个很好的设计选择。子智能体在自己的上下文窗口中完成这些工作,只返回最终结果,这样主智能体就永远不会看到“噪音”。

SubAgentMiddleware 允许我们为子智能体自定义系统提示和可用工具。

market_researcher = {
    "name": "market-researcher",
    "description": "Researches a company and returns the 3 most notable developments.",
    "system_prompt": "Use pull_market_report, then return 3 short bullet points.    Do not return the raw report.",
    "tools": [pull_market_report],
}

agent = create_deep_agent(model=model, subagents=[market_researcher])

在我们的案例中,主智能体将每家公司交给 market-researcher 子智能体。 

研究子智能体在自己的上下文中阅读完整报告,只返回三个要点,因此主智能体只会看到这三个要点(甚至看不到原始报告的预览)。此外,由于子智能体任务往往规模小且范围明确,它们通常可以运行在更便宜的模型上并并行执行。

‍

长期记忆:跨对话持久化

到最后,你的智能体将学到很多东西。有些有用,有些没用。 

假设你告诉你的智能体:“在摘要中使用项目符号,而不是段落。”如果没有记忆,你可能每周都要重复这句话。在 Deep Agents 中,长期记忆是一个 markdown 文件。你用一个参数将智能体指向它,MemoryMiddleware 会处理其余的事情:

  1. 加载:该文件在运行开始时被读取。
  2. 注入:其内容会在每次模型调用时添加到系统提示中。
  3. 更新:当智能体学到值得保留的东西时,比如你的格式偏好,它会更新该文件。

‍

更换模型,保留记忆

Deep Agents 与提供商无关,这一点也延续到了记忆上。因为记忆是你自己存储中的一个普通文件(而不是提供商持有的东西),你可以将同一个后端指向不同的模型:

agent = create_deep_agent(
    model="openai:gpt-6",  # different provider, same memory
    memory=["/memories/AGENTS.md"],
    backend=backend,
    store=store,
)

下周,另一个模型可以读取同一个文件,并且仍然用项目符号写摘要。

需要牢记的上下文工程策略

无论你是基于 Deep Agents 构建、使用其他框架,还是自己构建一个,以下是一些需要注意的事项:

  • 大输入应当卸载,而非截断。截断是有损的;卸载则保持其可检索。
  • 压缩应当可恢复。摘要之后,智能体仍能取回所需的细节。
  • 知识应当按需加载。把每项任务的指令都放进系统提示词,会在每次调用时消耗上下文。
  • 嘈杂的工作应当隔离。拥有独立上下文的子智能体能让主智能体保持专注。
  • 你应当掌控记忆。存储在你所控制的存储中的记忆,能在模型或提供商切换后依然存续。
  • 默认值应当可自定义。适合某一任务的阈值,很少能适合所有任务。

试一试

uv add deepagents

‍

想向真人讲师学习更多?我将在11月3日讲解这些内容:点此处注册!

‍

来源:LangChain Blog · langchain.com