Skip to content

About

带执行过程可视化的中文谣言核查:拆原子事实、多源并行检索、逐条判真假,全程流式可观测

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

较真 · 让每一条传闻都能被查证

一个带执行过程可视化的谣言核查产品:接收一条消息,把它拆成可判定的原子事实,联网找证据,逐条判定真假,并把「结论是怎么来的」完整展示出来。

较真:让每一条传闻都能被查证

Next.js 前端 · FastAPI 后端 · 多 Agent 并行检索 · 全程流式可观测


目录


产品能力速览

能力 关键特征
🎯 拆 claim 逐条判 一条消息拆成原子事实,每条独立判 属实 / 不实 / 证据不足 / 各方矛盾,混合情况不粉饰
🌐 5 路真检索 百度 + 小红书 + 今日头条 + 搜狗微信 + 联合辟谣平台,多源调度且 SERP 日期真实抓取而非伪造
🔎 语义证据重排 证据按「意思相关度」重排(embedding 余弦),治字面撞词;可选、失败自动回退字面打分
🧠 Agent 多轮迭代 深度档跑「搜 → 判 → 再搜」循环,配合 LLM Critic 单调下调(永不加强判定)
📊 真伪概率 除 verdict 外每条还有 0–100 概率,明确标注是「基于证据」还是「基于常识先验」
👁️ 全程可观测 流式事件直播每一步,用户能看到「哪一步、找到了什么、为什么这么判」
📜 可复核证据链 每条判定都能展开 <details>判定依据:来源分级 · 相关性说明 · 原始 URL

核查全流程 · 一张图看懂

核查全流程:从一句话到一份可复核的报告

六步流水线,每一步都以流式事件推给前端,用户实时看到「后端现在在干什么」:

  1. 输入 — 支持文本、URL、问句三种输入类型
  2. 标准化 — 识别输入类型,抽取标题/摘要/关键词/来源
  3. 多源检索 — 固定流水线顺序补充 5 路证据源;深度多 Agent 模式并行调度,随后去重并规范化时间戳
  4. 拆 Claim — 把核心事实和存疑细节各自独立,一条消息可能拆成 3–5 条
  5. 逐条判定 — 每条 claim 独立走 verdict + 真伪概率
  6. 报告 — 组装成带来源标注、时间线、可信度打分的结构化 Report

多 Agent 并行架构

深度档 (mode=deep) 开启多 Agent 编排后,检索层从串行 5 源 升级为并行 DAG:

多 Agent 并行 DAG · 一次分析调度 5 路检索

为什么这么设计:

  • 5 个源之间没有数据依赖,串行是浪费;ThreadPool 默认最多 4 个 worker 并发拉取
  • NORMALIZE / RETRIEVAL_MERGE 是关键路径节点,任何源失败降级为 SKIPPED 空 bundle,MERGE 继续跑
  • CRITIC 可选多视角并行;默认 3 个独立视角,至少 2 个视角同时质疑时才降级
  • Supervisor.execute_batch 用 copy_context().run() 绑 ContextVars,否则 ThreadPool 的 worker 拿不到 progress 回调,观测会静默失效

在代码里:入口 backend/app/services/analyze_pipeline.py::_run_multi_agent · Supervisor backend/app/agent/multi/supervisor.py · 6 个 sub-agent 各一个文件在 backend/app/agent/multi/

触发条件:MULTI_AGENT_ENABLED=true + AGENT_ORCHESTRATOR_ENABLED=true + 请求带 mode=deep。三个都开才走这条路,任一关闭回退到固定 pipeline,行为不变。


两档核查 · 秒级 vs 分钟级

同一后端进程同时提供两档,不靠环境变量切换。前端主按钮走 fast,出结果后再给「深度核查」二次入口。

两档核查:秒级实时 vs 分钟级深度

档位 触发 时延 判定路径 适用
fast ⚡ 默认 / 请求带 mode=fast ~0.2–0.3s 5 路检索 → 规则引擎 verdict 秒级实时查证
deep 🔬 请求带 mode=deep 分钟级 Agent 循环 → LLM synthesis → Critic 校验 → 结构化补全 需要更强证据时的深度核查

