mcpField
V
vecscope

vecscope

本地服务
github.com/076923开发工具1今天更新

关于 vecscope

vecscope 是用于嵌入空间和向量搜索索引的本地诊断命令行工具,面向构建检索系统和 Agent 的团队。它无需模型或网络,即可给出可复现的测量结果、判定和建议检查。

主要功能

  1. 01测量枢纽性、坍塌和各向异性
  2. 02将 ANN 召回率与精确近邻比较
  3. 03分析查询嵌入和聚类
  4. 04扫描 HNSW 与 IVF 索引参数
  5. 05为脚本和 Agent 输出 JSON

适用场景

  1. 01诊断搜索结果重复或无法触达
  2. 02检查 ANN 索引是否漏掉近邻
  3. 03比较查询与语料的嵌入空间
  4. 04按召回率目标选择索引参数

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"076923-vecscope": {
"command": "uvx",
"args": [
"vecscope"
]
}
}
}
启动命令uvx vecscope

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypivecscope0.12.0

工具

4

收录的工具列表 · 更新于 今天

参数类型必填说明
kinteger否
pathstring是
sampleinteger否
verboseboolean否
ann_pathany否
baselineany否
clustersinteger否
qrels_pathany否
thresholdsany否
tolerancesany否
ranked_pathany否
near_dup_cosnumber否
queries_pathany否
sample_repeatsinteger否
ann_from_sourceboolean否
query_baseline_pathany否

详细介绍

English · 한국어

vecscope

为 Agent 提供嵌入空间证据。

在 Agent 信任向量索引之前,先让它看看自己正在搜索的空间。

PyPI version Python CI MIT license

快速开始 · 测量内容 · 查询与聚类 · 索引扫描 · 比较两种嵌入 · 检索质量与修复顺序 · 从向量存储读取 · 与 Agent 搭配使用 · 在 CI 中使用 · 输入格式 · 真实嵌入 · 常见问题

pip install vecscope


Agent 每天都在构建 RAG 流水线、语义搜索和推荐检索,但它们看不到自己正在搜索的嵌入空间。

检索出错时,通常看起来不像是程序错误,而是:

  • “无论查询什么,返回的都是同样的三个文本块”
  • “每个分数都在 0.8 左右”
  • “top-k 里全是重复项”
  • “暴力搜索能找到,HNSW 却找不到”

文献中已有针对这些问题的明确、确定性诊断方法。vecscope 将它们整合为一条命令和一个 JSON:

  • 测量结果
  • ok / warn / fail 判定
  • Agent 可以据此采取行动的解释

它在本地使用 NumPy 运行,不需要模型或网络,每次都会报告相同的数字。

vecscope 诊断的是向量搜索实现:模型生成的几何结构、语料的整洁度、距离度量、索引和查询流水线。

它不衡量相关性。要判断针对某个问题是否能返回正确文档,需要带标签的查询;审计结果良好只能证明搜索没有妨碍检索,并不能证明检索效果良好。

快速开始

1. 安装
pip install vecscope
2. 获取嵌入文件

vecscope 读取每行包含一个嵌入向量的文件:即模型 encode 返回的 (n, dim) 数组,保存为 .npy、.parquet、.jsonl、.json 或 .csv (所有格式)。

import numpy as np
embeddings = model.encode(texts)          # shape (n, dim), e.g. (3000, 128)
np.save("embeddings.npy", embeddings)

手头没有嵌入?生成三个合成矩阵(健康、各向异性、塌缩):

python -c "import urllib.request as u; u.urlretrieve('https://raw.githubusercontent.com/076923/vecscope/main/examples/make_examples.py', 'make_examples.py')"
python make_examples.py
3. 运行审计
vecscope audit embeddings.npy
vecscope x.y.z  n=3000 dim=128 k=10  verdict=FAIL
  [ok  ] hubness                skewness=1.1661  (ok < 2, warn >= 2, fail >= 4)
                               k-occurrence skewness 1.17; top 1% of items take 3.9% of all neighbor slots (max N_k=51, expected 10).
  [WARN] antihubs               antihub_excess=0.0523  (ok < 0.05, warn >= 0.05, fail >= 0.2)
                               5.2% of items are never anyone's neighbor (random data at k=10 would give 0.0%); the excess is unreachable through k-NN retrieval.
                               try: inspect a sample of never-retrieved items; drop empty inputs, then apply the hubness fix
  [FAIL] anisotropy             mean_random_cosine=0.8732  (ok < 0.5, warn >= 0.5, fail >= 0.85)
                               random pairs have mean cosine 0.873 (isotropic ~0); mean unit vector has norm 0.93; centering would bring the cosine to -0.000.
                               try: subtract the corpus mean vector and re-normalize, applying the same transform to queries; details.mean_random_cosine_after_centering shows the expected result
  [ok  ] effective_rank         effective_rank_ratio=0.5407  (ok >= 0.1, warn < 0.1, fail < 0.05)
                               effective rank 69.2 of 128 attainable (128 dims; 54.1%); low values mean most dimensions carry nothing.
  [ok  ] collapse               near_duplicate_ratio=0.0000  (ok < 0.02, warn >= 0.02, fail >= 0.1)
                               0.0% of items have a neighbor with cosine >= 0.99 or an exact copy (exact duplicates 0.0%); no row has a near-duplicate.
  [ok  ] rogue_dimensions       top1_dim_variance_ratio=1.3070  (ok < 10, warn >= 10, fail >= 50)
                               dimension 94 alone holds 1.0% of variance, 1.3x the uniform share of 0.78%; top 3 dims hold 3.0%. Rogue dimensions dominate cosine.
  [ok  ] norms                  norm_cv=0.0000  (ok < 0.1, warn >= 0.1, fail >= 0.3)
                               vector norms vary with CV 0.000 (median 1.000, range 1.000-1.000); 0.00% are zero vectors. Unequal norms bias inner-product search toward long vectors.
  [ok  ] metric_mismatch        topk_overlap=1.0000  (ok >= 0.9, warn < 0.9, fail < 0.7)
                               cosine and inner-product top-10 share 100.0% of neighbors; 100.0% of items get identical lists. Low overlap means the index metric changes the results.
  [ok  ] distance_concentration contrast_over_noise=2.2784  (ok >= 1.4, warn < 1.4, fail < 1.1)
                               on 3000 rows a random item is 3.27x farther than the nearest neighbor (random distance 0.127); isotropic noise at 128-d and the same rows would give 1.44x, so the contrast is 2.28x that of noise. On all rows the contrast is 3.27x (median NN distance 0.039). Near 1.0 means distances carry no signal.
  [ok  ] outliers               outlier_ratio=0.0030  (ok < 0.01, warn >= 0.01, fail >= 0.05)
                               0.30% of items have no neighbor within cosine distance 0.050 (median 0.039); see details.sample_ids. Typically junk, empty or foreign-language content.
  [ok  ] connectivity           island_ratio=0.0000  (ok < 0.01, warn >= 0.01, fail >= 0.1)
                               0.00% of items sit in 0 small island(s) of the k-NN graph (fewer than 15 items; 60 components in total). Graph-based ANN (HNSW) attaches islands to the wrong region or never reaches them.
4. 阅读结果
  • 第一行给出整体 verdict:所有发现中最严重的判定。
  • 每项发现都会显示测量值、判定为 ok 的取值范围,以及一句说明该数字对检索意味着什么。判定不是 ok 的发现还会附上 try: 行,指出最值得先测试的最小改动。
  • 遇到 FAIL 时,退出码为 1,因此同一条命令也可以用作 CI 门禁。
  • 添加 --json,即可为 Agent 或脚本获取同样的 JSON 证据。

vecscope 的测量内容

