上下文注入在干什么?
前面几步已经完成了检索,拿到了相关文档。但 LLM 并不会自动"知道"这些文档的内容。上下文注入就是把检索到的文档"喂"给 LLM 的过程。
检索结果: 3段相关文档
│
▼
上下文注入: 把文档组织好,拼进 Prompt
│
▼
LLM 读到这些文档,基于它们生成回答
听起来简单,但怎么拼、拼多少、拼什么顺序、加什么指令,直接决定了最终回答的质量。
一、最简单的注入方式
Prompt = 指令 + 检索到的文档 + 用户问题
具体形式:
┌──────────────────────────────────────────────┐
│ 请根据以下参考资料回答用户的问题。 │ ← 系统指令
│ 如果资料中没有相关信息,请如实说明。 │
│ │
│ 【参考资料】 │ ← 检索到的文档
│ [1] iPhone 15 Pro 搭载 A17 Pro 芯片, │
│ 采用3nm工艺,性能提升显著... │
│ [2] 电池容量为3274mAh,支持USB-C充电... │
│ [3] 起售价7999元,有四种颜色可选... │
│ │
│ 【用户问题】 │ ← 用户原始问题
│ iPhone 15 Pro 值不值得买? │
└──────────────────────────────────────────────┘
这就是最基础的上下文注入。但实际工程中远比这复杂。
二、Prompt 结构设计
2.1 基础三段式
┌──────────────────────────────────────────┐
│ System Prompt (系统指令) │
│ 告诉 LLM 它的角色和行为规则 │
├──────────────────────────────────────────┤
│ Context (检索到的上下文) │
│ 从向量数据库检索到的文档片段 │
├──────────────────────────────────────────┤
│ User Query (用户问题) │
│ 用户的原始提问 │
└──────────────────────────────────────────┘
2.2 完整工程级结构
┌──────────────────────────────────────────────────┐
│ System Prompt │
│ ┌────────────────────────────────────────────┐ │
│ │ 角色定义: 你是一个专业的数码产品顾问 │ │
│ │ 行为约束: 只根据提供的资料回答 │ │
│ │ 输出格式: 分点回答,给出明确建议 │ │
│ │ 兜底策略: 资料不足时如实说明 │ │
│ └────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────┤
│ Context (上下文) │
│ ┌────────────────────────────────────────────┐ │
│ │ 元数据: [来源: Apple官网] [更新: 2024-09] │ │
│ │ 内容: iPhone 15 Pro 搭载 A17 Pro 芯片... │ │
│ ├────────────────────────────────────────────┤ │
│ │ 元数据: [来源: 评测文章] [更新: 2024-10] │ │
│ │ 内容: 电池实测续航8.5小时... │ │
│ ├────────────────────────────────────────────┤ │
│ │ 元数据: [来源: 价格平台] [更新: 2024-11] │ │
│ │ 内容: 当前最低价格6899元... │ │
│ └────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────┤
│ Chat History (对话历史, 可选) │
│ ┌────────────────────────────────────────────┐ │
│ │ 用户: 我在考虑换手机 │ │
│ │ 助手: 您目前用的是什么机型? │ │
│ │ 用户: iPhone 13 │ │
│ └────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────┤
│ User Query │
│ ┌────────────────────────────────────────────┐ │
│ │ iPhone 15 Pro 值不值得买? │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
三、上下文注入的核心策略
3.1 文档数量控制(Top-K 选择)
检索返回很多结果,不能全塞进去:
问题: LLM 上下文窗口有限(4K/8K/32K/128K tokens)
检索返回 20 条结果:
#1 score=0.96 "iPhone 15 Pro 芯片..." ← 高度相关
#2 score=0.94 "iPhone 15 Pro 电池..." ← 高度相关
#3 score=0.91 "iPhone 15 Pro 价格..." ← 高度相关
#4 score=0.85 "iPhone 15 对比 14..." ← 相关
#5 score=0.80 "苹果发布会总结..." ← 有点相关
...
#15 score=0.55 "三星 Galaxy S24..." ← 不太相关
#20 score=0.40 "手机壳推荐..." ← 无关
策略:
方式1: 固定 K=5,取前5条
方式2: 设置阈值 score>0.85,动态取
方式3: 固定 K + 阈值结合,取 Top-5 且 score>0.8
太多的问题:
- 超出上下文窗口
- 无关内容干扰 LLM,降低回答质量("噪声淹没信号")
- 成本增加(按 token 收费)
太少的问题:
- 信息不够,LLM 无法生成完整回答
- 遗漏关键细节
经验值:
简单事实问答: K = 3~5
复杂分析问答: K = 5~10
长文档总结: K = 10~20
多轮对话: K = 3~5 (留空间给对话历史)
3.2 文档排序策略
相关性排序(默认):
按检索分数从高到低:
[1] score=0.96 最相关的放最前面
[2] score=0.94
[3] score=0.91
Lost in the Middle 问题:
研究发现 LLM 有个特点——对开头和结尾的内容记忆最深,中间容易忽略。
LLM 对不同位置信息的关注度:
位置: [开头] [中间偏前] [正中间] [中间偏后] [结尾]
关注度: 高 中 低 中 高
示意图:
关注度
▲
│ ██ ██
│ ████ ████
│ ██████ ██████
│ ████████ ████████████ ████████
│ ██████████████████████████████████████████████
└──────────────────────────────────────────────▶
开头 中间 结尾
应对策略——三明治排列:
普通排列:
[1] 最相关 ← LLM 重点关注 ✓
[2] 次相关 ← LLM 关注减弱
[3] 第三相关 ← LLM 最容易忽略 ✗
[4] 第四相关 ← LLM 关注回升
[5] 第五相关 ← LLM 重点关注 ✓
三明治排列 (把最重要的放两头):
[1] 最相关 ← LLM 重点关注 ✓
[3] 第三相关 ← 无所谓
[5] 第五相关 ← 无所谓
[4] 第四相关 ← 无所谓
[2] 次相关 ← LLM 重点关注 ✓
3.3 元数据注入
光注入原文不够,加上元数据能让 LLM 更好地判断和引用:
不带元数据:
[1] iPhone 15 Pro 搭载 A17 Pro 芯片...
带元数据:
[1] [来源: Apple官网] [日期: 2024-09-15] [类型: 产品介绍]
iPhone 15 Pro 搭载 A17 Pro 芯片...
LLM 就能回答:
"根据 Apple 官网 2024年9月 的信息,iPhone 15 Pro 采用了..."
→ 有出处,可验证,更可信
常见元数据类型:
文档级:
- 来源 (source): Apple官网 / 评测文章 / 用户评价
- 日期 (date): 2024-09-15
- 作者 (author): xxx评测
- 文档标题 (title): iPhone 15 Pro 详细评测
chunk级:
- 所属章节 (section): 第三章 - 性能测试
- 页码 (page): P.23
- chunk编号: chunk_3 of 15
- 相似度分数: 0.94
3.4 上下文压缩(Context Compression)
当检索到的内容太长,可以先压缩再注入:
原始 chunk (500 tokens):
"iPhone 15 Pro 是苹果公司于2023年9月12日在Apple Park
发布的旗舰智能手机。发布会由Tim Cook主持。当天天气晴朗,
现场座无虚席。该手机搭载了全新的A17 Pro芯片,基于台积电
3nm工艺制造。芯片拥有190亿个晶体管。GPU性能比上一代
提升了20%。支持硬件级光线追踪。存储方面提供256GB、
512GB和1TB三个版本..."
用户问的是 "A17芯片性能如何"
压缩后 (150 tokens):
"iPhone 15 Pro 搭载 A17 Pro 芯片,台积电3nm工艺,
190亿晶体管。GPU性能比上一代提升20%,支持硬件级光线追踪。"
只保留和问题相关的信息,去掉无关描述
三种压缩方式:
| 方式 | 做法 | 优缺点 |
|---|---|---|
| LLM 提取 | 用一个小模型先提取相关句子 | 效果好,但多一次 LLM 调用 |
| 关键句抽取 | 计算每句话与 query 的相似度,取高分句 | 快,不需要额外模型 |
| 摘要生成 | 用 LLM 生成文档摘要后注入 | 适合长文档,但可能丢细节 |
3.5 多轮对话中的上下文管理
多轮对话时,Prompt 里不只有检索文档,还有对话历史,空间更紧张:
总上下文窗口: 8192 tokens
分配:
System Prompt: ~200 tokens
检索文档: ~3000 tokens
对话历史: ~2000 tokens
用户当前问题: ~100 tokens
保留给回答: ~2500 tokens
安全余量: ~400 tokens
对话历史的管理策略:
策略1: 滑动窗口 —— 只保留最近 N 轮对话
第1轮: 用户问A → 回答A ← 丢弃
第2轮: 用户问B → 回答B ← 丢弃
第3轮: 用户问C → 回答C ← 保留
第4轮: 用户问D → 回答D ← 保留
第5轮: 用户问E (当前) ← 当前
策略2: 摘要压缩 —— 早期对话压缩成摘要
[摘要] 用户在咨询iPhone 15 Pro,关注芯片性能和价格
第4轮: 用户问D → 回答D ← 保留原文
第5轮: 用户问E (当前) ← 当前
策略3: 关键轮次保留 —— 只保留和当前问题相关的历史轮次
对话历史5轮,只有第2轮和第4轮与当前问题相关
→ 只保留第2轮和第4轮
3.6 查询改写(Query Rewriting)
有时用户的问题不适合直接拿去检索,先改写再检索效果更好:
场景: 多轮对话
第1轮: 用户: "介绍一下 iPhone 15 Pro"
第2轮: 用户: "它的电池怎么样?"
↑
"它"指什么? 直接拿 "它的电池怎么样" 去检索
检索不到有效结果!
改写后: "iPhone 15 Pro 的电池续航怎么样?"
→ 现在能检索到了 ✓
改写策略:
策略1: 指代消解
"它的电池" → "iPhone 15 Pro 的电池"
策略2: 意图扩展
"iPhone好不好" → "iPhone 15 Pro 优缺点评测"
策略3: 多查询生成 (一个问题变多个)
"iPhone 15 Pro 值不值得买?"
→ 查询1: "iPhone 15 Pro 性能评测"
→ 查询2: "iPhone 15 Pro 价格性价比"
→ 查询3: "iPhone 15 Pro 用户口碑"
分别检索,合并结果,覆盖更全面
策略4: HyDE (假设性文档嵌入)
让 LLM 先"假装回答"用户问题,生成一个假设性文档
用这个假设性文档去检索(而不是用问题本身)
因为文档和文档的语义比问题和文档更接近
四、Reranker 重排序
检索返回的 Top-K 是用 Embedding 向量的相似度排序的。但 Embedding 是分别编码 query 和 document 的(双塔模型),精度有限。
Reranker 用交叉编码器(Cross-Encoder)对 query 和 document 联合打分,更精准:
双塔模型 (Embedding检索):
Query → Encoder → 查询向量 ┐
├→ 余弦相似度 → 0.92
Doc → Encoder → 文档向量 ┘
Query 和 Doc 分开编码,交互不够充分
交叉编码器 (Reranker):
[Query + Doc] → Encoder → 相关性分数 → 0.87
Query 和 Doc 拼在一起编码,每个词都能看到对方
理解更深入,但计算更慢
两阶段流程:
向量检索 (快,粗排): 100万条 → Top-50 ~10ms
Reranker (慢,精排): Top-50 → Top-5 ~100ms
先用向量快速缩小范围,再用 Reranker 精细排序
五、完整的上下文注入流水线
用户: "iPhone 15 Pro 值不值得买?"
│
▼
① 查询改写
│ 结合对话历史消解指代,扩展意图
│ → "iPhone 15 Pro 性能价格优缺点评测"
▼
② 向量检索 (Top-20)
│ Encoder编码 → 向量库检索 → 20条候选
▼
③ Reranker 重排序 (Top-5)
│ Cross-Encoder 精细打分 → 5条精选
▼
④ 上下文压缩
│ 去掉与问题无关的句子,保留关键信息
▼
⑤ 元数据附加
│ 给每条文档加上来源、日期等标签
▼
⑥ 排序策略
│ 三明治排列 / 相关性排序
▼
⑦ Prompt 组装
│ System + Context + History + Query
▼
⑧ LLM 生成回答
│
▼
"根据 Apple 官网和多个评测的信息,
iPhone 15 Pro 搭载 A17 Pro 芯片...
综合来看,如果你目前使用 iPhone 13,
升级到 15 Pro 是值得的,因为..."
六、常见问题与调优
| 问题 | 原因 | 解决方案 |
|---|---|---|
| LLM 回答"我不知道"但文档里有答案 | 文档被放在了中间位置,LLM 忽略了 | 用三明治排列,或减少文档数量 |
| 回答和文档内容矛盾 | LLM 的预训练知识干扰 | System Prompt 强调"只根据提供的资料回答" |
| 回答太笼统不具体 | 检索到的是泛泛而谈的文档 | 减小 chunk_size,让检索更精准 |
| 回答包含过时信息 | 检索到了旧文档 | 元数据加时间过滤,优先取最新文档 |
| 多轮对话越聊越差 | 对话历史占用太多空间 | 用滑动窗口或摘要压缩历史 |
| 回答混入了无关内容 | Top-K 太大,混入了低相关文档 | 降低 K 值,或加相似度阈值过滤 |
| 用户问题太模糊检索不准 | 短查询信息量不足 | 加查询改写,或多查询扩展 |