做AI Agent落地也有段时间了,从最初天天调Prompt调到怀疑人生,到后来慢慢发现——真正决定Agent上限的,根本不是那几句花哨的提示词,而是你喂给它的上下文怎么组织、怎么流转、怎么管理。这个话题就是所谓的"上下文工程"(Context Engineering)。最近圈子里越来越多人在聊,但能讲明白的不多。很多教程还在教你怎么写Prompt模板,却忽略了上下文才是Agent的"内存"和"工作台",上下文工程才是让Agent从"能聊天"变成"能干活"的关键。
我前后带过好几个Agent项目,有基于Spring AI Agent搭的企业问答,有用FastAPI+LangChain+LangGraph做的流程自动化中台,也研究过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 Token |
| 1个英文单词 | 约1~1.5 Token |
| 1个JSON字段 | 约10~20 Token(取决于内容长度) |
| 工具函数定义(含参数) | 约50~200 Token |
| 1条对话历史(一问一答) | 约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 实现一个带上下文管控的Agent
4.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_id=state["user_id"]) state["retrieved_docs"] = docs[:3] # 只保留最相关的3块 return state第二个是"生成节点",它负责调用LLM,把组装好的上下文传进去:
def generate_answer(state: ConversationState) -> ConversationState: system_prompt = build_system_prompt( user_profile=load_user_profile(state["user_id"]), summary=state["summary"], docs=state["retrieved_docs"], tool_definition=get_tool_definitions(state["current_task"]), ) response = llm.invoke( [SystemMessage(content=system_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(content=summary_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 = f"agent: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,技术栈可以换,但上下文工程的方法论是通用的。希望这篇分享能让你少走几步我当年走过的弯路。