检查项指标正常警告失败失败时的含义
hubnessk 次出现分布的偏度< 2.02.0 到 4.0≥ 4.0少数项目出现在所有人的 k-NN 列表中,并会在每次查询中排在最前面
antihubs从未作为邻居被检索到的项目占比,高于随机基线 e^-k< 5%5 到 20%≥ 20%语料库的一部分无法通过 k-NN 触达
anisotropy随机配对的平均余弦值< 0.850.85 到 0.95≥ 0.95向量挤在狭窄的锥形区域中,导致分数压缩在一个区间内;优秀的检索器在不损失 nDCG 的情况下远低于此值,因此仅标记极端值
effective_rank协方差谱的参与率 / 可达到的秩(维度数;当行数不超过维度数时为行数 − 1)≥ 0.100.05 到 0.10< 0.05大多数维度没有承载任何信息
collapse与某个邻居的余弦值 ≥ 0.99 或完全相同(包括零向量)的行所占比例;详情会列出重复组和样本配对< 2%2 到 10%≥ 10%top-k 名额被相同项目的副本占用
ann_recallANN 邻居相对于精确 k-NN 的 recall@k,按距离计数,因此精确并列项(重复项)可互换;并列容差为 1e-6 + 2e-6 x 距离,即 float32 舍入误差的两到三倍≥ 0.950.85 到 0.95< 0.85丢失结果的是索引,而不是模型
rogue_dimensions最大维度的方差相对于均匀的 1/dim 占比的倍数< 10x10 到 50x≥ 50x少数坐标决定每个余弦分数
norms向量范数的变异系数< 0.100.10 到 0.30≥ 0.30内积搜索偏向较长的向量;也会报告零向量
metric_mismatch余弦和内积之间的 top-k 重叠度≥ 0.900.70 到 0.90< 0.70索引度量会改变返回的结果
distance_concentration相对对比度(随机距离 / 最近邻距离)除以相同维度和行数下各向同性噪声的相对对比度≥ 1.41.1 到 1.4< 1.1最近邻几乎没有比随机项目更近;1.0 表示纯噪声
outliers最近邻距离异常远的项目占比< 1%1 到 5%≥ 5%垃圾、空内容或外来项目;ID 会列在 details 中
connectivity位于 k-NN 图中小型孤岛的项目占比< 1%1 到 10%≥ 10%规模太小、无法形成簇的自包含群组;HNSW 难以将它们连接起来,或者根本无法到达
query_gap查询到语料库的最近距离中位数除以语料库内部最近距离中位数< 2.62.6 到 3.0≥ 3.0查询和语料库不共享同一个空间,通常是因为查询由不同模型嵌入(5 到 10)。能够复现已发布 nDCG@10 的流水线得分为 0.94 到 2.52;缺少查询前缀不一定会让该值发生变化
query_shift查询中距离语料库超过参考查询集的中位数 + 3 MAD 的比例(需要基线)< 20%20 到 30%≥ 30%查询流水线相较于参考版本发生了变化:使用了其他模型或版本,或者流量涉及语料库未覆盖的主题。健康的查询集得分为 0 到 9%
ood_queries已弃用,请参阅 query_gap:查询中距离语料库超过语料库项目之间距离的第 99 百分位的比例< 50%50 到 80%≥ 80%与 query_gap 检查的问题相同,但此项也会统计短查询和长段落之间的正常距离(优秀模型在 BEIR 上的得分为 0 到 65%),并在复现的 18 条流水线中有 4 条触发了误报
query_hubness查询结果中语料库 k 次出现的偏度< 4.04.0 到 10.0≥ 10.0同样少数的项目回答了大多数查询;该值与不同模型的 nDCG@10 相关(Spearman −0.5 到 −0.9),语言不匹配的模型得分为 11 到 30。该值会随语料库规模增大而上升:最多包含 26k 个文档时,优秀的英文模型得分为 1.5 到 3.4;包含 58k 个文档时,得分为 4.0 到 4.8,因此对于大型语料库,请与基线比较
JSON 中的每条发现都包含 healthy_range、warn_at 和 fail_at,以及一个包含原始数值的 details
块。对于 anisotropy,该块还包含这些向量经过中心化后的余弦值,让 Agent 可以判断仅靠中心化是否能解决问题。

只有在传入相应输入时,才会出现某些发现:

  • ann_recall:通过 --ann 传入 ANN 邻居文件
  • query_gap、query_hubness 和 ood_queries:通过 --queries 传入查询嵌入
  • query_shift:传入查询嵌入,并通过 --baseline 指定一个包含查询的较早审计运行结果

每条发现还包含 likely_causes、suggested_action 和 verify_by。这些是推测,不是处方:

  • 只做最小幅度的更改
  • 重新审计
  • 在采用更改之前,先比较真实查询上的检索效果

Agent 技能详细说明了这一流程。

阈值

阈值为默认值:

  • 根据 7 个公开模型校准
  • 使用两种语言、9 个带标签的数据集对 19 个模型重新检查 (验证)

这些阈值是诊断的起点,并非通用的质量标准。嵌入空间因模型、语言、领域和维度而异,因此优质模型出现 warn 属于正常情况,审计结果干净也不保证搜索效果良好。

可在每次运行中覆盖任意一组阈值,也可以使用你自己的数据推导阈值,参见 vecscope calibrate:

vecscope audit vectors.npy --thresholds my_thresholds.json
# my_thresholds.json: {"anisotropy.mean_random_cosine": [0.4, 0.7], "hubness.skewness": [2.5, 5]}

这些键列在 src/vecscope/evidence.py(THRESHOLDS)中,并会在每个 JSON 结果的 thresholds 下回显。

查询和聚类

同时传入查询嵌入后,vecscope 会检查查询是否位于与语料相同的空间中。

  • 如果查询端使用了不同的模型,会显示为 query_gap。
  • 如果总是由同一批少数片段回答所有问题,会显示为 query_hubness;覆盖不了语料语言的嵌入模型也会导致这一问题。
  • 如果基线来自一次查询已知健康的运行,query_shift 会报告今天的查询与当时相比,有多少偏离了语料。这也能发现部分变化(部分流量使用了另一种模型,或涉及新主题)。
vecscope audit corpus.npy --queries queries.npy
vecscope audit corpus.npy --queries good_queries.npy --json > baseline.json
vecscope audit corpus.npy --queries todays_queries.npy --baseline baseline.json   # adds query_shift

这些发现无法检测缺失的查询前缀(bge、e5):在 SciFact 和 NFCorpus 上,这会使 nDCG@10 变化 -0.014 至 +0.007,查询距离也可能朝任一方向变化(验证)。请检查嵌入查询的代码中是否包含前缀。

添加 --clusters 10,即可附加每个聚类的映射:

  • 大小
  • 紧密度
  • 近重复项占比
  • 每个聚类承载的热门流量比例
  • Agent 可以查找的示例 ID

它不会给出任何判定,只会告诉 Agent 问题出在哪里。

  clusters (3):
     id   size  share  cos2ctr  neardup  hubshare  sample_ids
      2   1113  37.1%    0.217     0.0%     37.0%  [1282, 1611, 496, 283, 2041]
      0   1023  34.1%    0.232     0.0%     34.2%  [1855, 2932, 1786, 1025, 2476]
      1    864  28.8%    0.222     0.0%     28.9%  [2505, 647, 2740, 2499, 2965]

索引扫描

vecscope sweep 使用一组较小的参数网格构建 HNSW 和 IVF 索引,并针对该语料中的每种设置报告:

  • 相对于精确 k-NN 的 recall@k
  • 构建时间
  • 每秒查询数
  • 单次查询延迟百分位数(p50/p95/p99)
  • 序列化后的索引大小

随后,它会推荐达到目标召回率(默认为 0.95)的最快设置,并报告该设置超出目标的 margin。

如果 margin 小于 0.02,推荐结果会标记为 tight,并指出有足够余量的最快设置,因为语料增大时,固定参数下的召回率会下降:在 50k 行样本上刚好达到目标的设置,在完整语料上通常会达不到目标。

此功能需要 faiss-cpu 扩展。

pip install "vecscope[index]"
vecscope sweep embeddings.npy -k 10 --target-recall 0.95
  index params                                        recall  build s       qps  p50 ms  p95 ms     MiB  dev
 *hnsw  M=8, ef_construction=100, ef_search=16         0.987     0.35    237954   0.034   0.051     0.6  cpu
 *hnsw  M=16, ef_construction=100, ef_search=32        0.998     1.23    165139   0.084   0.108     0.8  cpu
 *ivf   nlist=76, nprobe=64                            0.999     0.14     33251   0.087   0.095     0.4  cpu
  recommendation: hnsw (M=8, ef_construction=100, ef_search=16)  recall=0.987  qps=237954  p95=0.051ms  index=0.6MiB  margin=+0.037  (fastest configuration with recall@5 >= 0.95)
  note: recall is measured on 200 queries drawn from the corpus itself; real query traffic can differ, pass it as queries when available
  note: HNSW recall at fixed parameters falls as the corpus grows, so a sweep on a sample is optimistic for HNSW; IVF nlist is set from the row count (4*sqrt(n)), so its recall at a given nprobe can move either way with size. Re-run at production size before fixing parameters
  note: queries_per_second is the median of 3 runs on this machine with one query batch; it ranks settings against each other and is not a serving throughput
  note: latency_ms_p50/p95/p99 time 200 queries one at a time in this process, with no network, concurrency or serving stack; compare settings with them, do not quote them as service latency
  note: index_bytes is the serialized index (vectors plus graph or lists); the process needs more while building, and a product-quantized index would be smaller

