去年下半年开始,身边越来越多朋友在讨论同一个问题:AI代理明明接入了能力很强的大模型,为什么用起来总有种“它是鱼的记忆”的即视感?上一轮刚交代清楚的需求,这一轮它就凭空失忆;明明给足了资料,它却拎不清哪些才是当前任务真正要用的。后来我仔细复盘了自己做的几个代理项目,发现绝大多数问题都不在模型本身,而在于我们根本没把“上下文”当成一个需要认真治理的工程对象。
上下文不是什么玄乎的东西。它就是你喂给模型的全部信息,是模型做出判断的全部依据。开发普通软件时,你绝不会放任全局变量无限增长、不做声明周期管理,代码里到处都是野引用,早晚出事。但到了AI代理这里,很多人反而松懈了——一轮会话下来,历史消息无脑追加,工具返回的原始数据原样堆砌,系统提示词越写越长,直到某天模型开始胡说八道,还在纳闷“我给了它这么多信息,怎么还会错?”
这篇文章想聊的,就是如何用“开发生命周期”的思维去治理AI代理的上下文。上下文从产生、组装、注入、更新到清理,每个环节都需要有明确的策略和机制。如果你是正在用Claude、GPT等模型API构建代理的开发者,或者在Cursor这类AI编程工具里经常被上下文问题困扰,又或者只是好奇“上下文超限到底会发生什么”,这篇内容应该能帮你少踩不少坑。
1. 为什么AI代理上下文需要一个生命周期
1.1 你管理的是模型思考的边界
先看一个容易被忽视的事实:上下文窗口,就是模型“当下能感知到的全部世界”。窗口之外的信息,哪怕你再怎么觉得“它应该知道”,对它来说就是不存在的。你在后台配置了一堆工具函数、塞了一份120页的产品文档、保留了完整的多轮对话记录,可当这些内容的总长度逼近模型上下文窗口上限时,真正送给模型推理的,往往只是被截断或稀释后的残片。
上下文一旦超出限制,不同的模型服务商处理方式不同。有的会直接报错,提示你减少输入;有的会从最前面悄悄裁剪历史消息;还有的会把过长的内容以某种方式摘要后塞进后续请求。无论哪种,结果都很糟糕:要么请求直接失败,要么模型在关键信息被裁剪后开始“一本正经地胡说八道”。这就是很多人遇到的幻觉问题的真正来源之一——不是模型变笨了,是你给它的上下文已经残缺了。
把上下文管理比作内存管理,其实特别贴切。一个长期运行的程序,如果每次申请内存都不释放,最终一定会OOM。AI代理的上下文窗口就是一块固定大小的内存,而每一次对话历史、每一段工具返回、每一条系统提示,都在占用这块内存。上过操作系统课的人都明白,内存碎片化之后系统性能会急剧下降。上下文也一样,当无关信息占据大量空间时,模型必须费力地在“噪声”中识别真正重要的内容,回答质量自然会下降。
1.2 应该说,不是所有信息都值得进入上下文
大多数代理设计失败的共同点,就是把“上下文工程”做成了“上下文堆积”。我见过不少项目,系统提示词里写了一大堆业务规则,对话记录不做任何裁剪就无限追加,工具调用返回的JSON结果原样塞进消息列表。结果就是:上下文越来越臃肿,Token费用越来越高,模型回答越来越飘。
你需要建立的第一认知是:上下文质量远比数量重要。宁可精准地给模型三五百个Token的高密度信息,也不要稀里糊涂地塞进去两三万个Token的杂烩。这就像带人去图书馆找一本书,你告诉他“书架第三层、从左边数第七本、红色封面”就够,不需要把整个图书馆的导览图都背给他听。
所以,上下文工程的一个核心原则就是:有意识地控制哪些信息进入上下文,以及以什么形式、什么顺序进入。这需要你像治理代码依赖一样治理上下文依赖——明确每个Agent任务真正需要哪些数据,剔除无关内容,对长文本做结构化提炼,对历史交互做压缩摘要。这套治理动作,本质上就是上下文的生命周期管理。
2. 上下文生命周期的四个关键阶段
2.1 构建阶段:从多源信息到结构化上下文
上下文生命周期的第一步,是“构建”。这个阶段解决的核心问题是:当前任务需要什么信息,信息从哪里来,以及用什么样的结构组织这些信息。
以一个典型的企业知识库问答Agent为例。用户提问“公司今年的差旅报销政策是什么”,背后至少涉及三类信息:系统提示词里固定的业务规则、用户当前提问的意图、知识库中与报销政策相关的文档片段。如果你直接把整本员工手册塞进上下文,模型也能回答,但浪费了大量Token,还可能在政策更新后引用到过期版本。
更合理的做法是:先做一次检索,把员工手册中与差旅报销相关的几个章节抽出来,再做一次裁剪,移除表格格式、页眉页脚,保留关键条款,最后组装成结构化的上下文块。这个过程,和你在代码里先读取配置、再加载对应模块、最后把依赖注入构造函数,本质上是同一回事。
这里有个容易被忽略的细节:上下文构建的顺序会影响模型的理解效果。通常,系统指令要放在最前面,因为它定义了模型的角色和任务边界;随后是用户当前的需求;再往后才是检索到的参考材料。如果顺序颠倒,模型可能把系统指令误当作普通对话内容,行为模式就混乱了。这已经是被不少团队的实验验证过的经验。
2.2 路由阶段:把上下文送到该去的地方
管理过微服务的人都知道“服务路由”的重要性——不同的请求要打到不同的后端实例上。AI代理同样需要“上下文路由”:在同一个Agent内部,可能有多个子任务、多个工具函数、多个知识域,你不能总是把全量上下文一次性推给模型。
举个例子。你做了一个内容创作Agent,它既能写产品文案,又能写技术文档,还能做竞品调研。如果不管用户说什么,都把三套指令和两套资料库全量塞进上下文,模型的表现一定会很糟——过多的任务定义会互相干扰,模型甚至可能把写技术文档的严谨语气用在了趣味文案上。
正确的做法是:先让一个轻量的分类模型(或者一个廉价的规则判断器)识别用户意图,然后只加载对应技能所需的提示词和知识片段,再拼接用户当前输入,最后才发送给主模型执行。这个过程,就是上下文的路由。不少成熟的Agent框架,比如LangChain里的RouterChain一类组件,干的就是这件事。
路由阶段的另一个要点是控制多工具调用的上下文污染。有些Agent在一个任务里要连续调用多个工具,如果把前一个工具的完整原始输出一字不改地塞给下一个工具,很容易超出上下文限制。我习惯给每个工具定义统一的输出Schema,返回前先做字段裁剪和格式整理,只保留对后续步骤有意义的那几个字段。相当于微服务之间的接口约定——你返回给我的,应该是我真正消费得动的数据。
2.3 维护阶段:让上下文跟上对话节奏
多轮对话是上下文生命周期管理中最容易失控的场景。每轮用户的提问,都会基于上一轮模型的回答;每轮工具调用,都可能改变业务状态。上下文如果只是简单“追加”,体积会不断膨胀,而且早期信息可能已经过时。
维护阶段要做的,是持续跟踪上下文中关键信息的变化。比如你做一个多步骤的数据分析Agent,用户先上传了一份CSV,然后要求“先做数据清洗,再统计缺失值,最后画分布图”。每一步执行完后,任务状态都发生了变化。你需要在上下文中动态更新:哪些文件已经处理过了、当前处于哪个步骤、下一步的输入是什么。
一个实用的技巧是引入“状态摘要”。每完成一个子任务,就把这一步的结论浓缩成几十个Token的摘要,覆盖掉原始的冗长输出。比如原始工具返回了一个500行的JSON表格,经过摘要后变成“数据清洗完成:删除12条重复记录,修复38处缺失值,无异常编码”。这样下一轮模型做判断时,既能获得足够的信息,又不用面对原始的海量数据。这种“摘要化”手段,是控制上下文体积增长最有效的策略之一。
维护阶段还涉及上下文版本管理的概念。你可能会调整系统提示词、替换参考文档或者改变工具配置。每次调整后,上下文的语义都发生了变化。如果调试时发现模型表现异常,你最好能回溯到上一版上下文的状态去对比。这跟代码版本回滚是同一个道理。我以前用自己实现的ContextManager时,给每次上下文快照加过一个版本号,调试效率提升非常明显。
2.4 清理阶段:上下文超限不是灾难,而是信号
很多开发者一看到“context length exceeded”就紧张,觉得是自己没处理好。我的看法正好相反:上下文超限是一个重要的健康信号,它提醒你“当前的任务状态已经积累了太多过时的信息,该做一次清理了”。
清理阶段的策略有好几层,从轻到重分别是:丢弃、截断、摘要、外部化。丢弃最粗暴,直接删掉最早的用户消息,适合早期寒暄;截断相对精细,按照重要程度逐条淘汰,比如优先保留最近的对话和系统指令;摘要就是我们前面提到的方式,将早期对话整体压缩为一段概述;外部化则是把详细的历史记录存到数据库或向量检索里,需要时再按关键词捞回上下文。
选择一个怎样的清理策略,取决于你的业务对历史信息的敏感度。如果只是写代码、做转换这类无状态任务,丢几条旧消息影响不大;但如果是法律咨询、医疗问诊这类需要完整追溯历史的场景,你就必须做摘要,甚至要保留可查询的外部存档。
清理动作不能等到超限报错才做,而应该设置一个“水位线”。比方说,当已用Token达到窗口大小的70%时,就触发一次清理流程。提前清理,既不会影响当前任务的连贯性,又让模型始终有充裕的上下文空间处理最新输入。这跟磁盘告警阈值是一个道理——你不可能等到磁盘写满了再去做迁移。
3. 实操:为你的AI代理搭建上下文生命周期管理
3.1 一个可运行的上下文管理器骨架
光讲理论不落地,等于白说。下面我用FastAPI配合一个大语言模型API的方式,演示一个简单的上下文管理器是怎么工作的。它做的事情不复杂,但足够勾勒出生命周期管理的基本轮廓:
# context_lifecycle.py import time import json from dataclasses import dataclass, field from typing import List, Optional, Callable from enum import Enum class ContextStage(Enum): CREATED = "created" ROUTED = "routed" MAINTAINED = "maintained" ARCHIVED = "archived" @dataclass class ContextUnit: role: str content: str stage: ContextStage = ContextStage.CREATED token_count: int = 0 created_at: float = field(default_factory=time.time) class ContextManager: """一个简单的上下生命周期管理器""" def __init__(self, max_tokens: int = 8000, watermark: float = 0.7): self.units: List[ContextUnit] = [] self.max_tokens = max_tokens self.watermark = watermark self.summarizer_history: List[str] = [] def append(self, role: str, content: str): """追加一条上下文,并触发水位线检查""" unit = ContextUnit(role=role, content=content) self.units.append(unit) self._check_watermark() def _total_tokens(self) -> int: # 生产环境可以用tiktoken计算,这里按字符数粗略估算 return sum(len(u.content) // 2 for u in self.units) def _check_watermark(self): """当上下文使用量超过水位线,触发压缩""" if self._total_tokens() >= self.max_tokens * self.watermark: self._compress_early_units() def _compress_early_units(self): """将最早的几条用户/助手消息压缩成一段摘要""" early_units = self.units[:-4] # 保留最近4条 if len(early_units) < 3: return compressed = self._summarize(early_units) self.units = [ContextUnit( role="system", content=f"[历史对话摘要] {compressed}", stage=ContextStage.ARCHIVED, token_count=len(compressed) // 2 )] + self.units[-4:] def _summarize(self, units: List[ContextUnit]) -> str: """调用模型对早期消息做总结""" # 这里留给你对接具体的模型API return "[这里由模型根据早前对话生成概述]" def snapshot(self) -> List[dict]: """输出当前可用于API请求的消息列表""" return [{"role": u.role, "content": u.content} for u in self.units]这个类做的事情,就是维护一个消息列表,当总长度超过水位线时,把最靠前的历史消息压缩成一条系统摘要。核心逻辑很少,但已经把生命周期管理的构建、维护、清理三个环节串起来了。
用一个FastAPI接口来演示它的使用方式:
# main.py from fastapi import FastAPI, HTTPException from context_lifecycle import ContextManager from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): user_message: str class ChatSession: def __init__(self): self.ctx = ContextManager(max_tokens=8000, watermark=0.7) self.ctx.append("system", "你是可靠的技术助手,回答简洁且准确。") sessions: dict[str, ChatSession] = {} @app.post("/chat/{session_id}") async def chat(session_id: str, req: ChatRequest): if session_id not in sessions: sessions[session_id] = ChatSession() session = sessions[session_id] user_content = req.user_message.strip() if not user_content: raise HTTPException(status_code=400, detail="Request body cannot be empty") session.ctx.append("user", user_content) # 这里替换为你实际使用的模型API调用 # response = call_openai_style_api(session.ctx.snapshot(), temperature=temperature) response_content = "模型返回的示例回答" session.ctx.append("assistant", response_content) return {"reply": response_content}你可能注意到,我没有把真实的模型API调用放进来,因为代码的重点是生命周期管理的骨架,而不是某一家模型的对接细节。你只要把那段注释替换成你自己的API调用即可。
3.2 上下文超限时的自动摘要与降级策略
刚才的ContextManager里预留了_summarize方法。这一步是上下文生命周期管理中的重头戏,值得单独展开讲。
一个常见的摘要策略是“分层摘要”。想象你在跟模型长谈三小时,如果要把三小时的对话全文塞进上下文,早就爆了。但你可以每半小时生成一次摘要,最后再对摘要做二次摘要,形成金字塔结构。建模时,你只需要携带最近一轮的完整对话和最顶层的摘要即可。
实现上,我建议在调用摘要模型时使用一个独立、便宜的模型。这里注意几个要点:摘要模型也要有一个明确的指令,比如“提取对话中所有已确认的需求、技术决策、用户偏好和未完成事项”,而不是让它笼统地“总结一下对话”——否则摘要会丢关键信息。
分层摘要之外,另一个实用的降级策略是“关键信息抽取”。不是所有历史内容都值得被摘要,有些信息本来就该被直接丢弃。比如用户问候的客套话、和当前任务无关的闲聊、重复表达同样意思的冗余发言。在一次对话中,我先让摘要模型判断“哪些内容与当前任务目标有关”,只对有关部分生成摘要。这个过程,本质上是把上下文的清理和路由结合了起来。
还有一个容易被忽视的坑:摘要本身的Token消耗。每次触发压缩,摘要模型会产生一个输入和输出。如果对话频率很高,频繁地做摘要会产生可观的额外成本。所以我给压缩动作设置了一个最小间隔——比如60秒内最多触发一次,避免短时间大量消息涌入时反复做无意义的压缩。
外部化存储也值得一提。上下文生命周期到了“归档”阶段,并不意味着信息彻底消失。我会把完整的原始消息记录同步写入数据库,同时也把摘要存一份带时间戳的版本。后续如果用户问“我们之前讨论过的那个功能点是什么”,Agent就可以通过检索去数据库捞取当时的完整记录,再作为新增的上下文块注入当前对话。这样,有限的上下文窗口留给了当前任务,历史知识则放进了无限的外部存储。
3.3 在IDE、本地模型和复杂代理中的延伸应用
上下文生命周期管理不只适用于后端API调用。现在很多人每天都在用的AI编程助手,比如Cursor这类工具,底层也在做类似的事情。你打开一个项目,它需要把哪些文件当作上下文给你的AI对话窗口,这其实就是“构建阶段”的决策。你有没有手动指定过@文件引用?有没有用过“Codebase索引”?这些动作,本质上都是在构建和路由上下文。
结合搜索词里的“ai代理助手加本地模型”,再补充一句:本地模型因为参数规模小,上下文窗口往往更紧张,生命周期管理的价值反而更大。云端模型动辄支持几十万Token的上下文,你可以稍稍偷懒,但本地模型通常只有4K、8K、16K的场景,如果不在上下文构建和清理上下功夫,几乎必然会被截断问题困扰。
对于复杂的、多步骤的Agent,我还会做“上下文数据流图”的分解。我会先画出整个代理执行过程中的信息流向:什么数据从哪个工具来,经过什么转换,最后进入模型的哪一次调用里。画完这张图,哪些环节会堆积冗余数据,哪些环节存在上下文越权访问,都能看得一清二楚。这跟调试代码时的调用链追踪是类似思维。
4. 常见问题与排查技巧实录
4.1 幻觉问题:先查上下文,再查参数
我处理过的绝大多数“AI幻觉”案例,刨根问底之后,问题都出在上下文。要么是相关材料根本没有进入上下文,模型只能靠“印象”去编;要么是无关信息太多,把模型真正要用的关键信息挤压甚至截断了。
排查幻觉问题时,我通常按这个顺序走一遍:先打印出每一次调用模型的完整请求数据,确认系统提示、历史消息、检索资料实际送进去的内容是什么;然后手动判断这些内容里是否有直接回答当前问题的信息;如果信息确实在,再看是否被截断、位置是不是太靠后。绝大多数幻觉案例在前两步就能定位方向。
温度参数的影响我也顺带提一句。温度控制的是模型生成时的随机性。很多人习惯把所有任务都调成0.7,这是不对的。需要确定性输出的任务(比如信息抽取、代码生成、结构化输出),我通常把温度调到0~0.3;只有创意写作、头脑风暴类任务才调高到0.8以上。但请注意:温度调低可以减少“随机编造”,却无法弥补上下文缺失导致的“无据可依”。
4.2 上下文超限的报错与应对
不同模型服务商的报错信息不一样。有的提示“maximum context length exceeded”,有的返回4xx错误码,还有一些SDK会直接抛出异常。遇到这类报错,第一反应应该是查看“发送出去的请求”到底有多少Token,而不是急着去调参数。
如果发现确实是单次请求就超限,那就要优化上下文输入:检查系统提示词是否过于冗长,参考文档是否有更精简的版本,历史消息是否需要压缩。如果请求本身没有超限但经常在多次调用后失败,那大概率是会话管理端没有做水位线检查,下一次请求还在增长,终于在某一步突破了上限。
我自己的做法是在调用API封装层统一拦截这一类错误,收到超限信号时自动触发一次上下文压缩逻辑,压缩完重试一次。这个兜底机制,极大减少了线上代理的崩溃率。但需要提醒的是,兜底机制只能救急,不能替代前面的水位线管理——如果每次都靠超限后再压缩,体验和成本都不可控。
4.3 上下文与执行环境:两个容易混淆的概念
热词里面提到的“执行上下文”和“js的this指向”,其实跟我们要讨论的AI代理上下文不是一回事,但在排查问题时经常被混在一起。从编程语言的视角看,执行上下文关心的是当前代码运行时可以访问到的变量和函数;AI代理的上下文关心的则是模型生成回复时可以看到的信息集合。前者是程序语义层面的概念,后者是模型输入层面的数据。
不过我们可以借鉴一些编程实践来管理代理上下文。比如,很多语言强调“变量作用域要尽量小”,代理上下文设计也可以有类似思路:某个工具函数执行时,只需要把它真正用到的数据片段传入,而不要让它看到全部用户对话。这样的隔离设计,不仅减少了上下文体积,还降低了敏感信息泄露的风险。
另外,如果你在FastAPI这类异步框架里做代理开发,注意HTTP请求本身也有一个上下文对象。你需要小心区分:哪些是FastAPI的请求上下文(比如当前用户、请求头),哪些是你要组装给模型看的对话上下文。把它们混在一个数据结构里,代码会越来越难维护。
4.4 一个速查表:不同场景的上下文管理推荐配置
最后分享一张我平时常用的配置参考表,覆盖几个典型场景。这里的数值不是金科玉律,仅供参考。核心是让你有个起点,再根据实际效果微调。
| 场景 | 系统提示词建议 | 历史保留策略 | 水位线触发阈值 | 建议温度 |
|---|---|---|---|---|
| 代码生成/补全 | 简洁,3~5条规则 | 保留最近5~10轮 | 60% | 0~0.2 |
| 信息抽取/结构化输出 | 明确输出格式要求 | 保留最近2~3轮即可 | 50% | 0~0.1 |
| 技术问答/知识库RAG | 定义角色和答案风格 | 保留最近5轮 | 70% | 0~0.4 |
| 创意写作 | 给出主题和调性参考 | 保留较多历史,尽早摘要 | 75% | 0.8~1.0 |
| 多步骤数据分析代理 | 定义步骤和工具边界 | 每个子任务结束立即摘要 | 65% | 0~0.3 |
每次调整完上下文策略,我都建议做一轮回归测试:准备固定的10~20条任务样本,分别记录回答的准确率、上下文Token消耗、平均响应延迟。不要依赖感觉做决策,用数据说话。如果你懒得自己搭评测集,也可以在项目里临时建一个简单的脚本,把每次调用的请求和响应都存成日志,之后抽样分析。
我自己踩过不少坑,试过调大温度值想“提升创造力”,结果只是增加了噪声;试过把所有历史对话一股脑都塞进去,结果成本翻倍、准确率反而下降;也试过把摘要做得太激进,结果模型丢失了用户曾经明确表达过的偏好。后来我总结出来,上下文管理这件事没有一劳永逸的银弹,它就是一套需要持续观察、持续调优的工程实践。
最后再分享一个小技巧:每次修改完上下文管理逻辑后,我会专门跑一轮“长时间对话压力测试”。让Agent连续处理多个相关任务,人为制造上下文增长,观察它在不同阶段的表现是否稳定。通过这种方式,可以在项目上线前提前暴露很多生命周期管理上的薄弱环节。这套做法,算是真正把“上下文开发”当成一个严肃的软件工程问题了,而不是靠临时抓瞎去救火。