效率工具

用 Perplexity Agent API 做一个事实核查助手

用 Perplexity Agent API 做一个事实核查助手:把一篇文章交给它,它找出所有可以核查的说法(数字、日期、事件),逐条联网核查,按固定的 JSON 结构返回“属实 / 部分属实 / 不实 / 无法核实”和来源,脚本再生成一张 Markdown 核查表。

BINBHO · 图文教程

用 Perplexity Agent API 做一个事实核查助手:把一篇文章交给它,它找出所有可以核查的说法(数字、日期、事件),逐条联网核查,按固定的 JSON 结构返回“属实 / 部分属实 / 不实 / 无法核实”和来源,脚本再生成一张 Markdown 核查表。

先看清成品

成品是 factcheck.py,输入 article.txt,输出 article.check.md 和 article.check.json。本文已用模拟客户端实际运行 2 个测试并全部通过:请求带上了名为 fact_check 的 JSON Schema,表格中正确显示结论图标和来源链接,结论不在四个选项内时校验报错。另外用真实 SDK 和无效密钥发了请求,服务器返回 401“Invalid API key”,SDK 能接受这些参数;但 401 发生在参数校验之前,参数本身只按官方文档核对过。本文环境没有有效的 Perplexity API 密钥,没有拿到真实搜索结果。

注意:Perplexity API 按用量计费,价格以官方价格页为准,每次响应的 usage 字段会给出实际费用。请在服务可用的地区使用,不要绕过地区限制。搜索结果和 AI 总结可能出错,发布前要点开来源核对;转载他人文章要遵守版权规定。

注意:核查结果只是辅助,最终判断要由人根据原始来源作出。不要把 AI 核查结果直接当作对他人的指控公开发布。

做完之后你会得到什么?

成果 说明
说法提取 自动挑出可核查的事实,跳过观点
结构化输出 response_format 的 json_schema
固定结论选项 Pydantic Literal 限定四种
核查表 Markdown,可贴进文档

需要准备哪些环境?

项目 要求 说明
Perplexity API 密钥 在 API 控制台创建并充值 保存为环境变量 PERPLEXITY_API_KEY
Python 3.10 或更高
SDK pip install perplexityai 本文使用 0.44.0
Pydantic pip install pydantic 定义输出结构

第一步:结构化输出怎么用?

官方输出控制文档的写法:

{
  "response_format": {
    "type": "json_schema",
    "json_schema": { "name": "your_schema_name", "schema": { } }
  }
}
官方说明 影响
name 必填,1~64 个字母数字字符 本文用 fact_check
required 里的字段必须输出 Pydantic 生成的结构会自动列出
新结构第一次使用可能要 10~30 秒准备 第一次请求可能慢甚至超时,之后正常

第二步:代码怎么写?

1. factcheck.py

"""事实核查表:把一段文章里的说法逐条交给 Perplexity 联网核查,输出结构化结果和 Markdown 表格。
用法:python factcheck.py article.txt
"""
import json
import sys
from pathlib import Path
from typing import List, Literal

from pydantic import BaseModel, Field


class Check(BaseModel):
    claim: str = Field(description="原文中的一个可核查的事实性说法")
    verdict: Literal["属实", "部分属实", "不实", "无法核实"]
    explanation: str = Field(description="一到两句话说明理由")
    sources: List[str] = Field(description="支持判断的来源网址")


class Report(BaseModel):
    checks: List[Check]


PROMPT = """你是严谨的事实核查编辑。从下面文章中找出所有可以核查的事实性说法(数字、日期、人物、事件),
逐条联网核查。观点和预测不要列入。找不到可靠来源时判定为“无法核实”,不要猜测。

文章:
{text}"""

ICON = {"属实": "✅", "部分属实": "⚠️", "不实": "❌", "无法核实": "❔"}


def check(client, text: str, preset: str = "low") -> Report:
    resp = client.responses.create(
        preset=preset,
        input=PROMPT.format(text=text),
        response_format={"type": "json_schema", "json_schema": {
            "name": "fact_check", "schema": {**Report.model_json_schema(), "additionalProperties": False}}},
    )
    return Report.model_validate_json(resp.output_text)


def to_markdown(r: Report) -> str:
    rows = ["| 说法 | 结论 | 理由 | 来源 |", "| :--- | :--- | :--- | :--- |"]
    for c in r.checks:
        src = "<br>".join(f"[{i}]({u})" for i, u in enumerate(c.sources, 1)) or "—"
        rows.append(f"| {c.claim} | {ICON[c.verdict]} {c.verdict} | {c.explanation} | {src} |")
    return "\n".join(rows) + "\n"