默认情况下,查询采用从语料中以固定随机种子抽取的样本;传入 --queries 可对真实查询计时。如果存在查询日志,请务必这样做:在从语料中抽取的查询上达到目标召回率的最快设置,可能并不适合真实流量。

  • 每种设置计时三次,并保留中位数(--timing-repeats)。
  • --latency-queries(默认为 200,设为 0 可跳过)也会逐个搜索,以计算百分位数。
  • index_bytes 是序列化索引的大小,因此评估设置时可以同时考虑内存和速度。
  • IVF 的 nlist 随行数变化(4*sqrt(n)),除非通过 --nlist 固定。要为现有索引调优 nprobe,请传入该索引的 nlist。

末尾的说明也会作为 caveats 纳入 JSON。MCP 工具为 sweep_index,参数与相应选项相同(n_queries、target_recall、device、margin、timing_repeats、latency_queries、nlist)。

扫描旨在回答一个问题:哪些参数能在此语料和这些查询上达到目标召回率,以及需要付出什么代价。这些数据仅适用于当前机器和当前进程;确定设置前,请在服务环境中使用该环境自身的延迟、吞吐量和内存测量结果进行确认。

使用 GPU 版 faiss(pip install "vecscope[index-gpu]";Meta 为 Linux x86_64 提供的 faiss-gpu 需要 CUDA 12.6 或更高版本)时,IVF 索引和精确基线会自动在 GPU 上运行;输出标题会标明设备。faiss 没有 HNSW 的 GPU 实现,因此 HNSW 仍在 CPU 上运行。使用 --device cpu|gpu 可强制指定设备。index 和 index-gpu 都会安装 faiss 模块,因此只能安装其中一个;stores 包含 index,因此在 GPU 机器上,请逐个选择存储后端扩展("vecscope[index-gpu,qdrant]")。如果驱动版本较旧,请自行安装社区构建的 faiss-gpu-cu12(CUDA 12.1)或 faiss-gpu-cu11,而不要安装 index-gpu。

比较两个嵌入

当两个模型嵌入了相同的项目,或同一个模型在变更前后进行了嵌入时,vecscope compare 会告诉你两个空间对邻近关系的判断有多一致。它会报告:

  • 最近邻和 top-k 列表重合的频率
  • 每个空间及两个空间中近重复项的占比
  • 两个空间是否选中了相同的重复项匹配对象
  • 分歧最大的行

它不会给出结论。它的作用是区分项目问题与模型问题:

  • 两个空间都以相同的匹配对象标记出 10% 的坍缩,说明这是项目本身的特性 (例如,同一张图像使用了多个 ID)
  • 只有一个空间标记出的重复项,则指向该模型
vecscope compare clip.npy lens.npy -k 10          # rows must be the same items in the same order
vecscope x.y.z  compare  n=3000 dim=128/128 k=10  neighbors=recomputed/full
  nn1_agreement=0.7050  topk_overlap=0.8537  topk_overlap_p10=0.7000  rows_with_no_overlap=0.0000
  near_dup A=0.0400  B=0.0400  both=0.0400  only_A=0.0000  only_B=0.0000  same_nearest_among_both=1.0000
    the two spaces share the nearest neighbor for 70.5% of rows and 85.4% of their top-10 lists
    on average (0.0% of rows share nothing). Near-duplicates: 4.0% in A, 4.0% in B, 4.0% in both,
    and 100.0% of those agree on the partner. Duplicates flagged by both spaces with the same
    partner are a property of the items; duplicates flagged by one space only point at that model.
  largest disagreements (row: nearest in A / nearest in B, top-k overlap):
    1769: 1340 / 1340  0.50
    1810: 1229 / 1229  0.50

引用一致性结果前,请先阅读 neighbor_scope。

超过 --sample 行(默认 100k)时,会比较一个有种子的子集;默认情况下,邻居会在该子集内部精确重新计算:某个项目的真实邻居若落在样本之外,比较时就会用替代项,这些替代项的一致率高于真实邻居。

在一对包含 2,000 行的合成空间中,500 行样本报告的 top-10 重合率为 0.75,而完整语料库的结果为 0.57。此类结果会标记为 neighbor_scope: sample,并在说明中注明。

若要在行样本上比较完整语料库的排序,请传入每个空间中基于完整语料库计算的邻居列表,可以是精确结果,也可以来自生产索引((n x >= k) ID 文件,会丢弃自身 ID 和负数填充值):

vecscope compare clip.npy lens.npy -k 10 --neighbors-a clip_knn.npy --neighbors-b lens_knn.npy

此时结果为 neighbors=supplied/full:行可以抽样,但邻居不是抽样结果;一致性比较的是你提供的列表,无论这些列表是精确计算还是近似计算。MCP 工具为 compare_embeddings(neighbors_a_path、neighbors_b_path)。

检索质量与修复顺序

检查结果描述的是向量空间;要判断是否返回了正确的文档,需要标签。

为查询传入相关性判断后,审计结果会新增一个 retrieval 区块:

  • 对每一行进行精确余弦搜索所得的 nDCG@k、recall@k、MRR@k 和命中率(即使检查结果经过抽样也如此)
  • duplicate_result_share:结果位中被排名更高结果的近重复项占据的比例

--ranked 会将生产索引返回的 top-k 与精确搜索进行比较评分,并给出 overlap_with_exact。

--ranked 接收任何用于响应查询的系统最终返回的 top-k:ANN 索引、带筛选条件的存储查询、混合搜索或重排器。精确基线是在每一行上执行的纯余弦搜索,不应用任何筛选条件、关键词评分或重排。

  • 如果这些阶段都在工作流中,overlap_with_exact 衡量的是整个工作流偏离向量搜索的程度,而不只是 ANN 召回率。
  • vecscope 不会分别评估各个阶段。若要找出导致质量下降的阶段,请依次将每个阶段的输出作为 --ranked 传入。
vecscope audit corpus.npy --queries queries.npy --qrels qrels.tsv
vecscope audit corpus.npy --queries queries.npy --qrels qrels.tsv --ranked prod_topk.npy

qrels 包含以下任一格式:

  • 每行一个 query row, corpus row, grade(.tsv/.csv,会跳过表头)
  • .json 中的 {"query row": {"corpus row": grade}}
  • .jsonl 中每行一个对象

BEIR 的 query-id/corpus-id/score 字段名也适用。行是查询文件和语料库文件中的位置。

分数会对至少有一个相关性(grade > 0)判断的查询取平均,因为没有相关项时 nDCG 和召回率未定义。没有相关项或完全没有判断的查询会计入 unscored_queries,并在说明中列出。

使用 --baseline 时,差异报告会在检查结果变更旁列出 retrieval_changes,因此评估变更时既会检查其预期修复的结果,也会检查检索效果:

  retrieval vs baseline:
    exact   duplicate_result_share   0.0512 -> 0.0000  (-0.0512)
    exact   hit_rate_at_k            1.0000 -> 1.0000  (+0.0000)
    exact   mrr_at_k                 1.0000 -> 1.0000  (+0.0000)
    exact   ndcg_at_k                1.0000 -> 1.0000  (+0.0000)
    exact   recall_at_k              1.0000 -> 1.0000  (+0.0000)

如果两次运行使用了不同的 qrels 或 k,retrieval_comparable 将为 false。

修复顺序

每份审计还包含一个 plan:按可能的共同原因将非 ok 的发现分组,并按修复顺序排列。每个步骤列出:

  • 其中的发现及其数值
  • 需要检查的共同原因
  • 用于验证原因的最小改动
  • 需要重新测量的数值
  • 该步骤位于此位置的原因
  plan (fix in this order; each step is a hypothesis to test):
    1. metric: metric_mismatch=0.0329 fail
       cause: vector norms vary, so cosine and inner product rank differently
       first: decide the index metric; L2-normalize if cosine is intended
       check: norm_cv near 0 and topk_overlap 1.0 on re-audit
    2. shared_direction: anisotropy=0.926 warn, antihubs=0.061 warn, collapse=0.108 fail
       cause: a large component shared by all vectors (a mean offset or a few high-variance coordinates) that compresses scores and creates hubs
       first: subtract the corpus mean and re-normalize (anisotropy.details shows the expected cosine after centering); if rogue_dimensions fails, z-score dimensions instead. Apply the same transform to queries and rebuild the index
       check: anisotropy and rogue_dimensions below warn_at, hubness.skewness down; retrieval ndcg_at_k not lower than before

