Agent 的记忆系统
# Agent 的记忆系统
让 Agent 拥有短期的专注和长期的经验
📝 先看一个最小记忆例子
如果用户第一轮说"我喜欢简洁风格",第二轮说"帮我写周报",没有记忆的 Agent 会重新追问风格;有记忆的 Agent 会直接按简洁风格输出。
所以你可以把本章先简单理解成一句话:记忆不是为了存很多内容,而是为了让 Agent 在下一轮做出更一致、更省心的决定。
# 5.1 没有记忆的 Agent 会怎样?
想象一个失忆的助手:你上午告诉他你喜欢简洁的报告,下午他又问你"你需要什么风格?";你刚给了他一份资料,下一秒他就忘了。
这就是没有记忆的 Agent。它只能处理单轮、即时的请求,无法积累经验、无法保持一致性、无法处理需要跨轮引用的复杂任务。
💡 记忆的本质
记忆让 Agent 从"活在当下"变成"有过去、有经验"。它影响 Agent 的连贯性(同一对话内的一致性)、个性化(记住用户偏好)和学习进化(从历史中优化)。
# 5.2 三层记忆架构
人类有感觉记忆、短期记忆、长期记忆三层结构。Agent 也有类似的设计:
流程:感觉记忆(输入缓冲) →[瞬时] 短期记忆(上下文窗口) →[任务相关] 工作记忆(任务便签) →[直接使用] 影响输出
- 短期记忆 →[压缩存储] 长期记忆(向量数据库)
- 长期记忆 →[检索回忆] 工作记忆
| 类型 | 类比 | 实现 | 容量 | 持久性 |
|---|---|---|---|---|
| 短期记忆 | 工作台上的文件 | LLM 上下文窗口 | 4K~200K token | 对话期间 |
| 工作记忆 | 便签纸 | Scratchpad / 变量 | 任务相关 | 任务期间 |
| 长期记忆 | 图书馆/档案室 | 向量数据库 | 理论无限 | 永久 |
# 5.3 短期记忆的管理策略
短期记忆 = LLM 上下文窗口。对话越长,上下文越大。但窗口有上限,必须管理:
# 四种管理策略
- 滑窗截断(Sliding Window):保留最近 N 轮对话,丢弃最旧的消息。优点:实现最简单。缺点:可能丢失关键早期信息,如用户在第一轮提到的约束条件。
- 摘要压缩(Summarization):当上下文超过阈值时,用 LLM 对旧对话生成摘要,用摘要替代原始内容。优点:保留关键信息。缺点:每次压缩消耗额外 token,摘要可能丢失细节。
- 选择性保留(Selective Retention):用 LLM 判断哪些消息重要(包含事实、决策、用户偏好),只保留重要消息。优点:信息密度高。缺点:判断逻辑复杂,可能误判。
- 分层压缩(Hierarchical):结合以上策略:最近几轮完整保留,中期做摘要,最旧的存入长期记忆。类似人类的记忆机制--刚发生的事记得清楚,旧事只记大概。
# 5.4 长期记忆:向量数据库
长期记忆让 Agent 能回忆起几天前甚至几个月前的交互。核心技术是向量数据库 + 语义检索。
流程:交互文本 → Embedding 模型 → 向量 [0.12, 0.85, ...] →[写入] 向量数据库(Chroma/Pinecone)
查询时:查询向量 → 相似度搜索(余弦相似度) → Top-K 相关记忆
# 主流向量数据库对比
| 数据库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| Chroma | 嵌入式 | 轻量、Python原生、零配置 | 原型开发、小规模 |
| Pinecone | 云服务 | 全托管、高性能、易扩展 | 生产环境、大规模 |
| Weaviate | 自部署 | 支持混合搜索、GraphQL | 需要复杂查询 |
| Milvus | 分布式 | 高可用、支持十亿级向量 | 超大规模企业 |
| pgvector | PostgreSQL扩展 | SQL生态、事务支持 | 已有PG基础设施 |
# 5.5 上下文压缩实战
在长任务中,Agent 的上下文窗口会不断膨胀--每次工具调用返回结果、每次推理步骤、每次对话轮次都在累积 token。这种现象称为上下文膨胀(Context Bloat),也叫上下文腐烂(Context Rot)。
⚠️ Context Rot 的典型症状
- 行为漂移:Agent 早期遵循的规则(如"用中文回答"),在 50 轮对话后可能被遗忘
- 注意力稀释:上下文中有太多无关历史,LLM 的注意力被分散,输出质量下降
- 成本飙升:每次调用都要处理全部历史 token,费用随对话长度线性增长
- 窗口溢出:超过模型最大上下文长度后,被迫截断或报错
# 五种压缩策略详解
- 截断(Truncation):最简单的方案:超过窗口上限时,直接丢弃最早的消息。优点:实现成本为零,性能开销最小。缺点:可能导致行为不一致--Agent 可能忘记用户在第一轮设定的约束条件。截断不考虑内容重要性,一刀切地丢弃历史。
- 摘要(Summarization):有损压缩:用 LLM 对旧对话生成摘要,用摘要替代原始内容。优点:保留了关键信息的"精华"。缺点:压缩质量依赖摘要模型的能力;每次压缩消耗额外 token 和时间;二次摘要会进一步损失细节(信息蒸馏的熵增问题)。
- 滑动窗口(Sliding Window):按时间维度丢弃:始终保留最近 N 轮对话,超出的部分自动淘汰。优点:简单可控,上下文长度恒定。缺点:只考虑时间远近,不考虑重要性--可能丢掉关键决策但保留了无关闲聊。
- 选择性保留(Selective Retention):按类型分别设定保留策略:系统指令全量保留、用户偏好标记后保留、工具调用结果压缩后保留、闲聊直接丢弃。优点:信息密度最高。缺点:需要额外的分类器或规则引擎来判断消息类型,实现复杂度较高。
- 语义保护型压缩(Semantic-Preserving):最先进的方案:锁定核心指令(System Prompt 不压缩),确保行动-结果完整性(工具调用与其返回值成对保留),只压缩中间推理过程。优点:在压缩率和信息完整性之间取得最优平衡。缺点:实现最复杂,需要精确识别消息间的因果依赖。
# 压缩前后 Token 对比
# ===== 模拟一次 30 轮对话的上下文膨胀 =====
# 原始对话(无压缩)
raw_tokens_per_turn = 150 # 每轮平均 150 token
turns = 30
total_raw = raw_tokens_per_turn * turns + 500 # +500 为 System Prompt
print(f"原始上下文: {total_raw} tokens") # 输出: 5000 tokens
# ----- 策略 1: 截断 -----
# 保留最近 10 轮,丢弃其余
truncated = raw_tokens_per_turn * 10 + 500
print(f"截断后: {truncated} tokens") # 输出: 2000 tokens
# 损失: 20 轮对话完全丢失,包含用户初始约束条件
# ----- 策略 2: 摘要 -----
# 旧 20 轮压缩为一段摘要(约 200 token)
summarized = 200 + raw_tokens_per_turn * 10 + 500
print(f"摘要后: {summarized} tokens") # 输出: 2200 tokens
# 损失: 细节丢失,但保留了关键决策和偏好
# ----- 策略 3: 滑动窗口 -----
# 等同于截断策略(按时间维度)
sliding = truncated
print(f"滑动窗口后: {sliding} tokens") # 输出: 2000 tokens
# ----- 策略 4: 选择性保留 -----
# 从 30 轮中标记 8 轮为"重要",其余丢弃
selected = raw_tokens_per_turn * 8 + 500 + 150 # +150 为类型标签开销
print(f"选择性保留后: {selected} tokens") # 输出: 1850 tokens
# 信息密度最高,但可能遗漏看似不重要实则关键的上下文
# ----- 策略 5: 语义保护型压缩 -----
# System Prompt 全量 + 6 个关键行动-结果对 + 12 轮推理压缩为 300 token
semantic = 500 + raw_tokens_per_turn * 6 + 300 + 100 # +100 为因果链标注
print(f"语义保护型后: {semantic} tokens") # 输出: 1800 tokens
# 最低压缩率 + 最高信息完整性
# ===== 效果总结 =====
print("\n--- 压缩效果对比 ---")
strategies = {
"原始": total_raw,
"截断": truncated,
"摘要": summarized,
"滑动窗口": sliding,
"选择性保留": selected,
"语义保护型": semantic
}
for name, tokens in strategies.items():
ratio = tokens / total_raw
print(f"{name:10s}: {tokens} tokens (压缩率 {ratio:.1%})")
# 输出:
# 原始 : 5000 tokens (压缩率 100.0%)
# 截断 : 2000 tokens (压缩率 40.0%)
# 摘要 : 2200 tokens (压缩率 44.0%)
# 滑动窗口 : 2000 tokens (压缩率 40.0%)
# 选择性保留: 1850 tokens (压缩率 37.0%)
# 语义保护型: 1800 tokens (压缩率 36.0%)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# Solon AI 框架的 SummarizationInterceptor
Solon AI 是一个国产的 Java AI 框架,它提供了 SummarizationInterceptor 作为上下文压缩的内置方案。其核心思路是:当对话轮次超过阈值时,自动触发摘要压缩。
// Solon AI 框架 - 上下文压缩拦截器配置
@Configuration
public class AiConfig {
@Bean
public ChatService chatService() {
return new ChatService.builder()
.model(openAiModel) // LLM 模型
.interceptor(summarizationInterceptor()) // 添加压缩拦截器
.build();
}
@Bean
public SummarizationInterceptor summarizationInterceptor() {
return new SummarizationInterceptor()
.threshold(10) // 超过 10 轮对话时触发压缩
.summaryModel(openAiModel) // 用哪个模型做摘要(可以用更便宜的模型)
.preserveSystemPrompt(true) // 保护 System Prompt 不被压缩
.preserveLastN(3) // 保留最近 3 轮原始对话
.summaryPrompt("请将以下对话历史压缩为简洁摘要," +
"保留所有关键决策、用户偏好和事实信息。" +
"丢弃闲聊和重复内容。");
}
}
// ===== 工作原理 =====
// 1. 用户每轮对话后,Interceptor 检查当前对话轮次
// 2. 轮次 <= threshold(10): 正常拼接所有历史
// 3. 轮次 > threshold:
// - System Prompt 全量保留(preserveSystemPrompt=true)
// - 最近 3 轮完整保留(preserveLastN=3)
// - 其余轮次调用 summaryModel 生成摘要
// - 摘要替代原始对话,拼接到上下文中
// ===== 压缩前后对比 =====
// 压缩前: [System] + [15轮原始对话] = ~3000 tokens
// 压缩后: [System] + [摘要~200tokens] + [最近3轮] = ~1150 tokens
// 压缩率: 38.3%,且核心指令和近期上下文无损保留
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
压缩不是删减,而是蒸馏。好的压缩策略就像化学蒸馏--把混合物中的精华提取出来,丢弃的是杂质而非有效成分。
# 5.6 记忆架构实战
选择合适的向量数据库和 Embedding 模型是构建记忆系统的关键决策。本节提供完整的选型对比和架构实现。
# 向量数据库深度对比
| 数据库 | 部署方式 | 最大向量数 | 混合搜索 | 过滤查询 | 事务支持 | 成本 | 适用场景 |
|---|---|---|---|---|---|---|---|
| Chroma | 嵌入式/内存 | ~百万级 | ❌ | 基础 | ❌ | 免费 | 原型/小规模 |
| Pinecone | 全托管云服务 | 亿级 | ✅(稀疏+稠密) | 丰富 | ❌ | 按量计费 | 生产/大规模 |
| Weaviate | 自部署/Docker | 千万级 | ✅(BM25+向量) | GraphQL | ❌ | 免费/云付费 | 复杂查询场景 |
| Milvus | 分布式集群 | 十亿级 | ✅ | 丰富 | ❌ | 免费/运维成本高 | 超大规模企业 |
| pgvector | PG扩展/Docker | 百万级 | ✅(全文+向量) | SQL全能力 | ✅ | 免费/低成本 | 已有PG基础设施 |
🎯 选型决策树
- 快速原型验证 → Chroma(零配置,pip install 即用)
- 不想运维 → Pinecone(全托管,按量付费,注册即用)
- 需要混合搜索(关键词+语义) → Weaviate(BM25+向量双路召回)
- 已有 PostgreSQL 且数据量不大 → pgvector(不用引入新组件,SQL+向量一体化)
- 十亿级向量 + 高可用 → Milvus(分布式架构,专门为超大规模设计)
# 完整记忆系统架构设计
流程:用户输入/工具结果 → 短期记忆(上下文窗口) →[超阈值时] 压缩引擎(摘要/选择性) →[压缩后存入] Embedding 模型 → 长期记忆(向量数据库)
- 用户输入 →[触发检索] 语义检索(Top-K)
- 长期记忆 →[候选记忆] 语义检索(Top-K) →[注入工作记忆] 工作记忆(Scratchpad)
- 短期记忆 → LLM(推理引擎) → Agent 输出
- 工作记忆 →[辅助推理] LLM(推理引擎)
# 短期/长期/工作记忆协同代码实现
import chromadb
from openai import OpenAI
class MemorySystem:
"""三层记忆协同系统"""
def __init__(self, embedding_model="text-embedding-3-small"):
self.client = OpenAI()
self.chroma = chromadb.PersistentClient(path="./memory_db")
self.collection = self.chroma.get_or_create_collection(
name="agent_memory",
metadata={"hnsw:space": "cosine"}
)
self.short_term = [] # 短期记忆:对话历史列表
self.working = {} # 工作记忆:当前任务的中间变量
self.compress_threshold = 15 # 超过 15 轮触发压缩
# ===== 短期记忆管理 =====
def add_to_short_term(self, role, content):
"""添加一条消息到短期记忆"""
self.short_term.append({"role": role, "content": content})
# 检查是否需要压缩
if len(self.short_term) > self.compress_threshold:
self._compress_short_term()
def _compress_short_term(self):
"""压缩短期记忆:摘要 + 转存长期记忆"""
# 保留最近 3 轮和 System Prompt
system_msgs = [m for m in self.short_term if m["role"] == "system"]
recent_msgs = self.short_term[-6:] # 最近 3 轮(每轮2条)
# 其余消息交给 LLM 生成摘要
old_msgs = self.short_term[len(system_msgs):-6]
if old_msgs:
summary = self._summarize(old_msgs)
# 摘要存入长期记忆(向量数据库)
self._save_to_long_term(
text=summary,
metadata={"type": "summary", "turn_range":
f"turns {len(system_msgs)}-{len(self.short_term)-6}"}
)
# 用摘要替代原始对话
self.short_term = system_msgs + [
{"role": "system", "content": f"[历史摘要]: {summary}"}
] + recent_msgs
def _summarize(self, messages):
"""用 LLM 对旧对话生成摘要"""
text = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
response = self.client.chat.completions.create(
model="gpt-4o-mini", # 用便宜模型做摘要
messages=[
{"role": "system", "content":
"将以下对话压缩为简洁摘要。保留所有关键决策、"
"用户偏好、事实信息和约束条件。丢弃闲聊和重复内容。"},
{"role": "user", "content": text}
],
max_tokens=300
)
return response.choices[0].message.content
# ===== 长期记忆管理 =====
def _save_to_long_term(self, text, metadata=None):
"""将内容存入长期记忆(向量数据库)"""
vector = self._get_embedding(text)
doc_id = f"mem_{len(self.short_term)}_{hash(text) % 10000}"
self.collection.upsert(
ids=[doc_id],
embeddings=[vector],
documents=[text],
metadatas=[metadata or {}]
)
def retrieve_from_long_term(self, query, top_k=5):
"""从长期记忆中检索相关内容"""
results = self.collection.query(
query_texts=[query],
n_results=top_k,
include=["documents", "metadatas", "distances"]
)
memories = []
for doc, meta, dist in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0]
):
if dist < 0.7: # 相似度阈值
memories.append({
"text": doc,
"metadata": meta,
"similarity": 1 - dist
})
return memories
# ===== 工作记忆管理 =====
def update_working(self, key, value):
"""更新工作记忆中的变量"""
self.working[key] = value
def get_working(self, key):
"""获取工作记忆中的变量"""
return self.working.get(key)
def clear_working(self):
"""任务完成后清空工作记忆"""
self.working = {}
# ===== Embedding =====
def _get_embedding(self, text):
"""调用 OpenAI Embedding API"""
response = self.client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
# ===== 完整响应流程 =====
def respond(self, user_input):
"""Agent 完整响应流程(三层记忆协同)"""
# 1. 检索长期记忆
long_memories = self.retrieve_from_long_term(user_input, top_k=3)
memory_context = "\n".join([m["text"] for m in long_memories])
# 2. 注入工作记忆
working_context = "\n".join(
[f"{k}: {v}" for k, v in self.working.items()]
)
# 3. 构建完整 prompt
messages = self.short_term + [
{"role": "system", "content":
f"[相关历史记忆]\n{memory_context}\n\n"
f"[当前任务状态]\n{working_context}"},
{"role": "user", "content": user_input}
]
# 4. 调用 LLM 生成响应
response = self.client.chat.completions.create(
model="gpt-4o",
messages=messages
)
answer = response.choices[0].message.content
# 5. 更新三层记忆
self.add_to_short_term("user", user_input)
self.add_to_short_term("assistant", answer)
self._save_to_long_term(
f"用户: {user_input}\n助手: {answer}",
metadata={"type": "interaction"}
)
return answer
# ===== 使用示例 =====
memory = MemorySystem()
answer = memory.respond("帮我分析一下最近的销售数据趋势")
print(answer)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# Embedding 模型选择
| 模型 | 提供商 | 维度 | 质量 | 成本 | 延迟 | 适用场景 |
|---|---|---|---|---|---|---|
| text-embedding-3-large | OpenAI | 3072 | ⭐⭐⭐⭐⭐ | $0.13/1M | ~100ms | 高精度检索 |
| text-embedding-3-small | OpenAI | 1536 | ⭐⭐⭐⭐ | $0.02/1M | ~50ms | 通用场景 |
| bge-large-zh-v1.5 | BAAI(本地) | 1024 | ⭐⭐⭐⭐ | 免费 | ~20ms | 中文场景最佳 |
| all-MiniLM-L6-v2 | SentenceTransformers | 384 | ⭐⭐⭐ | 免费 | ~5ms | 快速原型/轻量 |
💡 Embedding 选型建议
- 中文为主 → BGE 系列(BAAI 北京智源),中文语义理解显著优于 OpenAI 模型
- 成本敏感 → 本地模型(bge / MiniLM),零 API 费用,但需要 GPU 或 CPU 推理
- 追求最高质量 → OpenAI text-embedding-3-large,MTEB 排行榜领先
- 平衡成本与质量 → OpenAI text-embedding-3-small,性价比最优
# 5.7 反思记忆与情景记忆
前面介绍的三层记忆架构解决了存储和检索问题,但记忆系统还有更高阶的能力:从经验中学习(反思记忆)和记录完整事件(情景记忆)。
🪞 反思记忆(Reflective Memory)
从执行结果中提炼经验教训。不是存储原始对话,而是存储"我学到了什么"。
例:任务失败后反思 → "查询天气 API 时需要加超时处理,否则会卡死整个流程"
📖 情景记忆(Episodic Memory)
记录具体事件的完整上下文:谁、什么、何时、何地、为什么、结果如何。
例:记录完整事件 → "2024-03-15,用户请求分析报告,用了 Python 分析工具,耗时 3 分钟,输出 500 字摘要,用户满意"
# 与三层记忆的关系
流程:短期记忆(对话窗口) →[任务结束后反思] 反思记忆(经验教训) →[经验存入] 长期记忆(向量数据库)
- 短期记忆 →[关键事件记录] 情景记忆(完整事件) →[事件存入] 长期记忆(向量数据库)
- 长期记忆 →[检索注入] 工作记忆(任务便签)
- 反思记忆 →[经验指导] 工作记忆
- 情景记忆 →[上下文参考] 工作记忆
反思记忆和情景记忆都存储在长期记忆(向量数据库)中,但它们的元数据标签不同,检索时可以按类型过滤:
| 记忆类型 | 存储内容 | 元数据标签 | 检索场景 |
|---|---|---|---|
| 短期记忆 | 原始对话轮次 | 无(直接拼接) | 当前对话上下文 |
| 反思记忆 | 提炼的经验教训 | {type: "reflection", task: "xxx"} | 遇到相似任务时参考经验 |
| 情景记忆 | 完整事件的5W1H | {type: "episodic", timestamp: "xxx"} | 需要历史上下文时参考事件 |
| 工作记忆 | 当前任务中间变量 | 无(Scratchpad) | 多步推理过程中的状态 |
# Reflexion 框架的反思循环
Reflexion 是一个经典的反思型 Agent 框架,其核心创新在于引入反思循环:Agent 执行任务后,如果结果不理想,会进行自我反思,将反思结果存入记忆,下次执行时参考这些经验。
流程:接收任务 → 执行行动(ReAct循环) → 评估结果 →[✅ 成功] 任务成功 / →[❌ 失败] 自我反思(为什么失败?) → 反思记忆(存入经验教训) → 重新执行(参考反思经验) →[带经验重试] 评估结果
class ReflexionAgent:
"""基于 Reflexion 框架的反思型 Agent"""
def __init__(self, llm, memory_system):
self.llm = llm
self.memory = memory_system
self.max_retries = 3 # 最大反思重试次数
def execute_task(self, task_description):
"""执行任务,失败时触发反思循环"""
for attempt in range(self.max_retries):
# 1. 检索相关反思经验
past_reflections = self.memory.retrieve_from_long_term(
query=task_description,
top_k=3
)
reflection_context = "\n".join([
f"[过往经验 #{i+1}]: {r['text']}"
for i, r in enumerate(past_reflections)
if r.get('metadata', {}).get('type') == 'reflection'
])
# 2. 构建带反思经验的 prompt
prompt = f"""
任务: {task_description}
{f'过往反思经验:\n{reflection_context}' if reflection_context else '这是首次尝试此任务。'}
请执行任务,注意避免过去犯过的错误。
"""
# 3. 执行 ReAct 循环
result = self._react_loop(prompt)
# 4. 评估结果
evaluation = self._evaluate(task_description, result)
if evaluation["success"]:
# 成功 → 记录为情景记忆
self.memory._save_to_long_term(
text=f"任务成功: {task_description}\n方法: {result['approach']}\n结果: {result['output']}",
metadata={"type": "episodic", "outcome": "success"}
)
return result
# 5. 失败 → 触发反思
reflection = self._reflect(task_description, result, evaluation)
# 6. 将反思存入记忆
self.memory._save_to_long_term(
text=reflection,
metadata={
"type": "reflection",
"task": task_description,
"attempt": attempt + 1,
"outcome": "failure"
}
)
print(f"第 {attempt+1} 次尝试失败,反思已存入记忆")
return {"output": "任务失败,已达最大重试次数", "success": False}
def _reflect(self, task, result, evaluation):
"""对失败结果进行反思"""
reflect_prompt = f"""
任务: {task}
我的行动: {result['actions_taken']}
得到的结果: {result['output']}
评估反馈: {evaluation['feedback']}
请反思以下问题:
1. 我哪里做错了?具体是哪个步骤出了问题?
2. 为什么会犯这个错误?根本原因是什么?
3. 下次应该怎么避免?给出具体的改进策略。
用简洁的要点总结你的反思。
"""
response = self.llm.generate(reflect_prompt)
return response
def _evaluate(self, task, result):
"""用 LLM 评估任务结果"""
eval_prompt = f"""
任务: {task}
结果: {result['output']}
这个结果是否成功完成了任务?请判断:
- 成功: 返回 JSON {{"success": true}}
- 失败: 返回 JSON {{"success": false, "feedback": "具体问题描述"}}
"""
response = self.llm.generate(eval_prompt)
# 解析评估结果...
return {"success": False, "feedback": "结果不完整"} # 简化示例
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
反思记忆让 Agent 从"重复犯错的傻瓜"进化为"从失败中学习的聪明人"。每次失败都是一次学习机会,关键是把教训提炼出来,而不是简单地丢掉失败记录。
# 5.8 记忆治理与上下文工程
记忆系统的终极问题不是"怎么存",而是怎么管。本节从工程视角讨论记忆的完整生命周期治理,以及从 Prompt Engineering 到 Context Engineering 的范式转变。
# Context Engineering vs Prompt Engineering
❌ Prompt Engineering(旧范式)
关注单次请求的措辞优化:"怎么写 prompt 让 LLM 回答更好?"是一次性的、静态的。
- 优化 System Prompt 措辞
- 设计 Few-shot 示例
- 调整温度、Top-P 等参数
✅ Context Engineering(新范式)
关注整个上下文的动态编排:"在正确的时刻,为 LLM 提供正确的信息,不多也不少。"是持续性的、动态的。
- 记忆的检索、压缩、淘汰策略
- 工具结果的格式化和过滤
- 多源信息的优先级排序
Prompt Engineering 是写一首诗,Context Engineering 是管理一个图书馆。前者关注措辞,后者关注信息流的完整治理。
# Just-in-Time Context:在正确时间提供正确信息
JIT Context 的核心原则是:不在一开始就塞满上下文,而是在每一步只注入当前需要的信息。就像 JIT 生产--零库存,按需供应。
流程:任务开始(只加载 System Prompt) → 执行步骤 N → 需要什么信息? →[需要额外信息] 按需检索记忆(只取当前步骤需要的) → 注入最小必要上下文(不超不缺) → LLM 推理+行动 →[下一步] 执行步骤 N
# 记忆生命周期管理
记忆不是一次性写入就完事的,它有完整的生命周期,每个阶段都需要工程化治理:
- 写入(Write):记录交互内容,附带元数据(时间、类型、用户ID、重要性评分)。写入时需决策:哪些内容值得存储?全部存储会导致噪音过多,只存关键内容则可能遗漏。策略:默认全部写入,后续通过索引和淘汰来管理质量。
- 索引(Index):为记忆建立索引,加速后续检索。包括:向量索引(Embedding→向量数据库),关键词索引(BM25→全文检索),时间索引(按时间范围快速过滤),类型索引(reflection/episodic/interaction 分类标签)。多索引 = 多路召回 = 更高命中率。
- 检索(Retrieve):根据当前任务需求,从记忆库中找到最相关的信息。JIT 原则:只检索当前步骤需要的,不提前加载。检索策略:多路召回(向量+关键词+时间),相似度阈值过滤(距离 < 0.7),元数据过滤(按类型/用户/时间范围),Top-K 截断(取最相关的 K 条)。
- 压缩(Compress):上下文超阈值时触发压缩。压缩策略优先级:语义保护型 > 选择性保留 > 摘要 > 滑动窗口 > 截断。压缩时锁定:System Prompt 不压缩,行动-结果对成对保留。压缩后内容存入长期记忆,原始内容可淘汰。
- 淘汰(Evict):记忆不是永久保留的。淘汰策略:时效淘汰(超过 N 天的低重要性记忆自动清理),容量淘汰(总条数超过上限时淘汰最不重要的),质量淘汰(从未被检索过的记忆降低优先级),用户主动淘汰(用户标记"忘掉这件事")。淘汰前可以先做最终摘要归档。
# 记忆治理策略代码实现
import time
from datetime import datetime, timedelta
class MemoryGovernor:
"""记忆生命周期治理引擎"""
def __init__(self, memory_system):
self.memory = memory_system
self.config = {
"max_short_term_turns": 15, # 短期记忆最大轮次
"compress_threshold": 12, # 触发压缩的轮次
"preserve_last_n": 3, # 压缩时保留最近 N 轮
"preserve_system_prompt": True, # 保护 System Prompt
"eviction_age_days": 90, # 超过 90 天的低重要性记忆淘汰
"eviction_max_count": 10000, # 总记忆条数上限
"retrieval_similarity_threshold": 0.7, # 检索相似度阈值
"retrieval_top_k": 5, # 检索返回 Top-K
"jit_mode": True # JIT 上下文模式
}
self.access_stats = {} # 记忆访问统计(用于淘汰决策)
# ===== 写入阶段 =====
def write(self, content, metadata=None):
"""写入记忆,自动评估重要性"""
importance = self._assess_importance(content)
enriched_meta = {
**(metadata or {}),
"importance": importance,
"created_at": datetime.now().isoformat(),
"access_count": 0,
"last_accessed": None
}
self.memory._save_to_long_term(content, enriched_meta)
self.memory.add_to_short_term(
enriched_meta.get("role", "user"), content
)
def _assess_importance(self, content):
"""用 LLM 快速评估内容重要性(1-5分)"""
# 简化版:基于规则的启发式评估
score = 1
if any(kw in content for kw in ["决策", "偏好", "规则", "约束"]):
score += 2
if any(kw in content for kw in ["失败", "错误", "反思"]):
score += 2
if len(content) > 200: # 长内容通常更重要
score += 1
return min(score, 5)
# ===== 索引阶段 =====
def index(self):
"""确保记忆索引完整且高效"""
# Chroma/Milvus 等向量数据库自动维护向量索引
# 额外维护:时间索引、类型索引、访问频率索引
pass # 由向量数据库自动处理
# ===== 检索阶段(JIT 模式) =====
def retrieve_jit(self, current_step_description):
"""Just-in-Time 检索:只取当前步骤需要的信息"""
if not self.config["jit_mode"]:
# 非 JIT 模式:一次性加载所有相关记忆
return self.memory.retrieve_from_long_term(
current_step_description,
top_k=self.config["retrieval_top_k"]
)
# JIT 模式:分步检索,按需注入
results = self.memory.retrieve_from_long_term(
current_step_description,
top_k=3 # JIT 模式下减少初始检索量
)
# 更新访问统计(用于淘汰决策)
for r in results:
doc_id = r.get("metadata", {}).get("doc_id", "")
if doc_id:
self.access_stats[doc_id] = \
self.access_stats.get(doc_id, 0) + 1
# 相似度过滤
filtered = [
r for r in results
if r["similarity"] >= self.config["retrieval_similarity_threshold"]
]
return filtered
# ===== 压缩阶段 =====
def compress_if_needed(self):
"""检查并执行上下文压缩"""
if len(self.memory.short_term) > self.config["compress_threshold"]:
self._semantic_preserving_compress()
def _semantic_preserving_compress(self):
"""语义保护型压缩"""
short = self.memory.short_term
# 分层提取
system_msgs = [m for m in short if m["role"] == "system"]
recent = short[-(self.config["preserve_last_n"] * 2):]
middle = short[len(system_msgs):-(self.config["preserve_last_n"] * 2)]
# 识别行动-结果对(工具调用 + 返回值)
action_pairs = []
i = 0
while i < len(middle):
if middle[i].get("role") == "assistant" and "tool_call" in middle[i].get("content", ""):
if i+1 < len(middle) and middle[i+1].get("role") == "tool":
action_pairs.append(middle[i:i+2])
i += 2
continue
i += 1
# 其余内容生成摘要
remaining = [m for m in middle if m not in
[p[0] for p in action_pairs] + [p[1] for p in action_pairs]]
if remaining:
summary = self.memory._summarize(remaining)
# 重组上下文
compressed = system_msgs.copy()
if remaining:
compressed.append({
"role": "system",
"content": f"[历史摘要]: {summary}"
})
for pair in action_pairs[-3:]: # 保留最近 3 个行动-结果对
compressed.extend(pair)
compressed.extend(recent)
self.memory.short_term = compressed
# ===== 淘汰阶段 =====
def evict(self):
"""执行记忆淘汰策略"""
# 1. 时效淘汰:超过 N 天且重要性 <= 2 的记忆
cutoff = datetime.now() - timedelta(days=self.config["eviction_age_days"])
# 向量数据库不支持直接按元数据范围删除,需要查询+批量删除
# 此处为简化逻辑示意
# 2. 容量淘汰:总条数超过上限时,淘汰最少访问的
if self.access_stats:
least_accessed = sorted(
self.access_stats.items(),
key=lambda x: x[1]
)[:100]
# 从向量数据库中删除 least_accessed 的条目
# 3. 质量淘汰:从未被检索过的记忆
never_accessed = [
k for k, v in self.access_stats.items() if v == 0
]
# 从向量数据库中删除 never_accessed 的条目
# ===== 使用示例 =====
governor = MemoryGovernor(MemorySystem())
# 写入记忆(自动评估重要性)
governor.write("用户偏好简洁的中文回答", metadata={"type": "preference"})
# JIT 检索
context = governor.retrieve_jit("分析销售数据趋势")
# 检查是否需要压缩
governor.compress_if_needed()
# 执行淘汰
governor.evict()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
✅ 记忆治理的黄金法则
- 写入要慷慨:宁可多存,后续靠检索和淘汰来筛选
- 检索要精准:JIT 原则,只在需要时取需要的量
- 压缩要保守:保护核心指令和行动-结果对,只压缩中间推理
- 淘汰要渐进:先降优先级再删除,给"冷记忆"一个回暖的机会
- 指标要量化:追踪命中率、压缩率、检索延迟,用数据驱动治理策略
# 5.9 长期记忆的静态与动态分类
长期记忆不是铁板一块。仔细观察就会发现,有些记忆几乎不变,有些记忆持续更新。把长期记忆分为静态长期记忆和动态长期记忆,是面试中的高频考点,也是工程落地时的关键设计决策。
# 为什么要做这个分类?
如果所有长期记忆都用同一种策略管理,会出现两类问题:
❌ 不分类的后果
- 存储浪费:用户偏好这种不变的信息,如果存入向量数据库每次做语义检索,检索开销大且没有必要--它永远都该被注入
- 信息过时:历史对话摘要这种高频更新的信息,如果像用户偏好一样写入就不管了,很快就会变得过时和冗余
- 召回混乱:不同性质的记忆混在一起检索,模型很难区分这是永远有效的规则还是这是上次的经验
所以,分类的核心目的是让不同性质的记忆用不同的管理策略,从而提升存储效率、召回精准度和上下文利用率。
# 静态长期记忆:几乎不变的核心知识
静态长期记忆的特点是变化频率极低,写入后几乎不需要更新。它像一本个人手册,定义了 Agent 与用户交互的基本规则。
📖 静态长期记忆的典型内容
- 用户偏好:用户偏好简洁的中文回答、用户不喜欢表格输出--这些偏好一旦确定,几乎不变
- 系统规则:不要生成代码中的硬编码密码、回答必须附带引用来源--规则是确定性的约束
- 领域知识:公司 API 文档的 Base URL 是 https://api.example.com/v2--事实性知识,除非系统变更才更新
# 动态长期记忆:持续更新的经验积累
动态长期记忆的特点是变化频率高,需要持续更新、压缩和淘汰。它像一本工作日记,记录了 Agent 的成长轨迹。
📝 动态长期记忆的典型内容
- 历史对话摘要:之前多轮对话的压缩摘要--每次新对话都可能产生新的摘要,旧的摘要需要定期合并
- 任务执行经验:上次用 pandas 读 CSV 时遇到了编码问题,解决方案是指定 encoding=utf-8--经验会不断积累
- 用户行为模式:用户通常在下午 3 点后提问、用户习惯先问背景再问具体方案--模式需要持续观察和修正
# 分类的意义:不同策略适配不同性质
[流程图: 长期记忆 到 静态长期记忆(偏好/规则/知识) 到 本地配置文件(YAML/JSON);
长期记忆 到 动态长期记忆(摘要/经验/模式) 到 向量数据库(Chroma/Milvus);
静态 到 直接注入 System Prompt; 动态 到 按相关性语义检索;
静态 到 不压缩(本身已精炼); 动态 到 定期压缩摘要+淘汰过时内容]
2
3
4
分类带来三方面的差异化策略:
- 不同的存储策略:静态记忆存本地配置文件(YAML/JSON),读写简单、确定性高;动态记忆存向量数据库,支持语义检索和大规模管理
- 不同的召回策略:静态记忆直接注入 System Prompt,每次对话都有,不需要检索;动态记忆按相关性从向量库检索,只在需要时注入
- 不同的压缩策略:静态记忆本身已经是精炼的表达(一条偏好就是一句话),不需要压缩;动态记忆需要定期压缩摘要、淘汰过时内容
# 对比表格
| 维度 | 静态长期记忆 | 动态长期记忆 |
|---|---|---|
| 典型内容 | 用户偏好、系统规则、领域知识 | 对话摘要、执行经验、行为模式 |
| 变化频率 | 极低,几乎不变 | 高,持续更新 |
| 存储方式 | 本地配置文件(YAML/JSON) | 向量数据库(Chroma/Milvus) |
| 召回方式 | 直接注入 System Prompt | 按相关性语义检索 |
| 压缩策略 | 不压缩(本身已精炼) | 定期压缩摘要 + 淘汰过时内容 |
| 更新触发 | 用户明确修改偏好时 | 每轮有价值交互后 |
✅ 面试要点
面试中回答长期记忆的管理策略时,先分类再谈策略。不要一上来就说存向量数据库,而要先区分:静态记忆存配置文件、直接注入 Prompt;动态记忆存向量库、按需检索、定期压缩。这体现了你对记忆系统不同性质的深刻理解。
关联面试题 Q10
# 5.10 记忆存储触发链路
一个常见误解是:每轮对话都触发长期记忆写入。如果真的每轮都写,长期记忆库会变成完整对话日志的备份--噪音爆炸,检索失效。
记忆写入不是录一切,而是记值得记的。就像人类不会记住每顿饭吃了什么,但会记住第一次吃到惊艳味道的那顿。
# 不是每轮都触发!有筛选机制
记忆写入有明确的触发条件,只有满足条件的信息才进入长期记忆:
🎯 记忆写入触发条件
- 用户表达了新偏好 写入静态长期记忆(如我不喜欢表格输出)
- 完成了一个有价值的任务 写入动态长期记忆(执行经验,如 CSV 编码问题的解决方案)
- 发现了新的上下文信息 写入动态长期记忆(知识片段,如公司的 API 版本升级到 v2 了)
- 简单闲聊 不触发长期记忆写入(如你好、今天天气不错)
# 判断标准:什么信息值得记住?
判断一条信息是否值得写入长期记忆,核心标准是两个问题:
❓ 问题一:是否有长期参考价值?
如果这条信息在未来的对话中还可能被用到,就值得记住。用户的偏好、问题的解决方案、关键的事实信息--这些都可能在下次交互中再次需要。
反例:今天中午吃什么只在当天有价值,不值得写入长期记忆。
❓ 问题二:是否包含决策/偏好/知识点?
如果这条信息包含明确的决策、稳定的偏好或可复用的知识,就值得记住。
反例:嗯嗯好的这种确认性回复不包含任何决策或知识,不值得写入。
# 链路设计:对话信息提取价值评估分类写入
记忆写入是一个五步链路,每一步都有明确的职责:
[流程图: 用户对话 信息提取 价值评估 分类 写入静态记忆/写入动态记忆/跳过]
✅ 面试要点
面试中回答记忆写入机制时,重点强调筛选而非全量。链路是:对话信息提取价值评估分类写入。不是每轮都触发,只有有长期参考价值的信息才写入。简单闲聊不触发,偏好/规则写入静态记忆,经验/知识写入动态记忆。
关联面试题 Q11
# 5.11 记忆召回决策与上下文污染防控
记忆召回的核心问题不是能不能找到,而是该不该注入。召回太多无关记忆,比没有记忆更危险--这就是上下文污染。
上下文污染就像往一杯清水里倒泥浆--信息越多,水质越差。精准的召回不是把所有相关的都找出来,而是只把最必要的放进去。
# 召回决策机制:如何判断哪些记忆需要召回?
大模型判断哪些长期记忆需要召回,依赖三个核心维度的综合评分:
🎯 意图匹配
当前对话意图与记忆标签的语义相似度。
⏰ 时间衰减
越近期的记忆权重越高。时间衰减函数:weight x e^(-λ x Δt)
📊 频率加权
多次被召回验证有效的记忆权重更高。
综合评分公式:
recall_score = intent_similarity x w1 + time_decay_weight x w2 + frequency_weight x w3
其中:
intent_similarity = cosine_similarity(query_embedding, memory_embedding)
time_decay_weight = e^(-λ x days_since_creation)
frequency_weight = log(1 + successful_recall_count)
w1 + w2 + w3 = 1(权重归一化)
2
3
4
5
6
7
# 召回数量限制:不是越多越好
❌ 召回过多的三大危害
- 注意力稀释:模型关注无关内容,忽略核心问题
- 指令冲突:多条记忆互相矛盾
- Token浪费:无关记忆消耗宝贵的上下文窗口
# 上下文污染防控策略
防控上下文污染需要四道防线:
防线 1 - 数量上限:每次召回不超过 5 条记忆。简单问题 3 条,复杂问题 5 条。
防线 2 - 相关性阈值:相似度低于 0.7 的记忆不召回。
防线 3 - 冲突检测:多条记忆互相矛盾时,优先选择最新的。
防线 4 - 优先级排序:核心指令 > 最近经验 > 补充信息。
2
3
4
✅ 面试要点
面试中回答记忆召回策略时,先谈召回决策再谈污染防控。召回决策靠三维度评分(意图匹配时间衰减频率加权),污染防控靠四道防线(数量上限5、相关性阈值0.7、冲突检测取最新、优先级排序核心>经验>补充)。不要只说用向量搜索找相关记忆--那只是第一步,真正的难点是防控上下文污染。
关联面试题 Q12
# 5.11a Prompt 工程:静态框架与动态组装
把 Prompt 想象成一栋建筑:静态部分是地基和框架(角色定义、安全规则、工具列表)--不会随对话变化;动态部分是家具和装饰(记忆片段、对话历史、当前任务)--每轮都换。
# 静态 Prompt:不随对话变化的固定框架
🔒 静态 Prompt 的四个组成部分
- 角色定义:你是一个专业的代码审查专家--定义 Agent 身份
- 安全规则:不要泄露用户隐私、不要执行危险命令--行为边界
- 输出格式约束:回答格式:风险等级问题修复建议--控制输出
- 工具/Skill 描述:Function Calling Schema + Skill 触发词列表--Agent 的能力菜单
# 动态 Prompt:每轮对话变化的活的内容
🔄 动态 Prompt 的三个来源
- 长期记忆召回:根据当前意图检索的5条相关记忆
- 对话历史:累积的多轮交互记录(每轮增加)
- 当前任务上下文:用户刚输入的指令 + 工具返回结果
# 每轮对话的完整 Prompt 组装流程
Layer 1: 静态框架(System Prompt + 工具Schema + Skill触发词) 100%缓存命中
Layer 2: 记忆注入(长期记忆召回 + Skill完整指令 + 静态记忆偏好) 部分命中
Layer 3: 对话历史(多轮交互记录 + 工具返回结果) 旧部分可缓存
Layer 4: 当前请求(用户输入) 完全不命中缓存
2
3
4
✅ 面试要点
组装策略:固定半固定动态当前请求,四层叠放。核心原则:不变的放前面最大化缓存,变化的放后面最小化缓存失效。
# 5.11b 多轮对话状态爆炸与完整解决方案
状态爆炸不是某一个环节的问题,而是整个系统的压力累积。解决方案不是堵一个洞,而是设计一套泄压系统。
# 完整解决方案:四层泄压体系
Layer 1: 预防(预算控制+JIT原则) 超阈值触发
Layer 2: 压缩(语义保护型压缩) 压缩后仍超
Layer 3: 淘汰(记忆治理+上下文裁剪) 淘汰后仍超
Layer 4: 兜底(紧急压缩+降级回答)
2
3
4
🛡️ Layer 1: 预防--不让压力累积
- Token Budget:设定全局上限(如20万Token),实时累加,超预算立即停止
- JIT召回原则:只检索当前步骤需要的记忆,不提前加载全量
- 输出长度约束:要求模型只输出关键结论
- 工具结果精简:工具返回后先做格式化过滤
📦 Layer 2: 压缩--在信息完整的前提下缩减体积
- 语义保护型压缩:锁定System Prompt+原始问题,保护行动-结果对完整性,只压缩中间推理
- 三层压缩触发:主动压缩(80%阈值)后台压缩(60%)反应式压缩(413状态码)
- 增量压缩:只压缩上次压缩后新增的部分
- 对话历史摘要:旧对话压缩为摘要存入长期记忆
🗑️ Layer 3: 淘汰--扔掉不再需要的
- 记忆淘汰策略:时效淘汰 + 容量淘汰 + 质量淘汰
- 召回数量硬限制:每次召回5条,简单问题3条
- 工具结果裁剪:长输出只保留Top-K关键行
- 淘汰前归档:淘汰的记忆做最终摘要存入归档层
🚨 Layer 4: 兜底--当所有策略都不够时
- 紧急压缩(413反应式):只保留System Prompt + 最近1轮 + 任务摘要
- 降级回答:告诉用户当前对话过长,建议开启新会话
- 自动会话切分:超过阈值(如50轮)后自动摘要+开新会话
- 人工介入:预算超限+压缩失败后通知用户
✅ 面试要点:四层泄压体系答题模板
面试回答多轮对话状态爆炸时,不要只说压缩上下文,要给系统级方案:四层泄压体系递进式处理。不是某一层单独起作用,而是四层递进。预防失败压缩兜住,压缩不够淘汰兜住,淘汰也不够紧急兜底。
# 5.12 工程深度:生产级上下文压缩系统
# 5.12.1 轮次分割算法
压缩的第一步是理解对话的结构。对话不是扁平的消息列表,而是由多个轮次组成--每个轮次以 user message 开始,包含后续的所有 assistant/tool 消息。
关键细节:inline tool_result 不开新轮次。Anthropic 格式中,工具结果以 role: user, content: type: tool_result 形式出现。如果把它当新轮次,会把一个完整的AI调工具获取结果继续推理轮次错误地拆成两半。
# 5.12.2 三种压缩触发模式
| 模式 | 触发条件 | 执行方式 | 阻塞对话 | 压缩力度 |
|---|---|---|---|---|
| 主动压缩 | token 占比 > 80% | 同步执行 | 是(短暂) | 中等(保留近期 3-5 轮) |
| 后台压缩 | token 占比 > 60% | 异步执行 | 否 | 中等 |
| 反应式压缩 | API 返回 413 | 同步紧急执行 | 是 | 激进(只保留 1-2 轮) |
# 5.12.3 AI 摘要 vs 规则摘要
AI 摘要:用专门的压缩 Prompt 让 AI 生成 500 字以内的摘要,保留:用户意图、文件操作、错误信息与解决方案、关键决策、当前进度。质量高但消耗 token。
规则摘要:纯规则提取关键信息,不调 AI。质量低但零成本、零延迟。
降级策略:AI 摘要失败(API 错误、超时、返回空)时自动降级为规则摘要。因为压缩是基础设施--如果压缩本身都能失败,那对话就没法继续了。
# 5.12.4 增量压缩
问题:如果每次 token 超过 80% 就压缩,压缩后降到 50%,用户继续对话又涨到 80%,又要压缩......这会导致频繁压缩,每次压缩都消耗 token。
解决方案:增量压缩阈值--只有当 token 再次上升到上次压缩水平的 80% 时才再次压缩。
# 5.12.5 Provider/Reducer 可插拔架构
WaLiCode 的上下文管理采用Provider/Reducer 可插拔架构:
Provider 模式:可插拔的上下文来源,每个 Provider 负责一种类型的上下文(文件上下文、任务上下文、里程碑上下文)。
Reducer 模式:可插拔的裁剪策略,按优先级裁剪消息。
消息优先级:Critical(错误消息、系统指令、当前任务) > High(文件操作、用户配置) > Medium(工具调用结果、AI 推理过程) > Low(AI 长文本解释、闲聊内容)
# 5.12.6 双作用域记忆系统
| 维度 | User Scope(用户级) | Project Scope(项目级) |
|---|---|---|
| 作用范围 | 跨项目共享 | 仅当前项目 |
| 存储内容 | 用户偏好、通用知识、历史决策 | 架构决策、模块依赖、编码规范 |
| 容量限制 | LRU 淘汰,最多 20 条 | LRU 淘汰,最多 20 条 |
| 示例 | 用户喜欢用 TypeScript | 本项目用 pnpm,不要用 npm |
🔗 5.12 核心要点
轮次分割:按 user message 分割对话为多个轮次,inline tool_result 不开新轮次。 三层压缩触发:主动(80%) 后台(60%) 反应式(413),层层递进。 AI 摘要 vs 规则摘要:AI 质量高但贵,规则零成本但糙,AI 失败降级为规则。 增量压缩:上次压缩水平的 80% 才再压缩,避免频繁压缩浪费 token。 Provider/Reducer 架构:可插拔设计,按场景组合不同的上下文来源和裁剪策略。 双作用域记忆:User Scope 跨项目,Project Scope 仅当前项目,LRU 淘汰。
# 5.13 主流记忆框架对比(2026更新)
# Mem0:三层记忆架构(Episodic + Semantic + Procedural)
💡 Mem0 的三层记忆
- Episodic(情景记忆):存储具体的交互片段,按时间线组织,支持回溯。
- Semantic(语义记忆):从交互中提取的结构化知识,去重后存储,支持增量更新。
- Procedural(过程记忆):从历史交互中总结的怎么做类知识。
Mem0 的工作流程:对话输入 LLM 提取记忆 分类(episodic/semantic/procedural) 去重/合并 存储。三层是逻辑分类,底层可以共用同一个向量库。
# Letta(原 MemGPT):自编辑记忆 + 记忆分页
💡 Letta 的两大核心机制
- 自编辑记忆(Self-Editing Memory):Agent 可以主动修改自己的记忆。
- 记忆分页(Memory Paging):Core Memory(常驻上下文,类似RAM)和 Archival Memory(外部存储,类似磁盘)。
# Zep:时序知识图谱 + 自动摘要
💡 Zep 的两个核心设计
- 时序知识图谱(Temporal Knowledge Graph):把对话中的实体、关系、事件抽取为带时间戳的三元组。
- 自动摘要(Auto-Summarization):每轮对话后自动生成摘要,旧摘要随时间衰减,新摘要权重更高。
# 三者对比
| 维度 | Mem0 | Letta (MemGPT) | Zep |
|---|---|---|---|
| 记忆组织 | 三层分类(episodic/semantic/procedural) | 两级分页(core/archival) | 时序知识图谱 |
| 记忆更新 | LLM 提取 + 去重合并 | Agent 自编辑(工具调用) | 图谱增量 + 自动摘要 |
| 检索方式 | 向量相似度(分层召回) | Core 直接注入 + Archival 语义搜索 | 图谱查询 + 向量检索 + 时序过滤 |
| 核心优势 | 分类清晰,开箱即用 | Agent 自主管理记忆,最像OS | 支持时序查询,结构化最强 |
| 适用场景 | 通用对话 Agent、客服 | 长期伴随型 Agent、个人助手 | 需要时序推理、实体关系追踪 |
| 开源协议 | Apache 2.0 | Apache 2.0 | Apache 2.0 |
🔗 5.13 核心要点
Mem0:三层记忆(episodic/semantic/procedural)按性质分类,LLM 自动提取+去重,适合通用场景。 Letta:OS 式记忆分页(core/archival),Agent 自编辑记忆,最接近Agent 自我管理的理念。 Zep:时序知识图谱 + 自动摘要,支持什么时候什么关系类时序查询,结构化最强。 选型建议:通用对话选 Mem0,长期伴随选 Letta,需要时序推理选 Zep。
# 面试八股
# Q1: Agent 的记忆系统分为哪几层?各自的特点和实现方式?
A: 短期记忆 = LLM 上下文窗口,存当前对话历史。容量 4K~200K token,速度最快,对话结束即消失。 工作记忆 = Scratchpad/变量,存当前任务的中间状态。任务结束清除,辅助多步推理。 长期记忆 = 向量数据库,存历史交互经验。理论无限容量,需语义检索,永久持久化。
三者关系:短期工作(任务相关提取),短期长期(压缩存储),长期工作(检索回忆)。
# Q2: 短期记忆的压缩策略有哪些?各有什么优缺点?
A: 1 滑窗截断:保留最近 N 轮,丢弃旧的。简单但可能丢失关键信息。 2 摘要压缩:用 LLM 对旧对话生成摘要替代原文。保留关键信息但消耗额外 token。 3 选择性保留:用 LLM 判断消息重要性,只保留重要的。信息密度高但判断可能不准。 4 分层压缩:结合以上策略,近期完整保留、中期摘要、远期存入长期记忆。效果最好但实现最复杂。
# Q3: 向量数据库在 Agent 记忆中的作用?检索流程是什么?
A: 作用:作为长期记忆的存储引擎,支持语义相似度检索。
写入流程:交互文本 Embedding 模型 向量 存入向量数据库(附带元数据如时间、用户ID)。
检索流程:用户查询 Embedding 在向量库中做相似度搜索(如余弦相似度) 返回 Top-K 最相关记忆 注入 prompt。
# Q4: 反思记忆、实体记忆、情景记忆分别是什么?
A: 反思记忆:定期对历史交互进行反思,提取规律和偏好,存储提炼后的知识而非原始对话。 实体记忆:以实体为中心组织记忆,维护实体知识图谱,实现实体级别的快速回忆。 情景记忆:按时间线存储情节,带时间戳和上下文标签,适合时间敏感场景。
# Q5: 为什么不用 SQL 数据库而用向量数据库存 Agent 记忆?
A: 记忆检索是语义相似度查询,SQL 无法高效做语义相似度搜索。向量数据库专门优化了高维向量的相似度搜索,能在毫秒级从百万条记忆中找到最相关的几条。实践中也可用 pgvector(PostgreSQL 扩展)兼顾两者。
# Q6: 上下文膨胀(Context Rot)是什么?五种压缩策略的优先级如何排序?
A: Context Rot:长任务中上下文不断膨胀,导致行为漂移、注意力稀释、成本飙升和窗口溢出。
五种压缩策略优先级(从最优到最差): 1 语义保护型:锁定核心指令 + 保护行动-结果对完整性,只压缩中间推理。 2 选择性保留:按消息类型分别设定保留策略,信息密度高。 3 摘要压缩:有损压缩,保留关键信息但可能丢失细节。 4 滑动窗口:按时间丢弃,不考虑重要性。 5 截断:最简单但最粗暴,可能导致行为不一致。
# Q7: Context Engineering 和 Prompt Engineering 的区别是什么?
A: Prompt Engineering:关注单次请求的措辞优化,是静态的、一次性的。 Context Engineering:关注整个上下文的动态编排,是持续性的、动态的。 类比:Prompt Engineering 是写一首诗,Context Engineering 是管理一个图书馆。
# Q8: 记忆生命周期包含哪些阶段?
A: 五个阶段:1 写入 2 索引 3 检索 4 压缩 5 淘汰。每个阶段都需要工程化治理。
# Q9: Reflexion 框架的反思循环是如何工作的?
A: 1 接收任务 2 执行行动(ReAct循环) 3 评估结果 成功则记录为情景记忆;失败则4 触发反思 5 存入反思记忆 6 下次重试时参考经验教训。关键创新:不是简单重试,而是从失败中学习。
# Q10: 长期记忆为什么要分为静态和动态?
A: 静态长期记忆(偏好/规则/知识):变化频率低 存本地配置文件 直接注入 System Prompt 不压缩。 动态长期记忆(摘要/经验/模式):变化频率高 存向量数据库 按相关性语义检索 定期压缩摘要+淘汰过时内容。 面试要点:先分类再谈策略,不要一上来就说存向量数据库。
# Q11: 为什么不是每轮对话都触发长期记忆写入?
A: 否则长期记忆库会变成完整对话日志的备份,噪音爆炸,检索失效。判断值得记住的两个标准:1 是否有长期参考价值; 2 是否包含决策/偏好/知识点。链路:对话信息提取价值评估分类写入。重点强调筛选而非全量。
# Q12: 召回太多长期记忆会导致什么问题?
A: 三大危害:注意力稀释、指令冲突、Token浪费。四道防线:1数量上限(5条);2相关性阈值(0.7);3冲突检测(取最新);4优先级排序(核心>经验>补充)。
# Q13: 多轮对话状态爆炸怎么解决?
A: 三板斧:1 规范状态结构(固定状态模板);2 智能截断(按优先级保留);3 对话摘要(老旧历史压缩为摘要)。
# Q14: 动态 Prompt 和静态 Prompt 有什么区别?
A: 静态Prompt = System Prompt + 工具Schema + Skill触发词,整个会话不变,100%命中缓存。 动态Prompt = 记忆召回 + 对话历史 + 用户输入,每轮都变。 组装策略:四层叠放,固定半固定动态当前请求。
# Q15: 四层泄压体系是什么?
A: Layer 1 预防(预算控制+JIT召回) Layer 2 压缩(语义保护型) Layer 3 淘汰(记忆治理) Layer 4 兜底(紧急压缩+降级回答)。递进式处理,预防失败压缩兜住,压缩不够淘汰兜住,淘汰也不够紧急兜底。
# Q16: Mem0 的三层记忆是什么?
A: Episodic(情景记忆):存储具体交互片段。Semantic(语义记忆):提取的结构化知识。Procedural(过程记忆):总结的怎么做类技能知识。三层是逻辑分类而非物理分区。
# Q17: Letta 和 Zep 各有什么特点?
A: Letta:OS 式记忆分页(core/archival) + Agent 自编辑记忆。Zep:时序知识图谱 + 自动摘要,支持时序和关系查询。
# 课后练习
# 第 1 题(单选)
Agent 的短期记忆通常存储在哪里?
A. 向量数据库 B. LLM 上下文窗口 C. 关系型数据库 D. 文件系统
答案与解析
答案: B
解析: 短期记忆 = LLM 上下文窗口,存当前对话历史。容量受限于模型窗口大小(4K~200K token)。
# 第 2 题(单选)
长期记忆检索时使用什么技术找到相关记忆?
A. SQL LIKE 模糊查询 B. 正则表达式匹配 C. 向量相似度搜索(如余弦相似度) D. 全文索引
答案与解析
答案: C
解析: 长期记忆通过向量相似度搜索检索。文本先转为向量,再在向量空间中计算距离(如余弦相似度),找到语义最相关的记忆。
# 第 3 题(单选)
当对话历史超过 LLM 上下文窗口限制时,摘要压缩策略的做法是?
A. 直接丢弃最旧的消息 B. 用 LLM 对旧对话生成摘要,用摘要替代原始内容 C. 扩大 LLM 的上下文窗口 D. 停止对话,清空所有历史
答案与解析
答案: B
解析: 摘要压缩策略用 LLM 对旧对话生成摘要,用摘要替代原始内容。这样既控制了上下文长度,又保留了关键信息。
# 第 4 题(单选)
工作记忆(Working Memory)的主要用途是什么?
A. 存储所有历史对话 B. 存储用户的个人偏好 C. 存储当前任务的中间状态和推理过程 D. 存储向量数据库的索引
答案与解析
答案: C
解析: 工作记忆类似便签纸,存储当前任务的中间状态和推理过程。任务结束后清除,辅助多步推理。
# 第 5 题(单选)
反思记忆(Reflective Memory)的核心思想是?
A. 存储所有原始对话记录 B. 定期对历史交互进行反思,提取规律和偏好 C. 只存储用户的错误操作 D. 按时间戳排序存储记忆
答案与解析
答案: B
解析: 反思记忆不仅存储原始交互,还定期让 LLM 对历史交互进行反思,提取规律和偏好(如用户偏好简洁回答),存储提炼后的知识。
# 第 6 题(单选)
在五种上下文压缩策略中,哪种策略能锁定核心指令并保护行动-结果对的完整性?
A. 截断(Truncation) B. 滑动窗口(Sliding Window) C. 摘要(Summarization) D. 语义保护型压缩(Semantic-Preserving)
答案与解析
答案: D
解析: 语义保护型压缩锁定核心指令(System Prompt不压缩),确保行动-结果完整性(工具调用与返回值成对保留),只压缩中间推理过程。
# 第 7 题(单选)
Context Engineering 与 Prompt Engineering 的核心区别是什么?
A. Context Engineering 只关注 System Prompt 的措辞 B. Prompt Engineering 关注整个上下文的动态编排 C. Context Engineering 关注整个上下文的动态编排,而 Prompt Engineering 关注单次请求的措辞优化 D. 两者完全相同,只是名称不同
答案与解析
答案: C
解析: Context Engineering 关注整个上下文的动态编排(记忆检索/压缩/淘汰、信息优先级排序),是持续性的、动态的。Prompt Engineering 关注单次请求的措辞优化,是静态的、一次性的。
# 第 8 题(多选)
以下哪些是主流的向量数据库?(多选)
A. Chroma B. Pinecone C. MySQL D. Milvus
答案与解析
答案: A, B, D
解析: Chroma、Pinecone、Milvus 都是主流向量数据库。MySQL 是关系型数据库,虽然可通过 pgvector 扩展支持向量,但 MySQL 本身不是向量数据库。
# 第 9 题(多选)
关于长期记忆的写入流程,以下哪些步骤是正确的?(多选)
A. 将交互文本通过 Embedding 模型转为向量 B. 将向量存入向量数据库 C. 附带元数据(时间、用户ID等) D. 对每条记忆进行人工审核
答案与解析
答案: A, B, C
解析: 写入流程:文本Embedding存入向量数据库(附带元数据)。不需要人工审核,是自动化的过程。
# 第 10 题(多选)
短期记忆管理中,分层压缩策略结合了哪些方法?(多选)
A. 近期对话完整保留 B. 中期对话做摘要压缩 C. 最旧的内容存入长期记忆 D. 删除所有旧内容不保留
答案与解析
答案: A, B, C
解析: 分层压缩:近期完整保留 + 中期摘要 + 远期存入长期记忆。不是简单删除,而是分层处理。
# 第 11 题(多选)
记忆生命周期包含以下哪些阶段?(多选)
A. 写入(Write) B. 索引(Index) C. 检索(Retrieve) D. 压缩(Compress) E. 淘汰(Evict)
答案与解析
答案: A, B, C, D, E
解析: 记忆生命周期完整包含五个阶段:写入索引检索压缩淘汰。
# 第 12 题(多选)
Reflexion 框架的反思循环中,任务失败后会执行哪些步骤?(多选)
A. 直接丢弃失败记录并重试 B. 自我反思,分析失败原因 C. 将反思结果存入反思记忆 D. 重新执行时参考过往反思经验
答案与解析
答案: B, C, D
解析: Reflexion 反思循环:失败后触发自我反思将反思存入记忆重新执行时参考过往经验。不是简单丢弃重试,而是从失败中学习。
# 第 13 题(单选)
静态长期记忆和动态长期记忆的主要区别是?
A. 静态记忆存向量数据库、动态记忆存本地配置 B. 静态记忆变化频率低存配置文件直接注入Prompt,动态记忆变化频率高存向量库按相关性检索 C. 两者存储方式相同但压缩策略不同 D. 静态记忆需要定期压缩,动态记忆不需要压缩
答案与解析
答案: B
解析: 静态长期记忆(偏好/规则/知识)变化频率极低存本地配置文件直接注入System Prompt不压缩。动态长期记忆(摘要/经验/模式)变化频率高存向量数据库按相关性语义检索定期压缩摘要+淘汰。
# 第 14 题(单选)
静态 Prompt 和动态 Prompt 的核心区别是什么?
A. 静态Prompt只用于System Prompt,动态Prompt只用于User Prompt B. 静态Prompt整个会话不变放最前面100%命中缓存,动态Prompt每轮变化放后面按变化频率分层 C. 静态Prompt和动态Prompt只是不同命名没有实质区别 D. 静态Prompt不能包含工具定义,动态Prompt才能包含
答案与解析
答案: B
解析: 静态Prompt(角色+规则+工具Schema+Skill触发词)整个会话不变,放Prompt最前面100%命中缓存。动态Prompt(记忆召回+对话历史+用户输入)每轮都变,放后面按变化频率分层组装。
# 第 15 题(单选)
多轮对话状态爆炸的四层泄压体系中,Layer 4 兜底策略包含什么?
A. Token Budget全局预算控制 B. 语义保护型压缩和三层压缩触发 C. 记忆治理和召回数量硬限制 D. 紧急压缩(413)+ 降级回答 + 自动会话切分 + 人工介入
答案与解析
答案: D
解析: Layer 4是兜底层,当预防+压缩+淘汰都不够时启动:紧急压缩(413触发最激进压缩)+ 降级回答 + 自动会话切分 + 人工介入。其他选项分别对应Layer 1、2、3。
# 第 16 题(多选)
Prompt动态组装的四层叠放顺序中,哪些层可以命中缓存?(多选)
A. Layer 1 静态框架(System Prompt + 工具Schema + Skill触发词) B. Layer 2 记忆注入(长期记忆召回片段) C. Layer 3 对话历史(多轮交互记录+工具返回结果) D. Layer 4 当前请求(用户输入)
答案与解析
答案: A, B, C
解析: Layer 1固定不变100%命中缓存,Layer 2同会话内部分命中缓存,Layer 3的旧部分可命中缓存。Layer 4每次不同完全不命中缓存。
# 第 17 题(单选)
Mem0 的三层记忆中,Procedural Memory 存储的是什么类型的信息?
A. 具体的交互片段,如用户上周问了什么 B. 从交互中提取的结构化知识,如用户偏好简洁风格 C. 从历史交互中总结的怎么做类技能知识,如处理JSON错误先检查编码 D. 所有原始对话记录的备份
答案与解析
答案: C
解析: Procedural Memory(过程记忆)存储从历史交互中总结的技能类知识。Episodic 存具体交互片段,Semantic 存结构化知识。
# 第 18 题(单选)
Letta(MemGPT)的记忆分页机制中,Core Memory 和 Archival Memory 的关系类似操作系统的什么概念?
A. 进程和线程 B. RAM(内存)和磁盘(外部存储) C. 缓存和数据库 D. 内核空间和用户空间
答案与解析
答案: B
解析: Letta 借鉴 OS 虚拟内存机制:Core Memory 类似 RAM 常驻上下文,Archival Memory 类似磁盘外部存储。