☰
XERJ `/_decide` 实战:用本地 k-NN 投票在 autoindex 阶段给邮件打标签
2026/10/18 1:24:16 网站建设 项目流程
  • 人工智能
  • 搜索引擎
  • 本地部署
  • 后端
  • MCP 服务

【免费下载链接】xerj

XERJ is the new way for AI to search data. Its autoindex capability activates agents to know your data without the token waste of grep and sed. One command indexes code, docs, logs and PDFs for search, RAG, security audits and agent memory, using 40x fewer tokens than grep. Elasticsearch compatible, so existing clients just work.

项目地址:https://gitcode.com/GitHub_Trending/xe/xerj
点击查看免费下载

XERJ 的/_decide端点把"决策"变成了一次本地检索:在没有外部 API、没有出口流量、没有模型调用的情况下,对一个带标签的历史索引做加权 k 近邻投票,返回 label、confidence、abstain 与邻居证据。本文以 discussion #1012(是否让xerj autoindex在索引阶段为文档自动附加标签,以邮件为起点)为背景,完整复盘 2026-09-23 那次基于/_decide的本地邮件分类原型:合成语料的构造、easy/hard 两层评测方法、60/60 与 5/8 两个结果、置信度在"词汇跨界"输入上不可靠这一关键发现,并给出可复现的命令与源码级原理。

为什么要做这次原型:discussion #1012 的问题

discussion #1012 提出的问题是:xerj autoindex是否应该在索引每个文档的同时,为其附加一个"最佳标签 + 分数",起点是邮件分类。讨论中明确了两种路线:

  • 托管路线:扩展jev-reranker客户端,把 yes/no 问题改成分类问题——但该扩展当时并不存在;
  • 本地路线:直接使用已经发布、且不发任何数据出节点的POST /_decide端点,对带标签的历史索引做加权 k 近邻投票。

原型选择了本地路线,理由很直接:/_decide已经随 XERJ 发布(xerj v1.0.0-rc.76),"不需要客户端库、每次请求一个 POST、响应结构清晰(label/confidence/abstain/neighbours)"。这次评测的全部代码沉淀在 benchmarks/systemone-classify-email,随 PR #1026 合入。

/_decide是什么:一次投票,而非一次推理

