☰
Agent上下文管理选型:流水式与汉堡式架构对比及混合方案实践
2026/10/19 5:38:22 网站建设 项目流程

1. 从一次上下文溢出事故说起

去年冬天,我接手了一个内部知识库问答助手的调优工作。上线第一周就出了状况:用户连续追问七八轮之后,Agent 开始答非所问,甚至把三轮之前已经确认过的订单编号又当成新信息重新询问。日志里最扎眼的一行是上下文长度逼近模型窗口上限,系统自动截断了最早的一批消息——而那批消息里恰好包含用户最初设定的约束条件。

这次事故让我彻底意识到,Agent 的上下文管理不是"把历史消息拼起来丢给模型"这么简单。它本质上是一道资源调度题:窗口是有限的,信息是无限的,你得决定什么留下、什么压缩、什么丢弃、什么外置。业内围绕这道题分化出了两条截然不同的路线,我习惯把它们叫做流水式和汉堡式。前者像一条不断向前流淌的河,只保留最近的活水;后者像一层层叠起来的汉堡,把不同性质的信息分层固定。

这篇文章不打算给你一个"标准答案",因为选型从来不是非黑即白。我会把两条路线的底层逻辑、适用边界、实测表现和踩坑经验摊开讲清楚,让你在面对自己的业务场景时能做出有依据的判断。无论你是刚接触 Agent 开发的新手,还是已经被上下文问题折磨过几轮的老手,应该都能从中找到能直接抄作业的部分。

2. 流水式上下文:把对话当成一条不断截断的河

2.1 流水式的核心机制与设计哲学

流水式的思路非常朴素:维护一个先进先出的消息队列,新消息进来,旧消息出去。当累计 token 接近阈值时,从队首开始丢弃,直到腾出足够空间。它假设"最近说的话最重要",历史信息会随着时间自然衰减。

这个假设在很多场景下是成立的。比如闲聊型助手、实时客服的第一轮应答、单轮工具调用,用户关心的是"你现在能不能解决我的问题",而不是"你记不记得我十分钟前说过什么"。流水式的实现成本极低,一个collections.deque加上 token 计数就能跑起来,调试时也容易定位问题——因为上下文里有什么,一眼就能看全。

但它的软肋同样明显。一旦任务需要跨多轮保持约束(比如"全程用中文回答""金额单位统一用万元"),这些早期设定会在某次截断中悄无声息地消失。更麻烦的是,这种消失是静默的,模型不会报错,只会开始"失忆",而你在日志里看到的只是一条正常的请求。

2.2 滑动窗口的三种截断策略对比

同样是流水式,截断策略不同,效果差异很大。我实测过三种常见做法,整理成下面这张表:

策略做法优点缺点适用场景
按条数截断保留最近 N 条消息实现最简单长消息会瞬间吃满窗口消息长度均匀的短对话
按 token 截断累计 token 超限即丢队首窗口利用率高可能把一条长消息截成半句通用场景
按轮次截断保留最近 K 轮问答语义完整,不会截断半句轮次长度差异大时浪费空间多轮问答

我个人的经验是:按轮次截断 + 按 token 兜底的组合最稳。先按轮次保留,如果还是超限,再对最早的那一轮整体丢弃。这样既保证了语义完整,又不会因为某一轮特别长而撑爆窗口。具体实现时,我会给每一轮打一个turn_id,丢弃时以turn_id为单位,而不是以单条消息为单位。

def trim_by_turn(messages, max_tokens, count_tokens): # 按 turn_id 分组,保证一轮问答同进同出 turns = group_by_turn(messages) kept = [] total = 0 for turn in reversed(turns): turn_tokens = sum(count_tokens(m) for m in turn) if total + turn_tokens > max_tokens: break kept.insert(0, turn) total += turn_tokens return flatten(kept)

注意:按轮次截断时,系统提示词(system prompt)必须单独拎出来,永远不参与丢弃。我见过有人把 system prompt 也塞进队列,结果某次截断后模型直接"忘了自己是谁",开始用完全不同的语气回答。

