夜雨聆风学习资料网

ARTICLE · 1191019

AI Agent 入门 02|上下文管理

AI Agent 入门 02|上下文管理

FIELD NOTES / AGENT ENGINEERING

任务越长,Agent 越需要认真选择每轮输入的信息,并在失败后做出明确的恢复决策。

上一篇,我们拆解了 Agent 的模型、工具、状态和控制循环,也区分了 Tool Calling、Messages、State 与 Memory 的职责。

接下来,把这些部件放进一个稍长的任务:比如,分析代码仓库。Agent 读了多个文件,搜索了好几轮,用户还不断补充问题。消息越来越长,工具偶尔报错,原来的计划也可能走不通。

这时候,继续调用模型并不难,难的是决定两件事:下一轮到底给模型看什么,以及当前这条路径失败后该怎么办。

本篇围绕这两个问题展开:上下文窗口、摘要、裁剪、检索,以及错误分类、重试和恢复动作。

本文看点

01

一、上下文窗口:一次调用能同时看到多少信息

02

二、为什么 Agent 更容易把上下文塞满

03

三、管理上下文的三种常见方法

01

CONTEXT

一、上下文窗口:一次调用能同时看到多少信息

模型每次调用能使用的上下文都有容量限制,这就是上下文窗口(Context Window)。理解这个限制时,既要计算输入,也要为本次生成预留空间,通常以 token 为单位衡量。

上下文窗口不能直接等同于“最多能塞进去多少输入”。输入和生成内容如何共同占用窗口,要看具体模型和接口;部分模型的推理内容也消耗相关预算,最大输出长度还可能有单独限制。如果输出预算已包含推理用量,就不能再重复相加。

本轮输入通常包括:

• 系统提示词,也就是 system prompt。

• 用户消息,也就是 user message。

• assistant 的历史消息。

• tool 调用记录。

• tool 返回结果。

• 额外放进去的 memory 摘要。

• 本轮提供给模型的工具描述和参数 Schema。

因此,不能只看“用户这次问了多少字”。系统提示、对话历史、工具定义和工具返回,只要成为模型输入,就会占用相应空间。OpenAI 的函数调用文档也说明,工具定义会计入输入 token。

一个用户问题可能只有一两句话,Agent 为了回答它读进来的材料,却可能远远超过问题本身。安排下一轮调用时,要同时估算这些输入,并为生成内容留出预算。

02

CONTEXT

二、为什么 Agent 更容易把上下文塞满

最简单的一次聊天,可能只有系统提示、用户提问和模型回答。Agent 在推进任务时,还会不断增加新的信息:

• 多轮对话历史。

• 多次工具调用。

• 大段工具返回结果。

• 任务计划。

• 状态摘要。

• 长期记忆片段。

任务每向前走一步,这些内容都有可能继续累积。Agent 因而比简单聊天更容易遇到几类问题:

1

上下文越来越长,新的材料难以继续放入。

2

调用成本变高,同一批历史内容可能在后续轮次反复输入。

3

延迟变高,系统需要处理的上下文更多。

4

模型抓不住重点,关键线索混在大量旧信息里。

5

请求失败或内容被截断;如果运行时裁剪了旧历史,后面的判断还可能缺少前面已经讨论过的内容。

超过容量限制时,接口可能直接拒绝请求,也可能按配置裁剪输入,或在生成达到限制时结束。是否自动处理旧历史,取决于模型接口与运行时策略,不能假定系统总会替你妥善保留重点。

例如,用户只是继续追问“还有别的问题吗”,这句话很短;但如果系统把此前读过的所有文件、所有搜索结果和全部对话都带上,下一轮输入依然可能很大。

所以,设计 Agent 时需要持续做一件事:判断哪些信息值得留在下一轮,而不是每轮都把全部历史重新堆进去。

03

CONTEXT

三、管理上下文的三种常见方法

1. 摘要:把长内容变成可继续使用的短结论

摘要(Summarization)是把长对话或长工具结果压缩成更短的内容,让后续步骤继续使用。

假设工具返回了一大段日志,而且关联记录已经提供足够证据,可以把它提炼为几条结论:

• 调用了 3 次接口。

• 第 2 次返回 500。

