资讯动态

上下文工程实战:修复AI Agent失忆、幻觉与并发串号的系统方法

发布时间:2026/10/8 16:55:33 来源:尧图企业网站定制
如果你正被AI Agent的“智商忽高忽低”折磨这篇文章应该能帮你找到症结。我在实际项目里踩过一个典型的坑模型用的是当时最强的闭源模型工具调用配了十来个向量库也接了但Agent在第六轮对话之后就开始胡说八道甚至把用户第一轮明确说过的偏好忘得干干净净。排查两天后才发现问题根本不在模型而在于我们没做上下文工程——所有历史一股脑塞进窗口检索引擎返回的无关内容直接混入工具返回的原始JSON堆在那里没人管。从那次之后我总结出一套可复用的上下文管理方法今天详细拆给你看。先给没接触过的读者做个定位上下文工程指的是对喂给大模型的全部信息做采集、清洗、排序、压缩、检索和更新的全生命周期管理。它解决的是AI Agent在长对话、多工具、多数据源场景下变傻、跑偏、失忆的问题。适合正在做Agent开发、RAG应用、智能客服、自动化流程的团队和个人参考尤其是那些已经发现“换更强模型也没用”的人——因为你缺的往往不是模型智商而是信息组织能力。1. 为什么上下文工程成了AI Agent的胜负手1.1 模型能力同质化之后拼的是上下文管理大模型本身的智商差距正在快速缩小闭源模型和开源模型的基础能力已经拉不开代差。但同样是智能客服有的Agent能在二十轮对话里始终记住用户偏好有的Agent三轮之后就放飞自我差别恰恰出在上下文管理上。用一个生活类比模型像一个能力极强的分析师你给他一屋子杂乱无章的文件他再聪明也无法高效决策你给他一份整理好的简报他就能迅速给出高质量的判断。上下文工程就是那个整理简报的秘书决定哪些材料上桌、哪些材料按什么顺序上桌、哪些材料必须压成摘要再上桌。很多团队把预算花在换更强的模型上效果却不理想本质上是模型能力的天花板已经很高而上下文组织能力成了那块真正的短板。这也是我为什么把“上下文工程”当作Agent开发第一优先级的原因。1.2 上下文不只是“聊天记录”五类信息来源不少人以为上下文就是history列表把对话历史往窗口里一塞就完事。实际工程里一个Agent的上下文至少包含五类信息来源典型内容数据特征系统提示词角色设定、任务规则、技能清单静态长文本稳定但占位大用户输入当前问题、历史指令、用户偏好动态变化优先级高检索结果知识库命中片段、网页摘要量大且噪声多必须过滤工具返回API响应、数据库查询结果、代码执行输出结构化数据为主需要后处理内部状态任务进度、中间结果、执行计划状态性强容易和对话历史混淆很多人只关注第1和第2项忽略了第3到第5项。但Agent在生产环境里表现不稳多半是栽在检索结果和工具返回这两类信息上。检索库返回的相似片段不一定相关工具返回的JSON字段也不适合原样喂给模型——这些都是上下文工程要处理的脏活。1.3 上下文工程和Prompt Engineering是两回事这两者经常被混为一谈但职责完全不同。Prompt Engineering解决的是“模型应该以什么姿态和规则来回答”它是静态说明书上下文工程解决的是“每一轮到底把哪些数据送到说明书旁边”它是动态数据流。打个比方Prompt是菜谱上下文是当天买到的食材。菜谱写得再好食材不新鲜、主次不分、数量过剩做出来的菜照样难吃。Agent开发到了后期真正值得反复调优的不是系统提示词措辞而是上下文内容本身。两者还有一个关键协同点系统提示词通常会被塞进上下文的最前面并且是每轮都完整出现。如果这一段写得过于冗余就会挤占后面的有效数据空间如果写得过于简略又无法约束Agent行为。所以上下文工程的第一步恰恰是从精简系统提示词开始。2. 核心细节解析上下文工程需要管好四件事2.1 采集与清洗工具返回不是拿来就能用我见过最典型的反面例子是直接把工具返回的原始JSON拼进上下文。比如一个查询订单状态的工具返回下面这段{ code: 0, data: { order_id: A12345, status: shipped, items: [ {sku: X1, name: 无线鼠标, qty: 1}, {sku: Y2, name: 键盘, qty: 1} ], logistics: {tracking_no: SF123456789, company: 顺丰} } }如果直接把这段JSON喂给模型它会为那些无意义的字段名浪费Token而且容易在理解上发生偏差。正确的做法是先把工具返回处理成一段自然语言摘要def format_order_result(raw): if raw.get(code) ! 0: return 查询订单失败原因{}.format(raw.get(msg, 未知错误)) d raw[data] items 、.join(i[name] for i in d[items]) return ( f订单号{d[order_id]}的状态为{d[status]} f包含商品{items}物流公司{d[logistics][company]} f运单号{d[logistics][tracking_no]}。 )这条经验的价值在于模型对自然语言的理解远强于对嵌套JSON的理解。把结构化的复杂数据转成一句话既省Token又减少幻觉。清洗阶段还有两个原则一是脱敏二是截断。任何返回结果都要经过白名单字段提取避免敏感数据进入上下文超长结果只保留与当前任务相关的部分而不是把整个数据库表结构都塞进去。2.2 记忆管理短期、长期、中间态很多Agent的“失忆”问题根源在于把三类记忆混在一起管理了。我用一张表来说明它们在工程里应该怎么区分记忆类型存储介质典型内容读写时机短期记忆上下文窗口内最近几轮对话、当前任务中间结果每轮写入必要时滚动淘汰长期记忆向量库/知识图谱用户偏好、历史订单、产品说明按需检索检索到才写入中间态记忆结构化状态对象当前任务进度、已调用工具、待办步骤独立于聊天记录维护跨轮持久化短期记忆最容易理解就是对话窗口里即时可见的内容通常会配合滑动窗口和摘要压缩。长期记忆是关键差异点它不应该随着每一轮全部灌进窗口而是等用户提到相关信息、或者Agent判断需要某类知识时再通过检索注入进来。中间态记忆是最容易被忽视的。很多Agent在“多步骤任务”中表现不稳定是因为把“任务进度”和“对话历史”混在一个列表里。如果用户中途打断问一个问题Agent会分不清是继续执行原任务还是回答新问题。正确做法是把任务进度单独抽出来做成JSON状态让模型能一眼看到“我正在做什么、做到哪一步、还差什么”。2.3 Token预算与压缩策略上下文窗口从32K涨到128K、甚至1M很多人的第一反应是“既然窗口这么大那就全塞进去”。这个思路在工程上是灾难窗口越大模型注意力的分散程度越高关键信息会被淹没计算成本也会成倍上涨。这里给出一套我常用的预算比例适用于128K左右的上下文窗口按最终生效字符估算信息类型推荐预算占比说明系统提示词10%只保留核心规则技能清单按需动态拼装对话历史30%首尾保留、中间摘要绝不原样全存检索结果工具返回50%优先放高相关性内容低相关直接丢弃输出预留10%给模型生成留足空间压缩策略有三种按照优先级排列丢弃低价值的日志、重复的报错、与当前任务无关的检索片段直接不放进上下文。结构化把工具返回、状态信息转成紧凑的键值对或一句话摘要而不是JSON原文。摘要对不得不保留但很长且信息密度低的历史用一个小模型或专门模型生成摘要替换原始内容。很多人担心摘要会丢失细节所以不敢用。我的经验是摘要的丢失是有方向的只丢低价值细节保留事实性关键点比如用户偏好、已确认的结论、执行约束。这些信息占Token极少但价值极高。2.4 上下文隔离与状态绑定做多用户、多会话的Agent服务时一个最恐怖的问题是“串号”用户A的订单信息跑到了用户B的上下文里。这属于典型的上下文工程问题而不是模型问题。隔离的第一原则是全局绑定session_id和user_id所有上下文数据在写入时必须带上这两个字段。第二原则是上下文快照每次请求执行前从一个独立的存储介质比如Redis读取当前会话的上下文快照而不是在一个全局变量里共享数据。第三原则是命名空间隔离向量检索库、记忆库、缓存库都按用户维度做逻辑分区。我见过一些团队用单例全局变量来存对话历史的这在并发压测下一定会出问题。上下文的生命周期必须跟会话绑定而不是跟进程绑定这一点在架构设计阶段就要想清楚。3. 实操过程一套可复现的上下文管理管线3.1 动态组装系统提示词按需注入技能传统的做法是写一份超长的系统提示词把Agent的所有技能、所有规则、所有注意事项全部塞进去。这样做的问题在于技能数量一旦超过10个模型就会开始混淆工具边界甚至在一个简单任务里把不相干的技能也调用出来。我推荐的做法是“动态组装系统提示词”核心思路是先有骨架再根据当前任务和用户意图把相关的技能描述实时拼进去。下面是简化示例def build_system_prompt(user_intent: str, available_tools: list[str]) - str: # 骨架角色定义 通用规则 skeleton ( 你是自动化助手负责帮助用户完成业务操作。\n 你只调用与当前任务直接相关的工具不要做多余操作。\n 回答必须基于上下文提供的事实不编造信息。\n ) # 按需筛选技能说明 intent_keywords { 查询: [查询订单, 查询物流, 查询库存], 售后: [申请退款, 提交售后, 转人工], 营销: [发放优惠券, 创建活动], } selected_tools [] for intent, related in intent_keywords.items(): if intent_keywords[intent] and any(k in user_intent for k in related): selected_tools.extend(available_tools) # 这里实际应该根据工具注册表生成描述而不是硬编码 tool_desc \n.join(f- {t} for t in set(selected_tools)) if tool_desc: skeleton f\n当前可调用的工具清单\n{tool_desc} return skeleton关键点在于技能描述是按需注入的而不是常驻上下文。这样既能保证窗口空间利用率又能减少模型误用工具的概率。实际项目里这份技能描述通常会写在一个注册表里每个工具包含名称、用途、参数说明、调用示例组装时按相关性做Top-K筛选。3.2 对话历史的滑动窗口与摘要压缩对话历史不是全保留而是采用“首尾完整、中间摘要”的结构。所谓首部指的是开场的系统提示词和最初几轮关键对话它们往往包含用户的核心偏好所谓尾部指的是最近几轮完整对话它们是模型处理当前问题的主要依据中间的部分则用摘要压缩。def compress_history(messages, max_messages20, recent_n6): if len(messages) max_messages: return messages head messages[:recent_n] # 保留开头 tail messages[-recent_n:] # 保留末尾 middle messages[recent_n:-recent_n] # 对中间部分做摘要这里用一个布尔占位 if middle: summary_text summarize_with_model(middle) middle_block [{role: system, content: f此前的对话摘要{summary_text}}] else: middle_block [] return head middle_block tail这个做法有两个好处。第一模型永远能看见最近几轮的用户需求不会因为在长对话里被早先信息淹没第二中间摘要保留的是事实性结论而不是过程性寒暄信息密度更高。踩过的坑不要把摘要放在整个对话的最前面而要放在“中间”位置。模型对顺序极其敏感系统提示词后面紧跟一段长摘要会让它误以为摘要就是当前任务中心导致对用户最新问题的关注度下降。3.3 检索注入从“全塞进去”到“过滤后再喂”很多RAG应用效果差不是模型不行而是把不相关的内容也塞进去了。检索注入有一套固定的处理流程查询改写把当前对话转成适合检索的独立查询比如用户说“上次那个订单怎么还没到”要改写成“查询最近订单的物流状态”。召回从向量库召回候选片段Top-K可以设置大一些比如20条。过滤用相关性分数做阈值裁剪或者用重排序模型精排只保留最相关的3到5条。格式化把每条检索结果转换成统一格式标明来源和关键信息。def inject_retrieval(query: str, retriever, top_k: int 5) - list[str]: rewritten rewrite_query(query) # 查询改写 candidates retriever.retrieve(rewritten, k20) # 一排多召回 reranked rerank(query, candidates) # 精排 filtered [d for d in reranked if d.score 0.35] # 阈值过滤 selected filtered[:top_k] blocks [] for i, doc in enumerate(selected): blocks.append( f[参考文档{i1}]\n f来源: {doc.source}\n f内容: {doc.content[:500]}\n f相关度: {doc.score:.2f} ) return blocks阈值过滤这一步很容易被忽略但它才是防止上下文污染的关键。如果检索结果的相关度只有0.3喂给模型基本是噪音。我实际测过加上过滤之后幻觉率能下降一半左右。3.4 工具返回的后处理让模型看到结论而不是日志工具返回的内容往往很长但真正对决策有用的可能只有几个字段。后处理的目标是把“技术日志”升华成“业务结论”。以查天气为例API返回几百行JSON但Agent需要知道的只是“明天上午有雨温度20到24摄氏度建议带伞”。这一步既可以是规则模板也可以调用一个小模型做抽取和改写。规则模板可维护性强小模型泛化性好根据团队情况取舍。这里的实操心得是工具调用之后一定要把“这一次调用的目的”和“返回结论”一起写进上下文。否则模型很可能只看到结论却不记得自己为什么要调用这个工具在后续环节就断掉了逻辑链。4. 常见问题与排查技巧实录4.1 Token爆了加窗口不是唯一出路现象长对话运行到一半接口直接报context_length_exceeded或者Agent行为明显变慢、变飘。排查第一步看Token消耗曲线确认是哪类信息增长最快。是历史对话是检索结果还是工具返回日志第二步看组装的上下文内容用打印或日志方式把当前轮次的完整上下文输出检查。解决如果历史增长过快触发摘要压缩如果检索结果太多收紧Top-K和相关性阈值如果工具返回太长加强后处理。还有一种情况是系统提示词太长那就做技能按需注入把不必要的规则拆出去。不要无脑把窗口从32K调到128K。窗口越大单轮成本越高而且模型对关键信息的注意力会被稀释。先做压缩再考虑扩窗。4.2 上下文污染幻觉和跑偏的头号来源现象用户问AAgent回答时却引用了B场景的内容或者明明没有足够信息Agent却说得信誓旦旦。排查把组装后的完整上下文打印出来逐段检查每一条信息的来源和相关性。我在项目里给每类上下文加了前缀标记比如[系统规则]、[用户历史]、[检索文档]、[工具返回]这样复盘时一眼就能看出是哪一块内容在干扰模型。解决对低相关度的检索片段宁可丢弃也不要保留对工具返回内容只保留结论性字段对历史对话中的无效寒暄做摘要或直接丢弃。上下文工程的目标不是“信息全”而是“高密度且高相关”。4.3 状态漂移用户纠正了Agent还在坚持旧事实现象用户说“我之前选的是顺丰帮我改成中通”Agent仍然引用旧信息说“您的快递将由顺丰发出”。这是因为旧信息在上下文里的位置和权重仍然很高模型默认它是可信事实。解决在用户纠正发生时显式生成一条“覆盖指令”插入到上下文中最靠前的位置。例如[用户状态更新] 用户刚刚明确表示快递公司从“顺丰”修改为“中通”。 后续所有涉及快递公司的回答以本条信息为准。 之前的“顺丰”信息已失效不要继续引用。这一招非常管用相当于给模型一个“推翻旧事实”的许可。很多人调了半天Prompt不如直接在上下文里写清楚变更指令。同样的道理也适用于用户中途改变需求、改变偏好等场景。4.4 并发场景下的上下文串号架构层要做的四件事并发场景下上下文工程不只是逻辑问题更是性能问题。以下四件事按优先级排列session_id全链路绑定从接收到请求开始所有内部数据结构都必须携带session_id。上下文快照独立存储把上下文构建结果放在Redis或独立内存区而不是进程内全局变量。状态隔离任务进度、中间态数据和对话历史分开存储避免混写。日志链路为每次请求生成trace_id确保能追溯到某一个会话的完整上下文组装过程。我遇到过最典型的并发事故是用了单例字典存历史记录用户量一上来A用户的数据被B用户覆盖导致大量交叉信息。这属于最基础也是最严重的上下文隔离问题。5. 工具选型与架构层面的上下文工程5.1 LangChain、LangGraph 与自研怎么选工具选型要跟团队的技术栈和业务复杂度匹配。这里给出我的实测对比维度LangChainLangGraph自研上手速度快内置大量工具封装中等需要理解状态图慢但完全可控上下文管理有Memory组件但复杂度高后难维护状态节点化管理适合复杂流程完全自定义可观测性一般节点级可观测取决于你的日志设计适合场景原型、中等复杂度链多步骤、有分支、需要状态控制的Agent大规模生产、高并发业务我的倾向是原型验证用LangChain多步骤复杂Agent用LangGraph需要极致性能和深度定制的生产系统自研上下文管理层。尤其在高并发场景下框架自带的Memory组件往往不够用最终还是要自己做隔离和压缩。5.2 并发扛量时上下文工程是内存问题也是性能问题热搜里经常有人问“AI Agent怎么扛并发”这里必须点出一个关键上下文组装本身是有计算成本的。每轮请求都要做查询改写、召回、重排、压缩、格式化这一系列操作叠加起来非常吃内存和CPU。优化思路有四个方向缓存把同一会话的稳定上下文部分系统提示词、检索结果缓存起来只在用户状态变化时增量更新。池化对常用检索结果做预热缓存减少重复召回开销。异步摘要对话历史的摘要压缩移到后台异步执行不阻塞当前请求。只读复用系统提示词和静态知识部分在多轮请求中保持只读通过引用方式复用而不是每次重新拼接。我见过一个团队把所有上下文组装放在请求链路里同步执行结果并发一上来Latency直接飙到10秒以上。把摘要和检索改造成异步后单轮耗时才回到正常水平。5.3 低代码平台里同样存在上下文工程很多人用扣子Coze之类的低代码平台搭Agent以为不写代码就没有上下文问题。实际上平台里的“变量”、“数据库”、“知识库”、“工作流节点传参”本质都是上下文工程。在低代码平台里你要重点管好三件事一是知识库的召回设置不要每个节点都把所有知识塞给模型二是工作流节点的传参尽量只传当前步骤需要的字段三是会话变量和持久化变量的区分不要把临时状态当成长期记忆盲目写入。平台只是帮你省了写代码的时间并没有帮你省掉上下文管理这件事。5.4 Rust、Spring AI 生态里的上下文工程Rust语言写Agent追求的是极致性能和并发安全。在这种技术栈里上下文管理往往要自己做内存池和序列化优化因为Rust本身不提供高级的大模型框架但语言级的内存安全反而让并发串号问题更可控。如果你在Rust生态里做Agent上下文工程的重点会落在高性能序列化和状态存储上。Spring AI在Java生态里提供了ChatMemory之类的接口可以直接对接数据库做状态持久化。但接口只是起点生产环境里还是要处理并发隔离、Token压缩和信息净化。Java生态的优势在于企业和事务性场景如果你做的是客服系统、业务系统内部的AgentSpring AI的基础能力加上自定义上下文管理层是比较舒服的架构。我个人最后想说的是上下文工程不是一次性设计它应该像业务代码一样持续迭代。我在项目里给每个上下文组装环节加了结构化日志每次模型效果波动第一步永远是复盘上下文内容而不是急着换模型或者改Prompt。另一个很实际的经验是上下文工程的验收标准不是“内容全不全”而是“信息密度高不高”。同样解决一个问题别人用10万Token你用2万Token还能保证效果稳定这才是真正的工程能力。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