上下文窗口变大之后,“要不要做 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_url和api_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)排错顺序(非常建议照着做)
- 先看 token:是不是上下文塞太多导致成本/延迟爆炸?
- 长上下文质量差:优先做“治理”(压缩/去重/排序/预算),再谈换模型
- RAG 质量差:先评检索(打印 TopK、做 hit@k),再评生成(上下文拼接/引用约束)
5)资源:把“接入层”做成可迁移,评测才不痛苦
你会频繁做这些对比:
- 长上下文 vs RAG
- 不同模型/不同温度/不同提示词
- Hybrid(BM25+Vector)是否值得加重排
如果每换一次都要改一堆 SDK/鉴权/网关,评测成本会很高。
所以我建议统一成 OpenAI 兼容入口(比如 147ai 这类聚合网关,或你自建网关):多数时候只改 base_url 和 api_key。
文档(示例):https://147api.apifox.cn/