• 关联记录已确认 auth 服务超时,并保留对应证据引用。

比起每轮都带上整段日志,这几条信息更容易继续放进模型的上下文。

但“第 2 次返回 500”本身不能证明 auth 服务超时。如果关联证据还没核实,摘要就应明确区分:已观察到 500,auth 超时只是待核实线索。摘要不能为了写得简短,把假设改成事实。

整理诊断类摘要时,可以保留四类内容:已知事实、待验证假设、下一步动作、原文引用。这样后续既能继续推进,也能在需要时回到证据。Anthropic 的上下文工程文章同样强调,压缩应保留关键细节和继续任务所需的信息。

摘要的价值在于:把当前还要用的内容留下,同时减少后续轮次反复携带的原文。

2. 裁剪:移除已经不重要的旧消息

裁剪(Trimming)是把对当前任务已经不重要的旧消息移出上下文。

例如:

• 已经过时的中间步骤。

• 对当前任务没有帮助的内容。

并不是所有历史都值得一直保留。某一步的探索过程曾经有用,并不代表它在之后每一轮都同样重要。

摘要保留一份短的描述,裁剪则直接减少当前还要携带的内容。选择哪种方法,要看后续是否还需要依靠这段信息继续判断。

这里移除的是下一轮的活跃上下文,不等于删除审计记录或外部证据。当前目标、硬约束、未解决的问题和必要引用,仍应优先保留。

工具调用还可能受消息协议约束:如果接口要求调用与结果配对,就不能只留下某条工具结果,却删掉关联的调用记录。裁剪应按完整交互单元处理,并遵守实际接口的消息顺序要求。

3. 检索:需要的时候再取回来

检索(Retrieval)强调按需取回。长期 memory 不必始终整份放在上下文里,可以等当前任务需要时,再取出相关内容。

例如:

• 用户问 Python 代码问题时,再取出“用户偏好 Python”。

• 用户继续之前的仓库分析时,再取出“上次分析进度摘要”。

这样,系统可以保存比单次上下文更多的信息,同时只把与当前问题相关的部分交给模型。

这也是长期 memory 常常需要按需检索的原因:保存了某条信息,不意味着每次调用都要把它带上。

补充:需要精确原文时,可以先卸载再按需取回

如果工具结果很大,而后续又可能需要精确内容,可以先把原文存到任务受控的外部存储中,只在上下文里保留短预览和引用句柄;需要时再按范围取回。

这就是信息卸载:先改变信息的存放位置,再结合检索使用。摘要减少当前携带的内容,卸载则保留可取回的原文,两者可以配合。

例如,较长的日志不必每轮全文输入;模型可以先看到一段预览和有效引用,再读取需要的部分。是否能恢复原文,取决于存储是否仍存在、引用是否有效,以及当前任务是否有读取权限。

只留下“我查过了”或一个已经失效的路径,无法代替真正可用的证据引用。

04

ENGINEERING

四、分析一个仓库时,Messages、State 和 Memory 怎么安排

假设 Agent 正在分析一个代码仓库,已经发生了这些事情:

• 读了 15 个文件。

• 调了 8 次搜索工具。

• 用户又追问了 4 轮。

• 每个工具返回的内容都很长。

如果把这些原文全部塞进 Messages,上下文很快就可能变得庞大而混乱。

一种更容易管理的组织方式,是分别看“本轮继续判断要用什么”“程序要跟踪什么”“以后还可能复用什么”。

放在 Messages 中的内容

• 当前任务目标。

• 最近几轮最关键的上下文。

• 与当前步骤相关的文件摘要。

• 最近一次工具调用结果。

这些内容服务于下一轮判断:模型现在要解决什么问题,眼前有哪些线索。这里的“最近一次工具结果”只是简化例子,不是通用的固定保留数量;如果当前判断还依赖更早的结果或未闭合的工具交互,就需要把相关内容一并保留。

存进 State 的内容

• 已读文件列表。

• 已发现问题列表。

• 当前计划。

• 重试情况。

这些内容帮助系统跟踪任务进度,知道哪些工作已经做过、当前正在做什么,以及之前是否发生过失败。

存进 Memory 或外部存储的内容

