
做AI Agent落地也有段时间了从最初天天调Prompt调到怀疑人生到后来慢慢发现——真正决定Agent上限的根本不是那几句花哨的提示词而是你喂给它的上下文怎么组织、怎么流转、怎么管理。这个话题就是所谓的上下文工程Context Engineering。最近圈子里越来越多人在聊但能讲明白的不多。很多教程还在教你怎么写Prompt模板却忽略了上下文才是Agent的内存和工作台上下文工程才是让Agent从能聊天变成能干活的关键。我前后带过好几个Agent项目有基于Spring AI Agent搭的企业问答有用FastAPILangChainLangGraph做的流程自动化中台也研究过Rust生态里的Agent框架还见过用扣子这类低代码平台做的小红书自动发布机器人。无论底层是什么技术栈只要涉及多轮对话、工具调用、知识检索就绝对绕不开上下文工程。它决定Agent能不能记住关键信息、能不能在长任务里不迷路、能不能在并发场景下稳定输出不串号。这篇东西我不讲虚的直接从踩过的坑和实际调试记录出发把上下文工程从原理到落地拆开揉碎。适合正在做Agent开发、准备搭Agent中台、或者被Agent答非所问折磨的工程师参考。1. 上下文工程先想清楚它到底在解决什么问题1.1 先看一次真实的翻车现场去年我帮一家电商公司做客服Agent用户连续发三条消息帮我查一下上个月的订单对就是那个红色的改成周三发货吧如果Agent没有把第一轮的上个月订单作为隐性上下文带入第二轮和第三轮那这三句就是彼此孤立的Query——第二句根本不知道那个红色是什么第三句也不知道该改什么。我当时的初版Agent就翻车了用户的反馈是这AI像个失忆症患者。这个例子特别典型。很多Agent表现智障本质不是模型不行而是上下文没有做工程化处理。模型本身记忆力没问题问题在于我们没有帮它维护一个连贯、清晰、可检索的上下文环境。上下文工程的第一要务就是让Agent在多轮交互中保持前后一致不会说了下句忘了上句。1.2 上下文工程不是提示工程的升级版很多人把上下文工程和提示工程混为一谈觉得把Prompt写得更详细一点不就是在做上下文工程吗。这个理解偏差很大。提示工程解决的是如何把指令表达清楚上下文工程解决的是如何把信息在正确的时间、以正确的形态、送到正确的位置。两者不在一个维度。打个不一定严谨但很好懂的比方提示词是剧本规定了演员说什么台词上下文是演员的台词记忆和剧本注释决定了演员能不能接住对手戏、演好一整场。剧本写得再漂亮演员没记住前面的情节戏照样垮掉。从工作内容上看提示工程主要围绕模板、措辞、few-shot示例的设计上下文工程关注的是信息的全生命周期管理——构建、检索、路由、压缩、持久化。难度和收益完全不在一个量级。我见过很多团队把Prompt迭代到第几十版效果还是拉胯最后发现是上下文在传递过程中丢了关键字段这种问题改Prompt永远改不好。1.3 Agent场景下上下文到底特殊在哪单次LLM调用里上下文很简单system prompt加user input拼起来就完了。但在Agent里情况完全不一样上下文是多轮对话历史的累积不是单次请求工具调用返回的结果要回流到上下文里比如查数据库返回的JSON、调用API返回的报错信息知识库检索命中的片段要动态插入上下文用户状态、业务数据、权限信息也要作为上下文的一部分更麻烦的是Agent系统还要面对并发、多租户、长会话这些工程问题。比如我做过的一个中台项目高峰期数百个用户同时在用Agent每个用户的上下文都是独立的如果上下文没有做隔离和快照就会出现A用户问了一句B用户的Agent突然回答跑偏的严重事故。所以说上下文工程本质上是让Agent在复杂、动态、并发的环境里始终能拿到自己此刻最需要的那块信息拼图。它不是一个单一技巧而是一整套工程方法论。2. 上下文窗口与Token分配先学会算账再谈优化2.1 上下文窗口不是越大越好先说一个反直觉的结论上下文窗口不是越大越好。现在的模型动辄支持128K甚至1M的上下文窗口看起来很美好觉得反正窗口大我全塞进去就行了。但我实际测试下来窗口越大有几个问题越明显第一推理延迟明显上升。塞入的token越多模型需要处理的注意力计算量越大响应时间从几百毫秒飙到几秒是常有的事。第二成本翻倍。大模型API基本都是按token计费上下文每多一万token价格就是实打实往上走。第三注意力稀释。模型面对超长上下文时反而更不容易聚焦到关键信息上出现迷失在中间的现象——这是有论文支撑的也是我在实测中反复踩过的坑。我的经验是能用128K解决问题绝对不要让模型背着两百万token的包袱跑。上下文工程的一个重要目标就是在有限的预算里把最有价值的信息放进去。2.2 Token预算怎么做做上下文工程第一步是学会算Token账。不同模型的tokenizer不一样但大致可以按下面的经验值预估内容类型大致Token消耗1个汉字约1.5~2 Token1个英文单词约1~1.5 Token1个JSON字段约10~20 Token取决于内容长度工具函数定义含参数约50~200 Token1条对话历史一问一答约200~800 Token我一般会用模型的tokenizer工具直接统计而不是靠肉眼估算。比如OpenAI的tiktoken、各家平台的API调试界面都能看到精确的token消耗。Token预算的核心原则是给每条信息定价然后按优先级分配。我自己的分配比例如下System Prompt固定区不超过总窗口的10%~15%对话历史滚动区预留总窗口的30%~40%工具定义和调用结果区预留15%~20%检索结果上下文区预留20%~25%输出预留区至少留出总窗口的15%~20%输出预留区经常被忽略但非常关键。如果上下文中塞满了内容导致模型输出空间不够它会直接截断或者生成到一半报错。我在项目里就遇到过Agent话说到一半突然断了的问题最后定位就是输出预留token不够。2.3 上下文窗口的分区策略理解了预算下一步就是分区。我习惯把Agent的单轮上下文窗口看成五个区域固定区放system prompt、角色设定、全局约束这部分基本不动动态区放当前用户请求和最近的几轮对话工具区放可用的工具定义、当前调用结果知识区放RAG检索出来的参考文档片段预留区保证输出空间分区之后你才能对每一块做精细化控制。比如动态区满了就用滚动窗口覆盖最旧的历史知识区太大了就先做压缩再塞进去工具区定义了过多的工具就要做工具路由只把相关工具的定义放进窗口。我之前做的一个Agent中台就是把这个分区逻辑做成了配置化的模板不同的业务场景客服、数据分析、内容生成可以配置不同的分区占比效果比统一模板好了很多。说到底上下文工程的精髓就是给每类信息一个明确的座位而不是让它们在一张长桌上乱抢位置。3. 上下文工程的核心操作构建、检索、压缩、路由、持久化3.1 上下文的构建结构化是你的朋友很多开发者写上下文喜欢搞一大段拼起来的纯文本几百行塞进去看似信息全实际模型很难从中提取结构化的关系。我的做法是尽量使用结构化格式来构建上下文。比如一个客服Agent的上下文我会分成以下几块[系统角色] 你是XX平台的智能客服... [用户档案] 用户ID: 12345 会员等级: 黄金 最近30天订单数: 8 [对话历史滚动窗口] ... [当前任务] 用户正在查询订单修改发货时间 [可用工具] 1. queryOrder(orderId) 2. updateShippingTime(orderId, newTime) ...这里有几个好处第一模型可以很清楚地区分这是用户信息这是历史对话这是我要处理的任务第二自己写的解析代码也更方便从中抽取字段第三在并发场景下不同用户的上下文本质上就是一份结构化的JSON序列化、存储、恢复都非常方便。我强烈建议在项目初期就把上下文的定义做成Pydantic模型或TypedDict之类的强类型结构而不是一个大字符串。相信我等到你要做上下文压缩、快照、恢复的时候结构化能救你的命。3.2 上下文的检索向量检索不是银弹RAG已经是上下文工程的核心组件之一但我觉得有必要泼点冷水向量检索不是银弹。我见过太多人把知识库文档一股脑切成chunk丢进向量库然后Agent回答效果依旧稀碎。问题出在哪主要有三个切分太粗暴把原本连贯的知识切成七零八落的小块检索出来上下文不完整只靠向量相似度忽略了关键词匹配、文档结构等信号检索结果不分级把一堆低相关的片段全塞进上下文我的改进经验是第一chunk切分要结合文档结构来比如按标题、段落来切而不是固定每500个字切一个。第二用混合检索向量加BM25关键词再配合重排模型reranker把最相关的几个片段挑出来。第三检索结果的插入不要一股脑全放进去要做质量过滤和去重只保留Top 2~3块。在LangChain里我一般会用MultiQueryRetriever或者EnsembleRetriever来做混合检索再用CrossEncoder做rerank。这套组合下来回答准确率提升非常明显。还是那句话给Agent的上下文不是越多越好而是越精越好。3.3 上下文的压缩与遗忘让Agent该忘的就忘长会话是上下文工程绕不开的坎。用户跟Agent聊了五十轮你不可能把全部历史都塞进窗口。除了滚动窗口丢弃最旧的消息更聪明的做法是摘要记忆。我的做法是维护两层记忆短期记忆最近N轮对话原文保留细节长期记忆对更早对话生成的结构化摘要比如用户已确认购买红色款待修改发货时间为周三具体实现是在LangGraph里加一个压缩节点当对话历史超过阈值时触发调用一次LLM把旧历史总结成摘要然后替换掉原文。这里有个心得摘要不要只让模型总结一下之前的对话而要给一个模板让它按结构化字段来总结比如用户偏好已确认事项待办事项争议点这样长期记忆才能真正被后续环节用起来。另外一个容易忽略的点是遗忘。有些信息对当前任务无关甚至有害比如用户早期随口说的一句玩笑话就没必要一直留在上下文里。上下文工程不仅要知道放什么进去还要知道主动把什么清出去。定期清理上下文里的噪音信息看起来不起眼但对模型输出的稳定性的提升是实打实的。3.4 上下文的持久化与并发隔离中台绕不开的工程问题当Agent做大了不再是一个单机Demo而是要支撑很多用户同时使用、对接很多业务方就得上Agent中台上下文就不能只存在内存里了必须做到持久化与并发隔离。我在中台项目里每个用户的上下文都是一个独立的对象用上下文ID做唯一标识存储在Redis里。每次Agent开始处理一个新请求时先把该用户的上下文从Redis里加载出来处理完成后再把新的上下文快照写回。这个过程中的关键技术点是上下文必须是用户维度的绝对不能混上下文快照要有版本号防止并发写覆盖长时间不活跃的会话要自动回收释放存储符号学表完成之后再来说说并发。几千个用户同时使用Agent服务本身处理能力也许不是瓶颈但上下文的读写在并发下容易出问题。比如两个请求同时更新同一个用户的上下文就可能导致更新丢失。我采用的方案是给每个用户的上下文加一个乐观锁更新失败就重试同时把上下文加载和更新的部分做成异步队列避免阻塞主链路。我还研究过Rust语言里的Agent框架Rust在内存安全和并发上的优势确实明显但生态相比Python还是差一些适合对性能极致敏感的场景Spring AI Agent则更适合Java技术栈比较重的公司跟已有的Spring生态对接顺畅。不管用哪一套上下文工程的底层逻辑都是一样的。4. 实操拆解FastAPI LangChain LangGraph 实现一个带上下文管控的Agent4.1 选型思路为什么我选了这三件套我在自己的多个项目里最常用的组合是FastAPI LangChain LangGraph。理由很简单FastAPI做API层异步支持好扛并发能力在线社区活跃LangChain提供了大量的工具集成、检索器、模型抽象省去很多重复造轮子的时间LangGraph能把Agent的流程建模成一张图各个节点之间显式传递状态特别适合做上下文工程——因为它把状态作为一等公民如果你更习惯Java技术栈那Spring AI Agent是合理的替代方案它在Spring Boot生态里接入AI能力非常顺滑如果你追求极致的性能和并发Rust语言的Agent框架值得研究但学习和开发成本都会偏高。每个技术栈都有自己的取舍上下文工程的原理是通用的下面的代码思路你可以平移到任何技术栈里。4.2 核心代码骨架从状态定义到上下文管理先定义一个结构化的上下文状态。在LangGraph里状态就是一个TypedDict或Pydantic模型from typing import TypedDict, List, Optional from langchain_core.messages import BaseMessage class ConversationState(TypedDict): user_id: str messages: List[BaseMessage] # 短期对话历史 summary: str # 长期记忆摘要 retrieved_docs: List[str] # RAG检索结果 current_task: Optional[str] # 当前任务 tool_results: dict # 工具调用结果暂存然后定义几个核心节点。第一个是上下文构建节点它负责把用户档案、检索结果、历史摘要拼装成一份结构清晰的上下文传给生成节点def build_context(state: ConversationState) - ConversationState: docs retrieve_knowledge(state[messages][-1].content, user_idstate[user_id]) state[retrieved_docs] docs[:3] # 只保留最相关的3块 return state第二个是生成节点它负责调用LLM把组装好的上下文传进去def generate_answer(state: ConversationState) - ConversationState: system_prompt build_system_prompt( user_profileload_user_profile(state[user_id]), summarystate[summary], docsstate[retrieved_docs], tool_definitionget_tool_definitions(state[current_task]), ) response llm.invoke( [SystemMessage(contentsystem_prompt)] state[messages][-5:] ) state[messages].append(response) return state第三个是上下文压缩节点当对话历史超过阈值时把旧历史变成摘要def compress_context(state: ConversationState) - ConversationState: if len(state[messages]) 10: old_messages state[messages][:-6] summary_prompt f请总结以下对话的核心信息用户偏好、已确认事项、待办事项:\n{old_messages} new_summary llm.invoke([HumanMessage(contentsummary_prompt)]) state[summary] combine_summary(state[summary], new_summary.content) state[messages] state[messages][-6:] # 只保留最近6轮 return state最后把这些节点连成一张图from langgraph.graph import StateGraph, END graph StateGraph(ConversationState) graph.add_node(build_context, build_context) graph.add_node(generate, generate_answer) graph.add_node(compress, compress_context) graph.set_entry_point(build_context) graph.add_edge(build_context, generate) graph.add_edge(generate, compress) graph.add_edge(compress, END)这只是最简版本真实项目里还会有工具调用节点、路由节点、反馈闭环等但核心的上下文管理链路就是这三步构建、生成、压缩。这套骨架我用了很久扩展性非常好加节点就像拼积木一样。4.3 扛并发上下文隔离与快照的落地经验刚才热搜词里有人问AI Agent怎么扛并发我在这块花的时间最多踩过的坑也最多。这里把核心经验直接写出来。第一API层用FastAPI的异步支持Agent执行本身放到线程池或异步任务队列里不要在请求线程里同步等着LLM返回。第二上下文的存储用Redis每个用户一个key设置合理的过期时间比如30分钟无操作就清理。第三写入上下文快照时带上版本号用WATCH/MULTI或者lua脚本做乐观锁更新。import redis.asyncio as redis import json r redis.from_url(redis://localhost:6379) async def save_context(user_id: str, context: dict, version: int): key fagent:ctx:{user_id} value json.dumps({version: version 1, data: context}) # 用lua脚本保证只有版本号匹配时才更新 script local v redis.call(HGET, KEYS[1], version) if (not v or tonumber(v) tonumber(ARGV[1])) then redis.call(HSET, KEYS[1], version, ARGV[2]) redis.call(HSET, KEYS[1], data, ARGV[3]) return 1 end return 0 ok await r.eval(script, 1, key, version, version 1, value) if not ok: # 版本冲突重试加载并合并 pass # 这里做重试逻辑这套方案实测下来支撑一个几百并发的商用项目是没问题的。当然如果你真的要做到百万级并发那还需要在架构上引入消息队列和分布式缓存但核心思路不变——上下文是状态状态必须有独立的隔离与并发控制。很多低代码平台的Agent开发比如扣子这类工具底层其实也是帮你管理了上下文只是封装成了可视化配置。你不需要手写上面的代码但理解了原理你在配置记忆变量知识库的时候就会更有章法不会瞎配置。5. 常见问题与排查技巧实录5.1 问题速查表我把开发过程中踩过的典型问题整理成了一个速查表方便你对着排查症状可能原因排查方向Agent答非所问上下文里混入了太多无关噪音检查上下文构建逻辑看看哪些历史信息被保留多轮对话中失忆旧对话被直接截断没有做摘要记忆检查滚动窗口和压缩节点是否生效生成长文时突然截断输出预留token不够检查生成参数max_tokens设置并发高时串号上下文没有做用户隔离检查上下文key是否带了tenant_id/user_id响应越来越慢且贵上下文无限膨胀检查对话历史是否有无上限累积知识库回答不准确chunk切分不合理、检索相关性差检查rag的切分策略和重排逻辑工具调用结果处理错误工具返回的JSON没被正确解析后放回上下文检查工具结果的结构化清洗逻辑5.2 一次串号事故的排查实录这个案例我印象很深。我们的Agent中台上线两周后有用户投诉说我和客服聊天回答里面出现了另一个人的订单号。当时第一反应是缓存key冲突排查了半天没找到问题最后把日志里的上下文快照拉出来对比才发现问题出在一个共享的静态变量上——我在代码里为了省事把当前处理的user_id存成了模块级全局变量导致异步并发下不同请求互相覆盖。这个教训极其深刻。上下文工程不仅仅是把信息塞给模型那么简单它还包括工程层面的数据隔离。任何全局的、共享的、可变的状态都是隐患。从那次之后我给自己立了一条规矩Agent的上下文状态一律显式传递一律不从全局变量读取一律在请求入口处从Redis加载。5.3 独家避坑技巧分享下面这几个技巧是我在多个项目里反复验证过的第一个上线前一定要把每一个请求的上下文dump下来。我开发的时候会在日志里打印每一次发给模型的完整上下文结构出问题的时候直接翻日志用脚本分析token分布马上就能定位是哪个区域占太多、哪个关键信息丢了。这样做几次之后你对上下文的体感会强很多。第二个System Prompt尽量保持精简。很多人喜欢在System Prompt里堆砌大量规则和背景知识结果每一次请求都要把一堆静态内容重新扔给模型浪费token还稀释注意力。静态内容能放到上下文构建阶段动态加载的就不要一股脑全写死在System Prompt里。第三个给关键的上下文字段加监控。比如我在中台系统里会统计每个用户上下文的平均token数、摘要的更新频率、RAG命中率。这些指标能直观反映上下文工程是否健康。如果发现某个业务字段的摘要更新频率极低大概率说明这个字段对Agent决策没有实际帮助可以考虑优化。我还用过纯文本拼上下文的方式跑过业务线也试过用结构化方式管理上下文效果差距真的很大。前者的代码写起来快但后者的可维护性和可观测性高出一个层次。做上下文工程短视是要付出代价的前期多花点心思做结构化后面调试和迭代能省十倍的时间。最后说两句实在的根据我个人的项目体验上下文工程最核心的不是某一个炫技技巧而是克制两个字——克制地放信息、克制地做压缩、克制地给模型留空间。很多Agent项目翻车不是模型不够聪明而是我们把太多乱七八糟的东西倒进了上下文的锅里最后煮出来的自然不是好味道。如果你现在正准备做AI Agent我的建议是别急着堆功能先把上下文的数据结构定义清楚把窗口预算算明白把压缩和持久化的链路打通。这套地基打好了后面加多少工具、接多少渠道都不会乱。从FastAPI到LangGraph到Spring AI到Rust技术栈可以换但上下文工程的方法论是通用的。希望这篇分享能让你少走几步我当年走过的弯路。