1. 上下文窗口为什么成了Agent的头号瓶颈
做过Agent开发的人都有一个共同体会:Demo阶段一切都很美好,一旦把Agent放到真实业务里跑上十几轮对话,或者让它处理一份稍微像样的文档,模型就开始“失忆”——前面说过的约束忘了,中间调用的工具结果丢了,甚至开始胡编乱造。这不是模型变笨了,而是上下文空间被撑爆了。
所谓上下文空间,指的是大模型单次推理能够接收的Token总量上限。这个上限包含了你塞进去的所有东西:系统提示词、历史对话、工具调用的返回结果、检索到的文档片段、当前用户输入,以及模型自己即将生成的输出。早期模型的窗口只有4K、8K,现在主流模型动辄128K甚至200K,看起来很大,但实际用起来依然捉襟见肘。原因很简单:Agent的工作模式天然是“上下文吞噬者”。
一个典型的Agent循环是这样的:用户提出需求,模型思考后决定调用某个工具,工具返回一大段JSON或文本,模型读取后再思考,再调用下一个工具……每一轮循环,工具返回的原始数据都会完整地追加到对话历史里。如果工具返回的是一份数据库查询结果、一个网页的完整HTML、或者一段日志文件,几千上万Token瞬间就没了。跑个五六轮,窗口就见了底。
更麻烦的是,上下文一旦超限,不同模型和框架的处理方式不一样。有的直接报错中断,有的悄悄截断最早的消息,有的把中间内容丢掉。无论哪种,对Agent的任务完成质量都是毁灭性打击。你精心设计的系统提示词可能被截掉,关键的工具返回可能被丢弃,Agent就像失忆的人一样在原地打转。
所以,上下文工程(Context Engineering)这个概念这两年越来越热,不是没有道理的。它要解决的核心问题就是:在有限的上下文窗口里,如何让Agent始终“记得住该记的、忘得掉该忘的、找得到该找的”。这比单纯的提示词工程(Prompt Engineering)要复杂一个量级,因为它涉及的是动态的、多轮次的、带工具调用的信息流管理。
我见过不少团队在这个问题上栽跟头。有个做客服Agent的朋友,系统上线第一天就崩了,原因是用户上传了一份PDF,Agent把整份PDF内容塞进上下文,直接超限。还有个做代码助手的,Agent读了几个源文件之后就开始胡言乱语,因为它把文件内容全量保留在历史里,后面的推理全被无关代码淹没了。这些坑,本质上都是上下文管理没做好。
下面我会从架构设计、核心策略、实操落地、问题排查几个层面,把Agent上下文空间不够这个问题拆开讲透。无论你用的是LangChain、LlamaIndex、Dify还是自己手搓的框架,这些思路都是通用的。
2. 上下文工程的核心思路与方案选型
2.1 先搞清楚:上下文里到底装了什么
要解决问题,先得知道问题出在哪。一个运行中的Agent,它的上下文窗口里通常包含以下几类内容:
- 系统提示词:定义Agent的角色、能力边界、输出格式要求。这部分通常不大,但很关键,不能丢。
- 对话历史:用户和Agent之间的多轮交互记录。轮次越多,占用越大。
- 工具定义:告诉模型有哪些工具可用、参数是什么。工具多了,这部分也很可观。
- 工具调用结果:这是最大的“内存杀手”。一次数据库查询、一次网页抓取、一次文件读取,返回的内容可能几千到几万Token。
- 检索增强内容:从向量库或搜索引擎召回的文档片段。
- 当前任务指令:用户最新的一句话,通常不大。
- 模型输出预留:必须给模型生成留出空间,否则它会因为“写不下”而截断输出。
把这七类内容列出来,你会发现一个残酷的事实:工具调用结果和对话历史是增长最快的两个部分,而它们恰恰是Agent运行过程中最不可控的。你没法预知工具会返回多少数据,也没法限制用户会聊多少轮。
2.2 三种主流策略的取舍
面对上下文超限,业界目前主要有三种应对策略,各有优劣,实际项目中往往是组合使用。
第一种:截断与滑动窗口。最简单粗暴,就是只保留最近N轮对话,或者只保留最近M个Token的内容,超出的直接丢掉。优点是实现简单、零额外成本。缺点是会丢失早期的重要信息,比如用户一开始说的约束条件、Agent早期做出的关键决策。适合场景简单、任务链短的Agent。
第二种:摘要与压缩。把历史对话或工具返回结果用模型压缩成简短摘要,只保留关键信息。优点是信息密度高,能保留长期记忆。缺点是需要额外调用模型,增加延迟和成本,而且摘要本身可能丢失细节。适合长对话、需要记忆的Agent。
第三种:外部记忆与检索。把完整历史存到外部存储(向量库、数据库、文件),上下文里只放索引或摘要,需要时再检索回来。优点是理论上可以无限扩展,缺点是检索质量直接影响Agent表现,架构复杂度高。适合企业级、长周期运行的Agent。
我的经验是:短任务用截断,长对话用摘要,复杂业务用外部记忆。但不管用哪种,都要配合一个关键动作——Token预算管理。你得清楚地知道每一类内容大概占多少Token,给模型输出留多少,给突发情况留多少余量。这个预算不是拍脑袋定的,要根据你用的模型窗口大小和任务复杂度来算。
2.3 为什么不能只靠“换个大窗口模型”
有人会说,现在模型窗口都128K、200K了,还折腾这些干嘛?直接换个窗口大的模型不就完了?
这个想法很危险。原因有三:
第一,窗口大不等于用得好。业界有个著名的“Lost in the Middle”现象:当上下文很长时,模型对中间部分的信息注意力会显著下降,只对开头和结尾敏感。你塞了100K进去,模型可能只“看见”了前10K和后10K,中间的全当背景噪音。所以窗口大不代表有效容量大。
第二,成本随Token线性增长。大部分模型的计费是按输入Token算的,你每次调用都塞100K进去,费用是塞10K的十倍。Agent又是高频调用的场景,成本会迅速失控。
第三,延迟随Token增长。输入越长,模型首Token延迟越高。Agent需要快速响应,用户等不了你每次思考十秒钟。
所以,上下文工程的目标不是“塞更多”,而是“用更少的信息达到同样的效果”。这才是真功夫。
3. 核心细节解析与实操要点
3.1 Token预算怎么算:一个可落地的公式
做上下文管理,第一步是建立Token预算意识。我给你一个我实际在用的计算公式:
可用上下文 = 模型窗口上限 - 模型输出预留 - 安全余量其中:
- 模型输出预留:一般留2K到4K Token,取决于你的Agent输出长度。如果Agent要生成代码或长文,留8K。
- 安全余量:留10%左右,应对Token计数误差和突发内容。
假设你用128K窗口的模型,输出预留4K,安全余量12K,那么可用上下文就是112K。这112K要分配给系统提示词、工具定义、对话历史、工具结果、检索内容。
我的分配习惯是这样的(仅供参考,要根据业务调整):
| 内容类型 | 建议占比 | 说明 |
|---|---|---|
| 系统提示词 | 5% | 精简再精简,能一句话说清绝不用两句 |
| 工具定义 | 10% | 工具多的话考虑动态加载 |
| 对话历史 | 30% | 保留最近N轮,更早的做摘要 |
| 工具调用结果 | 35% | 重点压缩对象,原始数据尽量不直接进上下文 |
| 检索内容 | 15% | 控制召回数量和片段长度 |
| 当前指令 | 5% | 通常很小 |
这个表不是死的,但核心思想是:工具结果和对话历史是压缩的重点,系统提示词和当前指令是必须保住的。
3.2 工具返回结果的压缩:最容易被忽视的优化点
很多Agent开发者把精力花在提示词优化上,却忽略了工具返回结果才是上下文膨胀的元凶。我见过一个Agent,调用一次搜索API返回了50条结果,每条结果包含标题、摘要、URL、发布时间、作者、全文……一次调用就吃掉30K Token。而Agent实际只需要其中的标题和摘要。
工具结果压缩的核心原则是:在工具层面做裁剪,而不是在上下文层面做截断。
具体怎么做?几个实操方法:
- 让工具返回结构化精简数据。比如数据库查询,不要
SELECT *,而是明确指定需要的字段。网页抓取不要返回完整HTML,而是用解析库提取正文后返回纯文本。 - 在工具和Agent之间加一层“结果处理器”。工具返回原始数据后,先用规则或小模型提取关键信息,再喂给Agent。比如搜索API返回50条,处理器只保留Top 5的标题和摘要。
- 对必须保留的长文本做分块摘要。如果工具返回的是一份长文档,先用模型分段摘要,再把摘要给Agent。Agent需要细节时,再通过检索拿回原文片段。
注意:工具结果压缩要在“信息完整性”和“Token节省”之间找平衡。压得太狠,Agent会缺信息做决策;压得太松,上下文照样爆。我的经验是,先观察Agent实际用到了工具结果里的哪些字段,然后只保留那些字段。
3.3 对话历史的摘要策略:什么时候摘要,怎么摘要
对话历史的管理比工具结果更微妙,因为它涉及“记忆”的连续性。全量保留会爆,全丢掉会失忆,所以摘要成了主流方案。
但摘要不是随便摘的。我踩过的坑是:早期我用模型把每轮对话都摘要成一句话,结果Agent丢失了用户的具体约束(比如“不要用Python 2的语法”),后面生成的代码全是Python 2风格。
后来我调整了策略,分层摘要:
- 近期对话:保留最近3到5轮的完整内容,不做摘要。这部分是Agent当前工作的直接上下文,必须精确。
- 中期对话:对5到15轮之前的内容做结构化摘要,保留关键决策、用户约束、已完成的步骤。
- 远期对话:只保留一个极简的“会话概要”,比如“用户在做一个数据分析任务,已完成数据清洗,正在做特征工程”。
摘要的触发时机也很关键。不要等上下文快满了才摘要,那样容易触发截断。我的做法是在每轮对话结束后检查Token占用,超过预算的70%就触发摘要。摘要本身也要消耗Token,所以要留出空间。
另外,摘要最好用便宜的小模型来做,比如用7B级别的模型做摘要,主推理用大模型。这样成本可控。
3.4 外部记忆的架构:向量库不是万能药
外部记忆听起来很美:把所有历史存到向量库,需要时检索回来。但实际用起来,检索质量是最大的变数。
我见过一个Agent,把用户的所有对话都存进向量库,每次用户提问就检索Top 10相关片段。结果检索回来的经常是无关内容,因为向量相似度不等于任务相关性。用户问“帮我改一下这个函数”,检索回来的是三天前聊的天气。
外部记忆要分类型存储,不能一锅炖:
- 事实型记忆:用户偏好、项目背景、固定约束。这类适合存结构化数据库,直接按key查询,不走向量检索。
- 经验型记忆:过去解决类似问题的方案、踩过的坑。这类适合向量检索,但要在检索时加过滤条件(比如只检索同一项目下的)。
- 临时型记忆:当前任务的中间结果。这类适合存文件或KV存储,按任务ID索引。
检索回来的内容也要做二次筛选。我的做法是:向量检索召回Top 20,然后用一个轻量模型做相关性打分,只保留Top 3到5条真正相关的。这样虽然多了一步,但能显著提升上下文质量。
4. 实操过程与核心环节实现
4.1 搭建一个带上下文管理的Agent骨架
下面我用Python伪代码展示一个可落地的Agent上下文管理骨架。不依赖特定框架,思路通用。
class ContextManager: def __init__(self, model_window=128000, output_reserve=4000, safety_margin=0.1): self.model_window = model_window self.output_reserve = output_reserve self.safety_margin = safety_margin self.available = int((model_window - output_reserve) * (1 - safety_margin)) self.history = [] # 完整对话历史 self.summary = "" # 远期摘要 self.system_prompt = "" self.tool_definitions = [] def count_tokens(self, text): # 实际项目中用tiktoken或模型自带的tokenizer return len(text) // 4 # 粗略估算,中文约1.5字/token def get_context(self, current_input): # 计算各部分Token占用 system_tokens = self.count_tokens(self.system_prompt) tool_tokens = self.count_tokens(str(self.tool_definitions)) input_tokens = self.count_tokens(current_input) summary_tokens = self.count_tokens(self.summary) # 剩余给对话历史和工具结果 remaining = self.available - system_tokens - tool_tokens - input_tokens - summary_tokens # 从最近的历史往前取,直到用完预算 selected_history = [] used = 0 for msg in reversed(self.history): msg_tokens = self.count_tokens(str(msg)) if used + msg_tokens > remaining: break selected_history.insert(0, msg) used += msg_tokens # 组装最终上下文 context = [ {"role": "system", "content": self.system_prompt}, {"role": "system", "content": f"历史摘要:{self.summary}"}, *selected_history, {"role": "user", "content": current_input} ] return context def add_message(self, role, content): self.history.append({"role": role, "content": content}) self.maybe_summarize() def maybe_summarize(self): total = sum(self.count_tokens(str(m)) for m in self.history) if total > self.available * 0.7: # 触发摘要:把最早的一半历史压缩 half = len(self.history) // 2 old_messages = self.history[:half] new_summary = self.summarize(old_messages) self.summary = self.merge_summary(self.summary, new_summary) self.history = self.history[half:] def summarize(self, messages): # 调用小模型做摘要,实际项目替换为真实调用 text = " ".join(m["content"] for m in messages) return f"[摘要] {text[:200]}..." def merge_summary(self, old, new): return f"{old}\n{new}"[:1000] # 控制摘要长度这个骨架的核心逻辑是:每次组装上下文时,从最近的历史往前取,直到预算用完。同时,当历史总量超过70%预算时,触发摘要,把最早的一半压缩掉。
4.2 工具结果处理器的实现
工具结果处理器是压缩上下文的关键组件。下面是一个针对搜索API返回结果的处理器示例:
def process_search_result(raw_result, max_items=5, max_summary_len=200): """ 原始搜索结果通常包含大量字段,这里只保留Agent需要的 """ processed = [] for item in raw_result.get("items", [])[:max_items]: processed.append({ "title": item.get("title", ""), "summary": item.get("snippet", "")[:max_summary_len], "url": item.get("link", "") }) return processed def process_database_result(raw_result, max_rows=20): """ 数据库查询结果压缩:只保留前N行,且只保留非空字段 """ if not raw_result: return [] rows = raw_result[:max_rows] # 去掉全空字段 if rows: keys = [k for k in rows[0].keys() if any(r.get(k) for r in rows)] rows = [{k: r.get(k) for k in keys} for r in rows] return rows这两个处理器的思路是一样的:在工具返回和Agent消费之间加一层过滤,只传递Agent真正需要的信息。实测下来,一个搜索工具的结果从30K Token压缩到2K Token,Agent的表现反而更好,因为噪音少了。
4.3 摘要提示词的设计
摘要质量直接决定Agent的长期记忆能力。我用的摘要提示词是这样的:
你是一个对话摘要助手。请将以下对话历史压缩成结构化摘要,保留以下信息: 1. 用户提出的所有约束条件和偏好 2. Agent已经完成的关键步骤和结论 3. 尚未解决的问题和待办事项 4. 重要的数据、文件、工具调用结果的关键信息 不要保留:寒暄、重复内容、中间过程的细节。 输出格式: - 用户约束:[列表] - 已完成:[列表] - 待办:[列表] - 关键数据:[列表] 对话历史: {history}这个提示词的关键是明确告诉模型保留什么、丢弃什么。不加约束的摘要会丢失关键信息,加了约束之后,摘要的可用性大幅提升。
4.4 上下文组装的顺序优化
上下文里内容的排列顺序也会影响模型表现。根据“Lost in the Middle”现象,开头和结尾是模型的注意力高地。所以我的排列策略是:
- 开头放系统提示词和核心约束:这是Agent必须遵守的规则,放在最前面。
- 中间放历史摘要和检索内容:这些是背景信息,模型注意力低一点没关系。
- 靠近结尾放最近对话和当前指令:这是Agent当前要处理的核心内容,放在注意力高的位置。
- 最结尾放输出格式要求:提醒模型按格式输出。
这个顺序不是绝对的,但核心思想是:最重要的信息放在开头和结尾,次要信息放中间。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent突然报上下文超限 | 工具返回结果过大 | 打印每轮Token占用 | 加工具结果处理器 |
| Agent忘记早期约束 | 历史被截断或摘要丢失 | 检查摘要内容 | 优化摘要提示词,保留约束 |
| Agent回答质量下降 | 上下文噪音过多 | 检查检索内容相关性 | 加相关性过滤 |
| 响应变慢 | 上下文过长 | 统计平均Token数 | 压缩历史,减少检索量 |
| 成本飙升 | 每轮都塞满上下文 | 统计Token消耗 | 建立Token预算,动态分配 |
| Agent重复调用同一工具 | 工具结果被截断,模型没看到 | 检查工具结果是否完整 | 确保关键结果不被截断 |
5.2 独家避坑技巧
技巧一:给工具结果加“过期标记”。Agent运行过程中,有些工具结果只在当前轮有用,下一轮就没用了。我的做法是给这类结果打上ephemeral=True标记,在下一轮组装上下文时自动丢弃。比如“获取当前时间”这种工具,结果用完就扔,不需要留在历史里。
技巧二:用“引用”代替“全文”。如果工具返回的是一份长文档,不要直接把全文塞进上下文,而是存到外部存储,上下文里只放一个引用ID和摘要。Agent需要细节时,再用ID去取。这样上下文里永远只有摘要,Token占用可控。
技巧三:监控“有效上下文利用率”。我定义了一个指标:Agent实际引用到的上下文内容占全部上下文的比例。如果这个比例低于30%,说明上下文里噪音太多,需要压缩。这个指标可以通过分析Agent的输出和上下文的对应关系来估算。
技巧四:给摘要加“版本号”。每次摘要更新时,给摘要加一个版本号和时间戳。这样当Agent表现异常时,可以回溯是哪个版本的摘要导致了问题。我遇到过摘要把关键约束摘丢了的情况,有了版本号就能快速定位。
技巧五:不要迷信“自动摘要”。自动摘要适合处理大量重复性内容,但对于关键约束和决策,最好用规则提取而不是模型摘要。比如用户说“不要用某个库”,这种约束直接用正则提取存到结构化字段里,比让模型摘要可靠得多。
5.3 一个真实的排查案例
之前有个做数据分析Agent的项目,用户反馈Agent在处理大型CSV文件时经常“失忆”。我介入排查,发现问题是这样的:
Agent读取CSV后,把前100行数据塞进了上下文。这100行占了大约15K Token。然后Agent做了一些分析,又调用了几个工具,上下文很快到了80K。这时候Agent开始忘记用户最初说的“只分析A列和B列”,开始对所有列做分析。
排查过程:
- 打印每轮上下文的Token分布,发现工具结果占了60%。
- 检查工具结果,发现CSV前100行是完整塞进去的,包含所有列。
- 检查系统提示词,发现“只分析A列和B列”的约束在系统提示词里,但系统提示词在上下文最前面,被后面的长内容“淹没”了。
解决方案:
- 修改CSV读取工具,只返回A列和B列的前50行,Token占用从15K降到3K。
- 把关键约束从系统提示词里复制一份,放在当前用户指令的前面,确保模型在注意力高地能看到。
- 加了Token预算监控,超过70%就触发摘要。
改完之后,Agent再也没有“失忆”过,而且响应速度提升了40%,成本降低了60%。
这个案例的核心教训是:上下文管理不是单一策略能解决的,要组合使用工具结果压缩、关键信息前置、预算监控三个手段。
6. 上下文工程的进阶思路
6.1 动态工具加载:别一次性把所有工具都塞进去
工具定义本身也占Token。如果你有50个工具,每个工具定义200 Token,那就是10K Token。但Agent在单次任务中可能只需要其中3到5个工具。
我的做法是动态工具加载:根据当前任务类型,只加载相关工具。比如用户问的是数据分析问题,就只加载数据查询、统计、绘图工具,不加载文件管理、邮件发送工具。这样工具定义的Token占用可以从10K降到2K。
实现方式有两种:一种是按任务类型预定义工具组,另一种是用向量检索根据用户输入匹配工具。前者简单可靠,后者更灵活但需要调优。
6.2 上下文缓存:重复内容不要重复计算
Agent运行过程中,系统提示词和工具定义是固定的,每次调用都重新计算Token、重新传输,很浪费。现在很多模型支持上下文缓存(Context Caching),可以把固定部分缓存起来,只传输变化部分。这能显著降低成本和延迟。
如果你的模型不支持缓存,也可以在应用层做缓存:把固定部分的Token计数缓存起来,不用每次重新算。
6.3 多Agent协作时的上下文隔离
当多个Agent协作时,上下文管理更复杂。我的经验是:每个Agent维护自己的上下文,Agent之间只传递摘要和结果,不传递完整历史。比如一个“研究员Agent”和一个“写作Agent”协作,研究员把研究结果摘要传给写作Agent,而不是把整个研究过程的对话历史都传过去。这样每个Agent的上下文都保持精简。
6.4 上下文质量的评估指标
最后分享几个我用来评估上下文管理效果的指标:
- Token效率:完成任务消耗的总Token数 / 任务复杂度。越低越好。
- 信息保留率:关键约束在摘要后是否保留。可以通过人工抽查或规则检查。
- 检索命中率:检索回来的内容被Agent实际使用的比例。越高越好。
- 失忆率:Agent忘记关键信息的频率。越低越好。
这些指标不需要很精确,但要有意识地监控。我一般会在开发阶段每周抽查一次,上线后每月回顾一次。
上下文工程这个领域还在快速演进,新的模型、新的框架、新的策略层出不穷。但核心思想是不变的:在有限的空间里,让Agent始终拥有完成任务所需的最少必要信息。把这个思想吃透,无论工具怎么变,你都能找到合适的方案。