精选案例 · Agent / 实践案例
每日论文推送
这个案例围绕「每日论文推送」记录了一条真实 AI 实践线索,正文重点集中在「目标范围」「整体架构」,适合先按任务意图阅读再判断复用。
案例速读
README 标题「每日论文推送」下已经出现运行/配置路径、脚本或接口线索、结果证据,正文重点集中在「目标范围」「整体架构」,比纯概念介绍更适合进入精选阅读流。 这篇案例的阅读价值在于,它把真实任务、模型辅助过程和可迁移做法放在同一个上下文里,读者可以从 「每日论文推送」、「目标范围」、「整体架构」、「采集流程」 进入正文。
- 建议重点看 可参考其中的运行与配置路径、包含可迁移的命令、脚本或接口线索、已有结果或观测证据可用于判断复用价值。结合 Agent / 实践案例 和「任务驱动用户、AI 实践者」这一受众定位,它更适合作为任务检索后的精读材料,而不是只看一句短摘要后快速跳过。
- 正文目录和原始材料仍然是判断依据;导读只帮助你更快定位阅读重点。
- 看点
- 每日论文推送
- 读者
- 任务驱动用户、AI 实践者
- 复用
- 可参考其中的运行与配置路径
- 结构
- 12 个目录入口
原文内容
每日论文推送
这是一个 Docker 化的论文采集、筛选、摘要生成和邮件推送工具。它面向统一身份认证和身份安全研究,每周自动从论文数据源采集候选论文,使用 DeepSeek 判断相关性并生成中文摘要,保存到本地 SQLite;每天从数据库中选择未发送论文,通过邮件推送给配置的收件人。
当前默认调度:
- 每周日 09:00(UTC+8 / Asia/Shanghai)采集论文。
- 每天 08:00(UTC+8 / Asia/Shanghai)发送最多 3 篇未发送论文。
- Docker 容器、服务和镜像名统一为
mind-feed,镜像为mind-feed:latest。
目标范围
论文筛选重点是统一身份认证平台和账号身份安全,包括:
- SSO、OAuth、OpenID Connect、OIDC、SAML。
- MFA、RBA、FIDO2、WebAuthn、Passkeys。
- 账号盗用、凭证填充、账号共享、身份威胁识别与响应。
- 零信任身份、网络接入认证、持续认证。
- Bot 检测、Agent/AI Agent 检测,以及与身份安全控制相关的 Agent 研究。
项目会尽量排除明显偏离方向的领域,例如无人机、车联网、V2G、通用 IoT、传感器网络、物理层认证、区块链等。只有当论文明确与企业身份、Web 账号安全或上述主题相关时才会保留。
整体架构
flowchart TD
Scheduler["APScheduler 定时器"] --> Collect["每周采集任务 collect_papers"]
Scheduler --> Send["每日邮件任务 send_digest"]
Collect --> Fetchers["数据源 Fetchers"]
Fetchers --> Crossref["Crossref"]
Fetchers --> DBLP["DBLP"]
Fetchers --> Arxiv["arXiv"]
Fetchers --> IACR["IACR ePrint"]
Collect --> Dedupe["标题/链接去重"]
Dedupe --> AgeFilter["近 10 年过滤"]
AgeFilter --> DomainFilter["偏题领域过滤"]
DomainFilter --> Content["原文摘要/正文抓取"]
Content --> Metadata["OpenAlex 元数据增强"]
Metadata --> LLM["DeepSeek 相关性判断与中文摘要"]
LLM --> SQLite["SQLite 数据库 data/papers.sqlite3"]
Send --> SQLite
SQLite --> Rank["待发送论文排序"]
Rank --> Email["SMTP 邮件发送"]
核心模块:
app/main.py:命令行入口,支持scheduler、collect、send、init-db、stats。app/scheduler.py:APScheduler 定时任务配置。app/services.py:采集和发送主流程。app/fetchers/:不同数据源的候选论文抓取器。app/content.py:论文官网、DOI 页面、PDF、OpenAlex、Semantic Scholar 内容抓取。app/metadata.py:OpenAlex 元数据增强,补全作者、年份、正式发表来源。app/llm.py:DeepSeek 调用、相关性判断、中文摘要生成。app/storage.py:SQLite 初始化、去重、保存、查询、发送状态更新。app/emailer.py:Markdown 风格邮件正文生成和 SMTP 发送。
采集流程
一次完整采集任务由 collect_papers() 执行:
- 读取配置,并检查 DeepSeek API key。
- 按顺序启用数据源:
Crossref -> DBLP -> arXiv -> IACR ePrint。 - 从各数据源拉取论文候选。
- 使用全局
max_candidates_per_run限制一次采集最多处理的候选数,当前为120。 - 对候选执行同批次去重和数据库去重。
- 丢弃年份缺失或超过近 10 年范围的候选。
- 根据偏题关键词丢弃明显不相关领域论文。
- 抓取原始摘要或正文片段;没有原始摘要/正文的候选会被跳过。
- 使用 OpenAlex 补全作者、年份、正式发表来源和链接。
- 调用 DeepSeek 判断是否属于目标范围,并基于原始摘要/正文生成中文摘要。
- 相关论文保存到 SQLite,初始状态为未发送。
注意:max_candidates_per_run 是一次完整采集任务的总候选数,不是单个 venue 或单个数据源的数量。例如当前值为 120,表示 Crossref、DBLP、arXiv、IACR ePrint 加起来最多处理 120 篇候选。
数据源与候选数量
当前配置位于 config/config.yaml:
- arXiv:
max_results: 80,检索窗口lookback_days: 3650。 - IACR ePrint:
max_results: 80,检索窗口lookback_days: 3650。 - Crossref:
rows_per_venue: 35,按 venue 和 topic term 检索。 - DBLP:
rows_per_query: 30,按 venue 和 topic term 检索。
支持的主要来源标签包括:
TDSC
TIFS
Journal of Cryptology
CCS
EUROCRYPT
S&P
CRYPTO
USENIX Security
NDSS
arXiv
IACR ePrint
其中 IEEE Symposium on Security and Privacy 的展示标签统一为 S&P。
去重策略
当前有两层去重:
- 单次采集内去重:
collect_papers()维护seen_titles,对规范化标题去重。 - 入库前去重:
PaperStore.exists()检查title_hash或link是否已存在。
标题规范化逻辑会转小写、去掉标点和多余空格,再计算 SHA-256 哈希。数据库字段 title_hash 有唯一约束。
当前去重主要依赖:
规范化标题 hash
论文链接
这能处理 arXiv 和会议版本标题基本一致的重复论文。但如果预印本标题和正式发表标题差异较大,可能仍需要后续扩展 DOI、arXiv ID、OpenAlex ID 或模糊标题匹配来增强去重。
内容抓取与摘要生成
LLM 不会只根据标题生成摘要。进入 DeepSeek 前,系统至少需要拿到以下内容之一:
- 论文页面中的原始摘要。
- DOI 或官网页面中的摘要。
- PDF 中提取出的摘要或正文片段。
- OpenAlex 或 Semantic Scholar 返回的公开摘要。
如果可以拿到 PDF 正文,系统会提取前若干页文本,并限制输入长度,避免过长请求。当前 DeepSeek 输入长度上限为 max_input_chars: 22000。
DeepSeek 返回 JSON,包含:
relevant:是否相关。confidence:置信度。tags:主题标签,当前邮件中不展示。reason:判定理由。summary_zh:中文摘要。
只有 relevant = true 且置信度不低于 min_confidence 的论文才会保存。
发送流程
每日发送任务由 send_digest() 执行:
- 从 SQLite 读取最多 3 篇
sent_at IS NULL的论文。 - 排序时优先选择非 arXiv/IACR ePrint 来源。
- 同一优先组内按
published_date或year从新到旧排序。 - 生成 Markdown 风格正文和 HTML 正文。
- 通过 SMTP SSL 发送邮件。
- 发送成功后,将这些论文的
sent_at写入当前 UTC 时间。 - 如果没有可发送论文,会发送一封“无可推送论文”的提示邮件。
正常邮件主题:
每日论文推送 YYYY-MM-DD
无可发送论文时:
每日论文推送 YYYY-MM-DD 无可推送论文
快速开始
复制配置模板:
Copy-Item .\config\config.example.yaml .\config\config.yaml
Copy-Item .\.env.example .\.env
编辑密钥和密码:
notepad .\.env
至少需要配置:
DEEPSEEK_API_KEY=你的DeepSeekKey
SMTP_PASSWORD=你的邮件客户端密码
构建并启动:
docker compose up -d --build
查看容器:
docker ps --filter name=mind-feed
查看日志:
docker logs --tail 80 mind-feed
持续跟踪日志:
docker logs -f mind-feed
停止服务:
docker compose stop
重新启动:
docker compose up -d
重建并启动:
docker compose up -d --build
常用任务命令
初始化数据库或查看统计:
docker compose exec -T mind-feed python -m app init-db
docker compose exec -T mind-feed python -m app stats
手动采集论文:
docker compose exec -T mind-feed python -m app collect
限制本次最多处理 10 篇候选,适合测试:
docker compose exec -T mind-feed python -m app collect --max-candidates 10
手动发送一次邮件:
docker compose exec -T mind-feed python -m app send
进入容器:
docker compose exec mind-feed bash
查看 compose 解析后的配置:
docker compose config
查看镜像:
docker images mind-feed
删除旧容器并重新创建:
docker compose down
docker compose up -d --build
数据库常用查询
SQLite 数据库默认保存在:
data/papers.sqlite3
查看总数和未发送数量:
python -c "import sqlite3; con=sqlite3.connect('data/papers.sqlite3'); print(con.execute('select count(*) from papers').fetchone()[0]); print(con.execute('select count(*) from papers where sent_at is null').fetchone()[0])"
查看最近入库论文:
python -c "import sqlite3; con=sqlite3.connect('data/papers.sqlite3'); con.row_factory=sqlite3.Row; [print(dict(r)) for r in con.execute('select id,title,source,year,sent_at from papers order by id desc limit 10')]"
查看待发送的前 3 篇:
python -c "import sqlite3; con=sqlite3.connect('data/papers.sqlite3'); con.row_factory=sqlite3.Row; [print(r['id'], r['source'], r['year'], r['title']) for r in con.execute(\"select id,source,year,title from papers where sent_at is null order by case when lower(source) in ('arxiv','iacr eprint') then 1 else 0 end, coalesce(published_date, printf('%04d-12-31', year), '') desc, priority desc, created_at asc limit 3\")]"
将所有论文重置为未发送:
python -c "import sqlite3; con=sqlite3.connect('data/papers.sqlite3'); con.execute('update papers set sent_at = null'); con.commit()"
按标题删除某篇论文:
python -c "import sqlite3; title='论文标题'; con=sqlite3.connect('data/papers.sqlite3'); con.execute('delete from papers where title=?', (title,)); con.commit()"
备份数据库:
Copy-Item .\data\papers.sqlite3 .\data\papers.backup.sqlite3
配置说明
核心配置文件是 config/config.yaml。
app:
timezone:调度时区,当前为UTC+8。log_level:日志级别。user_agent:访问外部 API 和网页时使用的 User-Agent。max_candidates_per_run:一次完整采集最多处理多少候选。max_paper_age_years:只保存近多少年的论文。per_candidate_timeout_seconds:单篇候选处理超时。
storage:
sqlite_path:SQLite 数据库路径,默认data/papers.sqlite3。
network:
proxy_url:统一代理地址。http_proxy/https_proxy/no_proxy:可引用.env中的环境变量。
content_fetch:
fetch_pdf:是否抓取 PDF。min_abstract_chars:摘要最小长度。min_full_text_chars:正文片段最小长度。max_full_text_chars:正文片段最大长度。max_pdf_bytes:PDF 最大下载大小。pdf_page_limit:PDF 解析页数上限。use_openalex/use_semantic_scholar:是否使用公开元数据服务兜底。
metadata:
enabled:是否启用 OpenAlex 元数据增强。timeout_seconds:元数据请求超时。
deepseek:
api_key:建议写成env:DEEPSEEK_API_KEY,从.env注入。base_url:当前为https://api.deepseek.com,代码会自动补/chat/completions。model:当前为deepseek-v4-pro。temperature:摘要和判断的温度。min_confidence:低于该置信度的论文不保存。max_input_chars:传给 LLM 的摘要/正文最大字符数。
email:
smtp_host/smtp_port:SMTP 地址和端口。use_ssl/use_tls:邮件加密方式。username/password:SMTP 用户名和客户端密码。from_addr:发件人。to_addrs:收件人列表。subject_prefix:邮件主题前缀,当前为每日论文推送。
schedule:
weekly_collect:每周采集时间。daily_email:每日发送时间。run_on_startup:容器启动时是否立即采集或发送,默认关闭。
sources:
arxiv:arXiv 检索配置。iacr_eprint:IACR ePrint RSS 配置。crossref:Crossref 期刊/会议检索配置。dblp:DBLP 期刊/会议检索配置。
filters:
topics:检索和 LLM 参考的主题词。target_description:LLM 判定目标范围。strong_identity_terms:强身份安全关键词。exclude_domain_keywords:偏题领域排除词。
代理配置
如果部分 API 或官网在当前网络环境下访问不稳定,可以在 .env 中配置代理:
HTTP_PROXY=http://127.0.0.1:7890
HTTPS_PROXY=http://127.0.0.1:7890
NO_PROXY=localhost,127.0.0.1
然后重启容器:
docker compose up -d
当前 Docker 默认 bridge 网络会让容器访问外网时通过宿主机 NAT 出口。配置代理后,Python 的 requests 会优先使用注入到容器的代理环境变量。
安全注意事项
- 不要把 DeepSeek API key 和邮箱客户端密码写入 README 或代码。
- 推荐只在
.env中保存密钥,并通过docker-compose.yml注入容器。 .env应避免提交到 Git 仓库。- 更换密钥后重启容器即可生效。
更换密钥:
notepad .\.env
docker compose restart mind-feed
常见问题
明天早上会不会收到邮件?
只要容器运行、SMTP 可用、数据库里有未发送论文,就会在 08:00(UTC+8)发送。可以用下面命令确认:
docker ps --filter name=mind-feed
docker logs --tail 80 mind-feed
docker compose exec -T mind-feed python -m app stats
为什么 arXiv 和会议论文可能重复?
系统会按规范化标题和链接去重;如果标题基本一致,会跳过重复记录。如果预印本和正式发表版本标题差异较大,可能需要人工清理或后续增强 DOI/OpenAlex ID 去重。
为什么邮件里 arXiv/IACR 比较少?
发送排序会优先选择非 arXiv/IACR ePrint 来源;arXiv 和 IACR 会保留在数据库中,但发送优先级较低。
为什么有些来源不是配置中的会议/期刊?
arXiv/IACR 候选会通过 OpenAlex 尝试匹配正式发表版本;如果 OpenAlex 找到正式来源,系统会用正式来源替换预印本来源。
为什么没有论文被保存?
常见原因包括:抓不到原始摘要/正文、论文超过 10 年、标题命中偏题领域排除词、DeepSeek 判断不相关、DeepSeek/API 网络错误、数据库已有重复记录。
为什么邮件发送后数据库记录还在?
发送后不会删除论文,只会写入 sent_at。这样可以保留历史记录并避免重复发送。
项目文件结构
.
├── app/
│ ├── main.py
│ ├── scheduler.py
│ ├── services.py
│ ├── storage.py
│ ├── llm.py
│ ├── content.py
│ ├── metadata.py
│ ├── emailer.py
│ └── fetchers/
├── config/
│ ├── config.example.yaml
│ └── config.yaml
├── data/
│ └── papers.sqlite3
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md