Agent 上下文管理详细教程
Agent 上下文管理详细教程
面向进阶开发者:从原理到实战,系统讲解 LLM Agent 的上下文(Context)管理。
适合已上手过 LangChain / OpenAI / 自研 Agent,但被"上下文爆掉"、“记忆混乱”、"成本失控"困扰的同学。
目录
- 为什么需要上下文管理
- 上下文窗口与 Token 基础
- 上下文管理的核心难题
- 六大管理策略详解
- 实战:一个完整的上下文管理器
- 策略组合与选型
- 最佳实践清单
- 总结
1. 为什么需要上下文管理
一个 Agent(智能体)的核心是循环:感知 → 思考 → 行动 → 观察。在每一轮循环中,它都需要把"到目前为止的所有信息"作为上下文喂给 LLM。
如果你做过真实的 Agent 项目,你一定遇到过这些问题:
- ❌上下文爆掉:对话进行到第 20 轮,报错
This model's maximum context length is 8192 tokens。 - ❌记忆混乱:Agent 忘了用户两轮前的要求,重复执行同一操作。
- ❌成本失控:每轮把 5000 行的历史日志全部塞进 prompt,API 账单飞涨。
- ❌响应变慢:prompt 越长,首字延迟(TTFT)越高,用户等不起。
上下文管理,就是要在"信息完整"与"成本可控"之间找到平衡。它决定了 Agent 的稳定性、性能和经济性。
2. 上下文窗口与 Token 基础
2.1 上下文窗口(Context Window)
上下文窗口是模型单次请求能处理的最大 Token 数,包含输入(prompt)+ 输出(completion)。
| 模型 | 上下文窗口 |
|---|---|
| GPT-4 | 8K / 32K / 128K |
| GPT-4o | 128K |
| Claude 3.5 Sonnet | 200K |
| DeepSeek-V3 / R1 | 64K / 128K |
| Llama 3.1 405B | 128K |
注意:窗口越大 ≠ 越适合长上下文。研究表明,模型对"中间部分"的注意力会衰减,即"Lost in the Middle"(中间迷失)现象。所以哪怕窗口够大,也要主动管理关键信息的位置。
2.2 Token 是计费单位
不要把 Token 想成"字数"。中文大体上1 汉字 ≈ 1.5~2 Token,英文 1 单词 ≈ 1.3 Token。
"你好世界" → 4 个汉字 ≈ 6~8 Token "Hello World" → 2 个单词 ≈ 3 Token用 Python 快速估算:
importtiktoken# OpenAI 的 tokenizerdefcount_tokens(text:str,model:str="gpt-4")->int:enc=tiktoken.encoding_for_model(model)returnlen(enc.encode(text))print(count_tokens("你好,世界!Hello World!"))# 输出约 12~153. 上下文管理的核心难题
在动手写代码前,先理解我们要解决的三个矛盾:
3.1 记忆的"保质期"
- 短期记忆(工作记忆):当前对话轮次,必须保留。
- 长期记忆(长期偏好/事实):用户说过"我偏好 Python",几轮后仍要生效。
- 过期信息:已完成任务的中间推理,可以丢弃或压缩。
3.2 关键信息的"定位"
Agent 的每一步推理都依赖当前状态(已执行了哪些动作、结果如何)。如果动作列表被截断,Agent 会"失忆"。
3.3 成本与质量的权衡
每多 1K token 输入,费用增加,但信息更全。管理策略的本质是在信息熵和 token 预算之间做取舍。
4. 六大管理策略详解
策略一:滑动窗口(Sliding Window)
思想:只保留最近 N 轮对话,丢弃最早的部分。最简单、最常用。
fromcollectionsimportdequefromdataclassesimportdataclass,field@dataclassclassMessage:role:str# "system" | "user" | "assistant" | "tool"content:strclassSlidingWindowBuffer:"""滑动窗口:只保留最近 max_messages 条消息"""def__init__(self,max_messages:int=20):self.max_messages=max_messages self.system_prompt:Message|None=Noneself._history:deque[Message]=deque()defadd(self,msg:Message):ifmsg.role=="system":self.system_prompt=msgreturnself._history.append(msg)# 超出窗口则从最前面弹出whilelen(self._history)>self.max_messages:self._history.popleft()defbuild_prompt(self)->list[dict]:messages=[]ifself.system_prompt:messages.append({"role":"system","content":self.system_prompt.content})messages+=[{"role":m.role,"content":m.content}forminself._history]returnmessages优点:实现简单、零推理开销。
缺点:粗暴丢弃导致"慢性失忆";窗口太小丢关键信息,太大仍会爆。
策略二:Token 预算控制
思想:不按"轮数"截断,而是按"Token 数"截断——从最旧的消息开始删,直到总 token 数低于预算。
deftrim_to_budget(messages:list[dict],budget:int,count_fn)->list[dict]:"""从最旧的开始删,直到总 token 数 <= budget。 count_fn: (text) -> token_count 的函数。"""total=sum(count_fn(m["content"])forminmessages)# 保留 system 消息不动kept=[mforminmessagesifm["role"]=="system"]rest=[mforminmessagesifm["role"]!="system"]forminrest:iftotal<=budget:breaktotal-=count_fn(m["content"])else:# rest 全删了还不够?那只能压缩 systempassreturnkept+rest[len(rest)-len(kept):]ifFalseelsekept+[mforminrestif...]⚠️ 上面的伪代码只演示思路。生产环境请用成熟的库(见第 5 节的
LangChain方案),不要手写 tokenizer 边界判断。
策略三:摘要压缩(Summarization)
思想:把旧消息交给 LLM 生成摘要,用一小段摘要替代大量原文。这是"记忆保质期"问题的标准解法。
importopenaiclassSummarizingBuffer:def__init__(self,threshold_tokens:int=3000,summary_llm=None):self.threshold=threshold_tokens self.summarizer=summary_llmorself._default_summarize self.summary=""# 累积的历史摘要self.recent:list[Message]=[]# 最近原始消息defadd(self,msg:Message):self.recent.append(msg)# 若近期消息超阈值,触发压缩ifsum(count_tokens(m.content)forminself.recent)>self.threshold:self._compress()def_compress(self):merged=self.summary+"\n".join(m.contentforminself.recent)self.summary=self.summarizer(merged)self.recent.clear()def_default_summarize(self,text:str)->str:resp=openai.chat.completions.create(model="gpt-4",messages=[{"role":"system","content":("你是记忆压缩器。把下面的对话压缩成简洁摘要,""保留:用户偏好、完成的动作、关键结论、待办事项。""不要遗漏重要事实。"),},{"role":"user","content":text}],)returnresp.choices[0].message.contentdefbuild_prompt(self):messages=[]ifself.summary:messages.append({"role":"system","content":f"[历史摘要]\n{self.summary}"})messages+=[{"role":m.role,"content":m.content}forminself.recent]returnmessages优点:显著压缩 token,保留核心记忆。
缺点:有损(摘要丢细节);每次压缩有 LLM 调用成本;摘要可能"漂移"(越压越失准)。
进阶技巧:分层摘要(Hierarchical / Rolling Summary)——定时对已有摘要再做二次摘要,避免摘要无限增长。
策略四:向量检索(RAG 式记忆)
思想:把历史消息切成块(chunk),Embedding 后存入向量库;每轮只检索与当前问题最相关的 K 个块注入上下文。
这是处理"超长/跨会话记忆"的最优解。
# 用 Chroma 做向量存储(示例)importchromadbfromchromadb.utilsimportembedding_functions client=chromadb.Client()col=client.get_or_create_collection("agent_memory",embedding_function=embedding_functions.DefaultEmbeddingFunction(),)defstore_memory(text:str,mid:str):col.upsert(ids=[mid],documents=[text])defretrieve_memory(query:str,top_k:int=5)->list[str]:res=col.query(query_texts=[query],n_results=top_k)returnres["documents"][0]结合 Agent 的典型流程:
defbuild_context_with_memory(user_query:str)->list[dict]:# 1. system prompt(固定)system={"role":"system","content":AGENT_PROMPT}# 2. 检索相关历史记忆memories=retrieve_memory(user_query,top_k=5)memory_block="\n".join(f"-{m}"forminmemories)# 3. 动态注入return[system,{"role":"system","content":f"[相关历史记忆]\n{memory_block}"},{"role":"user","content":user_query},]优点:可扩展、跨会话、成本可控、信息命中率高。
缺点:需要向量库基础设施;检索质量依赖 chunk 切分与 embedding 效果;对"时序性"敏感(检索结果无序)。
进阶提醒:检索结果要带上时间戳,并在 prompt 里告诉模型"越新的越可信",否则可能把旧记忆当新事实。
策略五:结构化记忆(Memory Store)
思想:不进向量库、也不进 prompt,而是把 Agent 提取出的结构化事实(用户偏好、实体关系、任务状态)存进数据库;需要时再以结构化形式注入。
fromdataclassesimportdataclass@dataclassclassMemory:key:str# "user.preference.language"value:str# "Python"timestamp:floatconfidence:floatclassMemoryStore:def__init__(self):self._mem={}# key -> Memorydefextract_and_store(self,llm_response:str):"""让 LLM 从回复中提取结构化记忆"""# 示例:正则或 LLM 解析出 key-valuefacts=parse_facts(llm_response)# 伪代码fork,vinfacts:self._mem[k]=Memory(k,v,now(),1.0)defrelevant(self,keys:list[str])->str:return"\n".join(f"{k}:{self._mem[k].value}"forkinkeysifkinself._mem)优点:记忆精准、可查询、可更新(覆盖旧值)、token 开销极小。
缺点:需要设计记忆 schema;抽取环节有失败风险。
策略六:工具调用时的"上下文隔离"
思想:不要把所有工具结果都堆进主对话。超大工具输出(如日志、数据库返回)应单独存储,只把"摘要/指针"留在上下文。
TOOL_RESULT_STORE={}defrun_tool_with_large_output(tool_name:str,args:dict)->str:raw=call_tool(tool_name,args)# 可能是 10 万 tokenrid=f"{tool_name}:{uuid4()}"TOOL_RESULT_STORE[rid]=raw# 存到外部# 只把摘要 + 引用 ID 留给 LLMsummary=summarize(raw,max_tokens=200)returnf"[结果已存储] 引用ID={rid}\n摘要:{summary}\n如需全文请调用 read_result(id)"模型需要全文时,再显式调用read_result(rid)获取。
5. 实战:一个完整的上下文管理器
把上面策略整合成一个可用的管理器。这里用LangChain(成熟方案)演示,避免手写 tokenizer 边界。
fromlangchain_core.messagesimportHumanMessage,AIMessage,SystemMessagefromlangchain.memoryimportConversationTokenBufferMemoryfromlangchain_anthropicimportChatAnthropic# 1. 基于 token 预算的滑动窗口llm=ChatAnthropic(model="claude-3-5-sonnet-20241022")memory=ConversationTokenBufferMemory(llm=llm,max_token_limit=2000,# 上下文 token 预算return_messages=True,)# 2. 模拟多轮对话foruser_msgin["你好","帮我列 Python 学习计划","第二步详细一点"]:memory.chat_memory.add_user_message(user_msg)resp=llm.invoke(memory.chat_memory.messages)memory.chat_memory.add_ai_message(resp.content)# 3. 查看当前上下文(已被裁剪到预算内)forminmemory.chat_memory.messages:print(m.type,"|",m.content[:50])自研版:策略组合管理器
classContextManager:"""组合策略:窗口 + 预算 + 摘要 + 向量检索"""def__init__(self,token_budget:int,summary_llm,vector_store):self.budget=token_budget self.summarizer=summary_llm self.vector=vector_store self.summary=""self.recent:list[Message]=[]defadd(self,msg:Message):self.recent.append(msg)self.vector.store(msg)# 写入向量库(长期记忆)ifself._tokens(self.recent)>self.budget*0.7:self._summarize_old()# 超预算 70% 触发压缩defbuild_context(self,query:str)->list[dict]:# 1. 检索相关记忆memories=self.vector.query(query,top_k=3)# 2. 只保留最近消息 + 摘要 + 检索出来的记忆base=[{"role":"system","content":f"[历史摘要]\n{self.summary}"},{"role":"system","content":f"[相关记忆]\n{memories}"},]+[m.to_dict()forminself.recent[-5:]]returnbase6. 策略组合与选型
| 场景 | 推荐策略 | 原因 |
|---|---|---|
| 简单对话 bot | 滑动窗口 + 预算 | 够用、便宜、零依赖 |
| 单会话多轮工具调用 | 摘要压缩 + 工具隔离 | 保留推理链条,防爆 |
| 跨会话长期记忆 | 向量检索 + 结构化记忆 | 可扩展、可查询 |
| 复杂生产 Agent | 窗口 + 摘要 + 向量 + 结构化 组合 | 各取所长 |
通用组合公式:
系统提示词(固定,最前面) + 历史摘要(压缩后,靠前) + 检索到的相关记忆(动态,中前部) + 最近 N 轮原始消息(完整,靠后,距离提问最近)💡 依据"Lost in the Middle":最重要的信息放最前或最后,别放中间。通常把 system(规则)放最前,把当前问题相关的最近消息放最后。
7. 最佳实践清单
- Always 保留 system prompt:它定义 Agent 人格与规则,永不截断。
- 区分记忆类型:短期(轮次内)、长期(事实/偏好)、工作记忆(当前状态),分开管理。
- Token 预算要留余量:给模型输出留出空间(输出也占窗口)。
- 摘要要防"漂移":定期用原始数据校验摘要,或做分层摘要。
- 向量检索带时间上下文:检索结果标注时间戳,引导模型判断时效。
- 大工具输出走外部存储:绝不把巨型结果直接塞 prompt。
- 监控与埋点:记录每轮 token 用量、截断次数、摘要触发频率,用数据调参。
- 先压后测:任何策略上线前,用回归测试集验证"关键能力没丢"。
8. 总结
上下文管理是 Agent 工程中性价比最高的一环——它直接决定 Agent 能跑多久、多稳、多省钱。核心就一句话:
该留的留(系统提示、当前状态、关键记忆),该压的压(旧对话、巨型输出),该查的查(向量检索)。
从最简单的滑动窗口开始,业务变复杂后再逐步叠加摘要与向量检索,永远是性价比最高的演进路径。
如果你对某个策略(尤其是向量检索的 chunk 切分、或摘要防漂移)想深入了解,欢迎评论区交流 👋
本文为原创技术教程,转载需注明出处。
