
做 AI Agent 这半年我踩过最大的坑全在上下文里。模型能力再强上下文没管好Agent 就是个健忘症患者上一轮交代的事下一轮就忘。我一直觉得上下文工程这个被低估的环节才是真正决定 Agent 聪明的天花板。标题里挂着AI Agent和上下文工程两个词其实背后是一整套设计思路涉及 token 预算、记忆架构、检索策略、状态管理任何一环出了问题整个 Agent 就跑偏。这篇文章我想把自己项目里沉淀的上下文工程方法论完整摊开从为什么重要到核心拆解再到基于 FastAPI LangChain LangGraph 的实操落地最后附上我实测中遇到的典型问题。不管你是刚接触 Agent 开发还是已经在做多轮对话、复杂任务编排这篇文章都能给你一套可参考的上下文治理框架。1. 上下文工程到底是什么先搞清楚三个层次1.1 我为什么要花大力气研究上下文先说个真实事故。我早期做过一个客服问答 Agent用的模型是当时上下文窗口 8K 的版本。表面功能很简单就是让用户提问Agent 检索知识库后回答。测试阶段一切正常一上线就出问题用户多聊三轮回答质量明显下降到第五轮甚至开始答非所问。排查到最后发现原因特别蠢我把用户对话历史全部拼接进上下文不做任何裁剪。8K 窗口被聊天气泡占满检索结果只能塞进几百 token模型根本看不到知识库的关键内容。这就是典型的上下文没做工程化处理。从那以后我总结出一个观点上下文工程就是把有限的上下文窗口当作核心资源来调度。它解决的问题不是模型能不能理解而是模型能不能在有限注意力里看到最关键的信息。1.2 上下文工程的三个层次我习惯把上下文工程拆成三个层次分别对应不同的设计目标。第一个层次是上下文构建解决的是往窗口里放什么。这里包括对话历史的组织方式、系统提示词的编排、外部知识的引入顺序。大部分新手只做到这个层次就是把信息拼起来塞给模型但信息之间谁先谁后、谁重要谁次要往往没有设计。第二个层次是上下文管理解决的是窗口满了怎么办。模型有固定的上下文窗口超出就会报错或者被截断必须考虑压缩、摘要、丢弃、分片等策略。这个层次考验的是对 token 预算的精细控制。第三个层次是上下文状态化解决的是不同任务之间上下文怎么流转。在一个复杂的 Agent 工作流里可能多个工具调用、多轮用户交互交替发生上下文不是简单拼接而是按状态机的逻辑推进。这个层次通常需要 LangGraph 这类编排框架落地。三个层次层层递进缺一个都会出问题。只做构建不做管理窗口溢出只做管理不做状态化复杂任务梳理不清。后面我会逐个展开。2. 上下文管理的核心账本窗口、Token 与性能2.1 上下文窗口的现实约束你手里的牌就这么多很多人对上下文窗口有个误解觉得窗口越大越好似乎 128K、200K 的模型可以无限塞内容。实际上我实测下来大窗口不等于高质量反而往往伴随成本和延迟的翻倍增长。我在实际项目里养成了一个习惯每个 Agent 上线前先统计一次上下文预算表。预算表分四部分系统提示词占多少、对话历史占多少、外部检索内容占多少、预留的模型输出空间占多少。这四部分加起来必须低于模型上下文窗口的合理阈值。打个比方上下文窗口就像你租的仓库仓库大了租金就贵而且货物摆放不合理时要找的东西反而被埋在最深处。模型的注意力机制不是均匀分布的窗口中间的 token 往往会被弱化。所以哪怕你有 200K 的窗口也不能把 100K 的历史记录全部堆进去必须有所取舍。2.2 上下文的价格与性能账本为什么必须精打细算上下文工程的第二个硬约束是成本。当前主流大模型 API 的计费方式中输入 token 和输出 token 分开计价上下文越长单次调用的输入成本越高。如果你的 Agent 单次任务需要调用 5 次模型每次携带 10K token 上下文一天一万次请求这个成本翻倍得非常快。我算过一笔账一个中等规模的项目如果每轮请求多带 4K 无用 token按每月几十万次调用计算额外产生的费用非常惊人利润几乎被吃掉一大部分。更不用说延迟上下文越长首个 token 返回时间明显变慢用户的等待体验直接下降。所以在上下文工程里KPI 不是模型记住了多少而是用最少的 token 达成了多少有效理解。核心思想就是做减法和精选去掉冗余只保留能让模型完成任务的最小集合。2.3 上下文窗口、Token 与配置参数速查为了让你方便对照我把上下文工程里的关键参数整理成一张表。这是我项目里配置 Agent 时的常用参考。参数项说明我的常用配置系统提示词描述角色、任务、输出格式的固定文本控制在 800~1500 token 内对话历史保留轮数参与拼接的最近 N 轮对话常规任务 6~8 轮复杂任务 10~12 轮单轮对话最长截取防止用户输入过长导致溢出单条消息超过 2000 token 时做摘要或截断检索内容条数从外部知识库召回并注入上下文的片段数3~5 条每条控制在 500 token 内max_tokens模型输出上限生成类任务 1024结构化任务 2048上下文压缩触发阈值历史上下文超过窗口比例时触发压缩超过总窗口 60% 时触发摘要压缩这张表不是死标准你自己研发时可以根据模型和应用场景调整。但它提供了一个检查思路每一项配比都值得单独推敲。3. 实操落地基于 FastAPI LangChain LangGraph 的上下文工程实现3.1 整体架构我的 Agent 上下文流水线设计我当前的项目采用 FastAPI 提供 HTTP 服务LangChain 封装 LLM 调用和检索组件LangGraph 负责编排多步 Agent 工作流。很多朋友问为什么要用三个框架我的答案很简单它们各管一段上下文工程天然需要这样的分工。FastAPI 负责对外接口和并发挂载。每次用户请求进来由它创建独立的请求作用域保证并发场景下 A 用户的上下文不会串到 B 用户那里去。LangChain 负责封装模型的统一入口和检索器。我用它对接不同厂商的大模型通过统一的接口切换同时用它的向量检索组件做 RAG这是上下文工程里外部知识进入的关键通道。LangGraph 负责把整个会话流程变成一张状态图。节点的切换、状态的分支不再是散落的 if-else而是显式编成的图结构。每个节点看到的状态都从统一的上下文容器里读取。架构上还有一个核心设计我称之为上下文门面。所有模块读写上下文都通过这个门面而不是直接操作原始列表。这么做的好处是后续想改压缩策略或者换存储后端只动门面内部实现业务流程完全不用改。3.2 检索增强让外部知识真正进入上下文上下文不能只有对话内容必须要结合外部知识。这个环节我使用的是 RAG 思路也就是先召回再注入。在 LangChain 里的实现我大概是这样组织的from langchain_community.vectorstores import FAISS from langchain_huggingface import HuggingFaceEmbeddings from langchain_text_splitters import RecursiveCharacterTextSplitter embeddings HuggingFaceEmbeddings(model_nameBAAI/bge-small-zh-v1.5) splitter RecursiveCharacterTextSplitter(chunk_size500, chunk_overlap50)这里有两个细节值得说。第一文本切分不能只按固定长度硬切我试过按 500 字无脑切结果好多语义完整段落被腰斩召回的片段既不通顺也丢失关键信息。RecursiveCharacterTextSplitter 的优势在于它会优先按段落、句子等自然边界切分尽量保证 chunk 语义完整。第二检索条数不要贪多。很多人总觉得召回越多越好一次性注入十个 chunk结果上下文被无关内容占据。我实测下来召回的 top-k 设置为 4~5 条最稳。如果业务场景对准确率要求高可以先用重排序模型精排把最相关的两条放到最前面效果提升非常明显。检索这块最容易忽略的是查询改写。用户原话往往是模糊的直接拿去向量检索很可能召回不准。我通常在检索前加一轮查询改写让模型基于对话历史重新生成一个更完整、更明确的检索查询。这一步的收益比调 embedding 模型还要大。3.3 上下文压缩窗口快满时的止损方案上下文压缩是上线后必须处理的工程问题。我的压缩策略分三步按触发条件逐级执行。第一步是裁剪只保留最近 N 轮对话更早的记录直接丢弃。这一步做法最粗暴但效果立竿见影。适合那些早前信息对当前任务影响不大的场景。第二步是摘要压缩把旧对话喂给模型生成摘要再用摘要替换原始对话。这一步我封装成一个独立函数from langchain_openai import ChatOpenAI def compress_history(messages, llm): history_text \n.join([f{m[role]}: {m[content]} for m in messages]) prompt f请将以下对话历史压缩为一段100字以内的摘要保留关键事实、用户意图和未完成事项 {history_text} summary llm.invoke(prompt).content return [{role: system, content: f以下是更早的对话摘要{summary}}]摘要压缩的关键是提示词要强调保留未完成事项。我踩过坑有一次摘要生成得特别通顺但把用户明确要求过的一项后续操作漏掉了结果 Agent 后面再也想不起来这个待办被用户投诉了两次。这个提示词细节直接决定摘要的有效性。第三步是结构化存储压缩掉的内容不直接扔掉而是写入一个长期记忆区当用户再次提起相关话题时通过检索找回来。这一步是在解决短期窗口和长期记忆的矛盾也是我目前认为上下文工程最优雅的解法。3.4 用 LangGraph 管理上下文状态流转当 Agent 的任务链路变长多个模型调用、工具调用交错出现用 LangGraph 编排会清晰很多。我最初用 LangChain 的链式写法虽然能用但状态流转一复杂代码就开始发臭。LangGraph 的核心思路是把工作流定义成一个图每个节点负责一个子任务节点之间通过状态对象传递信息。我的设计是这样的from langgraph.graph import StateGraph, END from typing import TypedDict class AgentState(TypedDict): messages: list retrieved_context: str compressed: bool response: str def retrieve_node(state): query state[messages][-1][content] docs retriever.invoke(query) return {retrieved_context: format_docs(docs)} def generate_node(state): context state[retrieved_context] history state[messages] response llm.invoke(build_prompt(context, history)) return {response: response.content} graph StateGraph(AgentState) graph.add_node(retrieve, retrieve_node) graph.add_node(generate, generate_node) graph.add_edge(retrieve, generate) graph.set_entry_point(retrieve) graph.add_edge(generate, END) app graph.compile()状态对象里 messages 是当前轮对话上下文retrieved_context 是检索注入内容compressed 标记上下文是否已压缩过。LangGraph 的节点返回值会自动合并到全局状态里下一个节点能直接读取。这比手动维护全局变量的方式安全得多不同请求间不会串数据。我项目里实际用 LangGraph 做过一个有反思机制的研究助手整体流程是规划节点生成几个候选研究方向执行节点对每个方向检索资料反思节点对比收获率并调整方向最后汇总节点写报告。每个节点都在读写同一个状态对象但上下文里存放的内容完全由图结构控制。这种结构化编排是复杂 Agent 的必经之路。3.5 并发场景下上下文隔离的实战配置你搜索AI Agent 怎么扛并发时大概率会看到很多框架层面的讨论但很少有人讲并发场景下上下文怎么隔离。这两个问题的关联很深。FastAPI 有一个天然优势就是异步接口配合依赖注入可以非常优雅地实现上下文隔离。我习惯把每次请求的上下文容器放在一个独立的依赖作用域里from fastapi import FastAPI, Depends from contextlib import asynccontextmanager import uuid app FastAPI() def get_session_id(): return str(uuid.uuid4()) asynccontextmanager async def session_context(session_id: str Depends(get_session_id)): ctx ContextContainer(session_id) try: yield ctx finally: ctx.clear()这个设计有一个非常关键的点每个请求进来都生成独立 session_idContextContainer 只属于这个请求的生命周期。无论你的后台有多少个并发用户A 用户传入的上下文永远不会出现在 B 用户的状态里。我见过很多新手踩这个坑搞了个全局 message_history 变量一个用户请求还没处理完另一个用户的历史就追加进来了模型回答的内容完全错乱。这属于并发场景下最典型的上下文串线事故。用 FastAPI 的依赖注入做请求级上下文隔离是我目前最推荐的方案。4. 常见问题与排查技巧实录4.1 上下文截断导致的灾难这类问题最常见的表现是用户回答在关键位置戛然而止或者开始胡言乱语。我项目里出现过一次严重的上下文截断排查了好几个小时最后发现是 LangChain 的 prompt 模板在拼接时对超长文本做了自动截断截断后 list index 越界程序直接抛异常。我的排查思路是遇到上下文相关异常优先把实际发送给模型的完整 prompt dump 出来肉眼检查 input token 数量是否接近窗口上限。不看实际发送内容你永远猜不到问题出在哪里。预防这个情况最直接的手段是给所有 LLM 调用加统一的 token 预算检查函数进入调用前先估算输入长度超过阈值就走压缩逻辑。不要等到模型报错或者输出异常才反应主动防控的成本远低于事后排查。4.2 Token 超限报错的处理套路模型 API 返回上下文长度超限的报错是最高频的问题。不同厂商的报错信息不同有的明确告诉你超了多少 token有的只给一个通用错误码。遇到这类报错的处理顺序我总结成了几条第一立刻降级处理把最近对话轮数砍半或者去掉检索内容重试一次。第二持久化记录这次报错时的上下文大小事后统计是哪些模块吃掉了太多 token。第三给 Agent 加一层异常重试机制超限报错触发自动压缩后重试而不是直接把错误抛给用户。我项目中的自动重试逻辑长这样for attempt in range(2): try: return llm.invoke(messages) except ContextWindowExceeded: if attempt 0: messages compress_history(messages, llm) else: messages messages[-4:]两次重试分别对应摘要压缩和截断后备方案。这套逻辑上线后用户感知到的因超限导致的任务失败率降了至少一半。4.3 检索上下文质量差的排查方向RAG 场景下上下文质量差往往不是模型问题而是检索链路的问题。我总结了三个最值得排查的方向。第一切分的 chunk 粒度。chunk 太碎片化语义不完整模型理解困难chunk 太长跨段落噪音多向量表示不够精准。我最终的方案是段落级切分加固定窗口兜底优先按语义单元走。第二Embedding 模型和业务文本的匹配度。通用 embedding 在垂直领域表现往往一般。我做过一个测试同一个检索任务通用模型和领域微调模型的召回准确率差很多。如果预算允许用领域语料微调一个 embedding 模型收益非常直接。第三查询的表述方式。用户口语化问题直接检索会差很多。我后来专门加了一步查询改写用大模型将口语问题改写成结构化的检索语句。这一步实测下来检索质量提升明显强烈建议试一试。4.4 Token 统计与成本监控的一天我在上下文工程里最后补上的一个环节是 Token 的可观测性。没有统计就没有优化所有压缩策略的调整都要以数据为准。我实现了一套日志记录组件把每次模型调用的系统提示词长度、对话历史长度、检索内容长度、输出长度全部记录下来按用户会话维度聚合。每天跑一个定时任务统计每个用户的平均上下文成本、高频超限次数、压缩触发次数。这些数据帮我发现了一个很有意思的现象很多用户习惯粘贴大段文档让 Agent 分析单个用户的 token 消耗占了总成本的三成以上。但这类请求往往不需要太长的对话历史对历史轮数的敏感度很低。我针对这类请求做了一个特化配置降低历史保留轮数整体成本立刻降了不少。5. 从上下文工程到 Agent 中台化的思考5.1 把上下文能力沉淀成可复用的服务当你做了好几个 Agent 项目后会发现上下文管理的能力是可以沉淀成中台服务的。我在第二个项目开始前就把上下文容器、压缩策略、检索注入、Token 监控统一封装成一套独立的 SDK。这样做的好处非常明显。第一多个业务线共用一套上下文治理逻辑不同 Agent 表现稳定统一。第二压缩策略和检索策略升级时只需要在 SDK 层面改动所有 Agent 同时生效。第三新同学接手新 Agent 项目时不需要重新理解上下文那套复杂逻辑直接调用 SDK 就行了。封装 SDK 不用过度抽象核心就是把七件事做好会话初始化、历史存储、检索注入、压缩触发、状态管理、Token 统计、故障降级。这七个能力就是中台化的最小完整集合。5.2 我对上下文工程未来演进的一些看法最近深度学习领域关于稀疏注意力、内存机制的研究进展很快下一代模型可能在上下文利用效率上有明显提升。但无论模型怎么变上下文工程的底层思维方式不会变在有限的资源里做取舍把最关键的信息放到最合适的位置。我觉得接下来的方向会逐渐从管理上下文走向设计认知架构。Agent 不再被动接收用户给的上下文而是主动决定自己需要维护哪些长期记忆、哪些短期状态甚至判断什么时候需要遗忘。这个过程很像人类的工作记忆模型核心的信息保留边缘的信息释放。语义缓存也是一个值得关注的方向命中缓存的请求直接返回结果完全不消耗模型推理。在 Agent 中台化的场景里这类缓存能力的 ROI 非常高尤其是高频相似问题场景成本能降到原来的零头。我个人实际做项目有个体会上下文工程没有一劳永逸的方案每个项目都要重新调研和适配但方法论是稳定的。先把窗口预算盘清楚再做取舍再落地状态流最后用数据验证这个流程跑熟了Agent 的稳定性会有一个质的提升。