夜雨聆风学习资料网

ARTICLE · 1030360

翻完 GPT Researcher 的源码,它最花心思的地方跟 AI 没关系

翻完 GPT Researcher 的源码,它最花心思的地方跟 AI 没关系

AI代码蜂巢X

探索编程的无限可能

编辑:嘉禾


CODE READING · 源码观察2026.09

Agent 拼的是更强的模型?

拆开一个活了三年的研究 Agent

里面全是四十年前的老手艺

五层分层 · 上下文压缩 · 显式契约 · 成本回调

GPT Researcher

AI AGENTPYTHON

在 Agent 这个赛道,一个项目活过一年就算长寿。

GPT Researcher 活了三年多。

它 2023 年 5 月建仓,到今年 8 月还在发版本,GitHub 上快 3 万 star。这种项目,值得从头翻一遍。

翻的时候本来想看两件事,现在主流的 deep research Agent 模型是怎么接的,prompt 是怎么写的。

结果翻完,最先想聊的是它的目录结构

代码分五层,从上往下。

最上面是后端 API 层,FastAPI 的接口、WebSocket 的事件推送、报告落盘都在这儿。往下一层是编排层,就一个文件,里面那个 GPTResearcher 是主协调器。再往下是 Skills 层,持有状态,负责驱动流程。然后是 Actions 层,里面全是纯动作函数,不记状态。最底下是 Providers 层,检索器、爬虫、模型接入,全堆在这儿。

看到这儿可能会觉得,这不就是常规分层吗。

对,是常规分层。但最有意思的是中间那条线,画在 Skills 和 Actions 之间

Skills 持有状态,Actions 不持有。这两类东西被严格分开了。分开的好处很实在,任何一个 Actions 里的函数都能单独拎出来测试,也都能被整个换掉,不用动上面任何一行代码。

整个项目翻下来,这个原则贯彻得特别彻底。没有一份几百行的主文件什么都干,而是一堆各管一件事的类。主协调一个,研究执行一个,写报告一个,管上下文一个,来源排序一个,递归研究一个,生图一个。所有 prompt 收在一个叫 PromptFamily 的类里,配置装载单独一个类。

配置也单独拎出来放,默认值一份文件,类型约束另一份,用的是 TypedDict

好,分层看完了。从请求进来那一刻开始,顺着往下走一遍。

后端开两个入口,一个是 REST 的接口,一个是 WebSocket。前端走的是后者。入口函数只做三件事,注入 MCP 环境变量,按报告类型路由到不同的执行体,再把日志推回前端。

这里有第一处不太直观的设计。MCP 的接入,不是新加一条代码路径,而是把 MCP 这个名字拼进检索器的配置里,跟别的检索器一起在运行时解析。

好处是 MCP 直接复用了同一套上下文合并逻辑,一行新代码都不用写。代价是排查问题的时候得倒着找,先翻配置,发现配置里压根没提 MCP,再去翻环境变量。

乍一看挺聪明,直到后面又撞见它一次。

再往下走。研究开始的第一件事,不是检索,是让 LLM 先挑一个角色

选角色那一步返回两个东西,一个是 agent 类型,一个是角色 prompt,这两个值后面会注入每一个生成环节。这里有个细节,注意成本回调这个参数,它在第一步就出现了。选角色这个动作本身也是一次 LLM 调用,很便宜,但照样计费。

选完角色,开始规划。这一步会先跑一次初始检索,把结果喂给 LLM,让它生成 3 到 5 个子查询。

这 3 到 5 个子查询,是整个系统的并行单位。每一个后面都会独立跑一遍完整的检索、抓取、压缩。

所以想调这个系统的研究质量,真正的旋钮在这儿,不在模型参数上。子查询切得好不好,直接决定它最后能不能覆盖到想要的面。

如果报告类型传的是深度模式,流程到这儿就拐走了,普通子查询那条路一步都不走。

单个子查询要过五道手。先是 MCP 检索,再是网页检索,然后爬取,接着做嵌入相似度压缩,最后把两路上下文合并起来。

这五步里,有三处细节特别容易漏。

MCP 检索器是怎么被认出来的

靠类名字符串。类名转小写之后,去找里面有没有 mcpretriever 这个词。没有接口标记,没有抽象基类,就是纯字符串匹配。写自定义检索器的人想让自己的东西被当成 MCP 用,命名就得服从这个约定。这算不算好设计,挺可疑的,但它确实能跑。

已抓取的数据是作为参数传进来的

不是方法内部的局部变量。这个口子是给多智能体模式留的。上层已经抓过的内容,直接传进来,不用重抓一遍。

两路上下文是合并的

最后一步是把 MCP 和网页两路上下文拼在一起,不是二选一。私有数据源和公网结果会同时进入同一次研究的上下文

接下来这一段,是整个项目里最值得抄的

管上下文的那个类,自己不做任何匹配。它是个分派器,按数据源类型把活儿转给三个不同的压缩器

