
Arxiv Reader
- 8 installs
- 33 repo stars
- Updated April 26, 2026
- bighardperson/computer-science-skills-collection
arxiv-reader is a skill that uses an LLM agent to classify and deep-read a specified ArXiv paper and print reading notes.
About
This skill takes an ArXiv paper id or URL and uses an LLM agent to classify and deep-read the paper, printing reading notes. It runs a classifier plus reader agents whose behavior is defined by per-category prompt folders. A developer uses it to get structured notes on a research paper without reading it manually.
- Classifies and deep-reads a single ArXiv paper by id or URL using an LLM agent
- Prints reading notes directly; category-based reading prompts are extensible via folders
- Runs on Python with uv; requires an OpenAI-compatible LLM API key
Arxiv Reader by the numbers
- 8 all-time installs (skills.sh)
- Ranked #12,321 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
arxiv-reader capabilities & compatibility
Requires an OpenAI-compatible LLM_API_KEY and LLM_BASE_URL; LLM usage is billed to the user's key.
- Works with
- openai
- Use cases
- research
- Runs
- Runs locally
- Pricing
- Bring your own API key
What arxiv-reader says it does
利用python,指定某个arxiv_id/url, 基于 LLM Agent 对这篇arxiv论文进行分类与深度阅读,直接print打印阅读笔记
`LLM_API_KEY` — OpenAI 或兼容 API 的密钥
在 `skills/` 下新建文件夹,包含两个文件:
npx skills add https://github.com/bighardperson/computer-science-skills-collection --skill arxiv-readerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| repo stars | ★ 33 |
| Last updated | April 26, 2026 |
| Repository | bighardperson/computer-science-skills-collection ↗ |
What it does
Classify and deep-read a specific ArXiv paper by id or URL with an LLM agent, printing structured reading notes.
Who is it for?
Researchers who want an LLM to classify and produce structured reading notes for a specific ArXiv paper.
Skip if: Users without an OpenAI-compatible LLM API key, since it requires LLM_API_KEY and LLM_BASE_URL.
When should I use this skill?
you have an ArXiv id or URL and want an agent to classify and deep-read it into notes.
By the numbers
- Two files per category folder (_metadata.md and reading_prompt.md)
Files
快速开始
1. 配置 .env
cp .env.example .env # 或直接编辑 .env确定你已经配置了:
LLM_API_KEY— OpenAI 或兼容 API 的密钥LLM_BASE_URL— API 地址
2. 运行
uv venv
uv pip install -r "{baseDir}/requirements.txt"
# 单篇论文模式:指定 arxiv_id 或 URL
uv run python "{baseDir}/main.py" --arxiv-id 2401.12345
uv run python "{baseDir}/main.py" --arxiv-id https://arxiv.org/abs/2401.12345
uv run python "{baseDir}/main.py" --arxiv-id https://arxiv.org/pdf/2401.12345.pdf
# 指定以特定类别阅读
uv run python "{baseDir}/main.py" --arxiv-id xxxx --category yyy
# 查看所有类别
uv run python "{baseDir}/main.py" --list
添加新的阅读分类
在 skills/ 下新建文件夹,包含两个文件:
skills/your_new_category/
├── _metadata.md # 分类描述(告诉 Classifier 什么论文属于这个类别)
└── reading_prompt.md # 阅读指南(告诉 Reader Agent 重点关注什么)重启即可自动识别,无需修改任何代码。
Python包
- LangChain 1.x — Agent 框架(基于 LangGraph)
- LangChain OpenAI — LLM 接口(兼容 DeepSeek 等 OpenAI-compatible API)
- arxiv — 官方 Python 库
- arxiv-to-prompt 获取arxiv论文latex源码
"""
Agent Factory — creates LangChain agents / chains.
Uses `create_tool_calling_agent` when tools are supplied,
otherwise returns a simple prompt | llm chain.
"""
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_agent as create_langchain_agent
import config
from typing import Any
# ── LLM singleton cache ──────────────────────────────────────
_llm_cache: dict[str, ChatOpenAI] = {}
def get_llm(
temperature: float | None = None,
max_tokens: int | None = None,
model: str | None = None,
) -> ChatOpenAI:
"""Return a (cached) ChatOpenAI instance."""
t = temperature if temperature is not None else config.LLM_TEMPERATURE
m = max_tokens or config.LLM_MAX_TOKENS
mdl = model or config.LLM_MODEL
key = f"{mdl}_{t}_{m}"
if key not in _llm_cache:
_llm_cache[key] = ChatOpenAI(
base_url=config.LLM_BASE_URL,
api_key=config.LLM_API_KEY,
model=mdl,
temperature=t,
max_tokens=m,
)
return _llm_cache[key]
def create_agent(
system_prompt: str,
tools: list | None = None,
temperature: float | None = None,
max_tokens: int | None = None,
checkpointer: Any | None = None,
):
"""
Create a LangChain agent.
- With tools → AgentExecutor (tool-calling agent)
- Without tools → simple prompt | llm chain (invoke with {"messages": ...})
"""
llm = get_llm(temperature=temperature, max_tokens=max_tokens)
agent_graph = create_langchain_agent(
model=llm,
tools=tools,
system_prompt=system_prompt,
checkpointer=checkpointer
)
return agent_graph
"""
Classifier Agent — decides which skill category a paper belongs to.
"""
from __future__ import annotations
import json
import re
from typing import Any, Dict
import config
from agents.base_agent import create_agent
from skills.loader import get_categories_description
from utils.logger import get_logger
logger = get_logger(__name__)
PROMPTS_DIR = config.PROJECT_ROOT / "prompts"
def _load_prompt(filename: str) -> str:
path = PROMPTS_DIR / filename
return path.read_text(encoding="utf-8")
CLASSIFIER_SYSTEM_PROMPT = _load_prompt("classifier_system.md")
class ClassifierAgent:
"""Classify papers into skill categories."""
def __init__(self, skills: Dict[str, Any]):
self.skills = skills
categories_desc = get_categories_description(skills)
self.category_names = [
name for name in skills.keys() if name != "general"
]
system_prompt = CLASSIFIER_SYSTEM_PROMPT.format(
categories=categories_desc
)
self.chain = create_agent(system_prompt, temperature=0.1)
def classify(self, title: str, abstract: str) -> str:
"""Return the category name for a single paper."""
user_input = (
f"请对以下论文进行分类:\n\n"
f"**标题**: {title}\n\n"
f"**摘要**: {abstract}"
)
try:
result = self.chain.invoke({"messages": [{"role": "user", "content": user_input}]})
text = result["messages"][-1].content
parsed = self._parse_json(text)
category = parsed.get("category", "general")
confidence = float(parsed.get("confidence", 0))
if category not in self.category_names or confidence < 0.4:
category = "general"
logger.info(
f" [{category}] (conf={confidence:.2f}) {title[:60]}..."
)
return category
except Exception as e:
logger.warning(f"Classification failed for '{title[:50]}': {e}")
return "general"
# ------------------------------------------------------------------
def classify_batch(self, papers: list[dict]) -> dict[str, str]:
"""Classify a list of papers. Returns {arxiv_id: category}."""
results: dict[str, str] = {}
for paper in papers:
cat = self.classify(paper["title"], paper["abstract"])
results[paper["arxiv_id"]] = cat
return results
@staticmethod
def _parse_json(text: str) -> dict:
"""Robustly extract JSON from LLM output."""
# Try direct parse
text = text.strip()
# Remove possible ```json ... ``` wrapping
text = re.sub(r"^```(?:json)?\s*", "", text)
text = re.sub(r"\s*```$", "", text)
try:
return json.loads(text)
except json.JSONDecodeError:
# Try to find JSON object in text
match = re.search(r"\{.*\}", text, re.DOTALL)
if match:
return json.loads(match.group())
return {"category": "general", "confidence": 0, "reasoning": "parse error"}
"""
Reader Agent — performs two-pass deep reading of a full paper.
Pass 1: Abstract + Introduction + Preliminaries + Contributions + Limitations
→ initial summary
Pass 2: Initial summary + main body (excluding refs/appendix/Pass-1 sections)
→ detailed notes, plus a decision on whether to read the appendix
Pass 3 (optional): Previous notes + appendix → updated notes
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import Any, Dict
from agents.base_agent import create_agent
import config
from paper_reader.latex_parser import ParsedPaper
from utils.helpers import truncate_text
from utils.logger import get_logger
logger = get_logger(__name__)
PROMPTS_DIR = config.PROJECT_ROOT / "prompts"
def _load_prompt(filename: str) -> str:
path = PROMPTS_DIR / filename
return path.read_text(encoding="utf-8")
# ── Prompt templates ──────────────────────────────────────────
READER_SYSTEM_PROMPT = _load_prompt("reader_system.md")
FIRST_PASS_USER = _load_prompt("reader_first_pass_user.md")
SECOND_PASS_USER = _load_prompt("reader_second_pass_user.md")
APPENDIX_PASS_USER = _load_prompt("reader_appendix_pass_user.md")
class ReaderAgent:
"""Two-pass deep reading agent for a specific skill category."""
def __init__(self, skill_name: str, skill_config: Dict[str, Any]):
self.skill_name = skill_name
self.reading_prompt = skill_config.get("reading_prompt", "")
system_prompt = READER_SYSTEM_PROMPT.format(
reading_prompt=self.reading_prompt
)
self.chain = create_agent(system_prompt, max_tokens=16000)
# ------------------------------------------------------------------
def read_paper(
self, paper_info: dict, parsed_paper: ParsedPaper
) -> str:
"""
Execute the full reading pipeline and return Markdown notes.
"""
title = paper_info["title"]
authors = ", ".join(paper_info.get("authors", []))
arxiv_id = paper_info["arxiv_id"]
# ── Pass 1 ─────────────────────────────────────────
logger.info(f" [Pass 1] {title[:60]}...")
first_pass_text = truncate_text(parsed_paper.first_pass_text, 30000)
user_msg_1 = FIRST_PASS_USER.format(
title=title,
authors=authors,
arxiv_id=arxiv_id,
first_pass_content=first_pass_text,
)
result_1 = self.chain.invoke({"messages": [{"role": "user", "content": user_msg_1}]})
initial_summary = result_1["messages"][-1].content
# ── Pass 2 ─────────────────────────────────────────
logger.info(f" [Pass 2] {title[:60]}...")
main_body = truncate_text(parsed_paper.main_body_text, 50000)
if not main_body.strip():
# Paper has no extractable main body → return Pass 1 result
return self._format_final_notes(paper_info, initial_summary)
user_msg_2 = SECOND_PASS_USER.format(
initial_summary=initial_summary,
main_body=main_body,
)
result_2 = self.chain.invoke({"messages": [{"role": "user", "content": user_msg_2}]})
detailed_notes = result_2["messages"][-1].content
# ── Pass 3 (optional: appendix) ────────────────────
if self._should_read_appendix(detailed_notes) and parsed_paper.has_appendix:
logger.info(f" [Pass 3 - Appendix] {title[:60]}...")
appendix = truncate_text(parsed_paper.appendix_text, 30000)
user_msg_3 = APPENDIX_PASS_USER.format(
current_notes=detailed_notes,
appendix_content=appendix,
)
result_3 = self.chain.invoke({"messages": [{"role": "user", "content": user_msg_3}]})
appendix_notes = result_3["messages"][-1].content.strip()
# 将增量内容追加到第二次笔记后(跳过无补充的情况)
if appendix_notes and "附录无重要补充内容" not in appendix_notes:
detailed_notes = detailed_notes + "\n\n" + appendix_notes
return self._format_final_notes(paper_info, detailed_notes)
return self._format_final_notes(paper_info, detailed_notes)
# ------------------------------------------------------------------
@staticmethod
def _should_read_appendix(notes: str) -> bool:
"""Check if the model decided to read the appendix."""
match = re.search(r"APPENDIX_NEEDED:\s*(YES|NO)", notes, re.IGNORECASE)
if match:
return match.group(1).upper() == "YES"
return False
@staticmethod
def _format_final_notes(paper_info: dict, notes_body: str) -> str:
"""Wrap notes with YAML front-matter for Obsidian."""
notes_body = notes_body.strip()
authors = ", ".join(paper_info.get("authors", [])[:5])
categories = ", ".join(paper_info.get("categories", []))
arxiv_id = paper_info["arxiv_id"]
header = f"""\
---
title: "{paper_info['title']}"
arxiv_id: "{arxiv_id}"
authors: "{authors}"
categories: "{categories}"
date_read: "{paper_info.get('date_read', '')}"
skill_category: "{paper_info.get('skill_category', '')}"
pdf_url: "{paper_info.get('pdf_url', '')}"
---
# {paper_info['title']}
> **ArXiv**: [{arxiv_id}](https://arxiv.org/abs/{arxiv_id}) | **Authors**: {authors}
"""
return header + notes_body
"""
Summary Agent — quick abstract-only summary for uncategorized papers.
"""
from __future__ import annotations
from typing import Any, Dict
import config
from agents.base_agent import create_agent
from utils.logger import get_logger
logger = get_logger(__name__)
PROMPTS_DIR = config.PROJECT_ROOT / "prompts"
def _load_prompt(filename: str) -> str:
path = PROMPTS_DIR / filename
return path.read_text(encoding="utf-8")
SUMMARY_SYSTEM_PROMPT = _load_prompt("summary_system.md")
class SummaryAgent:
"""Generate quick summaries from title + abstract only."""
def __init__(self, skill_config: Dict[str, Any]):
reading_prompt = skill_config.get("reading_prompt", "")
system_prompt = SUMMARY_SYSTEM_PROMPT.format(
reading_prompt=reading_prompt
)
self.chain = create_agent(system_prompt, temperature=0.2, max_tokens=4000)
def summarize(self, paper_info: dict) -> str:
"""Generate a summary note for one paper."""
title = paper_info["title"]
abstract = paper_info["abstract"]
authors = ", ".join(paper_info.get("authors", [])[:5])
arxiv_id = paper_info["arxiv_id"]
user_input = (
f"请对以下论文进行快速总结:\n\n"
f"**标题**: {title}\n"
f"**作者**: {authors}\n"
f"**ArXiv ID**: {arxiv_id}\n\n"
f"**摘要**:\n{abstract}"
)
try:
result = self.chain.invoke({"messages": [{"role": "user", "content": user_input}]})
body = result["messages"][-1].content
except Exception as e:
logger.error(f"Summary failed for '{title[:50]}': {e}")
body = f"*自动总结失败: {e}*\n\n## 摘要\n\n{abstract}"
return self._format_note(paper_info, body)
# ------------------------------------------------------------------
@staticmethod
def _format_note(paper_info: dict, body: str) -> str:
authors = ", ".join(paper_info.get("authors", [])[:5])
categories = ", ".join(paper_info.get("categories", []))
arxiv_id = paper_info["arxiv_id"]
header = f"""\
---
title: "{paper_info['title']}"
arxiv_id: "{arxiv_id}"
authors: "{authors}"
categories: "{categories}"
date_read: "{paper_info.get('date_read', '')}"
skill_category: "general"
pdf_url: "{paper_info.get('pdf_url', '')}"
---
# {paper_info['title']}
> **ArXiv**: [{arxiv_id}](https://arxiv.org/abs/{arxiv_id}) | **Authors**: {authors}
"""
return header + body
"""
ArXiv Fetcher — fetch recent papers and their LaTeX source.
Strategy (in order):
1. RSS feed – fast, gives today's new listings directly
2. arxiv API – fallback, get the most recent N papers (no date filter)
Uses direct HTTP download for LaTeX source files.
"""
from __future__ import annotations
import gzip
import io
import re
import tarfile
import time
import xml.etree.ElementTree as ET
from datetime import date, datetime, timedelta, timezone
from typing import Callable, Optional
import arxiv
import requests
import config
from utils.helpers import extract_arxiv_id
from utils.logger import get_logger
logger = get_logger(__name__)
# arXiv RSS namespaces
_RSS_NS = {
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rss": "http://purl.org/rss/1.0/",
"dc": "http://purl.org/dc/elements/1.1/",
"taxo": "http://purl.org/rss/1.0/modules/taxonomy/",
}
class ArxivFetcher:
"""Fetch paper listings and LaTeX source from arXiv."""
def __init__(self):
self.client = arxiv.Client(
page_size=100,
delay_seconds=3.0,
num_retries=3,
)
self.categories = config.ARXIV_CATEGORIES
self.max_results = config.ARXIV_MAX_RESULTS
# ── Paper listing ─────────────────────────────────────────
def fetch_single_paper(self, arxiv_id_or_url: str) -> Optional[dict]:
"""
Fetch a single paper by arxiv_id or URL.
Parameters
----------
arxiv_id_or_url : str
Can be:
- arxiv_id: "2401.12345"
- abs URL: "https://arxiv.org/abs/2401.12345"
- pdf URL: "https://arxiv.org/pdf/2401.12345.pdf"
Returns
-------
dict or None
Paper metadata dict, or None if not found.
"""
# Extract arxiv_id from URL if needed
arxiv_id = extract_arxiv_id(arxiv_id_or_url)
if not arxiv_id:
logger.error(f"Invalid arxiv_id or URL: {arxiv_id_or_url}")
return None
logger.info(f"Fetching single paper: {arxiv_id}")
try:
search = arxiv.Search(id_list=[arxiv_id])
results = list(self.client.results(search))
if not results:
logger.error(f"Paper not found: {arxiv_id}")
return None
paper = self._result_to_dict(results[0])
logger.info(f" Found: {paper['title'][:60]}...")
return paper
except Exception as e:
logger.error(f"Failed to fetch {arxiv_id}: {e}")
return None
def fetch_papers(
self,
target_date: Optional[date] = None,
is_known_fn: Optional[Callable[[str], bool]] = None,
) -> list[dict]:
"""
Fetch recent papers from arXiv.
Strategy:
1. Try RSS feeds for each category (today's new listings).
2. If RSS yields nothing, fall back to the arxiv API.
Stop conditions (both RSS and API):
- Paper already in DB (is_known_fn returns True) → stop / filter out
- Paper older than (today - 1 day) → stop
Parameters
----------
target_date : date, optional
Unused currently but kept for interface compatibility.
is_known_fn : callable, optional
Function that takes an arxiv_id and returns True if the
paper is already tracked in the local DB.
"""
# ── Method 1: RSS feeds ──
logger.info(
f"Fetching papers via RSS from [{', '.join(self.categories)}] ..."
)
papers = self._fetch_via_rss()
if papers:
# Dedup by arxiv_id (a paper can appear in multiple category feeds)
papers = self._dedup_papers(papers)
logger.info(f"RSS: got {len(papers)} unique papers")
# Filter out papers already in DB
if is_known_fn:
before = len(papers)
papers = [p for p in papers if not is_known_fn(p["arxiv_id"])]
skipped = before - len(papers)
if skipped:
logger.info(
f" RSS: filtered out {skipped} papers already in DB, "
f"{len(papers)} new papers remaining"
)
if self.max_results and len(papers) > self.max_results:
papers = papers[: self.max_results]
return papers
# ── Method 2: arxiv API (fallback) ──
logger.info("RSS returned nothing, falling back to arxiv API ...")
return self._fetch_via_api(is_known_fn=is_known_fn)
# ── RSS fetching ──────────────────────────────────────────
def _fetch_via_rss(self) -> list[dict]:
"""Fetch today's new papers from arXiv RSS feeds."""
all_papers: list[dict] = []
for cat in self.categories:
url = f"https://rss.arxiv.org/rss/{cat}"
logger.info(f" RSS: {url}")
try:
resp = requests.get(url, timeout=30)
resp.raise_for_status()
papers = self._parse_rss(resp.text, cat)
all_papers.extend(papers)
logger.info(f" → {len(papers)} papers from {cat}")
except Exception as e:
logger.warning(f" RSS failed for {cat}: {e}")
return all_papers
def _parse_rss(self, xml_text: str, category: str) -> list[dict]:
"""Parse an arXiv RSS feed XML into paper dicts."""
papers: list[dict] = []
try:
root = ET.fromstring(xml_text)
except ET.ParseError as e:
logger.warning(f" RSS XML parse error: {e}")
return papers
# arXiv RSS uses RDF/RSS 1.0 format
items = root.findall(".//rss:item", _RSS_NS)
if not items:
# Try plain RSS 2.0 fallback
items = root.findall(".//item")
for item in items:
paper = self._rss_item_to_dict(item, category)
if paper:
papers.append(paper)
return papers
def _rss_item_to_dict(self, item: ET.Element, default_cat: str) -> Optional[dict]:
"""Convert a single RSS <item> to our paper dict."""
# Try RDF namespace first, then plain
title_el = item.find("rss:title", _RSS_NS) or item.find("title")
link_el = item.find("rss:link", _RSS_NS) or item.find("link")
desc_el = item.find("rss:description", _RSS_NS) or item.find("description")
creator_el = item.find("dc:creator", _RSS_NS)
if title_el is None or link_el is None:
return None
raw_title = (title_el.text or "").strip()
link = (link_el.text or "").strip()
raw_desc = (desc_el.text or "").strip() if desc_el is not None else ""
# Skip "UPDATED" entries, focus on new submissions
# arXiv RSS titles look like: "Title (arXiv:2401.12345v1 [cs.AI])"
# or sometimes: "Title. (arXiv:2401.12345v1 [cs.AI] UPDATED)"
is_updated = "UPDATED" in raw_title
# Extract arxiv ID from link: https://arxiv.org/abs/2401.12345
arxiv_id = extract_arxiv_id(link) if link else ""
if not arxiv_id:
return None
# Clean title: remove the trailing "(arXiv:...)" part
title = re.sub(r"\s*\(arXiv:[^)]+\)\s*$", "", raw_title).strip()
title = re.sub(r"\.\s*$", "", title) # remove trailing period
# Parse abstract from description (may contain HTML)
abstract = self._clean_html(raw_desc)
# arXiv RSS description often starts with "<p>Abstract: ..."
abstract = re.sub(r"^Abstract:\s*", "", abstract, flags=re.IGNORECASE).strip()
# Parse authors
authors: list[str] = []
if creator_el is not None and creator_el.text:
# Format: "<a href='...'>Author1</a>, <a href='...'>Author2</a>"
author_text = self._clean_html(creator_el.text)
authors = [a.strip() for a in author_text.split(",") if a.strip()]
# Extract categories from title bracket part
cat_match = re.search(r"\[([^\]]+)\]", raw_title)
categories = (
[c.strip() for c in cat_match.group(1).split(",")]
if cat_match
else [default_cat]
)
return {
"arxiv_id": arxiv_id,
"title": title,
"abstract": abstract,
"authors": authors,
"categories": categories,
"published": date.today(),
"pdf_url": f"https://arxiv.org/pdf/{arxiv_id}",
"is_updated": is_updated,
}
# ── API fallback ──────────────────────────────────────────
def _fetch_via_api(
self, is_known_fn: Optional[Callable[[str], bool]] = None,
) -> list[dict]:
"""
Fallback: fetch most recent papers via the arxiv API.
Papers are sorted newest-first. Early-stop when:
1. Paper already in local DB → all older papers should also be known
2. Paper published before yesterday → we only care about today + yesterday
"""
cat_query = " OR ".join(f"cat:{cat}" for cat in self.categories)
search = arxiv.Search(
query=cat_query,
max_results=self.max_results,
sort_by=arxiv.SortCriterion.SubmittedDate,
sort_order=arxiv.SortOrder.Descending,
)
# Only look at today and yesterday (1 day lookback)
cutoff_date = date.today() - timedelta(days=config.FETCH_LOOKBACK_DAYS)
papers: list[dict] = []
total_scanned = 0
logger.info(
f" API: fetching up to {self.max_results} papers "
f"(cutoff date: {cutoff_date}, stop on first known paper) ..."
)
for result in self.client.results(search):
total_scanned += 1
paper = self._result_to_dict(result)
# ── Early stop 1: paper older than yesterday ──
if paper["published"] < cutoff_date:
logger.info(
f" ■ Stop: reached paper from {paper['published']} "
f"(cutoff {cutoff_date}). Scanned {total_scanned}."
)
break
# ── Early stop 2: paper already in DB → we've fetched up to here ──
if is_known_fn and is_known_fn(paper["arxiv_id"]):
logger.info(
f" ■ Stop: paper {paper['arxiv_id']} already in DB. "
f"All older papers should be known too. Scanned {total_scanned}."
)
break
papers.append(paper)
if len(papers) >= self.max_results:
break
logger.info(f" API: got {len(papers)} new papers (scanned {total_scanned})")
return papers
# ── LaTeX source fetching ─────────────────────────────────
def fetch_latex_source(self, arxiv_id: str) -> Optional[str]:
"""
Fetch the LaTeX source of a paper.
1. Try `arxiv_to_prompt` library (if installed).
2. Fall back to downloading the e-print tar/gz from arXiv.
"""
# ── Method 1: arxiv_to_prompt ──
try:
from arxiv_to_prompt import process_latex_source
logger.info(f" Fetching source via arxiv_to_prompt: {arxiv_id}")
return process_latex_source(arxiv_id, keep_comments=False)
except ImportError:
pass
except Exception as e:
logger.warning(f"arxiv_to_prompt failed for {arxiv_id}: {e}")
return None
@staticmethod
def _find_main_tex(tar: tarfile.TarFile) -> Optional[str]:
"""Find the main .tex file in a tar archive."""
tex_files: list[tuple[str, str]] = []
for member in tar.getmembers():
if member.name.endswith(".tex") and not member.name.startswith("."):
f = tar.extractfile(member)
if f:
content = f.read().decode("utf-8", errors="ignore")
tex_files.append((member.name, content))
if not tex_files:
return None
# Prefer the file containing \begin{document}
for name, content in tex_files:
if "\\begin{document}" in content:
return content
# Fall back to largest tex file
tex_files.sort(key=lambda x: len(x[1]), reverse=True)
return tex_files[0][1]
@staticmethod
def _result_to_dict(result: arxiv.Result) -> dict:
"""Convert an arxiv.Result to a plain dict."""
return {
"arxiv_id": extract_arxiv_id(result.entry_id),
"title": result.title.replace("\n", " ").strip(),
"abstract": result.summary.replace("\n", " ").strip(),
"authors": [a.name for a in result.authors],
"categories": list(result.categories),
"published": result.published.date(),
"pdf_url": result.pdf_url or "",
}
@staticmethod
def _dedup_papers(papers: list[dict]) -> list[dict]:
"""Remove duplicate papers by arxiv_id, keeping the first."""
seen: set[str] = set()
unique: list[dict] = []
for p in papers:
if p["arxiv_id"] not in seen:
seen.add(p["arxiv_id"])
unique.append(p)
return unique
@staticmethod
def _clean_html(text: str) -> str:
"""Strip HTML tags from text."""
text = re.sub(r"<[^>]+>", "", text)
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
text = text.replace(""", '"').replace("'", "'")
return text.strip()
"""
Global configuration module.
Loads all settings from .env file at project root.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# ── Project Root ──────────────────────────────────────────────
PROJECT_ROOT = Path(__file__).parent.resolve()
load_dotenv(PROJECT_ROOT / ".env")
# ── LLM ──────────────────────────────────────────────────────
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://api.openai.com/v1")
LLM_API_KEY = os.getenv("LLM_API_KEY", "")
LLM_MODEL = os.getenv("LLM_MODEL", "gpt-4o")
LLM_TEMPERATURE = float(os.getenv("LLM_TEMPERATURE", "0.3"))
LLM_MAX_TOKENS = int(os.getenv("LLM_MAX_TOKENS", "16000"))
# ── Skills ───────────────────────────────────────────────────
SKILLS_DIR = PROJECT_ROOT / "skills"
#!/usr/bin/env python3
"""Single arXiv paper reader entrypoint.
Usage:
python main.py --list
python main.py --arxiv-id 2401.12345
python main.py --arxiv-id https://arxiv.org/abs/2401.12345
python main.py --arxiv-id 2401.12345 --category rag_and_retrieval
"""
from __future__ import annotations
import argparse
import sys
# Ensure project root is on sys.path
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from arxiv_fetcher.fetcher import ArxivFetcher
from skills.loader import load_all_skills
from agents.classifier_agent import ClassifierAgent
from agents.reader_agent import ReaderAgent
from agents.summary_agent import SummaryAgent
from paper_reader.latex_parser import parse_latex
from utils.logger import get_logger
logger = get_logger("daily-arxiv")
def process_single_paper(arxiv_id_or_url: str, category: str | None = None) -> str:
"""Read one paper and return raw markdown text."""
logger.info("=" * 60)
logger.info("Single Paper Mode")
logger.info("=" * 60)
skills = load_all_skills()
if not skills:
raise RuntimeError("No skills loaded. Please check the skills/ directory.")
fetcher = ArxivFetcher()
logger.info(f"Fetching paper: {arxiv_id_or_url}")
paper = fetcher.fetch_single_paper(arxiv_id_or_url)
if not paper:
raise RuntimeError("Failed to fetch paper.")
selected_category = category
if selected_category is None:
classifier = ClassifierAgent(skills)
selected_category = classifier.classify(paper["title"], paper.get("abstract", ""))
if selected_category not in skills:
valid = ", ".join(sorted(skills.keys()))
raise ValueError(f"Unknown category '{selected_category}'. Available: {valid}")
# Build agents
reader_agents: dict[str, ReaderAgent] = {}
for skill_name, skill_config in skills.items():
if skill_name != "general":
reader_agents[skill_name] = ReaderAgent(skill_name, skill_config)
summary_agent = SummaryAgent(skills.get("general", {"reading_prompt": ""}))
logger.info(f"Processing as category: {selected_category}")
if selected_category != "general" and selected_category in reader_agents:
notes = _deep_read(
fetcher,
reader_agents[selected_category],
paper,
summary_agent,
)
return notes
return summary_agent.summarize(paper)
def _deep_read(
fetcher: ArxivFetcher,
reader: ReaderAgent,
paper: dict,
fallback_agent: SummaryAgent,
) -> str:
"""
Attempt deep two-pass reading.
"""
arxiv_id = paper["arxiv_id"]
# Fetch LaTeX source
latex_source = fetcher.fetch_latex_source(arxiv_id)
if not latex_source:
logger.warning(f" No LaTeX source for {arxiv_id}, falling back to summary.")
return fallback_agent.summarize(paper)
# Parse LaTeX
parsed = parse_latex(latex_source)
if not parsed.abstract and not parsed.sections:
logger.warning(f" LaTeX parsing yielded nothing for {arxiv_id}, falling back.")
return fallback_agent.summarize(paper)
# Two-pass reading
return reader.read_paper(paper, parsed)
# ── CLI ───────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Single arXiv paper reader"
)
parser.add_argument(
"--list",
action="store_true",
help="List available paper categories (skills)",
)
parser.add_argument(
"--arxiv-id",
type=str,
default=None,
help="Read a single paper by arXiv ID or URL",
)
parser.add_argument(
"--category",
type=str,
default=None,
help="Optional: force a specific category (skill folder name)",
)
args = parser.parse_args()
skills = load_all_skills()
if args.list:
for name in sorted(skills.keys()):
print(name)
return
if not args.arxiv_id:
parser.error("--arxiv-id is required unless --list is used")
notes = process_single_paper(args.arxiv_id, category=args.category)
print(notes)
if __name__ == "__main__":
main()
"""
LaTeX Parser — parse a raw .tex file into structured sections
so the Reader Agent can perform multi-pass reading.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
@dataclass
class Section:
"""A single section extracted from a LaTeX document."""
title: str
content: str
level: int = 1 # 1 = \section, 2 = \subsection, 3 = \subsubsection
is_appendix: bool = False
@dataclass
class ParsedPaper:
"""Structured representation of a parsed LaTeX paper."""
raw: str = ""
abstract: str = ""
sections: list[Section] = field(default_factory=list)
appendix_sections: list[Section] = field(default_factory=list)
# ── Convenience properties for the reading pipeline ──────
# Names (case-insensitive) that belong to the first-pass reading
_FIRST_PASS_KEYWORDS: tuple[str, ...] = (
"introduction",
"preliminar",
"background",
"contribution",
"limitation",
"related work",
"overview",
"motivation",
)
@property
def first_pass_text(self) -> str:
"""Abstract + Introduction + Preliminaries + Contributions + Limitations."""
parts: list[str] = []
if self.abstract:
parts.append(f"## Abstract\n\n{self.abstract}")
for sec in self.sections:
title_lower = sec.title.lower()
if any(kw in title_lower for kw in self._FIRST_PASS_KEYWORDS):
parts.append(f"## {sec.title}\n\n{sec.content}")
return "\n\n---\n\n".join(parts) if parts else self.abstract
@property
def main_body_text(self) -> str:
"""Everything except first-pass sections, references, and appendix."""
parts: list[str] = []
for sec in self.sections:
title_lower = sec.title.lower()
# Skip first-pass sections
if any(kw in title_lower for kw in self._FIRST_PASS_KEYWORDS):
continue
# Skip references
if "reference" in title_lower or "bibliograph" in title_lower:
continue
parts.append(f"## {sec.title}\n\n{sec.content}")
return "\n\n---\n\n".join(parts)
@property
def appendix_text(self) -> str:
"""Appendix sections concatenated."""
if not self.appendix_sections:
return ""
parts = [f"## {sec.title}\n\n{sec.content}" for sec in self.appendix_sections]
return "\n\n---\n\n".join(parts)
@property
def has_appendix(self) -> bool:
return bool(self.appendix_sections)
# ── Public API ────────────────────────────────────────────────
def parse_latex(tex: str) -> ParsedPaper:
"""
Parse raw LaTeX source into a ``ParsedPaper``.
Handles:
- \\begin{abstract} ... \\end{abstract}
- \\section{}, \\subsection{}, \\subsubsection{}
- \\appendix marker
- \\bibliography / \\begin{thebibliography}
"""
paper = ParsedPaper(raw=tex)
# ── 1. Extract abstract ──
abs_match = re.search(
r"\\begin\{abstract\}(.*?)\\end\{abstract\}", tex, re.DOTALL
)
if abs_match:
paper.abstract = _clean_latex(abs_match.group(1).strip())
# ── 2. Locate key markers ──
appendix_pos = _find_marker(tex, r"\\appendix(?:\b|[^a-zA-Z])")
bib_pos = _find_marker(
tex,
r"\\(?:bibliography\{|begin\{thebibliography\}|printbibliography)",
)
doc_end_pos = tex.find("\\end{document}")
if doc_end_pos < 0:
doc_end_pos = len(tex)
# ── 3. Extract sections ──
section_pattern = re.compile(
r"\\(section|subsection|subsubsection)\{([^}]+)\}"
)
matches = list(section_pattern.finditer(tex))
for i, m in enumerate(matches):
level_str, title = m.group(1), m.group(2).strip()
level = {"section": 1, "subsection": 2, "subsubsection": 3}[level_str]
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else doc_end_pos
# Trim if content extends past bibliography/appendix/end
for boundary in [bib_pos, doc_end_pos]:
if boundary and start < boundary < end:
end = boundary
content = _clean_latex(tex[start:end].strip())
sec = Section(title=title, content=content, level=level)
# Determine if this section is in the appendix region
if appendix_pos is not None and m.start() >= appendix_pos:
sec.is_appendix = True
paper.appendix_sections.append(sec)
else:
paper.sections.append(sec)
return paper
# ── Helpers ───────────────────────────────────────────────────
def _find_marker(tex: str, pattern: str) -> int | None:
"""Return the position of the first regex match, or None."""
m = re.search(pattern, tex)
return m.start() if m else None
def _clean_latex(text: str) -> str:
"""Light cleanup of LaTeX content for readability.
We do NOT strip all LaTeX commands — LLMs read LaTeX fine.
We only remove very noisy elements.
"""
# Remove \label{...}
text = re.sub(r"\\label\{[^}]*\}", "", text)
# Remove \vspace{...}, \hspace{...}
text = re.sub(r"\\[vh]space\{[^}]*\}", "", text)
# Collapse multiple blank lines
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
你是一个学术论文分类专家。你的任务是根据论文的标题和摘要,将论文归入最匹配的研究类别。
可用类别
{categories}
输出格式
请严格输出 JSON(不要包含 markdown 代码块标记),格式如下: {{ "category": "<类别名称,必须是上述类别之一,或 general>", "confidence": <0-1 之间的置信度>, "reasoning": "<简短的分类理由>" }}
注意:
- 如果论文不明确属于任何特定类别,请归为 "general"
- 如果论文涉及多个领域,请选择最核心的那个
- confidence 低于 0.4 时建议归为 "general"
附录阅读 — 补充分析
以下是你目前的完整阅读笔记(仅供参考上下文,不要重复输出):
<current_notes> {current_notes} </current_notes>
请阅读以下附录内容,只输出需要补充的新内容(如详细证明、额外实验、实现细节、prompt要点等)。
要求:
- 只输出附录中值得记录的增量信息,以
## 附录补充开头 - 不要重复已有笔记中的任何内容
- 如果附录没有值得补充的信息,只输出一行:
(附录无重要补充内容)
---
{appendix_content}
第一轮阅读 — 初步总结
你擅长高屋建瓴,把握整体框架与核心方法,请阅读以下论文的核心部分(摘要、引言、预备知识、贡献声明、局限性),给出初步总结。这里可能缺失一些具体细节,在总结最后你可以提出问题,交给后面的方法细节agent来解决。
论文标题: {title} 作者: {authors} ArXiv ID: {arxiv_id}
---
{first_pass_content}
第二轮阅读 — 详细分析
以下是你在第一轮阅读后的初步总结:
<initial_summary> {initial_summary} </initial_summary>
现在请阅读论文的主体部分(方法、实验、分析等),补充详细分析。
要求: 1. 深入分析论文的方法细节、实验设计和结果 2. 提取关键公式和算法步骤 3. 整合第一轮总结,回答第一轮提出的问题,但不要局限于这个问题,而是生成完整的阅读笔记 4. 在笔记最后,判断是否需要阅读附录以获得更完整的理解,输出一行: APPENDIX_NEEDED: YES 或 APPENDIX_NEEDED: NO,后跟简短原因
---
{main_body}
你是一位资深的 AI 研究员,正在深度阅读一篇学术论文。
你需要遵循以下领域专属阅读指南:
{reading_prompt}
通用要求:
- 输出语言:中文
- 输出格式:Markdown,适合 Obsidian 笔记
- 提取关键公式时保留 LaTeX 格式(用 $...$ 包裹)
- 对重要概念加粗
- 如遇到不确定的内容,请标注 [待确认]
你是一位高效的学术论文摘要助手。你需要基于论文的标题和摘要,快速生成结构化的阅读笔记。
你的输出语言为中文,格式为 Markdown,适合存储到 Obsidian 笔记。
请遵循以下总结指南:
{reading_prompt}
langchain>=1.2.9
langchain-openai>=1.1.7
requests>=2.31.0
python-dotenv>=1.0.0
arxiv>=2.1.0
arxiv-to-prompt
Agent Systems
本分类涵盖与 AI Agent / 智能体系统 相关的论文,包括:
- AI Agent 架构设计(ReAct, Plan-and-Execute, Reflexion, AutoGuide),以及各类LLM multiagent框架设计
- 工具使用(Tool Use),skills,mcp相关
- Agent 记忆系统(Memory, RAG-augmented Agent)相关
- Agent 评估框架
- 设计broswer、search,deepresearch,具身智能、导航等垂域agent内容
注意,本类别主要聚焦于agent系统,并非agent/agentic模型训练。
典型关键词: agent, tool use, planning, multi-agent, function calling, action, memory, decision-making, reflection, self-improvement
Agent Systems 论文阅读指南
阅读此类论文时,请记录以下要点:
1. 论文的动机和核心创新点是什么?论文是如何建模一个问题的? 2. Agent 架构设计:系统整体架构是什么?有哪些模块/子agent、如何协作?每个模块的输入、功能和输出分别是什么?有没有反思等机制? 3. 工具使用方式:Agent 如何选择和调用工具?工具接口如何设计?是否支持动态工具发现? 4. 记忆与状态管理:如何管理长期记忆和短期上下文?是否使用外部存储? 5. 多 Agent 协作(如涉及):Agent 之间如何通信和协调?角色如何分配? 6. 评估方法:使用了哪些评估指标(metric)和 Benchmark?能介绍一下这些benchmark吗?成功率如何衡量? 7. 与现有方法对比:相比已有方法有何改进? 8. 局限性:存在哪些未解决问题?是否有token消耗过大/时间过长/场景单一/依赖具体prompt等问题?
在最终生成笔记时,首先进行一句话总结,再按条回答上述问题,要严谨、专业。如果有上面没有提到的关键问题,也请在笔记中补充。
Benchmark
本分类涵盖与 Benchmark / 评测基准 相关的论文,重点聚焦以下任务方向:
- 视觉导航任务(Vision-and-Language Navigation, Embodied Navigation)
- 机器人任务(Robot Manipulation, Embodied AI)
- 大模型/多模态模型能力评测:推理、代码、工具调用、deep research、browser 等
- Agent 与多 Agent 系统的评测基准
此类论文核心在于提出新的评测数据集、评测环境或评测框架,并系统性分析模型/Agent 能力。
典型关键词: benchmark, evaluation, dataset, embodied, navigation, robotics, reasoning, code, tool-use, browser, deep research, agent
Benchmark 论文阅读指南
阅读此类论文时,请记录以下要点:
1. 该 benchmark 属于哪个领域?提出是为了解决什么问题?
- 明确任务类型(视觉导航 / 机器人 / LLM/多模态推理 / 代码 / 工具调用 / deep research / browser 等)
- 论文提出该 benchmark 的动机与核心问题
2. 该 benchmark 具体包含多少数据,评测环境是什么,有没有一个具体的数据例子?
- 数据规模(样本数、任务数、场景数)
- 评测环境(仿真环境、真实环境、Web 环境等)
- 给出 1 个具体的数据样例/任务示例
3. 有没有同类 benchmark?如果有,和本文 benchmark 有哪些区别,本文的优势在哪?
- 现有相关 benchmark 列表
- 关键差异(任务设置、难度、评测指标、数据质量等)
- 本文 benchmark 的独特优势
4. benchmark采用的是什么评估方法?已有的模型/agent 在该 benchmark 下的结果如何?有没有对评测结果的分析?
- 评估方法/metric介绍
- 主要 baseline/模型表现
- 评测结果的关键分析(失败模式、能力瓶颈)
5. 作者有没有针对自己的 benchmark 提出新的模型/agent?如果有,这个新方法的创新在哪里?
- 新方法的核心思路
- 相比已有方法的改进点
在最终生成笔记时,首先进行一句话总结,再按以上 5 点逐条回答,要求清晰、专业。
General (通用 / 未分类)
本分类是默认的兜底分类,用于处理不属于任何特定领域的论文。
当一篇论文的主题不能明确匹配到其他任何分类时,将归入此类。 对于此类论文,仅基于摘要进行快速总结,不进行全文深度阅读。
General 论文快速总结指南
对于未命中特定分类的论文,请基于摘要进行快速总结。
请按照以下结构生成笔记:
1. 一句话总结:用一句话概括这篇论文做了什么 2. 研究问题:论文要解决什么问题? 3. 核心方法:提出了什么方法?(简要描述) 4. 主要结果:关键实验结果或结论是什么? 5. 潜在价值:这篇论文对 AI 领域有什么潜在影响?
注意:此类论文仅基于摘要总结,无需深入分析细节。
Inference Acceleration & Efficiency
本分类涵盖与 模型推理性能优化、计算效率提升及部署技术 相关的论文,包括但不限于:
- 模型压缩技术:量化(PTQ/QAT)、权重/激活剪枝、稀疏化(Sparsity)、知识蒸馏(Distillation)。
- 推理算法优化:投机采样(Speculative Decoding)、早停机制(Early Exit)、并行解码(Parallel Decoding)。
- 显存与带宽优化:KV Cache 管理(如 PagedAttention)、长文本推理优化、模型并行(TP/PP)调度。
- 系统层与内核优化:算子融合(Kernel Fusion)、FlashAttention 系列、低比特计算算子、定制化 CUDA Kernel。
- 硬件协同设计:针对特定硬件(NVIDIA/AMD GPU、端侧 NPU/FPGA)的部署优化及硬件感知架构搜索(NAS)。
- 服务治理与调度:连续批处理(Continuous Batching)、Prefill 与 Decode 分离、负载均衡。
典型关键词: inference, acceleration, latency, throughput, quantization, pruning, distillation, speculative decoding, KV cache, kernel fusion, PagedAttention, FlashAttention, efficiency, hardware-aware
推理加速论文阅读指南
阅读此类论文时,请记录以下要点:
1. 该加速方案针对什么场景?解决的核心瓶颈是什么?
- 目标模型与任务:明确加速的对象(如 LLM、扩散模型、多模态、推荐系统等)。
- 核心瓶颈:确定该方案解决的是 显存受限(Memory-bound)、计算受限(Compute-bound) 还是 通信受限(Communication-bound)?(例如:KV Cache 爆炸、自注意力计算量大、预填充阶段太慢等)。
2. 核心加速机制是什么?其技术实现逻辑是怎样的?
- 加速手段分类:属于哪种技术路线?(如:量化 PTQ/QAT、剪枝、蒸馏、算子融合、投机采样、并行策略、KV Cache 管理、硬件协同设计等)。
- 具体细节:详细描述论文中方法的实现细节,包括数学公式,代码分析等
3. 与主流 Baseline 的对比如何?该方案的“杀手锏”在哪?
- 对比对象:是否与业内标准(如 vLLM, TensorRT-LLM, DeepSpeed-Inference, FlashAttention 等)进行了对比?
- 关键差异:
- 精度 vs. 速度:是否实现了无损加速?如果有损,精度下降了多少?
- 通用性:是针对特定架构(如 Transformer)的定制优化,还是通用的框架优化?
- 核心优势:例如“在长文本场景下比 vLLM 吞吐量高出 2 倍”或“首次在移动端实现了 Llama-3 的实时推理”。
4. 实验评估指标有哪些?在不同硬件/负载下的表现如何?
- 核心指标:记录论文提供的 首字延迟 (TTFT)、每个 Token 的延迟 (TPOT)、吞吐量 (Throughput)、显存占用 (VRAM) 及 加速比 (Speedup)。
- 实验环境:测试所用的硬件(如 A100, H100, 消费级 RTX 4090 或嵌入式端侧设备)。
- 结果分析:该方案在什么情况下效果最明显?在什么情况下可能会失效或性能下降?
5. 该方案的部署难度与工程可行性如何?
- 兼容性:是否需要重新训练模型?是否需要特殊的硬件指令集支持(如 NVIDIA Tensor Cores)?
- 集成成本:是作为独立的推理引擎存在,还是可以作为插件集成到现有框架中?
- 开源情况:作者是否提供了可复用的代码库或预编译算子?
---
总结要求: 注意:在有例子可举例时,你生成的笔记里一定要包含一个示例!示例是化抽象为具体,有助于理解的最好方法! 在最终生成笔记时,首先进行一句话总结(概括其核心创新点与加速效果),再按以上 5 点逐条回答,要求清晰、专业、直击技术痛点。
LLM Training & Alignment
本分类涵盖与 大语言模型训练、微调和对齐 相关的论文,包括但不限于:
- 预训练方法与架构改进(Scaling Laws, MoE, 新型 Attention 等)
- 监督微调(SFT)和指令微调(Instruction Tuning)
- RLHF / DPO / PPO 等对齐技术
- 数据工程(数据质量、数据配比、合成数据)
- 模型压缩与高效训练(量化、蒸馏、LoRA、Adapter)
- 长上下文训练与处理
- 安全对齐与价值对齐
典型关键词: pre-training, fine-tuning, RLHF, DPO, alignment, instruction tuning, scaling law, LoRA, quantization, distillation, data curation, safety
LLM Training & Alignment 论文阅读指南
阅读此类论文时,请特别关注以下方面:
1. 训练范式与训练方法
- 训练范式:明确指出论文使用的训练范式
- 预训练(Pre-training)
- 监督微调(SFT / Supervised Fine-tuning)
- 指令微调(Instruction Tuning)
- 强化学习(RL):PPO、REINFORCE、RRHF 等
- 偏好对齐:RLHF、DPO、IPO、KTO、ORPO 等
- 持续学习(Continual Learning)
- 其他范式(Multi-task Learning、Meta-learning 等)
- 训练方法:明确指出具体的训练技术
- 全参微调(Full Fine-tuning)
- 参数高效微调:LoRA、QLoRA、Adapter、Prefix-tuning、P-tuning 等
- 如果使用 LoRA:秩(rank)、alpha、目标模块、dropout 等配置
- 量化训练:QAT、PTQ、具体 bit 数
- 知识蒸馏(Knowledge Distillation)
- 其他方法
- 相关领域: 明确指出论文是针对垂域任务,还是通用任务
2. 算法核心细节(必须讲清楚)
- 具体优化点是什么:
- 相比 baseline 或已有方法,本文的创新改进在哪里?
- 是改进了损失函数、采样策略、数据构建,还是其他?
- 损失函数:
- 完整的损失函数表达式(数学公式)
- 各项的含义与权重配置
- 是否有正则化项、辅助损失等
- 模型参数更新方式:
- 使用什么优化器(Adam, AdamW, SGD, Lion 等)?
- 学习率设置与调度策略(常数、余弦衰减、线性 warm-up 等)
- Batch size、梯度累积步数
- 梯度裁剪、权重衰减等配置
- 参数更新频率(特别是 RL 中的策略更新)
3. 训练数据
- 数据来源:
- 在哪些数据上进行训练?
- 数据规模(样本数、token 数)
- 数据类型(文本、代码、多模态等)
- 数据收集方式:
- 公开数据集、爬虫、人工标注、模型生成(self-instruct, distillation)?
- 数据清洗与质量控制流程
- 数据去重、过滤规则
- 数据配比与采样:
- 不同数据源的混合比例
- 采样策略(均匀采样、重要性采样等)
4. 训练效果与实验结果
- 主要 Benchmark 表现:
- 在哪些评测集上测试?
- 具体得分与 SOTA 对比
- 消融实验:
- 各组件/超参数的影响分析
- 哪些因素对性能影响最大?
- 消融实验的具体数据与结论
- 与已有方法的对比:
- 对比了哪些 baseline 或竞品方法?
- 在相同数据/计算资源下的公平对比
- 性能提升幅度与统计显著性
5. 其他关键要素
- 数据策略:训练数据如何构建?数据质量如何保证?
- 架构创新(如有):在 Transformer 基础上做了哪些改进?
- Scaling 分析:是否有 scaling law 分析?不同规模模型的表现趋势?
- 效率优化:计算效率、内存优化、训练加速方面有何创新?
- 局限性与展望:尚有哪些不足,未来工作方向
---
请在生成笔记时使用以下结构:
- 一句话总结
- 训练范式与方法(明确列出范式和方法)
- 核心算法:
- 具体优化点
- 损失函数(数学公式)
- 参数更新方式
- 训练数据:
- 数据来源与规模
- 数据收集与处理方式
- 实验结果:
- 主要 benchmark 得分
- 与已有方法对比
- 消融实验要点
- 创新点
- 局限性
要求准确、专业,对技术细节要具体明确,不能含糊其辞。
"""
Skill Loader — scans the skills/ directory structure and loads
category metadata + reading prompts for each sub-folder.
"""
from pathlib import Path
from typing import Dict, Any
import config
from utils.logger import get_logger
logger = get_logger(__name__)
METADATA_FILE = "_metadata.md"
READING_PROMPT_FILE = "reading_prompt.md"
def _read_md(path: Path) -> str:
"""Read a markdown file, return empty string if missing."""
if path.exists():
return path.read_text(encoding="utf-8")
return ""
def load_all_skills() -> Dict[str, Dict[str, Any]]:
"""
Load every skill category from the skills/ directory.
Returns:
{
"agent_systems": {
"name": "agent_systems",
"metadata": "<content of _metadata.md>",
"reading_prompt": "<content of reading_prompt.md>",
},
...
}
"""
skills_dir = config.SKILLS_DIR
skills: Dict[str, Dict[str, Any]] = {}
if not skills_dir.exists():
logger.warning(f"Skills directory not found: {skills_dir}")
return skills
for child in sorted(skills_dir.iterdir()):
if child.is_dir() and not child.name.startswith("_"):
metadata = _read_md(child / METADATA_FILE)
reading_prompt = _read_md(child / READING_PROMPT_FILE)
if not metadata:
logger.warning(
f"Skill '{child.name}' has no {METADATA_FILE}, skipping."
)
continue
skills[child.name] = {
"name": child.name,
"metadata": metadata,
"reading_prompt": reading_prompt,
}
logger.info(f"Loaded skill: {child.name}")
return skills
def get_categories_description(skills: Dict[str, Dict[str, Any]]) -> str:
"""
Build a textual description of all categories (used by the classifier).
"""
lines: list[str] = []
for name, info in skills.items():
if name == "general":
continue # general is the fallback, not a classification target
lines.append(f"### {name}\n{info['metadata']}\n")
return "\n".join(lines)
Multimodal Learning
本分类涵盖与 多模态学习 相关的论文,包括但不限于:
- 视觉-语言模型(VLM, MLLM)
- 文生图 / 图生文 / 视频理解
- 多模态预训练与对齐
- 跨模态检索与匹配
- 视觉推理与 Grounding
- 多模态 Agent
- 音频-语言模型
典型关键词: multimodal, vision-language, VLM, image generation, video understanding, cross-modal, visual grounding, CLIP, diffusion, text-to-image
Multimodal Learning 论文阅读指南
阅读此类论文时,请特别关注以下方面:
1. 模态对齐方式:不同模态如何映射到同一表征空间?使用了什么对齐目标? 2. 模型架构:视觉编码器、语言模型、连接模块分别是什么?如何融合? 3. 训练策略:是否分阶段训练?预训练和微调的数据分别是什么? 4. 生成质量(如涉及生成任务):生成结果的质量如何?有哪些控制手段? 5. 评估指标:使用了哪些多模态 Benchmark?在各项任务上的表现? 6. 数据构建:多模态训练数据如何收集和清洗? 7. 效率与部署:模型大小、推理速度、部署方案?
请在生成笔记时使用以下结构:
- 一句话总结
- 核心架构(各模态组件及其交互方式)
- 训练流程
- 关键实验结果
- 创新点
- 局限性
RAG & Retrieval
本分类涵盖与 检索增强生成(RAG)和信息检索 相关的论文,包括但不限于:
- 检索增强生成(RAG)架构与优化
- 向量检索与嵌入模型
- 知识库构建与管理
- 文档解析与分块策略(Chunking)
- 查询改写与意图理解
- 混合检索(稀疏 + 稠密)
- 长文档处理与摘要
典型关键词: RAG, retrieval, embedding, vector search, knowledge base, document parsing, chunking, reranking, hybrid search, dense retrieval
RAG & Retrieval 论文阅读指南
阅读此类论文时,请特别关注以下方面:
1. 检索架构:整体 RAG 流程如何设计?检索器和生成器如何协作? 2. 检索方法:使用了哪种检索方式(稠密/稀疏/混合)?嵌入模型是什么? 3. 知识源处理:文档如何解析、分块、索引?分块策略有何创新? 4. 查询处理:是否有查询改写、扩展、路由等优化? 5. 生成增强:检索结果如何融入生成过程?是否有迭代检索? 6. 评估方法:使用了哪些 RAG 评估指标(忠实度、相关性、答案质量)? 7. 效率与可扩展性:检索延迟、索引大小、扩展到大规模知识库的能力?
请在生成笔记时使用以下结构:
- 一句话总结
- 核心方法(RAG 流水线设计)
- 检索与生成细节
- 关键实验结果
- 创新点
- 局限性
- 可复用的工程经验
Technique Report (技术报告)
本分类涵盖各大厂商发布的 模型/智能体技术报告,这类报告通常篇幅较长,内容全面,包括:
- 模型架构设计与创新
- 预训练阶段(数据构建、训练策略、Scaling Laws)
- Mid-training(持续预训练、领域适配)
- 后训练阶段(SFT、RLHF/DPO、对齐方法)
- 训练细节与工程实践(分布式训练、优化器配置、学习率策略)
- 评测集设计与实验结果
- 消融实验与关键 Insights
- 数据收集、清洗与质量控制流程
- 安全对齐与红队测试
典型特征: 长篇幅、多训练阶段、详细的数据与训练流程、全面的评测、工程经验总结
典型发布方: OpenAI, Anthropic, Google DeepMind, Meta, Microsoft, 阿里, 百度, 字节, 智谱等
典型关键词: technical report, model release, pretraining, post-training, alignment, instruction tuning, RLHF, data curation, safety, evaluation
Technique Report 技术报告阅读指南
阅读技术报告时,请系统性地记录以下要点(篇幅较长,需全面覆盖):
1. 模型概览
- 模型名称与版本:具体型号、参数规模、发布时间
- 核心定位:通用模型还是垂域模型?主打能力是什么?
- 整体架构:基础架构类型(Transformer 变体、MoE 等)、关键创新点
2. 预训练阶段
- 训练数据:
- 数据来源与规模(token 数量、语言分布)
- 数据清洗与质量控制流程(去重、过滤规则、质量评分)
- 数据配比策略(不同来源数据的比例、动态调整策略)
- 是否包含代码、多模态等特殊数据
- 模型架构细节:
- 层数、隐藏维度、注意力头数等具体配置
- 是否有架构创新(新型注意力机制、归一化方法等)
- 训练策略:
- 优化器选择与超参数(学习率、warm-up、衰减策略)
- Batch size、序列长度
- 分布式训练配置(数据并行、模型并行、流水线并行)
- 训练时长与计算资源消耗
- Scaling 分析:是否有不同规模模型的对比?Scaling Laws 验证?
3. Mid-training(如有)
- 目标与动机:为什么需要 mid-training?针对哪些能力?
- 数据特点:与预训练数据有何区别?
- 训练策略:学习率、训练轮数等配置
4. 后训练阶段(Post-training)
- 监督微调(SFT):
- 训练数据:数据规模、数据来源(人工标注、模型生成、混合)
- 数据质量控制(标注规范、质量审核流程)
- 数据覆盖的能力维度(推理、代码、创作、多语言等)
- 训练方法:全参微调还是参数高效方法(LoRA、Adapter 等)?
- 如果使用 LoRA:秩(rank)设置、目标层选择、合并策略
- 如果全参微调:学习率、训练轮数、防止过拟合的策略
- 损失函数:标准交叉熵还是有改进?
- 强化学习对齐(RLHF/DPO 等):
- 训练范式:RLHF、DPO、PPO、RRHF、其他变体?
- 偏好数据收集:
- 数据规模、标注流程
- 标注员招募与培训
- 标注质量控制(一致性检查、多人投票)
- 奖励模型训练(如适用):
- 模型架构与规模
- 训练数据与方法
- 奖励模型的准确率/一致性评估
- 策略优化细节:
- 具体算法:PPO、REINFORCE、其他?
- 损失函数:完整的损失函数表达式
- 参数更新方式:梯度计算、更新频率
- KL 散度惩罚系数、Value 网络配置等
- 训练数据:在哪些 prompts 上进行 RL 训练?
- 训练效果:对比 SFT baseline 的提升
5. 评测与实验结果
- 评测集:
- 使用了哪些 benchmark(MMLU, GSM8K, HumanEval, MT-Bench 等)?
- 是否有内部评测集?评测维度有哪些?
- 主要结果:
- 在各 benchmark 上的得分
- 与竞品模型的对比(GPT-4, Claude, Llama 等)
- 不同规模模型的性能对比
- 消融实验:
- 哪些组件/策略对性能影响最大?
- 数据配比、训练阶段、超参数的消融
- 具体的消融结果与分析
6. 训练细节与 Insights
- 关键发现:训练过程中的重要观察与经验
- 踩坑与解决方案:遇到的问题及如何解决
- 工程优化:提升训练效率的技巧
- 数据质量的影响:高质量数据的重要性
7. 安全与对齐
- 安全对齐方法:如何减少有害输出?
- 红队测试:测试方法与发现的问题
- 偏见与公平性:如何评估和缓解?
8. 局限性与未来工作
- 已知局限:模型在哪些方面还有不足?
- 未来改进方向:计划如何提升?
---
在生成笔记时,请使用清晰的分级结构,突出:
- 核心创新点(相比前代模型或竞品的关键改进)
- 数据策略要点(数据规模、质量控制、配比策略)
- 训练流程图景(预训练 → Mid-training → SFT → RLHF 的完整链路)
- 算法细节(损失函数、参数更新、具体优化点)
- 实验结果亮点(最强的 benchmark 表现、关键消融结论)
- 可复现的经验(对其他研究者有参考价值的技术细节)
要求专业、准确、全面,对技术细节不能含糊其辞。
"""Utility helpers for text processing, truncation, etc."""
import re
from datetime import date, timedelta
def sanitize_filename(title: str, max_len: int = 80) -> str:
"""Convert a paper title into a safe filename slug."""
safe = re.sub(r"[^\w\s-]", "", title)
safe = re.sub(r"\s+", "-", safe).strip("-")
return safe[:max_len]
def truncate_text(text: str, max_chars: int = 60000) -> str:
"""Truncate text to fit within a character budget, keeping the beginning."""
if len(text) <= max_chars:
return text
return text[:max_chars] + "\n\n...[内容因超出长度限制被截断]..."
def get_target_dates() -> list[date]:
"""Return today and yesterday as candidate dates."""
today = date.today()
return [today, today - timedelta(days=1)]
def extract_arxiv_id(entry_id: str) -> str:
"""Extract clean arxiv id from a full entry URL.
e.g. 'http://arxiv.org/abs/2401.12345v1' -> '2401.12345'
"""
raw = entry_id.rstrip("/").split("/")[-1]
# Remove version suffix
return re.sub(r"v\d+$", "", raw)
"""Logging configuration for Daily Arxiv Reader."""
import logging
import sys
def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
"""Get a configured logger instance."""
logger = logging.getLogger(name)
if not logger.handlers:
logger.setLevel(level)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
fmt = logging.Formatter(
"[%(asctime)s] %(levelname)-7s %(name)s — %(message)s",
datefmt="%H:%M:%S",
)
handler.setFormatter(fmt)
logger.addHandler(handler)
return logger