
我一直觉得大模型项目做到后期拼的往往不是模型选得多好、Prompt写得多花哨而是上下文工程做得到不到位。很多人用LangChain搭应用最开始只顾着把链子搭起来跑通一个demo就兴高采烈结果一上真实场景就露馅回答跑偏、记忆混乱、Token成本飙升、偶尔还会报错说超了上下文长度。这些问题七成以上都出在上下文管理上。所以这篇LangChain系列的实战分享我想专门把上下文工程这件事拆开揉碎聊一聊。它不是什么高深莫测的玄学而是实打实的方法论加工具组合。我写下这些既是给自己做个沉淀也希望给正在用LangChain做AI应用、特别是做Agent方向的朋友一些可以直接照抄的参考资料。无论你是刚入门LangChain的新手还是已经在调教AI下地干活的开发者这个主题都值得仔细过一遍。1. 上下文工程的核心认知——为什么说上下文决定上限1.1 上下文工程到底在解决什么问题先说一个我的直观理解大模型本质上是一个瞬间记忆极强的对话者但它自己不会主动判断该记什么、不该记什么。你给它什么上下文它就基于什么作答。上下文工程就是一套系统地决定给模型看什么、不看什么、按什么顺序给它看的方法。很多人以为上下文工程就是把能塞的都塞进Prompt里这个想法坑了不少人。模型虽然有几十万Token的窗口但你把三天前的所有聊天记录、十几份参考资料统统塞进去它并不会因此变得更聪明反而会被噪音干扰注意力被稀释回答质量直线下降。上下文工程的本质不是做加法而是做减法、排序和动态调度——在正确的时机把最关键的信息以最清晰的结构送到模型面前。LangChain之所以在这个领域特别值得聊是因为它本身提供了大量现成的组件专门用来处理上下文生命周期从PromptTemplate做模板化注入到ConversationBufferMemory做短期记忆再到各种文档加载器、检索器做外部知识的筛选和注入。理解整条链路上的上下文流转比记住某个类的API参数重要得多。1.2 上下文工程的三个核心维度我在实际项目中一般会把上下文工程拆成三个维度来设计。第一个维度是窗口管理。不管模型窗口多大Token容量总是有限的成本和延迟也随Token数上升。窗口管理解决的是怎么在有限空间里放下最重要信息的问题。第二个维度是记忆设计。对话类应用最麻烦的就是失忆。用户上一轮说过的偏好、之前确认过的事实、中途改变的需求模型默认都会忘记。记忆设计决定了哪些信息要跨轮保留、保留多久、存在哪里。第三个维度是动态检索与注入。项目做大了以后知识库、文档、历史会话的体积会非常庞大不可能全都塞进上下文。这时候要借助向量检索、摘要、压缩等手段把最相关的片段在运行时抽取出来动态地注入到Prompt里。这三个维度不是割裂的。做一个真实项目时你往往是同时设计这三条线再通过LangChain把它们串成一个完整的上下文流。我下面几个部分就按这三个维度展开最后再给一个综合实战案例。2. 上下文窗口管理从能放多少到该放什么2.1 窗口大小的误区与Token成本意识先说一个很多人踩过的坑选模型时只看窗口大小觉得窗口越大越好。是的大窗口给了你更多操作空间但空间大不等于你可以乱堆东西。我们必须先建立Token的成本意识。市面上主流模型的计费都按Token来你每往上下文里多塞1000个Token不仅成本多一分每次请求的响应时间也会肉眼可见地增加。更关键的是当无关内容占满窗口时模型对真正重要信息的注意力会被摊薄专业说法叫迷失在中间——因为注意力机制的特性模型通常对开头和结尾的内容最敏感对中段内容容易遗忘。在我做过的一个客服知识库项目里最开始我把一份完整的操作手册大约2万Token直接塞给模型让它对应用户问题作答。结果模型经常引用错章节甚至编造出手册里不存在的内容。后来换了策略先做检索再注入每次只放相关片段约1500到2000Token准确率反而大幅提升成本也降了一半多。2.2 LangChain里的窗口裁剪与Token计量LangChain提供了get_num_tokens这一类工具来预估Token消耗底层调的是tiktoken但它给出的估算值只是一个参考不同模型家族的实际分词规则略有差异。我建议在正式环境里不要卡着最大Token数去设计留出20%到30%的余量否则一旦遇到多轮对话中用户输入变长很容易触顶报错。我自己常用一个简单的三层裁剪策略第一层设定硬上限。按照模型窗口的70%设定一个阈值任何时刻注入的上下文总量不能超过这个数。第二层按优先级排序。外部资料排在检索结果范围内按相关度从高到低放历史对话只保留最近几轮加摘要系统指令永远放最前。第三层动态压缩。如果上述内容加在一起还是超限就触发摘要压缩逻辑把较早的对话浓缩成几句话。在LangChain里ConversationSummaryBufferMemory就是用类似思路设计的组件它综合了缓冲和摘要两种策略超过阈值之后自动触发摘要。我后面第三节会细讲。2.3 系统指令的位置和权重窗口管理还有一个容易忽略的细节系统指令放在哪。前面提到模型对上下文首尾的关注度最高所以系统指令要放到Prompt最开头核心业务规则尽量靠前检索到的支撑性资料可以放中间偏后一点当前轮的用户问题放最末尾。这样安排是顺应注意力机制的特点让最重要的指令占据首尾的高注意力区域。我有一次调试一个Agent应用发现模型老是忽略我设定的必须先查数据库再回答的规则。排查了半天发现是因为我把这条规则埋在了大段业务文档的中后段。把规则移到系统指令的第一段之后问题立刻消失了。别小看这个位置调整很多看似诡异的模型行为根因就在上下文的排版顺序上。3. 记忆系统设计让AI会话不失忆3.1 短期记忆与长期记忆的边界记忆设计是最能拉开项目质量差距的地方。先理清概念短期记忆和长期记忆不是用同一个组件就能搞定的。短期记忆指的是当前会话窗口里需要持续保留的信息比如这轮对话中用户提过的偏好、确认过的选项、刚给过的约束条件。在LangChain里这类记忆往往通过ConversationBufferMemory、ConversationBufferWindowMemory这类内存组件来实现。长期记忆则是跨会话持久化的信息比如用户上次留下的偏好设置、历史订单信息、某个项目的长期背景。自建应用时长期记忆一般要落到外部存储数据库、向量库、Redis之类在下一次会话开始时通过检索灌入上下文。很多新手容易犯的错误是把所有历史一股脑塞进BufferMemory。短期记忆组件只是帮你把当前轮对话暂存起来并不负责筛选。如果一场对话持续了三十轮BufferMemory产出的历史文本就能轻松超过上下文上限。这时候要么限制窗口轮数要么做摘要压缩要么把部分信息转存到长期记忆。没有一套策略干到底的银弹只能按场景取舍。3.2 LangChain记忆组件的取舍与实测LangChain的记忆组件有好几个我挑几个常用的讲下适用场景。ConversationBufferMemory最简单完整保存每一轮问答。它适合对话轮次少、要求高保真的场景比如短会话的售前咨询。缺点也很明显对话一长会话历史体量急剧膨胀非常容易超Token限制。ConversationBufferWindowMemory只保留最近K轮。它的思路是太早的话不用记适合闲聊类、指令类应用。但要注意K设小了用户中途提过的关键信息会丢失K设大了成本又上去了。一般K取5到8轮比较均衡。我实际用得最多的是ConversationSummaryMemory。它把历史对话做摘要再存下来但是会把很多细节丢掉所以适用于需要保留主线信息而不是逐字记录的场景比如项目复盘助手、长周期协助类Agent。还有ConversationSummaryBufferMemory结合了Buffer和Summary两种策略在Token阈值内保留原始对话超过阈值就触发摘要。这算是短期记忆里比较省心的方案也是一般项目起步时的推荐选择。还要多说一句LangChain的记忆组件在较新版本里经历了不小的变动ConversationBufferMemory这类老组件有些已经标记为遗留状态官方更推荐用BaseChatMessageHistory配合RunnableWithMessageHistory或者干脆做一套自定义的存储方案。我个人建议是如果你搞清楚了上述每种记忆策略的取舍用哪个类其实只是技术细节换接口的成本很低。3.3 用外部存储搭建真正可用的长期记忆真正能支撑生产环境的长期记忆大多得自己搭。LangChain官方的RedisChatMessageHistory、PostgresChatMessageHistory都提供了接入方式方便把历史会话落到数据库里。但这里有个关键点持久化只是第一步更重要的是检索策略。我做一个企业知识助手的时候设计了一套标签向量双通道的长期记忆方案。用户的关键信息比如偏好简明回答关注成本控制被打上标签存到结构化字段里每次会话启动时初始化Prompt会把这些高优先级标签注入系统指令同时历史会话的语义向量存在向量库里当用户提到相关内容时通过ConversationalRetrievalChain检索出历史片段动态注入。这样设计的好处是高频强约束信息常驻上下文占空间很小低频弱相关信息按需检索出来不占常驻空间。既保证了模型不会忘记用户的核心偏好又避免了历史越积越大导致上下文爆炸。4. 提示词模板与上下文构建让每一粒Token都花在刀刃上4.1 PromptTemplate里的变量注入与格式陷阱LangChain的PromptTemplate是上下文工程里最不起眼却最常用的工具。很多人觉得它就是个字符串模板但实际上它承担的任务是把动态信息稳定、结构清晰地注入到Prompt指定位置。模板设计有一条核心原则结构与信息分离。固定的系统指令、业务规则、输出格式说明写成模板里的常量部分用户问题、检索结果、历史摘要、动态参数通过变量注入。这样一来不同来源的信息各归其位后续做裁剪和格式微调时也不用手忙脚乱。我举个具体例子一个多轮对话的模板结构大概长这样system: 你是企业内部IT支持助手。请严格遵循以下规则 1. 答案优先引用知识库内容 2. 如果知识库无相关内容明确说明未找到对应资料 3. 回答不超过200字 context: {retrieved_docs} history: {conversation_history} user: {current_question}这里面retrieved_docs来自检索器conversation_history来自记忆模块current_question是当前用户输入。模板把三块信息放在不同区域既方便模型区分信息来源也方便我们在注入前对每一块分别做裁剪。如果这三块内容挤在一起没有一个清晰边界模型就容易把检索到的资料误当成用户的话或者把历史对话当成当前指令。4.2 上下文压缩别做搬运工要做精炼师除了模板注入上下文工程里一个高阶技巧是上下文压缩。LangChain里有ContextualCompressionRetriever它的思路挺有意思先用常规检索器拉回一堆候选文档再用一个小模型通常是LLM对候选内容做压缩和提炼只保留和当前问题真正相关的句子。这个方法在知识库问答里特别实用。比如一个关于产品故障排查的问题检索器可能拉回五篇文档共3000Token但真正和故障A相关的只有三句话。直接把3000Token塞进去不仅浪费窗口还会让模型被其他故障的排查步骤干扰。用了压缩检索器之后可能只保留200Token的核心内容效果却反而更精准。不过这里也有个代价每轮检索都要额外调用一次小模型做压缩延迟和成本都会上升。所以我会建议对性能敏感的场景慎用或者把压缩逻辑放到后台异步完成、缓存结果。对于离线分析、管理后台这类对延迟不敏感的场景可以放心用。4.3 结构化输出与上下文引导的联动上下文工程还有一个很容易被漏掉的应用用上下文引导模型输出结构化内容。LangChain的StructuredOutputParser或较新版本中的with_structured_output就是为了解决这个痛点。它会在Prompt里注入目标格式的说明并要求模型以JSON等格式返回。看起来这只是输出解析问题但它和上下文设计强相关。因为解析器注入的格式说明会占用Prompt空间,而你自己的业务指令、示例数据同样要占空间。格式说明写得过于冗长反而会把业务信息的比重挤下去。我的经验是给模型做少样本示例时例子不要多两三个足够格式说明尽量精简把必须输出JSON和字段含义写清楚就行其余的交给模型发挥。你注入的上下文越能直击任务本质模型的输出就越稳定。5. 实操一个完整的多轮客服Agent上下文链路实现5.1 场景设计与整体上下文流理论讲了半天我分享一个自己做过并持续调优的项目面向企业IT部门的内部客服Agent用一个FastAPI服务承载底层用LangChain组织逻辑会话状态用LangGraph管理。用户可以在网页上直接提问比如打印机连不上公司Wi-Fi怎么办申请软件的流程是什么。这个场景对上下文工程的要求有几个特点需要多轮对话澄清问题细节需要参考知识库的具体章节还需要在回答中带上当前用户的部门、权限等级等信息。如果这些信息组织不好模型很可能给出一个通用但不贴合实际的回答。我设计的上下文流是这样的会话启动时从用户系统拉取身份信息部门、角色、历史工单数组装进系统指令开头。这部分是常驻信息占据约300Token。每轮用户提问时先做问题改写把它坏了这种指代不明的说法结合历史改成完整问题然后用改写后的问题去知识库做向量检索取Top3相关片段。每个片段限制在500Token以内。历史会话处理当前会话最近5轮对话保留原文更早的对话由摘要模块压缩成一段背景说明。组装Prompt最后按系统指令身份信息知识库片段历史摘要最近对话当前问题的顺序拼装并对总Token做检查超限时按优先级砍掉知识库冗余片段。5.2 关键代码链路与参数调优在LangChain里这个链路可以组织成这样一个简化版本我隐去了部分工程细节from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_community.chat_message_histories import RedisChatMessageHistory from langchain_core.output_parsers import StrOutputParser prompt ChatPromptTemplate.from_messages([ (system, 你是IT支持助手。用户身份信息\n{department}\n{role}\n必须遵循{rules}), (system, 以下是检索到的知识库内容\n{docs}), MessagesPlaceholder(variable_namehistory), (human, {input}), ]) chain prompt | model | StrOutputParser() chain_with_history RunnableWithMessageHistory( chain, lambda session_id: RedisChatMessageHistory(session_id, redis_urlredis://localhost:6379/0), input_messages_keyinput, history_messages_keyhistory, )这里有个值得注意的地方MessagesPlaceholder是LangChain里处理聊天历史的标准接口它会自动把历史消息列表插入到指定位置。所以构建history时我们要做的是保证历史消息类型正确——用户消息用HumanMessage助手回复用AIMessage中间的系统消息不要混进来否则模型对消息角色的判断会乱掉。至于参数调优我踩过几次坑后总结出三个关键点一是temperature客服场景取0到0.2比较稳高了容易发挥过头二是知识库检索的top_k我这边取3最均衡取多了后被压缩的难度增大取少了则经常搜不准三是历史窗口轮数综合成本和准确率最近5轮原文加历史摘要的模式最好。这些参数在不同项目里最优值不一样但调整方向是一致的——先保证上下文干净再去调模型能力。5.3 LangGraph会话状态里的上下文管理如果项目只是简单的问一句、答一句上面的链路已经够用。但做Agent类应用时比如让模型自主决定要调用知识库、要查数据库、要发起一个工单问题就复杂了。这种场景我用LangGraph来管理状态因为它的节点之间天然存在状态传递而上下文就藏在这份状态里。LangGraph的一个核心概念是State它会在各个节点之间传递。我的做法是把系统指令、用户身份、检索结果、历史摘要都放进State的字段里每个节点读取自己需要的部分、更新自己负责的部分。比如检索节点只负责往State里写docs字段回答节点从State里组装好Prompt再调模型。这里要特别提醒一个LangGraph的坑默认的State模式是覆盖写如果一个节点返回的字段覆盖了其他节点刚写好的值后面节点就看不到被覆盖的数据了。所以涉及多节点协作时要么用add_messages这类合并操作符要么让每个节点只返回自己独有的键。我第一次搭多节点Agent时就在这里栽过排查了半天才发现是State覆盖导致上下文丢失。6. 常见问题与故障排查实录6.1 上下文漂移导致回答越来越偏最典型的问题对话进行到第10轮之后模型开始重复之前已经回答过的问题或者突然忘记用户最开始提的需求。这种就是上下文漂移通常是因为历史信息在窗口里被挤占或顺序错乱。排查思路分三步。第一步先打印出当前Prompt完整内容看看历史消息是否完整、顺序是否颠倒第二步检查历史消息的role是否正确有没有把用户消息和助手消息位置搞反第三步检查记忆策略如果用的是窗口记忆确认K值是否小到把关键信息丢掉了。我见过一个很隐蔽的案例某项目用ConversationSummaryBufferMemory摘要触发阈值设得太低导致早期信息被过早压缩成了摘要。模型读到的是用户之前问了某个问题已解决但具体错误代码、设备型号全在原始内容里一压缩就没了。这其实是摘要策略和事实保真之间的矛盾。我的建议是涉及具体编号、版本号、金额这类硬信息的场景不要轻易用摘要压缩宁可多占一点Token也要保留原文。6.2 Token超限报错信息与应急降级另一个高频问题是模型报maximum context length exceeded。报这个错通常不是单纯内容太多而是你某一环的注入逻辑没控制住。常见原因有长文档检索结果直接原样塞入、历史对话无限累积、系统指令里拼入了大段重复数据。我的应急处理流程是首先检查Prompt各段Token占比确认哪一段是罪魁祸首可以在代码里按段打点算Token其次给各段设置独立的长度上限比如检索结果硬上限1500Token历史摘要硬上限500Token最后在链路最外层加一个兜底逻辑——如果总Token还是超限就把检索结果按相关度截断到只剩最相关的一段宁可让答案不完美也不要让请求直接报错。另外缓存也是降本增效的好手段。把同样问题同样上下文的问答结果缓存起来不仅能省费用还能避免重复命中同样的超限问题。我在项目里会给每个会话做一个内容哈希命中缓存直接返回实测能省三成左右的Token消耗。6.3 上下文里的敏感信息泄漏风险最后必须聊一个很多人忽略的问题上下文里塞了什么东西模型就能看到什么东西。如果你把用户身份证号、内部系统密码、未公开的业务数据放进Prompt里那这些信息在每次请求时都会被发送给模型服务商。这在企业内部应用里是个大忌。我的做法是三层过滤第一层入参清洗用户输入里敏感字段先脱敏再入Prompt第二层检索结果过滤给知识库文档打上密级标签高密级文档不出现在低权限用户的检索结果里第三层输出检查在返回用户前对模型输出做一次关键词匹配发现敏感模式就拦截重写。上下文工程不只是怎么放得下、放得准还要管住什么不能放。7. 我自己踩过几次坑之后的几点心得写到这里标题里上下文工程这件事能聊的基本都过了一遍。最后分享几个比较个人化的体会也算给这篇做个收尾。第一上下文工程没有一劳永逸的配置。不同模型、不同场景、不同数据规模最优策略都不一样。我现在的习惯是每次改动都做AB对比把Prompt分段的Token占比、首轮回答准确率、超限报错率这些指标记录下来用数据说话而不是靠感觉调参。第二先设计好上下文流再动手写代码。很多人包括以前的我自己是一上来就写chain prompt | model跑通了再考虑记忆、检索、压缩这些事结果后面改起来牵一发动全身。其实花半天时间把系统指令放哪、历史怎么存、检索结果怎么切、超限怎么办画清楚后面能省好几天的返工。第三上下文工程和Agent是天然绑定的。LangChain发展到现在Agent相关的组件比如LangGraph越来越多地接管了调度这件事而任何调度最终都离不开上下文的状态传递。如果你想做的项目是那种AI真的要下地干活——不是简单问答而是让模型自己决定调用什么工具、按什么顺序执行——那上下文工程就是你绕不开的基本功。先把这条基本功练扎实再上复杂Agent架构你会顺手很多。这篇文章讲的都是我自己在实际项目里验证过的做法不一定放之四海而皆准但方向肯定是通的。如果你在跑LangChain项目时也遇到过上下文方面的疑难杂症欢迎顺着这些思路去排查很多时候问题就藏在Prompt的某一段角落里。