Recursos
1Instal·la
npx skillscat add xngg1021/academic-research-kernel/academic-source-verification Instal·la des del registre de SkillsCat.
学术来源核查 Skill
验证论文与文献的真实性,交叉核对元数据与各渠道被引数,检查撤稿与更正记录,并下载可获取的开放获取全文以核验内容。本技能与 arxiv 技能互补:arxiv 负责检索发现论文,本技能负责核对文献并获取全文。
核心原则:引用科研结论前,须核对文献身份、已知撤稿信号、被引数口径以及结论原文,避免凭记忆或二手转述下结论。
When to Use
- 用户要求"下载论文交叉核查""验证这个文献是不是真的""核对被引数"
- 建模/论证里引用了文献,需要坐实它的标题/作者/年份/期刊/被引数
- 需要拿到论文全文(开放获取)并确认下载的是正确的那篇
- 核查一篇文献是否已被撤稿或发过更正(引用的底线检查)
- 查经典专著/公版书的原文(Ebbinghaus 遗忘曲线、Miller 1956 等)
Don't use for: 纯检索发现论文(用 arxiv skill)、OCR/解析已下载的 PDF 正文(用 pdf)。
Prerequisites
网络请求使用 Python stdlib urllib;PDF 提取需单独检查 pymupdf。OpenAlex 可匿名小规模查询,建议免费 key;Unpaywall 要求用户真实联系邮箱。各服务均有限额,免费访问不保证无需配置:
| 服务 | 用途 | 限制 |
|---|---|---|
| OpenAlex | 元数据、主题、OA 与撤稿标记 | 匿名 $0.10/日、免费 key $1/日预算;超过预算或 100 req/s 返回 429 |
| Crossref | Crossref 收录口径被引数 + 撤稿/更正记录 | 非 Crossref 注册 DOI 可返回 404(例如 DataCite 注册的 arXiv DOI) |
| Semantic Scholar | 另一数据库的被引数口径 | 匿名共享限额易 429;key 初始额度 1 req/s,按分配额度执行 |
| Unpaywall | OA 全文定位(is_oa + best_oa_location) | 需带 email 参数 |
核查四道关
第一关:三库交叉核对(真实性 + 被引数)
核对 DOI、标题、作者与版本,记录三个数据库的差异。数据库之间可能共享出版商或 Crossref 数据,不能将数据一致性视作三个独立证明;被引数不可用于判定文献身份。
| 库 | 端点 | 关键字段 |
|---|---|---|
| OpenAlex | https://api.openalex.org/works/https://doi.org/{DOI} |
cited_by_count、publication_year、authorships、primary_location.source.display_name、open_access.oa_status + oa_url |
| Crossref | https://api.crossref.org/works/{DOI} |
message.is-referenced-by-count |
| Semantic Scholar | https://api.semanticscholar.org/graph/v1/paper/DOI:{DOI}?fields=title,authors,year,venue,citationCount,influentialCitationCount,openAccessPdf(或 arXiv:{id}) |
citationCount |
请求要带 User-Agent(带 mailto 更礼貌),否则部分端点会拒。三库被引数必然不同(收录范围不同),分数量级差异也可来自版本合并/收录范围,须解释而不据此否定论文身份。
用 execute_code 批量查并打印对照表;按各服务实际配额控制调用速率;固定 sleep 不能保证避免共享限额的 429。标识符归一与 OpenAlex/Crossref 两库身份核对已抽为脚本:
python "${HERMES_SKILL_DIR}/scripts/normalize_identifier.py" <标识符>
python "${HERMES_SKILL_DIR}/scripts/verify_work.py" --doi <DOI> # 已有记录时 --openalex-json/--crossref-json 离线核对第二关:撤稿与更正核查(Crossref update 记录)
Crossref 于 2025-01 将 Retraction Watch 信号纳入 REST API(2023-09 是合作/收购时间)。记录原始 DOI、通知 DOI、类型、source 和查询时间。update-to 通常位于通知记录,指向被更新对象;不能把通知本身判成被撤稿论文。message.type 是文献类型,不是撤稿类型。
- 查原文 DOI 元数据;标题
RETRACTED:/WITHDRAWN:前缀只作需核实信号,普通标题用词不是结论。 - 反向查
https://api.crossref.org/works?filter=updates:<DOI>&rows=100,逐项核对update-to[].DOI是否是目标 DOI。超过一页则用 Crossref 自己的 cursor 规则继续,记录截断。 - 区分
retraction、withdrawal、correction与expression-of-concern,记录 publisher 或 retraction-watch 来源;不同信号不合并归为撤稿。 - OpenAlex
is_retracted=true表示数据库记录的撤稿信号;false 不排除遗漏。arXiv 另查 abs 页及版本历史的 withdrawal 状态;有疑问核对出版社通知或 Retraction Watch 原记录。
更新信号的离线提取与反向查询已抽为 scripts/check_updates.py(离线输入须来自已成功查询的 Crossref 记录;retraction、withdrawal、correction 与 expression-of-concern 分类计数,不合并):
python "${HERMES_SKILL_DIR}/scripts/check_updates.py" --doi <原始DOI> # 在线 filter=updates 反向查询
python "${HERMES_SKILL_DIR}/scripts/check_updates.py" --doi <原始DOI> --records records.json # 离线信号提取查无信号时仅报告“在已检查的数据源中未发现撤稿或撤回记录”,并注明数据来源、查询时间以及失败或未查项目。网络请求失败不等于未发现记录。命中撤稿信号时须说明证据层次;撤稿论文可作为撤稿事件的研究对象,不可在无说明的情况下当作可靠结论依据。
官方依据:Crossref 集成说明、更新查询过滤器。
第三关:OA 全文定位(Unpaywall 首选)
Unpaywall 是 OA 定位渠道之一,覆盖率和更新延迟与其他来源不同,未作普遍优劣保证。查询已抽为 scripts/locate_oa.py;UNPAYWALL_EMAIL 未配置时脚本明确 SKIP 不推断,is_oa=false 时 best_oa_location 为 null,先判再取:
python "${HERMES_SKILL_DIR}/scripts/locate_oa.py" --doi <DOI> # 已保存记录时 --record 离线解析顺序:Unpaywall best_oa_location → OpenAlex oa_url → Semantic Scholar openAccessPdf.url → archive.org(公版书/专著)。付费墙 403 不要死磕。
第四关:下载与内容核对
- arXiv(绿 OA):
https://arxiv.org/pdf/{id};可因限流、撤回或网络限制失败。 - 公版书/专著(如 Ebbinghaus 1885):走
archive.org—— 先用https://archive.org/advancedsearch.php?q=<标题>&fl[]=identifier,title,year,mediatype&rows=3&output=json搜 identifier,再查https://archive.org/metadata/{identifier}的 files 选择实际 PDF 文件名;不假定文件名与 identifier 相同,并核对访问状态。
首页文字提取与身份核对已抽为 scripts/verify_pdf_identity.py(需 pymupdf,缺失时明确报错;下载成功 ≠ 内容正确):
python "${HERMES_SKILL_DIR}/scripts/verify_pdf_identity.py" --pdf <路径> --title <标题> --doi <DOI>下载统一存到稳定目录(例如 Path("~/papers").expanduser() 并 mkdir(parents=True, exist_ok=True),路径保证 UTF-8 可用),回报绝对路径 + 字节数 + 页数。
Helper Scripts
scripts/ 内置确定性逻辑脚本,均可 --help 查看参数;网络访问集中在明确标记的 live 函数,无 key 或离线时明确 SKIP 不推断:
| 脚本 | 功能 |
|---|---|
scripts/normalize_identifier.py |
DOI/arXiv/PMID/OpenAlex ID 归一(纯离线) |
scripts/verify_work.py |
OpenAlex/Crossref 身份核对与字段级对照(可离线) |
scripts/check_updates.py |
Crossref update-to 撤稿/更正反向查询与信号分类 |
scripts/locate_oa.py |
Unpaywall OA 定位(is_oa + best_oa_location) |
scripts/verify_pdf_identity.py |
PDF 首页文字/元数据与声明标题、DOI 核对 |
Procedure
- 确定论文的 DOI/arXiv ID/标题。期刊论文用 DOI,arXiv 预印本用 arXiv ID;标识符用
scripts/normalize_identifier.py归一。 - 用
scripts/verify_work.py或execute_code批量查 OpenAlex + Crossref(+ Semantic Scholar),打印三库标题/作者/年份/期刊/被引数对照表。 - 按第二关核查双向更新关系(
scripts/check_updates.py)、OpenAlex 标记及出版社/arXiv 状态,分类记录,避免把通知记录判成被撤稿对象。 - 判断真实性:DOI、元数据与版本匹配 → 报告身份已核对。不一致 → 记下差异,如实报告。
- 能下 OA 就下(
scripts/locate_oa.py→ arXiv → archive.org),下完用scripts/verify_pdf_identity.py提首页文字核对内容。 - 付费墙 403 的,报告已完成的实际检查与“全文访问受限”;403 也可能是反爬/访问策略,不据此断定付费墙或撤稿状态,不反复尝试。
Pitfalls
- 被引数有来源口径。预印本与正式版本的合并、重复记录及数据库覆盖都会影响计数,标明来源和查询日;不把某库数值称为真实影响或严格下限。
- Crossref 与 OpenAlex 查 arXiv 的 data-DOI 可能 404/滞后。
10.48550/arXiv.<id>是 DataCite 注册的,Crossref 常返回 404,OpenAlex 偶有收录延迟。遇此情况直接调用 arXiv 官方 API(http://export.arxiv.org/api/query?id_list=<id>或search_query=ti:<标题>)核实标题、摘要、版本历史与撤回(withdrawn)状态。 - 经典奠基文献标题易与后继综述撞车。以简短通用标题(如 "Working Memory")检索时,极易命中作者多年后的同名回顾篇。核查奠基作须限定出版年份(Crossref
filter=from-pub-date:YYYY-01-01,until-pub-date:YYYY-12-31)并核对作者序列表。 - Semantic Scholar 无 key 极易 429。请求间隔 ≥1.1s,失败退避重试;持续 429 就退回 OpenAlex + Crossref 两个来源,并如实说明。
- update-to 非空 ≠ 撤稿。还须核对更新方向、目标 DOI 与 update.type;更正和表达关注分别记录,不能见 update-to 就报撤稿。
- 出版商 PDF 付费墙,脚本下载 403。OpenAlex 的
oa_status: "bronze"只表示"在出版商网站免费可读",不代表能脚本下载(Wiley 的 pdfdirect 就是 403)。403 后报告访问受限,不循环重试。 - 下载成功 ≠ 内容正确。必须提取 PDF 首页文字核对标题/作者,防止下到同名错误文件。
- Unpaywall 的 email 参数有硬校验。使用自己的真实联系邮箱;服务可用 422 拒绝缺失/示例邮箱,不推断邮箱域名的 DNS 行为。
is_oa=false时best_oa_location为 null,先判再取。
输出语言:默认跟随用户输入语言(用户用什么语言问就用什么语言答);论文标题与引文保留原文,译名首次出现时附原文。
Verification
# external-test: true
import json, os, time
from urllib.request import Request, urlopen
from urllib.parse import urlsplit, urlencode, quote
from urllib.error import HTTPError
def get(url):
headers = {'User-Agent': 'academic-research-kernel/1.2'}
if urlsplit(url).netloc == 'api.openalex.org' and os.environ.get('OPENALEX_API_KEY'):
headers['Authorization'] = 'Bearer ' + os.environ['OPENALEX_API_KEY']
for attempt in range(3):
try:
with urlopen(Request(url, headers=headers), timeout=20) as response:
return json.load(response)
except HTTPError as error:
if error.code not in {429, 500, 502, 503, 504} or attempt == 2:
raise
# 有界退避;长预算封锁交给调用者报告,不循环耗尽免费服务。
retry = error.headers.get('Retry-After', '')
delay = float(retry) if retry.isdigit() else 2**attempt
if delay > 10:
raise
time.sleep(delay)
doi = '10.1038/nature14539'
oa = get('https://api.openalex.org/works/https://doi.org/' + quote(doi, safe=''))
assert oa.get('doi', '').lower() == 'https://doi.org/' + doi
assert 'deep learning' in oa['title'].lower()
assert isinstance(oa.get('is_retracted'), bool)
cr = get('https://api.crossref.org/works/' + quote(doi, safe=''))['message']
assert cr['DOI'].lower() == doi and 'deep learning' in ' '.join(cr['title']).lower()
original = '10.1177/1758835920922055'
updates = get('https://api.crossref.org/works?' + urlencode({'filter': 'updates:' + original, 'rows': 5}))['message']
assert any(u.get('DOI', '').lower() == original and u.get('type') == 'retraction'
for item in updates['items'] for u in item.get('update-to', []))
print('OpenAlex/Crossref identity + reverse retraction lookup passed')
email = os.environ.get('UNPAYWALL_EMAIL')
if email:
up = get('https://api.unpaywall.org/v2/' + quote(doi, safe='') + '?' + urlencode({'email': email}))
assert isinstance(up.get('is_oa'), bool) and up['doi'].lower() == doi
print('Unpaywall passed')
else:
print('SKIP Unpaywall: UNPAYWALL_EMAIL not configured; no result inferred')无 key 时 OpenAlex 自检会实际走匿名路径;带 key 路径仅在配置 OPENALEX_API_KEY 后运行。未配置服务应明确 SKIP,不当作通过。Semantic Scholar 按任务与可用额度另行核对。
Evidence Receipt 输出约定
核查结束时,将结论写成机器可读的 evidence receipt JSON,格式见 schemas/evidence-receipt.schema.json,示例见 examples/evidence-receipt.example.json。要点:
- 每个来源单独记录查询时间、状态(ok/failed/skipped)与覆盖范围;失败的查询写进
failures,不得当作结果推断。 - 每条 claim 标明证据类型、来源、定位与支持状态(supported/contradicted/unverifiable/out_of_scope)。
- 互相矛盾的来源结论写入
conflicts,不在 receipt 里私自裁决。 - 下游技能(literature-analysis、academic-writing、math-computation)优先消费 receipt,不重新用自然语言转述上一阶段结论。
用户未要求落盘时,receipt 仅在对话中呈现;要求落盘时写入用户指定路径。