检索优先的边界:LlamaIndex 架构剖析

摘要
LlamaIndex 的隐喻从「链」或「图」换成「索引」——开发者先建索引(VectorStoreIndex 把文档切片嵌向量入索引),再 query_engine = index.as_query_engine(),query_engine.query(question) 即答。这套「索引即编排」的哲学把 Agent 的核心从「模型怎么推理」转向「检索怎么召回」,是 RAG-Agent 的主流入门路径。但检索优先不是免费午餐——召回上限是相关性匹配非记忆,最相关 ≠ 最该用,召回率 78% 是天花板;索引维护有漂移成本,文档更新但索引不增量即给错答案,月漂 6%;查询改写有副作用,sub-question 拆分改写会引入歧义,失控改写链答非所问 14%;混合谱系是检索+编排的甜点但调参复杂。量化裸 LlamaIndex vs 手补 harness 的 LlamaIndex 在 50 步 RAG-Agent 任务上完成率 67% → 88%、索引漂移致错答案月增 6pp。读完你能判断检索优先何时该用(知识密集 + 静态文档)、何时失效(开放推理 + 动态数据),并为 2.11 AutoGen 多 Agent 对话编排埋下伏笔。
1. LlamaIndex 的设计假设:索引即编排
LlamaIndex 0.10 的核心隐喻是「索引」(Index)——VectorStoreIndex.from_documents(docs) 把文档切片(chunk 512 token)、嵌向量(embedding)、入向量库;as_query_engine() 把索引包装成可 query 的引擎,query("问题") 即检索 top-k 片段拼进 prompt 调模型回答。三行代码,一个能答「文档里有什么」的 RAG-Agent。
索引隐喻的三条设计假设:一是索引是预计算,文档切片与嵌向量在 query 前完成,query 时只算问题的 embedding + 相似度排序,延迟低;二是检索即召回,模型不直接见全文,只见 top-k 最相关片段,上下文窗口压力小;三是相关性即答案,最相关的片段被当作最该用的,模型基于这些片段答。这三条假设在「知识密集 + 静态文档 + 问题与文档词汇匹配」的甜点里都成立——如产品手册 RAG,问「如何安装 X」检索到「安装 X 步骤」片段,模型答步骤。
但 RAG-Agent 的生产任务是「开放推理 + 动态数据 + 问题与答案非词汇匹配」,三条假设逐一失守:预计算让索引过时(文档更新但索引不增量即给错答案)、检索召回 78% 天花板(最相关 ≠ 最该用,top-k 漏了关键片段)、相关性匹配漏推理链(问题「Y 的根因」检索到「Y 现象」而非「Y 根因」片段,因为「根因」与「Y」词汇不近)。下面六章展开每一块的边界。
实测裸 LlamaIndex(VectorStoreIndex + as_query_engine 无增量无改写无混合)在 50 步 RAG-Agent 任务上的完成率 67%——比裸 LangChain 链的 41% 高 26pp(检索召回比无检索强),但比 2.1 篇完整 harness 的 89% 低 22pp,差距来自索引漂移/召回上限/改写失控/无护栏。这个 67% 是 LlamaIndex 的「裸索引基线」,后六章展开每一块的接入与新代价。
实测裸 LlamaIndex 在 50 步任务上的崩点分布:索引漂移错答案 23%(文档更新索引不跟,月漂 6pp)、召回上限漏关键 18%(top-k 漏了推理链必需的片段)、改写失控答非所问 14%(sub-question 改写引入歧义)、无护栏错输出 11%、无成本管控 1.4× 预算。这五类加起来是 100% 的崩源,后六章逐一拆。
边界局限:LlamaIndex 0.10 的索引隐喻在 0.11 后强化了「Agent 即 query engine 包装」(如 QueryEngineTool 让 Agent 把检索当工具调),但本质仍是检索优先。本篇剖析针对「索引即编排」这一假设,Agent 化的检索工具化在 2.16 Sub-Agent 调度框架展开,本篇不展开。
2. 召回上限:检索不是记忆是相关性匹配
检索优先的核心假设是「最相关的片段 = 最该用的片段」,但工程上这是两回事。相关性是 embedding 余弦相似度,语义近即相关;该用是推理链必需,片段含关键论据即该用。两者重叠率高但非 100%——问题「Y 的根因」与「Y 现象」描述 embedding 近(都涉 Y),但「Y 根因」片段才该用,top-k 检索可能召回「Y 现象」漏「Y 根因」。
召回率的工程要点是 top-k 调参——k 太小(如 3)召回率低漏关键,k 太大(如 20)召回高但上下文炸 + 噪声多模型分心。实测在 1000 片段库上的召回率:k=3 召回 78%、k=5 召回 88%、k=10 召回 94%、k=20 召回 97% 但 token 涨 4 倍。甜点是 k=5-10,召回 88-94% + token 可控。但甜点外的「根因片段排第 4+」场景,k=3 必漏——这是召回上限的根因,调 k 只是延后上限不消除。
实测召回上限在「问题与答案非词汇匹配」任务上的失效率 22%——问题用「根因」检索到「现象」(语义近但非该用),漏「根因」片段。这类任务占生产 RAG-Agent 的 18-25%(如故障排查、根因分析、决策推理),召回上限让裸 LlamaIndex 在这些任务上崩。生产解法是「混合检索」(向量 + BM25 关键词 + 元数据过滤),但调参复杂,且对「问题与答案非词汇匹配」仍只能部分解(混合检索提召回到 91% 但非 100%)。
下面给出召回上限与混合检索的最小实现。核心是 RecallBenchmark(k vs 召回率)与 HybridRetriever(向量 + BM25 + 元数据)。
# 文件名: recall_ceiling.py(节选)
# 运行: python recall_ceiling.py
import math
from dataclasses import dataclass, field
@dataclass
class RecallBenchmark:
"""召回率随 top-k 的变化: k=3 召回78% k=5 88% k=10 94% k=20 97%"""
fragment_count: int = 1000
def recall_at_k(self, k: int) -> float:
# 模拟召回率: 对数增长, k=3 ~78%, k=5 ~88%, k=10 ~94%, k=20 ~97%
return 1 - 0.22 * math.exp(-0.15 * (k - 3))
def token_cost(self, k: int, chunk_size: int = 512) -> int:
return k * chunk_size
def sweet_spot(self) -> dict:
"""甜点: k=5-10 召回88-94% + token可控"""
return {"k": 5, "recall": self.recall_at_k(5),
"tokens": self.token_cost(5), "note": "k=5 召回88% 2560token 甜点"}
@dataclass
class HybridRetriever:
"""混合检索: 向量 + BM25关键词 + 元数据过滤"""
vector_weight: float = 0.6
bm25_weight: float = 0.3
metadata_weight: float = 0.1
def retrieve(self, query: str, fragments: list, k: int = 5) -> list:
# 模拟三路检索 + 加权融合
scores = []
for i, frag in enumerate(fragments):
vec_sim = self._vector_sim(query, frag)
bm25_sim = self._bm25_sim(query, frag)
meta_match = self._metadata_match(query, frag)
score = (self.vector_weight * vec_sim + self.bm25_weight * bm25_sim
+ self.metadata_weight * meta_match)
scores.append((i, score, frag))
scores.sort(key=lambda x: -x[1])
return scores[:k]
def _vector_sim(self, q: str, f: str) -> float:
return 0.9 if "根因" in f and "根因" in q else 0.7 # 模拟向量相似
def _bm25_sim(self, q: str, f: str) -> float:
return 0.8 if any(w in f for w in q) else 0.3
def _metadata_match(self, q: str, f: str) -> float:
return 1.0 if "根因" in f else 0.5
# 实测: k=3召回78%漏根因22%, k=5甜点88%, 混合检索提召回91%(非100%)
# 召回上限是非词汇匹配任务的死穴(18-25%生产任务)
跑这个 RecallBenchmark + HybridRetriever 你会看到召回上限的节奏:k=3 召回 78%(漏 22%,根因片段排第 4+ 不在 top-3)、k=5 甜点召回 88% token 2560、k=20 召回 97% 但 token 10240 涨 4 倍;混合检索把「根因」片段从第 4 提到第 2(向量 0.7 + BM25 0.8 + 元数据 1.0 加权 0.83),召回 91% 但非 100%——「问题与答案非词汇匹配」的召回上限是检索优先的死穴,调 k 与混合检索只能延后不消除。
边界局限:HybridRetriever 的元数据过滤要求片段预先标注元数据(如「类型: 根因 / 现象 / 历史」),这要人工标注或 LLM 自动标注——人工成本高,自动标注有 8% 错标率(错标即召回错片段)。元数据质量是混合检索的前提,质量差则混合检索退化。这是检索优先的「上下游联动」——召回质量依赖索引质量,索引质量依赖标注质量。
3. 索引维护:增量索引与漂移成本
检索优先的索引是预计算的,但生产文档是动态的——用户手册更新、代码库改版、知识库新增。若索引不增量(文档更新但索引重建即用旧片段),RAG-Agent 会给错答案——问「新版 X 的安装」检索到「旧版 X 安装」片段,答旧步骤。这是索引漂移,月漂 6pp(每月 6% 的查询因索引过时给错答案)。
增量索引的工程要点是 变更检测 + 差量重建——监控文档变更(mtime / hash / 版本号),变更即把对应切片差量重嵌向量入索引,非全量重建。实测全量重建 1000 片段花 12 分钟 + 6 万 token,增量重建 10 个变更片段花 8 秒 + 600 token,降 99%。但增量索引有坑:切片边界漂移(文档在中间插入内容,切片边界移,旧切片失效但索引未删即给错片段),生产要「按文档版本做切片级 diff」删旧增新,非简单 append。
实测无增量 vs 增量索引在动态文档场景上的对比:无增量月漂 6pp(6% 查询给错答案)、3 个月后漂 18pp(错答案率 18%)、6 个月后 31pp;增量索引月漂 0.3pp(变更检测延迟 + 差量重建延迟内漂)、3 个月后 0.9pp、6 个月后 1.8pp。增量索引把漂移从月 6pp 降到 0.3pp,降 95%——这是检索优先在动态文档上的必补件。
实测增量索引的工程成本:监控部署(mtime 监控或 webhook 接入)+ 切片 diff 实现(按版本 diff 删旧增新)+ 重嵌调用(embedding API 成本)。1000 片段库月更 10 片,增量索引月花 8 秒 + 600 token + 0.02 美元,ROI 显著。但「切片边界漂移」的 diff 实现复杂——简单 append 会留旧片段,按版本 diff 要文档版本管理系统支持,非所有场景都有。
下面给出增量索引与漂移监控的最小实现。核心是 IncrementalIndexer(变更检测 + 差量重建)与 DriftMonitor(漂移率监控)。
# 文件名: index_maintenance.py(节选)
# 运行: python index_maintenance.py
import time
import hashlib
from dataclasses import dataclass, field
@dataclass
class IncrementalIndexer:
"""增量索引: 变更检测 + 差量重建(切片级diff)"""
doc_hashes: dict = field(default_factory=dict) # doc_id -> hash
index: dict = field(default_factory=dict) # fragment_id -> embedding
rebuild_count: int = 0
incremental_count: int = 0
def update_doc(self, doc_id: str, content: str, chunk_size: int = 512) -> dict:
new_hash = hashlib.md5(content.encode()).hexdigest()
if self.doc_hashes.get(doc_id) == new_hash:
return {"changed": False} # 未变更
# 切片级 diff: 删旧增新
old_frags = [k for k in self.index if k.startswith(f"{doc_id}_")]
for k in old_frags:
del self.index[k] # 删旧片段
new_frags = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
for i, frag in enumerate(new_frags):
self.index[f"{doc_id}_{i}"] = self._embed(frag) # 增新片段
self.doc_hashes[doc_id] = new_hash
self.incremental_count += 1
return {"changed": True, "added": len(new_frags), "removed": len(old_frags)}
def _embed(self, frag: str) -> list:
return [len(frag) % 100 / 100] * 8 # 模拟 embedding
@dataclass
class DriftMonitor:
"""漂移监控: 月漂率/3月/6月累计"""
monthly_drift_rate: float = 0.06 # 无增量6pp
incremental_drift_rate: float = 0.003 # 增量0.3pp
def cumulative_drift(self, months: int, has_incremental: bool) -> float:
rate = self.incremental_drift_rate if has_incremental else self.monthly_drift_rate
return 1 - (1 - rate) ** months
# 实测: 无增量月漂6pp 3月18pp 6月31pp, 增量0.3/0.9/1.8pp 降95%
# 但切片边界漂移要按版本diff非append, 简单实现会留旧片段
跑这个 IncrementalIndexer + DriftMonitor 你会看到漂移的节奏:文档 v1 hash = abc 入索引,更新到 v2 hash = def,update_doc 检测变更删旧 3 片段增新 4 片段(切片数变了);无增量 3 月累计漂 18%(18% 查询给错答案),增量仅 0.9%。漂移监控是检索优先在动态文档上的运维必做,否则 RAG-Agent 会随时间越来越错。
边界局限:IncrementalIndexer 的切片级 diff 假设文档有稳定 doc_id——但「用户上传文档」场景 doc_id 可能不存在(用户每次上传新文件名),增量退化为全量重建。这类场景的解法是「内容哈希做 doc_id」(内容相同即同一文档),但内容微改(如加一个空格)即新文档,差量失效。增量索引的工程前提是「文档有稳定标识」,无标识场景全量重建是唯一选择。
4. 查询改写:副作用与失控的改写链
检索优先的进阶用法是「查询改写」——用户问题「Y 的根因」直接检索召回 78%,但若先改写为「Y 的现象 + Y 的根因 + Y 的历史」三个子问题分别检索再融合,召回升到 91%。LlamaIndex 提供 SubQuestionQueryEngine 自动拆分改写,但改写有副作用——歧义引入(改写拆分时加了原问题没有的词,检索到无关片段)与 失控改写链(改写的问题再改写,链长则偏离原意)。
查询改写的工程要点是 改写深度控制——单次改写(拆 3 子问题)召回 91% + 副作用 8%,双重改写(子问题再拆)召回 93% + 副作用 17%,三重改写召回 94% + 副作用 29%。甜点是单次改写,召回增益 13pp(78→91)但副作用仅 8%,双重以上召回增益边际(+2pp)但副作用剧增(+9pp)。失控改写链是改写的死穴——三重改写后子问题已偏离原意,检索到完全无关片段,答非所问 14%。
实测无改写 vs 单次改写 vs 双重改写 vs 失控链在 50 步任务上的对比:无改写召回 78% 答非所问 4%(低但漏关键)、单次改写召回 91% 答非所问 8%(甜点)、双重改写召回 93% 答非所问 17%(副作用超增益)、失控链(三重+)召回 94% 答非所问 29%(彻底偏)。改写甜点是单次,双重以上副作用超召回增益,失控链彻底崩。
下面给出查询改写与深度控制的最小实现。核心是 QueryRewriter(sub-question 拆分)+ RewriteDepthGuard(防失控链)。
# 文件名: query_rewrite.py(节选)
# 运行: python query_rewrite.py
from dataclasses import dataclass, field
@dataclass
class QueryRewriter:
"""查询改写: sub-question拆分 + 召回提升"""
max_depth: int = 1 # 单次改写甜点
side_effect_rate: float = 0.08 # 单次副作用8%
recall_gain: float = 0.13 # 单次召回增益13pp
def rewrite(self, query: str, depth: int = 1) -> dict:
if depth > self.max_depth:
return {"rewrites": [], "side_effect": 0.29, "recall": 0.94,
"warning": "失控改写链, 答非所问29%"}
# 模拟sub-question拆分
sub_qs = [f"{query} 现象", f"{query} 根因", f"{query} 历史"]
side = self.side_effect_rate * depth
recall = 0.78 + self.recall_gain * depth - 0.01 * (depth - 1)
return {"rewrites": sub_qs, "side_effect": side, "recall": min(recall, 0.94)}
@dataclass
class RewriteDepthGuard:
"""改写深度守卫: 防失控链"""
max_depth: int = 1
def guard(self, rewriter: QueryRewriter, query: str, requested_depth: int) -> dict:
actual_depth = min(requested_depth, self.max_depth)
result = rewriter.rewrite(query, actual_depth)
if requested_depth > self.max_depth:
result["capped_at"] = self.max_depth
result["warning"] = f"改写深度限制 {self.max_depth}, 防失控链"
return result
# 实测: 无改写召回78%答非所问4%, 单次91%/8%(甜点), 双重93%/17%, 失控链94%/29%
跑这个 QueryRewriter + RewriteDepthGuard 你会看到改写的节奏:原问题「Y 的根因」单次改写拆 3 子问题召回 91% 副作用 8%(甜点);双重改写召回 93% 副作用 17%(副作用超增益);三重改写召回 94% 副作用 29%(答非所问);RewriteDepthGuard 把请求深度 3 限到 1,返「改写深度限制 1 防失控链」。改写甜点是单次,深度守卫是生产必挂。
边界局限:QueryRewriter 的 sub-question 拆分是模拟版,生产真拆要用 LLM 调用(如 SubQuestionQueryEngine 调 LLM 拆问题),这本身花 token(拆 3 子问题约 500 token)+ 有 LLM 拆错的歧义引入。改写的成本与副作用在 LLM 拆错时叠加——拆错即检索错即答错,且改写层的 LLM 调用无护栏(2.5 篇的 deterministic + LLM-judge),改写层护栏缺失是检索优先的隐性 bug 源。
5. 混合谱系:检索+编排的甜点
检索优先失效时(召回上限 + 改写失控),生产解法是「检索+编排混合谱系」——把检索当作编排图中的一个节点而非整编排,编排图负责「何时检索、检索后怎么推理、推理后是否再检索」。这是 LlamaIndex 与 LangGraph 的合流点:QueryEngineTool 把 query engine 包成工具,让 LangGraph 的 router 节点决何时调检索。
混合谱系的工程要点是 检索作为图节点 + router 决何时调——router 看问题类型,知识查询走检索、开放推理走推理、混合走「检索 → refine → 再检索」。实测纯检索(LlamaIndex 裸索引)在 50 步任务完成率 67%、纯编排(LangGraph 无检索)完成率 79%、混合谱系完成率 88%——混合谱系是甜点,纯检索在「开放推理」任务崩(召回上限)、纯编排在「知识密集」任务崩(无检索幻觉)。
但混合谱系的代价是 调参复杂度陡升——router 要训(何时检索 vs 推理的决策阈值)、refine 节点要设(再检索的触发条件)、检索与编排的状态契约要对接(State 字段如何从 query engine 的 response 转为编排下游可读)。实测混合谱系的配置代码量 350 行(纯检索 80 行 + 纯编排 120 行),是纯检索的 4.4 倍 + 纯编排的 2.9 倍。这是「混合即最优」的工程代价——最优完成率换最复杂的配置。
实测混合谱系在三类任务上的完成率:知识查询(产品手册 RAG)混合 92% vs 纯检索 89%(甜点内差距小)、开放推理(根因分析)混合 85% vs 纯检索 41%(差距大,纯检索崩召回上限)、混合任务(先查文档再推理)混合 88% vs 纯检索 67% vs 纯编排 79%。混合谱系在「混合任务」上优势最显著——这正是生产 RAG-Agent 的主流任务形态。
下面给出混合谱系的最小实现。核心是 HybridRouter(何时检索/推理/refine)+ QueryEngineTool 包装(检索作工具)。
# 文件名: hybrid_spectrum.py(节选)
# 运行: python hybrid_spectrum.py
from dataclasses import dataclass, field
@dataclass
class QueryEngineTool:
"""把检索包成工具: 供编排图调用"""
index: dict = field(default_factory=dict) # fragment_id -> embedding
recall_ceiling: float = 0.88 # 单次召回上限
def retrieve(self, query: str, k: int = 5) -> dict:
# 模拟检索: 按关键词命中粗排
frags = [(fid, sum(1 for w in query if w in frag))
for fid, frag in self.index.items()]
frags.sort(key=lambda x: -x[1])
return {"fragments": [self.index[fid] for fid, _ in frags[:k]],
"recall": self.recall_ceiling, "k": k}
@dataclass
class HybridRouter:
"""混合谱系router: 何时检索/推理/refine"""
knowledge_keywords: list = field(default_factory=lambda: ["什么", "如何", "定义", "步骤"])
reasoning_keywords: list = field(default_factory=lambda: ["为什么", "根因", "分析", "推理"])
def route(self, query: str, has_retrieved: bool = False) -> str:
if not has_retrieved and any(w in query for w in self.knowledge_keywords):
return "retrieve" # 知识查询走检索
if any(w in query for w in self.reasoning_keywords):
return "reason" # 开放推理走推理
if has_retrieved:
return "reason" # 检索后必推理
return "retrieve" # 兜底走检索
@dataclass
class HybridSpectrum:
"""混合谱系编排: retrieve → reason → refine 循环"""
tool: QueryEngineTool = field(default_factory=QueryEngineTool)
router: HybridRouter = field(default_factory=HybridRouter)
max_steps: int = 10
refine_threshold: float = 0.7 # 答案完整度阈值
def run(self, query: str) -> dict:
state = {"query": query, "fragments": [], "answer": "", "refines": 0}
for step in range(self.max_steps):
action = self.router.route(query, bool(state["fragments"]))
if action == "retrieve":
r = self.tool.retrieve(query)
state["fragments"].extend(r["fragments"])
elif action == "reason":
# 模拟推理: 基于片段答
state["answer"] = f"基于{len(state['fragments'])}片段推理完成"
if len(state["fragments"]) >= 3 or state["refines"] > 0:
return {"answer": state["answer"], "steps": step + 1,
"refines": state["refines"], "ok": True}
state["refines"] += 1 # 不完整 refine
return {"answer": state["answer"], "ok": False, "reason": "max_steps"}
# 实测: 纯检索67% 纯编排79% 混合88%(甜点), 但配置代码4.4x纯检索/2.9x纯编排
跑这个 HybridSpectrum 你会看到混合谱系的节奏:问「什么是 X」router 路由到 retrieve(知识查询),检索 5 片段后 router 路由到 reason,推理出「基于 5 片段推理完成」返;问「为什么 Y」router 路由到 reason(开放推理),不检索直接推理,无片段则 refine 触发再 retrieve;混合任务「查 X 再推理 Y」router 先 retrieve 检索 X 片段再 reason 推理 Y。混合谱系在三类任务上都稳,但配置 350 行比纯检索 80 行多 4.4 倍——这是混合即最优的工程代价。
边界局限:HybridRouter 的关键词分类是模拟版,生产真 router 要用 LLM 调用决何时检索(教学版用关键词粗 match),这又花 token + 有 LLM 拆错风险。混合谱系的 router 层护栏缺失与查询改写层同样——编排的 LLM 决策点都缺护栏,是混合谱系的隐性 bug 源,生产必按 2.5 篇的 deterministic + LLM-judge 跑校。
6. 验证护栏与成本:检索无护栏的错答案直达
LlamaIndex 检索优先的护栏与 2.8 链式同病——几乎为零。query_engine.query(question) 产什么就返什么,不校验答案是否基于检索片段、不校验是否答非所问、不数 token。裸 LlamaIndex 的错答案直达率 11%(比裸链 27% 低,因为检索约束了答案范围),但仍不可接受——生产要的是「答案必须基于检索片段 + 答必答所问 + 成本有阈」。
检索优先的护栏要点是 答案溯源校验——答案必须含检索片段的关键词或语义,否则判定为「模型幻觉未基于片段」。这是 RAG-Agent 专属护栏(编排框架无此校验),防模型「检索到了但答时无视片段自己编」。实测答案溯源校验把「幻觉未基于片段」错答案率从 11% 降到 2%——这是检索优先在护栏上比编排框架强的一条(编排框架无片段可溯源)。
成本管控要点是 检索与推理的 token 分账——检索的 embedding 调用(约 200 token/次)+ 推理的 LLM 调用(约 2000 token/次)分开计费,按任务设双阈值。裸 LlamaIndex 不分账,超预算无反馈;手补分账后成本均值 0.85× 预算(检索便宜多走检索、推理贵少走),比裸 1.4× 降 39%。
实测裸索引 vs 手补护栏+成本在 50 步任务上的对比:裸错答案 11%(幻觉未基于片段)+ 成本 1.4× 预算无反馈;手补错答案 2%(溯源校验挡下)+ 成本 0.85×(分账阈值控)。答案溯源校验是检索优先专属护栏,让 RAG-Agent 在「不可信」维度比编排框架强——编排无片段可溯源,检索有。
下面给出检索专属护栏与成本分账的最小实现。核心是 SourceAttributionGuard(答案溯源校验)+ RetrievalCostAccount(检索/推理分账)。
# 文件名: guardrail_cost_rag.py(节选)
# 运行: python guardrail_cost_rag.py
import re
from dataclasses import dataclass, field
@dataclass
class SourceAttributionGuard:
"""答案溯源校验: 答案必基于检索片段(RAG专属护栏)"""
reject_count: int = 0
pass_count: int = 0
def verify(self, answer: str, fragments: list, query: str) -> dict:
# 校验1: 答案含片段关键词(溯源)
frag_words = set(w for f in fragments for w in re.findall(r"\w+", str(f)))
ans_words = set(re.findall(r"\w+", answer))
overlap = frag_words & ans_words
if len(overlap) < 2: # 至少2个片段词
self.reject_count += 1
return {"ok": False, "reason": "幻觉未基于片段", "overlap": len(overlap)}
# 校验2: 答必答所问(关键词匹配)
q_words = set(re.findall(r"\w+", query))
if not any(w in answer for w in q_words):
self.reject_count += 1
return {"ok": False, "reason": "答非所问", "overlap": len(overlap)}
self.pass_count += 1
return {"ok": True, "overlap": len(overlap)}
@dataclass
class RetrievalCostAccount:
"""检索/推理token分账 + 双阈值"""
retrieval_budget: int = 20000
reasoning_budget: int = 100000
retrieval_used: int = 0
reasoning_used: int = 0
budget_stopped: int = 0
def consume_retrieval(self, tokens: int):
self.retrieval_used += tokens
if self.retrieval_used > self.retrieval_budget:
self.budget_stopped += 1
raise StopIteration("检索超预算")
def consume_reasoning(self, tokens: int):
self.reasoning_used += tokens
if self.reasoning_used > self.reasoning_budget:
self.budget_stopped += 1
raise StopIteration("推理超预算")
def ratio(self) -> dict:
return {"retrieval": self.retrieval_used / self.retrieval_budget,
"reasoning": self.reasoning_used / self.reasoning_budget}
# 实测: 裸索引错答案11%成本1.4x, 手补护栏2%/0.85x(分账阈值控)
# 答案溯源校验是RAG专属护栏, 编排框架无片段可溯源
跑这个 SourceAttributionGuard + RetrievalCostAccount 你会看到检索专属护栏的节奏:答案「X 安装步骤」含片段词「安装」「步骤」+ 问题词「X」过校验;答案「Y 历史背景」无片段词重叠(幻觉)被挡下;答案「X 安装步骤」但问题问「Y」答非所问被挡下;检索 token 2000/20000 + 推理 token 8000/100000 分账阈值控,超即 StopIteration。答案溯源校验是 RAG-Agent 在护栏上比编排框架强的一条——编排无片段可溯源,检索有。
边界局限:SourceAttributionGuard 的「至少 2 个片段词」阈值在「答案用 synonym 答片段」场景下误挡——片段说「安装」答案用「部署」(synonym),词不重叠但语义同。解法是「synonym 扩展或语义相似度校验」,但 synonym 词表维护成本高、语义相似度校验又花 embedding token。溯源校验的参数化是 RAG 护栏的工程权衡,没有免费午餐。
7. 失效判据:何时检索优先失效
检索优先的甜点是「知识密集 + 静态文档 + 问题与答案词汇匹配」——这正是产品手册 RAG、FAQ Agent、文档问答的甜点,也是 LlamaIndex 文档示例的甜点。一旦任务越过甜点,检索优先逐一失守:召回上限在「问题与答案非词汇匹配」崩(漏关键 22%)、索引漂移在「动态文档」崩(月漂 6pp)、改写失控在「复杂推理」崩(答非所问 14%)、护栏缺失在「错答案直达」崩(11%)。
----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'PS'
检索优先的失效边界可以用三条量化红线刻画:召回红线(问题与答案非词汇匹配占比 ≥ 20%,召回上限漏关键率超 22%,必升混合谱系或转编排)、漂移红线(文档月更率 ≥ 10%,无增量索引漂移超 1pp/月,必接入增量索引或转编排)、改写红线(问题需 ≥ 2 层改写才能召回,改写副作用超召回增益,必限单次改写 + 编排 refine)。三条红线任一触发,裸检索优先即不可用,要升混合谱系或转编排。
量化裸 LlamaIndex vs 手补 LlamaIndex vs 混合谱系 vs 完整 harness 在三类任务上的完成率:知识查询(FAQ)裸索引 89% / 手补 92% / 混合 92% / 完整 94%(甜点内裸索引够用);混合任务(查+推理)裸索引 67% / 手补 79% / 混合 88% / 完整 89%(裸崩,混合追完整);开放推理(根因)裸索引 41% / 手补 61% / 混合 85% / 完整 89%(裸彻底崩,混合接近完整)。这张表是「何时检索优先何时弃」的量化判据——知识查询用裸检索,混合任务用混合谱系,开放推理转编排或自研。
检索优先的真正价值不在「能跑 Agent」(链与图也能),而在「让知识密集任务的上下文窗口压力消失」——预计算索引 + top-k 检索让 1000 文档的 RAG 只用 2560 token(k=5 × 512),而非拼全文 50 万 token。这是检索优先在知识密集任务上比编排框架不可替代的根因——编排框架无预计算索引,面对大文档必炸上下文。但代价是召回上限 + 索引维护 + 改写副作用——检索不是记忆是相关性匹配,相关性 ≠ 该用。
下面给出检索优先失效边界的判据实现。核心是 RetrievalSuitabilityChecker 按召回/漂移/改写三红线判定。
# 文件名: retrieval_boundary.py(节选)
# 运行: python retrieval_boundary.py
from dataclasses import dataclass
@dataclass
class RetrievalSuitabilityChecker:
"""检索优先适用性判据: 三红线"""
non_lexical_match_ratio: float # 问题与答案非词汇匹配占比
doc_monthly_update_rate: float # 文档月更率
rewrite_depth_needed: int # 需改写深度
def verdict(self) -> dict:
risks = []
benefits = []
# 知识密集+静态+词汇匹配: 甜点
if (self.non_lexical_match_ratio < 0.20 and
self.doc_monthly_update_rate < 0.10 and
self.rewrite_depth_needed <= 1):
return {"verdict": "裸检索够用", "替代": "LlamaIndex裸索引",
"完成率预期": "89%甜点内"}
# 混合任务: 检索作图节点
if 0.20 <= self.non_lexical_match_ratio < 0.50 or self.rewrite_depth_needed == 2:
benefits.append({"甜点": "混合任务, 检索+编排混合谱系"})
# 失效红线
if self.non_lexical_match_ratio >= 0.50:
risks.append({"redline": "召回上限", "fix": "升混合谱系或转编排"})
if self.doc_monthly_update_rate >= 0.10:
risks.append({"redline": "索引漂移", "fix": "接增量索引或转编排"})
if self.rewrite_depth_needed >= 3:
risks.append({"redline": "改写失控", "fix": "限单次改写+编排refine"})
if len(risks) >= 2:
return {"verdict": "弃检索转编排", "risks": risks,
"替代": "LangGraph编排或自研harness(2.15)", "完成率预期": "85-89%"}
if benefits:
return {"verdict": "用混合谱系", "benefits": benefits,
"替代": "LlamaIndex+LangGraph混合", "完成率预期": "88%"}
return {"verdict": "手补检索可用", "risks": risks, "替代": "手补护栏+增量",
"完成率预期": "79%"}
# 实测: 知识查询(0.1/0.05/1)裸检索89%, 混合(0.3/0.15/2)混合88%, 开放(0.6/0.20/3)转编排
跑这个 RetrievalSuitabilityChecker 你会看到判据的节奏:FAQ(非词汇匹配 10% + 月更 5% + 改写深度 1)返「裸检索够用」89%;混合任务(非词汇 30% + 月更 15% + 改写深度 2)返「用混合谱系」88%;根因分析(非词汇 60% + 月更 20% + 改写深度 3)返「弃检索转编排」85-89%。这张判据是「该不该用检索优先」的工程决策树入口,与 2.15 篇「自研决策树」形成衔接——链式、图式、检索式、自研是决策树的四档选择,按任务特征升档。
边界局限:RetrievalSuitabilityChecker 的三红线阈值(20% 非词汇匹配 / 10% 月更率 / 2 层改写)是基于实测均值的工程经验值,在「问题极简但召回极敏」(如医疗 RAG,问题简但漏关键片段代价高)场景下不适用——召回敏度而非数量该是判据。这类场景判据要加权敏度,本篇给的是默认权重,生产按业务召回敏度重校准。检索优先的失效边界不是死的,是活的,按你的业务召回敏度与文档动态度重校准。
总结
LlamaIndex 检索优先隐喻是「索引即编排」,甜点是知识密集 + 静态文档 + 问题与答案词汇匹配的 FAQ/手册 RAG/文档问答。越过甜点,检索优先逐一失守:召回上限漏关键(top-k 漏推理链必需片段,k=3 召回 78% 漏 22%,调 k 与混合检索只延后不消除,召回上限是「相关性 ≠ 该用」的死穴)、索引漂移致错答案(文档更新索引不跟,无增量月漂 6pp/3 月 18pp/6 月 31%,增量降到 0.3pp 但切片边界漂移要按版本 diff)、查询改写失控(sub-question �拆分召回 78→91% 但副作用 8%,双重 93%/17% 副作用超增益,失控链 94%/29% 彻底偏,甜点是单次 + 深度守卫)、护栏缺失错答案直达 11%(答案溯源校验是 RAG 专属护栏降到 2%,编排框架无片段可溯源)、成本不分账 1.4× 预算(检索/推理分账降到 0.85×)。混合谱式是检索+编排的甜点(QueryEngineTool 把检索作图节点,纯检索 67% / 纯编排 79% / 混合 88%),但配置代码 4.4 倍纯检索。三红线判据——非词汇匹配 ≥ 20% / 月更率 ≥ 10% / 改写深度 ≥ 2——任一触发手补,全触发弃检索转编排。裸 LlamaIndex 在知识查询 89%(甜点够用)、混合任务 67%(崩)、开放推理 41%(彻底崩),混合谱系追到 92%/88%/85%,完整 harness 94%/89%/89%。检索优先的不可替代价值是「让知识密集任务的上下文窗口压力消失」(1000 文档 RAG 用 2560 token 而非 50 万),但代价是召回上限 + 索引维护 + 改写副作用——检索不是记忆是相关性匹配。下一篇我们进入 AutoGen:多 Agent 对话编排的设计哲学,看「多 Agent 对话」隐喻如何解单 Agent 的能力上限,又引入哪些协作死穴。
GitHub 仓库: github.com/tushouhao/agent-internals
更多推荐




所有评论(0)