2.3 流水式最容易踩的三个坑

第一个坑是静默失忆。前面提到的约束条件丢失就是典型。我的应对办法是在关键约束上做"锚点标记",比如给 system prompt 里的硬性规则打上pinned: true,截断逻辑跳过所有 pinned 消息。这样即使对话很长,核心规则也不会丢。

第二个坑是工具调用结果被截断。Agent 调用工具后返回的 JSON 往往很长,如果这条结果正好处在队首附近,很容易被整条丢掉,导致模型"忘了自己刚查过什么",然后重复调用同一个工具。我的做法是给工具返回结果做摘要压缩,只保留关键字段,把原始完整结果外置到文件或缓存里,需要时再按 ID 取回。

第三个坑是截断点落在 assistant 消息中间。有些实现按 token 数硬切,会把一条 assistant 的回复切成两半,模型看到半句话会非常困惑。解决办法很简单:截断时以消息为最小单位,宁可少留一条,也不要切半句。

3. 汉堡式上下文:分层固定,各司其职

3.1 汉堡式的分层结构长什么样

汉堡式的核心思想是"不同性质的信息放在不同层,各层有不同的生命周期和更新策略"。一个典型的汉堡结构从上到下大致是:

  • 顶层:系统指令层。包含角色设定、硬性规则、输出格式要求。这一层几乎不变,永远保留。
  • 次层:长期记忆层。包含用户画像、历史偏好、跨会话的稳定事实。通过检索按需注入,不是全量塞入。
  • 中层:任务状态层。包含当前任务的进度、已确认的参数、待办事项。随任务推进动态更新。
  • 底层:近期对话层。包含最近几轮的原始消息,允许被截断。

这个结构的好处是职责清晰。系统指令不会因为对话变长而丢失,长期记忆不会挤占近期对话的空间,任务状态可以精确控制。代价是实现复杂度上去了,你需要为每一层设计独立的存储、更新和注入逻辑。

3.2 长期记忆层的检索注入时机

汉堡式里最容易做砸的是长期记忆层。很多人一上来就把用户所有历史记录全量拼进上下文,结果窗口瞬间被占满,效果还不如流水式。正确的做法是按需检索:只在当前问题确实需要历史信息时才去查,查到后只注入最相关的几条。

判断"是否需要"可以用一个轻量分类器,也可以让模型自己决定。我常用的是一个折中方案:在 system prompt 里告诉模型"你可以调用recall_memory工具来查询历史信息",把是否检索的决策权交给模型。这样既避免了无脑全量注入,又不会因为规则写死而漏掉该查的情况。

检索回来的记忆也要做压缩。原始记忆可能是"用户上周三下午提到过他偏好用表格展示数据",注入时压缩成"偏好:表格展示"。信息密度上去了,窗口占用下来了。

3.3 任务状态层的更新与一致性维护

任务状态层是汉堡式里最考验工程能力的一层。它需要回答三个问题:状态什么时候更新、更新时如何避免冲突、状态过期了怎么办。

我的做法是给任务状态定义一个显式的 schema,比如:

{ "task_id": "order_query_001", "confirmed_params": {"order_id": "A123", "date_range": "last_30d"}, "pending_items": ["确认退款金额", "选择退款方式"], "last_updated": "turn_7" }

每次模型输出后,用一个轻量的解析步骤判断状态是否需要变更,变更时以task_id为锁做串行更新,避免并发写坏。状态过期则通过last_updated和当前轮次的差值来判断,超过阈值就标记为"待确认",下次注入时提醒模型"这个信息可能已过时,请和用户核实"。

提示:任务状态层千万不要让模型自由发挥去写。我早期试过让模型自己维护一个 JSON 状态,结果它经常漏字段、改错键名,甚至编造不存在的参数。后来改成"模型只负责提议变更,由代码校验后落库",稳定性提升了一个档次。

4. 两条路线的正面对撞:一张选型决策表

4.1 六个维度的横向对比

光讲原理不够,选型要看具体指标。我把两条路线在六个关键维度上的表现整理如下:

