AI 工具

把一批问题交给 Felo,结果汇总到 CSV 并检查来源链接

把一批问题交给 Felo,结果汇总到一张 CSV 表里,并且检查每个来源链接现在还能不能打开。没有来源、或者来源全部失效的回答,标记为“需人工核对”。链接检查器还可以单独使用(--links),用于检查任何一批网址。

BINBHO · 图文教程

把一批问题交给 Felo,结果汇总到一张 CSV 表里,并且检查每个来源链接现在还能不能打开。没有来源、或者来源全部失效的回答,标记为“需人工核对”。链接检查器还可以单独使用(--links),用于检查任何一批网址。

先看清成品

  • 测试: pytest 3 项通过。

  • 模拟服务器: 3 个问题都跑完了。其中一个问题没有来源,被标记为“需人工核对”;一个返回 404 的来源被识别为失效。

  • 用 --links 检查真实网址:

    网址 状态码 结论
    维基百科 200 可打开
    B 站 200 可打开
    知乎 403 可能拦截了自动访问,请人工打开
    httpbin 404 404 失效

没有在 box 上跑的部分: box 上没有 Felo API Key。对真实接口只做过无效 Key 的调用:/v2/chat 和 /api/v1/models 都返回 HTTP 401,body 是 {"status":401,"code":"UNAUTHORIZED","message":"Invalid API Key",…};不带 Key 时 message 是 "API Key is required"。其余部分在我写的模拟服务器 fake_felo.py 上运行,它按文档的结构返回固定的假内容。

合规提醒:FELO_API_KEY 只放在环境变量或服务器端,不要写进网页前端或提交到 Git。问题内容会发到 Felo 的服务器,不要提交个人隐私或公司机密。AI 回答引用网页时,版权归原作者:保留来源链接,引用前打开原文核对,不要整篇转载。检查链接时请求要少、要慢,并遵守对方网站的规则。计费规则以 Felo API Platform 的页面为准,本文不列价格。

做完之后你会得到什么?

成果 说明
felo_batch.py 逐个提问(请求之间有间隔)→ 链接去重后检查 → 写入 CSV(utf-8-sig 编码);--links 模式只检查链接
felo_search.py 和 A 篇相同
questions.txt / urls.txt 示例问题和示例网址
test_links.py 状态码分类

需要准备哪些环境?

同 A 篇。--links 模式不需要 Felo Key。

文件结构

46-felo-B-src/
├── felo_batch.py
├── felo_search.py
├── questions.txt
├── urls.txt
└── test_links.py

第 1 步:代码

felo_batch.py:

"""批量提问 Felo → 汇总 CSV,并检查每个来源链接现在还能不能打开。
- 每个问题调用一次 /v2/chat(复用 felo_search.ask,含 429 重试),两次请求之间间隔 INTERVAL 秒
- 链接检查:先 HEAD,失败再 GET;401/403/412/429 记为“可能拦截了自动访问”,其他 4xx/5xx 和连接错误记为失效
- 没有来源、或者来源全部失效的回答,标记为“需人工核对”
环境变量:FELO_API_KEY、FELO_BASE、INTERVAL(默认 2)
用法:python felo_batch.py questions.txt 结果.csv
      python felo_batch.py --links urls.txt     只检查链接"""
import csv, os, sys, time
from urllib.parse import urlparse
import requests
from felo_search import FeloError, ask

# 用一个说明身份的 User-Agent:不少网站(实测维基百科 403、B 站 412)会拒绝 python-requests 默认的 UA;
# 不要伪装成浏览器,写清楚用途和联系方式(维基媒体的 UA 规范也要求这样做)
# 联系方式用 CONTACT 环境变量填你自己的;不要留 you@example.com 占位符(实测 upload.wikimedia.org 会对它返回 403)
CONTACT = os.environ.get("CONTACT")
UA = {"User-Agent": "binbho-link-check/1.0" + (f" (personal research notes; contact: {CONTACT})" if CONTACT else "")}
BLOCKED = {401, 403, 412, 429}   # 这些更可能是“拦截了自动访问”,不等于链接失效

def link_status(url):
    try:
        r = requests.head(url, timeout=10, allow_redirects=True, headers=UA)
        if r.status_code >= 400: r = requests.get(url, timeout=10, stream=True, headers=UA); r.close()   # 有的站不支持 HEAD
        return r.status_code
    except requests.RequestException:
        try: r = requests.get(url, timeout=10, stream=True, headers=UA); r.close(); return r.status_code
        except requests.RequestException as e: return type(e).__name__

def ok(s): return isinstance(s, int) and 200 <= s < 400
def verdict(s): return "可打开" if ok(s) else ("可能拦截了自动访问,请人工打开" if s in BLOCKED else "失效")

