日期:2026-07-16
我们当时承接的是一个多轮任务型 Agent 的内部落地项目,核心场景是帮某省级国资集团的运营团队处理跨系统的工单流转与数据核对。这个 Agent 不是一问一答的问答机器人,而是要连续完成"读取工单 → 调用多个内部系统接口 → 比对数据 → 生成处置建议 → 回写结果"这类需要十几步交互的任务。
项目一开始我们直接把每轮对话和工具调用结果全量塞进上下文窗口。初期单轮任务没问题,但一旦进入长链路多轮任务,问题很快就暴露出来了:上下文越积越长,轻则超出我们采用的推理框架的窗口上限触发截断,重则无关的历史信息污染了模型对当前步骤的判断。最极端的一次,一个 23 步的工单核对任务,模型在第 17 步把第 4 步已经作废的中间结论又当成有效依据,导致最终结果错了一半。
那次事故之后我们意识到,上下文不是越多越好,得有一套分层的记忆与压缩机制。这篇文章复盘的就是我们把这套机制从想法落地到生产环境的关键路径。
在这个项目里,记忆机制主要覆盖三个场景:
第一个是短期记忆滑窗。Agent 在执行单个多轮任务时,需要保留最近若干步的对话与工具返回,但更早的步骤如果已经收敛成结论,就不需要逐字保留。我们想要的效果是"窗口里永远是最相关的近况"。
第二个是长期记忆向量检索。跨任务、跨会话里那些可复用的背景知识,比如"该集团工单的优先级判定规则""接口字段口径",我们不想每次都让模型重新推断,而是沉淀成可检索的长期记忆,需要时按语义拉取。
第三个是上下文压缩与信息保真。当上下文逼近窗口上限时,需要把历史内容压缩成高密度摘要,同时保证关键实体(订单号、字段名、判定结论)不丢失。这三点串起来,才构成了我们这套记忆系统。
挑战一:短期记忆滑窗与摘要
最直接的做法是固定保留最近 N 轮,但我们发现"轮"不是好的切分单位——有的轮次工具返回很长,有的轮次只是确认。我们改成了按 token 预算做滑动窗口:给短期记忆一个约 4000 token 的硬预算,超出时把最旧的若干条合并成一段滚动摘要。
关键配置如下,我们用 YAML 描述窗口策略:
short_term_memory:
window_token_budget: 4000
keep_recent_rounds: 6
summarize_when_overflow: true
summary_prompt: "将以下历史交互压缩为要点,必须保留所有订单号、字段名与最终结论"
summary_model: "local-small-7b"这里有个工程细节:摘要模型我们刻意用了本地部署的小模型(约 7B 参数)而不是主模型,原因是摘要本身是高频操作,每次都走大模型会显著抬高单轮成本。另外 summary_prompt 里强制要求保留实体,这是后面保真度的第一道关卡。
挑战二:长期记忆向量检索召回
长期记忆我们用的是我们采用的向量数据库层(pgvector 方案),把沉淀知识切块做 embedding 后近邻检索。初期召回率只有约 62%,排查后发现两个坑:一是切块粒度太粗,一条"工单优先级规则"里混了五六个子规则,检索命中了但模型取不到精确片段;二是 embedding 模型对内部术语(如"核销""直配")区分度差。
我们的解决思路分两层。第一,把知识块按语义子句再切细,单块控制在 200–400 token;第二,检索时做两阶段:先用向量召回 top-20,再用我们采用的 AI 网关编排层做一次轻量重排,按与当前任务的语义相关度筛选出 top-5 注入上下文。
检索调用片段示意:
hits = vector_store.search(
query=current_task_embedding,
top_k=20,
filter={"scope": "ticket_rule", "version": "2025Q2"}
)
reranked = gateway.rerank(hits, query=current_task, top_k=5)
context.inject(reranked)上线后检索命中率从约 62% 提升到约 89%,误召带来的噪声也明显下降。
挑战三:上下文压缩与信息保真
这是最棘手的一环。压缩本质是信息有损的,但任务型 Agent 又要求关键实体零丢失。我们尝试过两种方案:纯摘要压缩和"结构化提取 + 摘要"混合。前者简洁但丢实体,后者稍占 token 但保真。
最终我们采用混合方案:每轮工具返回先过一道结构化抽取,把订单号、字段名、状态、判定结论抽成一张小表,这张表永不以摘要形式压缩,始终原样保留在上下文末尾的"关键事实区";其余叙述性内容才进入滚动摘要。
案例片段(已脱敏): 在一次 23 步工单核对任务中,第 4 步工具返回的作废中间结论原本会被摘要吞掉,改为结构化抽取后,关键事实区始终保留如下记录,使第 17 步正确判断:
[FACT] ticket=TICK-2025-0831 step4_status=已作废(中间态) [FACT] ticket=TICK-2025-0831 final_decision=挂起待复核 [FACT] field_match: order_no==wms_order_no -> true压测数据显示,启用结构化抽取后,长任务(≥15 步)因实体丢失导致的错误率从约 18% 降至约 3%。
这样设计后,压缩发生在"叙述层"而非"事实层",保真度和压缩比得以兼得。我们实测单轮 token 成本因此下降约 41%。
改造前后的核心指标对比如下(数据为脱敏示意值,基于我们采用的推理与网关底座的计量层统计):
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| 多轮任务完成率 | 约 71% | 约 93% | 提升约 22 个百分点 |
| 上下文超窗次数(每千任务) | 约 47 次 | 约 6 次 | 降至约 1/8 |
| 长期记忆检索命中率 | 约 62% | 约 89% | 提升约 27 个百分点 |
| 单轮 token 成本 | 基准 100% | 约 59% | 下降约 41% |
| 长任务实体丢失错误率 | 约 18% | 约 3% | 降至约 1/6 |
案例片段(已脱敏): 某省级国资集团运营团队接入后,典型工单链路从平均 19 步交互收敛到 14 步(无关历史被压缩),网关计量层记录的单日 token 消耗从约 2.3M 降至约 1.4M;一次回归测试中 200 条历史工单重放,任务完成率由 69% 提升至 92%。
第一,记忆要分层,不要全塞窗口。短期滑窗管"近况",长期向量管"复用知识",事实区管"零丢失实体",三层各司其职,比单纯堆窗口长度有效得多。
第二,压缩先保关键实体,再做叙述摘要。把实体抽成结构化事实区永远原样保留,叙述部分才允许有损压缩,这是保真度的核心取舍。
第三,摘要这类高频辅助动作尽量下沉到小模型或本地实例,别让它们占用主模型资源与成本,这是我们单轮成本能降 41% 的关键。
第四,检索召回别只靠向量,加一道轻量重排能显著抬升命中率,尤其对内部术语密集的场景。
Agent 的记忆与压缩,本质上是在"上下文有限"和"任务需要全局视野"之间找平衡。这套分层机制上线半年,已成为我们后续所有任务型 Agent 的默认底座配置。工程上没有什么银弹,但把实体保真作为不可妥协的红线,方向就不会跑偏。