抓来的网页交给第一个,返回 10 条,相似度阈值读配置,默认 0.42。向量库那一路交给第二个,返回 8 条。已经写好的章节交给第三个,也是 10 条,阈值写死 0.5。

前两个都好理解。第三个,有点东西。

它对付的是一个特别具体的麻烦。报告一旦长到需要分章节写,后面几节特别容易跟前面说重。内容一重复,读者立刻就走。

常规做法是什么。在 prompt 里加一句别重复前面的内容,然后祈祷模型听话。

它的做法是,把当前子主题,加上草稿里已经有的章节标题,拼成一批查询一起发出去,结果取并集去重,再截到 10 条。

这个操作值得琢磨一下。它把已经写好的内容,当成了一份可以检索的语料。每开一个新章节,先拿新章节的主题去这份语料里查一遍,看看有没有撞车。

前者是求模型自觉,后者是把重复变成一个可以计算的东西

注意,这一整段跟 AI 一点关系都没有,纯是工程思路

这也是整篇稿子想说的主线,后面还会回来。

嵌入向量是从统一的记忆模块拿的。压缩器自己不持有 embedding 模型,所以换模型只改一个地方。

下面聊聊检索器。这个目录底下导出了 21 个。

通用搜索最多,Tavily、Google、Bing、Brave、DuckDuckGo、Searx、Serper、SerpApi、SearchApi、Exa、BoCha,这一挂全在。学术源单独一组,Arxiv、Semantic Scholar、PubMed Central、OpenAlex。社交源有两个,Xquik 和 GetXAPI。剩下的 GroundRoute、CRW、MCP、Custom 归到其他。

21 个,覆盖面比想象中宽。

注册表就是一段 match 语句,配延迟导入。解析顺序是请求头优先,然后是配置,都没有就落到默认的 Tavily。名字写错,它不报错

静默回落。

!踩坑提示 🕳

把检索器写成 RETRIEVER=tevaly,这个拼写错误不会抛异常,日志里也不喊一声,只会在若干小时之后让人纳闷,为什么今天的结果质量突然这么差。调检索相关问题的时候,第一个动作就该是确认实际生效的到底是哪个检索器。

然后说一个修得很漂亮的改动。

v3.6.1 之前,它判断检索回来的结果要不要再爬一次,靠的是这个条件,len(raw_content) > 100

就是拿内容长度去猜内容的性质。超过 100 个字符,就认为这是个摘要页,得去爬原文。

这种代码能活很久,因为大部分时候它是对的。但它其实是错的,只是错得不明显

现在改成检索器自己声明。PubMed Central 和自定义检索器声明为不需要,因为它们返回的就是全文。Tavily、DuckDuckGo、Searx 声明为需要,拿到的只是摘要,得去爬原文。

21 个检索器里目前只声明了 5 个,剩下的保留默认值,为的是不把社区已经写好的检索器搞坏。这个取舍挺成熟的,新契约先铺路,不搞一刀切

把隐式的猜测,换成显式的契约

自己加一个检索器要四步。建文件,注册表里加一个分支,导出,然后用配置项启用。接口契约短得离谱,一个构造函数,一个搜索方法,返回的是一组带标题、链接、正文的对象。

深度研究是另一条路。三个参数决定这棵树的形状,每层展开几个子主题,递归几层,并行跑几个。给的默认值是 4、2、2。

算法本身是广度递归。研究主主题,按广度展开子主题,每个子主题再递归下去,最后汇总成一份报告。4 和 2 展开出来,就是一棵四叉两层的树

内部状态分了三个列表攒着,结论、来源、上下文分开存。为的是跨分支汇总的时候,引用还能对得上号。

开销也集中在这条路上。这棵树上每一个节点,都会把前面那一整套流程完整跑一遍。

多智能体是另一套完全独立的流水线,用 LangGraph 编排的,角色比单智能体版本细得多。

主协调一个,深度研究一个,规划大纲一个,校验正确性一个,按反馈修订一个,汇总成稿一个,导出 PDF、DOCX、Markdown 一个。还有一个角色,留给人工介入。

流程是这样。先做初始研究,出大纲,然后对每一个大纲主题并行跑一轮研究、校验、修订的循环,跑完汇总,再导出。

这套东西里最容易失控的,就是校验和修订那个循环

一个负责挑毛病,一个负责改。听着很合理对吧。但两个 LLM 凑在一起互相推,特别容易推到停不下来。修订完,校验又挑出新毛病,改完再挑,无限套娃。

v3.5.1 加了一个修订次数上限来兜这个底,v3.6.0 又给循环加了一道界,还引入了精确的哨兵值。多智能体跑飞一次的成本,比单智能体高一个量级,这个循环必须盯死。

回到 MCP。服务端配置写成一个对象列表,每一项声明它是本地 stdio 还是远程 WebSocket,各自的地址和凭据。

三种策略,差别只在调用频次。

fast 是默认值,只用原始查询跑一次 MCP,结果缓存起来复用。deep 是每个子查询都各跑一次 MCP。disabled 就是完全跳过。

