跳到主内容

从被漏读到稳定召回:长上下文防迷失中间实战 ​

问题背景:长上下文不是“塞进去就会用” ​

很多团队把 128K、200K 甚至 1M token 上下文窗口当成“万能记忆”。在实际的 AI 编程助手、代码库问答、合同审查和智能体工作流里,却经常出现一种反直觉现象:关键信息明明已经放进提示词,模型仍然回答“未提供”或引用了无关片段。更典型的是,位于开头和结尾的信息更容易被使用,位于中间的证据被忽略,这就是常说的 lost-in-middle(迷失中间)。

这不是简单的“模型不认真”。长上下文中的注意力分配、位置编码、训练数据分布以及指令与证据之间的距离,都会影响信息召回。对于工程落地而言,真正要解决的问题不是如何把更多 token 塞进去,而是如何让模型在需要时稳定找到、理解并使用关键信息。

本文给出一套可直接落地的策略:先识别长上下文风险,再通过信息前置、分段摘要、证据重排和最终答案约束,把“模型可能看见”变成“模型明确使用”。

方案对比:四种 long-context 处理策略怎么选 ​

方案一:全文直接拼接 ​

最常见做法是把检索结果、日志、代码文件或历史对话全部拼进一个 prompt。优点是实现简单;缺点是关键事实可能落在中间,且模型容易被重复信息、噪声和格式变化干扰。上下文越长,答案稳定性越差。

方案二:信息前置 ​

把最关键的约束、结论、异常日志或高相关代码放到用户问题之前,并明确标注“必须优先使用”。这适合事实集中、证据数量不多的场景,成本低,收益明显。

方案三:分段摘要 ​

先让模型对每个长片段生成结构化摘要,再基于摘要回答。它适合多文档、多文件、多轮日志分析,但摘要阶段可能丢失细节,因此必须保留可回溯的原文编号。

方案四:检索后重排加分层阅读 ​

先按相关性召回,再用 rerank 或规则把高相关证据前移;回答时要求模型先列出证据编号,再生成结论。这是 RAG 和智能体工作流中最稳健的方案,但系统复杂度更高。

原始长上下文文档/日志/代码分段与清洗编号、去噪、切分相关性重排关键证据前移分段摘要保留证据编号答案生成引用与缺失说明

实操步骤:用 Python 构建“前置 + 摘要”提示词 ​

下面示例不依赖特定厂商 SDK,只用字符串模板模拟处理流程。你可以把 call_llm 替换成任意大模型 API。示例输入是一组长文档片段,输出包含证据摘要、关键证据和最终答案提示。

python
import json
import textwrap

# 可替换为 OpenAI、Anthropic、通义、DeepSeek 等 API 调用
# 这里用伪函数表示,便于在本地或测试环境中替换实现
def call_llm(prompt: str) -> str:
    """调用大模型,并返回文本结果。"""
    return "模型返回内容"


def build_chunks(documents: list[str], chunk_size: int = 900) -> list[dict]:
    """把长文档切成带编号的小块,方便摘要和引用回溯。"""
    chunks = []
    chunk_id = 1
    for doc_id, doc in enumerate(documents, start=1):
        for start in range(0, len(doc), chunk_size):
            text = doc[start:start + chunk_size]
            chunks.append({
                "id": f"C{chunk_id:03d}",
                "doc_id": f"D{doc_id:02d}",
                "text": text.strip(),
            })
            chunk_id += 1
    return chunks


SUMMARY_PROMPT = """
你是长文档分析助手。请阅读每个证据片段,并输出 JSON 数组。
要求:
1. 不得编造片段中没有的信息;
2. 每条摘要保留原始片段 id;
3. 标注与用户问题的相关性:high、medium、low;
4. 摘录最多 2 个关键原句,用于最终回答引用。

用户问题:
{question}

证据片段:
{chunks}

输出格式:
[
  {
    "id": "C001",
    "relevance": "high",
    "summary": "用中文概括该片段",
    "key_quotes": ["关键原句"]
  }
]
"""


def summarize_chunks(question: str, chunks: list[dict]) -> list[dict]:
    """调用模型生成分段摘要;生产环境应增加 JSON 修复与校验。"""
    chunks_text = "\n\n".join(
        f"[{c['id']}] 来源={c['doc_id']}\n{c['text']}" for c in chunks
    )
    prompt = SUMMARY_PROMPT.format(question=question, chunks=chunks_text)
    raw = call_llm(prompt)
    try:
        return json.loads(raw)
    except json.JSONDecodeError:
        # 简单兜底:要求模型修复 JSON;更稳妥的方式是使用 structured output
        fix_prompt = f"请把下面内容修复为合法 JSON,不要输出解释:\n{raw}"
        return json.loads(call_llm(fix_prompt))


def rerank_summaries(summaries: list[dict]) -> list[dict]:
    """把高相关片段前置,缓解关键信息落在中间的问题。"""
    order = {"high": 0, "medium": 1, "low": 2}
    return sorted(summaries, key=lambda x: order.get(x.get("relevance"), 3))


