2026/10/9 0:25:45

从Prompt到Context:AI Agent上下文工程实战指南

从Prompt到Context:AI Agent上下文工程实战指南 最开始做 AI Agent 的时候我以为把 Prompt 写漂亮就完事了。结果第一个真实项目上线不到两天就被打脸了用户在对话里提到一个半小时前交代的关键信息Agent 直接“失忆”开始自己编造需求。我一开始怀疑是模型不行后来排查了半天才发现——问题压根不在模型在上下文。那段时间我翻遍了各种资料试过各种方案最后算是把“上下文工程”Context Engineering这条路摸通了。这篇东西不是我随便整理的技术科普是我在真实项目里踩坑踩出来的实操总结适合那些已经开始做 Agent、或者正准备把 Agent 推向生产环境的人。1. 上下文工程到底在解决什么问题先说结论上下文工程不是 Prompt Engineering 的升级版它俩虽然关系近但维度完全不同。Prompt 工程解决的是“怎么把指令写清楚”上下文工程解决的是“怎么在有限的空间里动态地、系统性地管理模型能看到的所有信息”。什么叫“所有信息”你在和 Agent 对话的时候喂给模型的不仅仅是用户的这句话还包括系统提示词、工具定义、历史对话、检索回来的文档、模型上一次的输出、当前任务状态。这些内容叠加在一起共同构成了模型每一次推理时拿到的完整上下文。我见过太多人栽在同一个坑里单轮测试的时候效果惊艳一进入真实的多轮使用场景就崩。为什么因为单轮场景下上下文是干净的、静态的真实场景下上下文是脏的、动态的、不停膨胀的。你根本控制不了用户会聊多久、会塞多少信息、工具会返回多长的结果。上下文工程要解决的核心问题我总结下来就是三个第一个是容量问题。所有模型都有上下文窗口上限Claude 3.5 Sonnet 是 20 万 tokenGPT-4o 是 12.8 万 token看起来很大对吧但一个正常的多轮对话光历史记录加工具返回结果几千个 token 是分分钟的事。如果是那种长任务型的 Agent比如让它分析一批文档、做多步骤的爬虫任务上下文膨胀的速度远超你想象。没有合理的淘汰和压缩策略溢出是必然的。第二个是信息精度问题。这不是“塞得越多效果越好”的事。有研究表明模型对长上下文中间位置的信息感知能力明显偏弱也就是所谓的“Lost in the Middle”现象。信息放错了位置、顺序不对、或者无关信息太多都会导致模型抓不住重点表现为答非所问、遗漏关键约束。第三个是成本问题。每个 token 都是要花钱的。一个不加管理的 Agent在高并发场景下API 费用可能比你想象的高出一个数量级。把同样的信息反复喂进去、把不需要的旧内容一直留在窗口里……这些都是烧钱的无底洞。理解这三件事你就明白了上下文工程本质上是一种资源调度。上下文窗口是一个有限资源池你要做的是设计一套策略决定什么信息进来、什么信息出去、什么信息压缩、什么信息保留原样。2. 从 Prompt 到 Context一次思维升级我刚开始写 Agent 的时候习惯把所有规则全部写进 System Prompt然后希望它一劳永逸地遵守。这种方法在简单场景下没问题但一旦你的 Agent 有工具调用、有多轮交互、有外部检索你就会发现System Prompt 里的内容和对话历史里的信息在模型看来是同一种东西没有优先级上的天然区别。也就是说你以为你在 System Prompt 里写了“必须优先遵守用户最早的需求”但模型并不会特殊照顾这句话它只会基于整个上下文做统计性的推理。当上下文里后续内容越来越多早期指令的权重就会被稀释表现为 Agent “越来越不听话”。这个认知升级很关键不要把上下文当成一个可以无限追加的文档要把它当成一个有预算的运行时状态。我在实际项目里是怎么理解这个问题的我把它拆成四层的结构系统层不动的内容角色设定、全局规则、输出格式。这一层基本恒定适合做缓存节省重复计费。任务层当前任务的目标、约束、进度状态。这一层是动态的但优先级很高不能被历史对话挤出窗口。数据层工具返回的结果、检索到的资料、用户提供的文件内容。这一层是大头也是膨胀最快、最需要管控的部分。历史层过去的对话记录和中间推理过程。这一层最容易被压缩、替换、甚至丢弃。每一层对当前推理的贡献不一样管理策略自然也不一样。系统层要稳定任务层要常驻数据层要精准历史层要精简。上下文工程的核心工作就是把这四层的信息按合理比例组合起来塞进模型有限的窗口里。很多人找我聊的时候会问上下文工程适合什么规模的项目我的回答是只要你的 Agent 不是那种“一问一答就结束”的玩具就都需要考虑这个问题。哪怕你只是在做一个内部工具上下文管理做得不好用户的体感就是“这个智能体很蠢”。3. 上下文窗口管理动手实现前的核心策略窗口管理是整个上下文工程的地基你想清楚了这两件事——信息怎么进、信息怎么出——后面的记忆设计、检索增强才有意义。3.1 先说 Token 预算怎么分我做 Agent 时有个习惯给每个项目的上下文分配固定预算。比如模型窗口是 12.8 万 token我会分给系统层 5%、任务层 10%、数据层 40%、历史层 40%剩下的 5% 留给模型输出的余量。这里说的百分比是整体策略上的初始值实际运行时还要结合消息长度动态调整。这个预算思维特别重要。因为如果你不主动分配上下文就会变成“历史层无限膨胀其他层被挤出去”的失衡状态。最常见的失控场景就是用户聊了 30 轮之后历史记录占了 90% 的空间任务目标和工具结果都被挤没了Agent 开始胡言乱语。另外注意一点不同模型家族的 tokenizer 不一样同一个汉字、同一段英文在不同模型里的 token 消耗有差异。我之前跨模型迁移的时候踩过坑——同一段代码在 A 模型下计出来是 8000 token换到 B 模型下直接变成 11000。所以做上下文管理的时候token 统计要跟着模型走用实时计算的方案不要拍脑袋估一个固定数字。3.2 淘汰策略不是简单扔东西上下文窗口满了怎么办很多人第一个想到的是“删最旧的”。这就是最简单的滑动窗口策略实现起来也就几十行代码但在真实场景里效果往往一般。为什么因为对话的重要性和时间顺序不是线性反比关系。用户可能在第 2 轮交代了一个关键偏好在第 20 轮的时候还在依赖这个偏好——如果你在第 15 轮就把第 2 轮的内容删除那 Agent 就会“忘记”用户的核心要求。这种遗忘是最容易被用户发现、也最掉链子的。所以实际项目中我更推荐用重要度评分 位置约束的组合策略对每条历史消息做重要性打分维度包括是否包含工具调用结果、是否与当前任务关键词相关、是否包含用户明确指令、消息长度是否异常。评分低的旧消息优先丢弃。但无论评分多少消息队列的头部始终保留最近几轮对话因为模型的近期记忆对连贯性至关重要。我见过有的团队做得更细致加了一个“濒危保护”机制凡是包含“记住”“务必”“很重要”这类关键词的消息会被标记为高优先级淘汰的时候跳过它。这个做法很聪明性价比极高等于让用户自己决定哪些信息不能丢。3.3 关键细节提示词位置与格式上下文工程光管了“信息放不放得下”还不够还得管“信息放在哪里”。刚才提到过 Lost in the Middle 现象——模型对长上下文的开头和结尾部分信息感知最强中间最弱。这意味着系统的核心指令、当前任务目标尽量放在上下文开头和结尾附近。大段检索文本、冗长的工具结果放中间位置影响相对小但不能太多太多会把所有东西都“变成中间”。如果某个信息特别重要且很长可以考虑拆分后分别放在开头和结尾各出现一次用“重复强调”来抵抗位置衰减。格式上我也有过教训。早期我把上下文拼成一大段纯文本用换行分隔不同模块。后来发现用 XML 式的标签或者 Markdown 结构分隔各模块模型对信息边界的感知明显更清晰。虽然不是所有模型都吃这一套但实测下来这种结构化方式在多数主流模型上都有正向效果。4. 记忆机制让 Agent 不再“失忆”窗口管理解决的是“当前这次推理放什么”但用户对 Agent 的期待是“你能记住我”这个“记住”就要靠记忆机制来支撑了。窗口之外必须有记忆存储否则每次会话从零开始用户说一句“我上次跟你说过那个需求”Agent 就彻底蒙了。4.1 理解记忆的三种类型我习惯仿照人类的记忆体系把 Agent 的记忆分三类工作记忆当前任务闭环内需要的信息就放在上下文窗口里通过窗口管理来控制。这类记忆不需要长期持久化。情景记忆跨会话的关键事实、用户偏好、过去的决策摘要。这类信息要持久化但不需要全部原文保存压缩成摘要即可。语义记忆领域知识、事实性资料、产品文档。这类信息通常存到向量数据库通过检索按需注入上下文。这三类记忆的读写频率、数据量、管理方式差别很大如果你全都塞到同一个地方八成会乱套。我之前犯过的错误就是把所有会话历史无差别地存进数据库结果检索的时候噪声巨大召回质量差到没法用。4.2 压缩是门手艺活历史层的上下文压缩是整个上下文工程里最考验功力的环节之一。简单来说就是把一段长对话浓缩成几句摘要在保留关键信息的同时大幅减少 token 占用。但是压缩有一个致命陷阱压缩必然会丢失信息而且模型在理解“什么信息重要”这件事上并不能做到完全可靠。我见过最典型的翻车案例Agent 把用户提供的一个具体数字搞丢了结果后续步骤基于错误的参数继续跑最后产出一个完全错误的结果。而且这种错误非常隐蔽因为 Agent 看起来一切正常只是“记错了一个数”。所以我总结出几条压缩的硬性规则希望大家少走弯路压缩必须保证事实性信息不丢——数字、日期、姓名、金额、产品型号这些硬信息宁可多留几个 token也不能省。压缩时优先保留具体事实适当丢弃情绪、语气、客套类内容。压缩要多级进行不要一把梭。我的做法是对话超过 N 轮时把最老的 1/3 压缩成摘要再过 N 轮把摘要和新的对话再次合并压缩。这样梯度式的操作比一次性压全部要稳定得多。压缩结果要回写和校验。每次生成摘要之后可以对摘要做一次事实一致性检查——把摘要里的关键实体和原文比对一下发现缺失或错误就补上。大模型做不了百分之百的校验但能降低相当比例的出错率。4.3 记忆注入的时机要克制很多人做记忆系统的时候容易犯一个毛病每次请求都把所有记忆塞进上下文生怕模型不知道。这种做法的后果有两个一是 token 消耗暴涨二是模型被大量低相关性信息干扰反而影响推理质量。我是怎么做的先有一个“记忆调度层”每次用户发起新请求时调度层根据当前任务关键词、用户身份、对话主题从持久化存储里捞最相关的记忆片段捞回来的片段还要做一次相关性过滤最后只把过滤后的结果注入上下文。说白了记忆和检索的目的不是“让模型知道更多”而是“让模型刚好知道它此刻需要知道的”。克制是记忆系统设计的核心美学。5. 检索增强与数据注入信息要准不要多上下文工程的另一大块是把 Agent 需要的外部知识送进上下文。现在大家普遍用 RAG检索增强生成来做这件事但很多 RAG 方案做得粗糙问题不在检索本身而在“检索结果怎么放进上下文”。5.1 检索不是拼“相关性”我早期做 RAG 的时候只靠向量相似度搜索自认为效果挺好。直到一个真实场景教育了我用户问“我们公司今年第一季度财报里研发投入增长了多少”向量检索返回了三段内容其中一段准确地提到了 12.7% 这个数字另外两段只是泛泛讲研发战略。模型拿到三段内容后被那两段泛泛的内容干扰最后给出的回答里愣是把数字弄错了。后来我改成**混合检索 重排Rerank**的策略向量检索负责“语义召回”BM25 关键词检索负责“精确匹配”两者结果合并后再经过一个 Rerank 模型做精细排序。这样做的结果就是排在最前面的段落大概率是真正包含答案的那一段模型被干扰的概率大幅下降。如果你用的检索方案过于简陋我建议至少加上一个关键词召回通道——向量模型对精确数字、专有名词的处理一直不是很稳定BM25 正好能补这个短板。5.2 注入格式与位置检索回来的段落不能直接平铺在上下文里需要对它做三件事第一切块格式统一。每段前面标明来源、时间、可信度等级比如“来自内部文档更新于上月”给模型一个判断信息可靠性的依据。第二做去重。向量召回的结果经常会高度重叠多个段落讲同一件事白白浪费 token。先做相似度去重再注入能省下不少空间。第三位置管理。如果检索内容很短几百 token直接放在任务层后面模型感知最强如果检索内容很长几千 token放到中间位置并用明确的分隔标签包住防止它污染指令区域。5.3 上下文压缩缓存与重复计费很多人忽略的一个成本痛点每次请求系统 Prompt 和工具定义都会被重复计费。这些内容基本不变但模型每次都要“重新读一遍”。现在主流模型服务商基本都提供了 Prompt Caching 能力开启之后相同的 System Prompt 前缀在短时间内不会重复计算 token 费用。别小看这个我在一个高频调用的项目里开了缓存之后光这一项就省了接近 30% 的 API 费用。前提是你要把不变的内容全部放到 Prompt 的前缀部分任何动态替换的内容都在前缀之后这样缓存命中率最高。6. 落地实操我常用的上下文管理器实现前面讲了半天策略接下来我分享一个我自己在项目里反复用的一组核心代码思路。不吹牛这几十行逻辑是我所有 Agent 项目的基石无论是 FastAPI 后端还是 LangGraph 工作流我都复用过这套结构。先看这个 ContextManager 的骨架我用 Python 写了一个落地版本import tiktoken from dataclasses import dataclass, field dataclass class ContextManager: model: str gpt-4o max_tokens: int 128000 system_prompt: str turns: list field(default_factorylist) # 原始对话轮次 memos: list field(default_factorylist) # 压缩摘要 / 重要事实 budget: dict field(default_factorylambda: { system: 0.05, task: 0.10, history: 0.40, retrieve: 0.40, output: 0.05, }) def _count(self, text: str) - int: enc tiktoken.encoding_for_model(self.model) return len(enc.encode(text)) def add_turn(self, user_msg: str, assistant_msg: str) - None: self.turns.append({user: user_msg, assistant: assistant_msg}) for memo_key, score in self._score_turn(user_msg, assistant_msg).items(): if score and memo_key not in self.memos: self.memos.append(memo_key) self._enforce_budget() def _score_turn(self, user_msg: str, assistant_msg: str) - dict: # 简化版检测包含关键指示的消息提取为记忆 result {} for phrase in [记住, 务必, 强调, 不要忘记]: if phrase in user_msg: result[user_intent_ phrase] user_msg[:200] return result def _enforce_budget(self) - None: # 按预算比例裁剪历史层 history_budget int(self.max_tokens * self.budget[history]) count 0 keep_start 0 for i in range(len(self.turns) - 1, -1, -1): turn_tokens self._count( self.turns[i][user] self.turns[i][assistant] ) if count turn_tokens history_budget: keep_start i break count turn_tokens # 保留最近 keep_start 之后的轮次 一摘要描述 if keep_start 3: self.memos.append(f早期对话摘要{self._summarize(self.turns[:keep_start])}) self.turns self.turns[keep_start:] def _summarize(self, old_turns: list) - str: # 实际项目中这里调用 LLM 做摘要 return f[共 {len(old_turns)} 轮对话已压缩] def build_payload(self) - list: messages [] messages.append({role: system, content: self.system_prompt}) if self.memos: memo_text \n.join(self.memos) messages.append({role: system, content: f【记忆摘要】\n{memo_text}}) messages.extend(self.turns) return messages这个实现要表达的核心是所有管理逻辑都收敛在一个管理类里外部业务代码只需要调用add_turn和build_payload不需要关心上下文怎么裁剪、怎么合并、怎么预算。这样的抽象让 Agent 的核心逻辑保持干净上下文策略可以独立演进。在生产里我会把这个类再打磨一下token 计数从每次全量计算改成增量累计否则长对话性能顶不住摘要逻辑接上真正的 LLM 调用build_payload时可以传入任务层和检索层的内容做动态组装。代码不是重点重点是这套思路上下文不是一个 Append 不完的列表而是一个有预算、有淘汰、有压缩、有持久化的资源池。每一个 Agent 项目都应该有一个类似的管理层哪怕是你自己写的裸函数也不要把上下文拼这件事散落在业务代码的各个角落。7. 常见问题与排查实录最后把我调试 Agent 上下文问题时踩过的坑整理成一份速查表。这些全是真实发生过的场景比任何文档都值钱。7.1 Agent 重复调用同一个工具陷入死循环这是最诡异的故障之一Agent 明明已经拿到了结果还是在不停调用同一个函数甚至调用了十几遍。排查后发现问题出在工具调用历史没有被正确截断——模型的输出里保留了上一次的部分工具结果导致模型认为“任务还没完成”。解决思路给工具链加一个显式的“调用状态”字段Agent 每次看到工具结果时必须同时看到“该任务是否已标记完成”。同时把工具调用的完整日志从上下文里移出只保留总结性的结果。7.2 Agent 忘掉用户最初的需求用户在第 1 轮提出了明确需求绕过第 12 轮Agent 开始按自己的理解自由发挥。典型原因是任务层信息没有常驻被历史数据挤出了窗口。解决思路在build_payload里强制把“用户原始需求”作为一条高优先级消息紧跟 System Prompt 之后注入且这条消息不参与淘汰。相当于在上下文的黄金位置“钉”了一颗钉子。7.3 上下文溢出发生在“工具返回超大结果”时有时工具返回的是整份文档、整个文件内容直接塞进上下文一次性超限。你代码里算好的预算根本拦不住这种突发大块数据。解决思路对所有工具输出做两层防护——先截断到预设上限比如 3000 token再做结构化精简只保留关键字段。如果业务确实需要完整内容拆分后分段交给模型并行处理或分批总结而不是一次全塞进去。7.4 API 费用突然暴涨有一次我们上线新功能后账单翻了 4 倍排查发现是日志系统把完整对话历史重复写入导致部分用户请求的上下文变成了原来的 2 倍还多。这不是模型问题是工程问题。解决思路给每个 Agent 请求增加一个日志采样率平常只记录关键事件同时全面开启 Prompt Caching把恒定前缀成本降下来。另外所有检索注入的文本默认做一次去重和裁剪别让“冗余”白白烧钱。7.5 压缩总结后关键信息丢失这个坑前面提过但值得再次强调压缩摘要导致数字类信息丢失是 Agent 生产事故的主要元凶之一。解决思路强制摘要模板化。让 LLM 在压缩时按固定 schema 输出人物、时间、数字、承诺事项、用户偏好、待办事项各占一栏。只填摘要字段先不用自然语言自由概括。结构化模板的容错率远高于自由摘要后续也方便程序化检索和校验。做上下文工程这一年多我个人体会最深的一点是Agent 的能力上限很大程度上不是模型决定的而是你喂给它的信息质量和组织方式决定的。模型再强上下文一团乱麻输出照样一塌糊涂反过来说把上下文管理到位一个本来平平无奇的模型也能在业务场景里跑出让人满意的效果。这套东西没有一劳永逸的标准答案只能根据你的业务场景、模型选型、成本预算去反复调优。但方向是确定的把上下文当成一个资源系统来设计你的 Agent 就已经赢在起跑线上了。