上个月导师让我整理一份关于蛋白质折叠预测的文献综述,涉及 2023-2026 年的 120 多篇论文。手动一篇篇读摘要、提取关键信息、归类整理,我算了下大概要两周。后来我用 Gemini 3.5 Flash + LangChain 搭了一条文献综述 Agent 链路——从文献检索到摘要提取再到结构化输出,整个流程跑下来大概 3 小时搞定初稿,后面人工修订花了半天。说实话效果超出预期。

这篇文章把我的完整实现方案贴出来,包括踩过的坑和最终可复用的代码模板。如果你也有类似的科研文献整理需求,直接 fork 改改参数就能用。

先说结论

环节 手动耗时 Agent 耗时 提速比
文献检索+筛选 8h 15min 32x
摘要提取+关键信息 16h 45min 21x
结构化归类输出 6h 12min 30x
人工审校修订 - 4h -
总计 30h+ ~5.2h ~5.8x

核心技术栈:LangChain 0.3 + Gemini 3.5 Flash API + Semantic Scholar API + Pydantic 结构化输出。

整体架构

graph TD
 A[输入:研究主题+关键词] --> B[文献检索 Agent]
 B --> C[Semantic Scholar API]
 C --> D[候选论文列表 N=200+]
 D --> E[相关性筛选 Agent]
 E --> F[高相关论文 N=50-80]
 F --> G[摘要提取 Agent]
 G --> H[结构化信息抽取]
 H --> I[分类聚合 Agent]
 I --> J[输出:Markdown 综述框架]

环境准备

pip install langchain==0.3.4 langchain-google-genai==2.1.0 pydantic==2.7 httpx semanticscholar

你需要两个 API Key:
- Gemini API(用于 LLM 推理)
- Semantic Scholar API Key(免费申请,限速 100 req/s)

第一步:文献检索模块

Semantic Scholar 的 API 免费且数据量大,比 PubMed 覆盖面广。我一开始想用 Google Scholar,但它没有官方 API,爬虫方案太脆弱。

import httpx
from typing import List, Dict

class PaperSearcher:
 def __init__(self, api_key: str):
 self.base_url = "https://api.semanticscholar.org/graph/v1"
 self.headers = {"x-api-key": api_key}

 def search(self, query: str, year_range: str = "2023-2026", limit: int = 200) -> List[Dict]:
 params = {
 "query": query,
 "year": year_range,
 "limit": limit,
 "fields": "title,abstract,year,authors,citationCount,venue,publicationDate"
 }
 resp = httpx.get(
 f"{self.base_url}/paper/search",
 params=params,
 headers=self.headers,
 timeout=30.0
 )
 if resp.status_code != 200:
 raise Exception(f"Semantic Scholar API error: {resp.status_code} - {resp.text}")

 data = resp.json()
 # 过滤掉没有摘要的论文
 papers = [p for p in data.get("data", []) if p.get("abstract")]
 print(f"检索到 {len(papers)} 篇有摘要的论文")
 return papers

实测 protein folding prediction 这个 query,返回 187 篇有摘要的论文,耗时 2.3s。

第二步:相关性筛选 Agent

200 篇论文不可能全要,得让 LLM 判断相关性。这一步是整个链路里最关键的——筛选质量直接决定综述质量。

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
from openai import OpenAI

# 用 OpenAI 兼容接口调 Gemini,方便后续切模型
client = OpenAI(
 api_key="your-api-key",
 base_url="https://api.ofox.io/v1"
)

class RelevanceScore(BaseModel):
 score: int = Field(description="相关性评分 1-5,5为最相关")
 reason: str = Field(description="判断理由,一句话")

def score_relevance(paper: Dict, research_topic: str) -> dict:
 prompt = f"""你是一个科研文献筛选助手。请判断以下论文与研究主题的相关性。

研究主题:{research_topic}

论文标题:{paper['title']}
摘要:{paper['abstract'][:500]}
发表年份:{paper['year']}
引用数:{paper.get('citationCount', 0)}

请返回 JSON 格式:{{"score": 1-5的整数, "reason": "一句话理由"}}
只返回 JSON,不要其他内容。"""

 response = client.chat.completions.create(
 model="gemini-3.5-flash",
 messages=[{"role": "user", "content": prompt}],
 temperature=0.1,
 max_tokens=150
 )

 import json
 result = json.loads(response.choices[0].message.content)
 return result

