OpenResearch:本地优先的开源调研工具,从问题拆解到报告生成
2026/9/19 18:05:14 网站建设 项目流程

做技术调研最焦躁的时候,我同时开着二十多个浏览器标签页,一边读一边往文档里贴链接,贴到最后自己都分不清这条信息是哪篇文章里的。后来我写了OpenResearch,一个专门对付这种“资料收集混乱症”的开源小工具。它做的事情不复杂:把我临时起意的研究问题拆成子问题,去网页上抓相关材料,清洗成干净文本,再做向量化检索和要点汇总,最后输出一份带来源链接的研究报告。它不是要取代搜索引擎,而是把“查资料-整理-输出”这条链路固化下来,让我每次调研不至于从零开始。如果你也经常写行业分析、技术选型、竞品拆解,这个项目会很有参考价值。

1. 项目背景:为什么我要再造一个“研究工具”的轮子

1.1 传统调研流程的低效点

过去我做一份竞品分析,流程基本上是:先打开搜索引擎,输入几个关键词,然后一个个点进网页看,遇到有价值的段落就复制到一个临时文档里,再顺手把链接粘在下面。这个流程看起来没毛病,但实际操作中问题非常明显。

首先是上下文断层。当你连续打开十几个页面的时候,大脑很难记住“这个数据是在哪个页面看到的”,只能反复切页确认,时间和注意力都耗在切换上。其次是信息重复,同一个观点可能在五六篇文章里出现,复制时不会自动去重,最后整理出来的文档越来越臃肿,真正有用的东西反而被淹没。最后是流程不可复用,同样的调研动作,下次换个题目,又得重新来一遍。

我一直觉得,调研这件事完全可以标准化:先定义清楚问题,再按问题去采集素材,接着对素材做清洗和结构化,最后生成一份带引用来源的报告。OpenResearch就是想把这四步固化成一个可持续运行的系统。

1.2 OpenResearch的定位与适用场景

我把OpenResearch定位成“本地优先的研究辅助流水线”。它不是一个搜索框,也不是一个笔记软件,而是一个可以从“研究问题”一路跑到“研究报告”的半自动化工具。你给它一个具体的问题,它会自己去拆解、检索、采集、分析、汇总,最后给你一份有参考来源的结构化文档。

这个工具最适合三类人:一是像我一样需要经常写技术选型报告和行业分析的人,二是做产品调研的运营和产品经理,三是有大量文献整理需求的学生和研究人员。它不要求你懂特别深的编程,只要你愿意改改配置文件,就能把它跑起来。

我在设计的时候刻意不把它做成一个依赖云端服务的“黑盒”,而是把每个环节都拆开,让用户能介入和修改。这也符合“OpenResearch”这个名字里的“Open”:流程开放、代码开放、数据也留在本地。

2. 整体设计思路:先拆解问题,再组织信息

2.1 把研究任务拆成可执行的研究大纲

OpenResearch的第一步不是抓取网页,而是拆解问题。我在项目里设计了一个“Research Task”的数据结构,它包含研究主题、研究问题列表、来源限定、时间范围和输出要求。举个例子,如果研究主题是“2025年开源RAG工具的选型”,那么系统会把这个问题拆成几个子问题:主流开源RAG工具有哪些?它们的架构差异是什么?在中文场景下效果如何?社区活跃度怎么评估?

每个子问题都是一个独立的检索单元。这样做的好处非常明显:抓取目标更集中,生成的报告结构也更清晰,不会出现东讲一句西讲一句的混乱。我在代码里把子问题列表维护成一个Markdown大纲文件,用户可以手动编辑它,系统会根据大纲逐个去收集资料。

这个设计思路借鉴了我做项目管理时的习惯:不要直接啃一个巨大的目标,而是先拆成可验证的小任务。研究问题拆得越细,后面的检索和生成就越轻松。

2.2 采集、清洗、存储三段式架构

OpenResearch的整体架构可以分成三段:采集层、处理层和生成层。采集层负责从搜索引擎结果页或指定URL列表里抓取网页内容;处理层负责把HTML转成干净正文,去掉导航、底部声明、广告这些干扰信息;生成层负责把处理后的内容片段做向量化、检索,最后汇总成报告。