mode 只切换分析深度,检索 provider 始终由 RETRIEVAL_PROVIDER 决定,两档共用同一套真实检索层。


四种命运 · 每条事实独立判定

一条传闻往往「核心属实、细节存疑」。把它拆成原子 claim 独立判定,能诚实反映混合情况——当既有属实成分又有被反驳成分时,标「各方矛盾」而不是干净的「属实」。

一条 Claim 的四种命运 + 真伪概率

真伪概率与 verdict 解耦(核心设计原则):

  • 每条 claim 除 verdict / confidence 外,带 truth_probability(0–100)和 probability_basis(evidence / prior)
  • 无证据时决定性 verdict 仍降级为 insufficient(grounded 安全兜底不变),但概率是另一维度
  • 一条 insufficient 的 claim 仍可带 truth_probability=15, basis=prior——basis 诚实区分「有检索证据」还是「仅凭常识先验」

语义证据重排 · 治「字面撞词」

检索回来的证据在喂给判定前先重排一次,让最相关的排在最前(判定模型自上而下读,靠前的更受重视)。排序以语义相似度为主:把「传闻」和每条证据都过一遍 embedding,算向量余弦——比的是意思,不是共享了几个字。

语义证据重排:按意思相关度排序,治字面撞词

为什么要它:纯字面打分会被「共享通用词但主题无关」的结果骗到。实测查询「Tesla 特斯拉 FSD 中国落地」,字面打分把只共享「发布/中国/市场」的无关新闻排在最前、把真正相关的「马斯克旗下电动车企自动驾驶获批」(几乎零字面重叠)埋到最后;语义重排把正确证据提到第一。别名、改写、中英混检这类场景收益最明显。

  • 语义主导 + 权威微调:余弦相似度为主,叠加一个上限约 +0.1 的 authority_score 微调——权威分只在语义近似平票时才决定顺序,不会把高权威但离题的来源顶上来
  • 自愈式可选:开关 EVIDENCE_RERANK_ENABLED,配 embedding 模型 + key 即生效;未配置、超时、或任何调用失败都静默回退到原来的字面打分,输出永不劣于基线
  • 走网关、不出域:embedding 复用现有 LLM 网关(Qwen3-Embedding,独立 key),数据不出域

在代码里:语义打分 backend/app/services/embedding_ranker.py · 回退封装 backend/app/services/evidence_ranker.py::rank_results


来源可信度打分

语义重排之外,每个 SERP hit 除 source_tier(S/A/B/C 四档硬分类)外还带一个 authority_score(0–100 连续分),既作上面的微调项,也用于同 tier 内区分。由 4 组信号叠加:

authority_score 4 组信号叠加:tier 锚点 + 白名单 + HTTPS/独立域 + 可疑扣分

信号 分值 何时触发
tier 基础分 S=40 · A=32 · B=20 · C=10 保留原四级分类作为主锚
白名单加成 +15 命中 TOP_TIER_DOMAINS(新华社/人民网/央视/路透/AP…)或 piyao.org.cn
HTTPS + 独立域 +4 / +4 HTTPS 得 +4;域名非 medium/wordpress/wixsite/blogspot 等托管平台再 +4
可疑信号扣分 -8 / -10 / -10 .top/.xyz/.click/.buzz 等 TLD → -8;URL 含 /promotion /advertorial → -10;标题含「震惊」「全网疯传」「删前速看」等 clickbait 词 → -10

只影响排序,不参与 verdict 决策——verdict 引擎依旧靠 tier 分级 + 数量阈值,稳。evidence_ranker 用 authority_score * 0.005 替代原来的 TIER_WEIGHTS[tier] * 0.05,量级一致但同 tier 内可区分。

在代码里:打分函数 backend/app/services/retrieval_provider.py::_authority_score · 消费方 backend/app/services/evidence_ranker.py::score_result


