1. 上下文窗口为什么成了Agent的头号瓶颈做过Agent开发的人都有一个共同体会Demo阶段一切都很美好一旦把Agent放到真实业务里跑上十几轮对话或者让它处理一份稍微像样的文档模型就开始“失忆”——前面说过的约束忘了中间调用的工具结果丢了甚至开始胡编乱造。这不是模型变笨了而是上下文空间被撑爆了。所谓上下文空间指的是大模型单次推理能够接收的Token总量上限。这个上限包含了你塞进去的所有东西系统提示词、历史对话、工具调用的返回结果、检索到的文档片段、当前用户输入以及模型自己即将生成的输出。早期模型的窗口只有4K、8K现在主流模型动辄128K甚至200K看起来很大但实际用起来依然捉襟见肘。原因很简单Agent的工作模式天然是“上下文吞噬者”。一个典型的Agent循环是这样的用户提出需求模型思考后决定调用某个工具工具返回一大段JSON或文本模型读取后再思考再调用下一个工具……每一轮循环工具返回的原始数据都会完整地追加到对话历史里。如果工具返回的是一份数据库查询结果、一个网页的完整HTML、或者一段日志文件几千上万Token瞬间就没了。跑个五六轮窗口就见了底。更麻烦的是上下文一旦超限不同模型和框架的处理方式不一样。有的直接报错中断有的悄悄截断最早的消息有的把中间内容丢掉。无论哪种对Agent的任务完成质量都是毁灭性打击。你精心设计的系统提示词可能被截掉关键的工具返回可能被丢弃Agent就像失忆的人一样在原地打转。所以上下文工程Context Engineering这个概念这两年越来越热不是没有道理的。它要解决的核心问题就是在有限的上下文窗口里如何让Agent始终“记得住该记的、忘得掉该忘的、找得到该找的”。这比单纯的提示词工程Prompt Engineering要复杂一个量级因为它涉及的是动态的、多轮次的、带工具调用的信息流管理。我见过不少团队在这个问题上栽跟头。有个做客服Agent的朋友系统上线第一天就崩了原因是用户上传了一份PDFAgent把整份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_window128000, output_reserve4000, safety_margin0.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_items5, max_summary_len200): 原始搜索结果通常包含大量字段这里只保留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_rows20): 数据库查询结果压缩只保留前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 TokenAgent的表现反而更好因为噪音少了。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运行过程中有些工具结果只在当前轮有用下一轮就没用了。我的做法是给这类结果打上ephemeralTrue标记在下一轮组装上下文时自动丢弃。比如“获取当前时间”这种工具结果用完就扔不需要留在历史里。技巧二用“引用”代替“全文”。如果工具返回的是一份长文档不要直接把全文塞进上下文而是存到外部存储上下文里只放一个引用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始终拥有完成任务所需的最少必要信息。把这个思想吃透无论工具怎么变你都能找到合适的方案。