每一层之间通过文件或数据库解耦。采集层产出的原始HTML和元数据会缓存到本地,处理层产出的干净文本会写入SQLite和向量库,生成层只读取处理后的数据,不关心原始网页长什么样。这样设计的好处是,任何一个环节出问题,都可以单独重跑,不必从头再来。

2.3 为什么坚持“本地优先,模型可选”

做这类工具,最省事的方案是全部调云端API,但我刻意留了一个口子:所有环节都支持本地运行。网页抓取用Playwright本地跑,内容提取用trafilatura本地跑,向量化用sentence-transformers全家桶,唯一可选的外部依赖是大模型生成报告。

这样设计有现实考量。一是成本,调研类任务经常需要反复调整问题,每次调整都调API不划算;二是隐私,很多调研内容可能涉及内部数据,不适合上传到第三方模型;三是可控性,如果生成结果不理想,我可以随时换模型,不需要改动整个系统。

3. 技术选型解析:为什么是Playwright、Trafilatura和Chroma

3.1 抓取层选型:Playwright比requests更适合现代网页

最早的版本我用的还是requests加BeautifulSoup,后来发现很多网站的内容是JavaScript动态渲染的,直接请求HTML只能拿到空壳。换成Selenium又觉得太重,浏览器驱动版本匹配经常让人抓狂。最后选了Playwright,一是因为它对动态渲染支持好,二是因为它自带浏览器上下文管理,模拟真实用户操作很方便。

我在抓取层还做了一些封装,比如按照Robots协议限制请求频率、给每个页面设置随机延迟、失败自动重试三次。这些细节看起来不起眼,但对稳定性影响很大。我实测过用requests硬爬,大概运行半小时就会被部分站点封禁,而用Playwright模拟浏览器访问,加上低于1Hz的请求频率,基本能稳定跑完整个调研流程。

3.2 内容提取选型:Trafilatura比正则表达式靠谱得多

清洗HTML这一步,一开始我用的是自写的正则表达式,试图把所有标签剥掉,但很快发现网页里到处是脚本、样式、注释和数据JSON,正则根本写不完。后来换成了Trafilatura,这个库就是专门做自然语言文本提取的,它对新闻、博客、文档站点的正文识别效果都相当好。

Trafilatura的优点在于它不只会提取文本,还会输出标题、段落、列表等结构化信息。我可以直接拿到带段落边界的内容,再做后续分块。它对正文长度的判断也比较智能,能自动过滤掉过短的片段,比如版权声明和“”这类链接文本。

这里要注意一个细节:Trafilatura对某些高度定制化的单页应用支持一般,比如完全依赖JavaScript渲染的文档站。我的处理方法是,抓取前先用Playwright等页面完全加载,拿到渲染后的HTML再交给Trafilatura,这样兼容性会好很多。

3.3 向量存储选型:Chroma够轻量,也够用

向量数据库我选了Chroma,没有用Weaviate或Milvus这类更重的服务。原因很简单,OpenResearch是本地优先的工具,我不希望用户为了跑一个调研工具还得部署一个分布式数据库。Chroma支持嵌入式运行,直接把数据存在本地目录里,而且提供了非常直观的Python API。

和Chroma配套的是sentence-transformers里的BAAI/bge-small-zh-v1.5模型。这个模型在中文语义相关性上的表现不错,模型体积也只有100MB左右,普通CPU可以流畅运行。向量维度和分块大小我在项目里都做成了配置项,方便用户根据自己的数据量调整。

也有朋友建议我直接用更高精度的Embedding模型,比如OpenAI的text-embedding-3-small。我在设计上留了接口,但默认不依赖外部API,因为很多调研场景不允许把文本传出本地。对于英文内容,用户也可以切换成all-MiniLM-L6-v2,准确性会更高一些。

4. 核心实现过程:从零搭一条研究流水线

4.1 数据模型先行:ResearchTask和SourceItem

代码还没写之前,我先把数据模型定义清楚了。一个ResearchTask包括:

@dataclass class ResearchTask: title: str questions: List[str] # 子问题列表 source_allowlist: List[str] # 允许采集的域名白名单 crawl_depth: int # 每个子问题的最大采集页数 time_range_days: int # 只保留指定天数内的文章

一次处理的每一个网页都会生成一个SourceItem,保存原始URL、标题、抓取时间、正文内容和后续的向量表示。数据模型的清晰程度直接决定了代码好不好维护,所以这个阶段不要急着写抓取逻辑,先把数据库表结构设计好。