抓取正文 · 三层降级链路

搜索只给几十字的摘要;深度档会挑最高价值的证据页抓全文,喂给判定当额外依据。抓取走三层降级链路,任一层失败都安全下沉到下一层,绝不因为一层挂了就中断整条核查:

抓取正文三层降级:静态 httpx → 真浏览器 → 沿用摘要

层 手段 何时用
① 静态抓取 httpx 抓静态 HTML 最快(<1s),成功即用
② 真浏览器渲染 Playwright 无头 Chromium 静态抓回空壳时触发(JS 渲染页如 163/微博),渲染出完整正文再抽取,约数秒
③ 沿用摘要 回退检索摘要 前两层都拿不到正文时的兜底
  • 全异常隔离:整条 render+extract 路径(含 playwright 未装、浏览器崩、抽取报错)都被兜住,失败只降级、绝不向上抛断 fetch_url
  • 路径可见:走了静态还是浏览器、为什么降级,都写进 fetch_url 的 trace(path=static/browser、rendered=<原因>)
  • 默认关、零影响:Playwright 是可选依赖,浏览器路径由 RENDERED_FETCH_ENABLED 控制(默认关);不装、不开时行为与原来完全一致,只走静态→摘要两层

在代码里:降级编排 backend/app/agent_tools/tools.py::fetch_url / _try_rendered_fallback · 真浏览器 backend/app/services/rendered_page_fetcher.py::render_page_with_reason


快速开始

环境要求

  • Python >= 3.12(CI 用 3.12)
  • Node.js >= 18.18.0(推荐并由 frontend/.nvmrc 锁定为 20.9.0)

1. 配置环境变量

cp backend/.env.example backend/.env

默认基线(零 key、可复现):

ANALYSIS_PROVIDER=off
RETRIEVAL_PROVIDER=mock
RETRIEVAL_FALLBACK_TO_MOCK=true

启用真实联网检索(纯 httpx 抓百度/Bing,无需额外依赖):

RETRIEVAL_PROVIDER=playwright
RETRIEVAL_FALLBACK_TO_MOCK=true

启用多 Agent 并行 DAG + LLM 深度档:

RETRIEVAL_PROVIDER=playwright
ANALYSIS_PROVIDER=kimi
AGENT_ORCHESTRATOR_ENABLED=true
MULTI_AGENT_ENABLED=true
MULTI_AGENT_RETRIEVAL_MODE=parallel
LLM_API_KEY=你的真实 key
LLM_BASE_URL=你的 OpenAI 兼容网关端点
LLM_MODEL=你的模型名

ANALYSIS_PROVIDER=kimi 只是历史遗留的开关字面量,不代表具体供应商;通常走标准 OpenAI 兼容 chat/completions。Magpie JoyCode 的 DeepSeek 模型需要额外签名及登录身份:在本地 backend/.env 将模型同时列入 LLM_MODELS 和 JOYCODE_MODELS,并配置 JOYCODE_BASE_URL、JOYCODE_SIGN_KEY、JOYCODE_AUTH_FILE(指向本机 Magpie 的私有授权文件)。这条链路通过相同的模型选择器、流式分析和非流式判定;现有默认模型不变。网关地址和凭据全部留在 git 忽略的本地配置,不进入版本库。

可选增强(默认关,配了才生效,失败自动回退):

# 语义证据重排:按意思相关度重排证据,治字面撞词
EVIDENCE_RERANK_ENABLED=true
EVIDENCE_EMBED_MODEL=你的 embedding 模型名
EVIDENCE_EMBED_API_KEY=embedding 网关 key(与 chat 独立)

# 真浏览器抓正文:静态抓空时用无头 Chromium 渲染 JS 页面
# 需先 pip install playwright && playwright install chromium
RENDERED_FETCH_ENABLED=true

2. 启动后端

python -m pip install -r backend/requirements-dev.txt
uvicorn backend.app.main:app --reload

默认地址:http://127.0.0.1:8000(健康检查 GET /api/v1/health)

