ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent上下文工程:从记忆管理到LangGraph实战

Agent上下文工程:从记忆管理到LangGraph实战 做 Agent 开发的这几个月我最大的体会是真正难的不是把模型调通而是让 Agent 在多轮、多工具、多任务的状态下始终保持清醒。上下文工程这个名词听起来像学术黑话实际上就是解决 Agent 记忆力、注意力、判断力的系统工程。如果你也在用 LangChain、LangGraph、FastAPI 这套技术栈搭 Agent或者正在被跑了几轮就开始胡言乱语的问题折磨这篇文章应该能给你一条清晰的路线。我对上下文工程的定义很简单它是 Agent 系统的内存管理。你给大模型喂什么、按什么顺序喂、喂多少、什么时候回收重写直接决定了 Agent 是聪明稳定还是像个间歇性失忆的患者。接下来我把自己踩过的坑、验证过的方案、以及一整套可复用的设计思路按模块拆开来讲。1. 从调 Prompt到管上下文Agent 的智力上限取决于它的内存管理很多人以为 Agent 变聪明 换更大的模型 写更长的 Prompt。我一开始也是这么想的直到被现实教育了一顿。换了个更强的模型面对长对话场景照样忘事把 Prompt 写到 3000 字结果模型只记住了开头和结尾。问题不在模型在于 Agent 每一步真正看见的信息组织得太粗糙了。1.1 Prompt 和 Context 是两回事Prompt就是你对模型说的话它通常是一段固定的指令。Context则是模型生成答案时手里握着的全部材料——系统指令、历史对话、工具定义、检索回来的文档片段、工具返回的结果、当前这一问的问题。在只有一个对话轮次时Prompt 和 Context 的差别不大但在 Agent 这种多步决策系统里Context 是动态变化的每一步执行都可能往里面追加新的信息。一个 Agent 完整跑一次任务Context 一般由这几部分组成系统指令角色、行为边界、输出格式要求工具定义Agent 能调用哪些函数每个函数的参数 schema历史消息之前轮次的用户消息和助手回复工具执行记录哪个工具被调用、传入了什么参数、返回了什么结果检索证据从知识库或数据源中捞回来的片段当前输入用户最新说的这句话这些内容堆在一起构成了模型做下一步决策所依赖的全部信息。这句话换个角度说如果这一堆信息组织得混乱模型再强也会做错决策。1.2 为什么 Agent 比 ChatBot 更依赖上下文工程ChatBot 的上下文管理可以偷懒——它只需要把历史消息喂回去就行了。但 Agent 不一样它要调用工具、根据返回结果做推理、推理完再调用下一个工具这个链条只要有一环信息没接通整个任务就崩了。我举个实际例子。某次我搭了一个客服 Agent它需要先调用订单查询工具再根据订单状态回答用户问题。前几轮表现都正常到第 5 轮左右开始发疯用户问那我之前那个投诉处理了吗Agent 回答请提供订单号但用户第一轮就提供过了而且 Agent 自己也成功查过单。为什么因为我在构造上下文时把工具调用记录和工具返回结果放在了历史消息的中间位置前面又插入了新的检索文本把关键信息挤到了模型的注意力盲区。这个例子说明了一个关键点Agent 的上下文工程本质上是给 Agent 每一步的决策提供刚刚好的信息支撑。少了会失忆多了会稀释注意力顺序错了会误导推理。这不是调 Prompt 能解决的问题而是要在系统架构层面做设计。1.3 上下文工程是持续运行时的管理不是写一次就完事还有一点很容易被忽视上下文工程是运行时行为不是静态配置。同一个会话跑第 1 轮和第 50 轮上下文长得完全不一样。要不要压缩要不要遗忘要不要把旧对话变成摘要这些都是要在代码里动态判断的逻辑。我后来把上下文工程理解成一套关于信息选择与信息组织的决策系统什么时候保留原始内容、什么时候用摘要替代、什么时候把信息从长时记忆里捞回来、什么时候把垃圾信息踢出去。这些决策需要一套完整的生命周期管理。下一节就按这个思路拆开讲。2. 把上下文拆成一条流水线采集、组织、注入、回收在代码里落地之前我习惯先在纸上画一条上下文生命周期流水线。无论你用什么框架——LangChain、LangGraph、Spring AI 还是自己写状态机——这条流水线都是通用的。它包括四个环节采集、组织、注入、回收。2.1 采集从输入端控制质量采集是指用户输入和外部数据进入系统的那一步。很多人忽略了这一步对上下文质量的影响。用户贴了一篇 8000 字的文章让 Agent 分析你是不是原封不动塞进上下文用户一句话里带着多个错别字和无关废话你是不是也照单全收我在这条流水线里加入了两个处理输入截断和数据清洗。超过一定长度的用户输入先做一个初始摘要把原文存在存储里备用明显无意义的噪声信息比如连续的表情符号刷屏、无语义的乱码直接过滤掉。这个动作看起来不起眼但对上下文预算的节省立竿见影。2.2 组织把信息结构化成模型容易读的格式信息进入系统后面临的第一个问题是以什么形态存在。原始字符串是最好的形态吗不是。我在设计消息结构时会非常刻意地区分消息类型user_message用户自然语言输入assistant_messageAgent 的推理和回复tool_callAgent 发起的工具调用请求工具名 参数tool_result工具返回的结构化结果system_event系统自动产生的提示比如上下文已压缩、已提取关键实体这种结构化存储的价值在于组装上下文时你可以按类型做聚合并决定优先级而不是面对一堆无差别的字符串。同时工具结果我会做第二层处理把 JSON 格式化成可读的条目文本。这不是为了让人类看懂是为了让模型在推理时更容易定位到关键字段。2.3 注入按窗口分配预算按优先级排顺序注入环节解决两个问题什么该进窗口、什么不该进进了窗口之后放在哪。我的经验是把上下文窗口当做一个办公桌来管理。桌面上只放当前任务需要的材料参考资料放进旁边的抽屉用的时候再拿出来。对应到技术实现上就是始终优先注入当前决策最依赖的信息——当前问题、工具定义、最近的决策链次优先是近期对话原文再其次是早期历史的压缩摘要。过期的、无关的、已经被摘要覆盖过的内容不占窗口。2.4 回收主动遗忘与压缩重写最后一个环节最容易被人忽略回收。人脑需要睡眠来整理记忆Agent 系统需要主动的遗忘机制。我常用的回收策略有两种。第一种是滑动窗口加摘要保持最近 N 轮对话的原文向早期历史滚动生成摘要摘要替换原文进入上下文。第二种是定期记忆固化把某个任务的关键信息用户偏好、已完成操作、重要实体提取出来写入持久化的记忆存储下次会话开始时直接注入。这套生命周期管理能保证上下文是流动的、新鲜的而不是一个只增不减的垃圾场。3. Token 预算这笔账Agent 能跑多少轮取决于你兜里有多少钱上下文工程绕不开成本问题。我见过不少团队模型换成了 128K 窗口操作上却仍然把所有历史全塞回去结果 Token 消耗高得吓人模型并没有变聪明——因为每轮都塞满几万 Token注意力早就被稀释了。Token 预算是上下文工程里最需要算清楚的一笔账。3.1 Token 消耗的三条线一个 Agent 系统的 Token 支出主要涵盖三块输入、输出、缓存命中。输出相对可控因为你要控制回复长度缓存命中取决于你有没有做 Prompt 缓存真正容易失控的是输入。输入 Token 的构成又分三块系统指令、工具定义、历史与检索内容。前两者每次调用基本都固定历史与检索内容才是变量。我在给 Agent 做压测时最怕看到这样的日志一轮对话消费 80K 输入 Token其中 75K 都是历史消息。这属于典型的上下文预算失控。3.2 一层层分配预算我习惯在搭建 Agent 前先做一个预算分配表用表格把每一层该占多少空间定死。假设模型窗口是 128K上下文层预算占比说明系统指令与角色设定2%~3%固定开销控制精简工具定义含参数 schema3%~5%只为当前任务加载必要工具检索证据 / 参考资料5%~10%只注入与当前问题强相关的片段近期对话原文15%~25%保留最近 5~10 轮视任务复杂度调整早期历史摘要5%~10%压缩沉淀只保留结论与状态输出预留6%~10%给模型生成答案留下足够空间安全冗余10%~15%应对工具返回超长结果等突发情况这个表的执行逻辑很简单先给固定项系统指令、工具定义、输出预留留出空间再给当前问题和检索证据留位置剩下的空间才分配给对话历史。如果历史太多就做摘要不要让某一块无限膨胀去挤占其他模块的预算。3.3 用首因效应和近因效应安排顺序预算是空间维度顺序是影响效果的另一维度。心理学里的首因效应和近因效应在 LLM 上下文里同样成立模型对上下文开头和结尾的信息更敏感对中间部分容易失忆。基于这个原理我的上下文顺序通常是这样的系统指令开头利用首因效应让模型明白角色和任务当前需要强关联的检索证据早期历史的压缩摘要近期对话原文中间偏前的位置当前用户问题结尾利用近因效应让模型聚焦这一问输出格式要求与约束可以在系统指令里也可以在结尾二次强调这个顺序看起来简单实际带来的效果提升非常明显。我常用 A/B 对比验证同一批测试问题优化前模型准确率在 60% 左右优化后能到 85%。4. 实战落地用 LangGraph 把上下文工程嵌进 Agent 里附代码理论讲完来说说在 FastAPI LangChain LangGraph 这套技术栈里怎么落地。LangGraph 的核心优势是它天然支持状态管理——State 里的内容会随着节点执行而演化这正好是上下文工程需要的运行时动态能力。4.1 消息历史的结构化存储第一件事别把上下文当字符串存。我用一个简单的数据模型在我的快应用里用 Python 字典或 Pydantic 模型表示from typing import List, Dict, Any, Optional from enum import Enum class MessageType(str, Enum): USER user ASSISTANT assistant TOOL_CALL tool_call TOOL_RESULT tool_result SYSTEM_EVENT system_event class ContextMessage(BaseModel): type: MessageType content: str metadata: Dict[str, Any] {} created_at: Optional[float] None每条消息都带类型和元数据这样在组装上下文时就可以按类型筛选、排序、聚合而不是用正则去切字符串。4.2 上下文组装函数接下来是核心函数从历史消息 当前输入中组装出这一次调用模型时的完整上下文。def build_context( system_prompt: str, messages: List[ContextMessage], user_input: str, max_history_rounds: int 8, max_summary_chars: int 1500, ) - List[Dict[str, str]]: # 1. 系统指令放在最前面 context [{role: system, content: system_prompt}] # 2. 筛选工具调用与工具结果按时间顺序合并成一个决策链 decision_chain [] for msg in filter(lambda m: m.type in (MessageType.TOOL_CALL, MessageType.TOOL_RESULT), messages): decision_chain.append(f{msg.content}) # 3. 历史摘要放在中间 if len(messages) max_history_rounds * 2: summary summarize_early_history(messages[:-max_history_rounds]) if summary: context.append({role: system, content: f[历史摘要] {summary[:max_summary_chars]}}) # 4. 近期对话原文按时间追加 recent messages[-max_history_rounds * 2:] for msg in recent: if msg.type MessageType.USER: context.append({role: user, content: msg.content}) elif msg.type MessageType.ASSISTANT: context.append({role: assistant, content: msg.content}) # 5. 当前问题放在最后 context.append({role: user, content: user_input}) return context这个函数把前几节讲的预算与顺序原则直接变成了代码。它有几个可调参数max_history_rounds保留最近多少轮原文、max_summary_chars历史摘要的长度上限你可以根据实际任务调。4.3 工具结果回填最容易断链的环节Agent 最常见的上下文事故不是历史太多而是工具结果没有正确回填。模型调用了一个工具拿到了结构化结果如果你直接把这个结果原样拼进消息列表模型很可能看不懂。我做了两层处理。第一每次工具调用产生的 result 是一个带标记的字段我会把它格式化成工具名关键字段的文本这样模型能快速定位是什么工具返回了什么信息。第二我会加一行提示告诉模型这个信息来自外部工具查询结果请基于它回答用户问题避免模型把工具结果当成一段闲聊内容看待。def format_tool_result(tool_name: str, result_body: str, query_params: Dict[str, Any]) - str: return ( f工具执行结果 {tool_name}, 请求参数 {json.dumps(query_params, ensure_asciiFalse)}\n f返回: {result_body} )4.4 摘要节点在 LangGraph 里做上下文压缩LangGraph 里我习惯加一个条件节点专门负责上下文压缩。判断条件很简单当前会话的消息数量超过阈值或者上下文估算的 Token 超过预算时触发摘要。from langgraph.graph import StateGraph class AgentState(TypedDict): messages: List[ContextMessage] context_summary: str task_state: Dict[str, Any] def should_summarize(state: AgentState) - bool: return len(state[messages]) 20 def summarize_node(state: AgentState) - AgentState: # 保留最近10轮原文早期历史生成摘要 early state[messages][:-10] new_summary llm.ask(f将以下对话提炼为要点保留关键决策和状态{early}) state[context_summary] new_summary state[messages] state[messages][-10:] return state这个设计的好处是上下文压缩成了 Agent 执行流程中的一个普通节点什么时候压缩、压缩到什么程度完全由你的业务规则决定而不是靠模型自己感觉。5. 检索与注入不是把知识库全塞给模型而是给它刚刚够用的证据很多团队把 RAG 做得走火入魔用户问一句话他们就把知识库里相关的 20 个文档全部塞给模型。结果 Token 爆炸、回答模糊、甚至被不相关的内容带偏。做 Agent 的上下文工程检索这一步要克制要有非常明确的选择标准。5.1 检索是过滤注入是呈现检索层负责从大海里捞回可能有用的材料注入层负责决定哪些材料真正进入上下文窗口。这两个环节要分开设计。我自己在项目里用的是一个两级筛选法第一级用向量检索发回 Top 15 候选第二级用一个小模型或规则引擎做重排只保留与当前问题强相关的 Top 3~5 个片段再送到 LLM 上下文里。5.2 检索块的大小会影响上下文质量块大小chunk size对上下文质量的影响比很多人想象中大。块太小上下文断裂模型拿到的是零碎的片段没法理解前因后果块太大无关信息混入注意力被稀释。我的经验是面向 Agent 场景的检索块建议控制在 300~500 字之间且块与块之间可以有 10%~15% 的重叠保证语义连续。5.3 混合注入指令 问题 证据 历史要点在最终注入时我用的是四段式结构系统指令告诉模型你是干什么的证据片段与当前问题强相关的检索内容放在对话历史之前历史摘要与关键决策压缩过的早期信息放在证据之后当前问题放在最后直接聚焦这一轮要做什么这个顺序里隐含了一个逻辑模型先知道我是谁再知道我有什么可用的材料然后大概扫一眼之前发生了什么最后全力处理现在要干什么。6. 并发、多会话与 Agent 中台上下文隔离是扛住流量的地基聊到AI Agent 怎么扛并发很多人的第一反应是加机器、做负载均衡。但我在并发压测里发现最隐蔽的坑是上下文串线——多个用户共享了同一个历史消息存储A 用户的订单信息跑到 B 用户的上下文里Agent 回答得牛头不对马嘴。上下文隔离是并发场景下比扩容更优先要做的事。6.1 会话级上下文隔离最基础的做法是按 session_id 隔离上下文。每个用户会话有独立的上下文存储这个存储可能是 Redis 里的一个 key也可能是数据库里的一个 session 表。关键是Agent 运行时读取上下文永远只能按当前请求的 session_id 读取对应数据绝不能用一个全局变量把所有会话的历史堆在一起。在 FastAPI 里我喜欢把 session 上下文做成一个依赖注入的组件每次请求进来先根据 session_id 加载上下文用完再写回。这样天然避免串线问题。6.2 缓存策略别每次请求都重新构造上下文并发场景下的第二个问题是重复计算。同一个用户的连续请求历史消息大部分是一样的没必要每次都去重新 embedding、重新检索、重新摘要。我的做法是给上下文建立两级缓存会话级缓存存当前会话最近的上下文组装结果用 session_id 做 key检索级缓存对相同问题的检索结果做 TTL 缓存短期重复提问直接命中这样做能显著降低请求延迟和 Token 成本Agent 在高并发下表现也稳定得多。6.3 多 Agent 协作与中台视角上下文分层管理如果你的系统已经做成了多 Agent 协作形态或者已经上升到Agent 中台的层面上下文管理还要再拆一层。我的分层设计是这样的全局上下文组织级别知识所有 Agent 共享注入系统指令层工作流上下文一次业务流程内的数据比如订单流程中已收集的信息贯穿多个 Agent 节点会话上下文单次用户对话的具体内容与用户个人状态绑定这三层上下文要严格隔离、按需注入。全局上下文不能随意写入要有权限和审批规则工作流上下文要有生命周期流程结束即回收会话上下文要有遗忘与摘要机制。从平台治理的角度看上下文工程不再是某个 Agent 的事而是中台需要提供的通用能力。7. 用数据衡量上下文质量从感觉变聪明了到可量化的指标上下文工程做得对不对不能靠感觉。我做了很长一段时间的主观判断结果方向经常跑偏。后来我在项目里引入了几个可量化的指标用起来效果很好。这里分享给你。7.1 上下文命中率 / 采纳率这个指标衡量的是Agent 的最终回答里到底有多少信息是来自我们注入的上下文而不是模型自己编的。具体做法在关键节点给 Agent 的回答做溯源标注统计回答中引用或依赖的上下文片段命中情况。如果命中率低说明要么检索没捞到对的材料要么捞到了但注入顺序让它没看到要么材料太泛没有足够的有效信息。7.2 无效 Token 占比这个指标用来衡量预算花得值不值。公式很简单无效Token占比 未被模型决策利用的上下文Token / 总输入Token。实际操作时我给一部分历史消息做个标记用工具埋点如果某轮对话结束后模型在下一轮决策中完全没有依据某个历史片段做事那这一段就是无效 Token。无效 Token 占比高说明上下文里塞的无关内容太多要压缩、要摘除。7.3 端到端任务成功率对比最终极的指标还是端到端成功率。我常用同一批测试任务跑两组比较一组用全历史塞入方式另一组用分层上下文工程方式统计任务成功率、平均轮次、Token 总消耗。用数据说话比任何理论都更有说服力。我建议每一个上下文工程调整都保留一个 A/B 测试记录记录改动内容、改动前后指标变化。这不是形式主义是让你在迭代时有据可依不至于改出一版更差的配置还浑然不知。8. 我踩过的五个坑以及现在会怎么避开上下文工程这门手艺很多细节真的只有踩过坑才会懂。我把自己踩过的五个典型的坑写出来希望能帮你少绕一点弯。8.1 把所有历史都塞进去以为模型记得住最早我在做客服 Agent 的时候直接把整个会话的原始消息全塞回上下文。Token 消耗巨大模型反而变笨了它在海量历史中迷失重点连最新这一问都回答不清。后来我改成保留最近 8 轮原文 提前面历史的摘要模型准确率大幅提升Token 成本也降了 40% 以上。8.2 工具返回结果不格式化模型把 JSON 当成闲聊有一次 Agent 调用订单查询工具返回的 JSON 没有做任何加工就拼接进了消息历史。模型把 JSON 里的字段当成了用户在描述需求回答完全跑偏。现在我坚持格式化工具结果再加上这是工具查询结果的提示语模型就不再搞混了。8.3 在上下文中间塞大段检索文本导致模型失忆有段时间我在历史消息的中间位置直接插入检索回来的文档结果用户上一轮刚说了自己的偏好下一轮 Agent 就完全不记得了。原因是那一大段文档把用户最近的信息挤到了注意力盲区。现在的做法是证据片段放在历史之前、当前问题之前而且严格控制长度选项明确、宁短勿长。8.4 并发压测时用全局 List 存历史结果用户串话这是我印象最深的一次事故。为了快速做一个并发性能演示我用一个全局 List 存储所有会话的消息结果两个用户同时提问Agent 把 A 用户的订单号回答给了 B 用户。这个教训让我意识到会话隔离不是优化项而是必须项。现在我的所有 Agent 应用session 上下文一律按 session_id 隔离存储绝不与全局状态混用。8.5 忽略了上下文压缩对模型心智的副作用做上下文摘要的时候我最初把压缩做得太狠一段 20 轮的历史被压成了两三句话结果摘要丢失了关键决策信息Agent 又犯错了。后来我在摘要节点里保留了关键实体关键决策未完成任务三个固定维度强制要求摘要不要漏掉这些内容问题才解决。上下文工程没有银弹。它是一套需要不断围绕业务目标做度量、调整与平衡的系统工程。我现在每搭一个新 Agent第一步一定不是写 Prompt而是把这一条信息生命周期画出来采集哪些、组织成什么样、按什么顺序注入、什么时候回收、怎么压缩、如何隔离。把这件事想清楚Agent 的稳定性就有了地基。
返回列表