4.2 搭建抓取模块:Playwright封装与页面解析

抓取模块的核心是一个Crawler类,它负责启动浏览器上下文、打开页面、等待渲染、拿到最终HTML。我封装了一个方法,输入URL,输出干净的Markdown正文:

async def fetch_page(url: str) -> str: async with async_playwright() as p: browser = await p.chromium.launch() context = await browser.new_context( user_agent="Mozilla/5.0 ... Chrome/120.0", locale="zh-CN", ) page = await context.new_page() await page.goto(url, timeout=30000, wait_until="networkidle") html = await page.content() await browser.close() return trafilatura.extract(html, output_format="markdown")

使用wait_until="networkidle"是为了等所有动态资源加载完,避免拿到半渲染的页面。这个参数会让抓取速度略慢,但提取效果明显更好。如果是抓取列表页,还要额外滚动两三次页面,否则懒加载内容不会出现。

4.3 文本拆分与向量化入库

清洗后的文本可能是几千字的文章,不能整篇丢进向量库,检索效果会很差。我把文本按Markdown标题结构和段落边界做了分块,每块控制在500到800字之间,块与块之间保留50字重叠,避免把关键信息切散。

from langchain.text_splitter import MarkdownTextSplitter splitter = MarkdownTextSplitter(chunk_size=700, chunk_overlap=50) chunks = splitter.split_text(markdown_text)

分块之后,用Embedding模型对每一块做向量化,然后把向量连同原文、来源URL、标题一起存入Chroma集合。这里的文档ID我用的是URL加块序号,这样后续调整分块策略时,可以按来源URL整体删除旧数据。

4.4 检索与报告生成:让答案长在引用上

报告生成是整个系统里最需要“克制”的部分。我没有让模型自由发挥,而是先根据子问题从向量库检索排名靠前的文本片段,再把这些片段按相关性排序后拼接成上下文,最后生成一个要点式回答。

我在提示词里明确要求模型必须引用片段编号,并且不能编造片段中不存在的结论。输出格式也做了约束,要求每个论点都附带来源链接。这样做之后,报告虽然不像纯人工写那么流畅,但胜在每条信息都有出处,后续人工复核的成本大大降低。

4.5 用配置文件串起整个流程

为了让系统能被非程序员使用,我把所有可调参数都集中在一个YAML文件里:

research: title: "主流RAG工具在中文场景下的对比" questions: - "有哪些主流开源RAG工具?" - "这些工具的核心架构区别是什么?" - "它们在中文语义检索上的表现如何?" source_allowlist: - "github.com" - "zhuanlan.zhihu.com" crawl_depth: 5 time_range_days: 180 model: embedding: "BAAI/bge-small-zh-v1.5" generator: "local" # local 或 openai vectorstore: persist_dir: "./data/chroma_db"

执行时只需要一行命令:python main.py --config example.yaml。系统会按研究问题逐个采集、处理、检索和生成,最终把报告写到output/目录下。

5. 实操中的高频问题与排查技巧

5.1 反爬限制和请求频率控制

搭建过程中最头疼的就是抓取频繁触发网站的反爬机制。一开始我的爬虫跑大概二三十个页面就会被拒绝访问,错误信息五花八门。我后来总结了几条经验:设置随机延迟、使用真实浏览器的User-Agent和Header、限制并发数为1到2、对单个域名的请求时间间隔至少控制在3秒以上。

如果网站仍然返回403或验证码页面,我会直接把它加入黑名单,不让它拖慢整个流程。调研类任务通常有大量候选网址,没必要跟某个网站死磕。这个思路很重要:抓取覆盖率超过八成就够了,没必要追求100%。

5.2 正文提取结果为空或乱码

Trafilatura偶尔会返回空内容,原因多半是页面在未完全渲染时就被抓取,或者正文是图片型内容。我加了两个兜底逻辑:一方面在Playwright抓取后主动等待页面中的主内容节点出现,另一方面如果Trafilatura提取结果为空,就回退到BeautifulSoup按<article><main>标签提取。

乱码问题一般出在字符编码判断上。我在请求头里显式声明Accept-Charset,并且在读取HTML时指定charset="utf-8",遇到非UTF-8页面再做额外判断。现在主流网站基本都统一到UTF-8,但老旧站点仍有GBK编码,这里尽量用chardet自动检测。