def main():
    if sys.argv[1] == "--links":     # 只检查一批链接(不需要 Felo key)
        for u in [l.strip() for l in open(sys.argv[2], encoding="utf-8") if l.strip()]:
            st = link_status(u); print(f"{str(st):>16}  {verdict(st)}  {u}")
        return
    qs = [l.strip() for l in open(sys.argv[1], encoding="utf-8") if l.strip()]
    rows, checked = [], {}
    for i, q in enumerate(qs):
        if i: time.sleep(float(os.environ.get("INTERVAL", 2)))
        try: d = ask(q)
        except FeloError as e:
            rows.append({"问题": q, "回答": "", "来源数": 0, "失效链接": "", "域名": "", "状态": f"出错:{e}"}); print(f"✗ {q}:{e}"); continue
        res = d.get("resources") or []
        for x in res:
            if x["link"] not in checked: checked[x["link"]] = link_status(x["link"])
        bad = [x["link"] for x in res if verdict(checked[x["link"]]) == "失效"]
        state = "需人工核对:没有来源" if not res else ("需人工核对:来源全部失效" if len(bad) == len(res) else "可用(引用前打开来源核对)")
        rows.append({"问题": q, "回答": d.get("answer", "").strip(), "来源数": len(res), "失效链接": " ".join(f"{u}({checked[u]})" for u in bad),
                     "域名": " ".join(sorted({urlparse(x['link']).netloc for x in res})), "状态": state})
        print(f"{'✓' if state.startswith('可用') else '⚠'} {q}:{len(res)} 个来源,失效 {len(bad)} 个;{state}")
    with open(sys.argv[2], "w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    print(f"已写入 {sys.argv[2]}(共检查 {len(checked)} 个不重复链接)")

if __name__ == "__main__":
    main()

test_links.py:

import felo_batch as b
def test_ok_codes():
    assert b.ok(200) and b.ok(301) and not b.ok(404) and not b.ok("ConnectTimeout")
def test_unreachable_is_error_name():
    assert isinstance(b.link_status("http://127.0.0.1:9/nothing"), str)
def test_blocked_is_not_dead():
    assert b.verdict(403).startswith("可能拦截") and b.verdict(404) == "失效" and b.verdict(200) == "可打开"

第 2 步:运行

$ python felo_batch.py questions.txt 结果.csv          # box 上指向模拟服务器
✓ 电动牙刷怎么选:3 个来源,失效 1 个;可用(引用前打开来源核对)
✓ 声波牙刷和旋转牙刷哪个好:3 个来源,失效 1 个;可用(引用前打开来源核对)
⚠ 冷门问题:某型号牙刷的停产时间:0 个来源,失效 0 个;需人工核对:没有来源
已写入 结果.csv(共检查 3 个不重复链接)

$ python felo_batch.py --links urls.txt                  # 真实网址,box 上实测
             403  可能拦截了自动访问,请人工打开  https://www.zhihu.com/question/19550225
             200  可打开  https://zh.wikipedia.org/wiki/%E7%89%99%E5%88%B7
             200  可打开  https://www.bilibili.com/
             404  失效  https://httpbin.org/status/404

调试示例:好好的链接被判成“失效”

第一版用 requests 的默认设置检查链接。检查真实网址时,结果是这样的:

网址 HEAD GET
维基百科 403 403
B 站 412 412
知乎 403 403

这些页面在浏览器里都能打开。原因是 python-requests/x.y 这个默认的 User-Agent 被不少网站直接拒绝。

正确的做法不是伪装成浏览器,而是使用能说明身份的 UA:写清用途和联系方式,维基媒体的 UA 规范就是这样要求的。换成 binbho-link-check/1.0 (personal research notes; contact: …) 之后,维基百科和 B 站都返回 200。知乎仍然是 403,它会拦截自动访问。

所以 401、403、412、429 这几个状态码,现在归为“可能拦截了自动访问,请人工打开”,不算失效。真正的 404 仍然判为失效。联系方式通过 CONTACT 环境变量填写你自己的邮箱。不要留 you@example.com 这类占位符:47-runway-C 里实测,upload.wikimedia.org 对带占位邮箱的 UA 返回 403(“Please honor our robot policy”),不带联系方式或者填真实邮箱则返回 200。

常见问题

现象 原因 处理
大量 403 网站拦截自动访问 人工打开核对;不要绕过网站的拦截
检查很慢 每个链接最多请求两次,超时 10 秒 链接已经去重;可以把超时调小
CSV 用 Excel 打开是乱码 编码问题 本文使用 utf-8-sig

模板规则

  • 链接检查使用说明身份的 UA,不伪装成浏览器。
  • “被拦截”和“失效”分开统计。
  • 没有可用来源的回答必须人工核对。

项目资源

用途 地址
Felo API Platform 文档 https://openapi.felo.ai/docs
Chat API(/v2/chat) https://openapi.felo.ai/docs/api-reference/chat
认证与限流 https://openapi.felo.ai/docs/authentication
维基媒体 User-Agent 规范 https://foundation.wikimedia.org/wiki/Policy:Wikimedia_Foundation_User-Agent_Policy

来源链接今天能打开,下个月就不一定了。定期检查一遍,调研资料才不会过期。