ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
结构化索引 vs 扁平检索:离线对比演示。
|
||||
|
||||
本模块不依赖 OpenAI / 向量模型 / 网络,纯 Python + networkx 即可运行。
|
||||
它用一个手工整理的「Intel x86 SIMD 指令集」小知识库,直观对比两条检索路线:
|
||||
|
||||
* 扁平检索(Flat):把每个知识点当成互相独立的文本块,按词面相似度打分召回。
|
||||
这是传统 RAG「文档分块 + 向量检索」的抽象——只能返回零散片段。
|
||||
* 结构化检索(Structured):
|
||||
- GraphRAG 式的实体-关系图:沿关系边做多跳遍历,能回答扁平检索答不了的
|
||||
「A 通过什么和 B 相连」这类关系性问题(对应书中「多跳关系推理」)。
|
||||
- RAPTOR 式的层次树:把细节聚合成上层摘要,能回答「概述某主题」这类
|
||||
需要跨片段综合的宏观问题(对应书中「多层次导航」)。
|
||||
|
||||
这段演示对应实验 3-7(structured-index)中「知识表达哲学的对比研究」。
|
||||
构建真实索引需要调用 LLM(见 main.py build),本演示则把索引结果预先手工写好,
|
||||
让读者无需 API Key 也能看到「结构化索引到底解决了扁平检索的什么问题」。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import deque
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 手工整理的小知识库(对应 test_indexing.py 中的 Intel x86 示例文档)
|
||||
# 每个实体的 description 同时充当「扁平检索的一个文本块」。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ENTITIES: Dict[str, Dict[str, str]] = {
|
||||
"ADDPS": {"type": "instruction",
|
||||
"desc": "ADDPS:对打包的单精度浮点数执行并行加法,一次处理四路单精度浮点运算。"},
|
||||
"MOVAPS": {"type": "instruction",
|
||||
"desc": "MOVAPS:在向量寄存器与对齐内存之间搬运 128 位打包单精度浮点数据。"},
|
||||
"VADDPS": {"type": "instruction",
|
||||
"desc": "VADDPS:AVX 版本的打包单精度浮点加法,一次处理八路单精度浮点运算。"},
|
||||
"CPUID": {"type": "instruction",
|
||||
"desc": "CPUID:返回处理器标识与特性信息,用于探测处理器是否支持 SSE、AVX 等扩展。"},
|
||||
"SSE": {"type": "extension",
|
||||
"desc": "SSE(Streaming SIMD Extensions):引入 128 位向量寄存器,支持打包单精度浮点并行运算。"},
|
||||
"AVX": {"type": "extension",
|
||||
"desc": "AVX(Advanced Vector Extensions):把向量寄存器扩展到 256 位,进一步增强 SIMD 能力。"},
|
||||
"XMM": {"type": "register",
|
||||
"desc": "XMM0-XMM15:128 位向量寄存器,供 SSE 指令存放打包数据。"},
|
||||
"YMM": {"type": "register",
|
||||
"desc": "YMM0-YMM15:256 位向量寄存器,供 AVX 指令使用,低 128 位与 XMM 共享。"},
|
||||
"CR4.OSFXSR": {"type": "control-bit",
|
||||
"desc": "CR4.OSFXSR:操作系统支持 FXSAVE/FXRSTOR 的控制位,置 1 后才允许使用 SSE 指令。"},
|
||||
"CR0.EM": {"type": "control-bit",
|
||||
"desc": "CR0.EM:仿真标志位,为 1 时禁用 SIMD,必须清零才能执行 SSE / AVX 指令。"},
|
||||
}
|
||||
|
||||
# 实体-关系三元组(主语, 关系, 宾语),构成 GraphRAG 的知识之网。
|
||||
TRIPLES: List[Tuple[str, str, str]] = [
|
||||
("ADDPS", "属于", "SSE"),
|
||||
("MOVAPS", "属于", "SSE"),
|
||||
("VADDPS", "属于", "AVX"),
|
||||
("ADDPS", "操作", "XMM"),
|
||||
("VADDPS", "操作", "YMM"),
|
||||
("SSE", "使用寄存器", "XMM"),
|
||||
("AVX", "使用寄存器", "YMM"),
|
||||
("AVX", "扩展自", "SSE"),
|
||||
("SSE", "需要启用", "CR4.OSFXSR"),
|
||||
("AVX", "需要启用", "CR4.OSFXSR"),
|
||||
("SSE", "要求清零", "CR0.EM"),
|
||||
("CPUID", "探测", "SSE"),
|
||||
("CPUID", "探测", "AVX"),
|
||||
]
|
||||
|
||||
# RAPTOR 式层次树:把细粒度叶子聚合为上层摘要(父节点)。
|
||||
TREE_SUMMARY = {
|
||||
"id": "SIMD 指令集综述",
|
||||
"summary": ("x86 的 SIMD 指令集自 MMX 起步,SSE 引入 128 位 XMM 向量寄存器并支持打包"
|
||||
"单精度浮点运算,AVX 进一步把寄存器扩展到 256 位 YMM,逐代提升单指令多数据"
|
||||
"的并行宽度;使用前需通过 CR0/CR4 控制位使能,并可用 CPUID 探测支持情况。"),
|
||||
"children": ["ADDPS", "MOVAPS", "VADDPS", "SSE", "AVX", "XMM", "YMM"],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 扁平检索:把每个实体描述当作独立文本块,按词面相似度(词频余弦)召回。
|
||||
# 这是「向量检索」在离线场景下的一个确定性替身:无内在结构、只看片段本身。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _tokenize(text: str) -> List[str]:
|
||||
"""粗粒度分词:ASCII 词(如 ADDPS、CR4、XMM)整体保留,中文按单字切。"""
|
||||
tokens = re.findall(r"[a-zA-Z0-9]+", text.lower())
|
||||
tokens += re.findall(r"[一-鿿]", text)
|
||||
return tokens
|
||||
|
||||
|
||||
def _cosine(a: Dict[str, int], b: Dict[str, int]) -> float:
|
||||
common = set(a) & set(b)
|
||||
dot = sum(a[t] * b[t] for t in common)
|
||||
na = sum(v * v for v in a.values()) ** 0.5
|
||||
nb = sum(v * v for v in b.values()) ** 0.5
|
||||
return dot / (na * nb) if na and nb else 0.0
|
||||
|
||||
|
||||
class FlatRetriever:
|
||||
"""按词面相似度召回独立文本块(模拟扁平向量检索)。"""
|
||||
|
||||
def __init__(self, entities: Dict[str, Dict[str, str]]):
|
||||
self.docs = {name: e["desc"] for name, e in entities.items()}
|
||||
self.types = {name: e["type"] for name, e in entities.items()}
|
||||
self._vecs = {name: self._tf(text) for name, text in self.docs.items()}
|
||||
|
||||
@staticmethod
|
||||
def _tf(text: str) -> Dict[str, int]:
|
||||
vec: Dict[str, int] = {}
|
||||
for tok in _tokenize(text):
|
||||
vec[tok] = vec.get(tok, 0) + 1
|
||||
return vec
|
||||
|
||||
def search(self, query: str, top_k: int = 3) -> List[Dict]:
|
||||
qvec = self._tf(query)
|
||||
scored = [
|
||||
{"name": name, "type": self.types[name],
|
||||
"desc": self.docs[name], "score": _cosine(qvec, self._vecs[name])}
|
||||
for name in self.docs
|
||||
]
|
||||
scored.sort(key=lambda r: r["score"], reverse=True)
|
||||
return scored[:top_k]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 结构化检索:基于实体-关系图的多跳遍历(GraphRAG 的核心能力)。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_graph(triples: List[Tuple[str, str, str]]) -> nx.DiGraph:
|
||||
g = nx.DiGraph()
|
||||
for name, meta in ENTITIES.items():
|
||||
g.add_node(name, **meta)
|
||||
for src, rel, dst in triples:
|
||||
g.add_edge(src, dst, rel=rel)
|
||||
return g
|
||||
|
||||
|
||||
def multi_hop_paths(graph: nx.DiGraph, start: str, max_hops: int = 3) -> List[List[Tuple[str, str, str]]]:
|
||||
"""从 start 出发沿关系边做 BFS,返回所有 <= max_hops 跳的关系路径。
|
||||
|
||||
每条路径是若干 (源实体, 关系, 目标实体) 步骤的列表。这正是扁平检索无法表达的
|
||||
「沿关系边遍历」——对应书中「知识图谱天然支持沿关系边遍历,使多跳查询高效可靠」。
|
||||
"""
|
||||
if start not in graph:
|
||||
return []
|
||||
paths: List[List[Tuple[str, str, str]]] = []
|
||||
# 队列元素:(当前节点, 到达该节点的路径)
|
||||
queue: deque = deque([(start, [])])
|
||||
while queue:
|
||||
node, path = queue.popleft()
|
||||
if len(path) >= max_hops:
|
||||
continue
|
||||
for nbr in graph.successors(node):
|
||||
step = (node, graph[node][nbr]["rel"], nbr)
|
||||
new_path = path + [step]
|
||||
paths.append(new_path)
|
||||
queue.append((nbr, new_path))
|
||||
return paths
|
||||
|
||||
|
||||
def match_entity(graph: nx.DiGraph, query: str) -> Optional[str]:
|
||||
"""在查询中找出出现的起始实体(按名字最长匹配,确定性)。"""
|
||||
q = query.lower()
|
||||
hits = [name for name in graph.nodes if name.lower() in q]
|
||||
return max(hits, key=len) if hits else None
|
||||
|
||||
|
||||
def format_path(path: List[Tuple[str, str, str]]) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
parts = [path[0][0]]
|
||||
for src, rel, dst in path:
|
||||
parts.append(f" --{rel}--> {dst}")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 三个演示查询:分别凸显扁平检索的三类短板。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def demo_multi_hop(flat: FlatRetriever, graph: nx.DiGraph, query: str, top_k: int) -> None:
|
||||
print(f"\n【查询 1|多跳关系推理】{query}")
|
||||
print("-- 扁平检索(按词面相似度返回独立片段)--")
|
||||
for i, r in enumerate(flat.search(query, top_k), 1):
|
||||
print(f" {i}. [{r['type']}] {r['name']} (score={r['score']:.3f})")
|
||||
print(" ✗ 只能召回词面相近的孤立片段,无法把 ADDPS 与某个控制位「连」起来——"
|
||||
"缺少关系,就无法判断哪个控制位是 ADDPS 的答案。")
|
||||
|
||||
print("-- 结构化图检索(沿关系边多跳遍历)--")
|
||||
start = match_entity(graph, query)
|
||||
paths = multi_hop_paths(graph, start, max_hops=3)
|
||||
# 只展示终点为控制位的路径(问题问的是「控制寄存器位」)
|
||||
answers = [p for p in paths if graph.nodes[p[-1][2]]["type"] == "control-bit"]
|
||||
for p in answers:
|
||||
print(f" {format_path(p)}")
|
||||
enable = [p for p in answers if p[-1][1] == "需要启用"]
|
||||
if enable:
|
||||
target = enable[0][-1][2]
|
||||
print(f" ✓ 答案:{target}(从 {start} 经 {len(enable[0])} 跳可达)")
|
||||
print(f" {graph.nodes[target]['desc']}")
|
||||
|
||||
|
||||
def demo_compare(flat: FlatRetriever, graph: nx.DiGraph, query: str, top_k: int) -> None:
|
||||
print(f"\n【查询 2|跨节点综合对比】{query}")
|
||||
print("-- 扁平检索 --")
|
||||
for i, r in enumerate(flat.search(query, top_k), 1):
|
||||
print(f" {i}. [{r['type']}] {r['name']} (score={r['score']:.3f})")
|
||||
print(" ✗ SSE 与 AVX 各自的寄存器事实散落在不同片段里,扁平检索把它们分别召回,"
|
||||
"却不会主动把「谁用哪种寄存器」对齐成一张对比表。")
|
||||
|
||||
print("-- 结构化图检索(顺着「使用寄存器」边取回两侧事实)--")
|
||||
for ext in ("SSE", "AVX"):
|
||||
regs = [dst for _, dst, d in graph.out_edges(ext, data=True) if d["rel"] == "使用寄存器"]
|
||||
for reg in regs:
|
||||
print(f" {ext} --使用寄存器--> {reg}:{graph.nodes[reg]['desc']}")
|
||||
print(" ✓ 沿同一种关系边遍历两个实体,即可直接综合出「SSE=128 位 XMM,AVX=256 位 YMM」的对比。")
|
||||
|
||||
|
||||
def demo_hierarchical(flat: FlatRetriever, query: str, top_k: int) -> None:
|
||||
print(f"\n【查询 3|多层次导航(RAPTOR 层次树)】{query}")
|
||||
print("-- 扁平检索 --")
|
||||
for i, r in enumerate(flat.search(query, top_k), 1):
|
||||
print(f" {i}. [{r['type']}] {r['name']} (score={r['score']:.3f})")
|
||||
print(" ✗ 召回的是零散的细节片段,过于细碎,答不了「概述」这种需要跨片段综合的宏观问题。")
|
||||
|
||||
print("-- 结构化树检索(返回上层摘要节点)--")
|
||||
print(f" [父节点摘要] {TREE_SUMMARY['id']}")
|
||||
print(f" {TREE_SUMMARY['summary']}")
|
||||
print(f" ✓ 由宏观摘要切入,需要细节时再向下钻取到 {', '.join(TREE_SUMMARY['children'][:4])} 等叶子节点。")
|
||||
|
||||
|
||||
def run_demo(top_k: int = 3, custom_query: Optional[str] = None,
|
||||
output: Optional[str] = None) -> Dict:
|
||||
"""运行离线对比演示;返回结构化结果(便于 --output 落盘)。"""
|
||||
flat = FlatRetriever(ENTITIES)
|
||||
graph = build_graph(TRIPLES)
|
||||
|
||||
print("=" * 68)
|
||||
print("结构化索引 vs 扁平检索 · 离线对比演示(无需 API Key)")
|
||||
print(f"知识库:Intel x86 SIMD 指令集 | 实体 {graph.number_of_nodes()} 个,"
|
||||
f"关系 {graph.number_of_edges()} 条,层次树 1 棵")
|
||||
print("=" * 68)
|
||||
|
||||
if custom_query:
|
||||
# 自定义查询:同时给出扁平与图检索两种视角
|
||||
print(f"\n【自定义查询】{custom_query}")
|
||||
print("-- 扁平检索 --")
|
||||
flat_hits = flat.search(custom_query, top_k)
|
||||
for i, r in enumerate(flat_hits, 1):
|
||||
print(f" {i}. [{r['type']}] {r['name']} (score={r['score']:.3f})")
|
||||
print("-- 结构化图检索(从查询中识别到的实体多跳遍历)--")
|
||||
start = match_entity(graph, custom_query)
|
||||
if start is None:
|
||||
print(" (未在查询中识别到已知实体,无法进行图遍历)")
|
||||
paths = []
|
||||
else:
|
||||
paths = multi_hop_paths(graph, start, max_hops=3)
|
||||
for p in paths:
|
||||
print(f" {format_path(p)}")
|
||||
result = {"query": custom_query,
|
||||
"flat": [{"name": r["name"], "score": r["score"]} for r in flat_hits],
|
||||
"graph_start": start,
|
||||
"graph_paths": [format_path(p) for p in paths]}
|
||||
else:
|
||||
q1 = "运行 ADDPS 指令前,操作系统必须把哪个控制寄存器位置 1?"
|
||||
q2 = "SSE 与 AVX 使用的向量寄存器有什么区别?"
|
||||
q3 = "概述一下 x86 的 SIMD 指令集"
|
||||
demo_multi_hop(flat, graph, q1, top_k)
|
||||
demo_compare(flat, graph, q2, top_k)
|
||||
demo_hierarchical(flat, q3, top_k)
|
||||
start1 = match_entity(graph, q1)
|
||||
result = {
|
||||
"queries": [q1, q2, q3],
|
||||
"multi_hop": {
|
||||
"query": q1,
|
||||
"start": start1,
|
||||
"paths": [format_path(p) for p in multi_hop_paths(graph, start1, 3)
|
||||
if graph.nodes[p[-1][2]]["type"] == "control-bit"],
|
||||
},
|
||||
}
|
||||
|
||||
print("\n" + "=" * 68)
|
||||
print("结论:扁平检索擅长「找到含某信息的片段」,但一旦查询需要跨片段的关系推理或"
|
||||
"多层次综合,就必须依赖结构化索引(图 / 层次树)。——对应书中实验 3-7 的核心观点。")
|
||||
print("=" * 68)
|
||||
|
||||
if output:
|
||||
with open(output, "w", encoding="utf-8") as f:
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
print(f"\n结果已写入:{output}")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_demo()
|
||||
Reference in New Issue
Block a user