第一种的实现就是前面那五步里的第一段。第一次跑完写进缓存,后面每个子查询直接复制一份走。覆盖度有损失,换来的是不用每个子查询都去敲一遍 MCP 服务

工具也不是全量塞给模型的。先用 LLM 排一次序,只留前几名,要求它返回一个数组,每项带索引、工具名、相关度分数和理由。

MCP Server 那一侧已经拆到独立仓库了,供别的 AI 应用调用。

所有 prompt 收在一个类里,靠一个按报告类型分派的 match 来取。收成一个类,是为了让模型特定的变体能继承覆盖。

子查询规划那条 prompt 规定了三件事。生成几个,彼此不能重复,每一条都得是可搜的,输出要求是 JSON 数组。初始检索的结果会同批喂进去,所以子查询不是凭空想的。

报告生成那条是临场拼的,引用格式支持 APA、MLA、Chicago、Harvard、IEEE,再加上字数、语气、语言,还有图片嵌入指令。可用图片会以标题加链接的清单形式注入,要求用 Markdown 图片语法放到合适的位置。

配置是三级覆盖,环境变量盖 JSON 文件,JSON 文件盖默认值。

LLM 按用途拆成三个角色,这是成本控制最主要的抓手。FAST_LLM 调用次数最多,负责摘要压缩。SMART_LLM 写报告。STRATEGIC_LLM 管规划和选角色。想省钱,先动前两个。

检索侧的参数是这几个,每次检索取几条、最多爬几个页面、相似度阈值卡在多少。默认阈值 0.42。

这里是最容易绊人的地方。配置项在代码里一律小写访问,配置文件里写的是大写,代码里读的是小写。写插件的时候特别容易在这儿愣住。

还有个数字容易看岔。输出长度的默认值是 4000,但 v3.5.0 把上限从 32k 提到了 200k。这两个不打架,一个是开箱默认值,一个是允许的天花板,给长输出的模型留的空间。

图片那组参数有三项,模型指到一个 Gemini 的生图版本,最多生成 3 张,风格在 dark、light、auto 里挑。图片会在报告生成前并行预生成 2 到 3 张,落到一个按 research id 分的目录里。

成本这一块靠回调串起来。计费函数以回调的身份,一路传给所有 LLM 调用点,累计值最后从一个取值函数里读。

这个设计带来一个隐性的约束。新加一个 LLM 调用,如果忘了把这个 callback 接上,这次消耗就静默丢了,成本报告上看不见。v3.6.0 那条按真实用量计费修的,就是这一类遗漏,顺带把 Anthropic 的 cache 读写、OpenAI 的 prompt cache 折扣,都算进了成本。

从今年 4 月到 8 月,五个版本,主要在加固。先是把输出上限从 32k 提到 200k,改成从原生元数据里取 Anthropic 的用量。然后加了无来源时弃权、修订次数上限,还修了超大上下文塌缩成空的问题。接着做了 20 项检索器畸形结果防护,6 项爬虫健壮性修复,给多智能体修订循环加了界。最后是检索器显式声明要不要二次爬取,加了链路追踪,一次合入 97 个积压 PR。

这几个版本里,最值得在意的是两条。一条是检索结果为空的时候直接弃权,不生成报告。另一条是修掉超大上下文塌缩成空字符串那个 bug。

这两条都不算功能特性,属于长链路 Agent 才会碰上的失效模式。前者是幻觉防线,后者是上下文压缩在极端输入下的边界情况。搭过这类系统的人,看到这两条大概会心里一动。

写到这儿,有个念头冒出来。

这个项目里所有让人觉得聪明的地方,几乎都跟 AI 没关系

分层,是把职责切开。有状态和无状态那条线,是给状态找出边界。检索器自己声明要不要二次爬取,是把隐式的猜测变成显式的契约。那三个压缩器,一个分派,一个按需压缩。MCP 的三种策略,就是在延迟和覆盖度之间挑一个点。

这些都是软件工程几十年前就在做的事。分而治之,显式优于隐式,拿缓存换延迟。

AI 时代大家总爱说 Agent 是新的软件形态。可把这份代码从头翻到尾就会发现,Agent 的新意几乎全在它调用的那个模型里,剩下的部分,是四十年前写操作系统和编译器的人就已经想明白的东西。

所以如果也在搭自己的 Agent,这里有个不太成熟的看法。

最值得花时间的地方,可能不是 prompt 怎么写,也不是换个更强的模型,而是把它拆成几个能单独测试、能单独替换的块。模型会换,架构不用重来。

真正难的地方也不在调模型。子查询切多细,上下文压到多短,递归什么时候该收手,这几个判断没有标准答案,只能拿自己的语料和预算一点点试出来。

项目地址:https://github.com/assafelovic/gpt-researcher?__from__=talkingdev&utm_source=chatgpt.com

这个项目里最该抄走的第一样东西

把已经说过的话,变成可检索的语料

如果觉得有用,随手点个赞、在看、转发三连吧。

点赞
在看
转发

THANKS FOR READING

“关注公众号AI代码蜂巢x 获取更多信息

相关学习资料

返回首页浏览学习资料