LangGraph Deep Research Agent 全流程设计:多轮研究、人机协同与真实来源管理
导读:普通问答调用一次模型就结束,但"帮我调研一下向量数据库选型"这种复杂需求需要持续规划、搜索、阅读、评估、再搜索------这就是 Deep Research Agent。它不是简单的"问答增强",而是一个围绕复杂问题持续迭代的完整研究链路。本文基于 LangGraph 的完整案例,拆解 Deep Research 与普通问答/RAG 的本质区别,以及 Human-in-the-Loop、Send 并行研究、轮次限制、真实来源管理四个核心机制的工程实现。
适合读者:
- 需要构建"调研型 Agent"而不仅是"问答型 Agent"的开发者
- 对多轮搜索、证据链管理、人机协同中断恢复感兴趣的技术人员
- 正在用 LangGraph 设计复杂工作流、需要参考完整案例的工程师
- 准备 LangGraph 面试、需要回答"Deep Research 怎么设计"的同学
阅读收益:
- 理解 Deep Research 与普通问答、RAG 的三层区别
- 掌握 Human-in-the-Loop 在研究计划确认中的成本控制作用
- 学会用 Send 并行分发研究子问题,提升效率
- 理解真实来源管理:从搜索工具到报告引用的证据链
- 掌握轮次限制和充分性判断,防止无限搜索
目录
- 三种应用的本质区别
- [Deep Research 核心流程](#Deep Research 核心流程)
- 案例架构:机制职责映射
- [State 设计:研究状态全景](#State 设计:研究状态全景)
- Human-in-the-Loop:控制研究方向和成本
- [Send 并行:分发研究子问题](#Send 并行:分发研究子问题)
- 工具调用:搜索与网页读取
- [真实来源管理:从 URL 到引用编号](#真实来源管理:从 URL 到引用编号)
- 充分性判断与轮次限制
- 报告生成与来源追加
- [SQLite Checkpoint 中断恢复](#SQLite Checkpoint 中断恢复)
- [踩坑清单:Deep Research的8个关键问题](#踩坑清单:Deep Research的8个关键问题)
- 面试速答版
- 总结与延伸
- 文末互动
1. 三种应用的本质区别
1.1 普通问答 vs RAG vs Deep Research
| 维度 | 普通问答 | RAG | Deep Research |
|---|---|---|---|
| 调用次数 | 1 次模型调用 | 1 次检索 + 1 次生成 | 多轮规划→搜索→评估 |
| 知识来源 | 模型参数 | 预建知识库 | 实时搜索互联网 |
| 规划能力 | 无 | 无(固定检索) | 动态生成研究计划 |
| 证据链 | 不可追溯 | 有文档引用 | 有真实 URL 来源 |
| 人工介入 | 无 | 无 | 计划确认、方向修正 |
1.2 流程对比
普通问答:
用户问题 → 模型 → 答案
RAG:
用户问题 → 检索知识库 → 模型 → 答案
Deep Research:
研究需求 → 生成研究计划 → 人工确认
↓
并行研究多个子问题(Send)
↓
搜索网页 → 读取正文 → 整理证据
↓
评估资料是否充分?
├── 不充分 → 补充研究(最多 N 轮)
└── 充分 → 生成报告
↓
报告 + 真实来源列表
1.3 Deep Research 的核心不是"搜索次数"
错误理解:
"Deep Research 就是多搜几次"
正确理解:
"Deep Research 的核心是完整的证据链"
证据链要求:
报告结论
↓
研究 worker 整理的证据
↓
网页正文
↓
搜索工具返回的真实 URL
最终报告只能引用搜索工具返回的 URL,不能编造!
2. Deep Research 核心流程
1. 生成研究计划
↓
2. 人工确认或修改计划(Human-in-the-Loop)
↓
3. 并行研究多个子问题(Send 动态分发)
├── 搜索网页
├── 读取正文
└── 整理证据
↓
4. 合并资料并评估是否充分
├── 资料不足:补充研究一次(最多 max_iterations 轮)
└── 资料充分:进入报告生成
↓
5. 生成报告(限制引用来源列表)
↓
6. 程序追加真实来源列表
3. 案例架构:机制职责映射
本案例综合使用了前面章节的所有机制:
| 机制 | 本章中的作用 |
|---|---|
| Tool Calling | 让研究 Agent 搜索网页并读取正文 |
| Human-in-the-Loop | 搜索开始前确认或修改研究计划 |
| SQLite Checkpoint | 保存暂停位置和研究 State |
| Send | 根据计划数量动态创建研究任务 |
| Multi-Agent | 规划、研究、评估、报告分别处理 |
4. State 设计:研究状态全景
python
from typing import TypedDict
class ResearchState(TypedDict):
topic: str # 用户的完整研究需求
research_plan: list # 当前需要研究的子问题列表
approved: bool # 用户是否批准研究计划
findings: list # 各研究 worker 返回的结论
sources: list # 搜索工具真实返回的来源
need_more_research: bool # 是否需要补充研究
iteration: int # 已完成的研究轮次
final_report: str # 最终报告
4.1 字段设计 rationale
| 字段 | 为什么需要 |
|---|---|
topic |
研究主题,所有子问题围绕它展开 |
research_plan |
动态生成,可能多轮迭代 |
approved |
Human-in-the-Loop 的决策结果 |
findings |
reducer 字段,多个 worker 并行写入 |
sources |
reducer 字段,收集真实 URL |
need_more_research |
控制是否进入下一轮 |
iteration |
防止无限搜索 |
final_report |
最终输出 |
4.2 reducer 配置
python
from typing import Annotated
import operator
class ResearchState(TypedDict):
findings: Annotated[list[dict], operator.add] # 多个 worker 并行更新
sources: Annotated[list[dict], operator.add] # 多个 worker 并行更新
# ... 其他字段
注意 :findings 和 sources 会被多个并行 worker 同时写入,必须使用 reducer。
5. Human-in-the-Loop:控制研究方向和成本
5.1 为什么需要人工确认
问题:如果不做人工确认,直接开始搜索:
→ 研究计划可能有偏差(理解错用户意图)
→ 搜索 10 个子问题,其中 5 个 irrelevant
→ Token 费用和 API 调用浪费在错误方向上
人工确认的目的:
→ 在发生大量搜索和模型调用前,确认研究范围
→ 避免成本花在错误方向上
5.2 交互设计
系统显示研究计划:
1. Milvus 的部署方式和硬件要求
2. Qdrant 的部署方式和硬件要求
3. Pinecone 的 SaaS 定价模式
用户输入:
a → 批准当前计划并开始研究
r → 输入修改后的研究问题(如"再加一个 Chroma")
c → 取消任务
5.3 中断与恢复
python
"""Human-in-the-Loop 中断恢复"""
from langgraph.types import interrupt
def confirm_plan(state: ResearchState):
"""暂停等待人工确认"""
plan = state["research_plan"]
# interrupt() 暂停图执行,等待外部输入
decision = interrupt({
"plan": plan,
"options": "[a]批准 [r]修改 [c]取消",
})
if decision == "a":
return {"approved": True}
elif decision == "c":
return {"approved": False}
else:
# 用户输入新的研究问题
return {"research_plan": [decision], "approved": True}
5.4 Checkpoint 恢复机制
python
# 使用相同 thread_id 时:
# 1. 先检查 SQLite 中是否存在等待人工确认的任务
# 2. 如果存在,直接显示原来的计划并恢复
# 3. 不重新执行 plan_research(避免重复生成计划)
# 恢复时需要设置相同的 thread_id
config = {"configurable": {"thread_id": "research-thread-001"}}
result = graph.invoke(None, config=config) # None 表示从 Checkpoint 恢复
6. Send 并行:分发研究子问题
6.1 动态分发研究任务
python
from langgraph.types import Send
def dispatch_research(state: ResearchState):
"""根据研究计划动态创建研究 worker"""
plan = state["research_plan"]
return [
Send("research_worker", {
"topic": state["topic"],
"question": question,
})
for question in plan
]
# 如果 plan 有 3 个问题 → 创建 3 个 worker 并行研究
# 如果 plan 有 5 个问题 → 创建 5 个 worker 并行研究
6.2 Worker 隔离设计
python
class ResearchTaskState(TypedDict):
topic: str # 总体主题
question: str # 该 worker 负责的子问题
def research_worker(state: ResearchTaskState):
"""每个 worker 只研究一个子问题"""
topic = state["topic"]
question = state["question"]
# worker 独立调用研究 Agent(含搜索工具)
# 不共享其他 worker 的工具消息和网页正文
# 避免上下文污染
为什么隔离很重要:
错误设计:所有 worker 共享同一个 State
→ worker1 的搜索结果混入 worker2 的上下文
→ 模型被无关信息干扰
→ 结论质量下降
正确设计:每个 worker 只收到自己的 topic + question
→ 上下文干净
→ 结论精准
7. 工具调用:搜索与网页读取
7.1 两个核心工具
python
"""搜索工具:返回结构化结果 + 真实 URL"""
@tool
def web_search(query: str) -> str:
"""搜索网页并返回结果"""
results = tavily_client.search(query)
# 返回 JSON,方便后续提取 URL
return json.dumps([{
"url": r["url"],
"title": r["title"],
"snippet": r["content"],
} for r in results])
"""网页读取工具:只允许读取本轮搜索返回的 URL"""
@tool
def read_webpage(url: str, state: ResearchState) -> str:
"""读取网页正文,限制只读已搜索的 URL"""
# 安全检查:只允许读取本轮搜索到的 URL
allowed_urls = {s["url"] for s in state["sources"]}
if url not in allowed_urls:
return "Error: 只能读取搜索工具返回的 URL"
# 读取网页
response = httpx.get(url, timeout=30)
html = response.text
# 转成 Markdown,限制长度
markdown = markdownify(html)
return markdown[:5000] # 限制最大长度,避免无限上下文
7.2 创建研究 Agent
python
"""创建能自主调用工具的研究 Agent"""
from langchain.agents import create_tool_calling_agent
research_agent = create_tool_calling_agent(
llm=llm,
tools=[web_search, read_webpage],
prompt="你是一个研究助手,请通过搜索和阅读网页来回答研究问题。",
)
# Agent 会自主决定:
# 1. 是否需要搜索
# 2. 搜索后读取哪些网页
# 3. 何时停止搜索、给出结论
8. 真实来源管理:从 URL 到引用编号
8.1 为什么强调"真实来源"
错误做法:让模型自由生成引用
模型:"根据 [某网站] 的数据..."
→ "某网站"可能是编的
→ 用户无法验证
→ 不可信
正确做法:只引用搜索工具返回的真实 URL
模型:"根据 [1] 的数据显示..."
[1] https://real-website.com/article
→ URL 来自 Tavily 搜索结果
→ 用户可点击验证
→ 可信
8.2 来源收集流程
python
def research_worker(state: ResearchTaskState):
"""研究 worker:收集来源和证据"""
# 1. 调用研究 Agent(含工具调用)
agent_result = research_agent.invoke({
"topic": state["topic"],
"question": state["question"],
})
# 2. 从 ToolMessage 中提取真实来源
sources = []
for msg in agent_result["messages"]:
if isinstance(msg, ToolMessage) and msg.name == "web_search":
search_results = json.loads(msg.content)
sources.extend([{"url": r["url"]} for r in search_results])
# 3. 保存证据和来源
return {
"findings": [{"question": state["question"], "answer": agent_result["output"]}],
"sources": sources, # 真实 URL,不是模型编的!
}
8.3 报告中的引用限制
python
"""生成报告前的来源处理"""
def prepare_sources(state: ResearchState):
"""去重并编号来源"""
# 1. 按 URL 去重
unique_sources = {}
for source in state["sources"]:
url = source["url"]
if url not in unique_sources:
unique_sources[url] = source
# 2. 分配编号
numbered_sources = []
for i, (url, source) in enumerate(unique_sources.items(), 1):
source["id"] = i
numbered_sources.append(source)
return {"sources": numbered_sources}
# 报告 Agent 的 Prompt 限制:
REPORT_PROMPT = """
请基于以下资料生成研究报告。
【严格规则】
1. 只能使用来源列表中的编号 [1]、[2]... 引用
2. 不得编造 URL
3. 没有证据的内容必须标记为"推断"
4. 正文使用 [1]、[2] 形式引用
来源列表:
{sources}
研究发现:
{findings}
"""
8.4 程序追加来源列表
python
# 报告由模型生成,但来源列表由程序追加
# 避免模型自由编写来源
final_report = state["final_report"]
sources_list = "\n".join([
f"[{s['id']}] {s['url']}"
for s in state["sources"]
])
full_report = f"""
{final_report}
---
来源列表:
{sources_list}
"""
9. 充分性判断与轮次限制
9.1 为什么需要充分性判断
问题:第一轮搜索的资料可能不够全面
→ 需要判断"是否还需要再搜一轮"
但也不能无限搜索:
→ Token 费用爆炸
→ 用户等待时间太长
→ 可能陷入循环
9.2 充分性评估节点
python
def evaluate_research(state: ResearchState):
"""评估当前资料是否充分"""
findings = state["findings"]
iteration = state["iteration"]
max_iterations = 3 # 最多研究 3 轮
# 让模型判断资料是否充分
prompt = f"""
基于以下研究发现,判断资料是否足以回答原始问题:{state['topic']}
研究发现:
{findings}
请只回答以下两种之一:
- "充分":资料足够生成报告
- "不充分":需要补充研究,并说明还需要了解什么
"""
result = llm.invoke(prompt)
is_sufficient = "充分" in result.content
need_more = not is_sufficient and iteration < max_iterations
return {
"need_more_research": need_more,
"iteration": iteration + 1,
}
9.3 路由条件
python
def route_after_evaluate(state: ResearchState):
"""评估后的路由决策"""
if state["need_more_research"]:
return "more_research" # 再来一轮
return "write_report" # 生成报告
# 即使模型一直认为资料不足,达到 max_iterations 后也会进入报告节点
9.4 清空计划避免循环
python
# 评估充分时,清空当前研究计划
# 避免因为上一轮计划仍然存在而错误地继续搜索
if is_sufficient:
return {
"need_more_research": False,
"research_plan": [], # 清空!避免继续搜索
}
10. 报告生成与来源追加
10.1 报告生成节点
python
def write_report(state: ResearchState):
"""生成最终报告"""
# 1. 准备去重编号后的来源
sources = prepare_sources(state)
# 2. 组装 Prompt
prompt = REPORT_PROMPT.format(
sources=sources,
findings=state["findings"],
)
# 3. 调用模型生成报告
report = llm.invoke(prompt)
return {"final_report": report.content}
10.2 最终输出格式
# 向量数据库选型研究报告
## 1. Milvus
Milvus 支持分布式部署,适合大规模数据场景 [1]。
硬件要求:至少 8GB 内存,推荐 SSD 存储 [2]。
## 2. Qdrant
Qdrant 以轻量级部署著称,支持 Docker 一键启动 [3]。
...
---
来源列表:
[1] https://milvus.io/docs/install_cluster-milvusoperator.md
[2] https://milvus.io/docs/prerequisite-docker.md
[3] https://qdrant.tech/documentation/guides/installation/
...
11. SQLite Checkpoint 中断恢复
11.1 为什么需要 Checkpoint
场景:用户看到研究计划后,去开会了,1 小时后回来
没有 Checkpoint:
→ 研究计划丢失了
→ 需要重新生成计划
→ 可能生成不同的计划
有 Checkpoint:
→ 计划保存在 SQLite 中
→ 1 小时后用相同 thread_id 恢复
→ 直接从确认步骤继续
11.2 配置代码
python
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
# 创建 Checkpointer
connection = sqlite3.connect("research_checkpoints.sqlite", check_same_thread=False)
checkpointer = SqliteSaver(connection)
# 编译图时绑定
graph = builder.compile(checkpointer=checkpointer)
# 脚本结束时关闭连接
connection.close()
11.3 恢复流程
python
# 首次运行
topic = "帮我调研向量数据库选型"
config = {"configurable": {"thread_id": "research-001"}}
# 运行到 interrupt() 暂停
graph.invoke({"topic": topic}, config=config)
# 1 小时后恢复(相同 thread_id)
# 自动检测到之前暂停的位置,显示原计划
result = graph.invoke(None, config=config)
12. 踩坑清单:Deep Research的8个关键问题
| 序号 | 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|---|
| 1 | 不做人工确认 | 研究方向跑偏,浪费 Token | 计划可能理解错意图 | Human-in-the-Loop 确认 |
| 2 | worker 不隔离 | 上下文污染,结论质量差 | 共享 State | 每个 worker 只收 topic + question |
| 3 | 来源是模型编的 | 报告不可信 | 没从 ToolMessage 提取 | 只从 web_search 的 ToolMessage 提取 URL |
| 4 | 无限搜索 | Token 费用爆炸 | 没设轮次上限 | max_iterations 限制 |
| 5 | 没去重 | 来源列表重复 | 多个 worker 搜到相同网页 | 按 URL 去重后编号 |
| 6 | 没开 Checkpoint | 中断后从头开始 | 没配置 SQLite | compile(checkpointer=...) |
| 7 | 报告引用越界 | 引用 99 但只有 5 个来源 | 模型自由发挥 | Prompt 限制只能用列表中的编号 |
| 8 | 网页内容太长 | 上下文溢出 | 没限制长度 | markdown:5000 截断 |
13. 面试速答版
Deep Research 与普通问答和 RAG 的本质区别是:它不是单次调用,而是围绕复杂问题持续迭代的多轮研究链路。核心流程:生成计划 → 人工确认 → Send 并行研究 → 评估充分性 → 生成报告。四个关键机制:Human-in-the-Loop 控制研究方向和成本(确认后再搜,避免浪费)、Send 并行分发研究子问题(每个 worker 隔离上下文)、真实来源管理(只从搜索工具的 ToolMessage 提取 URL,模型不能编造)、轮次限制(max_iterations 防止无限搜索)。证据链要求:报告结论 → worker 证据 → 网页正文 → 搜索工具 URL,每一层都可追溯。Checkpoint 保存中断状态,支持跨进程恢复。
14. 总结与延伸
14.1 核心知识点回顾
Deep Research 核心流程:
计划 → 确认 → 并行研究 → 评估 → 报告
四种机制:
Human-in-the-Loop:控制方向和成本
Send:动态并行分发子问题
工具调用:搜索 + 读取网页
真实来源:从 ToolMessage 提取 URL,程序追加来源列表
安全设计:
max_iterations 防止无限搜索
充分性判断决定是否继续
清空 plan 避免循环
Checkpoint 支持中断恢复
证据链:
报告结论 → worker 证据 → 网页正文 → 搜索 URL
14.2 延伸方向
- 自适应搜索策略:根据子问题类型选择不同搜索工具(学术/新闻/技术文档)
- 研究质量评分:给每轮研究的资料质量打分,低分时自动换关键词
- 多语言研究:自动检测主题语言,切换搜索区域
- 可视化研究路径:展示每轮搜索的关键词变化和证据积累过程
15. 文末互动
你有没有遇到过"模型编造来源"的问题?是怎么发现和解决的?评论区聊聊你的来源管理经验。
思考题:如果研究 Agent 搜索到了 20 个网页,但其中 5 个是内容农场(低质量、重复内容),你的系统应该如何自动识别并过滤这些低质量来源------是基于域名黑名单、内容相似度检测,还是让 LLM 判断可信度?欢迎在评论区讨论。
本文聚焦 LangGraph Deep Research Agent 的全流程设计。如果觉得有帮助,欢迎点赞收藏,后续会更新自适应搜索策略和研究质量评分的进阶内容。