一个带执行过程可视化的谣言核查产品:接收一条消息,把它拆成可判定的原子事实,联网找证据,逐条判定真假,并把「结论是怎么来的」完整展示出来。
Next.js 前端 · FastAPI 后端 · 多 Agent 并行检索 · 全程流式可观测
- 产品能力速览
- 核查全流程 · 一张图看懂
- 多 Agent 并行架构
- 两档核查 · 秒级 vs 分钟级
- 四种命运 · 每条事实独立判定
- 语义证据重排 · 治「字面撞词」
- 来源可信度打分
- 抓取正文 · 三层降级链路
- 快速开始
- 持续集成
- 接口与运行路径
- 文档入口
- 当前边界与如实口径
| 能力 | 关键特征 | |
|---|---|---|
| 🎯 | 拆 claim 逐条判 | 一条消息拆成原子事实,每条独立判 属实 / 不实 / 证据不足 / 各方矛盾,混合情况不粉饰 |
| 🌐 | 5 路真检索 | 百度 + 小红书 + 今日头条 + 搜狗微信 + 联合辟谣平台,多源调度且 SERP 日期真实抓取而非伪造 |
| 🔎 | 语义证据重排 | 证据按「意思相关度」重排(embedding 余弦),治字面撞词;可选、失败自动回退字面打分 |
| 🧠 | Agent 多轮迭代 | 深度档跑「搜 → 判 → 再搜」循环,配合 LLM Critic 单调下调(永不加强判定) |
| 📊 | 真伪概率 | 除 verdict 外每条还有 0–100 概率,明确标注是「基于证据」还是「基于常识先验」 |
| 👁️ | 全程可观测 | 流式事件直播每一步,用户能看到「哪一步、找到了什么、为什么这么判」 |
| 📜 | 可复核证据链 | 每条判定都能展开 <details>判定依据:来源分级 · 相关性说明 · 原始 URL |
六步流水线,每一步都以流式事件推给前端,用户实时看到「后端现在在干什么」:
- 输入 — 支持文本、URL、问句三种输入类型
- 标准化 — 识别输入类型,抽取标题/摘要/关键词/来源
- 多源检索 — 固定流水线顺序补充 5 路证据源;深度多 Agent 模式并行调度,随后去重并规范化时间戳
- 拆 Claim — 把核心事实和存疑细节各自独立,一条消息可能拆成 3–5 条
- 逐条判定 — 每条 claim 独立走 verdict + 真伪概率
- 报告 — 组装成带来源标注、时间线、可信度打分的结构化
Report
深度档 (mode=deep) 开启多 Agent 编排后,检索层从串行 5 源 升级为并行 DAG:
为什么这么设计:
- 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,行为不变。
同一后端进程同时提供两档,不靠环境变量切换。前端主按钮走 fast,出结果后再给「深度核查」二次入口。
| 档位 | 触发 | 时延 | 判定路径 | 适用 |
|---|---|---|---|---|
| fast ⚡ | 默认 / 请求带 mode=fast |
~0.2–0.3s | 5 路检索 → 规则引擎 verdict | 秒级实时查证 |
| deep 🔬 | 请求带 mode=deep |
分钟级 | Agent 循环 → LLM synthesis → Critic 校验 → 结构化补全 | 需要更强证据时的深度核查 |
mode 只切换分析深度,检索 provider 始终由 RETRIEVAL_PROVIDER 决定,两档共用同一套真实检索层。
一条传闻往往「核心属实、细节存疑」。把它拆成原子 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 组信号叠加:
| 信号 | 分值 | 何时触发 |
|---|---|---|
| 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 抓静态 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)
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=truepython -m pip install -r backend/requirements-dev.txt
uvicorn backend.app.main:app --reload默认地址:http://127.0.0.1:8000(健康检查 GET /api/v1/health)
cd frontend && npm install && npm run dev默认地址:http://127.0.0.1:3000
python -m pytest backend/tests/ -q # 后端回归(当前 700+ tests)
cd frontend && npm run typecheck && npm test # 前端类型检查 + 单测CI 每次 push/PR 也会跑同一套(见下节)。
.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 会将任何“已配置但不可用”的来源视为失败,适合部署门禁。
逐次 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 | 说明 | 适用 |
|---|---|---|
mock |
稳定回归样例 | 默认,用于开发/联调/演示 |
playwright |
httpx 抓百度(主)+ Bing(兜底),日期从 prefix-time span 真实抽取 |
推荐真实联网路径,中文覆盖好,无浏览器依赖 |
gdelt |
GDELT 公开新闻 API | 英文事件,中文覆盖弱 |
kimi |
LLM 内建 $web_search |
仅对支持该工具的供应商有效 |
off |
关闭检索 | 只走 LLM 内部知识 |
| 环境变量 | 默认 | 说明 |
|---|---|---|
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 |
中国互联网联合辟谣平台源 |
- docs/README.md — 文档总索引
- docs/current-code-architecture-guide.md — 代码结构与架构详解(图文并茂;§7.7 推理器可靠性、§11 事实边界仲裁、§12 联网检索选型均归档在此)
- backend/README.md · frontend/README.md — 前后端说明
- contracts/README.md · evals/minimal_v1/README.md — 协议与评测
- DEMO.md — 演示脚本 + 演示前 Smoke 检查
- CONTRIBUTING.md · SECURITY.md · docs/adr/ — 协作、安全与架构决策
- 前端只消费后端返回的真实
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-timespan 真实抽取(2026-07-16/6天前/昨天三种格式),SERP 里没有的场景返回空字符串(前端显示「时间未知」),永远不伪造datetime.now()。 - 相关性过滤:
retrieval_service._result_matches_query采用主体品牌 + 事件词双层过滤,避免「共享一个动词就算相关」(如「美团 裁员」query 拉回 Amazon / Meta 的裁员新闻)。 - 内网网关地址是硬密码,绝不写入代码或文档;模型名运行时可露,但不硬编码进版本库。
- 演示口径:
mock demo稳、可复现、零 key;real live已联调,但延迟高、非默认、需按配方配置,别讲成随手就能实时跑。
用一句话总结:结构化流水线 + 流式可观测 + 契约化输出,让每一条判定都可追溯、可复核。








