跳到主要内容

GPT Researcher — 架构与原理

30 秒导读: GPT Researcher 是一个「自动做研究、出报告」的 agent。你给它一句问题(比如「2024 年固态电池的商业化进展」),它会自己上网搜、抓网页、筛出相关内容,最后写一份几千字、带引用来源的 Markdown 报告。它的核心不是某个花哨模型,而是一套编排:把问题拆成多条子查询并行研究,用词向量相似度把海量网页压成精华上下文,再让 LLM 基于这堆精华一次性写完报告——用工程手段对抗「幻觉、慢、上下文塞不下」。


1. 这是什么(零基础也能懂)

一句话定义: GPT Researcher 是一个深度研究 agent(deep research agent)——输入一个开放式研究问题,它自主完成「上网搜索 → 抓取网页 → 筛选相关内容 → 撰写带引用的长报告」全过程,人不用插手。

解决什么问题 / 给谁用:

假设你要写一份调研:

"对比 2024 年主流开源向量数据库的性能与生态。"

手动做要几小时:搜十几个关键词、开几十个网页、读、摘录、组织、写、标引用。GPT Researcher 把这套流程自动化。它面向需要快速产出可信调研的人:分析师、研究者、开发者,也可作为库嵌进别的应用(它本身就被打包成 Claude Skill,见 README.md)。

它能做什么:

  • 面向 Web 或本地文档(PDF / Word / 向量库)做研究,report_source 决定源(gpt_researcher/utils/enum.py 里的 ReportSource)。
  • 输出多种报告形态:标准报告、资源清单、大纲、详细报告(多章)、深度研究(递归),见 ReportType
  • 抗幻觉:所有结论都基于抓来的真实网页,报告末尾附引用 URL。
  • 并行加速:子查询、抓取、深度分支都用 asyncio.gather 并发跑。

用起来什么样:

最小调用(摘自 README.md 的库用法)大致是:

# 示意,非源码:典型三步调用
from gpt_researcher import GPTResearcher
researcher = GPTResearcher(query="固态电池2024商业化进展", report_type="research_report")
await researcher.conduct_research() # 搜 + 抓 + 压缩,攒出 context
report = await researcher.write_report() # 基于 context 写出带引用的报告

注意这是两段式 API:conduct_research() 先把「研究材料」攒好放进 self.context,write_report() 再基于它写。这个切分贯穿整个架构——见 gpt_researcher/agent.py:330(conduct_research)和 gpt_researcher/agent.py:450(write_report)。

一句话直觉/类比: 把它想成一个尽职的研究助理:先列出「要查哪几个方向」(规划),分头去搜集资料(执行),把每份资料里只跟问题相关的段落划重点、其余扔掉(压缩),最后合起来写成一篇有出处的报告。它省你力气的地方,恰恰是「筛掉无关内容」和「并行铺开」。


2. 顶层全景(它大概怎么转)

整个系统是一个**协调器 + 一组技能(skill)**的结构。中枢是 GPTResearcher 类(gpt_researcher/agent.py:36),它在 __init__ 里把每个子系统实例化成一个 skill 对象,自己只做编排、不做脏活(agent.py:184-197)。

一次标准研究的数据流:

主要部件一句话职责:

部件干什么在哪个文件
GPTResearcher顶层协调器:持有全部 skill,暴露 conduct_research / write_reportgpt_researcher/agent.py
ResearchConductor研究主循环:规划子查询、并发跑、合并 contextgpt_researcher/skills/researcher.py
plan_research_outline「先规划」:把原问题拆成多条 google 式子查询gpt_researcher/actions/query_processing.py
retriever(如 Tavily)搜索引擎抽象:一条 query → 一批带 URL 的结果gpt_researcher/retrievers/
Scraper按 URL 类型分派抓取器(网页/PDF/arxiv)gpt_researcher/scraper/scraper.py
ContextCompressor用 embedding 相似度把网页压成相关片段gpt_researcher/context/compression.py
SourceCurator可选:用 LLM 再评一遍来源的可信度与相关性gpt_researcher/skills/curator.py
ReportGenerator基于 context 生成报告/引言/结论/子主题gpt_researcher/skills/writer.py
DeepResearchSkill深度模式:递归研究树,越挖越深gpt_researcher/skills/deep_research.py

