# LangGraph Deep Research Agent 全流程设计:多轮研究、人机协同与真实来源管理

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 并行分发研究子问题,提升效率
  • 理解真实来源管理:从搜索工具到报告引用的证据链
  • 掌握轮次限制和充分性判断,防止无限搜索

目录

  1. 三种应用的本质区别
  2. [Deep Research 核心流程](#Deep Research 核心流程)
  3. 案例架构:机制职责映射
  4. [State 设计:研究状态全景](#State 设计:研究状态全景)
  5. Human-in-the-Loop:控制研究方向和成本
  6. [Send 并行:分发研究子问题](#Send 并行:分发研究子问题)
  7. 工具调用:搜索与网页读取
  8. [真实来源管理:从 URL 到引用编号](#真实来源管理:从 URL 到引用编号)
  9. 充分性判断与轮次限制
  10. 报告生成与来源追加
  11. [SQLite Checkpoint 中断恢复](#SQLite Checkpoint 中断恢复)
  12. [踩坑清单:Deep Research的8个关键问题](#踩坑清单:Deep Research的8个关键问题)
  13. 面试速答版
  14. 总结与延伸
  15. 文末互动

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 并行更新
    # ... 其他字段

注意findingssources 会被多个并行 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 的全流程设计。如果觉得有帮助,欢迎点赞收藏,后续会更新自适应搜索策略和研究质量评分的进阶内容。

相关推荐
高工智能汽车14 分钟前
从“借船”到“造船”,汽车芯片出海迎历史性一跃
人工智能·汽车
Frank_refuel14 分钟前
MYSQL【进阶】 -> 索引(了解)
数据库·mysql
leoZ23116 分钟前
AI+前端提效- 06 AI辅助调试排错:前端报错、白屏、兼容问题极速定位
前端·人工智能·chatgpt·状态模式·超分辨率重建·openvino·dreamfusion
方银的技术分享16 分钟前
五、AI训练师:数据标注-视频标注
人工智能·音视频
SelectDB21 分钟前
开源!Apache Doris 上线 Profile 可视化诊断:基于 Doris Skills 破解 AI 误诊难题
数据库·agent·自动化运维
DBA_G21 分钟前
南大通用GBase 8s数据库的“一库三模“
数据库
李昊哲小课22 分钟前
SpringBoot4 云端咖啡站 阶段五:交付与进阶
人工智能·spring boot·大模型·log4j·智能体
阿里云大数据AI技术27 分钟前
一套 Spark SQL,打通多种 Catalog:EMR Serverless Spark 统一数据处理实践
人工智能·sql·spark
BFT白芙堂28 分钟前
Franka & DROID :面向真实场景的机器人操作数据集
人工智能·学习·机器学习·机器人·具身智能·franka·robotiq