Files
autumn-recruitment/06.AI/agent-design/上下文工程与记忆管理.md
T

211 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [ai/agent, rag, context-engineering, memory-management, prompt-compression]
create time: 2026-08-08 18:43
update time: 2026-08-08 18:43
---
# 上下文工程与记忆管理
## 概述
LLM 本身是无状态的,它的"知识"完全依赖每次请求时注入的 Prompt Context。上下文工程的本质是在有限的 token 预算内,最大化信息密度和相关性。记忆管理则是跨会话持久化用户偏好和事实,让 Agent 具备成长能力。
## 详解
### RAG 流程:从文档到可检索的向量
RAG(Retrieval-Augmented Generation)将外部知识库与生成式模型结合,解决 LLM 知识截止和幻觉问题。完整链路如下:
```mermaid
graph LR
A["原始文档"] --> B["Chunking\n分块处理"]
B --> C["Embedding\n向量化"]
C --> D["向量数据库"]
D --> E["Top-K 相似度检索"]
E --> F["Prompt Context 组装"]
F --> G["LLM 生成答案"]
```
#### Chunking 分块策略
把长文档切分为适合 Embedding 的片段是关键,直接影响检索精度:
| 策略 | 实现方式 | 优缺点 |
|------|---------|--------|
| 固定长度 | 按字符数 N 切割,重叠 M | 简单快速;可能截断语义 |
| 语义边界 | 在标题、段落间切割 | 保持结构完整;需启发式规则 |
| 递归分块 | Markdown 级别逐层深入 | Hierarchy-aware;实现复杂 |
| 文档感知 | 基于 PDF/Docx 解析的结构信息 | 最精准;依赖文档格式质量 |
实际项目中推荐**递归分块 + 重叠**:先按 Markdown 层级分,同一块超过阈值再按句子切,相邻块保留 10-15% 重叠区避免关键句被拆分。
#### Embedding → 向量存储
将文本映射为高维稠密向量(通常 768-1536 维),然后存入向量数据库:
```go
// Go 示例:Embedding + 存储到 pgvector
func StoreDocument(ctx context.Context, db *sql.DB, doc string) error {
// 1. 调用 Embedding API 获取向量
vec, err := embeddingClient.Embed(ctx, doc)
if err != nil {
return err
}
// 2. 存入 PostgreSQL + pgvector
_, err = db.ExecContext(ctx,
`INSERT INTO documents (title, content, embedding)
VALUES ($1, $2, $3::vector)`,
doc.Title, doc.Content, pq.Array(vec))
return err
}
```
> [!NOTE]
> 向量维度选择是权衡:768 维够用且快,1536 维更精细但消耗更多内存和计算资源。国内常用的 text-embedding-v3 默认 1536 维。
#### Top-K 检索与重排
检索不是终点——召回 Top-K 后还需要精排:
```
检索阶段(召回): BM25 / 向量相似度 → 取 Top 50
↓
重排阶段(精排): Cross-Encoder 对 50 条打分 → 取 Top 5
↓
过滤阶段: 关键词匹配校验 + 去重 → 最终 Top 3 注入 Prompt
```
Cross-Encoder 比 Single-Encoder 精度高约 10-15%,但速度慢 20 倍,所以只做二次筛选而非全量检索。
#### Prompt Context 组装策略
组装顺序和内容直接影响生成质量:
1. **系统提示**(固定,不可变):定义 Agent 角色和行为准则
2. **事实上下文**(动态检索):从向量库查得的 Top-K 文档片段
3. **对话历史**(滑动窗口):最近 N 轮消息
4. **用户最新提问**
组装优先级:**系统提示 > 事实上下文 > 历史对话**
> [!WARNING]
> 不要盲目把全部 Top-K 塞进 Prompt。token 预算有限,超出的部分会触发 truncation 丢失重要信息。建议按相关度降序填充,直到触及 token 上限为止。
### Prompt 压缩方法
当检索结果过多或对话历史过长时,需要压缩上下文以节省 token:
| 方法 | 原理 | 效果 |
|------|------|------|
| 信息密度排序 | 按每 token 包含的事实数量排序,保留高密度片段 | 保留核心信息,丢弃废话 |
| 摘要浓缩 | 用 LLM 将多个片段合并为一句话摘要 | 显著缩减长度,但有信息损失 |
| Token 池修剪 | 计算每个片段的 Self-Influence Score,剔除冗余 | 理论最优,需额外推理开销 |
| 滑动窗口 | 只保留最近 N 轮对话 | 最简单但丢弃早期关键信息 |
```go
// Go 示例:基于信息密度的 Context 压缩
type Compressor struct {
model *llm.Client
}
func (c *Compressor) Compress(ctx context.Context, segments []Segment) []Segment {
// 计算每个段落的「非停用词比例」作为密度评分
scored := make([]ScoredSegment, len(segments))
for i, seg := range segments {
score := float64(len(stopWordFree(seg.Text))) / float64(len(seg.Text))
scored[i] = ScoredSegment{Segment: seg, Score: score}
}
// 按密度降序排列,取前 K 个
sort.Slice(scored, func(i, j int) bool {
return scored[i].Score > scored[j].Score
})
return takeK(scored, MaxSegments)
}
```
### 记忆管理架构
Agent 的记忆分三层,对应不同的生命周期和查询方式:
```
短期记忆 ──────→ 当前会话内的对话历史 ──────→ 滑动窗口删除
│ │
▼ ▼
长期记忆 ──────→ 跨会话的知识库 ───────────→ 定期增量更新
│ │
▼ ▼
工作记忆 ──────→ 运行时临时状态 ───────────→ 任务结束后清空
```
#### 短期记忆 — 对话历史
维护最近 N 轮的用户-AI 交互记录。实现要点:
- **双端队列**:头部保留最近 N 轮,尾部超过时淘汰
- **Token 预算优先**:不是简单地固定轮数,而是累计 token 不超过配额
- **关键信息提取**:每轮结束时自动提取实体和意图摘要,追加到摘要缓冲区
```go
type ShortTermMemory struct {
messages []*ChatMessage
budget int // token 预算
}
func (m *ShortTermMemory) Add(msg *ChatMessage) {
m.messages = append(m.messages, msg)
for totalTokens(m.messages) > m.budget {
m.messages = m.messages[1:] // 移除最早的一条
}
}
```
#### 长期记忆 — 知识库 + 向量检索
用户在交互中表达的偏好、事实陈述、决策记录会被抽取并持久化:
```
输入: "我偏好使用 Gin 框架,不喜欢 Echo"
↓
NLP 抽取: Entity="Gin", Relation="preferred over", Target="Echo"
↓
存储: INSERT INTO long_term_memory (type, entity, relation, target, created_at)
↓
检索时: 通过关键词搜索 + 向量相似度召回相关记忆片段
```
记忆类型包括:
| 类型 | 内容示例 | 保留策略 |
|------|---------|---------|
| UserPreference | 技术栈偏好、代码风格偏好 | 永久保留,定期去重 |
| FactStatement | "PostgreSQL 支持 JSONB" | 有效期 90 天,可续期 |
| DecisionRecord | "项目选择了 gRPC 而非 REST" | 永久保留 |
| TaskProgress | "已完成认证模块的开发" | 任务结束后归档 |
#### 记忆更新策略
记忆不是一次性写入的,需要考虑:
1. **新增**:新发现的信息插入知识库
2. **修正**:用户说"我之前说错了",标记旧记忆为 obsolete
3. **合并**:多轮对话中提取到的碎片信息聚合为统一事实
4. **衰减**:随时间推移降低旧记忆的权重,`weight *= decay_factor`
> [!TIP]
> 面试常考点:为什么 RAG 不能替代记忆?答案是 RAG 面向的是外部知识(文档、手册),而记忆面向的是个性化数据(用户偏好、历史对话)。两者正交,互补而非互斥。
## 实践场景
**ThumbUP 项目的二级缓存设计** 就是上下文工程的典型应用:
1. 第一级缓存:本地 LRU,命中率最高(热点 query)
2. 第二级缓存:Redis + 向量检索,覆盖中长尾请求
3. HeavyKeeper 算法实时探测热点 key,避免缓存雪崩
这种分层思路同样适用于记忆管理——短期记忆(本地 LRU)+ 长期记忆(远程向量库)。
## 扩展阅读
- [[Eino DAG 工作流设计]]
- [[沙箱权限治理]]
- [[循环状态机在Agent中的应用]]