写在前面

第一篇聊了意图识别——把用户想干嘛搞清楚,第二篇聊了规划、纠偏与执行循环——通过 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 的时候,我把”上下文”当成了一个笼统的概念。后来才发现,ContextContext 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
2
3
4
5
6
7
8
9
10
11
{
"id": "ctx_001",
"type": "constraint",
"content": "不能推荐 Redis Cluster",
"source": "user",
"scope": "current_task",
"authority": "high",
"confidence": 1.0,
"expires_at": null,
"token_cost": 8
}

几个关键字段:

  • source:信息从哪来(用户、系统、工具、模型推测)
  • scope:有效范围(当前步骤、当前任务、当前会话、全局)
  • authority:权威程度,用于冲突仲裁。参考排序:系统规则 > 用户明确输入 > 工具事实 > 已确认摘要 > 模型推测
  • confidence:可信度
  • expires_at:过期时间。用户偏好有效期长,工具查询的 QPS 数据有效期短

这里有个细节:要区分观点事实。用户说”Redis Cluster 太复杂了”,这是观点,要尊重并给予重要权重,无须分辨对错。用户说”我们的 QPS 是 5 万”,这是事实,可以在后续阶段尝试验证。

设计每个 Context Item 的时候,我都会问自己三个问题:

  1. 怎么获得:这个信息从哪来?用户画像怎么提取?硬约束怎么识别?
  2. 怎么存储:要不要持久化?放 Redis 还是 MySQL?生命周期多长?
  3. 怎么查询:用的时候怎么找回来?关键词检索还是向量召回?

这三个问题看着简单,但很多 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 又影响效果。”弱水三千,只取一瓢。”

三种筛选机制:

  1. 强规则筛选:Prompt 模板/代码层面直接决定。比如我在 Go 里用模板渲染 Prompt,{{ .UserProfile }} {{ .Input }} 这种,代码层面就决定了放什么。
  2. 检索召回:向量数据库/ES 查找相关内容,参考 RAG 机制。
  3. 模型筛选:先调用轻量模型判断需要什么,再发起真正调用。成本高,不太常用。

压缩

压缩不是静态机制,而是根据实时情况动态调整的。核心原则:越重要的内容越不能压缩,剩余窗口越大越不需要压缩。

我用过一种多版本预压缩策略:同一份信息提前准备好多个版本,运行时按需加载:

层级 内容 适用场景
L0 一句话摘要 窗口几乎放满
L1 关键结论摘要 窗口比较满
L2 结构化事实 窗口放了一些内容
L3 详细摘要 窗口还挺大
L4 完整原文 窗口非常充足

好处是运行时没有额外压缩开销。

还有两个细节值得注意:

级联压缩:两个强相关的 Context Item(比如用户输入和对应的工具返回)要保持同一压缩级别——一个压到 L0 另一个还是 L4,信息就对不上了。反过来,两个有信息重合的 Item(比如对话历史和关键事实),可以让一个极度压缩,另一个尽量原文,避免重复占用窗口。

兜底机制:在 Prompt 里加一句兜底条款——“如果发现某个内容压缩程度太高,导致无法完成当前任务,可以调用工具召回更详细的版本”。这样即使压缩过头了,模型也有自救的能力。

排序

大模型对内容顺序敏感——同样的内容,排序不同,输出也会不同。我的排序原则是越重要越靠前:

  1. System Prompt / 业务 Spec
  2. 当前 Goal
  3. 硬约束
  4. 当前任务状态 / Plan 节点
  5. 已确认关键事实
  6. 当前步骤需要的证据
  7. 工具返回结果
  8. 候选 Action / Tool
  9. 动态 few-shot
  10. 历史摘要
  11. 补充背景

但要注意缓存友好性——不常变动的内容可以往后放,更容易命中 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 要求:

  1. 删除礼貌表达、转场句、口语填充词、重复解释、情绪价值表达
  2. 保留目标、实体、关键事实、硬约束、软偏好、用户确认/否认、数字、时间、决策、理由、风险、缺失信息、证据来源
  3. 可以用短句、列表、键值对,不要求自然语言流畅
  4. 不得改变原意,不得把不确定信息改成确定事实
  5. 不得把建议改成已执行决策,不得把模型推测改成用户确认事实
  6. 硬约束和用户否认内容必须显式保留

细节召回

什么样的细节值得召回?判定标准就一个:这个细节是否会影响当前决策