维度流水式汉堡式
实现复杂度低,几十行代码高,需要多层存储与调度
窗口利用率高,几乎不浪费中,分层有固定开销
长程一致性差,早期信息易丢好,关键信息分层固定
调试难度低,上下文一目了然高,需追踪多层注入
单轮延迟低略高,多一次检索/组装
适用任务长度短对话、单轮任务长任务、多轮复杂流程

这张表不是让你照着打分选高分项,而是帮你识别你的场景最在意哪个维度。如果你的业务是"用户问一句答一句",流水式的低延迟和易调试就是压倒性优势,汉堡式的分层纯属过度设计。反过来,如果你的 Agent 要陪用户走完一个跨几十轮的下单流程,流水式的静默失忆会要了命。

4.2 什么场景该选流水式

我总结了三类明确适合流水式的场景。第一类是实时性优先的问答,比如搜索助手、翻译助手,用户要的是秒回,历史信息价值极低。第二类是单轮工具调用,比如"帮我查一下明天的天气",查完就结束,没有跨轮状态。第三类是原型验证阶段,你还没想清楚业务需要什么记忆,先用流水式跑通主流程,等需求明确了再升级。

这里有个反直觉的点:不是所有长对话都需要汉堡式。如果长对话的每一轮都是独立的(比如用户连续问十个不相关的问题),流水式完全够用,强行上汉堡式只会增加延迟和 bug 面。

4.3 什么场景必须上汉堡式

必须上汉堡式的信号有三个。第一个信号是存在跨轮硬约束,比如"全程用某种格式输出""金额单位统一",这些约束一旦丢失任务就失败。第二个信号是任务有明确的多阶段状态,比如订票流程要经过选航班、填乘客、选座位、支付四个阶段,每个阶段依赖前序阶段的确认结果。第三个信号是需要跨会话记忆,比如用户今天说过的偏好,明天再来时要能想起来。

只要命中其中任意一个,流水式就会在某个时刻让你付出代价。我见过最惨的案例是一个客服 Agent,用户投诉到第三轮时,Agent 把用户第一轮提供的订单号忘了,开始重新索要,用户直接炸了。

5. 混合方案:我实际项目里最常用的折中打法

5.1 分层流水:把汉堡的层用流水的方式管

纯流水和纯汉堡都有极端,实际项目里我用得最多的是分层流水。具体做法是:把上下文分成"固定层"和"流动层"两部分。固定层放 system prompt 和任务状态,永远保留;流动层放近期对话,用流水式截断。这样既拿到了汉堡式的关键信息保护,又保留了流水式的低复杂度。

实现上,固定层和流动层在拼装时是分开的,截断逻辑只作用于流动层。固定层如果太长(比如任务状态膨胀),单独做压缩,不影响流动层的截断节奏。这个方案我在三个不同项目里用过,稳定性明显好于纯流水,复杂度又远低于完整汉堡式。

5.2 关键信息锚定:让该留的永远留

混合方案里最关键的一步是锚定。我会在对话过程中动态识别"关键信息",给它打上锚点,锚点信息不参与截断。识别方式有两种:一种是规则匹配,比如包含订单号、金额、日期这类结构化信息的消息自动锚定;另一种是让模型在回复时显式标注"这条信息很重要,请保留"。

锚定信息要定期清理,否则锚点会越积越多,最后把窗口占满。我的清理策略是:任务结束时清空该任务的所有锚点,任务进行中如果锚点总数超过阈值,就把最老的锚点降级为普通消息,参与正常截断。

5.3 上下文压缩:用摘要换空间

当窗口实在紧张时,压缩是最后的武器。把最早的一批对话交给模型做摘要,用摘要替换原文,能省下大量 token。但摘要有个致命问题:摘要会丢细节,而细节往往正是后面要用的。

我的经验是,摘要只对"已经完成且不会再引用"的对话做。比如任务的第一阶段已经结束,那这个阶段的详细对话可以压缩成一句"第一阶段已完成,确认了 A、B、C 三个参数"。正在进行的阶段绝不压缩,宁可多占点窗口。另外,摘要要保留结构化字段(参数、ID、金额),只压缩自然语言的寒暄和解释部分。

