04-把记忆塞进Prompt的代价-为什么只能取20条
黒漂技术佬的 AI 伙伴(AI-Partner)源码拆解系列。前面三篇把"记忆怎么存、怎么查、怎么注入"讲完了。本篇算一笔容易被忽略的账:把用户的记忆塞进系统提示词,到底要花多少 Token、多少延迟、多少银子?以及为什么"只能取 20 条"是个不得不做的取舍。
一、先建立直觉:系统提示词是"每轮都重发"的固定成本
很多人以为"记忆注入"只发生一次。错。在 AI 伙伴(AI-Partner)里,每次用户发一句话,后端都会走PersonaProvider.buildSystemMessage(userId)重新拼一遍系统提示词,连同这句用户消息、短期记忆窗口一起发给大模型。
也就是说:系统提示词 = 每次对话都付一次的固定开销。记忆越多、写得越啰嗦,这个固定开销就越大。它不是一次性的,是"对话次数 × 系统提示词长度"的累积。
所以"取多少条记忆"不是体验问题,是钱和延迟问题。
二、Token 成本量化估算
先说计数口径。项目里ChatService.estimateTokens的规则是:中文字符约 1 字 1 token,非中文字符约 4 字符 1 token。我们就按"中文 1 字 ≈ 1 token"粗算系统提示词的构成(以下为估算,便于建立量级感,非精确测量)。
系统提示词由四段拼成(回顾PersonaProvider):
| 段落 | 内容 | 估算 Token |
|---|---|---|
固定人设PERSONA_RULES | 性格 + 能力边界 | ~300 |
| 【当前时间】 | 形如2026年9月14日 14:30 | ~15 |
| 【用户信息】 | 昵称/角色/手机/生日 + 画像摘要(可选) | 基础 ~80;画像摘要最长 ~2000 |
| 【长期记忆 20 条】 | 每条[类型·重要度X] 内容 | 见下 |
| 【回复要求】6 条 | 共情/字数/求助等规则 | ~200 |
单条记忆的写法:[偏好·重要度3] 用户喜欢喝绿茶,前缀约 12 token + 正文约 10-30 token,取均值约 30 token/条。20 条就是:
30 token × 20 条 = 600 token(记忆段)于是两套典型场景的系统提示词体量:
| 场景 | 系统提示词 Token 估算 |
|---|---|
| 轻量用户(无画像摘要,20 条短记忆) | 300+15+80+600+200 ≈1195 token |
| 重度用户(画像摘要 2000 字 + 20 条记忆) | 300+15+2080+600+200 ≈3195 token |
再叠加每轮对话本身的"用户消息 + 短期记忆窗口(最多 12 条) + 模型回复"。即便用户只说"在吗",系统提示词那上千 token 也已经先发出去了。
关键结论:系统提示词是"沉默的固定税"。一次对话真实消耗的 Token ≈ 系统提示词(固定) + 短期记忆窗口 + 当轮问答。记忆段是系统提示词里最大、也最该被压缩的一块。
三、上下文膨胀:对延迟和费用的双重打击
把记忆无节制地塞进去,会从两头反噬:
1. 费用:输入 Token 累积。
虽然输入 Token 单价通常低于输出,但系统提示词每轮重发,对话量一大就是实打实的开销。假设系统提示词 1200 token、日均单用户 30 轮对话,那就是1200 × 30 = 36000 token/天/用户纯系统提示词消耗——而且这还没算问答本身。用户上了规模,记忆段每多塞 10 条,成本线性上涨。
2. 延迟:首字时间被拖长。
大模型处理请求时,要先"读完"整个上下文才开始吐字。系统提示词越长,****首字延迟(time-to-first-token)****越大,用户会明显感觉"机器人反应慢了"。陪伴场景里,慢半秒都影响"像家人"的体感。
3. 注意力稀释:贵且效果差。
更隐蔽的代价——上下文越长,模型越容易在海量记忆里"视而不见",真正该用的记忆被淹没。这叫"lost in the middle"现象。所以塞得多 ≠ 陪得好,反而可能更差。
四、为什么"只能取 20 条":一个工程取舍
回到源码,两处硬截断到 20:
PersonaProvider.buildSystemMessage:.limit(20)注入系统提示词。MemoryService.search:.limit(20)工具检索返回上限。
20 是三层约束下的平衡点:
| 约束 | 对条数的拉扯 |
|---|---|
| Token 预算 | 希望越少越好(省钱、快) |
| 个性化质量 | 希望越多越好(记得全) |
| 注意力上限 | 希望精选而非堆量(避免稀释) |
20 条在"够个性化"和"不撑爆上下文"之间取了中间值。配合第 03 篇讲的importance DESC排序,保证被截掉的 20 名开外,本来就是相对不重要的记忆——截断有优先级保护,不是随机丢。
但要诚实指出:20 是个经验值,不是算法最优解。它的成立依赖"重要度排序靠谱"。如果模型把闲聊都打 5 分(第 02 篇提到的风险),20 条里就会混进噪音,截断也救不了。
五、当前实现里一处可优化冗余
读PersonaProvider源码会发现一个小浪费:同一条记忆可能出现在两个地方。
系统提示词先写【用户信息】段,里面含personaSummary(画像摘要,来自MemoryService.refreshPersonaSummary取前 10 条记忆的content用";"拼成);随后又写【长期记忆】段,取前 20 条记忆。于是前 10 条记忆,既在画像摘要里,又在记忆列表里——重复注入了。
// 示意:系统提示词里前 10 条记忆出现两次 【用户信息】 - 画像摘要:用户喜欢喝绿茶;用户有一只叫团团的猫;...(前10条) 【关于这位用户,你记得(长期记忆)】 [偏好·重要度3] 用户喜欢喝绿茶 ← 重复 [关系·重要度3] 用户有一只叫团团的猫 ← 重复 ...(前20条)这不算 bug(画像摘要和记忆列表角色不同:一个是"一句话印象",一个是"可逐条引用"),但从 Token 角度看,前 10 条记忆被算了两次。重度用户那 2000 字画像摘要,和记忆段高度重叠,正是系统提示词膨胀的主因之一。优化的第一刀,就该切这里。
六、进阶方案:三层记忆与召回打分
想既"记得多"又"塞得少",业内通用思路是不要把所有记忆都常驻提示词,而是分层 + 按需召回。
方案 A:分层注入(hot / warm / cold)
| 层 | 内容 | 是否进系统提示词 |
|---|---|---|
| 热记忆 hot | 最近 + 高重要度(如 top 8) | 常驻注入 |
| 温记忆 warm | 其余生效记忆 | 不注入,模型用searchMemory按需翻 |
| 冷记忆 cold | 历史/低频 | 不注入,归档 |
这样系统提示词只背"最该当下用"的少量记忆,模型需要时再调工具翻笔记——和第 02 篇的searchMemory天然契合。
方案 B:摘要压缩personaSummary已经是摘要思路的雏形。可进一步:把 20 条压成 3-5 句"用户画像简介"常驻,细节全走按需检索。相当于"给人设喂简历,不给档案"。
方案 C:召回打分(向量/语义)
当前排序只按importance + createdAt,是规则排序,理解不了"用户问猫,该调出团团那条"。引入 Embedding 后,可按"当前对话语义 × 记忆相似度"打分,挑最相关的 20 条——解决第 03 篇说的"字面匹配近义词盲区"。代价是要接向量库(项目目前没接,属可扩展方向)。
诚实边界:上面 A/B/C 均不是当前
t_memory/PersonaProvider的现有实现。项目用的是"重要度排序 + 20 条截断 + 画像摘要"三板斧,已能跑通;进阶方案是给二次开发指的路,别当成已完工的功能写进文档。
七、一个具体的量级推演
光说"贵"没感觉,我们代入一组假设数字,看记忆段膨胀如何放大成月度账单。口径仍是"中文约 1 字 1 token",且仅算系统提示词里的记忆段,便于看清趋势。
假设单用户日均对话 30 轮,分三种记忆体量:
| 记忆体量 | 系统提示词记忆段 Token | 系统提示词合计(估) | 日系统提示词 Token | 月系统提示词 Token |
|---|---|---|---|---|
| 轻度(8 条短记忆) | ~240 | ~835 | ~25,050 | ~75 万 |
| 当前默认(20 条) | ~600 | ~1,195 | ~35,850 | ~107 万 |
| 贪婪(50 条) | ~1,500 | ~2,095 | ~62,850 | ~188 万 |
只看"记忆段从 20 条涨到 50 条"这一步:系统提示词月消耗从约 107 万跳到 188 万,涨了约 75%,而用户感知到的陪伴质量未必同步提升——因为 50 条里大量是低重要度闲聊,模型反而更易"lost in the middle"。这正好反证"掐在 20 条"的合理性:边际记忆带来的体验收益,远抵不过边际 Token 成本。
再叠加短期记忆窗口(最多 12 条,第 02 系列讲过memory-window-size=12)和用户消息,每轮真实消耗还会更高,但那是"对话本身"的开销,记忆段是其中唯一可由我们主动压缩的大头。所以优化记忆注入,是性价比最高的一刀。
八、可落地的优化清单
如果你要在现有代码上动刀,按性价比排个序:
- 消除重复注入:系统提示词里画像摘要与记忆段去重,或二选一精简(省下重度用户近 2000 token)。
- 下调常驻条数做 A/B:把
limit(20)抽成配置项(如memory-inject-size,默认 20),按需调到 8-12 观察陪伴质量。// 示意:把硬编码的 20 提为可配置项@Value("${ai.partner.llm.memory-inject-size:20}")intmemoryInjectSize;// 使用处:.limit(memoryInjectSize) - 缩短
content存储:存记忆时让模型写"精炼一句",避免塞进整段闲聊(1000 字上限别用满)。 - 系统提示词缓存:同一
userId在记忆未变时缓存buildSystemMessage结果,别每次查库重拼(注意时间字段需单独刷新)。// 示意:用 userId -> 系统提示词 的缓存,记忆变更时失效Map<Long,String>systemCache=newConcurrentHashMap<>();// save/remove 触发 refreshPersonaSummary 后,一并 systemCache.remove(userId) - 监控 Token 用量:
Conversation.tokenUsage已在落库,定期看均值,定位"记忆肥胖"用户。 - 引入向量召回(远期):接 Embedding 服务,把"重要度排序"升级为"语义相关度排序",提升 20 条的命中率。
合规提醒:把个人记忆注入提示词,等于把用户的生活片段(可能含健康、家庭、位置)每轮都外发给大模型服务商。请务必:① 仅注入陪伴所必需的记忆,遵循数据最小化;② 对外发内容做脱敏评估,敏感字段(如精确手机号、详细住址)谨慎进入提示词;③ 明确告知用户数据会被用于处理对话,并取得授权;④ 老人/儿童数据采用更保守的注入策略。记忆越全,责任越重。
算清这笔账后会发现:陪伴机器人"记得多"的浪漫背后,是 Token、延迟和隐私的三重成本。"取 20 条"不是偷懒,是在成本与体验之间画的的一条理性分界线。