具体来说:

  • 影响目标的细节(用户到底要优化什么?)
  • 影响路径选择的细节(方案 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 和路径选择:不同画像的用户,最优路径可能完全不同
  • 最终输出:写作风格、详细程度、专业深度

提取时机也很关键

时机 说明 适用场景
实时提取 每轮对话结束后立即判断 生成当前画像,不适合修改长期画像
对话结束后提取 一次对话/任务结束后统一提取 可以看到完整对话过程
离线提取 定期分析近期行为和对话 发现趋势、合并重复、判断稳定性

最佳实践是实时 + 离线混合:实时保证及时性,离线保证准确性。实时提取的画像先进候选层,离线分析确认后再升级到长期画像。

画像冲突怎么裁决?

画像之间也会冲突。我的裁决原则:

  1. 当前明确表达 > 历史推断:用户这次明确说了,比你推断的历史偏好更优先
  2. 用户明确表达 > 行为推断:用户说”我不喜欢”,比你从购买记录推断的更可信
  3. 用户纠正优先:用户说”你理解错了”,立即修正,不要争辩
  4. 领域画像 > 全局画像:用户在数码产品上是专家,在金融上是小白,不能用全局画像一刀切

层级设计

我维护三层用户画像:

  • 当前画像:本次对话中的即时偏好(用户这次说喜欢小米)
  • 候选画像:有待验证的新信号(用户最近三次都提到小米)
  • 长期画像:沉淀的稳定用户特征(用户长期偏好苹果生态)

三者是覆盖关系,后者覆盖前者。长期偏好一般是候选画像慢慢升级上来的。

怎么判断不是一时兴起?

这是最难的问题。用户今天说喜欢折叠屏手机,你能不能写入长期画像?

我总结了一个 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 模式中,每一轮都要重新回答**”我还差什么”**——拿到结果后不只是总结”我得到了什么”,更要判断”还缺什么”。具体来说,每一轮要回答五个问题:

  1. 目标判断:当前要逼近的最终目标是什么?(防止目标漂移)
  2. 状态判断:哪些已完成,哪些未完成?
  3. 约束判断:当前 Action 是否违反硬约束?
  4. 差距判断:距离目标还缺少什么?(最重要)
  5. 证据判断:上一轮 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
2
3
4
5
6
7
8
9
10
11
{
"window_content": {
"system_prompt": "技术选型 Agent 规范(4K)",
"goal": "为订单服务选型缓存方案",
"hard_constraints": ["运维团队人手有限"],
"plan": "采集现状→追问缺失→评估方案→输出推荐",
"current_step": "query_metrics",
"token_used": "8K / 32K"
},
"mode": "Full Mode"
}

窗口充足,Full Mode,所有内容原文放。执行查监控工具,拿到”峰值 QPS 5 万,P99 RT 150ms”,标 Available,写入 Fact State。

第二轮

用户补了一句:

“对了,Redis Cluster 别推了,运维搞不定”

新增硬约束。上下文窗口更新:

1
2
3
4
5
6
{
"hard_constraints": ["运维团队人手有限", "不能推荐 Redis Cluster"],
"fact_state": {
"peak_qps": { "value": "5万", "status": "Available", "source": "monitoring_tool" }
}
}

硬约束写入 Fact State,标 Verified,authority=high。同时在 System Prompt 里再次强调,防长上下文削弱。

第三轮到第五轮

Agent 继续执行,查询架构、追问信息、评估方案。每一轮的 Fact State 都在更新,关键事实独立维护。

到第五轮,窗口状态:

1
2
3
4
5
6
7
8
9
10
11
{
"token_used": "22K / 32K",
"mode": "Balanced Mode",
"compression": {
"history_dialogue": "结构化摘要(L2)",
"tool_results": "核心字段(L2)",
"hard_constraints": "原文保留",
"fact_state": "原文保留",
"user_profile": "任务相关画像"
}
}

进入 Balanced Mode,普通历史对话做了摘要,但硬约束和 Fact State 始终原文保留。这正是我之前踩坑后改的——之前只用滑动窗口,硬约束被挤出去了。

第六轮

用户说:

“还有没有其他选择?”

Agent 从 Fact State 里读取硬约束”不能推荐 Redis Cluster”,从候选路径表里排除已失败的方案,推荐了 Memcached 方案。

Checkpoint 校验:是否排除了 Redis Cluster?是。运维能否独立运维?是。通过。

示例小结

走下来能看到上下文管理是怎么运作的:

  1. 分层上下文:硬约束、Fact State、历史对话、工具结果分层管理
  2. 动态编排:窗口从 Full Mode 逐渐切换到 Balanced Mode,不同内容不同压缩策略
  3. 关键信息保留:硬约束和关键事实始终原文保留,不被摘要压缩
  4. Fact State 独立维护:不依赖对话历史,单独存储关键事实
  5. 档位切换:根据窗口占用自动切换,Balanced Mode 是主状态

写在最后

上下文管理这事,说简单也简单——把历史对话塞进 Prompt 就能跑起来;说难也难——要扛住多轮、长任务、工具过期、用户变卦,得在窗口编排、摘要压缩、细节召回、用户画像、长任务防漂移上下不少功夫。

核心就一句话:Context Window = f(Context),这个 f 的质量直接决定了 Agent 的上限。

说到底,Agent 和传统软件最大的不同是:传统软件的数据流是确定的,但 Agent 的”数据流”很大程度上取决于上下文——模型看到什么信息,就会做出什么决策。三篇写下来,我自己的一个体会是:意图识别、规划执行、上下文管理,这三件事不是独立的——意图识别决定”往窗口里塞什么”,上下文管理决定”怎么塞、塞多少”,规划执行决定”塞完之后怎么用”。它们是同一条链上的不同环节,任何一环掉链子,整个 Agent 都会出问题。模型能力会不断进化,但只要窗口有限,上下文管理就永远是一个核心问题。

如果你也在做 Agent,我的建议和前两篇一样:先跑起来,再逐步优化。先有效果,再谈效率。关键事实独立维护、硬约束不被摘要压缩这两件事,值得从第一天就埋进去。

站内搜索

没有找到内容!