长上下文 vs RAG:工程决策表 + token/成本测算 + 最小可运行 Python 脚手架

37 阅读5分钟

上下文窗口变大之后,“要不要做 RAG”不再是信仰题,而是工程题:

直接长上下文:上线快,但 token/延迟/质量风险可能炸。
做 RAG:更可控,但工程复杂度上来。

这篇给你可落地的三件套:

  • 决策表:长上下文 / RAG / 混合怎么选
  • token/成本测算:不求精确,但要能估数量级
  • 最小可运行脚手架:同一份回归集对比两条链路(长上下文 vs RAG)

0)TL;DR

  • 长上下文不是免费的:输入 token 线性放大成本与延迟,还会出现 lost-in-the-middle、噪音污染等问题。
  • 推荐路线:先长上下文做基线 → 记录 token/延迟/费用 → 再用 RAG/混合把成本打下来
  • 最小闭环:固定失败样本 → 打印 token & TopK → 回归评测。

1)工程决策表(可直接贴到评审文档)

维度长上下文RAG混合(RAG + 上下文治理)
上线速度
单次成本高(token 线性)低-中(TopK 可控)可控(先召回再压缩)
单次延迟受 token 影响大检索+生成(可控)可控(重排/压缩按需)
规模扩展
常见翻车点噪音、lost-in-the-middle召回/分块/过滤误伤复杂度可控、问题可定位
适合场景小语料/低频/快速验证大语料/高频/必须引用大多数真实业务

2)先会“算账”:token → 成本(粗算够用)

一次请求的费用近似是:

[ Cost \approx \frac{T_{in}}{1000}P_{in} + \frac{T_{out}}{1000}P_{out} ]

关键差别就是 资料 token(context tokens)

  • 长上下文:常常是“整份资料/多份资料”的合计(很快到几万 token)
  • RAG:TopN chunk 合计(通常可控在 2k~10k token)

工程上你要做的是:把 context token 变成可控旋钮


3)最小可运行脚手架(Python)

3.1 依赖

pip install -U openai tiktoken

如果你不想引入 tiktoken,也可以先用“字符数≈token×4”做粗估,但建议上线前换成真实 token 统计。

3.2 token 估算 + 上下文预算器

import tiktoken

def count_tokens(text: str, model: str = "gpt-4.1-mini") -> int:
    # 说明:不同模型编码可能不同;这里用通用编码做近似即可用于“预算控制”
    enc = tiktoken.get_encoding("cl100k_base")
    return len(enc.encode(text))

def pack_by_budget(chunks: list[str], budget_tokens: int, model: str) -> list[str]:
    packed = []
    used = 0
    for c in chunks:
        t = count_tokens(c, model=model)
        if used + t > budget_tokens:
            break
        packed.append(c)
        used += t
    return packed

3.3 统一 LLM 调用(OpenAI 兼容)

from openai import OpenAI
import time

def call_llm(base_url: str, api_key: str, model: str, messages: list[dict]):
    client = OpenAI(api_key=api_key, base_url=base_url)
    t0 = time.time()
    resp = client.chat.completions.create(model=model, messages=messages)
    latency_ms = int((time.time() - t0) * 1000)
    usage = getattr(resp, "usage", None)
    return {
        "text": resp.choices[0].message.content,
        "latency_ms": latency_ms,
        "usage": usage.model_dump() if usage else None,
    }

示例:如果你用的是某 OpenAI 兼容入口(如 147ai),通常只改 base_urlapi_key
base_url=https://147ai.com/v1,端点 POST /v1/chat/completions,鉴权 Authorization: Bearer <KEY>(具体以控制台/文档为准)。

3.4 两条链路:长上下文 vs RAG(占位版)

def build_long_context(docs: list[str], budget_tokens: int, model: str) -> str:
    # 建议:先做去重/摘要/排序;这里只做演示
    packed = pack_by_budget(docs, budget_tokens=budget_tokens, model=model)
    return "\n\n---\n\n".join(packed)

def retrieve_topk(question: str, k: int) -> list[str]:
    # TODO: 接你的检索(BM25/向量/Hybrid)
    return []

def messages_long_context(question: str, docs: list[str], model: str) -> list[dict]:
    context = build_long_context(docs, budget_tokens=8000, model=model)
    return [
        {"role": "system", "content": "你是严谨的技术助手。只基于资料回答;资料不足就说不足。"},
        {"role": "user", "content": f"资料:\n{context}\n\n问题:{question}\n\n要求:结论+依据要点(分点)。"},
    ]

def messages_rag(question: str, model: str) -> list[dict]:
    chunks = retrieve_topk(question, k=8)
    context = "\n\n---\n\n".join(chunks)
    return [
        {"role": "system", "content": "你是严谨的技术助手。只基于检索资料回答;资料不足就说不足。"},
        {"role": "user", "content": f"检索资料:\n{context}\n\n问题:{question}\n\n要求:结论+依据要点(分点)。"},
    ]

3.5 回归评测(先跑通数据闭环)

准备一个 eval.jsonl(几十条就够):

{"id":"1","question":"...","gold_keypoints":["..."],"docs":["doc1...","doc2..."]}

跑评测时先记录三件事:tokens / latency / output,质量先人工打标签(别一上来追求自动化)。

import json

def run_eval(path: str, base_url: str, api_key: str, model: str):
    rows = [json.loads(l) for l in open(path, "r", encoding="utf-8")]
    for r in rows:
        q = r["question"]
        docs = r.get("docs", [])

        m1 = messages_long_context(q, docs, model=model)
        out1 = call_llm(base_url, api_key, model, m1)

        m2 = messages_rag(q, model=model)
        out2 = call_llm(base_url, api_key, model, m2)

        print("===", r.get("id"))
        print("[long] latency_ms=", out1["latency_ms"], "usage=", out1["usage"])
        print("[rag ] latency_ms=", out2["latency_ms"], "usage=", out2["usage"])
        # TODO: 把 out1/out2 存起来,后面做人工标注与回归

if __name__ == "__main__":
    run_eval(
        path="eval.jsonl",
        base_url="https://147ai.com/v1",
        api_key="YOUR_API_KEY",
        model="gpt-4.1-mini",
    )

4)排错顺序(非常建议照着做)

  1. 先看 token:是不是上下文塞太多导致成本/延迟爆炸?
  2. 长上下文质量差:优先做“治理”(压缩/去重/排序/预算),再谈换模型
  3. RAG 质量差:先评检索(打印 TopK、做 hit@k),再评生成(上下文拼接/引用约束)

5)资源:把“接入层”做成可迁移,评测才不痛苦

你会频繁做这些对比:

  • 长上下文 vs RAG
  • 不同模型/不同温度/不同提示词
  • Hybrid(BM25+Vector)是否值得加重排

如果每换一次都要改一堆 SDK/鉴权/网关,评测成本会很高。
所以我建议统一成 OpenAI 兼容入口(比如 147ai 这类聚合网关,或你自建网关):多数时候只改 base_urlapi_key

文档(示例):https://147api.apifox.cn/