Metadata-Version: 2.4
Name: faithsift
Version: 0.3.0
Summary: FaithSift —— RAG 回答的句子级引用忠实性检测器:三分类判定 + 跨语言召回 + 多跳证据
Author: FaithSift contributors
License: MIT
Project-URL: Homepage, https://github.com/zicong334/faithsift
Project-URL: Issues, https://github.com/zicong334/faithsift/issues
Keywords: rag,hallucination,citation,faithfulness,llm,nli
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: openai>=1.40
Requires-Dist: python-dotenv>=1.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Provides-Extra: semantic
Requires-Dist: fastembed>=0.3; extra == "semantic"
Dynamic: license-file

# FaithSift

**中文** | [English](README_en.md)

> 本项目原名 CiteCheck (renamed to FaithSift) —— 因名称冲突于 2026-10-27 更名,
> 缘由与证据见 [CHANGELOG](CHANGELOG.md)。旧 `CITECHECK_*` 环境变量仍作回退。

**RAG 回答的句子级引用忠实性检测器** —— 逐句判定 LLM 的回答是「有据 / 无据 / 与源冲突」,
并给出结论所依赖的证据定位。

给定一份回答(md/txt)和一组源文档,FaithSift 会把回答拆成句级论断,对每条:

1. **检索**:BM25 词法 + 数值不变性 + 中英概念词表(可选再接向量通道),找出最相关的证据片段;
2. **判定**三分类:**supported**(证据明确包含)/ **contradicted**(与同一事实的取值冲突)/
   **unsupported**(证据沉默,既不支持也不冲突);
3. **出报告**:逐条证据摘录、理由、整体忠实性得分,渲染 md / html / json。

`unsupported` 被单列出来是整套设计的核心:把「说错了」和「编了一个语料没覆盖的话题」分开,
工程上才能判断问题出在检索召回还是生成篡改。

## 实测成绩(诚实声明)

> **v0.3 更新**:困难金标从 49 条扩到 **150 条**(13 个类别,新增「量级错误 / 时间错配 /
> 情态篡改」),并接入**本地离线向量通道**(`--embedder local`,fastembed + 多语 MiniLM,
> 中↔英跨语言,零 API)。下表的「启发式(词法)」是**零依赖默认档**;「启发式(+向量)」
> 指 `--retriever hybrid --embedder local`,判官在词法之外多一条语义余弦证据。

| 数据集 | 条数 | 启发式(词法) | 启发式(+向量) | LLM 判官 | 说明 |
|---|---|---|---|---|---|
| `gold/gold.jsonl`(合成) | 178 | **100.0%** | — | **99.44%** | 词法友好,只作**回归下限** |
| `gold/hard_gold.jsonl`(人工) | 150 | **88.7%** | **88.7%** | **99.33%** | 改写/跨语言/多跳/方向/实体/量级/时态/情态;产物 `results/baseline_v03_hard_heuristic.json` / `results/v03_hard_heuristic_hybrid150.json` / `results/v03_hard_llm.json` |

> 困难金标的**原 49 条子集**:纯词法 91.8%(45/49)、词法+向量 **100%**。向量通道专门补回
> 纯词法漏判的「字面几乎不重合的重度同义改写」(如「四到六个周期、每个九十分钟」
> ↔ 源文「4 至 6 个周期,每个约 90 分钟」),这正是 README 早先预告的向量通道用武之地。

困难金标(150 条)按类别准确率:

| 能力 | 词法 | +向量 | LLM | | 能力 | 词法 | +向量 | LLM |
|---|---|---|---|---|---|---|---|---|
| 逐字抽取 | 100% | 100% | 100% | | 数字篡改 | 100% | 100% | 100% |
| 单位错误 | 100% | 100% | 100% | | 量级错误(新) | 100% | 100% | 100% |
| 貌似合理缺失 | 100% | 92% | 100% | | 中文同义改写 | 94% | **100%** | 100% |
| 跨语言(无数字) | 94% | 94% | 100% | | 多跳证据 | 93% | 93% | 100% |
| 实体错配 | 92% | 92% | 100% | | 方向反转 | 86% | 86% | 100% |
| 时间错配(新) | 83% | 83% | 100% | | 相邻话题 vs 冲突 | 64% | 64% | 100% |
| 情态篡改(新) | **0%** | **0%** | 83% | | | | | |

