Install any skill in seconds. Free to start, no credit card required.
Get Started Free →从 PubMed/PMC 论文取结构化全文并按章节切分为 RAG 友好的 chunk。当用户需要批量抽取论文全文、按 Methods/Results 等章节分段、提取表格上下文、构建文献向量库时,使用本 skill。支持 PMID(标题+摘要)和 PMCID(全文,开放获取子集)。token 级 RAG chunking 不跨章节,支持缓存与限流。
.claude/skills/ethanyoq-bioc-fulltext-fetch/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-19 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 127% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 123% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 221% | 0% |
封装 NLM BioC API,把 PubMed / PMC 文献转成 RAG 友好的结构化全文 chunk。只做全文抽取与切片,不做检索、不做实体标注、不做向量化。
触发场景:
不要使用本 skill:
pubmed-eutils efetchpubmed-search / europepmc-searchpubtator-entity-search(可在本 skill 输出之上叠加,不替代)medical-evidence-grading(其内部会调用本 skill 抽 Methods 段落)| 上游 / 下游 skill | 协作方式 | |------------------|----------| | pubmed-eutils.elink_pubmed_to_pmc() | 上游,把 PMID 列表转换成 PMCID 列表,喂给本 skill | | europepmc-search.get_full_text_links() | 替代源,当 PMC OA 不可用时回退到 Europe PMC 的开放获取链接 | | pubtator-entity-search | 后置叠加,在本 skill 产的 chunk 上做实体标注,不替代本 skill | | medical-evidence-grading | 下游,调 extract_paragraphs 取 Methods 段落做 sample size / study design 检测 |
典型 pipeline:
europepmc-search ──► PMID list ──► pubmed-eutils.elink ──► PMCID list
│
▼
bioc-fulltext-fetch (本 skill)
│
├─► [可选] pubtator-entity-search 实体叠加
├─► [可选] medical-evidence-grading 证据评分
└─► LangChain / LlamaIndex 入库| 端点 | 输入 | 覆盖 | 内容 | |------|------|------|------| | BioC PubMed | PMID | 全 PubMed (~37M) | 标题 + 摘要 + MeSH(结构化) | | BioC PMC OA | PMCID | PMC 开放获取子集(~10M,约占 PMC 25%) | 全文章节、段落、表格、图注 |
摘要级: https://www.ncbi.nlm.nih.gov/research/bionlp/RESTful/pubmed.cgi/BioC_json/{PMID}/unicode
全文级: https://www.ncbi.nlm.nih.gov/research/bionlp/RESTful/pmcoa.cgi/BioC_json/{PMCID}/unicode格式细节、section_type 取值、字符 offset 一致性见 references/bioc-format-spec.md。
PMID list
│
▼
[可选] elink PMID→PMCID (调 pubmed-eutils)
│
├─ PMCID 存在且 OA ──► fetch_fulltext_bioc(pmcid) ──► extract_paragraphs / extract_tables_context ──► to_rag_chunks
│
└─ 不可用/非 OA ─────► fetch_abstract_bioc(pmid) ─────────────────────────────────────────────────► to_rag_chunksfetch_abstract_bioc(pmid: str) -> BioCDocument取标题 + 摘要 + MeSH 的 BioC 结构化版本。
pythondoc = fetch_abstract_bioc("39523456") # doc.passages → [{"infons": {"section_type": "TITLE"}, "text": "..."}, # {"infons": {"section_type": "ABSTRACT"}, "text": "..."}] # doc.infons["mesh"] → ["Leukemia, Myeloid, Acute", "FLT3 Mutation", ...]
fetch_fulltext_bioc(pmcid: str) -> BioCDocument取 PMC OA 全文,按章节切分(section_type ∈ {TITLE, ABSTRACT, INTRO, METHODS, RESULTS, DISCUSS, CONCL, REF, FIG, TABLE, ...})。
pythondoc = fetch_fulltext_bioc("PMC10234567") # 不可用时抛 NotOpenAccessError,调用方应回退到 fetch_abstract_bioc
extract_paragraphs(doc: BioCDocument) -> list[Paragraph]按 BioC passage 提取段落数组,保留 section 标签 + 字符 offset。
pythonparagraphs = extract_paragraphs(doc) # [{"section_label": "METHODS", "text": "Flow cytometry was performed...", # "offset_start": 2890, "offset_end": 3401}, ...]
要点:跳过 REF / FIG_LABEL 纯标签段;合并同 section 内 < 50 字符短 passage;offset 来自 BioC passage.offset。
extract_tables_context(doc: BioCDocument, window: int = 200) -> list[TableContext]提取每个表格的上下文(前后 200 字),帮 RAG 给表打语义标签。
pythontables = extract_tables_context(doc, window=200) # [{"table_id": "T1", "caption": "Patient baseline characteristics", # "before_context": "...", "after_context": "...", "section": "RESULTS"}, ...]
表格本体(cell 数据)不解析;只取 caption + 周围文本。
to_rag_chunks(doc: BioCDocument, chunk_size: int = 512, overlap: int = 50) -> list[Chunk]把 BioC 文档切成 RAG 入库 chunk。token 单位、不跨章节、段落优先、可追溯 chunk_id。
pythonchunks = to_rag_chunks(doc, chunk_size=512, overlap=50) # { # "chunk_id": "PMC10234567_METHODS_2890", # "text": "...", # "metadata": {"pmcid": "...", "pmid": "...", "section": "METHODS", # "offset_start": 2890, "offset_end": 3401, "doi": "...", # "year": 2024, "is_oa": True, "title": "...", "journal": "..."} # }
切片策略详见 references/chunking-strategies.md。
asyncio.Semaphore(5) 或 aiolimiter~/.cache/bioc/{id}.json + SQLite 索引 index.db(key, file_path, fetched_at, is_oa, doi, year)BIOC_CACHE_TTL_DAYS),force_refresh=True 绕过所有错误继承 BioCFetchError 基类。
| 错误 | 类型 | 处理 | |------|------|------| | 404 (PMC OA) | NotOpenAccessError | 调用方回退 abstract | | 404 (PubMed) | NotFoundError | PMID 不存在,跳过 | | 429 | RateLimitError | 指数退避 1s/2s/4s,最多 3 次 | | 5xx | BioCServerError | 重试 3 次后抛出 | | JSON 解析失败 | BioCParseError | 记录原始响应,跳过 | | 超时(默认 30s) | BioCTimeoutError | 重试 1 次 |
section_type 命名有差异(Methods vs Materials and Methods vs Subjects and Methods),BioC 已部分归一化但仍有例外(综述偶尔出现 INTRO_RESULTS 合并段)。下游过滤时用集合而非精确匹配。BioC_xml 端点取,或解析时检测 ? 占位符。to_rag_chunks 默认按 section 分组,不要绕过。fetched_at 而非只看文件存在。failed.jsonl),不能让一篇阻断整批。pubtator-entity-search 后置叠加。LangChain / LlamaIndex / 元数据过滤检索 / pubtator 实体叠加 / medical-evidence-grading 联用的完整代码示例见 references/rag-integration.md。
toml[project] dependencies = [ "httpx>=0.27", # 异步 HTTP "aiolimiter>=1.1", # RPS 限流 "tiktoken>=0.7", # token 计数 "tenacity>=9.0", # 退避重试 "platformdirs>=4.0", # 缓存目录 ]
环境变量:BIOC_CACHE_DIR(默认 ~/.cache/bioc)、BIOC_CACHE_TTL_DAYS(30)、BIOC_MAX_RPS(5)、BIOC_TIMEOUT_SEC(30)。
| 测试 | 预期 | |------|------| | fetch_abstract_bioc("39523456") | 返回 title + abstract passages | | fetch_fulltext_bioc("PMC10234567") | 返回章节切分的全文 | | fetch_fulltext_bioc("PMC1") 非 OA | 抛 NotOpenAccessError | | extract_paragraphs(doc) | 段落 ≥ 3,每段有 section_label | | extract_tables_context(doc) | 上下文左右各 ≤ 200 字 | | to_rag_chunks(doc, 512, 50) | chunk token ≤ 512、overlap ≈ 50、不跨章节 | | 缓存命中 | 第二次调用 < 50ms,无网络请求 | | 429 退避 | 模拟 429 后能在 3 次内成功 |
最低单元测试覆盖率 80%,集成测试用 5 篇真实 PMCID 跑端到端。
references/bioc-format-spec.md — BioC XML/JSON 格式规范、字段定义、section_type 取值references/rag-integration.md — LangChain Chroma + LlamaIndex 完整代码、批量入库要点、向量库选型references/chunking-strategies.md — token 切片、不跨章节策略、overlap 设计、chunk_id 设计、评测指标| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | fail→pass | 19,174 | 16,520 | -14% | 1 | 1 | 0% | 3,310 | 6,373 | +93% | 0 | 0 | — |
case-01 | pass→pass | 18,016 | 17,934 | -0% | 1 | 1 | 0% | 3,705 | 7,356 | +99% | 0 | 0 | — |
case-02 | fail→pass | 15,216 | 16,317 | +7% | 1 | 1 | 0% | 2,756 | 6,247 | +127% | 0 | 0 | — |
case-15 | fail→pass | 10,690 | 8,451 | -21% | 1 | 1 | 0% | 2,252 | 5,022 | +123% | 0 | 0 | — |
case-03 | pass→pass | 13,963 | 9,427 | -32% | 1 | 1 | 0% | 3,089 | 5,093 | +65% | 0 | 0 | — |
case-04 | fail→pass | 16,537 | 8,011 | -52% | 1 | 1 | 0% | 2,726 | 4,995 | +83% | 0 | 0 | — |
case-05 | fail→fail | 15,381 | 11,087 | -28% | 1 | 1 | 0% | 2,705 | 5,121 | +89% | 0 | 0 | — |
case-06 | fail→pass | 5,737 | 3,558 | -38% | 1 | 1 | 0% | 1,217 | 3,905 | +221% | 0 | 0 | — |
case-16 | pass→pass | 16,843 | 11,879 | -29% | 1 | 1 | 0% | 2,781 | 5,123 | +84% | 0 | 0 | — |
case-07 | pass→pass | 8,164 | 3,026 | -63% | 1 | 1 | 0% | 1,439 | 3,654 | +154% | 0 | 0 | — |
case-08 | fail→pass | 14,295 | 14,204 | -1% | 1 | 1 | 0% | 2,755 | 6,136 | +123% | 0 | 0 | — |
case-09 | fail→pass | 14,890 | 9,382 | -37% | 1 | 1 | 0% | 2,416 | 4,963 | +105% | 0 | 0 | — |
case-10 | fail→pass | 24,587 | 6,303 | -74% | 1 | 1 | 0% | 2,205 | 4,321 | +96% | 0 | 0 | — |
case-17 | pass→pass | 17,817 | 19,959 | +12% | 1 | 1 | 0% | 2,670 | 6,628 | +148% | 0 | 0 | — |
case-11 | pass→pass | 12,634 | 8,193 | -35% | 1 | 1 | 0% | 1,986 | 4,516 | +127% | 0 | 0 | — |
case-12 | fail→pass | 7,767 | 2,577 | -67% | 1 | 1 | 0% | 1,427 | 3,601 | +152% | 0 | 0 | — |
case-13 | fail→pass | 13,121 | 6,807 | -48% | 1 | 1 | 0% | 2,228 | 4,434 | +99% | 0 | 0 | — |
case-14 | pass→pass | 13,399 | 9,778 | -27% | 1 | 1 | 0% | 2,659 | 5,217 | +96% | 0 | 0 | — |
case-18 | pass→pass | 19,458 | 22,986 | +18% | 1 | 1 | 0% | 3,058 | 7,253 | +137% | 0 | 0 | — |
case-20 | fail→pass | 10,142 | 2,405 | -76% | 1 | 1 | 0% | 1,748 | 3,529 | +102% | 0 | 0 | — |
case-21 | fail→pass | 21,724 | 15,800 | -27% | 1 | 1 | 0% | 3,693 | 6,054 | +64% | 0 | 0 | — |
case-22 | fail→pass | 11,561 | 7,076 | -39% | 1 | 1 | 0% | 2,124 | 4,308 | +103% | 0 | 0 | — |
case-23 | fail→pass | 12,987 | 6,622 | -49% | 1 | 1 | 0% | 2,331 | 4,329 | +86% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +61 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.