def main(argv=None, client=None):
    path = Path((argv or sys.argv[1:])[0])
    if client is None:
        from perplexity import Perplexity
        client = Perplexity()
    r = check(client, path.read_text(encoding="utf-8"))
    path.with_suffix(".check.md").write_text(to_markdown(r), encoding="utf-8")
    path.with_suffix(".check.json").write_text(r.model_dump_json(indent=2), encoding="utf-8")
    bad = [c for c in r.checks if c.verdict in ("不实", "部分属实")]
    print(f"共 {len(r.checks)} 条说法,需要修改 {len(bad)} 条,结果见 {path.with_suffix('.check.md')}")
    return r


if __name__ == "__main__":
    main()
要点 说明
Literal[...] 结论只能是四种之一
additionalProperties: False 与官方示例一致,不允许多余字段
“观点和预测不要列入” 只核查能核查的内容
同时输出 JSON 方便以后汇总统计

2. 测试样例文章

埃菲尔铁塔高约 330 米,1889 年建成。长城在月球上肉眼可见。

第三步:怎样测试和运行?

1. 离线测试

import json
from types import SimpleNamespace as NS

import pytest
from pydantic import ValidationError

from factcheck import Report, main

OK = {"checks": [
    {"claim": "埃菲尔铁塔高约 330 米", "verdict": "属实", "explanation": "官方资料一致。", "sources": ["https://a.example"]},
    {"claim": "长城在月球上肉眼可见", "verdict": "不实", "explanation": "宇航员表示看不到。", "sources": []}]}


class Fake:
    def __init__(self, payload):
        self.payload, self.calls = payload, []
        self.responses = NS(create=lambda **kw: self.calls.append(kw) or NS(output_text=json.dumps(self.payload, ensure_ascii=False)))


def test_report(tmp_path):
    art = tmp_path / "a.txt"
    art.write_text("……", encoding="utf-8")
    c = Fake(OK)
    r = main([str(art)], client=c)
    fmt = c.calls[0]["response_format"]
    assert fmt["type"] == "json_schema" and fmt["json_schema"]["name"] == "fact_check"
    md = (tmp_path / "a.check.md").read_text(encoding="utf-8")
    assert "❌ 不实" in md and "| — |" in md and "[1](https://a.example)" in md
    assert len(r.checks) == 2


def test_rejects_unknown_verdict():
    bad = {"checks": [{**OK["checks"][0], "verdict": "差不多"}]}
    with pytest.raises(ValidationError):
        Report.model_validate(bad)
python -m pytest -q

本文实际输出 2 passed。

2. 真实运行

export PERPLEXITY_API_KEY="你的密钥"
python factcheck.py article.txt

期望输出 共 N 条说法,需要修改 M 条,结果见 article.check.md。核查表格式:

| 说法 | 结论 | 理由 | 来源 |
| :--- | :--- | :--- | :--- |
| 长城在月球上肉眼可见 | ❌ 不实 | …… | [1](https://…) |

3. 人工复核

结论 怎么处理
不实 / 部分属实 打开来源确认,再修改原文
无法核实 自己查找权威来源,或删除该说法
属实 抽查几条来源

出问题时该怎么调?

1. 第一次请求超时

官方说明新结构第一次需要准备时间。重试一次即可。

2. 校验报错

打印 resp.output_text 看原始内容。常见原因是输出被截断,文章太长时分段核查。

3. 把观点也当成说法

在提示词里举例说明什么是观点,例如“我认为”“可能会”。

常见问题怎么处理?

现象 原因 解决方法
401 密钥错误 重新复制
name 报错 名称含中文或符号 只用字母数字
来源是转载网站 搜索结果质量 提示词要求优先官方和权威来源
一篇文章费用较高 说法多、搜索多 先手动挑出重点段落
结论前后不一致 模型随机性 重要说法多跑一次对比

项目资源

用途 地址
结构化输出 https://docs.perplexity.ai/docs/agent-api/output-control
Agent API 快速入门 https://docs.perplexity.ai/docs/agent-api/quickstart
预设 https://docs.perplexity.ai/docs/agent-api/presets
快速入门(创建 API 密钥) https://docs.perplexity.ai/docs/getting-started/quickstart
价格 https://docs.perplexity.ai/docs/getting-started/pricing
官方 SDK https://docs.perplexity.ai/docs/sdk/overview

建议先用上面这段三句话的样例运行,结果应该能判断出“长城在月球上可见”不实。

然后拿一篇你自己写的文章试,逐条打开来源复核,体会哪些地方还需要人来判断。