| name | qiaomu-paper-interpreter |
| description | Transform academic papers into conversational Chinese articles in Qiaomu's style. Use when user provides arXiv URL/ID with keywords "解读论文", "论文解读", "理解paper", "读paper", or "乔木风格". Runs fully automatically. |
| allowed-tools | Bash, Read, Write, Edit, Glob, Grep, WebFetch, TodoWrite |
乔木论文解读
概述
将学术论文自动转化为乔木风格的深度解读文章。全自动执行,无需用户中途确认。
核心特点:
- 对话式语言,像和朋友聊天
- 关键术语用引用块(>)解释,每出现新术语立刻加
- 生活化类比帮助理解,每个核心方法后必须紧跟一个类比
- 真实论文图表嵌入文章(从 LaTeX 源码精确提取)
- AI 生成纸雕水彩封面 + 《纽约客》风格配图(需配置图片生成服务)
- 写作风格规范已内嵌,无需外部依赖
配置(首次使用必读)
配置方式:.env 文件
在 skill 目录下创建 .env 文件(已加入 .gitignore,不会被发布):
PAPER_OUTPUT_DIR=~/Papers/papers
PAPER_READING_DIR=~/Papers/reading
OBSIDIAN_VAULT=
IMAGE_PROVIDER=skip
IMAGE_GENERATOR_SCRIPT=
参考模板:skill 目录下的 .env.example 包含所有可用变量及说明。
变量说明:
| 变量 | 默认值 | 说明 |
|---|
PAPER_OUTPUT_DIR | ~/Papers/papers | 论文工作目录根路径 |
PAPER_READING_DIR | ~/Papers/reading | 最终文章存放目录 |
OBSIDIAN_VAULT | 空 | Obsidian vault 名称,空则跳过自动打开 |
IMAGE_PROVIDER | skip | skip(跳过配图)/ jimeng / openai |
IMAGE_GENERATOR_SCRIPT | 空 | 图片生成脚本路径,空则用内置默认 |
系统环境变量优先级高于 .env 文件,适合 CI/CD 或多项目场景。
配置读取逻辑(每次执行时运行)
import os
from pathlib import Path
SKILL_DIR = Path("~/.agents/skills/qiaomu-paper-interpreter").expanduser()
env_file = SKILL_DIR / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip())
OUTPUT_DIR = Path(os.environ.get("PAPER_OUTPUT_DIR", "~/Papers/papers")).expanduser()
READING_DIR = Path(os.environ.get("PAPER_READING_DIR", "~/Papers/reading")).expanduser()
OBSIDIAN_VAULT = os.environ.get("OBSIDIAN_VAULT", "")
IMAGE_PROVIDER = os.environ.get("IMAGE_PROVIDER", "skip")
IMAGE_GENERATOR_SCRIPT = os.environ.get("IMAGE_GENERATOR_SCRIPT", "")
if not IMAGE_GENERATOR_SCRIPT and IMAGE_PROVIDER != "skip":
IMAGE_GENERATOR_SCRIPT = str(
Path("~/.agents/skills/qiaomu-image-generator/scripts/generate.py").expanduser()
)
执行流程(4步)
顺序:步骤A → 步骤B → 步骤C → 步骤D
执行原则:全程自动,用 TodoWrite 显示进度,静默修复质量问题。
初始化 Todo
TodoWrite([
{"content": "A. 提取论文内容 + 并发转换图片", "status": "in_progress"},
{"content": "B. 生成乔木风格解读文章", "status": "pending"},
{"content": "C. 生成 AI 配图(封面 + 纽约客)", "status": "pending"},
{"content": "D. 保存发布", "status": "pending"},
])
步骤A:提取论文内容 + 并发转换图片
目标:一次完成 LaTeX 提取、元数据解析、图片并发转换、图表清单生成。
A1. 确定 arxiv_id
支持输入格式:
| 输入 | 处理方式 |
|---|
https://arxiv.org/abs/2605.03269 | 直接提取 ID |
https://arxiv.org/pdf/2605.03269 | 直接提取 ID(和 abs 等价) |
https://arxiv.org/pdf/2605.03269v2 | 提取 ID,保留版本号 |
https://huggingface.co/papers/2605.03269 | 先 WebFetch 页面找 arXiv 链接,再提取 ID |
2605.03269 | 直接作为 ID 使用 |
内部流程:extract_tex.py 拿到 ID 后,从 https://arxiv.org/e-print/{id} 下载 LaTeX 源码 tar.gz,自动解压,找 main.tex,提取结构化内容和图片。
如果论文没有 LaTeX 源码(PDF-only):
e-print 返回 PDF 而不是 tar.gz,extract_tex.py 会返回 has_source: false。此时自动切换到 markitdown fallback:
markitdown "https://arxiv.org/pdf/{arxiv_id}" -o "{paper_dir}/extracted_text.md"
fallback 情况下无法提取真实图表,figure_list.md 标注"LaTeX 源码不可用",文章中用文字描述代替图片引用。
A2. 并发启动:extract_tex.py + arXiv API(含断点续跑)
断点续跑:如果 extract_result.json 已存在且有效,直接跳过下载,从 A3 继续。适用于崩溃重试场景。
import subprocess, threading, urllib.request, re, time, json
from pathlib import Path
existing = list(OUTPUT_DIR.glob(f"*{arxiv_id.replace('.', '_')}*"))
_resume_dir = next((d for d in existing
if (d / "extract_result.json").exists()
and (d / "extract_result.json").stat().st_size > 100), None)
if _resume_dir:
paper_dir = _resume_dir
result_file = paper_dir / "extract_result.json"
print(f"⚡ 断点续跑:跳过下载,使用 {paper_dir.name}")
arxiv_meta = {}
_skip_extract = True
else:
paper_dir = OUTPUT_DIR / f"tmp_{int(time.time())}"
paper_dir.mkdir(parents=True, exist_ok=True)
result_file = paper_dir / "extract_result.json"
_skip_extract = False
arxiv_meta = {}
def fetch_arxiv_meta():
try:
xml = urllib.request.urlopen(
f"http://export.arxiv.org/api/query?id_list={arxiv_id}", timeout=10
).read().decode()
m = re.search(r'<published>(.*?)</published>', xml)
arxiv_meta["published_date"] = m.group()[:] m
entries = re.findall(, xml, re.DOTALL)
entry = entries[] entries
t = re.search(, entry, re.DOTALL)
arxiv_meta[] = t.group().strip().replace(, ) t
arxiv_meta[] = re.findall(, entry)
Exception:
arxiv_meta[] =
meta_thread = threading.Thread(target=fetch_arxiv_meta, daemon=)
meta_thread.start()
_skip_extract:
:
proc = subprocess.run(
[,
(Path().expanduser()),
arxiv_url_or_id, , , (paper_dir / )],
capture_output=, text=, timeout=
)
result_file.write_text(proc.stdout, encoding=)
subprocess.TimeoutExpired:
()
proc =
meta_thread.join(timeout=)
extract_tex.py 已内置于本 skill 的 scripts/ 目录,无需外部依赖。
A3. 解析 JSON,失败检测 + 合并 arXiv 元数据
import os
raw = result_file.read_text(encoding="utf-8") if result_file.exists() else ""
json_start = raw.find('{')
if json_start == -1 or len(raw.strip()) < 50:
print("⚠️ extract_tex.py 输出无效,切换 markitdown fallback")
import subprocess as _sp
_sp.run(["markitdown", f"https://arxiv.org/pdf/{arxiv_id}",
"-o", str(paper_dir / "extracted_text.md")], check=False)
data = {}
figures = []
else:
try:
data = json.loads(raw[json_start:])
except json.JSONDecodeError:
print("⚠️ JSON 解析失败,切换 markitdown fallback")
import subprocess as _sp
_sp.run(["markitdown", f"https://arxiv.org/pdf/{arxiv_id}",
"-o", str(paper_dir / "extracted_text.md")], check=False)
data = {}
figures = []
title = data.get("title", "") or arxiv_meta.get("title", "")
authors = data.get("authors", []) or arxiv_meta.get("authors", [])
arxiv_id = data.get("arxiv_id", arxiv_id)
markdown = data.get(, )
figures = data.get(, {}).get(, figures () [])
published_date = arxiv_meta.get(, )
生成 paper_id(代码化,避免每次结果不一致导致重名)
def make_paper_id(title: str, pub_date: str) -> str:
year = pub_date[:4] if pub_date else "0000"
# 提取全大写缩写词(长度 2-6),如 BERT、MACE、VL
abbrevs = re.findall(r'\b[A-Z]{2,6}\b', title)
if abbrevs:
return f"{''.join(abbrevs[:2])}{year}"
# 否则取前 3 个英文关键词(跳过冠词介词)
skip = {'a','an','the','of','for','on','in','to','and','or','with','via'}
words = [w for w in re.findall(r'[a-zA-Z]+', title) if w.lower() not in skip]
slug = "".join(w.capitalize() for w in words[:3])
return f"{slug}{year}"
paper_id = make_paper_id(title, published_date)
重命名临时目录(如已存在则加后缀避免冲突)
new_dir = OUTPUT_DIR / paper_id
if new_dir.exists():
new_dir = OUTPUT_DIR / f"{paper_id}_2"
os.rename(paper_dir, new_dir)
paper_dir = new_dir
保存文本和元数据
(paper_dir / "extracted_text.md").write_text(markdown, encoding="utf-8")
(paper_dir / "metadata.json").write_text(
json.dumps({
"paper_id": paper_id, "title": title, "authors": authors,
"arxiv_id": arxiv_id,
"arxiv_url": f"https://arxiv.org/abs/{arxiv_id}",
"published_date": published_date,
}, ensure_ascii=False, indent=2),
encoding="utf-8"
)
### A4. 并发转换图片
**过滤规则**:跳过 >20MB 的图(多页定性对比图,不适合放博客);其余全部转换。
```python
import concurrent.futures, subprocess, shutil
(paper_dir / "images").mkdir(exist_ok=True)
def convert_figure(fig):
idx = fig["index"]
raw_src = fig["local_files"][0] if fig.get("local_files") else None
if not raw_src:
return None
# local_files 可能是相对路径或绝对路径,逐级尝试
latex_dir = paper_dir / "latex_source"
candidates = [
Path(raw_src), # 原始路径(绝对)
latex_dir / raw_src, # 相对于 latex_source/
latex_dir / Path(raw_src).name, # 只取文件名,在 latex_source/ 下找
] + list(latex_dir.glob(f"**/{Path(raw_src).name}")) # 递归搜索
src = next((p for p in candidates if p.exists()), None)
if not src:
return None
caption = fig.get("caption", "")
words = re.findall(r'[a-zA-Z]+', caption)[:3]
slug = "_".join(w.lower() for w in words) or "fig"
dst = paper_dir / "images" / f"figure{idx}_{slug}.png"
ext = Path(src).suffix.lower()
size_mb = Path(src).stat().st_size / 1024 / 1024
if ext == ".pdf":
if size_mb > 20:
return {"index": idx, "skipped": True,
"reason": f"超大图({size_mb:.0f}MB)", "caption": caption}
r = subprocess.run(
["pdftoppm", "-r", "150", "-png", "-singlefile", src, str(dst)[:-4]],
capture_output=True
)
if r.returncode != 0 or not dst.exists():
subprocess.run(["convert", "-density", "150", f"{src}[0]", str(dst)])
elif ext in (".eps", ".ps"):
subprocess.run(["convert", "-density", "150", src, str(dst)])
elif ext in (".png", ".jpg", ".jpeg", ".gif"):
shutil.copy2(src, dst)
if not dst.exists():
return {"index": idx, "skipped": True, "reason": "转换失败", "caption": caption}
return {"index": idx, "file": str(dst),
"filename": dst.name, "caption": caption,
"size_kb": dst.stat().st_size // 1024}
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool:
results = list(pool.map(convert_figure, figures))
converted = [r for r in results if r and not r.get("skipped")]
skipped = [r for r in results if r and r.get("skipped")]
A4.5. TikZ 图补全(PDF 截图方案)
背景:arXiv 论文中有些图是用 LaTeX TikZ 代码直接绘制的,不是 \includegraphics 引用的文件。extract_tex.py 只提取 \includegraphics 图,TikZ 图不会出现在 figures 列表中,但它们往往是最重要的架构图和流程图。
触发条件:extracted_text.md 中出现 **Figure N**: 引用(说明正文里提到了这张图),但 images/ 目录里没有对应文件。
检测代码:
import re
from pathlib import Path
mentioned_figs = set(re.findall(r'\*\*Figure (\d+)\*\*', paper_text))
extracted_ids = set(str(r["index"]) for r in converted)
missing_ids = mentioned_figs - extracted_ids
if missing_ids:
print(f"⚠️ 检测到 {len(missing_ids)} 张 TikZ 图(Figure {sorted(missing_ids)}),启动 PDF 截图补全")
PDF 截图流程:
import subprocess
from pathlib import Path
if missing_ids:
pdf_path = "/tmp/arxiv_paper.pdf"
subprocess.run(
["curl", "-sL", f"https://arxiv.org/pdf/{arxiv_id}", "-o", pdf_path],
check=True
)
subprocess.run(
["pdftoppm", "-r", "150", "-png", pdf_path, "/tmp/arxiv_page"],
check=True
)
page_files = sorted(Path("/tmp").glob("arxiv_page-*.png"))
print(f"PDF 共 {len(page_files)} 页")
from PIL import Image
for fig_id in sorted(missing_ids, key=int):
target_idx = min(int(fig_id), len(page_files) - 1)
src_page = page_files[target_idx]
img = Image.open(src_page)
w, h = img.size
cropped = img.crop((0, 0, w, int(h * 0.5)))
dst = paper_dir / /
cropped.save(dst)
cap_match = re.search(
,
paper_text
)
caption = cap_match.group().strip() cap_match
converted.append({
: (fig_id),
: (dst),
: dst.name,
: caption,
: dst.stat().st_size // ,
:
})
()
f Path().glob():
f.unlink(missing_ok=)
Path(pdf_path).unlink(missing_ok=)
注意事项:
pdftoppm 来自 poppler-utils(macOS 用 brew install poppler);不可用时用 convert -density 150 paper.pdf[{page_idx}] output.png(ImageMagick)
Pillow 需已安装:pip install pillow
- 截图默认取上半页,论文图通常在页面上方;如果截出来有问题,调整
0.5 比例(例如改 0.4 或 0.6)
- TikZ 截图会包含 figure caption 文字,这是正常的,帮助读者理解图意
- 不要用 AI 生成图替代 TikZ 图:架构图、流程图必须用论文原图,它们是作者精心设计的,随意替换会误导读者
A5. 生成 figure_list.md
# 论文图表清单
## 可用图表(建议全部引用)
| 编号 | 文件 | 大小 | Caption 摘要 | 建议章节 |
|------|------|------|-------------|---------|
| Figure 1 | figure1_teaser.png | 2.1MB | 整体效果展示 | 开头引入 |
| Figure 2 | figure2_overview.png | 746KB | 系统架构图 | 方法解读 |
| ...
## 跳过的图
| 编号 | 原因 | Caption 摘要 |
|------|------|-------------|
| Figure 3 | 超大图(84MB) | 定性对比图 |
完成后:更新 Todo A 为 completed,B 为 in_progress。
步骤B:生成乔木风格解读文章
B0. 写作前准备(必须完整执行)
第一步:读取完整论文内容 + 当前日期
import datetime
paper_text = (paper_dir / "extracted_text.md").read_text(encoding="utf-8")
figure_list = (paper_dir / "figure_list.md").read_text(encoding="utf-8")
metadata = json.loads((paper_dir / "metadata.json").read_text(encoding="utf-8"))
today = datetime.date.today()
pub_date = metadata.get("published_date", "")
years_since = (today.year - int(pub_date[:4])) if pub_date else 0
print(f"论文发布于 {pub_date},距今 {years_since} 年({today})")
当前日期的用途:
- 在"写在后面"中具体说出"这篇论文发表于 X 年,距今 Y 年",不用模糊说"几年前"
- 主动调用训练知识,找出这篇论文发布后出现的重要后继工作:哪些论文直接引用并发展了它的思路?哪些产品落地了它的方法?
- 如果论文发布时间超过 2 年,必须在"写在后面"或相关章节自然提到后续影响(不是列表,而是融入正文),例如"2 年后 Stable Diffusion 用的正是 CLIP 做图文对齐"这样的具体陈述
- 知识截止日期内未发生的事不要猜测,只写确实知道的
第二步:自动判断是否为里程碑论文(决定字数下限)
LANDMARK_KEYWORDS = {
"transformer", "attention is all you need",
"bert", "gpt", "gpt-2", "gpt-3", "gpt-4",
"rlhf", "reinforcement learning from human feedback",
"instruct", "instructgpt",
"chain-of-thought", "chain of thought",
"in-context learning", "scaling laws",
"codex", "alphacode", "word2vec", "seq2seq",
"clip", "dall-e", "imagen", "stable diffusion",
"diffusion", "ddpm", "gan", "generative adversarial",
"vae", "variational autoencoder",
"resnet", "vit", "vision transformer",
"alexnet", "vgg", "googlenet", "inception",
"densenet", "efficientnet", "mobilenet",
"batch normalization",
"two-stream", "two stream", "i3d", "slowfast",
"optical flow", "action recognition",
, , , ,
, ,
, , ,
,
, , , ,
}
title_lower = metadata.get(, ).lower()
is_landmark = (kw title_lower kw LANDMARK_KEYWORDS)
min_words = is_landmark
()
第三步:阅读写作规范(见下方 B0 写作风格)
B0. 乔木写作风格(内嵌,无需读取外部文件)
语言特质
- 口语化、对话感强,像和读者面对面聊天
- 用"你"直接称呼读者
- 生活化类比触发规则:每讲完一个核心方法/设计决策的技术解释后,必须紧跟一个类比段落。全文 ≥ 3 处,分散在不同章节。
- 类比质量标准:类比必须同时做到两点:① 用一个具体的日常场景(不是抽象描述),② 包含"如果不这样做会怎样"的反事实,让类比揭示的是这个设计决策的代价和收益,而不只是装饰性的比喻。
- 禁止的类比:太宽泛的比喻("就像用地图导航")、无法推出设计必要性的比喻、和技术原理对不上的类比,这些不算数,必须重写。
- 在专业性和可读性之间自然平衡
表达习惯
- 短段落,多留白,视觉舒适
- 重要观点用 加粗,加粗句必须单独成段,不和其他句子同处一段
- 加粗句之前和之后都留空行
- 引用块触发规则:每当文章中出现一个新的专有名词、技术术语、缩写时,立刻在其后加引用块解释,不要等写完再补。格式:
> **术语**:一句话解释。全文累计 ≥ 10 处
- 引用块间距规则:两个引用块之间必须至少隔一个正文段落。如果连续出现了多个新术语,把解释合并到同一个引用块里(用换行分隔),不要连续放多个独立引用块
- 冒号后接长内容,冒号后另起段落
- 三条以上并列经验,用列表
内容层次
- 不满足于表面解释,延伸到更深的思考
- 善于在不同领域间建立联系(技术→生活→认知)
- 既讲"是什么",也讲"为什么重要"
- 每段通过「增量信息测试」:这段提供了前面没有的新信息吗?
风格调性
- 真诚、不装、承认自己的困惑
- 专业但不掉书袋,数据和案例支撑观点
- 批判性反思融入正文流,不辟独立章节
让文章"生动有趣"的具体技法(每篇至少用 3 种)
-
开头用悬念或反直觉事实:不是"本文提出了X方法",而是"2014 年,有一件让所有人困惑的事……"或"你可能不知道,深度学习曾经有整整两年打不过一个叫做'密集轨迹'的老方法"。
- 使用前提:这个"反直觉"必须是真的,读者读完之后会说"啊确实,我之前没想到"。如果你说的其实是领域常识,或者打了"反直觉"的标签但内容很平,给读者带来的是被欺骗感。宁可不写反直觉,也不要凑一个假的。
- 判断方法:写完这个反直觉事实后,问自己:一个对该领域有基本了解的读者,在读到这句话之前,他的默认预期是什么?如果这句话真的和他的预期相反,才算数。
-
写研究者的困境和直觉:不只说方法,要写"为什么他们会想到这个"。如果能推测研究者当时的思路(有据可查),大胆写出来
-
反事实推理:在讲完一个设计决策后,追问"如果不这样做会怎样"。例如"如果只用空间流,会发生什么?实验数据告诉我们:准确率从 88% 掉到 72.6%,整整少了 15 个点"
-
具体化数字场景:把抽象数字变成可感受的场景。"20 个百分点的差距,意味着什么?意味着每 5 个动作里,深度学习就要比手工方法多认错 1 个"
-
图片叙事:不只是插图,要描述图里能看到什么、这说明了什么。"看这张第一层卷积核的图,你会发现它们大多是方向性的滤波器……这不是巧合,这是网络自己学出来的,它发现光流场里最重要的信息就是方向"
-
人物和机构背景(仅限有公开信息的内容):Simonyan 和 Zisserman 是同一个 VGG 组的,他们几个月后发表了 VGGNet——两篇论文是同期的工作。这个细节本身就是故事
-
历史节点感:点明这篇论文发表的时刻在历史上的意义。"这是深度学习在视频动作识别上第一次赢过手工特征——在 2014 年,这句话的分量不亚于 AlexNet 横空出世"
B1. 硬性禁止清单(写完必须逐条自检)
| 禁止项 | 上限 | 替换方式 |
|---|
| 破折号(——) | 0 个 | 逗号、句号、冒号 |
| 总之 / 综上所述 / 综上 | 0 个 | 直接写结论 |
| 让我们 / 让我们来拆解 | 0 个 | 直接陈述 |
| 关键在于 / 关键来了 | 0 个 | 直接说关键点 |
| 想象一个世界 | 0 个 | — |
| 不是X而是Y | 最多 1 次 | — |
| 值得注意的是 / 重要的是 / 有趣的是 | 0 个 | 删掉,直接说 |
| delve / landscape / tapestry / robust / leverage | 0 个 | 用简单词 |
| 结尾写"总结" / 总结一下 | 0 个 | 画面或问题收尾 |
| 每个列表项都粗体开头 | 禁止 | 粗体只用于真正重点 |
| 连续的引用块(相邻无正文段落) | 禁止 | 合并到同一个引用块,或中间插入一句正文过渡 |
| 碎片式短句独立成段制造假强调 | 禁止 | 合并为一句 |
| 预告式渲染:"最震撼的部分"/"一针见血" | 禁止 | 删掉,直接呈现内容 |
| "不只是X,更是Y" 排比式拔高 | 禁止 | 直接说那个"Y"是什么 |
| "不只是一个工程方案,更是一种思维方式" 类套话 | 禁止 | 没有信息量,删掉 |
B2. 文章结构
文章由两部分组成:正文(主体解读)+ 写在后面(意义总结)。
正文结构
1. 开头场景引入(不直接说"这篇论文...",用具体场景)
2. 核心问题(这件事难在哪?)
3. 方法解读(每个核心贡献一个 H2 节)
- 是什么 → 为什么这么设计 → 生活化类比(必须有)
- 新术语出现立刻加引用块
- 插入对应论文原图
4. 数据表格(核心实验结果,加粗最佳值)
字数要求:
- 普通方法论论文:≥ 5000 汉字
- 里程碑论文:≥ 8000 汉字
- 由 B0 第二步的
is_landmark 自动判断,覆盖关键词见上方列表
- 写完必须自检:
grep -oP '[\x{4e00}-\x{9fff}]' article.md | wc -l,低于 min_words 则继续扩充,不得以"文章完成"为由停笔
每个 H2 节必须包含的四要素(缺一不可):
- 技术解释:这个设计是什么,为什么这样设计(不只是"是什么")
- 生活化类比:用日常场景说明这个技术决策,每节 ≥ 1 个,全文 ≥ 3 个
- 论文原图:插入对应 figure,并用 1-2 句话解释图里能看到什么
- 意义追问:这个设计决策解决了什么根本矛盾?如果不这样做会怎样?
历史现场要求(论文发表 > 2 年时强制)**:
- 在"核心问题"节或第一个 H2 节中,必须描述该论文发表时领域的具体状态:当时最强的方法是什么、准确率是多少、为什么大家认为这是极限
- 用具体数字说话,不说"当时方法不好",要说"当时最好的方法只有 X%,而手工特征达到 Y%,差了 Z 个百分点"
- 至少提及 1 个这篇论文发表前的代表性工作作为对照基准
影响链要求(写在后面)**:
- 必须追溯 ≥ 2 篇直接建立在本论文基础上的后续工作,用具体年份和改进点说明
- 格式参考:"3 年后,X 团队的 Y 论文把这个思路扩展到……,准确率提升到……"
- 禁止只说"影响了后来的研究",必须说出具体是哪篇论文、做了什么改变
写在后面(必须包含,放文章末尾正文之前)
这一节的核心标准只有一个:必须给读者带来正文里没有的新信息增量,或者一个真实的感悟、启发、乐趣。不是正文的复述,不是宏观意义的拔高,不是鸡汤。
可以写的内容(选其中有货的,不必全写):
- 读到这篇论文时,有什么具体的想法被触发了?(必须是具体的,不是"很有启发")
- 这个方法打破了什么你原来以为是常识的东西?(如果有的话)
- 论文里有什么细节,值得单独拿出来说一说?(比如某个反直觉的实验结果)
- 这个思路让你联想到什么完全不同领域的东西?(只有联想是真实的才写)
- 这篇论文还没解决的问题是什么?值得追问吗?
格式规范:
- 用
## 写在后面 作为节标题
- 150-300 字,短而有料,不要为了凑字数而展开
- 不强制用"我",但语气要是真实的,不是论文腔
- 结尾可以是一个开放性问题,但必须是你真正想问的,不是套话式的"未来值得期待"
- 禁止破折号、禁止"总之"、禁止"不只是X更是Y"这类排比
禁止的写法举例:
- "这篇论文不只是一个工程方案,更是一种思维方式" ← 套话,没有信息量
- "Lighthouse 的贡献对领域影响深远" ← 废话,读者已经知道了
- "未来的研究方向值得关注" ← 永远可以套用,等于没说
- 把正文已经说过的结论再说一遍 ← 没有新增量,直接删掉
结尾升华(紧接"写在后面"之后)
用一个让读者脑子停不下来的画面、故事或问题作为最后一段,不归纳、不总结。
完整顺序:
论文信息引用块(开头)→ 正文主体 → ## 写在后面 → 结尾升华段落
论文信息引用块(仅开头放一次,固定格式)
- 开头:紧跟在 H1 标题之后,正文第一段之前。让读者一眼看到论文出处。
- 末尾不再重复:结尾是画面或问题收尾,不加信息块。
所有字段均来自 metadata.json,禁止凭记忆填写。
⚠️ 换行规则:每行之间必须加空的 > 行,否则 Markdown 渲染器会把所有行合并成一段:
> **论文原文**:{完整英文标题}
>
> **arXiv**:https://arxiv.org/abs/{arxiv_id}
>
> **发布日期**:{published_date}
>
> **作者**:{前三位作者} et al.({机构})
注意:
- 不要在发布日期后加任何括号注释(如"来自 arXiv API,非推测"),直接写日期即可
- 如果
published_date 为空,写 未能获取 即可,不要猜测
B3. 图片引用策略
原则:所有已成功转换的图,都应在文章中找到对应位置引用。
- 读取
figure_list.md,了解所有可用图及其建议章节
- 写每个章节时,主动匹配并插入对应图片
- 不要集中堆放,每张图紧跟在最相关的段落之后
引用格式(必须用标准 Markdown,禁止用 Obsidian wiki 格式):
 ✅ 正确
![[figure2_overview.png]] ❌ 禁止
为什么禁止 wiki 格式:Obsidian 渲染 ![[xxx]] 没问题,但发布到博客时这种格式既不会被图片上传逻辑识别,也无法被 markdown 渲染器解析,结果就是博客上一堆裂图。文章在 Obsidian 里也能正常显示标准 Markdown 格式(路径相对于文章所在目录),所以永远用标准格式。即便文章只准备本地阅读,也按标准格式写,避免日后想发布时再返工。
图片-章节映射参考:
- teaser / result 展示图 → 开头引入或结尾
- overview / architecture 图(含 TikZ 截图的 fig1_tikz.png 类型)→ 方法总览节,紧跟在介绍整体架构的段落之后
- 模块细节图(含 TikZ 截图的 fig2_tikz.png 类型)→ 对应具体方法节,讲到该模块时插入
- comparison / ablation 图 → 实验数据节
- user study / visualization → 数据分析节
TikZ 截图图片的引用说明:文章正文中正常引用,图名用图的实际内容命名而不是 tikz(例如 )。读者看到的是正常论文图,无需知道截图来源。
B4. 写作后自检(必须执行,不合格必须修改后才能保存)
import re
text = open(article_path, encoding="utf-8").read()
chinese_count = len(re.findall(r'[\u4e00-\u9fff]', text))
checks = [
(len(re.findall(r'——', text)) == 0, f"破折号: {len(re.findall(r'——', text))} 个(必须为0)"),
(len(re.findall(r'总之|综上|让我们|关键在于|值得注意', text)) == 0,
"禁用词检查(必须为0)"),
(len(re.findall(r'> \*\*', text)) >= 10, f"术语引用块: {len(re.findall(r'> \*\*', text))} 个(≥10)"),
(len(re.findall(r'!\[', text)) >= 3, f"图片引用: {len(re.findall(r'!\[', text))} 张(≥3)"),
('## 写在后面' in text, "写在后面节(必须有)"),
(text.count('> **论文原文**') >= 1, "论文信息引用块(开头一次)"),
(chinese_count >= min_words, f"汉字数: {chinese_count}(要求 ≥ {min_words})"),
]
all_pass = True
for ok, msg in checks:
status = "✅" if ok else "❌"
print(f"{status} ")
ok:
all_pass =
all_pass:
()
:
()
保存到 {paper_dir}/{中文标题}_解读.md
完成后:更新 Todo B 为 completed,C 为 in_progress。
步骤C:生成 AI 配图
前提:IMAGE_PROVIDER != "skip",否则跳过本步骤,直接进入步骤D。
C1. 封面图
提炼 1-3 个核心关键词(如 MACE 音乐驱动舞蹈 级联专家):
mkdir -p "{paper_dir}/illustrations"
cat > "{paper_dir}/illustrations/visual_config_cover.json" << EOF
{
"task_id": "cover_{paper_id}",
"cover": {
"enabled": true,
"filename": "cover.png",
"style": "paper-watercolor-cover",
"aspect_ratio": "16:9",
"description": "{关键词1} {关键词2} {关键词3}"
},
"illustrations": [],
"defaults": {
"style": "paper-watercolor-cover",
"provider": "{IMAGE_PROVIDER}",
"retry_count": 2
}
}
EOF
python "{IMAGE_GENERATOR_SCRIPT}" \
"{paper_dir}/illustrations/visual_config_cover.json" --workers 1
python3 -c "
content = open('{article_path}').read()
open('{article_path}', 'w').write('\n\n' + content)
"
C2. 纽约客配图(3线程并发)
为每个主要 H2 节设计 visual_description(50-80字中文,具象场景隐喻抽象概念,不写风格指令):
{
"task_id": "illustrations_{paper_id}",
"cover": { "enabled": false },
"illustrations": [
{
"id": "01",
"h2_title": "对应H2标题",
"visual_description": "50-80字具象场景描述",
"filename": "01-slug.png"
}
],
"defaults": {
"style": "newyorker",
"provider": "{IMAGE_PROVIDER}",
"retry_count": 2
}
}
python "{IMAGE_GENERATOR_SCRIPT}" \
"{paper_dir}/illustrations/visual_config.json" --workers 3
完成后:更新 Todo C 为 completed,D 为 in_progress。
步骤D:保存 + 打开
D1. 复制到阅读目录
import shutil
from pathlib import Path
article_path = paper_dir / f"{chinese_title}_解读.md"
images_dir = paper_dir / "images"
dest_dir = READING_DIR
dest_images = dest_dir / f"{paper_id}_images"
dest_dir.mkdir(parents=True, exist_ok=True)
dest_images.mkdir(parents=True, exist_ok=True)
content = article_path.read_text(encoding="utf-8")
content = content.replace("images/", f"{paper_id}_images/")
(dest_dir / article_path.name).write_text(content, encoding="utf-8")
if images_dir.exists():
for img in images_dir.glob("*.png"):
shutil.copy2(img, dest_images / img.name)
print(f"已复制到: {dest_dir / article_path.name}")
D2. 在 Obsidian 中打开(仅当 OBSIDIAN_VAULT 非空)
import os, urllib.parse
from pathlib import Path
vault = os.environ.get("OBSIDIAN_VAULT", "")
if vault:
reading_dir = Path(os.environ.get("PAPER_READING_DIR", "~/Papers/reading")).expanduser()
dest_file = reading_dir / article_path.name
vault_root = None
for parent in dest_file.parents:
if parent.name == vault:
vault_root = parent
break
if vault_root:
rel = dest_file.relative_to(vault_root)
encoded = urllib.parse.quote(str(rel).replace(".md", ""), safe="/")
else:
encoded = urllib.parse.quote(article_path.stem, safe="")
uri = f"obsidian://open?vault={urllib.parse.quote(vault)}&file={encoded}"
os.system(f'open "{uri}"')
D3. 发布到博客
触发条件:用户输入包含"发布"/"博客"/"blog"/"post"时,本步骤必须自动执行,不询问用户。
上传本地图片并替换路径,然后发布(status 固定为 draft):
⚠️ API 调用统一走 curl(subprocess)。博客 API 会用 User-Agent 拦截 Python 默认 urllib(返回 403 Forbidden),requests 库带的 UA 也不稳定。下面所有 HTTP 请求都用 curl 子进程,不要换成 urllib/requests。
import json, re, subprocess
from pathlib import Path
TOKEN = open(Path("~/.claude/skills/qiaomu-blog-publish/config.json").expanduser()).read()
TOKEN = json.loads(TOKEN)["token"]
content = article_path.read_text(encoding="utf-8")
title_match = re.search(r'^# (.+)$', content, re.MULTILINE)
title = title_match.group(1).strip()
body = content[title_match.end():].lstrip('\n')
def _wiki_to_md(m):
name = m.group(1).strip()
base = Path(name).name
if (article_path.parent / "images" / base).exists():
return f""
return m.group(0)
body = re.sub(
r'!\[\[([^\]]+\.(?:png|jpg|jpeg|gif|webp|svg))\]\]',
_wiki_to_md, body, flags=re.IGNORECASE
)
def upload_image(abs_path):
for _ in range(2):
r = subprocess.run(["curl", "-s", "-X", "POST",
"https://blog.qiaomu.ai/api/uploads",
, ,
, ], capture_output=, text=, timeout=)
:
resp = json.loads(r.stdout)
resp.get() resp.get():
resp[]
Exception:
concurrent.futures
local_images = [
(m[], m[], article_path.parent / m[])
m re.findall(, body)
]
local_images = [(alt, img_path, abs_path)
alt, img_path, abs_path local_images abs_path.exists()]
():
alt, img_path, abs_path = item
url = upload_image(abs_path)
img_path, url
concurrent.futures.ThreadPoolExecutor(max_workers=) pool:
img_path, url pool.(upload_one, local_images):
url:
body = body.replace(, )
_model_words = re.findall(, title)
_model_slug = .join(w.lower() w _model_words[:]) _model_words
_domain_words = [w w paper_id.lower().replace(, ).split()
(w) > w (,,,,,,,,,,,,,)]
_domain_slug = .join(_domain_words[:])
_model_slug:
seo_slug = [:]
:
seo_slug = _domain_slug[:]
seo_slug = re.sub(, , seo_slug).strip()
_lines = body.split()
_i, _line (_lines):
_line.startswith():
_lines[_i]
_i < (_lines) _lines[_i].strip() == :
_lines[_i]
body = .join(_lines)
payload = json.dumps({
: title, : body,
: seo_slug,
: , :
}, ensure_ascii=)
r = subprocess.run([, , , ,
,
, ,
, ,
, payload], capture_output=, text=)
resp = json.loads(r.stdout)
slug = resp.get(, )
slug:
()
()
()
()
:
()
D4. 完成报告
✅ 论文解读完成!
📄 标题:{h1_title}
📝 字数:约 X 字
🖼️ 原文图表:N 张引用 / M 张转换(K 张超大跳过)
🎨 封面:已生成 / 已跳过(IMAGE_PROVIDER=skip)
🎭 配图:N 张 / 已跳过
📁 {paper_dir}/
📖 已在 Obsidian 中打开 / Obsidian 未配置,请手动打开
🌐 博客草稿:https://blog.qiaomu.ai/editor?slug={slug}(如触发了发布)
常见问题处理
arXiv 来源为 HuggingFace 链接
先 WebFetch 页面,找到 arxiv.org 链接,再传入 extract_tex.py。
LaTeX 源码不可用(论文未上传 arXiv)
markitdown <pdf_url_or_path> -o {paper_dir}/extracted_text.md
此情况下无法提取真实图表,figure_list.md 标注"不可用",文章中用文字描述代替图片引用。
extract_tex.py 不存在
该脚本已内置于本 skill 的 scripts/extract_tex.py。如文件缺失,重新克隆本 skill 即可。
直接使用 markitdown fallback 亦可。
论文图是 TikZ 代码,extract_tex.py 没有提取到
这是正常现象。extract_tex.py 只提取 \includegraphics 引用的文件;TikZ 图是 LaTeX 代码绘制的矢量图,没有对应的图片文件。
检测方法:在 extracted_text.md 里搜索 **Figure N**:,如果某张图有文字描述但 images/ 目录里没有文件,就是 TikZ 图。
解决方案:运行 A4.5 的 PDF 截图流程,或手动执行:
curl -sL "https://arxiv.org/pdf/{arxiv_id}" -o /tmp/paper.pdf
pdftoppm -r 150 -png -f 3 -l 3 /tmp/paper.pdf /tmp/paper_page
python3 -c "
from PIL import Image
img = Image.open('/tmp/paper_page-03.png')
w, h = img.size
img.crop((0, 0, w, int(h*0.5))).save('{paper_dir}/images/fig1_architecture.png')
"
页面编号从 1 开始;Figure 1 通常在第 2-4 页,Figure 2 在第 3-5 页,具体看论文结构。
超大图(>20MB)需要强制转换
调低分辨率:
pdftoppm -r 72 -png -singlefile src.pdf dst
图片生成失败
如果 IMAGE_PROVIDER 配置了但生成失败,跳过配图步骤,保存纯文章版本,在报告中说明。
质量检查清单(保存前强制)
参考文档
- LaTeX 提取:
scripts/extract_tex.py(已内置,自包含)
- 图片生成:
~/.agents/skills/qiaomu-image-generator/scripts/generate.py(可替换,需配置 IMAGE_GENERATOR_SCRIPT)
- 博客发布 token:
~/.claude/skills/qiaomu-blog-publish/config.json({"token":"qm_xxx"})
- 写作风格:已内联到本 skill(步骤 B0),无需外部依赖
- 风格指南:
references/style-guide.md
- 使用示例:
examples.md
- 故障排查:
TROUBLESHOOTING.md
- 配图设计:
visual_description_guide.md
- 版本历史:
CHANGELOG.md