• 用户偏好。

• 历史学习进度。

• 上次任务总结。

这些内容可能在以后继续发挥作用。当前需要其中哪一部分,再结合任务取回来。

这三组信息对应的是不同用途,并不要求实现成三个互斥的容器。Messages 记录交互内容,State 保存程序跟踪任务所需的信息;同一条信息可以同时出现在两处,也有框架会把 messages 作为 state 的一个字段。

把内容放进 State,不会自动减少模型输入。如果运行时每轮又把整个 State 原样序列化给模型,它仍然占用上下文。真正决定输入大小的,是本轮实际交给模型的内容。

以配置文件为例,同一件事可以有三种表示:

表示方式
在系统中的用途
工具消息记录“读了配置文件,得到这些内容”
保留本次交互及来源
State 保存已读文件、任务进度和失败次数
让程序控制和恢复任务
本轮 Context 选入相关配置片段和进度摘要
让模型判断下一步

例如,State 记录了已读 15 个文件,并不意味着下一轮要把文件清单和全部原文都发过去。运行时可以只选与当前步骤相关的文件、必要结论和证据引用;需要精确原文时,再按路径与范围取回。

OpenAI Agents SDK 也区分本地 Context 对象和模型可见的 LLM Context:本地对象的字段不会因为存在,就自动成为模型输入。

因此,整理这些信息时,需要分别判断:程序应当跟踪什么,本轮模型必须看到什么,以及之后可能复用什么。

05

ENGINEERING

五、什么时候应该做摘要

下面这些场景,通常值得考虑摘要:

• 对话明显变长。

• 工具返回了很大一段文本。

• 一个阶段已经完成,后面只需要保留结论,不需要一直携带原文。

• 下一轮只需要关键结论,不需要全部细节。

例如,读完 10 个文件后,不一定要让这 10 个文件的全文继续留在每一轮输入里。可以先保留每个文件的一行结论摘要,让后续步骤知道它大致负责什么、与当前问题有什么关系。

这一行摘要适合作为索引和定位线索。要修改代码、核对条件分支或引用精确内容时,还需要读回对应文件和范围;跨版本分析还应保留文件所属的版本,避免读回的已经是另一份代码。

但摘要有自己的边界:如果下一步依赖精确原文,就不能只保留摘要。

“我已经看过某个文件”和“我现在还能准确引用其中的内容”,是两种不同的能力。是否继续保留原文,需要结合下一步任务判断。

设计 Agent 时,可以反复问自己四个问题:

1

这条信息当前轮真的必须给吗?

2

给摘要就够了,还是必须给原文?

3

不给模型看,程序自己记着是否就够了?

4

以后如果需要,能不能从外部再取回来?

这四个问题能帮助我们判断,什么时候摘要、什么时候保留原文,以及什么时候应该让信息留在模型之外。

06

AGENT

六、错误会发生在 Agent 链路的每一层

上下文安排好了,任务也不一定能一路顺利执行。Agent 往往包含一条较长的处理链路:

1

用户输入。

2

模型决策。

3

工具选择。

4

参数生成。

5

工具执行。

6

结果回填。

7

下一步决策。

图 1|从用户输入、模型决策到工具执行与结果回填的七步链路

任何一层都可能出错。用户可能给错路径,模型可能选错工具,工具可能超时,运行时也可能没有正确更新状态。

如果缺少错误处理,常见后果包括:

• 一次报错就导致整个任务崩掉。

• 模型没有被告知失败,还继续编造后续答案。

• 系统进入死循环,不断重复重试。

• 用户完全不知道任务卡在哪里。

错误处理需要让系统明确知道:错了什么、能不能恢复、该不该继续。

07

ENGINEERING

七、先区分四类常见错误

错误看起来都是一次失败,但发生的位置不同,需要的处理方式也可能不同。

错误类型
常见例子
输入错误
用户给的文件路径不存在;参数不完整;用户目标本身模糊
工具执行错误
文件读取失败;网络超时;API 返回 500;权限不足
模型决策错误
选错工具;参数提取错误;明明失败了却继续编答案
系统控制错误
状态没有更新;停止条件没有生效;重试次数没有限制;进入死循环

