#!/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()