3. 启动前端

cd frontend && npm install && npm run dev

默认地址:http://127.0.0.1:3000

4. 跑测试

python -m pytest backend/tests/ -q                # 后端回归(当前 700+ tests)
cd frontend && npm run typecheck && npm test     # 前端类型检查 + 单测

CI 每次 push/PR 也会跑同一套(见下节)。


持续集成

CI 流水线:backend、frontend 与 PR dependency review

.github/workflows/ci.yml 在 push 到 main 或 PR 目标为 main 时触发:

  • backend:安装依赖 → ruff check backend/ → 契约检查 → 全量 pytest backend/tests/ -q → deterministic replay → 上传 replay artifact
  • frontend:npm ci → npm run typecheck → npm test → npm run build
  • dependency-review:仅 PR 运行,阻止引入 high severity 依赖;仓库需先启用 GitHub Dependency Graph

约束:ruff 无 --exit-zero,任何 lint 违规立刻红;pytest 无失败豁免;CI 不跳过 test_retrieval.py。


接口与运行路径

主要接口

接口 用途
GET /api/v1/health 健康检查
GET /api/v1/models 可选分析模型白名单 + 默认(只回模型名,不含网关地址/密钥)
GET /api/v1/model-health 进程内 LLM 模型健康度快照(失败/成功/驱逐计数,用于运维排查)
GET /api/v1/search-sources 可用检索源列表(用于前端 toggle)
GET /api/v1/source-capabilities 检索 Provider Doctor 快照(配置、运行时依赖、能力与安全故障原因;不发起网络探测)
GET /api/v1/agent-trace/{run_id} 只读导出 supervisor 的 span trace(AGENT_TRACE_ENABLED=true 时可用)
POST /api/v1/analyze 同步分析
POST /api/v1/analyze/stream 流式分析(NDJSON 事件流)

本地可用 Provider Doctor 快速检查当前主检索源、可选来源和本地依赖;它不会发起网络请求:

python backend/scripts/source_doctor.py
python backend/scripts/source_doctor.py --json
python backend/scripts/source_doctor.py --strict

默认模式只在没有可用主检索源时返回非零退出码;--strict 会将任何“已配置但不可用”的来源视为失败,适合部署门禁。

回放评测与 Phoenix

逐次 LLM 调用现在支持任务关联、上下文变化摘要、实际 Token 用量和失败记录;在 Agent Span 树查看详情。启用与字段解释见模型调用可观测性。

回放集位于 evals/live_replay/,共 56 条:seed/(8 条合成行为回归)、hard/(8 条合成对抗回归)、cfever_curated/(24 条来自固定版本 CFEVER dev、8 个主题的简体中文样本)、covid19_health_rumor_curated/(8 条有逐条来源与人工标注记录的历史健康谣言),以及 context_challenge/(8 条补充支持类、非空证据不足和同口径冲突的合成样本)。默认离线,评测的是给定 claim 和检索材料后的判定,不包含真实检索、claim 拆解或完整 Agent 执行:

python backend/scripts/replay_eval.py
python backend/scripts/replay_eval.py --dir evals/live_replay/hard
python backend/scripts/replay_eval.py --dir evals/live_replay/cfever_curated
python backend/scripts/replay_eval.py --dir evals/live_replay/covid19_health_rumor_curated
python backend/scripts/audit_datasets.py
python backend/scripts/replay_eval.py --dir evals/live_replay/cfever_curated --evaluation-split development
python backend/scripts/replay_eval.py --dir evals/live_replay/cfever_curated --evaluation-split holdout
python backend/scripts/replay_eval.py --dir evals/live_replay/context_challenge --evaluation-split development
python backend/scripts/replay_eval.py --json --run-name rule-v1 > replay-rule-v1.json
python backend/scripts/replay_eval.py --json --run-name rule-v2 \
  --compare-to replay-rule-v1.json > replay-rule-v2.json
python backend/scripts/replay_eval.py --run-name rule-v3 \
  --output artifacts/replay-rule-v3.json