例如,文件没有读到,既要检查调用时给出的路径,也要看实际执行时发生了什么;模型选错工具,则需要重新判断下一步动作。

分类的作用,是帮助系统找到应该处理的环节,而不是把所有失败都交给同一个重试按钮。

08

ENGINEERING

八、哪些错误值得重试,哪些需要先改变条件

并不是所有错误都应该重试。

在操作允许安全重复执行、且仍有剩余预算的前提下,通常适合考虑重试的,包括:

• 网络超时。

• 临时性服务不可用。

• 限流。

• 偶发依赖故障。

这些错误有可能只是临时状态,再尝试一次就能继续执行,但“网络超时”需要特别判断。

超时只说明调用方没有及时收到结果,不能证明服务端没有执行。只读查询通常可以在预算内有限重试;发送消息、提交订单或修改数据等有副作用的操作,应先核对执行状态,或依赖接口实际支持的幂等键和去重机制。幂等机制的作用,是让同一业务操作的重复请求按约定避免重复生效。

如果无法确认是否已经执行,就要记录为“结果未知”,不要盲目重复操作。限流或临时服务不可用时,还应结合服务端的重试提示、退避等待和抖动安排下一次尝试;抖动就是给等待时间加入一定随机性,减少多个调用方同时再次请求。

而下面这些问题,单纯重复同一次调用通常无法解决:

• 文件路径写错。

• 参数缺失。

• 工具名不存在。

• 用户目标不清楚。

• 权限永久不足。

路径写错了,重复读取同一个错误路径不会让文件出现;缺少关键输入,也不会因为调用次数增加就自动补齐。

因此,可以先记住这个原则:原样重试主要应对暂时性故障;逻辑错误需要先改变条件。

本文把 Retry 理解为:在相同任务条件下,对同一操作进行有限次重试。路径或参数错误应先纠正;工具不合适,则需要重新选择路径。部分框架也会把纠正参数后的再次调用称为 retry,具体术语可能不同,关键是识别本次到底有没有改变条件。

纠正后的再调用同样受次数、时间和费用预算限制。要决定是否重试,系统首先需要知道当前失败属于什么情况。这也是工具返回结构化错误信息的意义。

09

TOOL CALLING

九、让工具明确返回成功、失败和是否可重试

下面的文件读取示例,展示了一种简单的错误表达方式。这里按 OpenAI Agents Python SDK 说明 @function_tool 的来源:它用于把函数封装成框架工具,依赖安装、Agent 注册和运行时接入在片段中省略。

所需的装饰器导入是:

···python

from agents import function_tool

工具函数的主体如下:

···python

@function_tool

def read_local_file(path: str) -> dict:

  """Read a local text file."""

  try:

    with open(path, "r", encoding="utf-8") as f:

      return {

        "success": True,

        "content": f.read()

      }

  except FileNotFoundError:

    return {

      "success": False,

      "error_type": "file_not_found",

      "error": f"File not found: {path}",

      "retryable": False

    }

  except Exception as e:

    return {

      "success": False,

      "error_type": "unknown",

      "error": str(e),

      "retryable": False

    }

成功时,它返回 success: True 和文件内容。文件不存在时,它返回 success: False,并用 error_type、error 和 retryable 说明发生了什么。

这几个字段分别承担不同作用:

• success:当前调用是否成功。

• content:成功时得到的内容。

• error_type:便于程序区分的错误类型。

• error:当前错误的说明。

• retryable:是否允许针对这次失败继续重试。

在这个示例中,文件不存在和未分类异常都被标成不可重试。调用方可以据此停止重复调用,而不是只收到一个模糊的“失败”。

这段示例关注的是错误如何表达;恢复动作仍然需要由外层流程继续决定。工具返回的 retryable 是一次失败的分类信息,运行时还应结合操作权限、幂等性和剩余预算,决定是否真的再次执行。

@function_tool 装饰后得到的是框架的 FunctionTool 对象,不应把它当成未装饰的普通 Python 函数直接调用。后面出现的 call_tool() 只是“由运行时执行工具并取得结果”的抽象;两个片段不能直接拼成完整项目。官方仍支持 function_tool,tool 是它的别名,这里不需要为换名称而改写示例。