5.3 向量检索召回的内容不相关

刚开始接入向量检索时,我发现有些子问题召回的前几名片段跟问题关系不大。排查后发现是分块粒度的问题:文本块太小,语义信息不足;文本块太大,噪声又太多。我调了chunk_size,从300试到1000,最后发现中文学术和科技类文本在700字左右表现最稳。

还有一个提升召回率的小技巧:检索时把原始子问题做一次同义词扩展。比如“部署难度”同时检索“安装配置”和“上手成本”,效果比单纯用原文查询好很多。这个扩展规则我暂时用词表维护,后续打算接入模型生成。

5.4 大模型生成内容出现幻觉

报告生成阶段幻觉问题必须重视。我的处理方案是三层防线:第一层,提示词里明确限定“只能基于提供的片段回答”;第二层,生成后做引用校验,把报告中出现的来源链接与检索结果对比,保证链接确实在候选片段里;第三层,对关键结论增加“置信度标记”,如果片段里没有明确数据支撑,就标记为“待验证”。

实际跑下来,第三层防线最有价值。因为调研报告的价值在于可信度,与其让模型自信地编一个数字,不如坦诚地告诉读者“这个信息我没有找到直接证据”。

6. 效果验证:一次竞品调研的全过程记录

6.1 输入的研究问题与参数配置

我用一个真实任务来测试OpenResearch:调研“主流开源RAG工具在中文场景下的选型”。配置了五个子问题,来源限制在GitHub、知乎专栏、CSDN和几个技术社区,时间范围设为最近半年,每个子问题最多采集十五个页面。

整个运行耗时大概四十分钟,其中抓取占了一大半,向量化和生成只用了几分钟。生成的报告有四个章节,每个章节都附带了相关工具的名称、特点、社区活跃度和典型使用案例,末尾还有一张对比表格。

6.2 输出报告的质量评估

从结果来看,报告在信息广度上达到了我预期的八十分,覆盖了主流工具和社区讨论中频繁出现的关键词。当然,报告里有些细节还不够准确,比如一些版本的发布时间和最新特性,需要人工核对。这让我明确了项目的定位:它不是最终交付物,而是“初稿制造机”,真正的人工价值在于判断和修正。

6.3 让我意外的一个发现

这次测试里,OpenResearch把很多原本散落在不同文章里的实践问题归拢到了一起,比如中文分词对检索效果的影响、不同向量库在百万级数据下的性能差异。这些内容之前我虽然零散看过,但没有形成完整认知。工具的价值不只是省时间,它还能帮你发现单个页面里看不到的“模式”。

7. 后续演进方向:OpenResearch的下一步

7.1 把单线程流水线升级为多智能体协作

目前OpenResearch还是单线程执行,先跑完所有采集再统一处理。下一版我打算把流程拆成“检索员Agent”和“分析员Agent”两个角色。检索员负责并行处理不同子问题,分析员负责交叉验证信息矛盾。这样既能加快速度,也能发现不同来源之间的观点冲突。

7.2 支持更多信息源与交互方式

现在主要支持网页抓取,但我已经在规划加入PDF导入、视频字幕文件和本地Markdown笔记作为输入源。交互上准备做一个简单的Web界面,用户可以直接在页面上看到采集进度中间结果,不用每次改完配置再跑命令行。

7.3 让报告生成更“可溯源”

我准备在报告生成阶段增加一个双向索引,从结论反向追溯到具体的原文片段。这样用户点一下报告里的结论,就能看到它依据的原文列表。虽然技术上只是多存一层关联关系,但对使用体验的提升会非常明显。

8. 写在最后的一点体会

OpenResearch这个项目做下来,我最深的感受是:研究工具的核心不在于“自动生成”,而在于“结构化”。真正耗人精力的从来不是读那几篇文章,而是搞不清楚哪些问题需要回答、哪些信息才是关键。现在我把这个流程写成了工具,每次新选题只需要改一下配置文件,剩下的工作交给流水线跑,我再基于初稿做判断和修正,效率和深度都提升了不止一个档次。

如果你也想搭一个类似的研究辅助系统,我建议从最小闭环开始:先实现网页抓取和Markdown输出,再用脚本做关键词检索,最后再引入模型生成。不要一上来就追求多智能体和大模型,先把“能复现的流程”跑通,这个过程中积累的细节比任何炫酷的功能都值钱。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询