用 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 报错 |
名称含中文或符号 | 只用字母数字 |
| 来源是转载网站 | 搜索结果质量 | 提示词要求优先官方和权威来源 |
| 一篇文章费用较高 | 说法多、搜索多 | 先手动挑出重点段落 |
| 结论前后不一致 | 模型随机性 | 重要说法多跑一次对比 |
项目资源
建议先用上面这段三句话的样例运行,结果应该能判断出“长城在月球上可见”不实。
然后拿一篇你自己写的文章试,逐条打开来源复核,体会哪些地方还需要人来判断。