文件读取工具还需要哪些执行边界

上面的 open(path) 没有实现授权目录限制,f.read() 会把全文读入内存;相对路径还依赖进程的工作目录。文件较大时,返回全文又会加重本篇前面讨论的上下文问题。

接入实际 Agent 时,需要在执行层限制授权目录和读取大小,处理路径、符号链接和文本编码,并按需支持分段读取。路径规范化可以辅助定位文件,但不能单独代替授权或沙箱。

当前片段只单独捕获了 FileNotFoundError。在进一步实现时,可以按需要细分下面这些错误,让运行时更容易选择恢复动作:

错误类型
可以提供的恢复线索
FileNotFoundError
路径不存在,核对路径;缺少关键信息时询问用户
PermissionError
当前权限不足,按既有授权策略处理;需要时停止或补充授权
IsADirectoryError
传入了目录,重新选择具体文件
UnicodeDecodeError
当前解码方式不适合该内容,核对编码或文件类型

表中 PermissionError、IsADirectoryError、UnicodeDecodeError 的细分分支尚未实现,当前会进入 unknown;FileNotFoundError 已单独处理。对模型返回足以采取下一步动作的简短错误信息,供排查的完整异常则记录在受控日志中。

另外,示例的所有失败分支都设置了 retryable=False,它没有演示一次真正可重试的失败。文件不存在后,如果修正了路径或补齐输入,再调用属于条件改变后的新尝试。

10

ENGINEERING

十、给重试设置次数和明确出口

一个基本的重试策略,需要考虑四件事:

1

限定最大次数。

2

只对可重试错误生效。

3

每次重试前记录原因。

4

超过次数后停止。

下面这个循环展示了最基本的次数限制和退出判断:

···python

max_retries = 2

for attempt in range(max_retries + 1):

  result = call_tool()

  if result["success"]:

    break

  if not result.get("retryable", False):

    break

max_retries = 2 表示首次调用之外,最多再尝试两次,因此循环最多调用工具三次。

每一轮都检查两件事:当前调用是否成功;如果失败,这个错误是否允许重试。成功就结束,不允许重试也结束。只有失败且被标记为可重试,循环才有机会继续下一轮。

这个结构给当前这层重试设置了上限:即使工具持续报错,这个循环也最多调用工具三次。

但这段代码只实现了次数限制和两类 break 判断。call_tool() 是占位函数,实际运行时应返回约定结果,或把调用异常转换成受控的错误结果;如果它直接抛出异常,这个循环本身并没有捕获逻辑。

循环退出后,还要继续检查最后的 result。外层流程可以区分这些情况:

退出情况
后续如何理解
success=True
当前工具调用成功,再按任务验收条件继续判断
失败且 retryable=False
原样重试应结束,考虑纠错、询问用户或停止
失败且可重试,但调用次数已用完
本轮重试额度耗尽,记录原因并交还上层流程

循环结束不等于任务成功。工具重试是否结束、这次工具调用是否成功,以及整个任务是否完成,需要分别记录。

网络重试还要处理等待和总预算

前面提出的“每次重试前记录原因”,在这个最小循环里尚未实现。实际接入时还要补上:

• 单次调用超时,以及整个任务共同的时间和费用预算。

• 重试原因、已尝试次数和最后结果的受控记录。

• 对限流或暂时性依赖故障使用退避等待,按需要加入抖动。

• 次数耗尽、超时或不可重试时的明确出口。

还要检查 SDK、HTTP 客户端或工具内部是否已经重试。如果每一层都独立配置多次尝试,真实请求次数可能被放大。应明确由哪一层主导重试,避免额度层层叠加。

这些是把最小控制骨架接入真实工具时需要补齐的责任,不能把上面的几行循环当作完整的网络恢复实现。

11

ENGINEERING

十一、失败后,选择 Retry、Re-plan、Ask User 或 Stop

从错误恢复的角度,可以把下一步动作整理成四类:

动作
含义与例子
Retry
有限次再尝试当前动作,例如可安全重复执行的查询超时后重试
Re-plan
调整计划,例如发现当前工具不适合
Ask User
向用户补充询问,例如缺少继续判断的信息
Stop
停止当前自动运行,例如已经明确无法继续