主线走一遍(高层,不进代码):

  1. conduct_research() 先(若没预设)调 choose_agent 让 LLM 给这次研究选一个「人设」——比如给财经问题配「资深金融分析师」的 system prompt(agent.py:359)。
  2. ResearchConductor.conduct_research,按 report_source 分派;Web 场景走 _get_context_by_web_search(researcher.py:266)。
  3. 先规划: plan_research 先用第一个 retriever 快搜一轮拿点背景,再让战略 LLM据此拆出 N 条子查询(researcher.py:48)。
  4. 再执行: 对每条子查询,_process_sub_query 并行地「搜 URL → 抓正文 → embedding 压缩出相关片段」(researcher.py:449)。
  5. 所有子查询的片段用空格拼成一个大 context 串(researcher.py:357-361),存进 self.context;若开了 curate_sources 再让 LLM 排一遍。
  6. write_report() 把整个 context 塞进写作 prompt,让智能 LLM一次性流式写出带引用的报告(writer.py:124report_generation.py:275)。

一个关键认知:三个不同的 LLM 岗位。 GPT Researcher 刻意区分三种模型角色(默认见 gpt_researcher/config/variables/default.py:7-9):

角色默认模型用在哪
FAST_LLMgpt-4o-mini轻量摘要等便宜活
SMART_LLMgpt-4.1写长报告(要 2k+ 字长输出)
STRATEGIC_LLMo4-mini规划子查询(要推理,慢但准)

「规划用推理模型、写作用长输出模型、杂活用便宜模型」——这是理解它成本/质量取舍的一把钥匙。


3. 核心机制

3.1 先规划再执行:把一个问题炸成一束子查询

要解决的小问题: 直接拿原问题去搜一次,覆盖面窄、容易漏。人做研究会先想「这题该从哪几个角度查」。

思路: 借鉴 Plan-and-Solve 论文(README 明说),GPT Researcher 先让 LLM 把问题分解成多条互补的搜索查询,再对每条并行研究。分解前还会先快搜一轮,把「实时网络背景」喂给规划 LLM,让子查询更贴当下(researcher.py:62)。

真实实现: 规划入口是 plan_research(gpt_researcher/skills/researcher.py:48),它调 plan_research_outline(gpt_researcher/actions/query_processing.py:112),后者再调 generate_sub_queries(query_processing.py:37)。子查询的 prompt 很直白——generate_search_queries_prompt(gpt_researcher/prompts.py:213)让模型「写 N 条 google 搜索查询,以列表形式返回」,N 由 MAX_ITERATIONS(默认 3)控制。

巧妙细节 —— 三级降级容错。 规划用的战略 LLM(如 o4-mini)有时会因 max_tokens 参数不兼容而报错。generate_sub_queries 用了三层 try/except 降级(query_processing.py:71-108):先不带 token 上限试 → 失败则带上限重试 → 再失败回退到 smart LLM。这保证「规划这步绝不因为某个推理模型的怪脾气而整个崩掉」。