仓库维护 24 条可人工审查的 CFEVER 样本,开发与留出验收分区各 12 条,主题互不重叠;metadata.split=dev 仍表示上游划分,项目的 evaluation_split 不冒充官方 test 集。需要扩充时,可使用 CFEVER 转换器按固定 seed、标签和主题轮转抽样。转换前检查 claim 摘要,严格解析全部证据指针,保留等价证据组;缺页、缺句、重复 ID 或坏指针直接失败。未经验证的本地导入明确隔离,不能直接进入金标评分。

当前评分协议为 url-evidence-groups-v2:标准证据列表是一组必须全覆盖的 URL;evidence_sets 可表达多组替代证明,满足其中任一完整组即可。只引用冲突的一侧或多步证明的一部分不会过关。历史字段 fever_score 保留兼容,含义为“标签正确且覆盖完整 URL 组”,不是官方逐句 FEVER。不同评分协议、分区或语料摘要的报告禁止直接比较。

seed/ 的当前回归基线为 100%(8/8),hard/ 为 62.5%(5/8);CFEVER 开发分区为 33.3%(4/12),健康谣言集为 12.5%(1/8),语境挑战开发分区为 100%(4/4)、留出分区为 50%(2/4)。这些小样本只诊断规则判定的边界,低分不能作为放宽证据要求或修改正确标签的理由,也不能代表联网核查的端到端准确率。CI 对开发分区和既有回归集设置防退化门槛;留出分区通过显式参数另行验收。

健康谣言集保留历史语境,不能当作当前医疗建议。上游 wish/dread 是希望/恐惧类别,真假判断是本项目独立审阅的标注;80401 已补全原文并限定细菌消毒用途,同时记录页面更新时间与当前抓取时间。其规则基线仍为 12.5%,而全猜反驳可得 87.5%,因此必须结合标签分布解释。公开集的置信度为派生值,不参与置信度评分。

默认走离线规则引擎(确定、可复现)。加 --engine llm 可让同一语料改走线上 LLM 判定(llm_judge_claims,需已配置网关 key),用于对比"规则 vs LLM 判定"在同一批 case 上的准确率差异——该模式非确定、要联网,故 CI 仍用规则默认:

python backend/scripts/replay_eval.py --dir evals/live_replay/hard --engine rule \
  --output /tmp/hard_rule.json
python backend/scripts/replay_eval.py --dir evals/live_replay/hard --engine llm \
  --compare-to /tmp/hard_rule.json

报告给出标签、完整 URL 组、联合分数、引用精度及来源/日期诊断,并显示观测数;不适用的置信度、独立来源或新鲜度指标显示 N/A。来源独立性优先按 origin_id,记者转述同一机构不会变成两个独立信源。坏语料直接报错;隔离样本的 ID、原因和排除数量明确输出,全部隔离时返回非零。细节见 数据质量与评测协议。

每份 JSON 报告还包含可复现实验清单:Git SHA/工作区状态、Python 与平台版本、语料目录与 SHA-256、case ID、规则实现 SHA-256,以及脱敏后的确定性配置。CI 会保存该报告为 replay-eval-<commit> artifact,便于比较规则变化而不是只看单次总分。

Phoenix 仅作为可选观测后端,不替代本地评测。启用方式:

pip install -r backend/requirements-observability.txt
export AGENT_TRACE_ENABLED=true
export PHOENIX_ENABLED=true
export PHOENIX_OTLP_ENDPOINT=http://localhost:6006/v1/traces

启用后,agent trace 仍会写入本地 JSON,同时通过 OpenTelemetry OTLP/HTTP 批量导出到 Phoenix;依赖缺失或 Phoenix 不可用不会中断核查请求。可通过 PHOENIX_EXPORT_TIMEOUT_SECONDS 限制导出等待时间,默认 2 秒。

