多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI Agent上下文工程实践:构建、管理、压缩与并发隔离全解析

AI Agent上下文工程实践:构建、管理、压缩与并发隔离全解析 这两年做 AI Agent 的人越来越多了但我发现一个很普遍的现象很多人把精力都花在选框架、调模型、拼工具上却忽略了真正决定 Agent 能不能稳定干活的核心——上下文工程。上下文工程这个概念听起来玄乎说白了就是你怎么把“该说的话”以最合理的方式交给大模型同时控制成本、别让模型越聊越糊涂。我见过不少项目功能设计得挺像样一跑起来就翻车要么模型忘了早期的关键约束要么上下文越来越长导致 token 成本爆表要么并发一上来各种状态串台最后排查下来根子全在上下文管理上。这篇文章我想从一个实践者的角度把我做过的几个 Agent 项目里跟上下文相关的经验完整梳理一遍。内容包括上下文怎么构建、怎么管理、怎么压缩、怎么在并发场景下保持隔离以及哪些坑我踩过一次就不想再踩第二次。不管你是用 LangChain、LangGraph、Spring AI 还是 Rust 自己硬撸只要在做 Agent这套方法论基本通用。另外我还会把 FastAPI LangChain LangGraph 这套组合的实战代码拆开讲给你一份可以直接抄作业的参考方案。1. 别把上下文工程当成“拼字符串”的事1.1 AI Agent 的上下文到底包含哪些东西很多人以为上下文就是“把以前的聊天记录再发给模型一次”这个理解太粗了。一个正经 Agent 的上下文至少由五个部分组成系统提示词、用户当前输入、多轮历史对话、工具定义与调用结果、以及从外部知识库检索来的证据片段。这五类信息的来源不同、权重不同、生命周期也不同如果一股脑全塞进 prompt问题马上就来了。拿一个常见的“让 Agent 帮小红书自动发消息”的场景举例。用户说“今天帮我发三条笔记”系统提示词里要写清楚账号定位、内容禁区、发布频率工具定义里要声明调用 create_note 接口时有哪些参数历史对话里要保留之前的选题偏好还得从知识库里检索最近的爆文风格做参考。任何一个模块缺失或组织混乱Agent 要么不知道账号风格要么不会调用工具要么把过期信息当成新鲜话题发出去。另一个容易被忽略的部分是“中间的推理轨迹”。Agent 在执行复杂任务时模型可能会经历“想一下该怎么做 → 调用工具 → 看到结果 → 再想下一步”的过程。这个过程产生的中间思考内容scratchpad也是上下文的一部分。很多框架会把这些内容保留在上下文里供后续步骤参考。但如果管理不当它也会成为 token 消耗的大头。1.2 为什么说上下文工程决定 Agent 智商的下限模型本身的能力当然重要但在实际项目里决定 Agent 表现上限的往往是 Prompt 设计决定表现下限的则是上下文工程。模型再强你给它一堆互相矛盾的指令、残缺不全的证据、混乱不堪的历史记录它照样给你一本正经地胡说八道。反过来只要上下文干净、结构清晰、信息充分哪怕是中等规模的模型也能把任务做得很靠谱。从成本角度算一笔账就能直观理解假设你用的是 128K 上下文的模型输入价格是每千 token 几分钱。如果每次请求携带 50K 上下文一次对话的成本就是基础问答的十几倍。一个长会话跑下来几十轮交互成本能让你怀疑人生。更糟的是上下文越长模型的推理延迟越高用户在聊天界面看到“正在输入”转圈的时间就越长体验直线下降。还有一层是“信息密度”的问题。上下文窗口有限每多塞一段废话就少一块放关键信息的地方。有些项目喜欢把最近 20 轮对话不经过任何处理全部塞进去结果就是用户在第 3 轮说过“我不喜欢红色”系统在第 18 轮给出了红色方案本来模型应该能通过历史判断出用户偏好但因为中间夹杂了大量无关闲聊关键信息被“稀释”了模型反而抓不住重点。上下文工程的关键不在于“有没有把内容带上”而在于“有没有把内容放到该放的位置上”。2. 上下文构建从“塞进去”到“放对位置”2.1 系统提示词的结构化设计系统提示词是整个上下文的骨架它决定了 Agent 的“人设”、权限边界和输出规范。很多人写系统提示词就是一段大白话想到哪写到哪这种段落式提示词在简单场景下能用一旦任务复杂起来就容易出问题。我的经验是系统提示词必须结构化用【角色】【能力】【限制】【输出格式】【工作流程】这样的分区把不同类型的信息隔开。比如说做一个电商客服 Agent我会这样拆角色是“电商客服助手”负责商品咨询和售后问题能力边界是只能回答商品参数、库存、物流信息不能承诺赔偿、不能透露内部运营数据输出要求是先给结论再给依据避免诱导性话术工作流程里规定用户问物流必须先调用 track_order 接口拿到真实状态再回答禁止凭空猜测。这些规则单独拎出来看都很简单但把它们结构化地放在系统提示词里模型遵循起来会稳定很多。还有个细节容易被忽略系统提示词的顺序。模型对 prompt 开头和结尾的内容注意力更强中间部分容易被“忽略”。所以最重要的规则要放在开头比如权限边界和安全约束次要的格式要求放中间而“最后检查一遍你的回答是否满足所有要求”这类收尾指令放在最后面往往比放中间更有效。这不算什么玄学是 transformer 注意力机制带来的实际影响实测下来对复杂任务的完成率提升很明显。2.2 工具定义与多轮历史的组织方式工具调用是 Agent 区别于普通聊天的核心能力但工具定义本身也在吃掉上下文。一个工具的描述包含名称、参数说明、用途解释动辄上百 token。当工具数量超过十个时模型选择工具的准确率会明显下降。这时候需要做动态工具筛选先根据用户意图粗筛一遍只把可能用到的三五个工具的定义放进上下文而不是把二十个工具全部塞进去。多轮历史对话的组织方式也有讲究。一个常见的错误是把历史直接拼接成“用户… 助手…”的纯文本格式这样模型很难区分哪句话是用户的原始需求、哪句话是模型自己生成的、哪句话是工具返回的结果。我推荐的做法是对历史消息做角色标注同时把系统事件单独成类user、assistant、tool、system 四种角色清晰分开。这样模型在处理时能准确判断信息源减少因为角色混淆导致的错误推理。另外历史对话没必要全部保留。对很多任务来说最近三到五轮对话已经足够模型理解当前意图。更早的历史可以压缩成摘要提取其中的关键决策和用户偏好而不是把原始对话原封不动地继续带着走。这个操作能省下大量 token同时保留真正有用的信息。2.3 RAG 注入让知识库真正进到上下文里RAG检索增强生成是目前给 Agent 补充私域知识最主流的手段。但 RAG 不是把检索结果随便往 prompt 里一丢就完事。首先检索回来的片段不是越多越好。我见过有人把 top_k 设成 10恨不得把知识库翻个底朝天全塞进去结果模型被大量无关片段干扰反而答不到点上。一般来说针对一个具体问题三到五条高质量片段足够。多了就是噪声。其次检索到的片段必须标注来源和时间。模型看到一份“2025 年 3 月的价格表”和一份“2026 年 1 月的价格表”出现在同一批证据里才有能力判断该以哪个为准。如果你不给来源信息模型只会把它们当成同权重的事实产生矛盾时就会随机挑一个回答准确性大打折扣。还有一个实操中很容易踩的坑检索结果为空时该怎么办。很多实现的处理方式是直接把空结果拼进上下文或者干脆把检索模块整个跳过去。这种时候模型没有依据就会开始编。我自己的做法是在上下文里明确写入“本次检索未返回任何结果请如实告知用户相关信息暂未收录不要猜测”把“没查到”变成一个可控的状态而不是留给模型自由发挥。这个细节对减少幻觉非常有效。3. 上下文管理窗口、压缩与记忆的取舍3.1 三种窗口策略的选择上下文窗口策略直接决定了“历史信息”以什么形式存在、存在多久、以及被遗忘时如何过渡。常见的策略有三种定长窗口、滑动窗口、摘要窗口。定长窗口最简单只保留最近 N 轮对话之前的一律丢弃适合客服问答这类意图短平快的场景但缺点是一旦用户很久之前提过一个重要需求窗口滚掉就再也找不回来了。滑动窗口比定长窗口灵活一些它会把最近几轮原始对话保留下来同时把更早的对话压缩成摘要并放在系统中。这种方式兼顾了近期的细节和远期的要点适合多步骤任务比如让 Agent 帮用户做一个“明天要去杭州出差顺便给客户选礼物”的行程规划前期提到的预算和偏好需要用摘要持续锁定。摘要窗口则更激进它每隔几轮就把之前的对话整体做一次总结只保留总结内容。好处是 token 消耗非常稳定坏处是摘要过程有可能丢失关键细节。我个人的经验是对复杂任务优先选滑动窗口对长会话场景用摘要窗口但摘要节点要做得“有损可控”——每次摘都要明确保留预算、期限、偏好、禁忌四类信息其他内容可以省。同时窗口策略必须是可配置的同一个 Agent 服务不同场景时能灵活切换。3.2 上下文压缩与遗忘机制的设计上下文压缩听起来简单实际上有两个难点。第一压缩什么、保留什么要有明确的规则。不能只丢一半对话就完事要保留“结论”丢掉“推导过程”保留“约束条件”丢掉“客套寒暄”。第二压缩之后的格式要稳定。你让模型做摘要它可能今天给你输出一段话明天给你输出一个列表后天又变成 JSON。不定格式的摘要存入上下文后模型解析起来会产生额外的认知负担。我自己常用的压缩模板是这样的先把最近对话中的“用户明确提出的需求”提取成列表再把“已经达成的结论”单独成段然后把“尚未解决的问题”保留最后把“用户在对话中透露的偏好或禁忌”记录下来。压缩结果以固定结构存储每次压缩都套用同一套模板保证格式一致性。这套模板我用在好几个项目里都有效尤其是长会话场景token 消耗能压缩到原来的三分之一左右。遗忘机制也是一个值得单独设计的东西。模型在长对话里有一个很常见的毛病会把早前用户纠正过的旧信息又拿出来用。比如用户一开始说“预算 5000 以内”后来在对话中改口“预算可以放宽到 8000”如果上下文里两个说法同时存在模型就可能随机选择一个执行。我的做法是在摘要压缩时做“覆盖标记”一旦发现新的信息与旧记录冲突就把旧记录标记为“已废弃”并且在后续构建上下文时强制排除废弃内容。这个看似简单的机制实测对执行准确率的提升非常明显。3.3 记忆系统短期工作记忆与长期存储分离说到上下文工程记忆系统是绕不开的话题。我的理解是把记忆分成两层短期工作记忆就是当前会话的上下文负责支撑这次任务的推理长期记忆则跨会话存在记录用户的偏好、历史结论、常用信息在下一次会话开始时预载入上下文。一个典型场景是“让 Agent 帮用户做期货交易分析”用户上次提到的风险偏好、关注的品种、闲置资金量这些属于长期记忆而这次会话中的报价数据、技术指标分析则属于短期工作记忆。长期记忆写入是一个需要谨慎设计的动作。每次对话结束不能把所有内容都写进长期记忆否则记忆库会迅速膨胀并充满噪音。要做信息筛选哪些是用户主动提出的偏好、哪些是经过确认的事实、哪些只是临时性的上下文。我的做法是设置一个“记忆提炼节点”在会话收尾时由模型对整段对话做一次结构化总结提取需要长期保存的信息条目再写入记忆库。这个过程会把“对话记忆”转成“事实记忆”避免把聊天记录本身当记忆存下来。还有一点很重要长期记忆一定要可管理。用户说“我不喜欢红色的推荐”这是一条记忆但用户后来在另一个场合说“其实红色也可以接受”记忆就应该更新而不是叠加。我在记忆存储里给每条记录加了一个 confidence 字段新的信息与旧记录冲突时不是简单覆盖而是比较两者的明确程度和时效性再把更可靠的那条保留。这个细节做得好Agent 才真正像个“懂你的人”而不是一本只会翻旧账的破账本。4. 落地实操用 FastAPI LangChain LangGraph 搭一套带上下文管理的 Agent4.1 整体架构设计前面讲了不少理论这里上一套真能跑的架构。我最近一个项目就是用 FastAPI LangChain LangGraph 搭的目标是做一个“生产可用的通用 Agent”需要支持多轮对话、工具调用、RAG 检索以及相对复杂的并发压力。整体架构最高层是 FastAPI 提供的 HTTP 接口负责接收请求和返回响应中间层是 LangGraph 定义的状态图负责编排“理解用户 → 调用工具 → 更新状态 → 生成回复 → 管理历史”的流程底层是几个独立服务包括向量数据库、Redis 缓存、以及长期记忆存储。这套架构的核心优势是状态管理清晰。LangGraph 天然支持有向状态图每个节点处理上下文的某一个环节状态对象在节点之间流转。我对比过纯 LangChain 的链式写法LangGraph 对复杂 Agent 的编排能力明显更强尤其是处理循环、条件分支和并发分支时代码结构不会乱掉。如果你还没用过 LangGraph我的建议是先从这种小规模状态图入手别一上来就铺一个大而全的框架。4.2 核心代码实现状态对象与上下文构建器先定义一个状态对象。这个状态贯穿整个 Agent 的每一次交互可以把它理解成整个会话的“中央档案袋”。from typing import TypedDict, Annotated from langgraph.graph import StateGraph, END class AgentState(TypedDict): # 原始对话消息短期工作记忆 messages: list # 系统提示词渲染后的上下文 system_prompt: str # RAG 检索到的证据片段 evidence: list # 可供调用的工具定义集合 tools: list # 会话级别的临时变量 scratchpad: dict # 长期记忆跨会话加载 memory: dict有了状态对象下一步是上下文构建器。它负责在每次请求进入模型之前把“系统提示词 证据片段 多轮历史 工具定义”按照固定顺序拼成最终发给模型的 prompt。这一步是所有上下文工程思路的集中落地。def build_context(state: AgentState) - AgentState: sys_prompt render_system_prompt(state[memory]) recent_msgs apply_window_strategy(state[messages], text_budget3000) evidence rank_evidence(state[evidence], top_k3) tools filter_tools(state[tools], user_intentextract_intent(state[messages][-1])) state[context_prompt] assemble(sys_prompt, evidence, recent_msgs, tools) return state这不是伪代码是我项目里的实际简化版。关键点是每一步都抽成了独立函数render_system_prompt 负责把长期记忆渲染进系统提示词apply_window_strategy 负责压缩对话历史rank_evidence 负责对检索结果排序并限制数量filter_tools 负责动态筛选工具定义。这样做的最大好处是任何一个环节出了问题都可以单独调试不用扒着整条 prompt 逐行找原因。4.3 Token 成本优化与并发场景下的上下文隔离“AI Agent 怎么扛并发”是最近被问得很多的问题这里要区分两个层面服务本身能扛多少并发请求和并发请求之间会不会互相污染。后者跟上下文工程直接相关。最容易翻车的做法是把会话状态存在全局变量或类级别的共享字典里两个用户一起提问时状态互相覆盖A 用户的对话历史跑到 B 用户的上下文里去。我在项目里的做法是每个会话生成一个唯一的 session_id所有状态都按 session_id 隔离存储运行时通过请求头携带的 session_id 取回对应的状态对象。以下是一个基于 FastAPI 的简版示意from fastapi import FastAPI, Header, HTTPException app FastAPI() state_store RedisStateStore(hostlocalhost, port6379) app.post(/chat) async def chat(payload: dict, x_session_id: str Header(...)): state state_store.load(x_session_id) if state is None: state init_state(payload) # 执行 LangGraph 流程 result run_agent(state, payload[input]) state_store.save(x_session_id, state) return {reply: result}除了状态隔离并发场景还有一个优化点是缓存。同一套系统提示词模板和工具定义在每次请求中都会重复发送属于典型的“公共前缀”。不少云厂商的模型服务都对公共前缀缓存做了优化只要保证这部分内容生成的 token 完全相同就能命中缓存显著降低首 token 延迟和成本。因此实践上要特别注意不要把时间戳、随机数这类每次都会变化的字段塞进系统提示词里否则缓存直接失效。成本优化的另一个方向是尽量复用压缩后的历史。如果每次请求都重新做摘要压缩既消耗 token 又有延迟。我的做法是在 Redis 里缓存“对话摘要”的中间结果只要对话历史没有新增多少轮就直接复用上次的摘要。这个优化在长会话场景下效果非常显著实测能把平均每轮请求的 token 消耗降低四成左右。5. 踩坑实录上下文工程常见问题排查5.1 上下文溢出与截断错乱做 Agent 的人几乎都遇到过这个场景对话聊到一半突然报错说 token 超限了。早期的解决办法很粗暴——直接把最早的对话丢一半。结果就是用户问“你刚才不是说那个方案可行吗”模型一脸茫然。原因很简单丢历史的时候把关键结论一起丢掉了。我的排查经验是先看超限发生在哪个环节。如果发生在 RAG 检索之后说明检索结果太多调小 top_k如果发生在工具调用之后说明工具返回的结果太庞大需要在工具内部做字段裁剪只保留必要信息如果单纯是历史对话累积得太多就需要把窗口策略和压缩机制结合起来而不是无脑丢历史。另外日志里一定要记录每次请求的 token 数和截断时的具体位置否则出现问题时根本无从下手。5.2 历史记录带来的“记忆污染”“记忆污染”是我在长会话项目里遇到的最隐蔽的问题。比如用户之前提到“我下周要去上海出差”Agent 把这当成一个背景信息记住了。五轮之后用户问“帮我订一个离公司近的酒店”Agent 还是基于“去上海出差”这个信息来推荐选址。但可能用户早就取消了出差计划只是话题转走了没说清楚。这时候模型依然忠实于历史记忆反而办了错事。解决思路有两个方向。一是对短期记忆加时间衰减太老的对话如果一直没有被再次引用就自动降权二是在上下文构建时加入一个“当前关注点”的判断——在系统提示词里明确要求模型优先响应当前意图历史信息只作为参考背景不作为默认前提。这个指令看似简单但对减少“旧信息绑架新决策”的现象很有帮助。还有一种是检索结果和信息时效性不匹配。做 RAG 时知识库里旧版本文档和新版本文档同时存在检索系统按相似度排序不一定返回最新的。我的做法是在文档入库时强制打版本号检索结果里如果同时出现多个版本的信息就让模型以准确性校验为主优先选择版本号最新且与问题直接匹配的片段。宁可少给证据也别给互相矛盾的证据。5.3 并发与缓存的一致性难题并发场景下还有一个很容易踩的坑历史状态更新和响应生成之间出现竞争条件。比如同一个用户在极短时间内连发两条消息两个请求同时到达各自加载了同一份旧状态处理完再各自写回后写回的会覆盖先写回的导致其中一条消息的上下文丢失。这个问题在纯内存存储时经常出现用 Redis 也有隐患——如果你的写回操作是“读-改-写”模式就绕不开竞争窗口。我给的解决方案是把这个过程做成串行化。最简单的做法是对每个 session_id 加一个分布式锁同一会话的请求排队处理进阶做法是引入消息队列把同一会话的请求路由到同一个处理节点天然实现串行。另外FastAPI 的异步框架在处理这类场景时特别容易写出“看似正确但有竞态”的代码调试起来非常痛苦定位问题的时候一定要先检查状态写回日志确认是不是有覆盖的情况。下面把几个高发问题整理成一张速查表方便你按图索骥症状根因处理建议模型忘记早期约束上下文过长系统提示词被中间内容稀释关键指令放开头历史做摘要压缩回复开始胡言乱语RAG 证据冲突或检索结果为空标注来源、限制 top_k、空结果明确声明Token 成本异常飙升工具定义过全、历史不压缩、缓存失效动态工具筛选、摘要缓存、公共前缀固定两个用户状态互相串状态存在全局变量或共享字典按 session_id 隔离使用 Redis 等外部存储并发请求丢失上下文读-改-写竞态条件加锁或路由到同一处理节点串行执行用户改了需求但 Agent 不理会新旧信息冲突时旧记录未标记废弃摘要时做覆盖标记强制排除废弃内容这张表里的每一条我都实际碰到过不是凭空总结。上下文工程的麻烦之处在于很多问题不是立刻暴露的而是等会话长到一定程度、并发压力上来之后才慢慢浮现。所以建议在项目一开始就把状态隔离、摘要压缩、缓存策略都打好底子后面会省下大量填坑的时间。最后再分享一个我从这几轮项目中沉淀下来的小习惯每次上线新功能之前我都会跑一遍“上下文审计”把历史真实对话里触发的每一条消息记录拿回来逐条看这次请求的最终 prompt 到底长成什么样。这个习惯帮我发现了大量日志里看不出来的问题比如工具定义里一个不起眼的字段描述错误、RAG 片段里带进来一段格式错乱的数据、摘要模板里少了一个占位符导致整段上下文缺失。上下文工程没有太多高深的理论全靠一遍一遍地“看真实 prompt 到底长什么样”看得多了你自然就对这个东西有感觉了。
返回列表