我把阈值设在 score >= 4,187 篇最终筛下来 63 篇。Gemini 3.5 Flash 跑这个任务单次延迟在 280-350ms,187 次调用并发 10 路大概 3 分钟跑完。

第三步:结构化摘要提取

这一步要从每篇论文的摘要里提取:研究方法、核心贡献、数据集、性能指标。用 Pydantic 做 schema 约束输出格式。

from pydantic import BaseModel, Field
from typing import Optional, List

class PaperSummary(BaseModel):
 title: str
 year: int
 method: str = Field(description="核心方法/模型名称")
 contribution: str = Field(description="主要贡献,2-3句话")
 datasets: List[str] = Field(description="使用的数据集")
 metrics: Optional[str] = Field(description="关键性能指标,如有")
 category: str = Field(description="归类:预训练模型/微调方法/数据增强/评估框架/应用场景")

def extract_structured_info(paper: Dict, topic: str) -> dict:
 prompt = f"""从以下论文摘要中提取结构化信息。研究领域:{topic}

标题:{paper['title']}
摘要:{paper['abstract']}

请严格按以下 JSON schema 输出:
{{
 "title": "论文标题",
 "year": 年份数字,
 "method": "核心方法名",
 "contribution": "主要贡献2-3句",
 "datasets": ["数据集1", "数据集2"],
 "metrics": "关键指标或null",
 "category": "预训练模型|微调方法|数据增强|评估框架|应用场景 五选一"
}}"""

 response = client.chat.completions.create(
 model="gemini-3.5-flash",
 messages=[{"role": "user", "content": prompt}],
 temperature=0.0,
 max_tokens=500
 )

 import json
 raw = response.choices[0].message.content
 # 处理可能的 markdown 代码块包裹
 if raw.startswith("```"):
 raw = raw.split("\n", 1)[1].rsplit("```", 1)[0]

 return json.loads(raw)

第四步:分类聚合 + 综述框架生成

提取完所有论文信息后,按 category 聚合,然后让 LLM 生成综述框架。

from collections import defaultdict

def generate_survey_outline(summaries: List[dict], topic: str) -> str:
 # 按类别分组
 grouped = defaultdict(list)
 for s in summaries:
 grouped[s["category"]].append(s)

 # 构造分组摘要
 group_text = ""
 for cat, papers in grouped.items():
 group_text += f"\n### {cat}({len(papers)}篇)\n"
 for p in papers[:5]: # 每类展示前5篇
 group_text += f"- {p['title']} ({p['year']}): {p['contribution'][:80]}\n"

 prompt = f"""你是一个科研写作助手。基于以下已分类的论文信息,生成一份文献综述的完整框架。

研究主题:{topic}
论文总数:{len(summaries)}篇

分类汇总:
{group_text}

要求:
1. 生成 Markdown 格式的综述框架
2. 包含:引言、各技术方向的小节、对比分析、未来方向
3. 每个小节标注应引用哪些论文(用论文标题标注)
4. 写出每个小节的 2-3 句核心论述"""

 response = client.chat.completions.create(
 model="gemini-3.5-flash",
 messages=[{"role": "user", "content": prompt}],
 temperature=0.3,
 max_tokens=3000
 )

 return response.choices[0].message.content

踩坑记录

坑 1:Gemini 返回的 JSON 偶尔带 markdown 代码块

大概 15% 的请求会返回这种格式:

```json
{"score": 4, "reason": "..."}

解决办法就是上面代码里那个 strip 逻辑。挺烦人的,OpenAI 的模型这个问题少很多,但 Gemini 3.5 Flash 便宜太多了(输入 $0.075/1M tokens vs GPT-5.5 的 $2.5/1M),科研场景跑大量文本这个价格差距是 33 倍。

**坑 2:Semantic Scholar 限速**

免费 Key 标称 100 req/s,但实测超过 50 req/s 就开始偶发 429:

HTTP 429: Too Many Requests - Rate limit exceeded. Please slow down.


我最终用了 asyncio.Semaphore(30) 控制并发,稳定不报错。

**坑 3:摘要太长导致分类不准**

有些论文摘要超过 2000 字(综述类论文的摘要特别长),塞进 prompt 后 Gemini 的分类准确率从 91% 掉到 74%。解决方案:摘要超过 500 字的先做一次压缩再分类。

**坑 4:category 输出不在预设选项里**

约 8% 的情况 Gemini 会输出「混合方法」「其他」这种不在我五选一列表里的分类。加了一个后处理映射表:

```python
CATEGORY_MAP = {
 "混合方法": "微调方法",
 "其他": "应用场景",
 "综合": "评估框架",
}

不完美,但目前没找到比硬编码映射更好的办法。

成本核算

跑一次完整的 63 篇论文综述 Agent:

步骤 调用次数 平均 tokens/次 总 tokens 费用
相关性筛选 187 ~800 149,600 $0.011
结构化提取 63 ~1,200 75,600 $0.006
综述生成 1 ~4,000 4,000 $0.0003
合计 ~229,200 ~$0.017

没算错,一毛二人民币。Gemini 3.5 Flash 跑这种批量文本处理任务真的是白菜价。

完整调用串起来

import asyncio

async def run_survey_agent(topic: str, keywords: List[str]):
 # 1. 检索
 searcher = PaperSearcher(api_key="your-s2-key")
 all_papers = []
 for kw in keywords:
 papers = searcher.search(f"{topic} {kw}", year_range="2023-2026")
 all_papers.extend(papers)

 # 去重
 seen = set()
 unique_papers = []
 for p in all_papers:
 if p["title"] not in seen:
 seen.add(p["title"])
 unique_papers.append(p)

 print(f"去重后共 {len(unique_papers)} 篇")

 # 2. 筛选(并发)
 sem = asyncio.Semaphore(10)
 async def score_one(paper):
 async with sem:
 return {**paper, **score_relevance(paper, topic)}

 scored = await asyncio.gather(*[score_one(p) for p in unique_papers])
 relevant = [p for p in scored if p["score"] >= 4]
 print(f"相关论文 {len(relevant)} 篇")

 # 3. 结构化提取
 summaries = []
 for p in relevant:
 info = extract_structured_info(p, topic)
 summaries.append(info)

 # 4. 生成综述框架
 outline = generate_survey_outline(summaries, topic)

 return outline, summaries

# 运行
outline, data = asyncio.run(run_survey_agent(
 topic="protein structure prediction",
 keywords=["AlphaFold", "language model", "geometric deep learning", "diffusion model"]
))

print(outline)

我也不确定的地方

相关性筛选那一步,用 LLM 打分 vs 用 embedding 算余弦相似度,哪个更好?我两个都试了,LLM 打分在「理解研究方向的细微差别」上明显更强(比如能区分"蛋白质折叠预测"和"蛋白质折叠病"),但 embedding 方案快 10 倍且零 API 成本。如果你的研究主题很明确、不容易有歧义,embedding 可能就够了。

Gemini 3.5 Flash 的 1M token 上下文窗口理论上可以把所有摘要一次性塞进去让它分类,但我试了一下,超过 80 篇时分类质量明显下降。分批处理虽然慢点但结果更靠谱。

小结

这套 Agent 链路的核心思路:把文献综述拆成检索→筛选→提取→聚合四个原子步骤,每步用 LLM 做一个窄任务。Gemini 3.5 Flash 在这种「大量短文本、低创造性、高结构化」的场景下性价比极高。代码模板放在上面了,换个 topic 和 keywords 就能跑你自己的方向。聚合 API 方面我用的 ofox.io,OpenRouter 也行,主要是 OpenAI 兼容接口方便 LangChain 切模型不用改代码——哪天想换 Claude Sonnet 4.6 跑对比实验,改一行 model 参数就完事。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