这是一个在未归一化向量上带有共同偏移的合成语料(2,000 行,32 维);上面的差异是对两者都执行这两个步骤后,与归一化语料相比的结果。其 80 个带标签的查询足够简单,nDCG 始终为 1.0,因此只有重复项占比显示出变化。

顺序依据修复之间的依赖关系:

  • 首先处理有问题的输入(零向量、重复副本),因为它们会扭曲所有统计数据
  • 然后处理主导坐标(异常维度会决定每个距离,进而影响范数、离群点和枢纽,因此 z-score 处理先于归一化)
  • 然后处理度量指标
  • 然后处理居中等变换,因为它们会重建邻居图
  • 然后处理模型
  • 然后处理语料结构和索引
  • 最后处理查询侧,并根据修正后的语料进行测量

有些发现会移到其可能成因对应的步骤:

  • 如果近重复占比是由共同方向导致的(所有余弦值都高于阈值,没有完全重复的副本),则将它归入方向问题,而不是去重问题。
  • 如果 query_gap 与重复项同时出现,则将其归入重复项。近似副本会缩小该间隔所除以的语料距离,因此应先去重,再重新读取该指标,然后再归咎于查询。
  • 如果与近重复组相邻的孤岛小于孤岛大小,则将其归入重复项,因为这些副本可能就是孤岛。

规则是固定的,列在 src/vecscope/plan.py 中。每个步骤都是一个待验证的假设,应查看其重新测量行,而不是将其视为诊断结果。

步骤中的每项发现都带有 placed_because,取值为以下之一:

  • "this group's cause covers it"
  • 将其归入该步骤的另一项发现的 verdict,例如 "rogue_dimensions is not ok; one coordinate moves this finding too"。这是基于共同出现情况的分组,因此请通过该步骤的重新测量数值进行检查。

修复对真实嵌入检索的影响见验证文档。

从向量存储中读取

路径可以指向 faiss 索引文件或向量数据库,而不是嵌入文件。

  • 按 id 升序读取行,因此对未更改的内容读取两次会得到相同的矩阵。
  • 证据会记录所读取来源的指纹。
  • details.sample_ids 中的行 id 会附带 sample_source_ids,即存储自身的 id。
pip install "vecscope[qdrant]"     # or [chroma], [milvus], [weaviate], [index] for faiss, [stores] for all
vecscope audit index.faiss --ann-from-source --sample 0
vecscope audit "qdrant://localhost:6333/docs?vector=dense" --save-ids ids.json
vecscope audit "qdrant+local:///data/qdrant?collection=docs"
vecscope audit "chroma+local:///data/chroma?collection=docs"
vecscope audit "milvus+local:///data/milvus.db?collection=docs"
vecscope audit "milvus://milvus.internal:19530/docs?api_key_env=MILVUS_TOKEN"
vecscope audit "weaviate://localhost:8080/Docs?grpc_port=50051"

--ann-from-source 会查询存储自身的索引,检索其中每个向量并按 --ann 的方式评分,因此无需导出邻居列表即可测量索引在生产环境中的召回率。

选项写在查询字符串中:

  • vector=(具名向量,或 Milvus 字段)
  • https=1
  • pk=(Milvus 主键)
  • api_key_env=NAME(从该环境变量读取密钥;不会使用 URI 本身包含的密钥)
  • timeout= 网络存储每次请求的秒数(默认值为 60)

读取操作是只读的,除非 Milvus 集合尚未加载:此时会为读取而加载集合,并在读取后再次释放。

密钥不会出现在输出中:

  • 证据会记录不含用户信息的 URI,并将除上述选项之外的所有查询值替换为 REDACTED。
  • 存储客户端报错时,会在重新抛出错误前清除 API 密钥和这些值。
  • Chroma 的 HTTP 客户端本身不接受超时设置,因此会在连接后将 timeout= 设置到其会话上;客户端创建时执行的连接检查没有超时设置。

MCP 服务只会读取其允许的目录中的本地存储;只有在使用 --allow-network 启动时才会读取网络存储。

大型语料库:增量审计

对每一行进行审计,复杂度为行数的平方。当语料库通过追加而增大,或行在原位重新嵌入时,--knn-cache 会在多次运行之间保留精确的邻居列表,只计算这些行发生的变化。

vecscope audit day1.npy --sample 0 --knn-cache knn.npz    # created: all rows
vecscope audit day2.npy --sample 0 --knn-cache knn.npz    # extended: only the appended rows
vecscope audit day3.npy --sample 0 --knn-cache knn.npz    # updated: changed rows and their neighbors

缓存会保存计算所用每一行的摘要。缓存会:

  • 对于相同的行,重复使用
  • 当行被追加且缓存中的行未发生变化时,扩展
  • 当部分缓存行包含新向量时,更新:重新计算这些行,以及邻居列表中包含其中任一行的行,并针对每一行进行计算;其他行只检查发生变化和新追加的行
  • 其他情况下重建:需要重新计算的行超过总行数的一半(此时更新的成本与重建大致相同)、有行被删除,或 dim 发生变化。knn_cache.reason 会说明原因。

变化会扩散:当 k=10 时,每个变化的行会出现在另外约十个列表中。随机更改 1% 的行会导致重新计算约 10% 的行,更改 5% 则会导致重新计算约一半的行,因此,对于少量、分散的重新嵌入以及追加操作,更新更划算。

除距离完全相同的情况外,邻居列表与完整计算的结果一致,审计结果在 float32 舍入误差范围内一致。在 Python 中,vecscope.incremental.cached_knn_for_audit(x, k, path) 会返回供 audit(x, knn=...) 使用的列表。

在 100,000 行、768 维的数据上(与性能表中使用的机器相同):

  • 追加 5,000 行,扩展缓存耗时 11 s,使用缓存进行审计耗时 4 s;完整审计耗时 94 s
  • 追加 20,000 行,扩展缓存耗时 43 s,使用缓存审计耗时 4 s;完整审计耗时 123 s

所有判定结果都与完整审计一致,最大相对值差异为 7e-5。创建缓存的耗时与完整审计大致相同(此处为 83 s)。

在 100,000 行中原位重新嵌入 1,000 行,重新计算了 10,625 行:更新缓存耗时 15 s,使用缓存审计耗时 5 s;完整审计耗时 95 s,判定结果相同。若更改 10,000 行,更新将重新计算 68,933 行,耗时 83 s,与完整审计的 89 s 大致相同,因此此时会重建缓存。按此规则重新运行,更改 10,000 行时重建缓存耗时 87 s,与完整审计的 87 s 相同。

与 Agent 配合使用

vecscope 附带一个 MCP 服务,提供四个只读工具:audit_embeddings、sweep_index、compare_embeddings 和 describe_findings。将其注册到任意 MCP 客户端:

pip install vecscope
claude mcp add vecscope -- vecscope-mcp --allow-dir /data/embeddings     # Claude Code
claude mcp add vecscope -- uvx vecscope mcp --allow-dir /data/embeddings # or without installing

vecscope mcp 和 vecscope-mcp 是选项相同的同一个服务。它已列入 MCP Registry,名称为 io.github.076923/vecscope。

{ "mcpServers": { "vecscope": { "command": "vecscope-mcp", "args": ["--allow-dir", "/data/embeddings"] } } }

Cursor 使用的 JSON 配置放在 .cursor/mcp.json 中,Claude Code 项目的配置放在 .mcp.json 中。Codex 从 ~/.codex/config.toml 读取 TOML:

[mcp_servers.vecscope]
command = "vecscope-mcp"
args = ["--allow-dir", "/data/embeddings"]

然后提出请求:

Audit the embeddings in data/chunks.npy before we tune the retriever, and tell me
which finding to fix first.
工具和响应
  • audit_embeddings 运行审计,输入与 CLI 相同。
  • sweep_index 运行索引扫描。
  • compare_embeddings 运行比较。
  • describe_findings 列出每项发现及其阈值、原因和操作。

响应以一行 summary 和一个 audit_id 开头。摘要会列出非 ok 的发现、检索 nDCG、第一个计划步骤,以及重新审计时与基线的比较结果。

