feat(rag): 检索质量评测 —— 单路检索能力 + recall@k/MRR 评测台
- 引擎抽 searchPaths(三路召回) + SearchByMode(vector/fulltext/graph/hybrid, 纯检索不 rerank,公平对比);kb_search 加 mode 参数(空=生产含rerank), gateway KbSearch 透传 mode - scripts/rageval.py:标注语料+查询 → 四模式 recall@k/MRR 对比表(可复用) - live 量化:纯语义改写让全文0.88/图谱0.75 漏召回,混合 1.00 兜回, 混合=各路上界的稳健组合 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
# RAG 离线检索质量评测:四模式(向量/全文/图谱/混合) recall@k + MRR,量化"混合 vs 单路"。
|
||||
import json, sys, time, urllib.request
|
||||
|
||||
BASE = "http://127.0.0.1:8080"
|
||||
KB = "evalset"
|
||||
TOKEN = open(sys.argv[1]).read().strip()
|
||||
|
||||
# 语料:每条一句话事实,唯一标记=载具名(用于判定召回)。
|
||||
CORPUS = [
|
||||
("启明星一号", "启明星一号是一颗对地观测卫星,由总设计师周文渊研制,搭载高光谱相机。"),
|
||||
("玄鸟二号", "玄鸟二号是一型运载火箭,由总工程师林书豪设计,使用液氧甲烷发动机。"),
|
||||
("蛟龙三号", "蛟龙三号是一台载人潜水器,由首席科学家陈海平主持,最大下潜深度7000米。"),
|
||||
("烛龙四号", "烛龙四号是一颗深空探测器,由项目主管吴宇航负责,用于探测木星磁场。"),
|
||||
("麒麟五号", "麒麟五号是一座空间站,由总指挥赵长青建造,配备机械臂。"),
|
||||
("白泽六号", "白泽六号是一颗通信卫星,由总设计师钱明轩研制,实现量子通信。"),
|
||||
("饕餮七号", "饕餮七号是一艘破冰船,由总工程师孙铁柱主持,采用核动力驱动。"),
|
||||
("穷奇八号", "穷奇八号是一艘行星际飞船,由首席科学家周伯通设计,依靠太阳帆推进。"),
|
||||
("梼杌九号", "梼杌九号是一架预警机,由总设计师黄药师研制,配红外制导系统。"),
|
||||
("应龙十号", "应龙十号是一款高超声速飞行器,由总工程师欧阳锋设计,采用折叠翼。"),
|
||||
("重明十一号", "重明十一号是一架电动垂直起降飞行器,由总设计师洪七公研制,用全固态电池。"),
|
||||
("毕方十二号", "毕方十二号是一座防御平台,由总指挥段誉建造,装备激光武器。"),
|
||||
# —— 干扰项:与各目标同族近义,逼向量路在相似项中区分 ——
|
||||
("天罡卫星", "天罡卫星是一颗对地观测卫星,由总设计师周文博研制,搭载多光谱相机。"),
|
||||
("地煞火箭", "地煞火箭是一型运载火箭,由总工程师林书文设计,使用液氧煤油发动机。"),
|
||||
("潜龙号", "潜龙号是一台载人潜水器,由首席科学家陈海峰主持,最大下潜深度5000米。"),
|
||||
("探火号", "探火号是一颗深空探测器,由项目主管吴宇辉负责,用于探测火星大气。"),
|
||||
("天宫号", "天宫号是一座空间站,由总指挥赵长河建造,配备太阳能帆板。"),
|
||||
("鹊桥号", "鹊桥号是一颗通信卫星,由总设计师钱明远研制,做激光通信。"),
|
||||
("破冰者号", "破冰者号是一艘破冰船,由总工程师孙铁山主持,采用柴油动力。"),
|
||||
("远航号", "远航号是一艘行星际飞船,由首席科学家周伯文设计,依靠核电推进。"),
|
||||
("天眼号", "天眼号是一架预警机,由总设计师黄药剂研制,配相控阵雷达。"),
|
||||
("风刃号", "风刃号是一款高超声速飞行器,由总工程师欧阳峰设计,采用乘波体。"),
|
||||
("飞羽号", "飞羽号是一架电动垂直起降飞行器,由总设计师洪七叔研制,用液流电池。"),
|
||||
("盾卫号", "盾卫号是一座防御平台,由总指挥段正淳建造,装备动能拦截弹。"),
|
||||
]
|
||||
# 查询:q=问句, m=期望命中的标记, t=类型(便于看各路强弱)
|
||||
QUERIES = [
|
||||
("哪台设备可以下潜到七千米的深海", "蛟龙三号", "语义"),
|
||||
("用来在冰封海域开路的核动力船", "饕餮七号", "语义"),
|
||||
("能垂直起降的电动飞行器用了什么电池", "重明十一号", "语义"),
|
||||
("靠激光拦截目标的防御系统是哪个", "毕方十二号", "语义"),
|
||||
("液氧甲烷发动机用在哪", "玄鸟二号", "关键词"),
|
||||
("木星磁场探测", "烛龙四号", "关键词"),
|
||||
("量子通信卫星", "白泽六号", "关键词"),
|
||||
("太阳帆推进", "穷奇八号", "关键词"),
|
||||
("周文渊负责研制的是什么", "启明星一号", "实体关系"),
|
||||
("孙铁柱主持的项目", "饕餮七号", "实体关系"),
|
||||
("黄药师设计了哪款预警机", "梼杌九号", "实体关系"),
|
||||
("赵长青建造的空间站叫什么", "麒麟五号", "实体关系"),
|
||||
# —— 纯语义改写:尽量避开与目标文档的字面重叠,逼全文路失手、看混合是否兜住 ——
|
||||
("可搭乘人员探入数千米幽暗海沟的勘探艇", "蛟龙三号", "纯语义"),
|
||||
("环绕巨行星测量其磁场强度的太空仪器", "烛龙四号", "纯语义"),
|
||||
("能从陆地竖直升空、不烧油的载客飞行器", "重明十一号", "纯语义"),
|
||||
("以高能光束摧毁来袭目标的拦截阵地", "毕方十二号", "纯语义"),
|
||||
]
|
||||
MODES = ["vector", "fulltext", "graph", "hybrid"]
|
||||
|
||||
def post(path, body):
|
||||
req = urllib.request.Request(BASE + path, data=json.dumps(body).encode(),
|
||||
headers={"Authorization": "Bearer " + TOKEN, "Content-Type": "application/json"}, method="POST")
|
||||
return json.load(urllib.request.urlopen(req, timeout=20))
|
||||
|
||||
def ingest():
|
||||
for name, text in CORPUS:
|
||||
post("/api/v1/kb/note", {"kb": KB, "name": name, "content": text})
|
||||
print(f"已提交 {len(CORPUS)} 篇,等待入库(切块/向量/图谱)…")
|
||||
|
||||
def search(q, mode, topk=5):
|
||||
r = post("/api/v1/kb/search", {"kb": KB, "q": q, "topK": topk, "mode": mode})
|
||||
return [h.get("Text") or h.get("text") or "" for h in (r.get("hits") or [])]
|
||||
|
||||
def rank_of(hits, marker):
|
||||
for i, t in enumerate(hits):
|
||||
if marker in t:
|
||||
return i + 1
|
||||
return 0
|
||||
|
||||
def main():
|
||||
if "--noingest" not in sys.argv:
|
||||
ingest()
|
||||
time.sleep(40) # 等队列把 12 篇全部 embed+图谱抽取完
|
||||
print(f"\n{'模式':<10}{'recall@3':>10}{'recall@5':>10}{'MRR':>8}")
|
||||
print("-" * 40)
|
||||
per_type = {m: {} for m in MODES}
|
||||
for mode in MODES:
|
||||
r3 = r5 = mrr = 0.0
|
||||
bytype = {}
|
||||
for q, marker, typ in QUERIES:
|
||||
hits = search(q, mode)
|
||||
rk = rank_of(hits, marker)
|
||||
hit3, hit5 = (1 if 0 < rk <= 3 else 0), (1 if 0 < rk <= 5 else 0)
|
||||
r3 += hit3; r5 += hit5; mrr += (1.0 / rk if rk else 0.0)
|
||||
bytype.setdefault(typ, [0, 0]); bytype[typ][0] += hit5; bytype[typ][1] += 1
|
||||
n = len(QUERIES)
|
||||
print(f"{mode:<10}{r3/n:>10.2f}{r5/n:>10.2f}{mrr/n:>8.2f}")
|
||||
per_type[mode] = {t: f"{v[0]}/{v[1]}" for t, v in bytype.items()}
|
||||
types = list(dict.fromkeys(t for _, _, t in QUERIES))
|
||||
print("\n各模式 recall@5 分查询类型(看各路强弱):")
|
||||
print("模式".ljust(10) + "".join(t.rjust(10) for t in types))
|
||||
for mode in MODES:
|
||||
d = per_type[mode]
|
||||
print(mode.ljust(10) + "".join(d.get(t, "").rjust(10) for t in types))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user