写在前面
第一篇聊了意图识别——把用户想干嘛搞清楚,第二篇聊了规划、纠偏与执行循环——通过 Plan 五要素、TAO 状态循环和 Replan 机制让 Agent 一步步受控地走到目标。但做了一段时间后我发现,这两件事再做好,如果上下文管理没跟上,Agent 照样会翻车。
说实话,刚开始做 Agent 的时候,我对上下文的理解就是”把历史对话塞进 Prompt”。结果真正上线才发现,这想法跟”把 SQL 写在 Controller 里”一样天真。用户的硬约束被摘要丢了、工具返回的中间结果过期了还在用、长任务跑到第十轮已经忘了第一轮的目标——这些问题表面上各不相同,但根子上都是同一件事:上下文管理没做好。
第一篇里我提过,意图识别决定了”往上下文窗口里塞什么内容”——不同意图需要的上下文不一样。但当时只是一笔带过,真正做的时候才发现,”塞什么”这个问题远比我想象的复杂。这篇文章是”AI Agent 系统设计”系列的第三篇,把自己在上下文管理上踩过的坑整理下来。Context 抽象、窗口编排、摘要压缩、细节召回、用户画像、长任务漂移——这些东西看起来零散,但其实是一条线:怎么在有限的窗口里,让模型看到它真正需要的信息。
从一个让我头疼的场景说起
我那个技术选型 Agent 跑了一段时间之后,遇到了一个特别诡异的问题。
用户在第一轮说”运维人手不多,别推荐运维搞不定的方案”,Agent 记住了,推荐了 Redis Sentinel,用户很满意。然后到了第五轮,用户问”还有没有其他选择”,Agent 推荐了 Redis Cluster。
用户当场就炸了:”我第一轮就说了运维搞不定 Cluster,你又推?”
我去查了一下原因,发现第一轮的对话内容已经被滑动窗口挤出去了,摘要里只保留了”用户关注运维成本”,但”不能推荐 Redis Cluster”这个硬约束在摘要过程中被压缩成了软约束级别的描述。
更坑的是,这还不是个例。我还遇到过:
- 工具返回的 QPS 数据是三分钟前查的,但 Agent 在第十轮还在用,实际上流量已经翻倍了
- 用户在第三轮确认了”就用方案 A”,到第七轮 Agent 又开始推荐方案 B,因为”方案 A”这个关键事实只存在对话历史里,被摘要丢了
- 长任务跑到一半,Agent 已经忘了用户的预算是 5000 块,推荐了一个 8000 的方案
这些问题表面上各不相同,但本质上都是同一件事:Agent 没有持续、准确地维护执行所需的上下文。第二篇里我聊的 TAO 状态循环,核心就是靠 Fact State 单独维护关键事实来防幻觉——但 Fact State 本身也是 Context 的一部分,如果 Fact State 的管理策略不对(比如该标的 Available 没标、过期的数据还在用),TAO 循环照样会出问题。
先搞清楚两个概念
刚开始做 Agent 的时候,我把”上下文”当成了一个笼统的概念。后来才发现,Context 和 Context Window 是两个完全不同的东西。
Context 是 Agent 当前可获得的全部信息空间。数据来源分四块:
| 来源 | 说明 | 示例 |
|---|---|---|
| 用户输入 | 用户直接提供的数据 | 文本输入、上传的文件 |
| Agent 自身产生 | 提示词、大模型输入输出、中间结果 | 对话历史、Plan 状态 |
| 内部数据 | 来自业务系统的数据 | 通过接口或直接访问 MySQL/ES |
| 外部数据 | 与外部系统交互获得的数据 | 外部 API、MCP 工具调用 |
同一份数据可能在多个地方存着——用户画像 MySQL 里一份,Redis 里缓存一份。从时效角度,有只作用于当前轮的,也有作用于整个多轮对话的。
Context Window 是从全部 Context 中经过筛选、压缩、排序之后,真正交给模型的工作集。Context 是仓库,Context Window 是当前工作台。
用公式表达就是 Context Window = f(Context)。这个 f 就是 Agent 工程的核心——找准内容,放进窗口,传递给大模型。
把 f 展开来看,其实是五步流水线:
1 | Window = Inject(Order(Compress(Select(Retrieve(Context))))) |
- Retrieve:从全部 Context 中找出可能相关的内容
- Select:选择当前真正需要的
- Compress:压缩到合适的粒度
- Order:决定排列顺序
- Inject:以合适的结构注入 Prompt
这五步的每一步都可能出问题,后面会逐一展开。
从功能角度,Context 还可以分成六类:
| 分类 | 说明 | 示例 |
|---|---|---|
| Instruction Context | 指令上下文 | System Prompt、业务 Spec、行业标准 |
| Goal Context | 目标上下文 | 用户最终目标、当前子目标 |
| User Context | 用户上下文 | 用户画像、历史决策 |
| Task Context | 任务上下文 | 当前任务、执行步骤 |
| Tool Context | 工具上下文 | 工具输入、输出 |
| Constraint Context | 约束上下文 | 硬约束、软约束 |
这个分类在设计 Context Item 的存储和检索策略时很有用——不同类别的上下文,生命周期、更新频率、权威性都不一样。
另外一个我觉得比较重要的认知:Memory 只是 Context 的一个子集。所谓的”短期记忆”、”长期记忆”,本质上就是 Agent 运行过程中产生的数据,被拟人化地包装了一下。从工程角度看,所有东西都是 Context,区别只在于来源、生命周期和存储方式。搞清楚这一点,后面的设计思路会清晰很多。
Context Item:一种统一抽象
我在实践中给每个 Context Item 做了一个统一的结构:
1 | { |
几个关键字段:
- source:信息从哪来(用户、系统、工具、模型推测)
- scope:有效范围(当前步骤、当前任务、当前会话、全局)
- authority:权威程度,用于冲突仲裁。参考排序:系统规则 > 用户明确输入 > 工具事实 > 已确认摘要 > 模型推测
- confidence:可信度
- expires_at:过期时间。用户偏好有效期长,工具查询的 QPS 数据有效期短
这里有个细节:要区分观点和事实。用户说”Redis Cluster 太复杂了”,这是观点,要尊重并给予重要权重,无须分辨对错。用户说”我们的 QPS 是 5 万”,这是事实,可以在后续阶段尝试验证。
设计每个 Context Item 的时候,我都会问自己三个问题:
- 怎么获得:这个信息从哪来?用户画像怎么提取?硬约束怎么识别?
- 怎么存储:要不要持久化?放 Redis 还是 MySQL?生命周期多长?
- 怎么查询:用的时候怎么找回来?关键词检索还是向量召回?
这三个问题看着简单,但很多 Agent 项目只考虑了”怎么获得”,存储和查询随便搞搞,后面维护起来就是噩梦。
最朴素也最实用的:滑动窗口
说到上下文管理,最基础的方案就是滑动窗口——窗口只保留最近 N 轮对话,旧的逐渐被挤出去。
优点太多了:实现简单、性能稳定、近期上下文通常最相关、容易控制 Token 成本。在实践中我一般无脑先用滑动窗口,后面再根据效果迭代。
但基础滑动窗口有几个问题:
问题一:最近的上下文不一定是最重要的。 用户在第 3 轮说了一个关键约束,到第 50 轮早就被挤出去了。我上面那个”Redis Cluster”的坑就是这么来的。
问题二:按轮次还是按 Token? 我倾向于按 Token 控制,因为每轮对话长度差异很大——用户可能粘贴了一大段日志,也可能只打了一句话。不管哪种方式,都要限制每轮用户最大输入和模型最大输出——N 轮就是 N × (X+Y),不然一个用户粘贴了一篇论文进来,窗口直接爆了。
问题三:窗口内不一定要放纯对话。 关键点、中间产物、工具返回的核心字段也可以放进去。滑动窗口只是”保留最近内容”的策略,具体内容可以灵活组织。
问题四:窗口不要占满。 我的习惯是总窗口 128K 的话,滑动窗口限制在 32K,剩下的留给摘要、提示词、输出。
变种:混合策略
一个变种思路是把窗口分成两块:
- 近 N 轮对话:放原文
- 更早的对话:用户输入放原文,模型输出放摘要
核心洞察是用户输入通常很短,模型输出一般很长。保留用户输入原文成本不高,但模型输出做摘要可以省下大量 Token。
不过如果用户粘贴了一大段堆栈,那用户输入也不能原文保留了——要灵活处理。
分层上下文:把复杂问题拆开
滑动窗口在复杂 Agent 里很快力不从心。这时候需要分层上下文,核心思想是分而治之。
我在实践中用过的几种分层方式:
按生命周期分
| 层级 | 主要内容 | 生命周期 |
|---|---|---|
| 工作上下文 | 当前输入、当前步骤、工具结果 | 一次或几次模型调用 |
| 会话上下文 | 当前对话目标、最近历史 | 当前会话 |
| 长期上下文 | 用户画像、长期偏好 | 跨会话 |
| 归档上下文 | 完整历史、原始记录 | 长期保存,按需召回 |
按权威性分
| 层级 | 使用策略 |
|---|---|
| 强规则层 | 系统规则、安全约束,不允许被低层覆盖 |
| 已确认事实层 | 用户确认、权威系统查询结果,可作为决策依据 |
| 有证据推断层 | 基于多个证据推导的结论,需保留证据 |
| 待确认假设层 | 模型推测、临时假设,不得当成事实 |
| 已否定层 | 用户否认、验证失败的信息,禁止继续使用 |
这个分层方式我觉得特别实用——能有效避免模型把推测当事实、把已否定的信息继续拿来用。第一篇里聊的硬约束/软约束分层,第二篇里聊的 Fact State 状态标注(Available/Verified/Missing),其实都是这个思路的具体体现。
按冷热程度分
| 层级 | 描述 |
|---|---|
| 热 Context | 当前任务状态、当前 Goal,频繁使用,放内存/Redis |
| 温 Context | 当前会话早期内容,偶尔使用,需要检索 |
| 冷 Context | 完整历史消息,低频使用,主要用于归档 |
这些分层方式可以混用。比如先按冷热分,然后在热 Context 里再按权威性分。关键是结合自己的业务特点来设计。
还有两种分层方式值得了解:
按作用范围分:步骤级(当前工具调用参数)→ 阶段级(信息收集阶段收集到的内容)→ 任务级(任务目标、成功标准)→ 会话级(最近讨论的话题)→ 用户级(长期画像)→ 系统级(全局规范、权限策略)。这种分层方式在设计 Context 的存储位置时很有用——步骤级放内存,用户级放 Redis,系统级可能直接硬编码在 Prompt 里。
按信息形态分:原始层(完整原文)→ 结构化事实层(抽取后的稳定事实)→ 摘要层(保留整体语义)→ 索引层(只保存”去哪里找”)。这种分层方式和压缩策略直接对应——窗口充足用原始层,紧张了就逐层降级。
窗口动态编排:每次放什么、放多少
分层之后,下一个问题是:每次调用大模型的时候,各层的内容怎么放?
筛选
不是所有 Context 都需要每次放进去。不同步骤需要的内容不同,全部丢过去既浪费 Token 又影响效果。”弱水三千,只取一瓢。”
三种筛选机制:
- 强规则筛选:Prompt 模板/代码层面直接决定。比如我在 Go 里用模板渲染 Prompt,
{{ .UserProfile }}{{ .Input }}这种,代码层面就决定了放什么。 - 检索召回:向量数据库/ES 查找相关内容,参考 RAG 机制。
- 模型筛选:先调用轻量模型判断需要什么,再发起真正调用。成本高,不太常用。
压缩
压缩不是静态机制,而是根据实时情况动态调整的。核心原则:越重要的内容越不能压缩,剩余窗口越大越不需要压缩。
我用过一种多版本预压缩策略:同一份信息提前准备好多个版本,运行时按需加载:
| 层级 | 内容 | 适用场景 |
|---|---|---|
| L0 | 一句话摘要 | 窗口几乎放满 |
| L1 | 关键结论摘要 | 窗口比较满 |
| L2 | 结构化事实 | 窗口放了一些内容 |
| L3 | 详细摘要 | 窗口还挺大 |
| L4 | 完整原文 | 窗口非常充足 |
好处是运行时没有额外压缩开销。
还有两个细节值得注意:
级联压缩:两个强相关的 Context Item(比如用户输入和对应的工具返回)要保持同一压缩级别——一个压到 L0 另一个还是 L4,信息就对不上了。反过来,两个有信息重合的 Item(比如对话历史和关键事实),可以让一个极度压缩,另一个尽量原文,避免重复占用窗口。
兜底机制:在 Prompt 里加一句兜底条款——“如果发现某个内容压缩程度太高,导致无法完成当前任务,可以调用工具召回更详细的版本”。这样即使压缩过头了,模型也有自救的能力。
排序
大模型对内容顺序敏感——同样的内容,排序不同,输出也会不同。我的排序原则是越重要越靠前:
- System Prompt / 业务 Spec
- 当前 Goal
- 硬约束
- 当前任务状态 / Plan 节点
- 已确认关键事实
- 当前步骤需要的证据
- 工具返回结果
- 候选 Action / Tool
- 动态 few-shot
- 历史摘要
- 补充背景
但要注意缓存友好性——不常变动的内容可以往后放,更容易命中 Prompt Prefix Cache。这里解释一下:很多推理服务(比如 OpenAI、Anthropic)支持 Prompt 前缀缓存——如果连续两次调用的 Prompt 前缀部分相同,第二次可以直接复用第一次的计算结果,省掉大量推理开销。类比 MySQL 的前缀索引,排序的时候把不常变的内容(System Prompt、用户画像)放前面,经常变的内容(当前步骤、工具结果)放后面,可以大幅提高缓存命中率。
档位设计
不管窗口多紧张,有几类内容是必须原样放的:
- 最新用户输入(不然模型不知道用户这次要什么)
- 硬约束(丢了就踩雷)
- 已确认关键事实(决策依据)
- Plan 和执行状态(不然不知道跑到哪了)
这几类不管怎么压缩都不能动,其他的才可以根据档位做不同程度的压缩。
我在实践中总结了一个基于剩余窗口大小的档位设计:
| 档位 | 剩余窗口 | 策略 |
|---|---|---|
| Full Mode | 80%-100% | 基本不压缩,原文直接放 |
| Balanced Mode | 20%-80% | 部分压缩,Agent 应主要在此运行 |
| Compact Mode | 5%-20% | 大部分压缩,只保留关键原文 |
| Minimal Mode | 0%-5% | 极度紧张,应尽快解决 |
运行时根据窗口占用情况自动切换档位,比每次都硬算每个 Item 该压缩到什么程度要高效得多。而且 Balanced Mode 应该是 Agent 长时间运行的主状态——如果经常掉到 Compact Mode,说明窗口太小或者上下文管理策略有问题。
还可以结合更多因素
不只是剩余窗口大小,还可以结合:
- 当前步骤:不同步骤需要的 Context Item 不同
- 任务风险等级:高风险任务应尽量用大窗口模型,避免压缩带来的信息损耗
- 信息可恢复性:可恢复的先不放索引就行,不可恢复的优先放原文
- 信息可信度:优先保留高可信信息,压缩或丢弃低可信信息
- 信息新鲜度:新旧冲突时新信息通常更重要(但用户画像这种老信息可能是例外)
- 工具调用需求:需要选工具就放工具描述,但工具描述压缩要慎用——我体感压缩会严重影响工具选择准确率
把这些因素综合起来,我整理了一张决策表,实际开发中经常参考:
| Context 类型 | 窗口充足 | 窗口中等 | 窗口紧张 | 极度紧张 |
|---|---|---|---|---|
| 当前用户输入 | 原文 | 原文 | 原文 | 原文 |
| Goal | 完整结构化 | 完整结构化 | 简化结构化 | 一句话 |
| 硬约束 | 完整原样 | 完整原样 | 完整原样 | 完整原样 |
| 当前 Plan | 完整 Plan | 当前节点+依赖 | 当前节点 | 当前节点一句话 |
| 关键事实 | 完整 | 完整 | Top-K | Top-3 |
| 历史对话 | 详细摘要 | 结构化摘要 | 一句话+索引 | 只放索引 |
| 用户画像 | 相关画像完整 | 任务相关画像 | 关键画像 | 默认不放 |
| 工具结果 | 结构化+原文片段 | 核心字段 | 必要字段 | 最小字段 |
| 检索结果 | Top-K 片段 | Top-3 | Top-1 | 只放证据索引 |
| few-shot | 3-5 个 | 1-2 个 | 默认不放 | 不放 |
| 工具描述 | 完整描述 | 核心描述 | 名称+边界 | 名称+一句话 |
注意看硬约束那一行——不管窗口多紧张,硬约束都是完整原样。这是我踩坑之后定的铁律。
摘要与细节召回:一对双生子
你只要做了摘要,就一定有信息损失,后续就需要能把关键细节找回来的机制。这两个东西是绑定在一起的。
摘要的几种类型
我在实践中主要针对这几种场景做摘要:
对话整体摘要:总结已达成一致的结论、讨论进度、用户信息。建议长期保存,不要归档。如果对话中用户切换过多个不相关意图,应在摘要里分意图存储。
章节摘要:长对话中,整体摘要太精简、原文太长。每 N 轮生成一个章节摘要是折中方案——比整体摘要细节多,比完整对话占窗口少。
模型输出摘要:大模型回复里充满了对模型来说”无意义”的铺垫和转场。把这些去掉,只保留高信息密度的内容,可以大幅节省 Token。
关键事实:会影响后续决策、约束校验的信息。这是最重要的一类,必须结构化保存,不能只靠摘要。关键事实包括目标类(用户改了意图)、硬约束、软偏好、用户确认过的事实、决策类(做了什么决策以及为什么)、实体信息。第二篇里我聊的 Fact State 就是关键事实的结构化存储——每个事实标 Available/Missing/Verified,有来源、有置信度。但当时只说了”怎么用”,这篇补上”怎么管”。
中间结果保存:部分工具输出在后续步骤中会反复用到——比如查了一次数据库拿到的用户订单列表,后面分析、推荐、总结都要用。这种要考虑提前准备好摘要版本和细节召回索引。但要注意时效性,大部分实时查询结果只能用于当轮对话。
摘要什么时候生成?
摘要生成有两种时机:
- 同步生成:对话进行到第 K×N 轮时立刻生成。优点是确定性高,缺点是增加当轮延迟
- 异步生成:对话结束后、下一轮开始前、或者定时扫描触发。优点是不影响当轮延迟,缺点是可能在需要时还没生成好
我的建议是用异步——延迟对用户体验影响更大,而且异步可以用 WaitGroup 或 Semaphore 做并发控制,确保需要时已经生成完毕。
我有一个猜想:在关键事实提取做得好的情况下,大部分时候不需要召回细节。保守点说,至少关键的细节都在你提取出来的内容里面。
模型可读摘要
这个技巧是我摸索出来的:压缩后的内容不是给人看的,而是给模型看的。
比如大模型输出了一段”这是一个很好的问题。你其实已经意识到了上下文压缩最关键的一点……”,压缩后变成:
1 | 上下文压缩目标:降 token + 保留关键事实;必须保留:目标/约束/用户确认事实/中间产物 |
读起来不像人话,但大模型完全能理解——它有很强的语义补全能力,只要保留关键实体和关系就能继续推理。就好比老外说中文颠三倒四,但关键字在你就能脑补。
不过要注意,”不能”、”必须”、”不超过”、”大概”、”可能”这些承载约束或不确定性的词,绝对不能随便删。我参考的 Prompt 要求:
- 删除礼貌表达、转场句、口语填充词、重复解释、情绪价值表达
- 保留目标、实体、关键事实、硬约束、软偏好、用户确认/否认、数字、时间、决策、理由、风险、缺失信息、证据来源
- 可以用短句、列表、键值对,不要求自然语言流畅
- 不得改变原意,不得把不确定信息改成确定事实
- 不得把建议改成已执行决策,不得把模型推测改成用户确认事实
- 硬约束和用户否认内容必须显式保留
细节召回
什么样的细节值得召回?判定标准就一个:这个细节是否会影响当前决策。
具体来说:
- 影响目标的细节(用户到底要优化什么?)
- 影响路径选择的细节(方案 A 已经失败过了,不能再用)
- 影响工具调用的细节(某个工具之前调用失败了)
- 影响最终表达的细节(用户说”简单点”)
技术方案基础做法是用 ES 或向量数据库。一个关键原则是:当前 K 轮对话的细节不走召回,直接原文传递。K 轮之前的内容才走检索,规避数据同步延迟。比如 K=10,最近 10 轮原文保留,10 轮之前的内容即便每轮花 10 秒同步,查询时都已经过去 100 秒了,足够完成入库。
还有一种迭代式细节召回的思路:先只传递内容的索引给大模型,模型判断需要哪些细节,通过工具召回。召回后再次调用模型,判断是否需要更多细节。循环直到满足或达到最大迭代次数。这个方案在实践中要慎用(延迟和成本都高),但面试的时候大胆用——它展示了你对细节召回的深入理解。
冲突裁决
召回的细节可能有冲突。我的裁决套路:
- last win:最新的为主。召回了 500 块和 300 块两条预算,500 比较新,取 500。
- 权威优先:系统规则 > 用户明确输入 > 工具事实。
- 区分”冲突”还是”强化”:部门规定每周一次周会,你们小组每周两次,这不叫冲突叫强化。大模型很难处理好这个问题,只能在 Prompt 里尽可能给出区分规则。
一个暴论
只要上下文窗口有限,细节就不可能 100% 不丢。 摘要的本质是筛选,筛选就意味着信息损失。成熟的 Agent 不是幻想记住一切,而是通过关键事实提取、细节召回、冲突裁决这些机制,确保真正影响决策的细节尽量不丢。
用户画像:Agent 的杀手锏
如果要我选一个 Agent 最容易做不好、但又最能拉开差距的部分,我会选用户画像。
大模型的能力大家都能调用,拉不开差距。但只有你的 Agent 知道这个用户究竟想要什么、讨厌什么、能理解什么。这就是私有资产。
传统画像远远不够
很多人提到用户画像想到的还是”男、28岁、一线城市”。这种画像做推荐勉强能用,但在 Agent 里远远不够——用户对 Agent 的期望是”像真人”。
我来举几个反例,都是我日常用各种 Agent 遇到的:
- 永远在重复问:今天告诉了 Agent 我的偏好,明天它又忘了,还得再说一遍
- 明知道不喜欢还推:明确说了不要某个品牌,下次还是推
- 看起来正确实际驴唇不对马嘴:用户是实习生,Agent 改简历改成了高级工程师的样子
- 输出风格完全不匹配:用户是理性风格,Agent 写得花里胡哨
- 偶然行为当稳定偏好:看了一次折叠屏手机,就被标记为折叠屏爱好者
应该建模的维度
目标画像:用户想达成什么。学英语的目标是雅思 7.5 还是日常交流,训练计划完全不同。
能力画像:决定了 Agent 讲多深、用什么语言。用户在计算机上是大师,金融上是小白,那技术内容可以深入,理财内容要简单入门。
偏好画像:不喜欢什么比喜欢什么更重要。用户不喜欢的品牌你反复推荐,比喜欢的品牌你没推荐,更容易让用户反感。而且”不喜欢”和硬约束有强相关性——用户多次表达不喜欢某个品牌,其实隐含了一个硬约束。
决策画像:用户怎么做选择。有人先看价格,超出预算直接排除;有人先看品牌,不熟悉的直接不看。Agent 只会罗列优缺点而不知道用户怎么权衡,就无法帮用户做真正的决策。
关系画像:在社交、客服、企业协作类 Agent 里尤其重要。给上司写邮件和给同事写邮件,语气完全不同。
画像从哪来?
| 来源 | 说明 | 注意事项 |
|---|---|---|
| 用户明确表达 | 置信度最高 | 区分长期偏好和当前状态。用户说”回答简单点”可能只是当前着急 |
| 行为推断 | 从用户行为中推断 | 做了 ≠ 喜欢。买低价手机可能是买给父母的 |
| 多轮对话提取 | 分析对话过程 | 重点看”为什么”而不只是”做了什么” |
| 外部系统接入 | 接入已有画像 | 可能需要中间转换 |
核心:所有画像都只是一定程度上可信,并非完全可信。
画像影响 Agent 的哪些环节?
用户画像不是只影响最终输出,它可以影响 Agent 的任何环节:
- 用户输入理解:同一个词对不同用户含义不同。”性价比”对价格敏感型用户是”便宜”,对品质导向型用户是”物有所值”
- 意图识别和 Slot Filling:上一篇聊过的,用户画像能帮助模型更准确地理解意图
- 上下文选择:不同步骤需要不同的画像信息
- Plan 和路径选择:不同画像的用户,最优路径可能完全不同
- 最终输出:写作风格、详细程度、专业深度
提取时机也很关键
| 时机 | 说明 | 适用场景 |
|---|---|---|
| 实时提取 | 每轮对话结束后立即判断 | 生成当前画像,不适合修改长期画像 |
| 对话结束后提取 | 一次对话/任务结束后统一提取 | 可以看到完整对话过程 |
| 离线提取 | 定期分析近期行为和对话 | 发现趋势、合并重复、判断稳定性 |
最佳实践是实时 + 离线混合:实时保证及时性,离线保证准确性。实时提取的画像先进候选层,离线分析确认后再升级到长期画像。
画像冲突怎么裁决?
画像之间也会冲突。我的裁决原则:
- 当前明确表达 > 历史推断:用户这次明确说了,比你推断的历史偏好更优先
- 用户明确表达 > 行为推断:用户说”我不喜欢”,比你从购买记录推断的更可信
- 用户纠正优先:用户说”你理解错了”,立即修正,不要争辩
- 领域画像 > 全局画像:用户在数码产品上是专家,在金融上是小白,不能用全局画像一刀切
层级设计
我维护三层用户画像:
- 当前画像:本次对话中的即时偏好(用户这次说喜欢小米)
- 候选画像:有待验证的新信号(用户最近三次都提到小米)
- 长期画像:沉淀的稳定用户特征(用户长期偏好苹果生态)
三者是覆盖关系,后者覆盖前者。长期偏好一般是候选画像慢慢升级上来的。
怎么判断不是一时兴起?
这是最难的问题。用户今天说喜欢折叠屏手机,你能不能写入长期画像?
我总结了一个 4C 模型:
- Count(出现次数):跨独立对话的反复验证才有意义。同一轮说五次不等于五次独立对话
- Continuity(持续时间):跨 3 个月比同一天出现 5 次更可信。我参考心理学理论——激情保持约 90 天,超过 90 天还喜欢就是真喜欢
- Cross-context(跨场景一致性):买手机看重轻便、买电脑也看重轻便,才是真的喜欢轻便
- Confirmation(用户确认):直接问用户最可靠。”我注意到你最近几次选设备都重视便携性,以后默认优先考虑轻便?”
四个维度加权求和,得分越高越稳定。我在实践中还给用户确认加了更高权重——用户明确说出来的,就是比推断的更可信。
具体怎么用这个分数?我参考了一个分级标准:
| 分数 | 状态 | 使用方式 |
|---|---|---|
| 0-29 | candidate | 仅保留,不影响推荐结果 |
| 30-50 | weak | 只能用于排序参考,不能作为过滤条件 |
| 50-70 | active | 可影响推荐,但不能作为硬过滤 |
| 70-85 | stable | 可作为默认偏好 |
| 85+ | confirmed | 稳定使用,但仍服从用户当前输入 |
举个例子:用户最近三次都提到小米(Count=3, Continuity=2 周, Cross-context=1),没有明确确认——大概 35 分,candidate 级别,可以作为排序参考但不能默认推荐小米。如果用户明确说”以后默认小米”,直接跳到 confirmed。
生命周期控制
画像不是提取了就一成不变:
- 激活:经过多次验证,可作为默认偏好
- 弱化:长期无新证据或出现相反行为(用户原本喜欢苹果,最近开始看小米)
- 过期:超过有效期,默认不再使用
每三个月没有新证据则降级可信度。淘汰不是淘汰整个画像——用户画像分理财和写代码两部分,如果很长时间没咨询理财,只淘汰理财部分。
一个实战案例:电商场景
如果做电商 Agent,用户画像建议分三层:
- 全局画像:跨品类的用户特征(价格敏感度、品牌忠诚度)
- 品类画像:最重要的一层。用户买手机看重拍照,买电脑看重轻便,不同品类偏好完全不同
- 当前购物画像:当前任务的即时偏好(这次想买什么价位的)
价格偏好有个高级设计:通过离线计算,遍历用户历史订单,计算购买时价格在该品类的百分位数。没买过的品类,可以叠加相似类目参考(没买过裤子,参考上衣的价格偏好)。
还有两个坑要注意:购买者和使用者可能不是同一个人——买低价手机可能是买给父母的,不能据此判断用户偏好低价。浏览记录和购物车不等于满意——用户加了购物车但没买,可能是因为价格太贵,要分析”为什么不满意”。
长任务:上下文管理的终极考验
任务越长,Agent 越容易忘记目标、忽略约束、混淆状态。长任务表面上考验 Plan 和工具调用,实际上考验的是上下文管理。
这里有个认知我觉得很重要:长任务的核心难点不是步骤多,而是不断产生新信息,且信息之间的依赖关系复杂。第 5 步的结论可能依赖第 2 步的工具返回,第 8 步的约束可能推翻第 3 步的假设。信息越多、依赖越复杂,Agent 越容易出错。而大模型自身能力不足也会影响,但这不是我们开发者能直接改变的——我们能做的,就是把上下文管理做好。
六种漂移
我在实践中遇到的典型问题:
| 漂移类型 | 原因 | 示例 |
|---|---|---|
| 目标漂移 | Goal Context 管理不善 | 让修空指针,结果重构了几十个文件 |
| 约束漂移 | Constraint Context 管理不善 | 说不能虚构,结果擅自补上”千万级用户” |
| 状态漂移 | Task State Context 管理不善 | 工具返回失败,Agent 标记为成功 |
| 证据漂移 | Evidence Context 管理不善 | 根据两年前文档得出”不支持” |
| 细节丢失 | 上下文压缩不当 | “预算 5000”摘要后变成”预算有限” |
| 错误累积 | Observation Context 管理不善 | “累计用户数”看成”日活用户数”,后续全错 |
这些问题表面上各不相同,但本质上都是同一件事:Agent 没有持续、准确地维护执行所需的上下文。
基础方案:走一步检查一步
核心思路:不要让 Agent 一口气跑到最后,在执行过程中不断停下来检查。
在 Plan 模式中,通过 Checkpoint 定期校准——第二篇里我聊过 Checkpoint 是 Plan 五要素里最容易被忽略的,当时主要从”防偏离”角度讲的。但从上下文管理的角度看,Checkpoint 还有一个更重要的作用:在关键节点强制刷新上下文——检查目标有没有漂移、约束有没有被违反、状态是否准确、是否需要 Replan。Checkpoint 放在业务阶段结束、关键中间产物生成、高风险工具调用后。
在 ReAct 模式中,每一轮都要重新回答**”我还差什么”**——拿到结果后不只是总结”我得到了什么”,更要判断”还缺什么”。具体来说,每一轮要回答五个问题:
- 目标判断:当前要逼近的最终目标是什么?(防止目标漂移)
- 状态判断:哪些已完成,哪些未完成?
- 约束判断:当前 Action 是否违反硬约束?
- 差距判断:距离目标还缺少什么?(最重要)
- 证据判断:上一轮 Observation 能否支持当前结论?
举个例子:Agent 要分析某家公司营收下滑的原因。第一轮查到营收下降了 20%。比较差的 ReAct 会想”已经获取到营收数据,接下来分析原因”。更合理的 ReAct 应该想”当前只确认了营收确实下降,但还无法确定原因,还需要查看销量、价格、成本等数据”。
前者只是在跟随 Observation,后者始终围绕 Goal 检查信息缺口。
高级方案:异步检查 + 事件溯源
Context Verifier:引入一个独立的检查器,异步检查当前上下文是否发生漂移。主 Agent 负责推进任务,Verifier 负责检查它有没有在不知不觉中跑偏。可以理解为 Checkpoint 的异步化。
检查内容:Goal 是否偏离、硬约束是否丢失、Task State 是否一致、关键结论是否有 Evidence 支持、窗口是否混入过期或冲突的信息。
触发时机:阶段结束、上下文占用达到阈值、Replan 前后、出现异常信号。检查太频繁 Token 和延迟会增加,太少又可能等到错误扩散才发现。
事件溯源:不只维护一份不断被覆盖的当前状态,而是记录关键事件——用户新增约束、Agent 完成步骤、工具返回结果、某个事实被确认或推翻。关键节点生成可信快照。
最大价值是可追溯。发现跑偏时可以定位是哪一次状态更新引入了错误,而不是面对一份已经被污染的当前上下文猜测原因。恢复时回到最近一个可信快照,丢弃后续被污染的状态,重新执行受影响的步骤。
分阶段执行:将长任务拆成多个阶段,每个阶段只使用与自己相关的局部上下文。阶段结束后生成结构化的交接信息(总目标、已完成工作、已确认事实、硬约束、关键证据索引、未解决问题、下一阶段输入)。大量临时 Observation 和失败尝试留在原阶段,需要时再召回。
三个方案可以组合使用:分阶段控制上下文规模 + 事件溯源保存完整执行历史 + Context Verifier 定期检查漂移。
怎么评估效果
做 Agent 不能只做不评。上下文管理这块我主要看几类指标:
窗口编排相关:
- 窗口利用率(实际使用 / 总窗口)
- 关键信息保留率(硬约束、关键事实是否在窗口内)
- 缓存命中率(Prompt Cache 命中比例)
摘要与召回相关:
- 摘要信息损失率(关键事实是否在摘要中丢失)
- 细节召回准确率(召回的细节是否是当前需要的)
- 细节召回覆盖率(需要的细节是否被召回)
用户画像相关:
- 画像准确率(画像是否反映用户真实偏好)
- 画像更新及时性(用户偏好变化后多久被识别)
- 误判率(一时兴起被当成稳定偏好的比例)
长任务相关:
- 目标漂移率(长任务中目标偏离原始目标的比例)
- 约束违反率(硬约束被违反的比例)
- 漂移检测率(漂移发生后被检测到的比例)
- 恢复成功率(检测到漂移后成功恢复的比例)
示例展示
用我那个技术选型 Agent 走一遍,看看上下文管理在实际对话中怎么运作。
第一轮
用户输入:
“帮我看下订单服务用什么缓存方案好,运维人手不多”
Agent 生成 Plan,进入 TAO 循环。此时上下文窗口状态:
1 | { |
窗口充足,Full Mode,所有内容原文放。执行查监控工具,拿到”峰值 QPS 5 万,P99 RT 150ms”,标 Available,写入 Fact State。
第二轮
用户补了一句:
“对了,Redis Cluster 别推了,运维搞不定”
新增硬约束。上下文窗口更新:
1 | { |
硬约束写入 Fact State,标 Verified,authority=high。同时在 System Prompt 里再次强调,防长上下文削弱。
第三轮到第五轮
Agent 继续执行,查询架构、追问信息、评估方案。每一轮的 Fact State 都在更新,关键事实独立维护。
到第五轮,窗口状态:
1 | { |
进入 Balanced Mode,普通历史对话做了摘要,但硬约束和 Fact State 始终原文保留。这正是我之前踩坑后改的——之前只用滑动窗口,硬约束被挤出去了。
第六轮
用户说:
“还有没有其他选择?”
Agent 从 Fact State 里读取硬约束”不能推荐 Redis Cluster”,从候选路径表里排除已失败的方案,推荐了 Memcached 方案。
Checkpoint 校验:是否排除了 Redis Cluster?是。运维能否独立运维?是。通过。
示例小结
走下来能看到上下文管理是怎么运作的:
- 分层上下文:硬约束、Fact State、历史对话、工具结果分层管理
- 动态编排:窗口从 Full Mode 逐渐切换到 Balanced Mode,不同内容不同压缩策略
- 关键信息保留:硬约束和关键事实始终原文保留,不被摘要压缩
- Fact State 独立维护:不依赖对话历史,单独存储关键事实
- 档位切换:根据窗口占用自动切换,Balanced Mode 是主状态
写在最后
上下文管理这事,说简单也简单——把历史对话塞进 Prompt 就能跑起来;说难也难——要扛住多轮、长任务、工具过期、用户变卦,得在窗口编排、摘要压缩、细节召回、用户画像、长任务防漂移上下不少功夫。
核心就一句话:Context Window = f(Context),这个 f 的质量直接决定了 Agent 的上限。
说到底,Agent 和传统软件最大的不同是:传统软件的数据流是确定的,但 Agent 的”数据流”很大程度上取决于上下文——模型看到什么信息,就会做出什么决策。三篇写下来,我自己的一个体会是:意图识别、规划执行、上下文管理,这三件事不是独立的——意图识别决定”往窗口里塞什么”,上下文管理决定”怎么塞、塞多少”,规划执行决定”塞完之后怎么用”。它们是同一条链上的不同环节,任何一环掉链子,整个 Agent 都会出问题。模型能力会不断进化,但只要窗口有限,上下文管理就永远是一个核心问题。
如果你也在做 Agent,我的建议和前两篇一样:先跑起来,再逐步优化。先有效果,再谈效率。关键事实独立维护、硬约束不被摘要压缩这两件事,值得从第一天就埋进去。