这四种动作几乎覆盖了大部分 Agent 错误恢复场景。要处理的重点,是识别自己现在该进入哪一种。

什么时候应该 Ask User

下面这些情况,可以考虑向用户询问:

• 缺少关键输入,而且无法可靠推断。

• 用户需求存在歧义。

• 多个选项会带来不同后果,需要用户决定方向。

• 涉及高风险动作,需要确认。

• 工具失败表明外部信息缺失。

例如,当继续执行所需的信息并不在当前材料中,且系统不能可靠推断时,把缺口明确交还给用户,比反复尝试更容易推进任务。

不过,工具失败不代表一定要问用户。如果缺失信息能够通过已经授权、成本可控的其他途径可靠补齐,可以先调整计划;缺口需要用户判断、补充或授权时,再把控制权交还给用户。

涉及高风险动作时,仍由既有授权和外层策略判断能否执行;需要补充确认时才询问用户,不能让模型自行宣布已经获得权限。

什么时候应该 Re-plan

下面这些情况,说明需要重新考虑原来的路径:

• 原计划依赖的前提不成立。

• 工具结果推翻了原假设。

• 当前路径连续没有进展。

• 重复动作开始出现。

• 新信息表明存在更好的路径。

例如,工具已经返回了与原假设不符的结果,就需要结合这个观察重新选择下一步。重试原来的动作和重新规划,是两种不同的恢复选择。

重新规划同样受最大步数、总时长和费用预算限制,不能因为换了工具或计划就把预算清零。记录已尝试的路径、失败原因和新获得的线索,才能判断是否真的产生了进展。

停止时,把结果和缺口说清楚

停止当前自动运行时,应交代已经确认的事实、还缺少的信息,以及停止原因。等待用户补充、证据不足、预算耗尽和任务成功,需要作为不同结果记录;不能只因为循环结束,就把任务状态标为完成。

这也把错误恢复与 State 联系起来:程序要跟踪的不只是“执行过哪些步骤”,还包括为什么继续、为什么换路,以及为什么结束。

12

INTERVIEW

十二、面试时,怎样把这些边界讲清楚

为什么不用一个更大的上下文窗口解决所有问题

更大的窗口能容纳更多材料,但仍要考虑输入成本、延迟,以及这些信息是否与当前决策相关。应用需要挑选本轮必要内容,并保留取回精确原文的方式;具体预算和能力则以实际模型、接口与工作负载为准。

讨论时可以把问题落到一个具体任务:读过 15 个文件后,下一轮究竟需要它们的全文、摘要,还是其中两个文件的精确片段。窗口大小只是约束之一,信息选择本身仍需要设计。

把历史放进 State,模型是不是就能自动记住

State 让程序跟踪任务;模型能否利用其中的信息,取决于运行时有没有把必要字段、相关摘要或工具读回结果交给它。把整个 State 重新输入模型,依然占用上下文。

可以用“程序保存了什么”和“本轮模型实际看到了什么”来解释。两者可能有重叠,但不会因为数据结构名字不同,就自动改变模型的可见范围。

给工具加三次调用上限,就算设计好错误恢复了吗

次数限制只解决无限重试的一部分问题。系统还需要判断错误能否恢复、操作能否安全重复、何时改计划或询问用户,以及退出后如何记录结果。

超时尤其要区分“收到明确失败”和“结果未知”。如果外部操作可能已经生效,就要先核对状态或使用接口支持的幂等机制,再决定是否重试。重新规划也必须服从整个任务的共同预算。

∞

THE END

收尾:把每一轮输入和每一次失败都管起来

上下文管理决定模型下一轮带着哪些信息继续;错误处理决定一条路径失败后,系统是重试、重新规划、询问用户,还是停止。

接着第一篇的模型、工具、状态与控制循环,本篇把注意力放到了更具体的执行问题:历史如何整理,精确原文何时要保留和读回,工具错误如何表达,以及重试怎样有明确的次数、预算和出口。

做一个稍长的 Agent 任务时,可以从两件事开始检查:本轮输入里有没有不再需要的材料;遇到错误后,系统有没有明确的恢复动作。

END

相关学习资料