主题
1. 项目概述与需求分析
从"搜一下"到"帮我研究一下"
想象这样一个场景:你是一名产品经理,老板突然让你在一周内出一份关于"2025 年 AI Agent 市场趋势"的深度调研报告。按照传统做法,你需要打开 Google 搜索几十个关键词、翻阅几十篇行业报告、关注十几个技术博客、整理上百条信息碎片,最后才能拼凑出一份还算像样的报告。整个过程可能要花费你两三天的时间,而且很容易遗漏重要信息或者被过时的数据误导。
如果有一个助手,你只需要对它说:"帮我研究一下 2025 年 AI Agent 的市场趋势,重点关注技术路线图、主要玩家、商业模式和应用场景",然后它就能自动地去搜索网页、阅读论文、抓取数据、交叉验证信息,最终输出一份结构清晰、引用准确、有深度见解的报告——这就是**自主研究助手(Autonomous Research Assistant)**要解决的问题。
与第九章的客服工单系统不同,研究助手面对的不是结构化的用户输入和预定义的处理流程,而是开放性的研究主题和多变的探索路径。它需要具备更强的自主决策能力:决定搜索什么关键词、判断哪些来源可信、识别什么时候信息已经足够、知道如何把零散的信息组织成连贯的叙述。这些能力使得研究助手成为 LangGraph 最能发挥其状态管理和多步编排优势的场景之一。
为什么用 LangGraph 来构建研究助手
在深入需求之前,让我们先想清楚一个问题:为什么研究助手适合用 LangGraph 而不是其他框架来实现?
核心原因在于研究过程本质上是一个有状态的迭代式工作流。当你做一次真正的学术或商业研究时,你的大脑实际上在进行这样一系列操作:
- 理解问题——把模糊的研究主题拆解成具体的研究问题
- 制定计划——决定先查什么后查什么,从哪些角度切入
- 收集信息——搜索、阅读、提取关键信息
- 评估发现——判断信息的质量、相关性、时效性
- 调整方向——根据已有发现决定是否需要深挖某个方向,或者换个角度
- 整合输出——把所有发现组织成最终的报告
注意第 5 步——调整方向。这是研究过程中最关键的环节,也是让简单的"搜索→总结"模式无法胜任的原因。一个真正的研究者不会线性地执行上述步骤,而是在步骤之间来回跳转:可能发现了一个有趣的角度就回头重新调整搜索策略;可能读到了一篇关键论文就去追溯它的参考文献;可能发现某个假设不成立就需要换一个研究方向。这种非线性的、迭代的、基于中间结果动态调整执行路径的行为模式,正是 LangGraph 的强项。
相比之下,如果你用普通的 LangChain Agent(ReAct 模式),虽然也能实现"思考→行动→观察"的循环,但它的状态管理是隐式的——所有上下文都塞在一个巨大的 prompt 里,Agent 通过 LLM 自主决定下一步做什么。这种方式在简单任务上没问题,但对于复杂的多轮研究任务来说,存在几个致命缺陷:
- 上下文窗口限制:随着研究的深入,积累的信息量会迅速膨胀,很快就会超出模型的上下文限制
- 不可控的探索路径:LLM 可能会在无关的方向上越走越远,浪费大量 token
- 难以插入人工干预:当研究者想要调整方向时,没有一个自然的介入点
- 结果不可复现:同样的输入因为 LLM 的随机性可能产生完全不同的研究路径
LangGraph 通过显式的 State 管理和可控的条件路由完美地解决了这些问题。我们可以精确控制每一步的状态流转,在关键节点设置中断点让人工审核,通过条件边实现智能的分支和循环,还能利用检查点机制实现研究过程的完整回放和调试。
项目定位与目标用户
明确了技术选型的理由之后,我们来定义这个项目的具体定位:
项目名称:DeepResearch —— 自主深度研究助手
核心理念:不是简单地"搜索+摘要",而是模拟人类专家的研究思维过程,进行系统性的、多轮迭代的、可审计的信息收集与分析
目标用户画像:
- 学术研究者:需要快速了解一个新领域的文献脉络和研究前沿
- 行业分析师:需要定期产出市场调研报告和竞争分析
- 产品经理/战略规划:需要为决策提供充分的数据和信息支撑
- 投资分析师:需要深入研究标的公司和行业赛道
- 技术咨询顾问:需要在短时间内成为某个领域的"半个专家"
不做的事情:
- 不做实时新闻聚合(那是 RSS 阅读器的事)
- 不做简单的问答(那是搜索引擎的事)
- 不做原创内容生成(研究报告必须基于真实来源)
- 不替代人类的最终判断(AI 是研究助理,不是决策者)
功能需求详解
基于上面的定位,我们把功能需求分为三个层次来描述:
FR-1:研究任务管理与规划
这是系统的入口层,负责接收用户的研究请求并将其转化为可执行的研
究计划。
FR-1.1 研究主题解析 用户输入的自然语言研究请求可能是非常模糊的,比如"帮我研究一下大语言模型的安全问题"。系统需要能够:
- 提取核心研究主题和边界条件
- 识别隐含的研究维度(比如"安全问题"可能涉及对抗攻击、隐私泄露、偏见与公平性等多个子维度)
- 根据主题复杂度预估所需的研究深度和广度
FR-1.2 研究计划生成 基于解析后的主题,系统应该自动生成一份结构化的研究计划:
- 将研究主题拆解为若干个子问题(Sub-questions)
- 为每个子问题确定搜索策略(关键词组合、信息源类型偏好)
- 规划研究的执行顺序(哪些子问题之间存在依赖关系)
FR-1.3 研究范围控制 防止研究过程无限发散:
- 设置最大搜索轮次上限(默认 3-5 轮)
- 设置最大信息源数量上限(默认 20-30 个)
- 支持用户指定必须覆盖的信息源类型或排除某些领域
python
from typing import TypedDict, Annotated, List
import operator
class ResearchPlan(TypedDict):
topic: str
sub_questions: List[str]
search_strategies: List[dict]
max_rounds: int
max_sources: int
estimated_depth: str
class ResearchState(TypedDict):
user_query: str
plan: ResearchPlan
current_round: int
current_sub_question_index: int
collected_sources: Annotated[List[dict], operator.add]
extracted_facts: Annotated[List[dict], operator.add]
research_log: Annotated[List[str], operator.add]
final_report: str
status: str这里我们定义了两个核心的状态类。ResearchPlan 是研究计划的静态结构,包含了主题、子问题列表、搜索策略等元信息;ResearchState 则是整个研究过程的动态状态,其中 collected_sources 和 extracted_facts 使用了 Annotated + operator.add 来实现累加——每轮搜索都会往这两个列表中追加新的发现。research_log 记录了完整的研究轨迹,这对于后续的可审计性和调试非常重要。
FR-2:多源信息采集
这是系统的核心能力层,负责从各种渠道获取原始信息。
FR-2.1 网页搜索与抓取
- 支持主流搜索引擎 API(Google Custom Search / Bing / SerpAPI / Tavily)
- 抓取网页正文内容(去除导航栏、广告、脚本等噪声)
- 提取页面元信息(标题、作者、发布日期、域名权威度评分)
FR-2.2 学术资源检索
- 接入 Semantic Scholar / arXiv / Google Scholar 等 API
- 获取论文标题、摘要、引用数、发表时间
- 支持按引用数和时间范围筛选
FR-2.3 结构化数据获取
- 支持维基百科条目获取(作为背景知识的可靠来源)
- 支持特定领域的知识库查询(如医疗领域的 PubMed、法律领域的 CourtListener)
- 支持自定义 API 数据源的接入
FR-2.4 信息去重与质量评估
- 基于内容相似度的去重(避免重复抓取同一信息)
- 来源可信度评分(优先级:学术论文 > 官方文档 > 权威媒体 > 博客 > 论坛)
- 信息新鲜度加权(对于时效性强的主题,优先选择近期发布的内容)
python
@dataclass
class Source:
url: str
title: str
source_type: str
content: str
metadata: dict
quality_score: float
relevance_score: float
freshness_score: float
combined_score: float
def evaluate_source(source: Source) -> Source:
type_weights = {
"academic_paper": 0.9,
"official_doc": 0.85,
"news_article": 0.7,
"blog_post": 0.5,
"forum": 0.3
}
quality_score = type_weights.get(source.source_type, 0.5)
days_old = (datetime.now() - source.metadata.get("published_date", datetime.now())).days
freshness_score = max(0, 1 - days_old / 365)
source.quality_score = quality_score
source.freshness_score = freshness_score
source.combined_score = quality_score * 0.4 + source.relevance_score * 0.4 + freshness_score * 0.2
return source这段代码展示了信息源质量评估的基本逻辑。每种类型的来源都有一个基础的可信度权重(学术论文最高,论坛帖子最低),然后结合内容的时效性计算出一个综合分数。这个综合分数将直接影响后续信息筛选和报告生成的权重分配。
FR-3:信息提取与知识整合
光有原始信息是不够的,我们需要从中提炼出结构化的知识点。
FR-3.1 关键事实提取
- 从每个信息源中提取关键事实(实体、关系、数据点、观点)
- 用结构化的格式存储(Subject-Predicate-Object 三元组)
- 标注事实的来源出处以便追溯
FR-3.2 观点对比与冲突检测
- 识别不同来源之间的共识点和分歧点
- 对相互矛盾的观点进行标注和分析
- 尝试理解分歧产生的背景和原因
FR-3.3 知识图谱构建
- 将提取的事实组织成轻量级的知识图谱
- 支持实体消歧和关系推理
- 用于发现隐藏的关联和空白区域
python
@dataclass
class Fact:
subject: str
predicate: str
object: str
confidence: float
source_urls: List[str]
context: str
def extract_facts_from_source(source: Source, llm) -> List[Fact]:
prompt = f"""请从以下文本中提取关键事实,以 JSON 格式返回。
每个事实包含 subject(主体)、predicate(谓语/关系)、object(客体)、confidence(置信度0-1)。
只提取明确陈述的事实,不要推断。
文本标题:{source.title}
文本内容:{source.content[:3000]}
"""
response = llm.invoke(prompt)
facts_raw = parse_json_response(response.content)
facts = []
for f in facts_raw:
fact = Fact(
subject=f["subject"],
predicate=f["predicate"],
object=f["object"],
confidence=f.get("confidence", 0.7),
source_urls=[source.url],
context=f.get("context", "")
)
facts.append(fact)
return facts
def detect_conflicts(facts: List[Fact]) -> List[dict]:
conflicts = []
fact_groups = defaultdict(list)
for fact in facts:
key = (fact.subject.lower(), fact.predicate.lower())
fact_groups[key].append(fact)
for (subject, predicate), group in fact_groups.items():
if len(group) >= 2:
objects = set(f.object.lower() for f in group)
if len(objects) > 1:
conflicts.append({
"subject": subject,
"predicate": predicate,
"conflicting_values": list(objects),
"sources": [f.source_urls for f in group],
"needs_resolution": True
})
return conflictsextract_facts_from_source 函数使用 LLM 从单个信息源中提取结构化事实,输出的是 Subject-Predicate-Object 格式的三元组。detect_conflicts 函数则对所有已提取的事实进行冲突检测——如果同一个主体-谓语对应了不同的客体值(比如"OpenAI 的 GPT-5 发布时间"有人说"2025 Q1"、有人说"2025 Q3"、有人说"未定"),就会被标记为冲突并记录下来供后续处理。
FR-4:研究与推理引擎
这是系统的"大脑",负责驱动整个研究流程。
FR-4.1 迭代式研究循环
- 每轮研究聚焦于当前最重要的子问题
- 根据已收集的信息判断是否需要继续深挖
- 动态调整搜索关键词和策略
FR-4.2 信息充足性判断
- 定义"信息充足"的标准(覆盖了所有子问题、关键事实都有多方验证、没有明显的知识空白)
- 当信息不足时自动触发新一轮搜索
- 当信息已经足够时进入报告生成阶段
FR-4.3 研究方向自适应调整
- 如果某个方向的搜索结果特别丰富,可以适当增加该方向的投入
- 如果某个方向反复搜索都没有有价值的结果,及时放弃并转向其他方向
- 发现意外的重要线索时可以临时增加新的研究维度
FR-5:报告生成与呈现
最后一层是将研究成果转化为用户可消费的形式。
FR-5.1 结构化报告生成
- 自动生成包含以下章节的报告:
- 执行摘要(Executive Summary)
- 研究背景与方法论
- 核心发现(按子问题分章节)
- 数据与分析(含图表描述)
- 不同观点的对比讨论
- 结论与建议
- 参考资料清单
FR-5.2 引用管理
- 所有结论都必须标注信息来源
- 支持多种引用格式(APA、MLA、Chicago 等)
- 引用链接可直接点击跳转到原始来源
FR-5.3 交互式研究回顾
- 用户可以查看完整的研究过程日志
- 可以针对报告中的任何部分追问"这个结论是基于什么信息得出的"
- 支持"从这个结论继续深挖"的递归研究
非功能性需求
除了功能需求之外,作为一个生产级的研究助手,还需要满足以下非功能性要求:
NFR-1:性能要求
- 单次中等复杂度的研究任务(3-5 个子问题)应在 5-10 分钟内完成
- 每轮搜索的平均响应时间应 < 30 秒
- 支持并发处理多个独立的研究任务
NFR-2:可靠性要求
- 关键外部 API 调用必须有重试和降级机制
- 研究过程中的中间状态必须持久化(支持断点续传)
- 生成的报告必须经过基本的质量校验(不能出现自相矛盾的内容)
NFR-3:成本控制要求
- 单次研究的 Token 消耗应有预算上限(默认 $2-5)
- 应根据研究深度动态选择模型(初步搜索用便宜模型,深度分析用强模型)
- 缓存常见的背景知识查询结果
NFR-4:可扩展性要求
- 新的信息源类型应能通过配置而非代码修改的方式接入
- 报告模板应支持自定义(不同场景可能需要不同的报告格式)
- 研究策略应可调优(允许高级用户微调搜索参数)
系统架构设计
有了需求和约束之后,我们来设计整体架构。整个 DeepResearch 系统可以分为以下几个核心模块:
┌─────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Web UI │ │ CLI 工具 │ │ API (REST) │ │
│ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │
│ └──────────────┼───────────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 请求路由器 │ │
│ └───────┬───────┘ │
├──────────────────────┼───────────────────────────────────┤
│ ▼ │
│ ┌───────────────────────┐ │
│ │ LangGraph 编排引擎 │ │
│ │ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ ResearchGraph │ │ │
│ │ │ │ │ │
│ │ │ plan → search │ │ │
│ │ │ → extract → │ │ │
│ │ │ evaluate → │ │ │
│ │ │ decide → │ │ │
│ │ │ {loop/end} │ │ │
│ │ └─────────────────┘ │ │
│ └───────────┬───────────┘ │
├──────────────────────────┼───────────────────────────────┤
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ 搜索引擎 │ │ 学术检索 │ │ 内容提取器 │ │
│ │ Adapter │ │ Adapter │ │ Extractor │ │
│ └──────────┘ └──────────┘ └────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Tavily │ │Semantic │ │ Jina Reader│ │
│ │ /SerpAPI │ │Scholar │ │ /Trafilatura│ │
│ └──────────┘ └──────────┘ └────────────┘ │
├──────────────────────────────────────────────────────────┤
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 数据持久化层 │ │
│ │ ┌─────┐ ┌─────┐ │ │
│ │ │PostgreSQL│Redis│ │ │
│ │ └─────┘ └─────┘ │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────┘这个架构的核心设计思想是分层解耦:
最上层是用户界面层,提供 Web UI、CLI 和 REST API 三种交互方式,满足不同用户的使用习惯。Web UI 适合日常使用,CLI 适合开发者集成到自动化流水线中,API 则允许第三方系统集成。
中间层是 LangGraph 编排引擎,这是整个系统的心脏。它接收用户的查询,通过状态机驱动的多步流程完成研究任务。图的拓扑结构将在下一节详细展开。
下层是工具适配层,封装了对各种外部服务的调用逻辑。包括搜索引擎(Tavily/SerpAPI)、学术数据库(Semantic Scholar)、以及内容提取器(J Reader/Trafilatura)。这一层的价值在于提供统一的接口抽象——上层不需要关心底层用的是哪个搜索引擎,只需要调用 search(query) 就能得到结果。
最底层是数据持久化层,用 PostgreSQL 存储研究结果和报告,用 Redis 做缓存和实时状态共享。
图拓扑设计
现在让我们来设计核心的 ResearchGraph 的拓扑结构。这是整个系统最关键的设计决策,因为它直接决定了研究流程的逻辑和行为。
┌──────────────┐
│ START │
└──────┬───────┘
│
▼
┌──────────────┐
│ parse_query │
│ 解析研究查询 │
└──────┬───────┘
│
▼
┌──────────────┐
│ create_plan │◄──────────────────┐
│ 生成研究计划 │ │
└──────┬───────┘ │
│ │
▼ │
┌────────────────────┐ │
│ should_continue? │────────┐ │
│ 是否继续研究? │ │ │
└────────┬───────────┘ │ │
┌────────────┼────────────┐ │ │
▼ ▼ ▼ │ │
[continue] [sufficient] [stuck] │ │
│ │ │ │ │
▼ ▼ ▼ │ │
┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ select_ │ │generate_ │ │adjust_ │ │ │
│ focus │ │report │ │strategy │──┘ │
│ 选择焦点 │ │ 生成报告 │ │ 调整策略 │ │
└────┬─────┘ └────┬─────┘ └──────────┘ │
│ │ │
▼ ▼ │
┌──────────┐ ┌──────────┐ │
│search_ │ │ END │ │
│sources │ │ │ │
│ 搜索信息源 │ └──────────┘ │
└────┬─────┘ │
│ │
▼ │
┌──────────┐ │
│extract_ │ │
│facts │ │
│ 提取事实 │ │
└────┬─────┘ │
│ │
▼ │
┌──────────────┐ │
│evaluate_ │──────────────────────────────────────┘
│findings │ 循环回到 should_continue?
│ 评估发现 │
└──────────────┘这个图的拓扑结构体现了研究过程的本质特征——迭代式探索循环。让我解释一下各个节点的作用和它们之间的流转逻辑:
parse_query(解析查询):入口节点,负责将用户的自然语言查询转化为结构化的研究意图。它会提取核心主题、识别约束条件、估算复杂度。
create_plan(生成计划):基于解析结果生成初始研究计划,包括子问题拆解和搜索策略。这个节点只在第一次执行时运行,后续循环不再经过它。
should_continue?(是否继续):这是一个条件判断节点,也是整个循环的核心决策点。它有三条可能的出口:
- continue:当前信息还不够,需要继续搜索更多内容
- sufficient:信息已经足够充分,可以生成最终报告了
- stuck:连续几轮都没有获得新信息,说明当前策略可能走不通,需要调整
select_focus(选择焦点):当决定继续时,这个节点会选择下一轮应该聚焦于哪个子问题。选择策略可能基于"信息覆盖率最低的子问题优先"或"最有希望获得突破的方向优先"。
search_sources(搜索信息源):执行实际的搜索操作,调用底层的搜索引擎适配器获取候选信息源列表。
extract_facts(提取事实):对搜索到的信息源进行内容抓取和事实提取,产出结构化的知识三元组。
evaluate_findings(评估发现):对新提取的事实进行质量评估、冲突检测、与已有知识的整合,然后更新全局的研究状态,最后回到 should_continue? 判断是否需要继续循环。
adjust_strategy(调整策略):当陷入僵局(stuck)时,这个节点会尝试改变搜索策略——可能更换关键词、切换信息源类型、或者扩大/缩小搜索范围,然后回到 create_plan 或 select_focus 重新开始新一轮探索。
generate_report(生成报告):当信息充足时,这个节点会将所有收集到的信息和提取出的知识整合成一份完整的结构化报告。
目录结构与里程碑规划
最后,让我们规划一下项目的代码组织和开发节奏:
deep_research/
├── __init__.py
├── config.py # 全局配置(API keys、模型参数等)
├── state.py # 所有状态定义(ResearchState, Fact, Source 等)
├── nodes/
│ ├── __init__.py
│ ├── query_parser.py # FR-1.1 解析研究查询
│ ├── planner.py # FR-1.2 生成研究计划
│ ├── focus_selector.py # FR-4.1 选择研究焦点
│ ├── searcher.py # FR-2.x 多源搜索
│ ├── extractor.py # FR-3.1 事实提取
│ ├── evaluator.py # FR-4.2 信息充足性评估
│ ├── strategy_adjuster.py # FR-4.3 策略调整
│ └── report_generator.py # FR-5.1 报告生成
├── tools/
│ ├── __init__.py
│ ├── web_search.py # 网页搜索适配器
│ ├── academic_search.py # 学术检索适配器
│ ├── content_fetcher.py # 内容抓取与清洗
│ └── source_evaluator.py # 信息源质量评估
├── graph.py # ResearchGraph 组装
├── api.py # FastAPI 服务层
├── main.py # 入口文件
└── tests/
├── test_nodes.py
├── test_graph.py
└── test_tools.py开发里程碑:
| 阶段 | 内容 | 核心交付物 |
|---|---|---|
| Phase 1 | 核心骨架 | state.py + graph.py 基础组装,单轮搜索→提取→评估的闭环 |
| Phase 2 | 搜索能力 | tools/ 下所有适配器实现,至少接入 2 个搜索引擎 |
| Phase 3 | 迭代循环 | 实现完整的 should_continue 判断和循环逻辑 |
| Phase 4 | 报告生成 | report_generator 实现,输出 Markdown/HTML 格式报告 |
| Phase 5 | 生产化 | API 层、前端界面、部署配置、监控告警 |
到这里,我们对 DeepResearch 项目的全貌已经有了清晰的认知。接下来的一节,我们将深入到代码层面,一步步实现核心的状态定义和节点函数。