我用 Gemini 搭了一套文献综述 Agent,帮我省了 80% 的时间
上个月导师让我整理一份关于蛋白质折叠预测的文献综述,涉及 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 参数就完事。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)