8.0 KiB
tags, create time, update time
| tags | create time | update time | |||||
|---|---|---|---|---|---|---|---|
|
2026-08-08 18:43 | 2026-08-08 18:43 |
上下文工程与记忆管理
概述
LLM 本身是无状态的,它的"知识"完全依赖每次请求时注入的 Prompt Context。上下文工程的本质是在有限的 token 预算内,最大化信息密度和相关性。记忆管理则是跨会话持久化用户偏好和事实,让 Agent 具备成长能力。
详解
RAG 流程:从文档到可检索的向量
RAG(Retrieval-Augmented Generation)将外部知识库与生成式模型结合,解决 LLM 知识截止和幻觉问题。完整链路如下:
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 示例: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 组装策略
组装顺序和内容直接影响生成质量:
- 系统提示(固定,不可变):定义 Agent 角色和行为准则
- 事实上下文(动态检索):从向量库查得的 Top-K 文档片段
- 对话历史(滑动窗口):最近 N 轮消息
- 用户最新提问
组装优先级:系统提示 > 事实上下文 > 历史对话
Warning
不要盲目把全部 Top-K 塞进 Prompt。token 预算有限,超出的部分会触发 truncation 丢失重要信息。建议按相关度降序填充,直到触及 token 上限为止。
Prompt 压缩方法
当检索结果过多或对话历史过长时,需要压缩上下文以节省 token:
| 方法 | 原理 | 效果 |
|---|---|---|
| 信息密度排序 | 按每 token 包含的事实数量排序,保留高密度片段 | 保留核心信息,丢弃废话 |
| 摘要浓缩 | 用 LLM 将多个片段合并为一句话摘要 | 显著缩减长度,但有信息损失 |
| Token 池修剪 | 计算每个片段的 Self-Influence Score,剔除冗余 | 理论最优,需额外推理开销 |
| 滑动窗口 | 只保留最近 N 轮对话 | 最简单但丢弃早期关键信息 |
// 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 不超过配额
- 关键信息提取:每轮结束时自动提取实体和意图摘要,追加到摘要缓冲区
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 | "已完成认证模块的开发" | 任务结束后归档 |
记忆更新策略
记忆不是一次性写入的,需要考虑:
- 新增:新发现的信息插入知识库
- 修正:用户说"我之前说错了",标记旧记忆为 obsolete
- 合并:多轮对话中提取到的碎片信息聚合为统一事实
- 衰减:随时间推移降低旧记忆的权重,
weight *= decay_factor
Tip
面试常考点:为什么 RAG 不能替代记忆?答案是 RAG 面向的是外部知识(文档、手册),而记忆面向的是个性化数据(用户偏好、历史对话)。两者正交,互补而非互斥。
实践场景
ThumbUP 项目的二级缓存设计 就是上下文工程的典型应用:
- 第一级缓存:本地 LRU,命中率最高(热点 query)
- 第二级缓存:Redis + 向量检索,覆盖中长尾请求
- HeavyKeeper 算法实时探测热点 key,避免缓存雪崩
这种分层思路同样适用于记忆管理——短期记忆(本地 LRU)+ 长期记忆(远程向量库)。