Learn
RAG/11-prompt-assembly

上下文拼接与 Prompt 组装

检索到的片段最终要被「组装」成一段给 LLM 的 Prompt。组装得好,模型才知道怎么用这些证据;组装得差,再准的检索也白搭。

1. 基本模板结构

一个稳健的 RAG Prompt 通常含四部分:系统指令、上下文、问题、输出格式。

你是一个严谨的助手。只能依据下方【资料】回答,
若资料中没有相关信息,回答"我不知道"。
 
【资料】
1. {chunk_1}
2. {chunk_2}
...
 
【问题】
{question}
 
【回答】

2. 代码化组装

def build_prompt(question, chunks):
    ctx = "\n".join(
        f"[{i+1}] {c['text']}(来源:{c['metadata']['source']} 第{c['metadata']['page']}页)"
        for i, c in enumerate(chunks)
    )
    return f"""你是严谨的助手,仅依据资料作答。
【资料】
{ctx}
 
【问题】{question}
【回答】"""
ℹ️给每段编号,方便引用

为每段上下文加 [1][2] 编号,并在指令里要求模型回答时标注 [1],能直接实现「可溯源」(呼应下一章 Citation)。

3. token 预算控制

LLM 有上下文窗口上限,检索片段可能超预算:

MAX_TOKENS = 3000
est = lambda s: len(s) / 1.6   # 中文粗略 1 字≈1.6 token 估算
 
budget, kept = MAX_TOKENS, []
for c in ranked_chunks:        # 已按相关度排序
    if est(c["text"]) > budget:
        if not kept:           # 哪怕超也要留一段
            kept.append(c)
        break
    kept.append(c); budget -= est(c["text"])
⚠️预算超了别硬塞

把超出窗口的片段硬拼进去会被截断,反而丢掉了最关键的后段。应「按相关度从高到低截取」,并在超预算时提示模型「资料可能不完整」。

4. 上下文压缩

当片段冗长但只需其中一句时,可先做压缩:

  • 句子级筛选:只保留与问题 token 重叠高的句子
  • LLM 摘要:用一个小模型把长 chunk 压成关键句(适合超大文档)
  • 去重合并:MMR 已去冗余,这里避免重复来源
# 简单句子筛选
import re
def keep_relevant(text, question, top_n=3):
    sents = re.split(r"[。!?]", text)
    scored = [(s, overlap(s, question)) for s in sents if s.strip()]
    return "。".join(s for s, _ in sorted(scored, key=lambda x: x[1], reverse=True)[:top_n])

5. 指令设计要点

要点说明
限定依据「仅依据资料,不得编造」降低幻觉
未知处理「资料无则答不知道」避免瞎猜
引用要求「标注 [n] 来源」便于溯源
语言风格指定输出语言、长度、格式
💡把模板当配置管理

Prompt 模板会随效果调优频繁改。把它抽成独立配置(YAML/DB),和代码分离,方便 A/B 与版本管理,不要硬编码在逻辑里。

🎯练习

写一个有 8 个 chunk(每段约 600 字)的测试,故意让总 token 超 3000,跑一遍你的预算控制逻辑,确认只保留了最相关的若干段且没报错。

小结

  • RAG Prompt = 指令 + 编号上下文 + 问题 + 格式
  • 按相关度从高到低截取,遵守 token 预算
  • 超预算别硬塞,提示资料可能不完整
  • 上下文可句子级筛选/摘要压缩;模板要配置化
  • 下一章讲如何让回答标注来源、可追溯 →