数据截至 (上游 commit a53b13c7022b)
Local Deep Researcher — 架构与原理
30 秒导读: 给它一个话题,它就用一个「生成搜索词 → 搜网 → 摘要 → 反思找知识缺口 → 生成新搜索词」的循环,迭代几轮,最后输出一份带来源引用的 Markdown 报告。全部逻辑跑在你本机的 Ollama 或 LMStudio 上,不必调用任何云端大模型。
1. 这是什么(零基础也能懂)
一句话定义: Local Deep Researcher 是一个本地运行的网络研究助手——你丢给它一个话题,它自动上网查资料、边查边写、写完再反思还缺 什么、然后接着查,循环几轮后交给你一篇带引用的报告。
解决什么问题 / 给谁用: 假设你想快速摸清一个陌生话题(比如「2025 年最新的向量数据库有哪些」),但又不想:
- 一条条手动搜、一篇篇手动读、手动把要点抄下来;
- 也不想把话题和资料喂给云端大模型(隐私、成本、离线场景)。
它替你把「搜 → 读 → 记 → 想还缺啥 → 再搜」这套动作自动化,而且模型跑在你自己电脑上。适合:想要私密/离线做资料调研的工程师、研究者,以及想学「深度研究 agent 到底怎么搭」的人——因为它极其精简(核心就 7 个 Python 文件)。
它能做什么(功能):
- 把一个话题变成一条优化过的搜索查询;
- 用四种搜索后端之一(DuckDuckGo / Tavily / Perplexity / SearXNG)去搜;
- 用本地 LLM 把搜到的内容滚动式总结成一段不断增长的摘要;
- 反思当前摘要、找出「还没讲清楚的地方」,自动生成下一条追问式搜索词;
- 循环用户指定的轮数,最后拼出一篇
## Summary+### Sources的 Markdown 报告。
用起来什么样: 你不写代码,而是在 LangGraph Studio 里(或通过 API)给图一个输入,只有一个字段:
# 示意,非源码——这是喂给图的输入形状
{
"research_topic": "2025 年主流的开源向量数据库对比"
}
跑完后,图的输出也只有一个字段 running_summary,内容形如:
## Summary
(这里是迭代若干轮后攒出来的一大段研究摘要……)
### Sources:
* Milvus 官方文档 : https://milvus.io/...
* Qdrant vs Weaviate 基准 : https://...
一句话直觉/类比: 把它想成一个勤奋但健忘的实习生:它没法一次读完全网,所以采用「小步快跑」——查一点、总结进笔记本、看看笔记本还缺什么、再针对性地查下一点。那本「不断被改写的笔记本」就是全项目的灵魂:running_summary。
本项目的思路来自论文 IterDRAG(把一个复杂问题拆成子问题,逐个检索、逐个作答,后面的答案建立在前面的答案之上;见 README.md:165)。
2. 顶层全景(它大概怎么转)
整个系统就是一张 LangGraph 状态图(state graph,一种「节点 + 边 + 共享状态」的编排方式:每个节点是一个函数,读共享状态、返回状态更新;边决定下一个走谁)。
怎么读下面这张图: 从 START 往下是主流程,走到 reflect 后有一个菱形判断决定「再来一轮」还是「收尾」——这就是「深度研究」的循环所在。
START
│
▼
┌──────────────────┐
│ ① generate_query │ 话题 → 一条搜索查询
└──────────────────┘
│
▼
┌──────────────────┐
│ ② web_research │◀────────────┐ 搜网、格式化来源
└──────────────────┘ │ loop_count += 1
│ │
▼ │
┌──────────────────┐ │
│ ③ summarize │ │ 把新资料并进「笔记本」
└──────────────────┘ │
│ │
▼ │
┌──────────────────┐ │
│ ④ reflect │ │ 找知识缺口 → 生成下一条查询
└──────────────────┘ │
│ │
◇ route ◇ loop<=max? ──是──┘
│
否
▼
┌──────────────────┐
│ ⑤ finalize │ 去重来源 + 拼 Markdown
└──────────────────┘
│
▼
END
部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
SummaryState | 贯穿全程的共享状态(话题、当前查询、摘要、来源、轮数) | src/ollama_deep_researcher/state.py:7 |
Configuration | 所有可调项(模型、搜索后端、最大轮数、结构化输出模式…) | src/ollama_deep_researcher/configuration.py:16 |
generate_query | 把话题变成一条搜索查询 | graph.py:138 |
web_research | 调搜索后端、格式化并累加来源 | graph.py:192 |
summarize_sources | 用 LLM 把新资料并入 running_summary | graph.py:265 |
reflect_on_summary | 找知识缺口、生成追问式查询 | graph.py:331 |
finalize_summary | 去重来源、拼成最终 Markdown | graph.py:387 |
route_research | 条件边:决定再循环还是收尾 | graph.py:421 |
| 搜索后端 | 四个搜索函数,输出统一形状 | utils.py:166-387 |
| prompts | 五段提示词 + 两 套结构化输出指令 | prompts.py |
主线走一遍(高层,不进代码):
- 输入只有
research_topic。 generate_query让本地 LLM 把话题写成一条好搜的查询。web_research拿这条查询去搜,得到几条来源,格式化成一段文本,轮数 +1。summarize_sources让 LLM 读这段文本:没摘要就新建,有摘要就并进去(而不是重写)。reflect_on_summary让 LLM 审视摘要、指出「哪块还没说清」,并据此写下一条追问查询。route_research看轮数够不够:不够就带着新查询回到第 3 步;够了就往下。finalize_summary把一路攒的来源去重,拼在摘要后面,产出最终 Markdown。
从这里往哪读:
- 想先搞懂「状态怎么设计、循环怎么连、什么时候停」→ 读 01-state-and-graph.md。
- 想逐个节点看真实代码 → 读 02-nodes-walkthrough.md。
- 好奇「本地小模型经常吐不出合法 JSON,它怎么兜底」→ 读 03-structured-output.md。
- 想知道「四个搜索后端怎么被抹平成一种形状、来源怎么去重」→ 读 04-search-and-sources.md。
- 想要精华、坑、对比、代码地图 → 读 05-deep-dive.md。