主题
上下文与记忆治理
一句话:企业 Agent 的上下文不是“把能查到的都塞进去”,而是先按权限、安全和相关性做 Guard,再按五层记忆装配;换来的是不越权和可解释,代价是要维护多层存储、压缩和失效策略。
为什么需要
一个常见的失败路径:为了让 Agent“更懂业务”,把用户历史、知识库检索结果、工具返回和团队记忆全部拼进提示词。结果三个问题同时出现:
- 越权:检索到了用户无权查看的文档,模型在回答里复述出来;提醒“不要泄露”已经晚了。
- 腐化:几十轮后窗口里全是过期结果和重复段落,模型抓不住当前目标,开始引用旧结论。
- 不可解释:回答错了,无法判断是记忆里的事实错了、检索漏了,还是权限过滤把关键证据挡住了。
根因是把“记忆”当成一个桶,把“注入”当成字符串拼接。企业场景里,上下文是一条有权限、有来源、有生命周期的装配流水线。
核心机制
Guard > Memory:先治理,再注入
三个判定节点都写成“能放行吗”,所以右边只有一条通行路径、三条丢弃路径,不存在“守住不守”的歧义。丢弃必须带层名落审计:被安全层挡下的和因为不相关被裁掉的,是两种完全不同的事故线索。
三层 Guard 的顺序不能反:先过滤越权内容,再过滤敏感和注入,最后才判断相关性。反过来先算相关性,会把“很相关但无权看”的内容排到前面,后面再删已经浪费了预算,也容易在日志里留下不该留的内容。
Guard 的判定要尽量确定性:权限来自 ACL 和数据处理,安全检查来自规则和分类器,相关性可以用检索分数加规则。把三层都交给模型判断,既慢又不可解释。
五层记忆
| 层 | 存什么 | 生命周期 | 典型存储 |
|---|---|---|---|
| 轮次记忆 | 当前轮的消息与工具结果 | 单轮 | 进程内 / 短 TTL 缓存 |
| 会话记忆 | 本次会话的历史与已确认事实 | 会话 | Redis |
| 任务记忆 | 任务图、步骤状态、中间结果 | 任务 | 关系库 + 对象存储 |
| 团队记忆 | 团队共享的偏好、术语、决策 | 长期 | 向量库 + 元数据 |
| 跨会话记忆 | 用户长期偏好与稳定事实 | 长期 | 向量库 + 关系库 |
分层的意义是不同层有不同的写入者、读取者和失效策略。轮次记忆可以随请求结束丢弃;会话记忆要支持恢复;任务记忆要能审计和重放;团队与跨会话记忆必须经过策展,不能把每次对话原样写入,否则噪声会随时间累积。
上下文注入管道
- 解析身份与作用域:用户、租户、业务域、数据级别、当前任务。
- 收集候选:会话历史、任务状态、用户记忆、团队记忆、知识检索、工具结果。
- Guard 过滤:权限 → 安全 → 相关性,逐层裁剪。
- 去重与合并:同一事实多来源时保留最新、最权威的一条,记录来源。
- 预算分配:稳定规则、任务状态、证据、历史各占多少,预留输出空间。
- 压缩或引用化:超长结果落盘留摘要,旧历史结构化压缩。
- 编译视图:按“稳定前缀 → 会话能力 → 动态任务”排序,生成 Manifest。
- 校验:目标、未决项、权限和关键证据是否还在;不通过则回到第 3 步。
Manifest 记录每个来源的作用域、版本、可信度、入选原因和 token 数。没有它,回答错了只能猜;有了它,可以回答“模型当时看到了什么、为什么没看到另一份文档”。
压缩:先提交事实,再压叙述
压缩不是删消息,而是一次生命周期:
- Commit:把目标、已完成、阻塞、关键决策、待审批动作写入权威状态。
- Compact:把叙述性历史压成带来源的摘要,原始记录留在日志。
- Rebuild:用摘要、状态和最近消息重建视图。
- Validate:检查目标、权限、证据是否完整;不完整就回到 Commit。
规划状态、权限记录、未决审批和用户置顶约束属于永不压缩的内容。压缩切点不能切断工具调用与结果的配对,否则模型看到的是残缺因果链。所谓“最近”也不只按时间:未解决的错误、待审批动作和验收失败的证据,即使更早也必须保持活动。
记忆写入与策展
长期记忆最大的风险不是“记不住”,而是“记错还一直用”。写入要做四件事:
- 抽取:只保留稳定事实、偏好和决策,不保存寒暄和临时推理。
- 归因:记录来源会话、时间和可信度;冲突时保留双方并标注。
- 去重:同一事实的多次表达合并,保留最新确认版本。
- 过期:给时效性事实设 TTL,过期后降权或标记待确认。
团队记忆还要有 owner 和评审:一条错误的团队规范会被所有 Agent 复用,破坏面远大于单次对话。
隔离
| 维度 | 隔离要求 |
|---|---|
| 租户 | 记忆命名空间物理或逻辑隔离,检索默认不跨租户 |
| 业务域 | 不同业务域的知识与团队记忆不互相召回 |
| 用户 | 跨会话记忆按用户隔离,共享前必须显式授权 |
| 任务 | 任务记忆按 task_id 隔离,子任务只读父任务授权范围内的结果 |
| 环境 | 内网与互联网闭环不共享记忆存储 |
隔离要在存储层和检索层同时做:只靠查询条件过滤,一次漏写就可能跨租户;只在存储层隔离,跨域检索仍可能把数据带出来。
实战场景
场景一:A 租户的记忆进了 B 租户的窗口——先注入后 Guard
现象:SaaS 客服 Agent 由两个租户共用一套平台。一个周末 B 租户管理员投诉 14 次:助手回答里带出了 A 租户的内部项目代号和订单号,法务按跨客户数据泄露评估责任。翻轨迹发现模型不是幻觉,它引用的内容真实存在,且属于另一个租户。
排查:按“排障先看 Manifest,再看模型”定位问题层:
| 排除对象 | 判定证据 | 结论 |
|---|---|---|
| 模型 | 引用内容真实,Manifest 里有来源与入选原因 | 模型只是复述它看到的,不是幻觉 |
| 检索 | 检索请求带租户条件,没有命中跨租户记录 | 召回侧干净,不是检索问题 |
| 装配与写入 | 策展批处理把群聊摘要写进了团队记忆默认命名空间,没有租户字段 | 漏写标签,装配按公开放行 |
故障是两个错误叠加:为了省延迟,权限 Guard 被挪到预算分配和编译视图之后,无标签记忆被当作公开内容通过,进了窗口只剩“不要泄露”的提示,属于事后补救;写入侧漏标签,正是正文说的“一次漏写就可能跨租户”。
对症:三层 Guard 顺序回归“权限 → 安全 → 相关性”,且必须先于预算分配和编译视图;写入侧拒收无标签记忆:
yaml
context:
guards: [permission, safety, relevance] # 在 budget 与编译视图之前执行
memory_write:
require_fields: [tenant, owner, source_session]
reject_if_missing: true # 缺标签的记忆不落库复盘:Guard 挡的是候选,不是已注入内容,“召回后再删”会在窗口和日志里留下痕迹,还浪费预算。隔离要在写入、存储、检索三处同时做;验收回到正文两条:越权内容不出现在 Manifest,跨租户检索返回空。
场景二:压缩把限定条件压掉,结论反转
现象:合同审阅助手在 52 轮会话后触发压缩,摘要丢了“该折扣仅限 2026-06-30 前下单、渠道客户除外”里的限定条件,下一轮直接答复“渠道客户可用”。销售按它报了 3 单,回滚花 2 个工作日逐客户改报价。
排查:用固定任务集回放这条轨迹,对比压缩前后视图:
| 排除对象 | 判定证据 | 结论 |
|---|---|---|
| 检索 | 条款原文在压缩前的 Manifest 里 | 证据进过窗口,不是漏检 |
| 模型 | 把原文放回窗口,同题答对 | 模型读得懂原文 |
| 压缩 | 摘要里限定词消失,Validate 却判通过 | Validate 只查目标、权限、证据,没覆盖约束 |
对症:问题定位在 Commit 到 Compact 之间——限定条件被当作叙述内容一并压掉。做法是先把事实提交进任务记忆,再压叙述;限定词做成结构化字段并进 Validate 断言:
| 修饰词类别 | 压缩处理 | Validate 断言 |
|---|---|---|
| 仅限 / 除外 / 上限 | 结构化字段入任务记忆,按永不压缩处理 | 每条结论必须携带字段引用 |
| 生效日期 / 时效 | 随 TTL 写入,到期降权或标记待确认 | 未确认事实不得作为结论依据 |
| 前置条件 / 待审批 | 归入 never_compress | 压缩后不得缺失 |
分级压缩的顺序不动:先清理无用工具结果,再摘要历史,最后才强制压缩;连续 3 次校验失败熔断(经验值,需校准),回退到未压缩视图并转人工。
复盘:压缩必然有损,损失要落在叙述上而不是约束上;Validate 不过的摘要不是压缩,是毁信息。原始记录必须保留,关键事实先提交。
场景三:长会话成本随轮次近似二次上涨
现象:运维答疑 Agent 平均会话 60 轮以上,会话记忆整段历史逐轮重放,每轮 prompt 近似线性增长、整段会话累计成本近似二次。模型开始引用旧结论、抓不住当前目标,用户中途放弃率上升,一个季度内单会话 prefill 成本翻了一倍。
排查:用 Manifest 里每轮的 token 数分层归因:知识检索入选条数每轮基本稳定,排除检索问题;模型与提示词未变更,排除模型问题;60 轮时每轮 token 约七成被历史重放占据,锁定装配层的预算分配。compact_at: 0.8 其实一直在触发,但更早发生的未解决错误被反复引用,校验步骤通不过,裁剪只动历史尾部,下一轮又涨回来。
对症:用任务记忆做卸载——工具全量结果落盘留摘要,窗口里只留引用;引用化在去重合并与预算分配之间执行,校验步骤按 Manifest 来源指针回读:
yaml
budget:
history: { mode: reference, inline_recent_turns: 6 } # 经验值,需校准
evidence: { spill_at_tokens: 512 } # 经验值,需校准
read_back: 校验缺关键证据 / 用户引用窗口外结论 / 待审批重新激活复盘:“最近”不只按时间——未解决的错误、待审批动作和验收失败的证据即使更早也必须保持活动,回读时机由这三类事件触发,不靠模型自行决定;成本失控优先在存储层解决——轮次记忆随请求丢弃、会话记忆结构化压缩、任务记忆可重放,五层各有卸载策略,先分层再谈换便宜模型。
最小上手
先实现三层:会话、任务、知识,不要一开始做五层。
yaml
context:
identity: { user, tenant, domain, data_level }
guards: [permission, safety, relevance]
budget: { reserve_output: 8000, compact_at: 0.8 }
never_compress: [goal, permissions, pending_approvals]
manifest: [source, scope, version, trust, reason, tokens]
memory:
session: redis
task: postgres
knowledge: vector_store
ttl: { session: 7d, task: 30d }验收看五件事:越权文档不会出现在 Manifest;压缩后模型仍能说出目标和下一步;同一事实冲突时能追到来源;会话恢复后权限没有变化;跨租户检索返回空。
生产动作
- 先过滤再排序:把权限和数据处理放在检索之前,避免“先召回再删除”。
- 记录 Manifest:每轮保存来源、版本、入选原因和 token;排障先看 Manifest,再看模型。
- 分级压缩:先清理无用工具结果,再摘要历史,最后才强制压缩;连续失败要熔断。
- 记忆写入评审:团队记忆和长期事实走评审或抽样复核,避免错误规范扩散。
- 失效策略:制度、价格、权限这类时效性事实必须有 TTL 和版本,过期自动降权。
- 回放测试:用固定任务集回放,比较压缩前后、记忆开关前后的成功率与成本。
边界
- 记忆不是越多越好:长期记忆的噪声会降低检索准确率,策展比容量更重要。
- Guard 不能保证绝对安全:分类器会漏,真正的边界仍在工具权限和执行环境。
- 压缩必然有损:摘要会丢细节,原始记录必须保留,关键事实必须先提交。
- 跨会话记忆有隐私成本:用户可能不希望被长期记住,要有查看、删除和关闭入口。
- 小规模系统不需要五层:会话加知识两层就能跑,等出现团队共享和长期偏好需求再拆。
讲清楚
企业上下文治理的顺序是 Guard 先于 Memory:先按权限、安全和相关性裁剪候选,再按预算装配;否则“先召回再提醒模型别泄漏”只是事后补救。记忆分五层,是为了让不同生命周期的事实各有存储和失效策略,而不是把所有东西塞进一个向量库。压缩要先提交事实再压叙述,规划、权限和待审批动作永不压缩。判断这套机制是否可靠,看三件事:Manifest 能否解释模型看到了什么,越权内容是否在检索前就被挡住,压缩后任务是否还能继续。
适用:2026 年企业级 Agent 上下文与记忆治理 | 初稿 2026-10