更改后重新审计时,将该 ID 作为 baseline 传入。响应随后会包含 baseline_diff(判定和数值变化、regressions、improvements、检索变化),与 CLI 中的 --baseline 一样,无需 Agent 保存先前的响应。 服务会在内存中保留最近 32 次审计;已保存的响应或 --json 文件也同样适用。

verbose=false 会省略 ok 发现的详情、说明和操作。没有 MCP 的 Agent 可以使用带 --json 的 CLI 运行审计,并读取相同的证据。

.agents/skills/vecscope/SKILL.md 中的 Agent 技能会告知 Agent 何时运行审计,以及针对每种判定应采取什么措施:居中或更换模型、去重、重新排序,或重建索引。服务也包含这些说明,因此仅通过 MCP 连接的 Agent 也会遵循相同的流程。

服务可能读取的内容

这些工具为只读,但会读取 Agent 指定的任意路径,因此服务会限制可读取的范围。解析 .. 和符号链接后,位于允许目录之外的路径会在打开前被拒绝。

  • 默认情况下,仅允许读取启动服务的目录及其子目录。
  • --allow-dir DIR 用于指定允许的目录,如上面的设置所示。可以重复指定;环境变量 VECSCOPE_ALLOWED_DIRS(Unix 上路径以 : 分隔,Windows 上以 ; 分隔)具有相同作用。
  • --allow-any(或 VECSCOPE_ALLOW_ANY=1)会取消限制,使服务进程能够读取的所有文件都可访问。仅在自己的计算机上使用。
  • 网络向量存储还需要启用 --allow-network。只读并不意味着访问这些存储没有风险,因此仅在确实需要服务访问网络时授予此权限。

每个文件都通过一个句柄读取,并在打开后进行检查:它必须是普通文件,且必须是路径在允许目录内解析到的同一个文件。检查与读取之间被替换的符号链接、FIFO 或目录都会被拒绝。

