vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
---
|
||||
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中的应用]]
|
||||
Reference in New Issue
Block a user