资讯动态

Agent上下文管理:从提示词优化到状态设计的工程实践

发布时间:2026/10/22 14:44:30 来源:尧图企业网站定制
1. 单轮对话的胜利经验是怎么变成Agent路上的坑的1.1 过去提示词万能的错觉是怎么形成的我有很长一段时间坚信大模型应用效果不行九成是指令写得不够好。这个信念不是我拍脑袋得来的而是被实战喂出来的。早几年做文本改写、内容抽取这类功能时同一个模型接口我把提示词从帮忙改一下细化成你是资深编辑请保持原文信息密度压缩到300字以内不改变数字和结论之后输出质量肉眼可见地上了一个台阶。那时候团队里谁遇到模型产出不对第一反应就是找我调prompt。我后来想明白了为什么提示词在那些任务里那么管用。单轮任务的上下文极其简单模型每次看到的输入基本就是任务描述加一条用户输入没有历史包袱没有工具结果没有外部数据。这种情况下提示词承载了几乎全部的智力负担你把它写清楚模型需要的所有约束和知识就都齐了。一个实习生拿到一页纸的操作手册就能把手头的活儿干好因为他只需要面对这一件事。用赌桌来类比更直白单轮任务是你手里只有五张牌你的策略好不好完全取决于这五张牌是什么提示词就是牌面。所以大家拼命研究怎么出牌这没有错。可到了Agent场景牌面突然变成了几十张里面还混着别人打过的废牌这时候你再研究出牌顺序已经不够了先得弄清楚手里到底哪些牌是自己的。1.2 Agent每次调用看到的其实是一锅乱炖刚开始做Agent时我想当然地认为Agent也就是聊天把对话历史传进去再写好system prompt就行。真正上手后我才发现模型每次生成回答时看到的上下文根本不是用户说了一句什么而是一大坨由多个来源拼起来的混合体。常见结构大概是这样的messages [ {role: system, content: 你是客服助手请只回答用户当前问题...}, {role: user, content: 我要查订单A}, {role: assistant, content: None, tool_calls: [...]}, {role: tool, content: {order_id: A123, status: shipped, items: [...]}}, {role: user, content: A现在到哪了}, {role: assistant, content: 订单A已发货预计明天到达...}, {role: user, content: 那B呢}, ]中间那串tool结果就是工具返回的原始数据可能上千个token里面带着订单详情、商品明细、金额、地址。模型在看到那B呢这句话时它的处境像一个员工面前摊着一堆文件夹却没有人告诉他现在要处理的是B文件夹。你可以在system里写一百遍请专注于当前用户问题但上下文中间那段关于订单A的详细JSON实在太醒目了模型很容易被带着走因为它本质上是一个做条件生成的系统上下文里什么占权重高它就更容易顺着什么往下说。这个发现让我意识到提示词只是上下文里的一行文字它要跟其他所有内容竞争模型的注意力。Agent系统里模型看到了什么远比模型被告知了什么重要。1.3 我的第一个翻车现场提示词全对模型照样串台具体说说我第一次翻车吧。当时做一个客服工单Agent负责查订单、处理退换、转人工我给系统提示词写得特别细致角色定义、禁止事项、回答步骤、语气要求一条不缺。上线跑了一周低并发测试看着还行一旦用户多聊几轮问题就出来了。有个印象很深的case用户先问订单A的发货状态模型回答已发货接着用户又问订单B模型回答已签收第三轮用户说那这个能不能退。模型没有明确说这是在问A还是B而是结合上下文里最近出现的高权重信息给出了一个模棱两可的答案把订单B的签收状态和订单A的退款政策混在一起。用户当然不满意。我一开始固执地认为是提示词约束不够连夜加了很多句请严格区分不同订单请先确认用户所指对象。改完上线错误率几乎没动。后来我把每次请求真正发给模型的那一长串内容完整导出自己逐字读了一遍才看清问题第二轮工具返回的订单B完整JSON一直留在上下文里第三轮开始时根本没有被清理也没有任何标记告诉模型这个JSON是历史查询结果不是当前用户提到的事实。模型被这份陈旧但具体的事实带跑了。那次之后我把所有prompt恢复原样只改了上下文构造逻辑——在每次工具调用后给结果做摘要、打上时间标记并用一个结构化字段记录当前查询对象。同样的提示词错误率立刻降下来。也是从那时候起我认定问题不是提示词不行是上下文没做对。2. 上下文没做对的三种典型病污染、缺位、错序2.1 上下文污染旧工具结果成了假事实第一种病是上下文污染。模型对上下文里的文本几乎是一视同仁的它不会自己判断这句话是昨天的参考不是今天的任务。只要一段信息以事实陈述的形式出现在上下文里模型就会倾向于把它当作当前任务的事实来用。这跟我们看会议纪要一样——如果桌面上同时放着昨天和今天的会议记录而且都没有标注日期你很容易拿昨天的决议当天的事。Agent系统里最常见的污染源就是工具结果和外部检索片段。比如客服场景用户先查询了订单A工具返回了一长串JSON这个JSON被原封不动留在messages里。到了下一轮用户的问题明明是关于订单B的但模型眼前仍然飘着订单A的status: shipped这类字眼。它并不会因为你system prompt里写了请基于当前问题回答就自动忽略这些内容。排查污染的办法很笨但很有效把每次请求的完整上下文按来源打上标签然后逐条问自己这条信息对模型回答当前问题有没有必要。没有必要的就是污染。我后来定了一个原则上下文里每一个信息块都必须有存在的理由说不出来理由的就不放。2.2 上下文缺位关键前提被截断模型只能脑补第二种病正好相反该在的信息没在。最常见的是窗口一超就按token长度从后往前截断。很多系统的实现逻辑很简单消息太多就丢掉最旧的那些。可是最早的system指令和用户第一轮说的任务目标往往是整个对话的前提。你把前提扔了后面所有话就变成了无根之木。举个例子。用户第一轮说我要对比三款手机分别叫A、B、C请从续航、拍照、价格三个维度分析接下来几轮用户简说第一个怎么样那另一个呢最后一个呢。如果第一轮因为token超限被截掉了模型看着那另一个呢根本不知道另一个是什么但它不会承认自己不知道它会在当前上下文里找最像的实体然后编一个答案出来。这就是Agent里脑补式幻觉的重要来源——不是模型天生爱编是它真的缺少回答问题所需的前提又没人允许它说我不知道。除了截断还有一种缺位很隐蔽工具结果虽然放回了上下文但对应的查询参数没有一起放回。模型看到一份查询结果却不知道这个结果是针对哪个实体查出来的下一轮需要指代时就只能瞎猜。正确的做法是让上下文里永远保留这个结果的查询条件是什么。2.3 上下文错序信息都有但位置不对注意力就是看不见第三种病是错序。信息都在上下文里但摆放的位置不对模型就是看不见。大模型的注意力在长文本上并不是均匀分布的开头和结尾附近的区域权重往往更高中间一大段很容易被稀释。这不是我拍脑袋说的你随便找个长文档任务跑一跑就能感觉到核心指令放在开头比埋在正文中间有效得多。我自己就踩过这个坑。做某知识库Agent的时候我喜欢把检索回来的资料直接append到messages末尾想着离得近模型记得住。结果发现模型经常被末尾那段长资料带跑把参考资料当成用户指令或者被资料里的语气影响输出风格。后来我把检索结果用明确的参考资料块包裹起来放在任务指令之后、当前用户问题之前的固定位置效果立刻改善。错序还有一种表现是互相矛盾的信息并存。比如system里写了只能用中文回答结果某个嵌入的上下文片段里有一句Please reply in English。这种矛盾同时存在时模型往往会被后面的覆盖因为后出现的信息在局部范围里更新鲜。所以上下文不只是一个顺序问题更是一个结构问题你需要像布置工作台一样把当前任务要用的材料放在看得见的位置把与任务无关的杂物清走。这三种病我用一张表总结过病症典型表现后果排查方法污染旧工具结果、旧检索片段长期留在上下文模型把历史当现状回答串台按来源打标签逐条质问必要性缺位任务前提被截断、查询参数没有回流模型脑补缺失信息产生幻觉列出当前问题的最小事实集逐一核对错序核心指令被埋在中间、参考资料混入用户消息模型看不见关键约束注意力被带偏固定信息块位置用标签做结构3. 把上下文当状态管理分区、结构化与生命周期3.1 上下文分区固定区、工作区、历史区各司其职想通了问题之后我开始把上下文当成一套有结构的状态系统来设计而不是简单的一堆消息记录。我现在构造上下文的逻辑是三个区。固定区放系统级指令、全局安全约束、用户身份事实。这部分内容稳定几乎不随对话轮次变化token占比很小却决定了整个Agent的行为边界。工作区放当前任务的所有相关事实包括当前用户问题的解析结果、本次任务涉及的实体、最近真正必要的几轮对话、工具结果的有效摘要。历史区放过往对话的压缩记忆或检索条目一般不直接堆原始消息等需要时再拉进来。关键认知在这里工作区的目标不是完整记录历史而是为当前决策凑齐最小充分事实集。这跟人工作一模一样你不可能在开会的每个瞬间都在脑内完整回放所有相关会议你只会拿起与当前议题相关的那份材料。我后来用代码层面的ContextBuilder来保证这一点结构类似这样class ContextBuilder: def __init__(self, system_prompt, global_constraints): self.fixed [ {role: system, content: system_prompt}, {role: system, content: global_constraints}, ] self.history_store ... self.work_state {} def build_messages(self, current_input): working [] if self.work_state: working.append({ role: system, content: f[当前状态] {self.format_state(self.work_state)} }) if current_entity in self.work_state: working.append({ role: user, content: f[当前问题] {current_input}请围绕当前对象 {self.work_state[current_entity]} 回答 }) else: working.append({role: user, content: current_input}) history self.retrieve_relevant_history(current_input) return self.fixed history working核心思路是把当前状态显式地放在工作区开头让模型在生成回答前先看到现在处理的对象是谁、进行到哪一步了。这比它自己从历史消息里慢慢推算要可靠得多。3.2 状态显性化让模型少推理、多认事实用户说话天然充满省略和指代。一个人说我要买那个蓝色的不对变成两个吧如果没有额外的状态信息模型很难知道那个蓝色的到底是什么。如果上下文里恰好有一段更早的商品列表模型可能猜中也可能猜错。我的做法是不要依赖模型从对话历史里反推状态而是由工程侧把状态显式维护好然后喂给模型认。比如购物车Agent每次调用前先维护一份结构化状态{ current_entity: 蓝色保温杯, stage: 确认数量, cart_item_count: 2, total_price: 199.8 }然后把它格式化成【当前任务状态】放在工作区。模型看到这个东西就不需要费劲去历史里找线索了。它只要认这个字段再结合用户最新的变成两个吧就能直接更新数量。状态显性化还有个额外好处token消耗大幅下降。一段结构化字段比一大段自然语言历史紧凑得多。但要注意一点不要天真地让模型在每轮输出完整状态JSON然后再喂回去这样容易出现自说自话的循环状态越滚越乱。更稳的姿势是模型只负责解析用户意图和修改局部字段状态本身的存储与合并由代码控制。3.3 工具结果回流不是原始数据而是摘要标记工具调用的结果怎么放回上下文是Agent工程里最容易被忽视的细节之一。很多系统的做法是直接把API响应原封不动塞进messages这等于把一仓库的货物搬到谈判桌上还指望谈判代表能找到正确的报价单。我现在给工具结果回流定了一套规则。工具端在返回完整数据时同时生成一个摘要字段调用侧拿到完整数据后如果超过一定阈值就再用轻量处理把它压缩成3到5行关键事实。放回上下文之前给这个摘要加上明确的标记并且绑定对应的查询参数。还是拿订单查询举例。完整API响应可能有几十个字段但我放回上下文的只有{ query: order_id: A123, summary: 订单A123已发货含手机x1金额3999预计明日到达, queried_at: 2025-01-12 10:23:00 }然后格式化成一行的工具结果摘要放进消息流。这些摘要必须明确标注这是工具结果不是用户指令并且带上查询时间和查询参数。旧的结果在回答完当前轮之后立即退出工作区归档到日志里需要时再按条件召回。这套流程执行之后模型串台的频率肉眼可见地降低。3.4 信息生命周期什么时候该进来什么时候该退场上下文不是档案室是工作台。工作台上只留正在用的工具和正在处理的材料用完了就该归位。这是我后来反复跟团队讲的一句话。我做了张表来定义不同类型信息的生命周期信息类型进入时机保留策略退出条件全局约束会话创建时全程保留会话结束用户当前意图每轮重新解析本任务期间保留任务完成转为档案工具结果摘要工具调用返回后保留到当前轮回答完成回答完成后删除或归档历史对话记忆不再支持当前任务时压缩成摘要存外部不再需要时归档用户身份事实会话创建时固定保留随更新同步用户信息变更这套生命周期模型帮我解决了一个大问题什么时候清空上下文、什么时候归档、什么时候恢复。以前我总觉得上下文越多越好生怕丢信息现在我知道保留信息的关键不是多少而是秩序。该退场的信息不退场就是污染该召回的没召回就是缺位。4. 窗口不够用压缩与召回的正确姿势4.1 直接截断为什么是最差的兜底方案上下文窗口再大也有上限。窗口不够用了怎么办很多人第一反应是那我把旧消息丢掉保留最近的N条。这个方案最省事但也最伤人因为它完全不区分信息的价值密度只按时间远近决定去留。问题在于被丢掉的最早消息往往不是无关紧要的寒暄而是任务的前提。我在一个多轮整理报告的任务里测试过用户第一轮给出完整的目标和步骤后面几十轮都是围绕这个目标的具体操作。当上下文一超过限额系统自动丢掉了第一轮模型后续立刻像失忆一样频繁反问你需要我做什么甚至开始自拟一个目标继续执行。这比让它知道完整目标再回答糟糕得多。所以我的原则是至少保证任务启动块永不截断。这个块包括system指令、首轮用户目标、当前用户问题。其他内容可以被压缩、被归档但这三样东西必须时刻存在。如果这三个都不够放那要考虑的不是怎么截断而是这个任务是不是根本不需要那么长的历史。4.2 摘要压缩要防止事实漂移有些团队选择用模型把长对话历史压缩成一段摘要这比简单截断聪明但同样有坑我管它叫事实漂移。摘要天生就会损失精确信息数字被四舍五入订单编号被省略用户原话里的否定关系被翻转。我见过最典型的一次事故原文是用户说我不喜欢红色包装其他都可以第一轮摘要被写成用户对包装有偏好第二轮再压缩的时候模型直接理解成用户偏好红色包装整个方向都反了。对话越长、压缩次数越多这种变形就越严重。我的对策是用结构化模板压缩而不是让模型写自由段落。强制摘要生成几个固定字段实体列表、决策点、未完成任务、关键数字、排除条件。这些字段逐项对应原始信息逐项校验。摘要只当作索引关键不可变事实ID、金额、日期、否定条件必须保留原始片段。压缩做完之后还要回读一遍把摘要和原文的关键点对一遍发现不一致宁可留着原文也别冒险用摘要。4.3 按需召回不把整本历史塞回去只捞当前需要的上下文窗口有限但对话总长度是无限的。解决这个矛盾的正道不是无限压缩而是按需召回把历史交给外部存储每一轮根据当前问题只检索出相关片段再注入工作区。举例来说一个客服Agent的用户昨天聊了二十轮关于订单A的问题今天回来问昨天那个订单现在到哪了。模型并不需要昨天完整的二十轮它只需要知道昨天用户提到的订单是哪一单、当时的处理进度是什么、用户要求过什么东西。通过实体抽取拿到昨天订单的查询条件从存储里把这两条关键信息捞出来就够回答了。但按需召回有代价检索可能漏掉关键信息。如果召回结果里没有用户昨天明确说过必须今天发货这个条件模型就不知道今天要优先处理。所以必须做兜底召回结果带上来源标记system里明确写没有查到相关材料就直接说明不要编造关键场景用关键词和语义双路检索提高命中率。召回的本质是把无限记忆转成有限上下文的一座桥但它要和上下文分区配合着用否则召回来的信息照样可能变成污染。4.4 分层记忆配置长会话Agent的最终上下文形态把前面这些思路串起来我最后在一套长会话Agent里落地了分层记忆配置层级内容存储位置使用方式L1 常驻内存全局约束、用户身份、当前状态摘要固定字段每轮必带token约2kL2 工作空间当前任务事实、最近必要轮次、工具结果摘要动态构建每轮重算token约5-8kL3 短期历史按天/按任务压缩的摘要外部存储按需召回注入工作区L4 长期档案原始会话全量日志数据库/文件不上送模型仅供审计这套配置下模型每次看到的上下文是固定区当前状态必要的最近几轮针对性召回的摘要。token压力可控上下文整齐干净出了问题也好定位。窗口不够用的时候我优先调整L2让它再精简而不是去动L1和L3的边界。5. 从调提示词到设计上下文一套可以照抄的工程清单5.1 先把模型看到了什么变成可观测的日志如果你问我做Agent最应该先做哪件事我第一反应是把上下文日志系统搭起来。没有上下文日志所有排查都像是蒙着眼睛修电路。我现在团队里定了一条规矩没有上下文日志的Agent改动不算是改动。具体要记录的东西包括每次请求的完整messages快照、每个分区的来源与token数、工具结果摘要与原始数据的对应关系、每次压缩动作的触发条件和结果。日志不需要花哨关键是要能复现——用户出了一个问题我能按时间倒推看到那一次模型请求时上下文的完整面貌。很多神秘的模型失误在日志面前都会变成哦原来是这里放了旧数据原来是这里截断了。我自己的习惯是给每条上下文日志加上一个简单的来源标记比如[system]、[global_state]、[tool_summary]、[history_retrieved]。这样复盘的时候一眼就能看出来某个模型回答是基于哪部分上下文产生的。看着看着你会发现大多数问题根本不需要改提示词。5.2 构建上下文回归剧本防止改一处坏一片上下文策略一旦调整经常出现这个case修好了那个case又坏了的情况。为了不让每次修改变成拆东墙补西墙我建议准备一套固定的回归剧本。剧本不需要很大但一定要覆盖容易出问题的边界场景。我常用的几个固定场景是场景预期表现长对话指代用户说它模型能正确指到当前状态对象多实体切换从订单A切到订单B再切回来模型不串台信息缺位用户信息不足时模型明确询问或说明不编造工具结果冲突两次查询结果不一致模型能识别并追问超长窗口接近token上限任务前提不被丢弃每次改动上下文构造逻辑我就把这套剧本跑一遍记录任务完成率、关键信息错误次数、平均token消耗三个指标。改完之后对比表格指标变好才敢上线。这套方法帮我挡住了好几次看起来变聪明了、实际整体变傻的改动。5.3 review的对象从提示词文本变成上下文构建代码团队协作方式也因为这个认知改变了不少。以前大家review的是提示词原文看句式、看结构、看有没有歧义。现在Review的重点变成了上下文构建代码——因为上下文是代码拼出来的不是文案写出来的文本层面的肉眼检查已经完全不够了。同事A就踩过一个很典型的坑。他把当前查询对象的状态塞在一条user消息里没有加任何结构标签结果模型把这段状态当成了用户的新指令顺着它输出了一通莫名其妙的话。后来改成一个带【当前任务状态】前缀的system消息放在工作区开头问题立刻消失。这种事如果只review提示词文本根本看不出来必须review上下文是怎么构造的。我要求上下文构建代码里必须写清楚每个字段的来源、更新时机、退出条件像画数据流图一样把信息的流动设计明白。哪个字段是用户意图解析出来的、哪个字段是工具结果写入的、哪个字段在什么条件下被清除都要能追到代码行。5.4 我反复用到的几条实操原则最后说几条我自己反复用、每次都能救我一命的原则。能用字段表达的决不靠自然语言描述。状态、实体、约束能结构化就结构化别指望着模型从一大段历史里自己抽取。上下文分区不是可选项是必选项。固定区、工作区、历史区分清楚之后垃圾和真相就不会挤在一起。让每个信息块带上来源和时间标记。这条对排查太重要了没有标记的信息在上下文里就是一个无主之证出了问题你都不知道该找谁。宁可少放不可乱放。放进上下文的每一条信息都必须有存在的理由说不出来理由的就不放。少一点上下文模型反而更清醒。把压缩和截断看成数据管线的算子而不是后处理。这句话的意思是从一开始设计上下文的时候就要考虑信息怎么进、怎么出、怎么归档等窗口爆了再临时处理永远是亡羊补牢。回头想想做Agent这段时间最大的收获不是把提示词写得更漂亮而是把上下文从一个模糊概念变成了一等公民。如果你现在也被Agent的失忆串台问题折磨我的建议很直接先别急着改提示词去把每次请求的完整上下文打印出来一行一行问自己这里的每条信息对模型回答当前问题到底有没有用。可能你会发现问题真的不在提示词而在上下文没做对。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