多跳证据召回:词法 13/14、LLM 14/14。

**必须说清楚的几件事:**

1. 合成金标 100% 不代表真实水平。它由「原文抽取 / 数字篡改 / 模板套话」三类规则生成,
   天然对词法友好,只保证重构不会把做对的事弄坏。真实能力看困难金标。
2. 困难金标现为人工撰写的 **150 条**,每条带标注理由(`rationale`),且**确定性可重建**
   (`python -m faithsift gold` 幂等,固定 seed)。但它仍是一人判断,不是多人标注的工业集。
3. **情态篡改(modal)是词法引擎的彻底盲区(0%)**:把「可能 / 学界存疑 / 多因素」说成
   「确定 / 已有定论 / 唯一原因」,词法层没有任何语义情态信号可抓,只能靠 LLM 判官(83%)。
   这是本轮扩充暴露出的最大新缺口——**如实计入,没有为了好看去改金标答案**。
   「相邻话题 vs 冲突」词法档 64% 也是同类硬骨头(见已知局限)。
4. **LLM 判官 v0.3 全量复测**(2026-10-28):合成 178 条 **99.44%**、困难 150 条 **99.33%**
   (唯一错分是一条「蜜蜂是唯一授粉者」的情态/唯一性边界用例),两轮合计成本约 **¥0.54**
   (DeepSeek,逐调用记进 `results/cost_ledger.jsonl`),产物 `results/v03_{gold,hard}_llm.json`。
   v0.2 的历史结果(`gold/validation_*_llm_v0.2.json`,49 条旧困难金标)保留作对照。

## 双引擎 × 三检索通道

| 维度 | 选项 | 依赖 | 用途 |
|---|---|---|---|
| 判官 | `heuristic` | 零 API、纯本地规则 | CI 门禁、离线快检、批量回归 |
| 判官 | `llm` | DeepSeek 结构化 JSON 判官 | 语义级判定:改写、蕴含陷阱、跨语言 |
| 检索 | `lexical` | 零依赖(BM25 + 数值 + 概念词表) | 默认通道 |
| 检索 | `hybrid` | 需嵌入后端,BM25+数值+向量融合 | 开放领域、重度改写 |
| 检索 | `dense` | 需嵌入后端,纯向量 + 数值 | 语义召回为主 |

向量通道永远不是必需品:核心包不装向量依赖也能 import、也能跑词法引擎(依赖隔离)。
`--embedder local` 走**本地离线 ONNX 多语模型**(fastembed,不含 torch),首次经
HF 国内镜像下载、之后完全离线并复用磁盘缓存;缺依赖或下载失败时打印中文提示并
**自动退回 `lexical`**,不会崩(与"没配 embedder 时 hybrid 退回 lexical"同一套降级哲学)。
纯离线替身 `--embedder hashing` 可把整条链路接线跑通(但它不跨语言)。

```bash
pip install -e ".[semantic]"          # 装本地向量后端(fastembed)
export HF_ENDPOINT=https://hf-mirror.com   # 国内镜像(可选,首次下载用)
python -m faithsift check answer.md --sources gold/sources \
    --retriever hybrid --embedder local      # 词法+数值+向量融合
```

## 快速开始

```bash
python -m venv .venv && .venv/Scripts/pip install -e ".[dev]"
python -m pytest tests/ -q                       # 89 项离线测试,零 API(向量用例无模型时自动 skip)

python -m faithsift gold                         # 重建两套金标(确定性)
python -m faithsift validate --dataset gold      # 合成金标:回归下限
python -m faithsift validate --dataset hard      # 困难金标:能力缺口

# 核查一份回答
python -m faithsift check demo/sample_answer.md --sources gold/sources \
    --mode heuristic --out results/check
# -> results/check.{md,html,json}
```

`demo/report_multihop.md` 是多跳 + 跨语言召回的示例(3 条论断全对)。

## CLI

```text
faithsift check <answer> --sources <dir>
    --mode heuristic|llm|both         判官
    --retriever lexical|hybrid|dense  检索通道
    --embedder hashing|local|<模型名>  向量后端(local=本地离线 ONNX 多语)
    --top-k N --out results/check
    --fail-under 0.9                  忠实性低于阈值 -> 退出码 3
    --fail-on-contradicted            出现与源冲突 -> 退出码 4
    --json                            机器可读输出

faithsift gold                        重建 gold.jsonl 与 hard_gold.jsonl
faithsift validate --dataset gold|hard --mode heuristic|llm
    --min-accuracy 0.85               准确率低于阈值 -> 退出码 5
```