另一个细节 —— 解析用 json_repair 而非 json.loads LLM 返回的列表经常不是严格 JSON(多个逗号、缺引号、包在 ```json 围栏里)。它用 json_repair.loads(query_processing.py:110)去容错解析;深度模式里更进一步,_load_repaired_json 会先用多个正则把 JSON 从 markdown/散文里抠出来再修(deep_research.py:48-74)。这是「和 LLM 打交道」的现实工程:输出永远不完全守约,解析层必须皮实。

3.2 用 embedding 相似度压缩上下文(而不是硬塞)

要解决的小问题: 一条子查询可能抓回十几个网页、几十万字。全塞进 LLM 既超上下文又贵又稀释重点。得只留「跟这条子查询真正相关」的段落。

思路: 不用 LLM 去读全文筛(慢又贵),而是走一条纯向量的 RAG 管道:把网页切块 → 对每块算 embedding → 只保留和查询向量余弦相似度超阈值的块。相当于用「语义搜索」在抓来的内容里再做一次检索。

原理演示:

# 示意,非源码:embedding 过滤的核心想法
chunks = split_into_chunks(pages, size=1000, overlap=100) # 切块
q_vec = embed(query) # 查询向量
kept = [c for c in chunks
if cosine(embed(c), q_vec) > 0.35] # 只留够相关的块
context = "\n".join(kept) # 拼成相关上下文

真实实现: 核心是 ContextCompressor(gpt_researcher/context/compression.py:85)。它用 LangChain 的 DocumentCompressorPipeline 串起两个 transformer:RecursiveCharacterTextSplitter(1000 字块、100 重叠)+ EmbeddingsFilter(按相似度阈值过滤),见 compression.py:122-141__get_contextual_retriever。阈值默认 0.35,可用环境变量 SIMILARITY_THRESHOLD 调(compression.py:119)。

巧妙细节 —— 小内容走快路,跳过压缩。 async_get_context 先估算文档总字数;若小于 COMPRESSION_THRESHOLD(默认 8000)且文档数不超过 max_results,就直接返回原文、跳过整条 embedding 管道(compression.py:157-171)。因为对已经很短的内容做切块+向量过滤纯属浪费——省钱又省一次 embedding 调用。这是典型的「常见情况优化」。

成本记账。 走压缩路径时会先 cost_callback(estimate_embedding_cost(...)) 把 embedding 费用记进当次研究的账上(compression.py:175-176)。整个系统的 add_costs 会按当前步骤(_current_step)分桶累计成本(agent.py:717-738),所以最后能给出「规划花了多少、写作花了多少」的分项账单。

3.3 两种进阶编排:递归研究树 vs 子研究员 DAG

标准报告是「一层」的,但两种高级报告类型把上面的机制递归/嵌套起来,值得单独看。

A. 深度研究 = 递归研究树。 DeepResearchSkill(gpt_researcher/skills/deep_research.py:244)由两个旋钮控制:breadth(每层铺几条查询)和 depth(往下挖几层)。它的 deep_research 方法(deep_research.py:376)干这么几件事:

  1. 用 LLM 生成 breadth 条子查询(generate_search_queries)。
  2. 对每条子查询,新建一个完整的 GPTResearcher 实例跑一遍标准研究,拿到 context(deep_research.py:422-438)。
  3. 从结果里让 LLM 抽出「learnings(要点)+ followUpQuestions(追问)」(process_research_results,deep_research.py:344)。
  4. 若 depth > 1:把「上一层的研究目标 + 追问」拼成新 query,breadth 减半、depth 减一,递归往下挖(deep_research.py:494-514)。

这就形成一棵指数收窄的研究树:越往深,分支越少、问题越聚焦。并发由 asyncio.Semaphore(concurrency_limit) 限流(deep_research.py:413),避免同时开太多研究实例打爆 API。最后所有 learnings 去重、context 用 trim_context_to_word_limit(gpt_researcher/skills/deep_research.py:213)裁到 MAX_CONTEXT_WORDS(2.5 万词)以内(调用点 deep_research.py:529)。

B. 详细报告 = 子研究员 DAG。 DetailedReport(backend/report_type/detailed_report/detailed_report.py:10)走的是分而治之:

  1. 先对主问题做一轮标准研究攒 global_context(_initial_research,detailed_report.py:93)。
  2. 让 LLM 把主题拆成若干 subtopic(get_subtopics)。
  3. 对每个 subtopic,**再 new 一个 GPTResearcher(report_type="subtopic_report")**当子研究员,继承已访问 URL、已写章节、共享 context,单独研究并写出这一章(_get_subtopic_report,detailed_report.py:138)。
  4. 写子章前,用 WrittenContentCompressor(embedding)去查「之前章节里有没有写过类似内容」,避免各章重复(detailed_report.py:176context_manager.py:120)。
  5. 最后拼引言 + 目录 + 各章 + 结论 + 引用,组成整份长报告(_construct_detailed_report,detailed_report.py:197)。

两者的共同暗线: GPTResearcher 实例是可嵌套、可复用的原子单元。深度模式横向+纵向地生更多实例,详细模式按 subtopic 切成并列实例——都靠「共享 visited_urls / context」把这些独立实例串成一个协作整体。


4. 巧妙之处与局限

值得偷师的设计:

  • skill 化的协调器。 GPTResearcher 本身几乎不写业务逻辑,只把活分给 ResearchConductor / ContextManager / ReportGenerator 等 skill 对象,每个 skill 反过来持有 self.researcher 拿共享状态(见 agent.py:184-197 的成组初始化)。这让「换一个规划策略」「换一种压缩」都能局部替换,不动主干。
  • 拒绝无米之炊(abstain guard)。 若所有 retriever 都返回空(被封、限流、搜不到),write_report 不会硬编一份看着有理有据的报告,而是显式返回「我没能获取任何来源材料,无法产出可靠报告」(writer.py:81-88)。这是抗幻觉里最容易被忽略、却最诚实的一环。
  • 两段式 API 让流程可编排。 conduct_research()write_report() 分离,使得详细报告能「先研究、再对多个子主题分别写作」,深度模式能「只要 context 不要报告」(deep_research.py:603 注释明说 report 交给主 agent 写)。
  • 按 URL 类型分派抓取器。 Scraper.get_scraper 用后缀/域名把链接路由到不同抓取器:.pdf → PyMuPDF、arxiv.org → Arxiv、其余走默认(scraper/scraper.py:171-204)。同一套流程能吃网页、论文、PDF。
  • MCP 作为一等检索源,且不污染进程环境。 MCP retriever 可以和普通 retriever 混用,且有 fast/deep/disabled 三档策略缓存复用(researcher.py:279-327);_process_mcp_configs 特意不写 os.environ,避免并发请求互相污染(agent.py:282-308,修的是 issue #1676)。

边界与坑:

  • 成本与延迟随 breadth×depth 爆炸。 深度模式每个分支都 new 一个完整研究实例,每层又递归;breadth=4, depth=2 就可能跑十几次完整研究。旋钮开大 = API 账单和耗时指数上升。
  • 质量强依赖 retriever 与 embedding。 若搜索引擎(默认 Tavily)召回差,或 embedding 阈值把相关内容误杀,后面写得再好也是巧妇难为无米之炊。0.35 阈值是个全局经验值,不同领域未必最优。
  • 子查询规划由 LLM 主观决定。 拆得好不好、是否覆盖全,取决于战略 LLM 当次发挥;没有对「子查询是否互补/是否遗漏关键角度」的显式校验。
  • 详细报告的去重靠相似度而非严格约束。 各 subtopic 章节间的内容重叠靠 WrittenContentCompressor 的相似度过滤软性抑制(context_manager.py:88),并非硬性去重,长报告仍可能有内容交叠。
  • 一次性写作的长度受 SMART_LLM 上限约束。 报告是把整个 context 塞进一次 chat completion 流式生成(report_generation.py:275),超长研究会被 smart_token_limit 和上下文窗口卡住。

代码地图(导航索引)

关注点文件说明
顶层协调器 / 两段式 APIgpt_researcher/agent.pyGPTResearcherconduct_researchwrite_reportadd_costs(成本分桶)
研究主循环gpt_researcher/skills/researcher.pyResearchConductor_get_context_by_web_search_process_sub_query
子查询规划(先规划)gpt_researcher/actions/query_processing.pyplan_research_outlinegenerate_sub_queries(三级降级)
子查询 promptgpt_researcher/prompts.pygenerate_search_queries_promptgenerate_report_promptcurate_sources
embedding 上下文压缩gpt_researcher/context/compression.pyContextCompressorWrittenContentCompressorasync_get_context(快路优化)
上下文管理 skillgpt_researcher/skills/context_manager.pyContextManager.get_similar_content_by_query
深度研究(递归树)gpt_researcher/skills/deep_research.pyDeepResearchSkill.deep_researchtrim_context_to_word_limit
详细报告(子研究员 DAG)backend/report_type/detailed_report/detailed_report.pyDetailedReport.run_get_subtopic_report
报告生成gpt_researcher/skills/writer.py + gpt_researcher/actions/report_generation.pyReportGenerator.write_reportgenerate_report(abstain guard、流式写作)
来源筛选gpt_researcher/skills/curator.pySourceCurator.curate_sources
抓取分派gpt_researcher/scraper/scraper.pyScraper.get_scraper(按 pdf/arxiv/网页分派)
retriever 抽象gpt_researcher/actions/retriever.py + gpt_researcher/retrievers/get_retrievers、各引擎子目录
模型/行为默认值gpt_researcher/config/variables/default.pyFAST_LLM / SMART_LLM / STRATEGIC_LLMMAX_ITERATIONSDEEP_RESEARCH_*
报告/来源类型枚举gpt_researcher/utils/enum.pyReportTypeReportSourceTone