211 lines
8.0 KiB
Markdown
211 lines
8.0 KiB
Markdown
---
|
||
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中的应用]]
|