
最近在折腾AI Agent项目的时候我越来越确认一件事大多数Agent跑不稳、答非所问、越聊越笨问题不在模型而在上下文Context。你喂给模型什么、按什么顺序喂、喂多少、什么时候该清理缓存和记忆这些加起来就是所谓的“上下文工程”。它跟提示词工程不是一回事也比提示词工程更值得投入时间。这篇文章我想把它掰开揉碎讲清楚从概念到token开销再到一套真实的FastAPI LangChain LangGraph项目里怎么落地最后整理一些我踩过的坑和排查方法。如果你是正在搭Agent、在看Agent主流架构或者为“Agent怎么扛并发”这类事头疼的人这篇内容应该能帮上忙。1. 上下文工程是什么AI Agent的“工作记忆”危机1.1 从提示词工程到上下文工程概念的一次升级早几年大家聊提示词工程核心是“把指令写清楚”。一条prompt里写清角色、任务、输出格式模型基本就能给出不错的结果。但Agent出现后情况变了。Agent不是一个“问一句、答一句”的东西而是一个会循环执行“思考—调用工具—观察结果—再思考”的自治系统。这意味着每次调用模型时输入的不只是你那句用户指令还包括系统设定、历史对话、工具返回的JSON、向量检索的知识片段甚至还有上一次执行失败的报错信息。这些东西拼在一起才构成了模型那一刻能看到的全部“上下文”。上下文工程要解决的就是怎么把这一大堆异构的、动态变化的信息有策略地装进有限的上下文窗口里让模型在每一步都能拿到它真正需要的东西。我常打一个比方Agent就像一个临时工团队每接一个活儿都得把项目背景、会议纪要、临时数据、过往文档全堆到桌面上干活。桌面太乱他们会找不到关键文件桌面太小东西堆不下又开始丢三落四。上下文工程就是那个负责整理桌面的人。1.2 为什么Agent时代上下文工程比调参更关键单轮LLM调用里上下文顶多是“prompt写得好不好”的问题。Agent场景里上下文直接决定行为稳定性。举个例子。我在做机器人客服Agent的时候用户问“我的订单什么时候到”Agent先调用了订单查询工具工具返回一条很长的JSON里面包含十几个字段。如果这些字段原样塞回给模型模型很容易被无关字段干扰甚至从某个备注里脑补出错误结论。但如果我把JSON清洗成一句话“订单号: A123, 状态: 已发货, 预计送达: 明天”模型几乎不会跑偏。另一个常见的例子是历史消息污染。用户第五轮突然问“我刚才说的地址你还记得吗”如果上下文的滑动窗口把第二轮那条地址消息冲掉了Agent就只能装傻。这类问题在传统提示词工程里根本不会遇到但在Agent里三天两头出现。更关键的是上下文工程影响的是Agent的“每一跳”。一个Agent跑一次完整任务可能要调用5到10次模型每一步的上下文质量都会积累。前面某一步上下文里混入了一条错误信息后面每一步都可能被它带偏而且越偏越远。这跟代码里的全局状态被污染是一个道理。1.3 上下文工程和提示词工程的边界在哪里提示词工程做的是“静态设计”核心输出是一段精心编写的指令文本上下文工程做的是“动态编排”核心是一套运行时的信息管理策略。大家也可以这样理解提示词工程解决的是“模型在单点任务上的上限”上下文工程解决的是“模型在长链路任务上的稳定性”。对Agent开发来说前者是基础素质后者才是决定项目能否上线的关键差异。另外上下文工程还牵扯到一个容易被忽略的问题token经济学。模型按token收费Agent一次任务的token消耗可能是单轮对话的几十倍。上下文里多塞一段没用的内容不只影响效果还实打实地烧钱这就引出了下一部分要聊的事。2. Token视角看上下文先把钱和坑算清楚2.1 Token到底是什么意思为什么Agent会疯狂烧Token先解释一下token。模型并不像人一样按“字”理解文本而是把文本切分成“词元”再处理。英文里一个token大约对应0.7到1个单词中文里一个字大约对应1到2个token这个切分规则由每个模型的分词器决定。Agent烧token比普通聊天严重得多根源在于“每次调用都要带着历史”。我统计过一个真实案例一个简单的网页检索Agent功能是“根据用户问题搜索网页并总结答案”。用户只问了三个连续问题它的token消耗是这样的第1轮system prompt约500 token用户问题80 token工具返回结果1200 token模型输出300 token合计约2080 token。第2轮上述所有内容原样带上再加新问题和新的工具返回合计约3800 token。第3轮累计到约6000 token。三轮对话烧掉了接近一万两千token其中大部分是反复重发的历史内容。如果系统里有100个用户同时这么用成本压力一下子就上来了。这也是为什么很多Agent项目跑完测试一算账单比预想高出一个量级。2.2 上下文窗口的分层利用System、History、Observation现代模型大多支持多角色消息结构像System、User、Assistant、Tool。上下文工程的第一步就是给每一层什么内容定好规矩。以我自己惯用的分层方法为例System层放的是固定指令包括角色定义、任务目标、输出格式、安全边界。这一层要稳定、精简不要塞动态数据否则会影响缓存命中率也容易让模型“精神分裂”。History层放历史对话。但它不是越多越好需要结合窗口大小做取舍后面会细说。Observation层放工具调用结果和外部数据。这一层注入最频繁也最需要清洗。User层放当前用户的实时输入这是上下文里唯一“不可预编译”的动态部分。分层听起来简单但很多人实际项目里一个messages数组从头拼到尾不分角色、不分来源时间一长老老实实出问题。模型分不清哪句话是用户说的、哪句话是工具返回的就开始产生幻觉。2.3 上下文预算管理截断、摘要、淘汰三件套上下文窗口再大也是有限的。Claude的100K窗口、GPT-4o的128K窗口看着挺宽但Agent的每一次工具调用、每一段检索结果都能轻松吃掉几千token。指望窗口够大就全部塞进去是非常危险的想法。我在项目里用了三个策略它们的优先级从高到低排列第一个是“淘汰”。工具结果是有时效性的。比如用户上一轮问天气这一轮问股市那条天气Json就没价值了直接丢不要留在历史里。淘汰是最省成本的手段。第二个是“截断”。如果历史确实需要保留但总量太大就做滑动窗口只保留最近的N轮对话外加一个最早的核心约束摘要。比方说历史超过20条消息就只保留最近的12条把更早的内容压缩成两三句话的任务摘要。第三个是“摘要”。这是最后的手段因为摘要会丢失细节。比较稳妥的做法是让模型定期把旧历史改写成结构化摘要而不是用简单截断。比如每积累10轮历史触发一次摘要任务把前面几轮的核心事实用户偏好、已确认信息、待办事项提取出来替换进System层。这套“淘汰—截断—摘要”的组合几乎适用于所有基于大模型的Agent架构。它能控制token消耗也能减少噪音对模型的误导。后面实战部分我会演示它怎么落到代码里。3. 实战拆解在LangGraph里构建一个上下文工程样板3.1 整体架构与选型理由FastAPI LangChain LangGraph项目背景很简单我想做一个能真正干活的Agent服务接收用户HTTP请求让Agent自主调用工具最后返回结果。选型上用了FastAPI LangChain LangGraph原因很实际FastAPI负责对外提供接口自带异步支持和OpenAPI文档Python后端做Agent服务基本绕不开它。LangChain提供现成的模型封装、工具调用约定和提示词模板省去很多重复代码。LangGraph解决有状态编排问题。原生LangChain的链式调用在复杂分支场景下很别扭LangGraph把Agent流程建模成“图”节点之间的状态传递和循环控制都非常清晰还能保留每一步的上下文快照。我之前也考虑过纯手写状态机但LangGraph在状态管理上的成熟度确实更高社区也大踩坑时能找到参考。这个项目里我选择“FastAPI管接口、LangGraph管流程、LangChain管模型调用”分工明确代码维护起来很舒服。3.2 把上下文拆成四段拼装LangGraph里一切状态都走一个全局State对象。这个对象很关键它就是Agent运行时的“上下文容器”。我用的是TypedDict定义主要字段结构大致如下class AgentState(TypedDict): messages: Annotated[list, operator.add] # 历史消息 system_prompt: str # 系统提示词含动态摘要 current_input: str # 当前用户输入 tool_results: list # 工具调用结果队列 max_hist_len: int # 历史窗口大小每次模型调用前我会用一个专门的函数把State组装成最终发给模型的messages列表。这个函数是整个上下文工程的心脏顺序一般是System message由固定的系统指令 动态任务摘要拼接而成History消息从messages里按窗口截取工具观察消息把tool_results按规则清洗后追加当前用户消息最后一条def build_context(state: AgentState) - list[dict]: messages [] # 1. 系统层 messages.append({role: system, content: state[system_prompt]}) # 2. 历史层按滑动窗口截断 history state[messages][-state[max_hist_len]:] messages.extend(history) # 3. 工具结果层 for result in state[tool_results]: cleaned clean_tool_result(result) messages.append({role: tool, content: cleaned}) # 4. 当前输入 messages.append({role: user, content: state[current_input]}) return messages注意一点LangGraph的messages字段用了operator.add意味着每个节点往里reduce追加消息。这个机制虽然写起来方便但如果不做窗口截断状态会无限膨胀。所以我增加了一个裁剪节点在每条新消息进入后检查历史长度超出max_hist_len的部分就淘汰掉。3.3 工具结果的回流与清洗把JSON变成人话工具结果清洗是整个项目里收益最明显的一环。刚开始我没做清洗直接把工具返回的原始JSON塞回去结果模型经常回答得很生硬甚至出现幻觉。后来我改成了“结构化摘要”的方式每个工具在注册时都自带一个清洗函数把大JSON压缩成一行或几行重要信息。比如订单查询工具的结果清洗成def clean_order_result(raw: dict) - str: return ( f订单号:{raw[order_id]}, f状态:{raw[status]}, f预计送达:{raw.get(eta, 未知)}, f备注:{raw.get(note, 无)} )这个改动让模型对工具结果的利用率高了很多。核心原则是能丢的字段全丢只保留和当前任务相关的字段。如果工具结果超长我还会加一个长度上限截断后注明“结果过长已截断”。还有一点经验工具调用失败时不要直接把报错堆栈塞给模型。模型读到一堆Traceback很容易慌要么道歉要么编造修复方案。正确做法是把错误转换成一个温和但明确的中性描述比如“订单查询接口返回超时请引导用户稍后重试”。3.4 记忆层短期窗口加长期摘要怎么配合会话超过一定轮数后只有短期窗口是不够的。用户提到“我早上问过的那家店”这个信息可能已经被滑出窗口但它在摘要里还在。我在项目里给State加了一个长期摘要summary字段由独立的摘要节点维护。当历史消息超过N条时触发一次摘要更新让模型用旧历史旧摘要生成新摘要然后把历史消息清空一部分只保留最近几条。实现上摘要节点大概长这样def summarize_point(state: AgentState) - AgentState: if len(state[messages]) state[max_hist_len]: return state old_messages state[messages][:-10] recent_messages state[messages][-10:] summary_response llm.invoke( f请对以下对话历史进行摘要保留关键事实、用户偏好和待办事项\n{old_messages} ) # 更新system_prompt里的摘要段 state[system_prompt] update_summary_in_prompt( state[system_prompt], summary_response.content ) # 清空旧历史只留最近内容 state[messages] recent_messages return state这套机制跑下来多轮长会话的稳定性和token开销都改善了不少。它本质上就是我在前面说的“摘要策略”的工程化落地。4. 上下文工程引发的并发难题多会话隔离与缓存优化4.1 每用户一套上下文状态隔离是很严肃的事Agent服务和普通HTTP接口最大的区别是它是有状态的。不同用户、不同会话的上下文不能共享否则A用户的信息会串到B用户的对话里。在FastAPI LangGraph的架构里我采用的方式是一个会话一个Graph实例。每个会话在Redis里维护自己的State请求进来时按session_id取State请求结束时把State写回而不是把状态放在进程内存里。这样既支持多实例横向扩容也避免了进程重启丢状态的问题。最开始我用的是全局内存字典开发调试确实爽但一上并发就露馅多个请求同时修改同一个DialogState互相覆盖用户聊着聊着突然变成另一个人。后来改成“Redis 序列化State”Key为agent_state:{session_id}每次请求做一次读改写问题就消失了。还有一个容易踩的坑LangGraph的节点函数默认是异步友好的但如果你的工具调用是同步阻塞的并发能力会被卡死。我后来把所有工具调用都套了一层异步封装配合FastAPI的AsyncEndpoint才把吞吐提上去。4.2 上下文缓存高并发下省钱的隐藏手段并发量一旦上来token成本就绕不过去。现在几家主流模型厂商都提供了prompt缓存原理是如果请求的前缀内容相同就能命中缓存这部分token按折扣价计算响应速度也会更快。但要吃到这波红利上下文编排得配合。缓存命中的前提是“前缀高度稳定”。我总结出三个要点把稳定的系统提示词放在消息列表最前面越靠前越好。如果动态摘要每次都在变放中间或后面别影响前缀稳定性。工具定义的顺序要保持固定。不要因为某个工具偶尔没用到就从列表里删掉否则前缀一变缓存全失。近期历史变化快尽量放在缓存前缀之后。也就是让“稳定内容”占住前缀“易变内容”跟在后面实现最大化命中。在4路并发压测下做好缓存前后的token成本差距非常大。同一个Agent跑满一小时的压测前缀稳定的方案比乱排顺序的方案省了接近40%的重复token延迟也下降明显。4.3 会话重启、编排状态丢失的恢复策略并发场景下进程崩溃或服务发版是常有的事一旦State丢失用户的上下文就没了体验非常糟糕。我现在的做法是“持久化 重建摘要”双保险。每次状态变更都异步写入Redis和PostgreSQL双写Redis挂掉时还能从PG恢复。如果发现某个会话的State已经损坏或过期太久就退化成“只有系统提示词 用户当前输入”的无状态模式至少保证用户还能继续对话而不是报错。恢复上下文时还有个细节把过去的工具执行记录一并恢复。否则Agent恢复了聊天历史但不知道之前已经调用过哪些工具它可能会把同一个操作重复执行两遍。比如支付类Agent重复调用扣款接口这是很严重的线上事故。常规做法是在State里记录一个executed_tool_ids列表恢复时把里面的工具ID注入到上下文中并明确提示模型“这些操作已完成请勿重复执行”。5. 常见问题与排查实录5.1 Agent突然“失忆”上下文被滑动窗口冲掉了现象用户前几轮明确说了“我在北京工作”后面对话Agent却完全不记得甚至反问“您目前在哪个城市”。排查的思路很直接打印每次请求的实际messages数组看“在北京工作”这条消息是否还存在于历史窗口里。如果它真的被截断了解决方案不是简单加大窗口而是像前面说的那样把关键用户事实抽取到System层的摘要里。我用过一个轻量做法每次摘要节点执行时专门生成一个“用户关键信息”字段里面聚合了常住地、偏好、已确认事项等。这样即使历史被截断核心事实也能长期保留。注意排查失忆问题先看上下文再看模型。很多时候模型根本没收到那条信息而不是忘了。5.2 工具结果格式混乱带崩模型现象Agent调用工具后开始胡言乱语甚至把工具返回的JSON当成输出直接甩给用户。典型原因是我前面说的“没做清洗”。解决的步骤也不复杂第一步检查工具返回结果是否超出模型可用窗口第二步确认工具返回内容有没有明显的格式噪音比如多余的引号、换行、嵌套层级第三步把长JSON换成结构化摘要。改完这三个点这类问题基本能被根除。5.3 上下文重复注入导致Token暴涨另一种常见症状是每轮请求都会把一段大型知识库内容、完整历史、所有工具定义、全部系统指令原封不动塞进去token消耗成倍增长。我见过最离谱的项目里一段5000字的固定说明被重复注入了8轮单纯重复token就占了消耗的六成。处理方法分两块。固定知识切出来做成检索块只在需要时检索相关片段全量系统指令瘦身能压缩的压缩能拆分的拆分。另外善用缓存稳定前缀会大幅降低重复token的实际成本。5.4 常见问题速查表现象可能原因排查思路解决方案Agent突然失忆历史被滑动窗口截断打印messages数组检查关键信息是否还在关键事实抽取进System摘要工具结果带崩模型工具结果未清洗、格式噪音多查看工具返回内容是否直接被注入结构化摘要、丢弃无关字段Token消耗异常高固定内容反复全量注入统计每轮请求的token构成检索块 前缀缓存 窗口截断多用户上下文串线全局共享State检查状态存储作用域按session_id隔离RedisPG持久化会话越久越笨历史过长导致噪音占主导观察请求早段被谁占据摘要压缩 淘汰过时信息Agent重复执行工具恢复状态时缺少已执行记录检查恢复后的上下文是否含工具记录维护executed_tool_ids清单这套速查表是我在实际运营中逐步沉淀出来的几乎每个线上Agent项目都会中几条建议收藏备用。6. 最后分享一点个人的实际体验上下文工程这东西入门门槛不高但做到位很费功夫。我看过很多Agent项目Demo跑得飞起一上真实场景就崩几乎都是栽在上下文管理上。模型选得再好、工具写得再漂亮上下文拼装一团乱照样给出垃圾结果。所以我不建议新手一上来就研究Agent算法、研究推理优化先把上下文工程这一课补齐Agent的稳定性会有一个质的提升。我个人实操中最大的体会是上下文工程不是一次性设计出来的而是靠监控和复盘“养”出来的。上线后要把每次请求的上下文内容、token分布、模型行为日志全部记录下来定期分析哪一段信息真正帮助了模型哪一段只是添乱。我在第一个Agent项目里就是这么一步步调整过来的从最初的上下文乱成一锅粥到最后稳定跑通多轮复杂任务。如果你正在做Agent相关项目不妨先从“打印每一次模型调用前的messages”开始把上下文摊开来看一遍。相信我很多之前想不通的模型行为到这一步都会豁然开朗。最后再分享一个小技巧不要把所有上下文策略硬编码到业务逻辑里建议单独沉淀成一个context_manager模块提供构建、清洗、截断、摘要、缓存一套标准接口。后续每个Agent项目都能复用改起来也方便。上下文工程这件事值得专门花时间把它做成基础设施。