退出码:`0` 成功 · `2` 用法错误 · `3` 忠实性不达标 · `4` 存在冲突论断
· `5` 准确率不达标 · `6` 缺少密钥。

`--fail-under` / `--fail-on-contradicted` / `--min-accuracy` 就是挂在 CI 上的那道门禁。

## 判定标准(与 LLM 提示词一致)

- **supported**:证据明确包含论断的核心事实(实体与关系一致;非数值事实的同义改写、
  跨语言翻译算支持);需要多段证据才能成立时,把用到的片段编号全部列出。
- **contradicted**:证据就**同一事实**给出不同取值 —— 数字不符、方向相反(升高/降低、
  高于/低于)、实体张冠李戴、单位不同。
- **unsupported**:证据对论断主题保持沉默 —— 既不支持也不冲突。

三条硬规则:

1. **禁止数值逻辑蕴含**。证据说「200 度以上」、论断说「180 度以上」⇒ `contradicted`,
   哪怕逻辑上能推出来。改一个数字就是篡改。
2. **相邻话题不等于冲突**。语料讨论了邻近话题但没给论断所说的这个事实 ⇒ `unsupported`。
3. **声称支持必须给证据**。每条判定带 `evidence_idx`(多跳时为数组)。

## 几个值得一提的设计

**数字跨语言不变。** 数字不随翻译改变,所以「一爆 196 至 205 摄氏度」的中文论断能命中
英文段落。这一条把 LLM 引擎的 demo 从 5/8 提到 8/8。

**中文数词归一化。** 「两千」「四到六」「一成」「10 万」「1.9 million」「2,000」
都会先归一化成阿拉伯数字再比对,否则中文回答里的数词既进不了检索、也进不了数值校验。
`数百` 这类约数不会被误转成 100。

**概念词表补跨语言。** 只靠数字的话,一条不含数字的中文论断永远召回不到英文证据。
`faithsift/lexicon.py` 把中英表面形式映射到共享概念词元
(`茶树`/`tea` → `tea`,`一爆`/`first crack` → `first_crack`),
约 250 个表面形式、120 个概念,覆盖随附的六个领域。

**实体名录防张冠李戴。** 论断点名阿姆斯特丹,而「2005 年起投资 20 亿丹麦克朗」讲的是
哥本哈根 —— 数字全对、主题全对,但结论是错的。判官会把「只讲别的实体」的片段划为旁证,
数字全部落在旁证里就判 `contradicted`(实体错配)。

**多跳真的需要两段。** 判官不满足于「某段沾边」,只有在第二段证据带来足够新增覆盖、
并且合并后达到阈值时才判多跳支持;否则一篇主题相关但不含该事实的段落就会被当成证据。

**成本保险丝。** `results/cost_ledger.jsonl` 逐调用记账,硬顶 ¥12(`FAITHSIFT_CAP` 可调,旧名 `CITECHECK_CAP` 仍作回退),
到顶抛 `BudgetStop`,防止批量跑测试时超支。

## 结构

```text
faithsift/claims.py     中英句级论断拆分
faithsift/evidence.py   源加载 / 分块 / 分词
faithsift/lexicon.py    中英概念词表 + 实体名录
faithsift/numbers.py    中文数词与量级归一化
faithsift/retrieval.py  BM25 + 数值不变性 + 可选向量的混合检索
faithsift/embed.py      可选向量后端(OpenAI 兼容 / 本地离线 ONNX 多语 / 哈希替身 / 磁盘缓存)
faithsift/judge.py      双引擎判官(LLM JSON 判官 + 启发式,含多跳)
faithsift/pipeline.py   端到端流水线 + 金标验证 + CI 门禁
faithsift/report.py     md / html / json 报告渲染
gold/                   两套金标 + 6 篇双语语料 + 验证产物
demo/                   示例回答与渲染好的报告
scripts/gap_report.py   按类别列出错分样本的开发脚本
tests/                  89 项离线测试(向量端到端无 fastembed/模型时 skip)
```

## 密钥与安全

真实密钥**不放进仓库**。加载顺序:

1. 环境变量 `DEEPSEEK_API_KEY`
2. `FAITHSIFT_ENV_FILE` 指向的文件(可以在仓库外;旧名 `CITECHECK_ENV_FILE` 仍作回退)
3. `.env.local`(已 gitignore,本地私有)
4. `.env`(只有占位符)

`tests/test_security.py` 会扫描仓库内文件是否出现真实密钥形态,并校验 `.env` 未被 git 跟踪。
CI 里单独跑这个文件。

> 如果真实密钥曾经出现在文件里,请到控制台**轮换**它,而不是只删文件 —— 已经分发出去的副本不受你控制。

## 与已发表工作的关系(诚实定位)

系统调研(40+ 条目,逐条带核实状态与链接)见 [docs/survey_faithfulness_landscape.md](docs/survey_faithfulness_landscape.md)。要点:

- **不宣称首创的部分**:拆句→取证→判定的管道是 2023 年起的成熟范式(FActScore、SAFE、RAGAS、DeepEval、ALCE 等);三分类中"证据沉默"单列有 FEVER(NotEnoughInfo)与 NLI(neutral)先例;改名前的旧名 CiteCheck 与北大 2025 同名工作(arXiv:2502.10881,中文引用忠实性数据集)撞车——这正是本项目改名 FaithSift 的原因,并与该工作无任何代码/数据继承关系。
- **站得住的差异点**(调研核实范围内):①**纯词法零 API 判官**——现有低成本方案均为神经小模型,词法+数值一致性的可审计判官未找到对标物;②**seed 固定的确定性金标用于审计判官本身**(arXiv:2607.20527 恰好证明神经判官之间严重不一致,支持该设计动机);③**中文论断→英文源的跨语言词法检索管道**(数字不变性+概念词表,未找到相同设计);④多跳证据链的工程化判官(学术脉络引 FEVEROUS)。
- 引用纪律: survey 中 4 条【未核实:仅训练记忆】条目(如 FacTool venue)在正式引用前必须人工复核。

## 已知局限(v0.3 现状)

**v0.2 的这几条已修复(移出局限):**

- ~~同语言重度改写~~ → 接入向量通道(`--embedder local`)后,困难金标「中文同义改写」类
  从词法档 94% 升到 **100%**;「低估自己有多困」这类字面不重合的改写由语义余弦兜底。
- ~~相邻话题被判成冲突~~ → 「荷兰头盔立法始于 2010 年」(源文实为澳大利亚 1990 年代)
  现判 `unsupported`:缺失数字必须在**同一事实槽 + 同一实体**里找到竞争取值才算冲突。
- ~~无数字的语义方向(Agtron)~~ → 新增烘焙「深/浅」定性刻度反义词识别,
  「Agtron 数值越大越深」对源文「higher numbers indicate lighter roasts」现判 `contradicted`。

**仍未解决(如实列出):**

- **情态篡改是词法盲区(0%)**:把「可能 / 学界存疑 / 多因素」拔高成「确定 / 已有定论 /
  唯一原因」,词法与向量都没有可靠的语义情态信号,只能交给 LLM 判官(83%)。
- **隐蔽的相邻话题仍会被"多跳"凑成支持(词法 64%)**:当两段各沾一半主题词的片段
  联合起来把论断的概念覆盖满(如「新加坡屋顶绿化覆盖率全球第一」),词法档会误判
  `supported`。它与"忠实的多跳"在词法层结构相同,区分需要语义蕴含,LLM 档 100%。
- **跨语言稀疏概念**:中文论断对英文源、且可映射的概念词太少时(如「一个蜂群在温带
  地区有 20 万至 80 万只个体」),词法覆盖过低会漏召;向量通道能补一部分,但判官的
  覆盖度仍以词法/概念为主。
- **概念词表只覆盖随附六个领域**:开放语料请用 `--retriever hybrid --embedder local`
  或 `--mode llm`;词表外的词平滑退化为原始词法词元。
- **方向词表有限**:方向词与其修饰的数字/概念在源文里隔得较远,或用「低 / 高」这类
  未登记的裸方向词时,反转会漏判(150 条 direction 词法 86%)。
- **金标仍是一人标注**:150 条困难样本足以暴露缺口、给出按类别的相对强弱,
  但仍不足以给出置信区间,也没有第二标注者做一致性(Kappa)检验。

## License

MIT。