本地数据库目录(如 qdrant+local://)仅按路径检查,因为客户端会自行打开文件。

单个请求无法耗尽服务的内存:

  • 大于 --max-read-mb(默认值为 4096;环境变量 VECSCOPE_MAX_READ_MB;设为 0 表示不限制)的文件会在读取前被拒绝。
  • 当存储中的向量总大小超过同一限制时,服务会停止读取。
  • 文件会直接从经过检查的句柄解析,不会预先复制到内存中。对于 500,000 x 384 的 .npy 文件(0.72 GiB),MCP 读取的峰值内存为 0.79 GiB,与 CLI 相同。

并行调用或使用高开销设置的调用也会受到限制:

  • 服务会在独立线程中运行每次工具调用。审计、扫描和比较会占用 --max-concurrent 个并发槽位之一(默认值为 1;环境变量 VECSCOPE_MAX_CONCURRENT),后续调用会等待。因此,同时发起三次审计的 Agent 只会让一个语料库占用内存,而不是三个。
  • 会增加单次调用开销的设置会受到限制:k 和 clusters 上限为 1,000,sample_repeats 和 timing_repeats 上限为 10,n_queries 上限为 100,000,latency_queries 上限为 10,000。CLI 不设上限。

每次审计、扫描和比较都会向服务的 stderr 写入一行日志,MCP 客户端会将其保存在日志中。日志会列出工具名称和读取的每个文件或存储(凭据会脱敏)、判定结果或错误,以及耗时(包括等待槽位的时间):

vecscope-mcp: audit_embeddings path=/data/run/corpus.npy qrels_path=/data/run/qrels.tsv: fail in 2.1 s

baseline 所引用的近期审计记录会在锁的保护下保存。stdio 服务只为启动它的一个客户端提供服务;并行运行的 Agent 会各自启动独立的服务。

已测试的客户端

诊断、修改和重新验证循环已通过 MCP stdio 协议测试(tests/test_agent_workflow.py),并由 Claude Code 在无头模式下端到端运行(使用上述服务执行 claude -p)。

它按计划对一个存在共享偏移的语料库进行了审计、归一化、居中和重新审计,并在剩余步骤超出要求的修复范围时停止。Cursor 和 Codex 使用相同的协议,但尚未运行测试。

docs/agent_case.md 记录了两个使用真实 SciFact 嵌入的案例,每个案例均运行两次:

  • 两处数据摄取故障:一个批次被重复摄取,一个工作进程跳过了归一化。仅凭症状,Agent 就找到了这两处问题,修复语料库,并根据首次审计的 audit_id 重新审计。生产环境的 nDCG@10 从 0.100 提升至 0.713。
  • 使用 nprobe=1 搜索的 IVF 索引:审计发现嵌入正常,但生产环境的排序效果比精确搜索低 0.29 nDCG。Agent 扫描了索引,选定 nprobe,生产环境的 nDCG@10 从 0.422 提升至 0.700 和 0.711。
如果服务停止连接

“vecscope-mcp not found”通常意味着安装它的 Python 环境已被替换。这种情况常见于会重建环境的托管笔记本和集群。

在客户端使用的解释器中运行 pip show vecscope 检查,然后在该环境中重新安装。接着通过该解释器注册服务,这样报错就会指出正确的环境:

{ "mcpServers": { "vecscope": { "command": "/path/to/env/bin/python", "args": ["-m", "vecscope.mcp_server"] } } }

用作库

import numpy as np
from vecscope import audit

embeddings = np.load("embeddings.npy")    # any (n, dim) float array

ev = audit(embeddings, k=10)
ev.verdict                                # "ok" | "warn" | "fail"
ev.findings[0].value                      # measured value of the first finding
ev.to_dict()                              # the full evidence, same as --json

在 CI 中使用

JSON 输出在每次运行间保持稳定,并包含 schema_version,因此可以将其提交为基线,并在每次重建索引时进行差异比较。

--baseline 会打印每项发现的变化;如果任何判定结果变差(或触发了 --fail-on),则以状态码 1 退出。这样,流水线会因回归而失败,而不是因原本就存在的 warn 而失败。

vecscope audit embeddings.npy --json > baseline.json          # once, reviewed and committed
vecscope audit embeddings.npy --baseline baseline.json        # on every rebuild
  baseline baseline.json: verdict ok -> fail  (n, dim, k, sampling, thresholds or near-dup cutoff differ: values and verdicts are not directly comparable)
    hubness                1.0454 -> 0.8594  (-0.1860)  ok -> ok
    antihubs               0.0383 -> 0.0105  (-0.0278)  ok -> ok
    anisotropy             0.0105 -> 0.0127  (+0.0022)  ok -> ok
    collapse               0.0000 -> 0.8000  (+0.8000)  ok -> fail  WORSE
    ...
  regressions: collapse

示例会比较 healthy.npy 和 collapsed.npy,它们的行数不同,因此第一行会显示说明;如果使用相同设置重建相同语料库,则不会显示说明。

使用 --json 时,差异会附加在 baseline_diff 中;库调用为 diff_evidence(new, old)。如果基线由较旧的 schema_version 写入,则会被拒绝,并提示重新创建基线。

为基线和检查审计每一行(--sample 0),以便两次运行检查相同的行。如果两次运行都传入 --queries,基线中的查询距离还会生成 query_shift(参见查询与聚类);由于基线本身没有参照项,它会显示在差异中的 only_in_new 下。

漂移容差

判定结果是一个粗略信号:某个值可能在数月内逐渐接近阈值,却始终未越过它。因此,每条变化记录都会包含:

  • worsened:该值是否朝不利方向移动
  • worsening:变差的幅度,以相应指标的单位表示,正值 = 变差

--tolerances 会将这种漂移转化为独立于判定区间的门槛:

# tolerances.json: {"collapse": 0.01, "ann_recall": 0.02, "hubness": 0.3}
vecscope audit embeddings.npy --baseline baseline.json --tolerances tolerances.json --require-comparable
  • 如果某项发现的值恶化幅度超过其容差,该项会列在 value_regressions 下(文本:VALUE WORSE by 0.0300 > 0.01),并以状态码 1 退出,即使其判定结果没有变化。
  • 没有设置容差的发现仅根据判定结果评估。
  • 值改善绝不会被视为回归。

容差是针对已提交基线、每次重建可用的预算,因此,当某项变更经过审查并获批后,应重新提交基线。

运行还会报告 comparable:当 n、dim、k、采样、阈值或近重复项截断值与基线不同时,该值为 false。

--require-comparable 会将这种情况视为错误(以状态码 2 退出),而不是提示。对于门槛检查,这是正确的策略,因为不同设置之间的差异反映的是设置,而不是语料库。库调用为 diff_evidence(new, old, tolerances={...})。

哪条规则导致运行失败

有两种不同的规则可能导致运行失败,输出会将它们分开:

  • 绝对规则是相对于阈值的判定结果(默认为 --fail-on fail;使用 --fail-on warn 可采用严格策略)。
  • 基线规则是 regressions(判定结果变差)和 value_regressions(值恶化幅度超过其容差)。

默认值是根据公开模型校准的,因此,如果某个领域中的健康语料库在某些发现项上处于 warn 或 fail,就不应由绝对规则阻止运行。 --fail-on none --baseline baseline.json 只根据相对于已审查基线的变化进行判定。

每次运行结束时都会输出一个 gate 区块(JSON)或一行文本,指出最终生效的规则:

  gate: FAIL; verdict fail reaches --fail-on fail; verdict worse than baseline: collapse
  gate: pass (baseline)

首先将门槛视为趋势追踪工具。什么程度的变化算“过大”,取决于这些发现与您自己的查询检索质量之间的关系。

检索质量研究展示了这一关系在公开数据集上的情况;校准适合您领域的阈值则说明如何在自己的数据上进行测量,再据此阻止合并。

CLI 选项

vecscope audit vectors.npy -k 20                       # neighbors per item (default 10)
vecscope audit vectors.parquet --json                  # JSON evidence
vecscope audit vectors.npy --ann hnsw_neighbors.npy    # score ANN recall against exact k-NN
vecscope audit vectors.npy --queries queries.npy       # add query-side findings
vecscope audit vectors.npy --queries q.npy --qrels qrels.tsv   # add nDCG/recall/MRR of exact search
vecscope audit vectors.npy --queries q.npy --qrels qrels.tsv --ranked topk.npy   # and of the production ranking
vecscope audit index.faiss --ann-from-source --sample 0     # read a faiss index or store; score its own neighbors
vecscope audit "qdrant://host:6333/docs" --save-ids ids.json   # vector store URI; write ids in row order
vecscope audit vectors.npy --sample 0 --knn-cache knn.npz   # reuse / extend / update exact neighbor lists between runs
vecscope audit vectors.npy --clusters 10               # append a per-cluster report (at most n/10 clusters)
vecscope audit vectors.npy --thresholds th.json         # override (warn, fail) pairs
vecscope audit vectors.npy --fail-on warn              # exit 1 on WARN as well
vecscope audit vectors.npy --fail-on none --baseline old.json   # ignore the absolute thresholds; fail only on regressions
vecscope audit vectors.npy --baseline old.json         # diff against an earlier --json run; exit 1 on regression
vecscope audit vectors.npy --baseline old.json --tolerances tol.json   # also exit 1 when a value worsens beyond its tolerance
vecscope audit vectors.npy --baseline old.json --require-comparable    # exit 2 when the baseline used different settings
vecscope audit vectors.npy --near-dup 0.98             # cosine threshold for near-duplicates
vecscope audit vectors.npy --sample 0                  # audit every row (default samples 100k)
vecscope audit vectors.npy --sample-repeats 3          # re-audit 3 samples; report value ranges and verdict stability
vecscope sweep vectors.npy --target-recall 0.95        # HNSW/IVF parameter sweep (faiss extra)
vecscope sweep vectors.npy --device gpu                # IVF on the GPU (faiss GPU build)
vecscope sweep vectors.npy --margin 0.05               # headroom under which the pick is marked tight (default 0.02)
vecscope sweep vectors.npy --n-queries 2000 --timing-repeats 5   # queries to time (default 1000), timing runs per setting (default 3)
vecscope sweep vectors.npy --latency-queries 0         # skip the single-query latency percentiles (default 200 queries)
vecscope sweep vectors.npy --nlist 128                 # sweep nprobe on the production index's list count
vecscope compare a.npy b.npy -k 10                     # agreement between two embeddings of the same rows
vecscope compare a.npy b.npy --neighbors-a a_knn.npy --neighbors-b b_knn.npy   # full-corpus neighbors on a sample of rows
vecscope calibrate fit_*.json --min-quality 0.45 --out th.json   # gate thresholds from your own pipeline states
vecscope calibrate fit_*.json --min-quality 0.45 --holdout hold_*.json --include-low-confidence   # check on held-out states; also write exploratory pairs

输入格式

嵌入文件每行包含一个向量。审计前,每种格式都会转换为形状为 (n, dim) 的 2-D float32 数组。

值必须为有限值:NaN、Inf 和绝对值大于 1e16 的值(其平方会导致 float32 溢出)会被拒绝,并显示相应消息。

在以下示例中,embeddings 是该数组,例如模型 encode 方法的输出:

embeddings = model.encode(texts)        # shape (n, dim), e.g. (3000, 128)
embeddings[0]                           # the vector of the first item
# array([ 0.0312, -0.1187,  0.0459, ...], dtype=float32)
扩展名预期布局示例
.npy2-D 数值数组np.save("vectors.npy", embeddings)
.npz使用存档中的第一个数组np.savez("vectors.npz", emb=embeddings)
.parquet、.pq浮点数列表列,列名包含 emb 或 vec;其他列(id、文本)会被忽略pd.DataFrame({"id": ids, "embedding": list(embeddings)}).to_parquet("vectors.parquet")
.jsonl每行一个 JSON 对象;使用第一个值为列表的字段作为向量{"id": 7, "text": "...", "embedding": [0.1, -0.3, ...]}
.jsonJSON 对象数组(如上所示)或列表数组[[0.1, -0.3, ...], [0.2, 0.5, ...]]
.csv、.tsv、.txt每行一个向量,仅包含数字,不含表头或 id 列0.1,-0.3,0.8,...

以 50,000 x 384 个 float32 向量(一个 73 MiB 矩阵)为例,各格式加载时的内存占用:

格式读取方式峰值内存
.npy按存储形式读取0.10 GiB
.csv分块读取行0.18 GiB
.jsonl逐行读取,按行块转换0.34 GiB
.parquet仅将向量列分批读入一个数组0.39 GiB
.json一次性解析整个文件1.13 GiB

处理大型语料时,请使用 .npy、.csv 或 .parquet。.json 会整体解析,需要的内存是矩阵大小的数倍。

可选输入:

  • --ann(MCP 中的 ann_path):形状为 (n, k) 的整数数组。第 i 行包含 ANN 索引为第 i 个项目返回的 k 个邻居的 id,行顺序与嵌入文件一致。以上任意格式均可使用。
  • --queries(MCP 中的 queries_path):采用上述任意格式的查询嵌入,且 dim 必须与语料相同。
  • query_shift 参考:某次使用 --queries 运行的审计所生成的较早版 --json 输出(--baseline,或 MCP 中的 baseline 或 query_baseline_path)。

ANN id 指向行位置,因此文件必须来自基于这些完全相同的行构建的索引,并且必须审计每一行。超过默认的 --sample 大小(100k 行)时,请传入 --sample 0,否则审计会拒绝运行。指向项目本身的 id 会被忽略,因此,如果索引会先返回自身,查询时应使用 k + 1。

只有当 query_shift 参考是在相同语料或其未更改的前缀上进行审计时才会使用(通过 corpus_fingerprint 检查,该值是行的 SHA-256)。归一化或中心化后,较早的查询距离处于另一空间,因此会跳过 query_shift,并在 query_reference_note 中说明原因。

性能

精确 k-NN 的计算量与行数的平方成正比:行数翻倍,耗时大约会变为 4 倍; 在笔记本电脑上,对 10 万行数据进行完整审计(--sample 0)需要几分钟。

k-NN 遍历按行分块,以使距离矩阵的大小保持在约 1 GiB,无论行数多少都如此。其他遍历需要矩阵的几个 float32 副本,因此峰值内存会随行数 × 维度增长,大约是矩阵大小的 4 倍。

测量环境为配备 8 个 BLAS 线程的 8-vCPU AMD EPYC 7R13(仅使用 NumPy,单进程,聚类合成语料库,k=10)。内存是审计本身分配的峰值;输入矩阵另计。在单独进程中运行 3 次所得的中位数与以下单次运行结果相差不超过 2%,操作系统测得的峰值内存相差不超过 0.04 GiB。

行数维度秒峰值内存(不含输入)
10,0003841.30.40 GiB
10,0007682.00.41 GiB
10,0001,5363.60.44 GiB
50,000384171.63 GiB
50,000768231.70 GiB
50,0001,536361.84 GiB
100,000384651.71 GiB
100,000768861.85 GiB
100,0001,5361303.45 GiB
200,0007683383.46 GiB
200,000,默认抽样 100k768852.14 GiB
  • 从 50k 行开始,每次行数翻倍,耗时约增长 3.5 到 3.9 倍,接近 n²。
  • 在 384 和 768 维下,从 50k 行开始,内存用量保持稳定,因为 k-NN 分块会随 n 增大而缩小;在 1,536 维和 200k 行时,内存用量会增加。
  • 默认抽样 100k 行,因此无论语料库多大,参与审计的行数都接近 100k。
  • 每个查询批次都会让 --queries 增加一次遍历语料库;只有向量范数存在差异时,metric_mismatch 才会增加第二次 k-NN 遍历。

要在你的机器上针对所关心的数据规模重新测量:

python examples/benchmark_scale.py --rows 50000 100000 --dim 768   # the audit table above
python examples/benchmark_sweep.py                                 # the index sweep

两者都接受 --corpus(clustered、isotropic、anisotropic、duplicates、lowrank、zipf),并会在 JSON 中记录机器信息。

抽样审计

阻止操作限制的是内存,而非工作量:精确遍历仍需约 n² × dim 次乘加运算, 因此,768 维的 100 万行数据,计算量大约是 10 万行数据的 100 倍。

对于这么大的语料库,请审计默认样本(或使用 --sample-repeats 运行多个样本), 并将 --sample 0 留给需要检查每一行的发现。抽样审计反映的是样本情况。哪些发现需要检查每一行:

发现样本能提供什么是否需要检查每一行
anisotropy, effective_rank, rogue_dimensions, norms, metric_mismatch, distance_concentration分布估计:在 200k x 384 的数据上,使用 100k、50k 和 20k 个样本时,结果与全量值相差不超过 2.3%,且判定不变否
hubness, antihubs下限:k-出现频次会随密度增加而集中,因此同一语料库在较小样本上的 hubness 得分较低(全部 200k 行时偏度为 2.6,判定为 warn;取四分之一时为 1.15;取十分之一时为 0.16)。除 fail 以外的判定均标记为结论不确定要衡量语料库自身的 hubness
collapse样本内部看到的重复项占比:只有一对中的两行都被抽中时才计数,因此,抽样占比约为样本比例 x 全量占比的 0.96 到 1.03;判定采用估算的全量占比,至少不低于统计每一行后得到的精确副本数量要统计或列出所有近重复项
outliers小样本会高估离群点数量(取十分之一时为 3.9 倍);除 ok 以外的判定均标记为结论不确定要找出所有离群点
connectivity样本自身 k-NN 图中的孤岛,而这些孤岛无法覆盖语料库中的大多数孤岛(四分之一样本中,属于孤岛的行占 37%,得分为 0.08%);除 fail 以外的判定均标记为结论不确定是,判断连通性必须检查每一行
ann_recall不支持抽样:ID 必须与 ANN 文件中的 ID 对齐是(--sample 0)
query_gap, query_shift, query_hubness, ood_queries来自样本的参考距离;每个查询都会评分。query_gap 是两个距离的比值,而这两个距离在较小样本中都会增大;只有在样本大小相同的基准之间,才能比较 query_shift否

因此,抽样审计会:

  • 列出 inconclusive_findings(表中标记为结论不确定的判定)
  • 在文本输出中将它们标记为 INCONCLUSIVE (sampled),并在门控行中列出它们
  • 根据 details.estimated_full_share 得出 collapse 判定(见下文)
  • 在发现的说明中紧邻数值重复说明相应注意事项

estimated_full_share 是样本占比除以样本比例得出的值;如果对每一行进行统计得到的精确副本占比(details.full_corpus_exact_copy_share)更高,则采用该占比。统计整个语料库中的精确副本成本很低(每行只需计算一个摘要),因此,样本中未抽到的稀有副本仍会计入。非精确副本的近重复项只能通过样本发现。

200k 行数据的数值来自 验证; 每次运行时,tests/test_regression_injection.py 都会在 6k 行数据上检查相同性质。

对于实际检查的行,每个数值都是精确的;vecscope 从不以近似近邻搜索替代精确搜索。唯一的近似之处在于所选取的行。

vecscope 的工作原理

vecscope 会对整个矩阵执行一次精确 k-NN,并据此得出所有诊断结果:

  • k-出现次数分布可用于衡量枢纽性和反枢纽
  • 最近邻余弦值可用于检测坍塌、离群点和距离集中
  • k-NN 图可用于衡量连通性
  • 随机向量对和协方差谱可用于衡量各向异性、有效秩和异常维度
  • 向量范数可用于衡量范数离散程度
  • 第二次内积扫描(范数一致时会跳过)可用于检测度量不匹配
  • 如果存在 ANN 邻居文件,会将其与精确邻居进行比较,以计算召回率

如果存在查询嵌入,也会针对语料库进行独立的精确搜索,以得出查询侧的诊断结果。

所有操作都是确定性的:固定种子、精确邻居、不包含学习组件,也不访问网络。同一文件运行两次会得到相同的 JSON。

精确 k-NN 的计算复杂度与行数的平方成正比,因此,行数超过 100,000 的输入会基于带种子的随机样本进行审计,除非传入 --sample 0。

真实嵌入

阈值基于七种模型在八个数据集上的公开预计算嵌入进行校准:OpenAI ada-002 和 text-embedding-3-large、Cohere embed-v3(韩语和英语维基百科)、all-MiniLM-L6-v2、bge-m3、mxbai-embed-large-v1 和 gte-base-en-v1.5。

下表展示了每个数据集前 40k 行的测量值;未标注的单元格均为 ok。运行 python examples/real_models.py 可复现结果,无需下载模型。

诊断项Cohere v3(韩语维基)Cohere v3(英语维基)ada-002(dbpedia)3-large(dbpedia)MiniLM-L6(维基)bge-m3(维基)mxbai-large(维基)gte-base(arxiv)
枢纽性1.471.161.561.211.591.851.432.28
警告
反枢纽0.0090.0050.0130.0050.0220.0260.0190.013
各向异性0.3330.2280.6920.0860.0260.3070.3300.210
有效秩0.1580.1780.090
警告
0.1480.2850.1420.087
警告
0.136
坍塌0.0020.0020.0000.0000.0010.0000.0170.000
异常维度3.23.23.02.51.71.82.01.8
范数0.0000.0000.0000.0000.0000.0000.0000.007
度量不匹配1.0001.0001.0001.0001.0001.0001.0000.952
距离集中1.721.741.741.431.781.842.211.62
离群点0.0010.0010.0000.0000.0010.0020.0030.000
连通性0.0000.0000.0000.0000.0000.0000.0000.000

对每个数据集的前 40k 行重新运行审计,可复现全部 88 项判定。唯一的 warn 是 gte-base 的枢纽性,以及 ada-002 和 mxbai-large 的 effective_rank。

ada-002 众所周知的各向异性(0.692)低于 warn 阈值 0.85:其平均单位向量的范数为 0.83,因此无关文档的余弦值起点接近 0.7。同一语料库上的后继模型则是各向同性的(范数为 0.29)。中心化后,这里的每个模型的随机向量对余弦值都变为 0.00,因此该诊断项报告的是这个数值。

文本嵌入的枢纽性偏度通常在 1.2 到 1.9 之间,因此 warn 阈值设为 2.0;arXiv 摘要上的 gte-base 略高于该阈值。各向异性阈值(0.85、0.95)来自下文的检索质量研究。

这些发现能预测检索质量吗?

部分可以,而且有明确的适用方式。docs/retrieval_quality.md 使用精确余弦搜索,在两个带标签的 BEIR 数据集(SciFact、NFCorpus)上,对 8 个公开模型以及在其中表现最佳的模型中注入的受控缺陷,逐一测量每项发现与 nDCG@10 的关系。运行 python examples/retrieval_quality.py 可重现该结果。

  • 在同一条流水线中,这些发现能反映损害。 注入异常维度、降低秩、添加噪声或复制行,会使受监测的发现变为 fail(从 ok 变为 fail;如果语料本身已有少量重复项,则从 warn 变为 fail),同时 nDCG@10 下降;缺陷程度越高,下降越明显。

    使用无关模型嵌入的查询,query_gap 得分为 8 到 10(fail,nDCG@10 接近 0);在 gte-base 语料上使用 bge-base 查询,两者共享同一向量空间,得分为 2.9 到 3.9,且 nDCG@10 保持不变。

    大量近似重复项也会提高 query_gap,因为副本会缩小它作为分母的语料距离;计划会将此项与重复项一并列出,以便去重后再次检查。

  • 在表现良好的模型之间,几何特征无法给模型排名。 在两个数据集上表现最佳的模型(gte-base、bge、e5)具有最高的各向异性(统计验证中的 19 个模型时,为 0.58 到 0.82),且有效秩处于常规水平。

    能区分较弱模型的是 norms 与 metric_mismatch(未归一化的输出),以及 hubness;它们与 nDCG@10 的 Spearman |rho| 为 0.4 到 0.8。

  • 单独的各向异性不会损害排序。 添加共享偏移,直到随机向量对的余弦值达到 0.98,nDCG@10 仍保持不变。这会压缩分数,因此绝对余弦阈值和分数校准会失效;当信号本身丢失时,distance_concentration 才会发生变化。

anisotropy、query_hubness、query_gap 和 query_shift 的阈值来自验证:在 3 个带标签的数据集上测试了 19 个模型和注入的查询故障,并在另外 3 个数据集上重新检查。

这项工作未涵盖的是,在其他领域或生产查询日志中,warn 和 fail 对质量下降的预测效果。在你的领域完成相关测量之前,应将判定结果视为进一步检查的理由,并将基线回归视为比较检索效果的理由,而不是质量分数。

根据你的领域校准阈值

默认值基于公开模型和公开数据集校准;你的语料中出现 fail 是值得关注的信号,但不能证明检索效果差;全部为 ok 也不能证明返回了正确的文档。

在将阈值或容差用作阻断条件之前,先测量它们在你自己的数据上能预测什么:

  1. 收集该领域的带标签查询(几百条并附上相关文档 ID 就够了),然后划分数据:一部分用于选择阈值,另一部分作为留出集进行检查。
  2. 针对流水线的每个候选状态(模型、分块器、变换、索引设置)运行 vecscope audit --json,并使用相同查询在相同状态下计算检索质量(nDCG@k、Recall@k、MRR)。examples/retrieval_quality.py 会对公开数据集执行这两项操作,也可以用于你自己的数据。
  3. 分析每项发现的取值与这些状态下的质量指标之间的关系。在你的领域中,如果某项发现的变化与质量无关,就不应以它作为门槛,无论其默认值表示什么。
  4. 仅使用第一部分数据,根据质量可接受的状态设置 --thresholds 和 --tolerances。
  5. 在留出集上确认阈值能区分可接受状态和其他状态。在同一批查询上调优并检查的阈值,表现会显得比实际更好。

vecscope calibrate 会根据第 2 步的审计结果执行第 3 至第 5 步。使用拟合集查询审计每个状态,并使用留出集查询再次审计每个状态:

vecscope audit state_a.npy --queries q_fit.npy --qrels qrels_fit.tsv --json > fit_a.json
vecscope audit state_a.npy --queries q_hold.npy --qrels qrels_hold.tsv --json > hold_a.json
# ... the same for every state, then:
vecscope calibrate fit_*.json --min-quality 0.45 --holdout hold_*.json --out th.json
vecscope audit vectors.npy --thresholds th.json

对于每项发现,它会报告:

  • 在各状态中与检索指标的 Spearman 相关系数(--metric;默认值为 nDCG@k)、其 95% bootstrap 区间,以及相关方向是否符合该发现所暗示的方向
  • 沿该方向进行的单侧置换检验 p 值,并对所有受检发现进行 Holm 校正
  • 可接受状态中的最差值,以及其他状态中的最佳值
  • 仅当该发现与质量变化一致且能区分两组时,才会给出一对(warn、fail)阈值,分别设在两组间距的三分之一和三分之二处

如果某项发现与你的数据中的质量变化无关,或变化方向相反,则不会给出阈值对:保留其默认值作为值得关注的信号。

同时测试约 15 项发现时,其中一项可能会在少数状态下偶然与质量变化一致。因此,只有校正后的 p 值低于 0.05 时,阈值对才会写入 --out:

  • 其他阈值对会标记为 exploratory,不会写入门槛文件。 --include-low-confidence 也会写入这些阈值对。
  • 只有五个状态时,任何结果都无法达到该标准(校正前最小单侧 p 值为 1/120)。建议使用 10 个或更多状态;状态较少时,报告会说明这一点。

两项检查会在未参与拟合的状态上评估建议:

  • leave_one_out 会在除一个状态外的所有状态上拟合,并对每个被留出的状态进行评估。
  • holdout 行会统计门槛能捕捉到多少个不可接受的 --holdout 状态,以及在可接受状态上产生的误报数。来自另一个数据集的留出状态可用于检查阈值是否适用。

校准至少需要三个状态,且 --min-quality 两侧都要有状态。如果状态使用了不同的 qrels 或 k,则会拒绝处理,因为它们的质量值无法比较(可用 --allow-mixed-qrels 覆盖)。

同样的流程也适用于 suggested_action:它只是对原因的假设,而中心化、标准化或截断向量都会改变排序。

  • 保留原始向量和检索结果作为基线。
  • 对副本应用变换。
  • 测量两者的质量、延迟和内存。
  • 只有带标签查询的效果有所提升时才采用变更,而不是因为某项发现变成了 ok。

常见问题

它会把我的向量发送到其他地方吗?

不会。vecscope 会在你的机器上使用 NumPy 计算所有内容。它不会加载模型,也不会建立网络连接。

它能处理多大的矩阵?

精确 k-NN 的计算复杂度与行数的平方成正比。默认情况下,如果输入超过 100,000 行,vecscope 会审计一个经过固定随机种子处理的 100,000 行随机样本,并在输出中注明(sampled: true、n_total)。

采样结果反映的是样本,而不是整个语料库:可能会漏掉罕见簇、小型孤岛和少数离群点。

即使经过采样,details 中的行 ID(sample_ids、sample_pairs、聚类报告)也始终对应输入文件中的行。使用 --sample 0 可审计全部数据;成本说明请参阅性能。

要了解样本本身会让数值波动多大,请传入 --sample-repeats 3(MCP:sample_repeats)。此时,vecscope 会审计三个独立的固定随机种子样本,并为每条发现附上 value_range(各样本中的最小值至最大值)和 verdict_stable,并在顶层列出 unstable_findings。

如果某条发现的判定在不同样本间发生变化,应将其视为临界情况,而不是语料库的固有特征。代价是需要进行三次审计。

value_range 表示样本间的波动范围,并非包含全语料库数值的区间:hubness 是每个样本值的下界,而且即使某条发现非常稳定,其数值也可能略微超出三个样本的范围。

不同发现从样本中得出的含义并不相同:hubness 会被低估,离群点会被高估,孤岛基本无法发现。性能表格说明了每项发现可能代表什么,以及采样审计会将哪些判定标记为 inconclusive。

我的生产模型未通过各向异性检查。模型坏了吗?

不一定。许多流行的文本嵌入模型在设计上就具有很强的各向异性。

这项发现表明,原始余弦分数被压缩在狭窄的区间内;对向量进行中心化或白化,或者重新排序,通常可以恢复区分能力。details 区块会报告中心化后的余弦值,帮助你判断这是否足够。判定结果是提供给 Agent 的证据,而不是对模型的评分。

哪些 Agent 可以使用它?

任何支持本地 MCP 服务的 Agent,以及任何能运行 shell 命令并读取 JSON 的 Agent 都可以使用它。技能文件介绍了这两种方式。

文档

  • Agent 技能:说明何时运行审计,以及如何处理每项判定
  • 检索质量研究:在带标签的数据上,将每项发现与 nDCG@10 进行比较
  • 验证:精确性、召回率评分、采样相关主张、查询发现、干预措施、Agent 集成,以及在 9 个带标签数据集上测试的 19 个模型
  • Agent 案例:展示 Agent 如何通过 MCP 发现并修复两个数据摄取故障和一个调优不当的 IVF 索引,包含工具调用和数值
  • 示例:合成矩阵、校准脚本和检索质量研究
  • 更新日志

贡献

欢迎提交问题和拉取请求。有关如何运行测试以及什么样的更改才是优质更改,请参阅 CONTRIBUTING.md。安全问题请通过 SECURITY.md 报告。

许可证

MIT

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。