ANSWER_PROMPT = """
请严格基于【优先证据】和【分段摘要】回答用户问题。

回答规则:
- 优先使用 high 相关性证据;
- 每个关键结论后用 [证据ID] 标注来源;
- 如果证据不足以回答,明确写出“缺少哪类信息”,不要猜测;
- 先给结论,再给依据,最后给风险或下一步建议。

【优先证据】
{front_loaded_evidence}

【分段摘要】
{all_summaries}

【用户问题】
{question}
"""


def build_final_prompt(question, summaries, top_k=5):
    ranked = rerank_summaries(summaries)
    front = ranked[:top_k]
    return ANSWER_PROMPT.format(
        front_loaded_evidence=json.dumps(front, ensure_ascii=False, indent=2),
        all_summaries=json.dumps(ranked, ensure_ascii=False, indent=2),
        question=question,
    )


# 示例:实际使用时可替换为代码库文件、工单记录或合同条款
docs = [
    "服务默认超时时间为 3 秒。若调用批处理接口,需要在请求头设置 X-Request-Timeout。",
    "批处理任务单次最多提交 500 条记录。超过限制会返回 400,错误码为 BATCH_TOO_LARGE。",
    "系统支持重试,但只有幂等请求可以安全重试;非幂等请求必须先查询任务状态。"
]
question = "批处理提交 800 条记录失败,可能是什么原因?应该如何处理?"
chunks = build_chunks(docs)
# summaries = summarize_chunks(question, chunks)
# final_prompt = build_final_prompt(question, summaries)
# print(final_prompt)

可复用提示词模板:让模型先找证据再回答 ​

如果暂时不想做两阶段调用,也可以先用下面的单模板降低风险。它的核心不是要求模型“认真一点”,而是强制建立证据顺序和引用关系。

text
你将阅读一组按编号排列的资料。资料可能很长,位于任意位置的信息都同等重要。

请按以下步骤回答:
1. 先找出与问题直接相关的证据,列出证据编号和关键原句;
2. 区分“明确事实”和“需要推断”的内容;
3. 如果多个证据冲突,指出冲突点,不要自行选择其中一个;
4. 如果没有足够证据,明确说明缺失信息;
5. 最终答案中的每个结论都必须带证据编号。

【最高优先级资料】
{must_use_evidence}

【全部资料】
{all_chunks}

【问题】
{question}

在 AI 编程助手中,must_use_evidence 可以放报错堆栈、用户明确需求、接口约束和不可修改的文件;all_chunks 再放代码库检索结果。这样模型不会先被大量实现细节带偏。

评测方法:不要只看答案“像不像对的” ​

长上下文策略必须用可重复评测验证。建议构造三类样本:第一类,关键证据放在开头;第二类,放在中间;第三类,放在结尾。每类样本统计证据命中率、引用正确率、幻觉率和“缺失信息”误报率。

一个轻量评测表可以包含:问题、证据位置、期望证据 ID、模型引用 ID、答案是否正确、是否发生幻觉。如果中间位置的正确率显著低于首尾,就说明仍存在 lost-in-middle 问题。此时不要只通过扩大上下文窗口解决,而应调整摘要粒度、重排策略或前置证据数量。

避坑总结:长上下文提示设计的 5 个具体注意事项 ​

1. 只说“请阅读全部内容”通常无效 ​

常见错误现象是提示词末尾写上“以下所有资料都很重要”,但模型仍忽略中间片段。原因是自然语言强调不能稳定改变注意力分配。修正方法是把高相关证据物理位置前移,并用编号、标题和 JSON 字段降低查找成本。

2. 摘要不能替代原文证据 ​

分段摘要后,模型可能把摘要中的概括当成事实,甚至把相邻片段信息错误合并。典型报错是最终答案引用了不存在的条款或函数。修正方法是在摘要中保留 id、key_quotes 和来源文件,最终回答必须引用证据 ID;关键业务场景还要把高相关原文片段二次前置。

3. 前置证据不是越多越好 ​

有些系统把十几个“重点片段”全部塞到最前面,结果重点区域本身又变成新的长上下文。修正方法是只前置 3 到 5 条最关键证据,并在提示中说明它们与问题的关系,例如“这是错误码定义”“这是用户硬性约束”“这是不可变接口”。其余证据放摘要区供交叉验证。

4. 切分过大或过小都会降低召回 ​

切分过大时,一个片段包含多个主题,摘要相关性判断变模糊;切分过小时,函数签名、条件判断和异常处理被拆开,模型容易断章取义。修正方法是按语义边界切分,例如函数、类、接口说明、日志请求链路,而不是固定按字符数硬切;必须重叠时保留标题和编号。

5. 没有结构化校验会放大不稳定性 ​

常见现象是模型返回“看起来像 JSON”的文本,但缺少括号、夹带解释文字,导致工作流解析失败。修正方法是使用工具调用或结构化输出模式;如果平台不支持,就在提示中固定 schema,并用 json.loads、重试和字段校验兜底。生产环境不要依赖模型每次自觉遵守格式。

结语 ​

应对 long-context 的核心不是挑战模型极限,而是设计一条可靠的信息消费路径:先切分和编号,再摘要和重排,把关键证据前置,最后要求模型带证据编号回答。这样即使上下文很长,模型也能围绕明确证据工作。真正稳定的 LLM 应用,往往不是使用了最大窗口的那个,而是最会组织信息的那个。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。