def compress_completed_stage(stage_messages): # 只压缩已完成阶段,保留结构化字段 structured = extract_structured_fields(stage_messages) summary = llm_summarize(stage_messages, keep_fields=structured.keys()) return {"summary": summary, "fields": structured}

6. 实测数据与踩坑复盘

6.1 一次 A/B 测试的真实结果

我在一个多轮订单查询场景里做过一次 A/B 测试,同一批 200 条测试用例,分别跑流水式和混合方案。结果如下:

指标流水式混合方案
任务完成率71%93%
平均轮次6.25.8
约束丢失率24%3%
平均单轮延迟0.8s1.1s

流水式那 24% 的约束丢失率,几乎全部集中在轮次超过 5 轮的用例上。混合方案多出的 0.3 秒延迟,换来 22 个百分点的完成率提升,这笔账怎么算都划算。但要注意,这个结论只对"多轮有状态"的场景成立,换成单轮问答,两者完成率几乎没差别,流水式的低延迟反而更优。

6.2 那些文档里不会写的坑

第一个坑是token 计数不准。不同模型的分词器不一样,你用 A 模型算的 token 数,换到 B 模型可能差出 20%。我的做法是留 15% 的安全余量,并且用目标模型自己的分词器来计数,别图省事用估算公式。

第二个坑是工具返回结果里的隐藏字符。有些工具返回的 JSON 里带大量空白和转义字符,看着不长,实际 token 数惊人。我后来在工具返回后加了一步清洗,去掉多余空白和冗余字段,窗口占用直接降了三成。

第三个坑是并发请求下的状态竞争。同一个用户快速发两条消息,如果两条都触发状态更新,可能互相覆盖。解决办法是给每个会话加锁,或者用消息队列串行处理同一会话的请求。这个坑在压测时才会暴露,上线前一定要模拟并发场景。

6.3 监控指标:怎么知道上下文管理出了问题

上下文管理出问题往往是静默的,所以监控必须到位。我常看的四个指标是:上下文 token 占用的时间序列(看是否持续逼近上限)、约束丢失告警(定期用探针问题测试关键约束是否还在)、工具重复调用率(突然升高说明工具结果被截断了)、任务状态更新失败率(汉堡式专属,看状态层是否健康)。

其中"约束丢失告警"最容易被忽略但最有价值。我的做法是每隔几轮就插入一个探针,比如问模型"请复述一下当前的输出格式要求",如果答错就说明约束丢了,立即触发上下文重建。这个探针本身也占 token,所以频率不能太高,我一般设在每 5 轮一次。

7. 我的选型决策清单

如果你现在正面对一个具体项目,不知道选哪条路线,可以按下面这个清单走一遍:

  1. 任务是否跨多轮且轮次间有依赖?否,选流水式;是,进入下一步。
  2. 是否存在一旦丢失就导致任务失败的硬约束?否,流水式加锚定即可;是,进入下一步。
  3. 任务是否有明确的多阶段状态需要维护?否,分层流水够用;是,上汉堡式。
  4. 是否需要跨会话记忆?是,汉堡式加长期记忆层;否,分层流水即可。
  5. 团队是否有精力维护多层存储和调度?否,先用分层流水顶着,别硬上汉堡式。

这份清单不是教条,而是帮你把"拍脑袋选型"变成"有依据决策"。我见过太多团队一上来就照着最复杂的架构搭,结果维护成本压垮了迭代速度,最后又退回简单方案。选型的本质是匹配当前阶段,而不是追求架构上的先进。

最后分享一个我自己的习惯:无论选哪条路线,我都会在项目初期埋一个"上下文快照"功能,把每次请求实际发给模型的完整上下文存下来。出问题时翻快照,比看任何日志都直观。这个功能开发成本极低,但救过我至少三次命。上下文管理这件事,说到底就是"让该在的信息在,让该走的信息走",两条路线只是实现这个目标的不同手段,理解了这个本质,选型就不再纠结了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询