从源码 engine/crates/xerj-api/src/systemone_api.rs 的模块注释看,/_decide与原生路由POST /v1/systemone共享同一个"决策阶梯"(decide ladder,issue #1057):

  1. History vote(第 1 层):当[decisions] index配置且能返回带标签的邻居时,加权投票胜出。这是被基准测量的一层,且会展示证据;
  2. Local zero-shot head(第 2 层):ModernBERT 类 candle 分类器,需要decide-localfeature 与--decide-mode local --decide-model-dir <dir>(或环境变量对XERJ_DECIDE_MODE=local/XERJ_DECIDE_MODEL_DIR),回答所有"无支持"的问题;
  3. Hosted key(第 3 层):保留、未构建。

邮件分类原型走的是第 1 层(History vote),并且特意使用了默认词法嵌入器——没有--embed-mode neural,没有向量配置,纯 BM25 检索。投票的具体算术是倒数排名加权:每个邻居的权重1/(rank+1),按标签聚合后,得票占比最高的标签即为答案,其占比即confidence。带human: true的文档额外乘decisions.human_weight(默认 2.0)。

/_decide的请求格式(ES 兼容端口,默认 9200):

{"index": "email-classify-proto", "question": "<邮件全文>", "k": 10}

响应包含label、confidence(获胜标签的得票占比)、abstain(低于decisions.min_confidence时为 true)、neighbours(每个邻居的_id、label、_score、weight与原文)以及tier/source。也就是说,每个答案都带着生成它的证据,可以事后核查。

语料构造:为什么用合成邮件,而不是真实收件箱

gen_corpus.py 的 docstring 说得很直白:这批邮件是脚本原创生成的文本,不来自任何真实邮箱或第三方数据集——因此没有版权问题、没有隐私问题。它只是一个 sanity-check 语料,而不是对真实收件箱准确率的声明。

语料配置与讨论 #1012 的示例标签完全对齐:

  • 4 个标签:invoice、newsletter、personal、needs-reply;
  • 每个标签 50 封训练 + 15 封留出测试(200 训练 / 60 测试),seed 7,确定性生成——重跑gen_corpus.py会逐字节复现corpus.json;
  • 模板化文本让 4 个标签拥有近乎互斥的词汇("Invoice"、"unsubscribe"、"Hey<name>"、"confirm by"),这正是下面"hard 层"存在的原因。

生成器的模板从人物名、公司名、产品名、话题、计划、问题等词表中采样,例如发票邮件会生成"Invoice INV-10052 from Fenwick Cloud — $30,433.51 due Dec 3"这样的主题,正文固定包含"Amount due"、"late fee of 1.5%"等发票特征词。

两层评测:100% 是陷阱,hard 层才是真问题

README.md 记录了关键的方法论教训:第一轮跑出可疑的 100% 之后,评测补上了一个手写、故意跨界的 hard 层(8 封,每标签 2 封):

  • 一封提到金额的个人邮件("owe you for gas",含 $40);
  • 一封全文没有 "invoice" 一词的发票邮件("for the offsite",只有金额与分摊说明);
  • 一封以直接提问结尾的 newsletter("before Friday's launch",询问选 A 还是 B);
  • 一封措辞像状态更新的 needs-reply 邮件("This week at Cedarline: two quick things")。

hard 层的存在理由在 gen_corpus.py 中写得很清楚:模板层给了每个标签近乎专属的词汇,投票可以轻易抓住;hard 层故意借用另一标签的词汇和话题,让满分变得有意义。

结果:easy 层 1.000,hard 层 0.625

完整原始数据在 results/run.json,运行环境为本地构建的xerj v1.0.0-rc.76、--insecure --port 9420、一次性--data-dir、默认词法嵌入器:

层naccuracy平均 confidencems/item
held-out test(与训练同源模板)601.0001.0002.6
hard(手写、词汇跨界)80.6250.9021.7

README 对第一行的定性非常克制:它只是说明"模板按标签各自使用的词汇可被完美分开"——这正是 hard 层要抓的失败模式,不要把 1.000 当作任何准确率声明。

hard 层的 3 个错误(全部来自 8 封 hard 邮件):

主题goldpredictedconfidence
"This week at Cedarline: two quick things"needs-replynewsletter1.000
"for the offsite"invoiceneeds-reply0.836
"did that ever go through?"invoicepersonal1.000

注意两个invoice错误(该标签在此层 P=0.000、R=0.000,n=2)都是完全没有发票特征词汇的案例——一句随意的 "did the transfer land yet?"——并且两次都以 0.836–1.000 的置信度被错误且自信地分给了别的标签。

关键发现:confidence 不是可靠的 abstain 信号

这是这次原型留给维护者最有价值的一条:

对与标签历史在词法上毫无相似之处的输入,/_decide的 confidence不能作为可靠的弃权信号——它在 3/8 个故意设难的样本上"错了还自信"(0.84–1.0),与 discussion #1012 对托管路径标记的校准问题(FiQA 上 ECE 0.31)同形。

机制上的解释很直白:这个投票是在词法/BM25 相似度上进行的。一封与它自己标签的训练样本毫无共同词汇的跨界邮件,会被投票进它确实相似的另一个标签——而且投得很自信。因此在实践中:

  • decisions.min_confidence需要调得远高于 0才能兜住这类情况;
  • 即便如此,在本样本中 confidence 数字本身也不是可靠标志(0.836–1.000 同时覆盖了命中与未命中)。

本次运行把decisions.min_confidence留在默认值0.0(永不 abstain),所以每个测试项都会得到标签。真实部署大概率应将其设为大于 0,让低置信文档保持"未标记"而不是猜测。

复现步骤

# 从构建好的 release 二进制启动(engine/target/release/xerj) xerj -d /tmp/classify-proto-data --insecure --port 9420 & cd benchmarks/systemone-classify-email python3 gen_corpus.py # 一次性生成 corpus.json(seed 7,确定性) XERJ_URL=http://localhost:9420 python3 run.py

run.py 做的事情完整可见:

  1. 读取corpus.json,把 200 封训练邮件批量索引进一次性索引email-classify-proto(mapping 为label: keyword+text: text);
  2. 对每封留出邮件POST /_decide,k=10——与已发布的 Banking77 / SMS-spamdecisions-as-retrieval数字所用 k 一致;
  3. 对比返回的label与真实标签,输出 accuracy、混淆矩阵、各标签 P/R/F1、未命中明细,并把逐项结果写入results/run.json。

过程中用到的关键配置项(默认值见 engine/xerj.default.toml 与 docs/DECISIONS.md):

配置默认值本次原型的取值说明
[decisions] index""(空 = 两个端点 503)email-classify-proto带标签示例的历史索引,每文档一个示例
k1010参与投票的邻居数
label_field/text_fieldlabel/textlabel/text历史文档的标签字段与文本字段
min_confidence0.00.0(未调)/_decide低于此值即 abstain
human_weight2.0默认human: true文档的投票权重乘数
calibrationnonenonep_cal校准(isotonic/temperature)

回答 discussion 的四个问题:这次测量能说什么

README 用实测结果回应了讨论中的四个问题,界限划得很清楚:

  1. "邮件,还是别的?"——原型没有回答。它只证明/_decide这个机制能跨标签集合泛化(机制里没有任何邮件专属逻辑),不说明哪类文档受益最大;
  2. "托管 vs 纯本地"——本地/_decide路径零新客户端代码、零出口;代价是 hard 层展示的那样:必须先有带标签的样本,按与已见样本的相似度投票,而且不比托管 judge 更校准——跨界邮件在这里同样被自信地投错。哪个权衡更好是产品问题,基准只能说明"两条路径都不自带校准";
  3. "一个最佳标签,还是每标签一个分数?"——/_decide两者都已返回:获胜的label+confidence(获胜标签的得票占比),邻居的原始投票权重也在响应里,调用方可以自行重建完整分布。按 hard 层结果,不要未经调参就把confidence当 abstain 信号;
  4. "标签放哪里?"——记录上的label/text与xerj-autoindex自己的约定(DOC_BODY_FIELDS = ["body", "text"],见 engine/crates/xerj-autoindex/src/infer/mod.rs)足够接近:真实集成更可能在记录上加普通字段(如_label/_label_confidence),而不是另建索引——因为/_decide本来就是从带标签历史索引读取的。

从源码看/_decide的边界与诚实性设计

/_decide背后的工程约束在 systemone_api.rs 中有多处体现:

  • 投票文本有上限:MAX_VOTE_TEXT_CHARS = 100_000,防止调用方让节点搜索任意长度的文本;
  • k 有钳制:请求里的k被clamp(1, 100),缺省用配置的 10;
  • 零支持不编造概率:没有标签邻居时/_decide返回abstain: true与reason;/v1/systemone侧则 422no_support,点名问题 id——"零支持必须是错误,而不是伪造的 0.5"。本地头(tier 2)是文档化的例外;
  • 模型名永不冒充:model恒为xerj-history-vote-1,本地头是xerj-decide-local-1,从不回显 Jev 模型名;
  • 校准的诚实契约:p_cal只在校准配置且拟合成功时才出现,未拟合时是null并附原因,从不拿p_raw冒充校准值;GET /_decide/_calibration发布完整可靠性曲线。

端到端测试在 engine/crates/xerj-api/tests/systemone_local_decide.rs,覆盖了无历史索引节点的 503→200 转变、同请求内 history/local 两层的顺序、模型名纪律与"缺失模型目录时大声失败"。

还要构建什么:从原型到xerj autoindex集成

本次原型只是用一个一次性 Python 脚本对着已标记索引调用/_decide。README 明确列出真正接入xerj autoindex还需要的东西(目前均未编写):

  • 一个默认关闭的 CLI 标志(符合 discussion 中"我想保留的真话");
  • 索引流水线中一个逐文档调用/_decide并附加label/label_confidence的步骤,且必须容错——/_decide报错或 abstain 都不能中断整次索引运行(与rerank阶段已有的策略一致);
  • 一个决策:带标签历史索引是由操作者提前构建,还是从--label-examples文件引导启动。

结论:什么时候该用/_decide做本地分类

这次原型给出了一个可以带走的判断:如果你手里已经有一批带标签的历史数据,/_decide是快速搭建本地分类器的可靠起点——无需客户端库、无需出口、每个答案附邻居证据,easy 层 2.6ms/项、hard 层 1.7ms/项的延迟也足够轻。但它不是零样本判断器:它回答的是"与这段历史相似的数据怎么说",而不是"这封邮件客观上属于哪类"。对与历史毫无词法相似性的输入,confidence 会自信地错;上线前必须用形状接近真实流量的留出集调decisions.min_confidence,并把confidence之外的结果审查(邻居文本)纳入流程。真实的xerj autoindex标签集成,则是在这套已验证的机制之上补 CLI 开关与容错步骤——那一步尚未编写,也正因此,这次原型的边界才值得作为集成设计的输入保留下来。

  • 人工智能
  • 搜索引擎
  • 本地部署
  • 后端
  • MCP 服务

【免费下载链接】xerj

XERJ is the new way for AI to search data. Its autoindex capability activates agents to know your data without the token waste of grep and sed. One command indexes code, docs, logs and PDFs for search, RAG, security audits and agent memory, using 40x fewer tokens than grep. Elasticsearch compatible, so existing clients just work.

项目地址:https://gitcode.com/GitHub_Trending/xe/xerj
点击查看免费下载

相关推荐

上一篇:从频率论到贝叶斯:Probabilistic-Programming-and-Bayesian-Methods-for-Hackers思维转变指南
下一篇:Omost 完全指南:LLM 写代码完成图像合成的四步链路

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询