ARTICLE DETAIL

资讯详情

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

AI Agent上下文工程实战:从核心原理到代码落地

AI Agent上下文工程实战:从核心原理到代码落地 这两年AI Agent相关的项目我看了不少自己也从零搭过几个。有一个现象特别常见模型能力明明已经很能打Agent一跑复杂任务就乱套要么答非所问要么反复做同一件事要么干脆把前面的结果忘得一干二净。很多人第一反应是换模型、改提示词但排查到最后真正的瓶颈往往藏在一个更底层的地方——上下文工程。上下文工程这个概念这两年火得很快但它不是某个新框架的新功能也不是提示词工程的换皮改名。它是围绕“模型上下文窗口有限”这个铁律把信息筛选、记忆管理、检索增强、状态流转全部串起来的一套系统工程。最适合看这篇文章的人是正在用LangChain、LangGraph、FastAPI这些工具搭Agent却总感觉“模型不够聪明”的开发者也包括用扣子这类低代码平台搭智能体、想理解底层机制的产品同学。这篇文章我会先把上下文工程的核心问题拆开讲再给出落地架构和可复用的代码片段最后分享我在并发、长会话、检索增强上踩过的坑。前面是心法后面是招式建议按顺序读。1. 先搞清楚上下文工程到底在解决什么问题1.1 一句话定义与核心痛点如果把AI Agent比作一个刚入职的实习生模型本身是脑子工具调用是手脚那么上下文就是实习生面前那张工位桌上能放多少资料哪些资料摆在最上面哪些已经归档到抽屉直接决定他干活的效率和准确度。上下文工程干的事就三件决定桌上放什么、决定什么时刻换什么上来、决定哪些内容压缩归档、哪些直接丢掉。它解决的痛点非常具体模型有上下文窗口上限不是所有信息都能同时放进一次请求模型有注意力衰减塞得越久越靠后的内容越容易被忽略Agent是多步决策循环上一步的推理和工具结果如果组织不好下一步就会“跑偏”。这些问题叠加在一起表现就是Agent看起来“记忆差”“不听话”“爱幻觉”。我自己刚做Agent时有个错误认知只要把对话历史全部拼进Prompt模型就能记住。实际上框架默认的“全量历史拼接”在短会话里还行一旦超过十几轮或者工具返回了大段JSON立刻出现两个后果Token直接爆掉以及模型被大量无关历史噪声干扰开始答非所问。所以说上下文工程不是优化项而是Agent能否真正落地的前置条件。1.2 为什么AI Agent比单次对话更依赖上下文工程单次对话的上下文很简单用户问一句模型答一句这段对话的“记忆”只存在于一个请求里请求结束就烟消云散。真正复杂的上下文管理需求几乎全来自Agent的多轮多步执行过程。举个例子用户让Agent“帮我调研一下竞品A、B、C然后写一份对比报告”。Agent接到任务后要规划调研步骤可能要搜索网页、读取文档、调用数据库每一步都会产生中间结果。到写报告时模型需要同时记住三件事用户最初的目标和要求、前几步调研分别得到了什么结论、当前这一步该做什么。这个信息链条比单次问答长得多而且每引入一个工具结果上下文就膨胀一轮。我习惯把这个过程类比成项目协作单次对话像是问路一句话就结束Agent跑任务像是带一个新人独立做项目你不但要给他目标还要给他工作日志、材料清单、阶段结论还得定期把旧资料归档免得他该看的东西没看到、不该看的东西占了桌面。上下文工程就是为Agent设计这套“工位管理规则”。1.3 从token说起预算就像钱包聊上下文工程绕不开Token很多人对Token的理解停留在“计费单位”但它在上下文工程里更像“预算单位”。先给新手补个基础Token是模型处理文本的最小单元可以简单理解为“字块的碎片”。中文通常1个汉字约等于1到2个Token英文1个词约等于1.3个Token。模型一次能处理的Token总数决定了它的上下文窗口大小。现在主流大模型的窗口看着都不小128K甚至200K的都有。但真正用起来你会发现这钱不禁花。系统提示词占几千Token工具定义占几千再来一轮RAG检索塞进几万Token的参考资料最后留给对话历史和工具执行记录的配额就不多了。更要命的是模型对上下文中不同位置的注意力分布是不均匀的塞得太多关键信息反而被淹没。所以上下文工程本质上是预算管理和优先级排队。你得清楚每一部分上下文值多少Token、在什么环节必须出现、在什么环节可以被压缩甚至省略。我带团队时定了一条规矩任何人往上下文里塞内容之前先回答三个问题——这段内容对当前决策是否必要能不能用更短的表达能不能换成摘要或结构化字段回答不上来就别塞。2. 上下文工程的核心组件拆解2.1 系统提示词Agent的“人设操作手册”系统提示词是上下文工程的底盘也是大多数人最轻视的一块。它不是简单写一句“你是一个AI助手”而是Agent的宪法角色边界、可用工具、调用规则、输出格式、安全约束全都要在这里定清楚。我建议把系统提示词拆成“静态基底”和“动态注入”两部分。静态基底是长期不变的策略比如“你是数据分析助手只能使用以下三个工具调用工具时必须先说明目的”动态注入是每次会话或每轮任务才确定的信息比如当前用户ID、本次任务目标、临时约束条件。这样做的好处是方便维护静态部分可以统一迭代动态部分避免上下文里堆满过期的旧指令。写过一段时间提示词你就会发现系统提示词每多一句无关描述模型真正能被有效利用的空间就少一块。有次我在一个Agent里加了一段非常详细的“公司介绍”本意是让模型更有“企业文化”结果实测下来模型回答问题时总是先扯一段公司愿景严重拖慢任务速度。后来把这部分从系统提示词里移除只在特定品牌问答场景用RAG注入效果立刻正常了。2.2 短期工作记忆对话历史与工具调用轨迹Agent的短期工作记忆主要集中在两个地方对话历史以及工具调用轨迹。对话历史大家都很熟但工具调用轨迹经常被忽略。所谓轨迹就是Agent在每一步中产生的Thought思考、Action调用哪个工具、Observation工具返回什么结果这一整套记录。我第一次做带搜索功能的Agent时只把用户和AI的对话保存了下来没有完整记录工具调用的Observe结果。结果Agent出现了一个很蠢的行为上一轮刚搜索过某个关键词下一轮又搜了一遍因为它根本不知道“自己已经查过这个东西”。它只记得对话里“查过了”这句话但不知道查到了什么内容。这浪费了大量时间和Token。解决办法是把完整轨迹存下来但别原样拼回上下文。实际操作中我会做三层处理最近的几轮轨迹保留原文保证细节完整更早的轨迹做摘要化用一段话概括“当时查了什么、结论是什么”同时抽取关键事实比如找到的价格、时间、URL用结构化字段单独存。这样模型既知道已经发生了什么又不用背着一大堆原始记录往前走。2.3 长期记忆与外部知识RAG在上下文工程里的位置RAG最近被讨论得很多但很多人把它和上下文工程混为一谈。我自己的理解是RAG解决的是“模型不知道的知识”记忆管理解决的是“模型当时知道但后来忘了的信息”。两者是上下文工程里两个独立的模块只是经常配合使用。RAG的本质是把外部知识库里的内容通过检索方式找出最相关的片段注入到当前上下文。所以检索质量直接决定上下文质量。我踩过一个很典型的坑为了追求召回率把Top-K从5调到20结果上下文里塞满了语义相似但信息冗余的片段。模型不仅变慢还因为多个片段之间存在冲突开始在回答里出现“左右横跳”式的矛盾结论。后来我总结了一套稳妥做法向量检索只做第一轮粗筛召回范围可以大一点但注入前必须做重排重排后保留3到5个高相关片段每个片段带上来源标记和检索分数如果有多个片段冲突要在注入时明确告诉模型“以下资料存在观点差异请分别引用”。这套流程跑下来回答质量和Token利用率都提升了一个量级。2.4 上下文压缩与裁剪有限窗口里的取舍窗口是有限的信息是无限的。上下文工程走到最后一定会面对一个问题旧内容怎么处理主流手段有三种裁剪、摘要、关键信息抽取。裁剪是最暴力的直接把最旧的对话丢掉。优点是省事缺点是一旦丢掉的内容里有关键指令或重要前提模型就会“失忆”。摘要相对温和用一次LLM调用把长段历史浓缩成几句概括但摘要会丢失细节比如某个具体的数字、某个工具返回的精确值这些细节一旦丢失就再也找不回来。关键信息抽取则最精细只保留实体、结论、任务状态这类结构化信息但实现成本也最高。我的经验是不要把三种手段孤立使用而是分层组合。对话超过一定轮数早期历史做摘要摘要里再抽出一个极简的“任务状态段”始终留在上下文工具返回的大段JSON在进入上下文之前先经过一道Schema瘦身只保留后续节点需要用的字段。这样做的效果是即使是一个非常长的任务模型始终能看到“目标、当前进度、上次结论”三块核心信息其他都可以按需拉取和重建。3. 在真实Agent架构里怎么落地3.1 ReAct范式下的上下文组织ReAct是目前Agent最主流的范式之一核心循环就是Reason推理为什么做这一步、Act调用工具、Observe观察结果然后再Reason形成循环。像LangChain早期的Agent、很多自建的Agent框架底层都是这个模式。在这种范式下上下文的组织顺序非常关键。我常用的模板结构是系统提示词 - 对话历史/摘要 - 当前任务描述 - RAG检索结果 - 工具调用轨迹 - 要求模型输出下一步行动顺序为什么重要因为模型会倾向于把注意力放在靠后且没处理完的信息上。把要它“现在做什么”的指令放在RAG片段之后、轨迹之前它能更清楚地区分“资料”和“行动”。同时工具返回的Observation要尽量“短而准”能抽字段就抽字段能截断就截断。有一次我们在工具返回里带了一个完整的API原始响应里面上千行无用日志模型在后续步骤里还在引用那些日志里的随机数字导致回答完全没法看。从那以后我们规定所有工具返回必须经过一个“结果清洗节点”。3.2 用LangGraph搭一个带上下文管理的工作流LangGraph是我目前比较喜欢用来搭Agent的框架它让开发者把执行流程建模成一张图节点和节点之间有状态传递。这个“State”的设计天然适合做上下文工程。我在实际项目里把State拆成几个独立字段messages保存原始对话历史summary保存压缩后的摘要facts保存结构化关键事实tool_results保存最近一轮的工具结果。然后把“上下文管理”本身也当做一个节点每轮结束时检查总Token数一旦超过阈值触发一次压缩节点把部分messages转成summary把关键实体抽到facts。这样做的最大好处是每个节点只读它需要的子集。规划节点只读summary和facts不背原始历史工具节点只读当前任务不关注用户几十轮前的闲聊。上下文不再是一锅粥而是像数据库一样按模块存取。代码结构大概长这样class AgentState(TypedDict): messages: list summary: str facts: dict tool_results: list def plan_node(state: AgentState): context state[summary] format_facts(state[facts]) plan llm.invoke(context state[messages][-1][content]) return {messages: [{role: assistant, content: plan}]} def compress_node(state: AgentState): if count_tokens(state) LIMIT: state[summary] summarize(state[summary], state[messages][:-2]) state[facts] extract_facts(state[messages]) state[messages] state[messages][-2:] return state这段代码只是示意落地时还需要处理并发、持久化、失败重试。但你可以看到上下文管理在图结构里是清晰可控的它不是一个神秘的黑盒而是流程中的一个显式步骤。3.3 多Agent协作时上下文怎么隔离与共享任务复杂到一定程度单个Agent会显得笨重这时候我们通常会拆成多Agent协作比如一个主管Agent加几个执行Agent。但多Agent最容易翻车的点就是上下文串味。我第一版多Agent系统就很幼稚所有子Agent共享同一个消息列表结果A子Agent看到的资料混进了B子Agent的结论整个决策逻辑彻底乱套。后来我彻底改了设计每个子Agent维护自己独立的上下文彼此之间不直接读消息而是通过一个“黑板”模式的共享存储区交换必要信息。主管Agent向黑板写入任务描述和约束执行Agent从黑板取走自己的任务执行完只把“结果摘要状态标记”写回黑板而不是把原始对话全量广播。这个“黑板”可以用Redis、数据库或者内存消息队列实现关键是信息以结构化形式传输谁写的、写给谁、内容是什么、截止时间、状态。低代码平台比如扣子这类产品本质上也是把这个流程封装好了让你不用关心底层是怎么隔离的但理解了这个原理你才能判断平台的设计在什么场景下会失效以及自建系统时需要防哪些坑。3.4 高并发场景下的上下文安全与性能并发一上来上下文工程的复杂度会再上一个台阶因为很多问题在单用户场景下根本不会暴露。我见过最典型的翻车现场是用全局变量存Session上下文用户A的会话还没结束用户B的新请求进来直接覆盖了A的状态结果A的下一次请求带着B的上下文一起去问模型内容完全错乱。所以第一条铁律是上下文必须按Session隔离存储要放到Redis或数据库里绝不能放在应用进程的全局变量中。第二条铁律是每个请求要独立拼装完整上下文再发给模型不要在多请求之间共享可变的中间状态。有人为了省Token把“编辑中的上下文”存在内存里跨请求复用这是很多诡异Bug的源头。第三长会话要做生命周期管理。我处理过一台服务器被内存拖垮的事故排查后发现是会话上下文只增不减长时间不清理几千个Session全堆在内存里。后来给会话加了三个限制空闲超过30分钟自动回收、单会话最大轮数、上下文最大Token容量。另外如果对性能和内存安全要求特别高可以单独用Rust写一个上下文服务把状态管理做成独立组件如果你在Java生态里做AgentSpring AI提供的ChatMemory之类抽象也可以帮你省掉不少重复设计。框架选型看团队但隔离、持久化、生命周期管理这几条原则是不变的。4. 上下文工程的常见坑与排查实录4.1 上下文污染Agent为什么“突然失忆”上下文污染是我在Agent项目里遇到次数最多的问题。症状很典型Agent突然表现得“记错事”把用户很久以前提过的一句话当成当前需求或者回答问题时混进与当前任务完全无关的背景信息。有一回我们给客服Agent加了一个“用户近期浏览记录”的字段本意是做个性化推荐。结果上线后用户反馈非常奇怪Agent把用户昨天浏览过的商品当成今天想买的东西反复推荐同类产品。排查后发现检索逻辑把近7天的浏览记录里所有条目都塞进了上下文而系统提示词里又没有明确“浏览记录只能作为参考不能替代用户当前表述”。这就是典型的上下文污染模型分不清哪些是背景材料、哪些是当前指令。解决思路分两步。第一给进入上下文的每个信息块打标签和限定语例如“【历史浏览记录仅供参考】【用户当前指令】”“【工具输出结果】”。第二注入前做相关性过滤宁可少注入不可乱注入。我的经验是模型对“当前任务指令”和“背景资料”的区分能力远没有我们想象的强必须在文本层面明确提出边界。4.2 窗口溢出与截断伤害比想象的大上下文窗口溢出后的处理方式是另一个高频坑。很多框架的默认策略是“超出窗口就截断”但截断也有讲究。如果只是简单地从开头截断你很可能把最不该截的系统提示词或者当前任务指令给截掉。我们早期就干过这种蠢事Token超了以后直接在字符串层面裁掉前面的N个字符结果系统提示词被切走了一大块。Agent瞬间忘记自己有哪些工具对所有工具调用请求都回答“我没有这个功能”用户直接原地爆炸。更隐蔽的问题是裁剪之后模型不会告诉你它丢了信息它会照常回答只是回答里缺失了关键上下文。正确做法是给上下文做分层的不可裁剪保护。第一优先级是系统提示词和当前用户指令这部分永远不裁剪哪怕是损失后面的内容也不能动它第二优先级是RAG检索片段这部分可以动态调整数量第三优先级是早期对话历史和工具输出可以接受被摘要替换。每一层设置配额比如系统提示词占20%、当前指令占10%、RAG占30%、历史与轨迹占40%触发压缩时按配额执行。4.3 幻觉从哪来检索质量才是源头聊幻觉时大家经常把锅甩给模型但上下文工程层面的问题其实更多。我碰到过的幻觉场景有几种检索到的片段跟问题压根不相关模型却把它当依据两段检索内容互相矛盾模型自己编了一个“调和”后的结论还有更隐蔽的模型把推理过程中自己猜的中间值当成了检索到的外部知识写进最终答案。这些问题的共同根源是上下文里混入了低质量信息。所以我现在对RAG注入有比较严格的要求片段必须带来源标记例如“来源某某文档第X节”模型在回答中引用外部资料时必须注明出处如果资料中存在冲突要求模型明确说“这些资料之间存在矛盾存在两种说法”而不是强行给出一个不存在的共识。同时召回数量要克制。Top-K从5调到20不代表答案好4倍很多时候只是噪声多4倍。配合Rerank重排和相似度阈值过滤比单纯提高召回数量有用得多。4.4 排查工具与调优清单最后给一套可以直接抄的排查方法。无论出什么问题我的第一反应永远是把所有进入上下文的片段原样打印出来自己扮演一遍模型看看能不能回答正确。这一步能筛掉80%的问题。然后是定量检查用Token计数工具统计每一部分占比看有没有某部分异常膨胀核对每一次工具调用的输入和输出确认Observe信息没有被静默截断监控Agent的决策重试率如果Agent反复执行同一个动作大概率是上下文中缺少对应的“已尝试过”标记。我整理了一份简易调优清单项目里一直沿用检查项常见症状处理方案系统提示词是否被截断Agent忘记工具、行为散漫调整优先级设置不可裁剪区旧对话是否压制当前指令答非所问、重复历史结论摘要化旧历史给当前指令加权RAG片段是否冗余冲突回答矛盾、幻觉降Top-K、加Rerank、带来源标记工具返回是否过大Token超限、模型引用垃圾信息增加结果清洗节点只保留关键字段上下文是否串Session用户看到他人数据、状态错乱改用Redis/数据库隔离存储会话是否无限膨胀内存暴涨、响应变慢设置空闲回收、最大轮数与容量上限5. 用FastAPILangChain快速做一版上下文管理实操5.1 最小闭环设计前面聊了这么多理论还是得落到代码上。我以一个最小可跑的Agent为例演示怎么把上下文管理设计进实际项目。技术栈用最常见的FastAPI加LangChain这套组合的好处是轻量、上手快自己改动空间大。整个体系分三层API层接收用户请求ContextManager负责读取和更新该Session的上下文Agent执行层负责把上下文组装成Prompt并调用模型。核心设计是每个用户请求只传一个SessionId后端从存储里取该请求的完整上下文而不是依赖进程内的全局状态。这个设计在单机和分布式场景下都能平滑迁移因为上下文本身就是独立的。5.2 关键代码与配置先定义一个简单的上下文管理器负责从内存或Redis里按Session读取和保存状态。生产环境推荐把这块替换成Redis或PostgreSQL但本地演示用内存字典就够了。关键是接口要抽象好后面换存储不伤筋动骨。from typing import Dict, List import json class ContextManager: def __init__(self): self.sessions: Dict[str, dict] {} def get_context(self, session_id: str) - dict: if session_id not in self.sessions: self.sessions[session_id] { summary: , facts: {}, messages: [], tool_results: [] } return self.sessions[session_id] def save_context(self, session_id: str, context: dict): self.sessions[session_id] context def append_message(self, session_id: str, role: str, content: str): ctx self.get_context(session_id) ctx[messages].append({role: role, content: content}) self.save_context(session_id, ctx)接下来是组装上下文的函数。这里就体现出了刚才讲的分层思想先把历史摘要和关键事实放进去再放最近的对话最后加上RAG检索结果。Tool结果默认只保留最后两轮超出部分由摘要代替。def build_prompt(session_id: str, user_input: str, rag_results: List[str]): ctx context_manager.get_context(session_id) messages [{ role: system, content: SYSTEM_PROMPT }] if ctx[summary]: messages.append({ role: system, content: f[历史摘要] {ctx[summary]} }) if ctx[facts]: messages.append({ role: system, content: f[关键事实] {json.dumps(ctx[facts], ensure_asciiFalse)} }) for msg in ctx[messages][-4:]: messages.append(msg) if rag_results: messages.append({ role: system, content: [参考资料] \n.join(rag_results) }) messages.append({role: user, content: user_input}) return messagesFastAPI的接口层很简单。关键点是每个请求都带着SessionId进入处理完必须把新的消息和工具结果写回。这里不讨论业务细节只展示上下文更新的时间点。from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): session_id: str message: str app.post(/chat) def chat(req: ChatRequest): ctx context_manager.get_context(req.session_id) rag_results retrieve_relevant(req.message) # 你的RAG检索逻辑 prompt build_prompt(req.session_id, req.message, rag_results) response llm.invoke(prompt) context_manager.append_message(req.session_id, user, req.message) context_manager.append_message(req.session_id, assistant, response.content) # 每轮结束检查是否需要压缩 if count_tokens(context_manager.get_context(req.session_id)) LIMIT: compress(session_id) return {reply: response.content}这个版本的代码还有很多简化比如没有加错重试、没有工具调用、没有并发锁。但它已经把上下文工程最核心的几条原则固化了按Session隔离、显式拼接摘要和事实、限制注入量、两步压缩。把它当成一个起点去扩展比直接在大而全的框架里改要清晰得多。5.3 经验心得与后续扩展跑通最小闭环之后一定要记得做三件容易被忽略的事。第一加日志审计。把每次请求进入模型的Prompt完整保存下来我当时是直接落盘到JSONLines文件。出了任何线上问题第一件事就是翻日志看看模型到底“看”到了什么很多“灵异现象”其实都是上下文组装时出了问题。第二把压缩策略调成可配置。不同场景下对细节的容忍度完全不同。做代码审查Agent时关键代码片段一定不能被摘要替换做闲聊类Agent时历史全部压缩成摘要也没关系。所以我建议把“多少轮开始压缩”“摘要保留多少字”“哪些字段不能丢”全部做成配置项方便在不同任务间切换。第三也是最重要的事把上下文日志变成你的评估集。我做过一次很有意思的实验把线上真实进入模型的上下文收集起来人工标注“这里如果缺少了X信息会怎样”然后形成了一组回归用例。每改一次上下文策略就跑一遍这组用例防止优化A场景时把B场景打回原形。这个习惯帮我避免了至少三次“修好一个坑又踩了另一个坑”的尴尬。我现在的习惯是每接手一个Agent项目先不急着调模型而是花时间把会话里所有信息流转路径画出来用户输入会触发哪些检索工具结果会写到哪摘要什么时候被刷新并发下每个Session的状态放在哪。把这趟路径理清楚了模型能力的提升只是换一个更强的模型而已但上下文结构一乱换再强的模型也没救。这套思路也同样适用于后续继续扩展的方向比如从单Agent到多Agent的协作、从短期会话到长期用户画像底层都是同一套上下文工程的骨架在支撑。
返回列表