导出正确性已验证:backend/tests/test_trace.py 会在装了 observability extra 时,用 OTel 的 InMemorySpanExporter 实跑一遍导出,断言真实 span 的树形结构(root=CHAIN、子 span 父子嵌套)、openinference.span.kind、session.id、以及 success→OK / failure→ERROR 状态映射——即真实 Phoenix 会收到的同一批 span。未在真实 Phoenix 实例上端到端验证(无人值守环境无可达实例):span 构建与 OTLP 导出成功刷新这两段已用内存 exporter 证实,剩下的只是把 endpoint 指向一个真实 Phoenix 看 UI 里出现 trace。注意:若在并行 fan-out 里触发导出,worker 必须用 copy_context().run 包装,否则 progress/trace 的 ContextVar 不传播(见架构文档「流式观测怎么做」)。

最小联调:

curl -X POST http://127.0.0.1:8000/api/v1/analyze \
  -H "Content-Type: application/json" \
  -d '{"raw_input": "网传某地出台新规,要求周末全面停工整顿。", "input_type": "text"}'

检索 Provider 对照

provider 说明 适用
mock 稳定回归样例 默认,用于开发/联调/演示
playwright httpx 抓百度(主)+ Bing(兜底),日期从 prefix-time span 真实抽取 推荐真实联网路径,中文覆盖好,无浏览器依赖
gdelt GDELT 公开新闻 API 英文事件,中文覆盖弱
kimi LLM 内建 $web_search 仅对支持该工具的供应商有效
off 关闭检索 只走 LLM 内部知识

多 Agent 相关开关

环境变量 默认 说明
MULTI_AGENT_ENABLED false 启用 Supervisor 多 Agent 层
MULTI_AGENT_RETRIEVAL_MODE parallel parallel(推荐)/ sequential(回退)
MULTI_AGENT_MAX_PARALLEL 4 ThreadPool 并发上限
MULTI_AGENT_CRITIC_PERSPECTIVES 3 Critic 多视角并行数;3 个视角时至少 2 票才降级
MULTI_AGENT_LLM_ROUTING_ENABLED false 用 LLM 决定 loop_back / finalize(否则走规则阈值)
TOUTIAO_SEARCH_ENABLED true 今日头条源
SOGOU_WEIXIN_SEARCH_ENABLED true 搜狗微信源
XHS_SEARCH_ENABLED true 小红书源(需 xhs-cli)
PIYAO_SEARCH_ENABLED true 中国互联网联合辟谣平台源

文档入口


当前边界与如实口径

  • 前端只消费后端返回的真实 Report,不请求 replay,也不读本地 demo payload;请求失败展示错误态与重试入口。
  • report.provenance.source_type 当前只会是 backend_live 或 backend_mock;前端缺失 provenance 时保守落到 unknown。
  • URL 输入以公开 HTML 页面为主,不支持登录页、强反爬、PDF、图片正文;JS 渲染页(如 163/微博)在开启 RENDERED_FETCH_ENABLED(可选,需装 Playwright)后可由无头浏览器兜底渲染,默认关时仍只走静态抓取。
  • 日期字段:published_at 从 Baidu SERP 的 prefix-time span 真实抽取(2026-07-16 / 6天前 / 昨天 三种格式),SERP 里没有的场景返回空字符串(前端显示「时间未知」),永远不伪造 datetime.now()。
  • 相关性过滤:retrieval_service._result_matches_query 采用主体品牌 + 事件词双层过滤,避免「共享一个动词就算相关」(如「美团 裁员」query 拉回 Amazon / Meta 的裁员新闻)。
  • 内网网关地址是硬密码,绝不写入代码或文档;模型名运行时可露,但不硬编码进版本库。
  • 演示口径:mock demo 稳、可复现、零 key;real live 已联调,但延迟高、非默认、需按配方配置,别讲成随手就能实时跑。

用一句话总结:结构化流水线 + 流式可观测 + 契约化输出,让每一条判定都可追溯、可复核。

About

带执行过程可视化的中文谣言核查:拆原子事实、多源并行检索、逐条判真假,全程流式可观测

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages