--- name: research description: 查 paper_server 文献库(基于 OpenAlex 元数据 + Sci-Hub 下载的内部部署)。用户要查文献、找 DOI、拉 PDF、做文献综述、写带引文的申报书 / 研究方案 / 调研报告时使用。 --- # Research paper_server 是内部部署的 Django 文献库:元数据来自 OpenAlex,PDF / XML 由 Sci-Hub / OpenAlex 异步抓取。**库里主语料是英文**(OpenAlex 主索引英文文献),少量中文。本 skill 给你四个 helper(`search` / `get_paper` / `fetch_pdf` / `fetch_xml`),用 `run_python` 调用,**不要**自己 `httpx` 裸调 API。 ## 何时用 - 用户要查 / 找 / 看 / 推荐文献 - 要 DOI、要某篇 PDF、要 abstract - 写申报书 / 研究方案 / 调研报告的"国内外现状"段需要真实文献支撑 - 配合 `proposal` skill 的「立项依据」起草 —— 先 `search` 拿候选,看 abstract 决定要不要引 ## 何时不用 - 用户只问通识(直接答即可,不需要文献支撑) - 用户已经给了具体文献清单(直接用,不要二次校验) ## 准备 ```python from skills.research.paper import search, get_paper, fetch_pdf, fetch_xml ``` (import 路径由 `run_python` 注入的 `PYTHONPATH` 提供,直接写就行,不必折腾 `sys.path`) ## 关键:keyword 优先用英文 `search(keyword=...)` 走 paper_server SearchFilter,模糊匹配 **title / first_author / first_author_institution**(目前不含 abstract)。库里 95%+ 文献 title 是英文,中文 keyword 命中率很低。 **用户输入中文 → 先转成专业英文术语再 search**: | 用户原话 | 不要这样 | 这样 | |---|---|---| | 水泥水化 | `search("水泥水化")` | `search("cement hydration")` | | 钢筋锈蚀 | `search("钢筋锈蚀")` | `search("steel reinforcement corrosion")` | | 混凝土碳化 | `search("混凝土碳化")` | `search("concrete carbonation")` | | 锂离子电池电解液 | `search("锂离子电池电解液")` | `search("lithium-ion battery electrolyte")` | 转译策略: - 用领域内标准英文术语(查不准就先英文 keyword 试一次看返回 title 是不是相关) - 多词术语用空格分隔(`SearchFilter` 默认空格视作 AND,要更宽用单词) - 不确定时同义词都试一遍:`search("CO2 absorption")` 没结果 → 试 `search("carbon dioxide capture")` - 中文期刊 paper 也可中文 keyword 单独搜一次(`search("水泥") + filter 中文期刊` 命中率不算低,但远不如英文主搜) ## 四个函数 ### `search(keyword="", year=None, year_gte=None, year_lte=None, doi="", first_author="", publication_name="", has_pdf=None, is_oa=None, limit=10) -> list[dict]` 搜文献,返回精简列表(每条含 16 字段:id / doi / title / first_author / first_author_institution / publication_year / publication_date / publication_name / has_fulltext_pdf / has_fulltext_xml / has_abstract / is_oa / type / **abstract** / **pdf_url** / **xml_url**)。 - `keyword`: SearchFilter,匹配 title / first_author / first_author_institution(**英文为主**,见上节) - `year` / `year_gte` / `year_lte`: 精确年份 / 范围(做"近 5 年文献"用 `year_gte=2020`) - `doi`: 精确 DOI(命中 0 / 1 条) - `first_author` / `publication_name`: 精确作者 / 期刊 - `has_pdf=True` 仅返 PDF 已下好的;`False` 仅返没 PDF 的;`None` 都返 - `is_oa=True` 仅返开放获取(OA);`False` 仅返非 OA;`None` 都返 - `limit`: 默认 10,上限 50 ```python # 找近 5 年水泥水化研究,且 PDF 已下好 papers = search(keyword="cement hydration", year_gte=2020, has_pdf=True, limit=10) for p in papers: print(p["title"], p["publication_year"]) if p["abstract"]: print(p["abstract"][:200]) # 看摘要前 200 字判断是否切题 ``` ### `get_paper(id_or_doi) -> dict` 取单条完整 metadata + abstract。`id_or_doi` 既接受 paper_server 内部 id,也接受 DOI(自动解析)。 **list 端点已带 abstract,正常工作流不需要调本函数** —— 仅在用户给单个 id / DOI 想拿全字段(含 OpenAlex 原始字段如 `o_keywords` 等)时用。 ```python paper = get_paper("10.1016/j.cemconres.2020.106156") print(paper["title"]) print(paper["abstract"]) ``` ### `fetch_pdf(id_or_doi, working_dir) -> str` 下载 PDF 到 `/papers/.pdf`,返回相对路径 `papers/.pdf`(safe_doi 把 `/` 换成 `_`)。**走 paper_server media 静态直链**(从 list/retrieve 返回的 `pdf_url` 字段),跟 `fetch_xml` 同范式。已存在跳过下载直接复用。 `has_fulltext_pdf=False` 或 `pdf_url` 空(publication_date 缺失)→ 抛 `RuntimeError`。 ```python rel = fetch_pdf("10.1016/j.cemconres.2020.106156", working_dir=r"D:/projects/zcbot/workspace/users//") # rel == "papers/10.1016_j.cemconres.2020.106156.pdf" ``` ### `fetch_xml(id_or_doi, working_dir) -> str` 下载 XML 到 `/papers/.xml`,对称 `fetch_pdf`。**走 paper_server 的 media 静态直链**(由 list/retrieve 返回的 `xml_url` 字段提供),paper_pdf_view 只覆盖 PDF,XML 没对应 API。已存在跳过下载直接复用。 `has_fulltext_xml=False` 或 `xml_url` 空(publication_date 缺失时会空)→ 抛 `RuntimeError`。 **为什么 XML 优先 PDF**:XML 已结构化 —— 章节标题 / 摘要 / 段落 / 参考文献 / 图表 caption 都有标签,LLM 读取无需 OCR 或 PDF 文本抽取的不确定性;文献综述 / 引文清单 / 章节定位场景比 PDF 友好得多。能拿 XML 就别拿 PDF;只有需要看具体公式 / 图表内容 / 表格数据时才下 PDF。 ## 标准工作流 1. **(若用户输入中文)转专业英文术语** 2. **search**:按 keyword + filter(年份范围 / OA / has_pdf 等)缩窄候选,`limit=10` 起 3. **直接看返回里的 abstract**(list 端点已带): - abstract 非空 → 看前 200-400 字判断切题 - abstract 空(`has_abstract=False`)→ 仅凭 title + 期刊 + 年份判断,信号弱时下条候选 4. **下全文(若需要)** —— **优先级**: - `has_fulltext_xml=True` → `fetch_xml`(LLM 友好,结构化) - 仅 `has_fulltext_pdf=True` → `fetch_pdf`(回退,需要 PDF 文本抽取) - 两者都 False → 仅凭 abstract 写综述,告诉用户哪几篇没全文 5. **read 全文**:fetch 返回相对路径,用主 agent 的 `read` 工具读取(zcbot 已内置 PDF 文本抽取;XML 直接当文本读) ## 错误处理 - 网络超时 / paper_server 不可达:`httpx.ConnectError` / `httpx.TimeoutException` —— 告诉用户"paper_server 暂时连不上",不要重试堆栈刷屏 - `doi 未命中` / `doi 命中多条`:`get_paper` / `fetch_pdf` / `fetch_xml` 内部 `_resolve_to_id` 抛 `ValueError` —— DOI 拼写错或库里没收录,改 keyword 重搜 - `has_fulltext_pdf=False` / `has_fulltext_xml=False`:`fetch_pdf` / `fetch_xml` 抛 `RuntimeError` —— 服务器还没下到对应格式;若另一格式存在则换用,都没就只能用 abstract - `xml_url` 空(publication_date 缺失,paper 落到 unknown 目录):`fetch_xml` 抛 `RuntimeError(xml_url unavailable...)` —— 改试 `fetch_pdf` - 文件 disk 缺失(`has_fulltext_pdf=True` 但 paper_server 侧文件丢了 → HTTP 404):helper 透传 `httpx.HTTPStatusError`,告诉用户换一篇 - `abstract` 字段为空字符串:正常情况,不是错;告诉用户"这篇没收录摘要"即可 - search 命中 0 条:先尝试同义词 / 缩短 keyword / 放宽 filter,3 次还是 0 条告诉用户库里没覆盖,**不要凭训练数据脑补文献** ## 反模式 - 用 `httpx` / `requests` 裸调 paper_server API(走 helper,免得 base_url / auth / 字段名漂移时四处改) - 用户输中文直接 `search(中文)` —— 转英文术语,见上节 - `search(limit=50)` 一次拉满后 dump 给 LLM 全文(只 print 前 5-10 条精简就够,要全部让用户自己 `print(papers)`) - 已经看到 abstract 还 `get_paper` 一遍(list 已带 abstract,重复调白费 roundtrip) - 没看 abstract 就 `fetch_pdf` / `fetch_xml`(80% 场景 abstract 够用,下载全文慢且费带宽) - `has_fulltext_xml=True` 时盲走 `fetch_pdf`(XML 对 LLM 更友好,先试 fetch_xml) - 编造 DOI / title / 作者 —— 不在 paper_server 库里就**明确告诉用户"未命中"**,不要凭训练数据脑补 - 把 `fetch_pdf` 返回的相对路径当绝对路径用(它是相对 `working_dir` 的)