news 2026/10/22 9:00:22

用LangGraph搭建DeepResearch型AI Agent:从图编排到实战落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用LangGraph搭建DeepResearch型AI Agent:从图编排到实战落地

直接开工,咱们聊聊用LangGraph搭一个类似DeepResearch的AI Agent这件事。

我最早接触LangGraph是某次做复杂Agent项目,项目里要处理十几步动态决策,用LangChain的老写法写Chain怎么都别扭——条件分支、循环、人工确认、断点恢复,逻辑全堆一起,代码烂到不想看第二遍。后来换LangGraph,把Agent的每一步定义成节点,把流转规则画成图,整个项目结构瞬间清楚了。今天这篇就是把这段经验拆开揉碎,从核心原理到代码实操,按我实际跑通一套DeepResearch流程的路径来讲。

1. 为什么DeepResearch这类Agent必须用图来编排

1.1 传统Chain方案的根本痛点

先想一下DeepResearch这个场景到底在做什么。给一个复杂问题,Agent要先理解意图,拆成子问题,然后逐个调研、搜集信息、交叉验证,最后汇总成长文报告。这里面有几个绕不开的麻烦:

  • 调研深度没法预设。根据问题难度,可能需要查3个来源,也可能需要查10个来源,Agent得自己判断什么时候信息够了。
  • 分支逻辑复杂。某个子问题查不到答案,是换个关键词重查,还是直接跳过?答案需要追问时,要不要启动第二轮询问?
  • 流程会中断。API超时、单次查询结果异常、人类要中途补充信息,这些情况都需要流程能暂停、恢复。

老式的LangChain链式写法,本质是把步骤A到步骤F严格执行一遍,遇到"查不到就换个策略"这种动态分支,就得在代码里堆if-else,步骤一多整个逻辑就炸了。我见过一个项目写了十几个嵌套回调,中间改一次流程,等于把代码推倒重写。这不是写法问题,是抽象方式根本不匹配。DeepResearch的流程天然是一张图,不是一条链。

1.2 图结构如何让动态决策变成"搭积木"

LangGraph的核心抽象非常直接:把Agent的每一步定义成节点,把"下一步走哪"定义成边。节点之间通过共享状态传递数据,边可以是固定的,也可以是带条件判断的动态边。这样一来,流程的复杂度被分摊到一个个节点内部,节点之间的连接规则单独管理。

这个思路和电路图很像。每个节点像一个独立模块,输入进来,处理完,吐出去。你要加一个新功能,就增加一个节点,连两条边,不用去动老节点的代码。要改调研策略,就改一条条件边的判断逻辑。整个流程的复杂度从"代码里的逻辑"转移到了"图里的结构",而图是可视化的,一眼就能看懂。

1.3 状态管理与可控性:LangGraph的护城河

光有节点和边还不够,Agent系统最容易被忽略的是状态管理。DeepResearch跑起来可能持续几分钟,中间Agent的记忆、已查过的信息、待办任务、最终报告草稿,这些状态散落各处的话,调试起来会让人崩溃。

LangGraph用共享State对象管理所有数据,Any类型、TypedDict都行,灵活度很高。再加上LangGraph的检查点机制,每步执行完可以自动保存状态,进程挂了能从上次节点恢复,这个能力对长时间运行的Agent太关键了。更别提它还内置了"人在回路"的支持——在某个节点暂停,等用户确认再继续。这些不是锦上添花的功能,是做生产级Agent的刚需。

所以我的结论是,只要Agent流程里存在条件分支、循环、人工介入,图就是比链更合适的抽象方式。DeepResearch这种场景,几乎把图的所有优势都用上了。

2. LangGraph核心概念拆解:小白视角的通俗翻译

2.1 状态(State):Agent的共享笔记本

LangGraph里State是所有节点共享的数据容器,理解成团队协作时的共享笔记本就行。每个节点可以在笔记本上读内容、写新内容,下一个节点接着读。

代码里最常见的定义方式是用TypedDict,指定每个字段的类型:

from typing_extensions import TypedDict class ResearchState(TypedDict): question: str # 原始问题 plan: list # 拆分后的子问题列表 research_data: dict # 收集到的调研信息 report: str # 最终报告 iteration: int # 当前循环轮次

State的更新规则有一个关键点,默认是"覆盖式"的:节点返回 {"plan": [...]},会直接覆盖旧的plan。如果你想让多个字段累加,比如不断追加research_data,需要在定义State时用Annotated加reducer函数。

from typing import Annotated from langgraph.graph import add_messages class ResearchState(TypedDict): messages: Annotated[list, add_messages]

我用add_messages的示例比较多,因为它能自动处理消息追加,正好适合多轮调研的数据积累。其他自定义字段用什么reducer,根据业务逻辑定,初期用覆盖式足够了。

2.2 节点(Node):一次只做一件事的工人

节点就是普通的Python函数,输入整个State,输出要更新的字段。设计原则只有一个:职责单一。比如"生成调研计划"的节点只负责拆解问题,"执行搜索"的节点只负责调用搜索接口,"撰写报告"的节点只负责汇总输出。

每个节点就是一个工人,用笔记本(State)交流,各干各的活儿:

def generate_plan(state: ResearchState): question = state["question"] sub_questions = [f"{question}的最新进展", f"{question}的主要挑战"] return {"plan": sub_questions} def search_node(state: ResearchState): plan = state["plan"] results = [] for sq in plan: results.extend(search_api(sq, top_k=5)) return {"research_data": results}

2.3 边(Edge)与条件边(Conditional Edge):红绿灯

边就是"这个节点跑完,下一步跑谁"。固定边用add_edge直接连,但真正体现LangGraph能力的,是条件边。条件边是一个判断函数,输入State,输出下一个节点的名字。

def should_continue(state: ResearchState): if len(state["research_data"]) < 5: return "search_node" else: return "write_report"

这就像一个红绿灯,根据当前情况自动选择路径。DeepResearch的循环、提前终止、任务切换,都是靠条件边实现的。掌握条件边,等于掌握了图编排的灵魂。

2.4 Checkpointer:Agent的存档系统

Checkpointer负责在每步执行后保存State快照。有了它才有"断点续跑"、"随时暂停"、"流程回溯"这些能力。

from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() graph = workflow.compile(checkpointer=checkpointer)

MemorySaver适合开发和调试,数据放在内存里。生产环境建议用SQLite或PostgreSQL适配器持久化到磁盘或数据库,防止重启丢状态。这个细节,很多人一开始不重视,等系统真跑起来就知道有多重要了。

3. DeepResearch实战:从零搭一套多阶段调研Agent

3.1 项目需求与整体设计

目标很明确:输入任意问题,Agent自动完成"拆解问题-收集资料-补充验证-输出报告"全流程。技术栈选LangGraph做编排核心,搜索用通用的搜索API(任何你手头能用的封装都行),LLM调用先用OpenAI兼容接口示例。

整体流程图设计思路如下:

  • 入口:接收问题,生成调研计划。
  • 主循环:执行每轮搜索、提炼信息。
  • 判断:轮次不足或信息不够,继续循环;超过最大轮次,强制收尾。
  • 报告生成:所有信息汇总,写最终报告。
  • 人工确认(可选):报告前加一道确认闸门。

3.2 候选方案选型与理由

我之前想过用更多节点把流程做细,比如拆出"意图识别节点"、"关键词扩展节点"、"去重节点",但后来砍掉不少。原因是:图节点越多,流程越灵活,但每一跳都有函数调用开销和LLM调用开销,节点过多会让单轮流程变慢。DeepResearch的核心瓶颈在于LLM调用数量,所以节点划分要顺着"一次LLM调用"来切,而不是把函数拆得过碎。

搜索API我优先用了支持异步的版本,后面聊并行加速时你会看到异步的好处。LLM接口统一封装在util里,方便快速切换模型供应商。

3.3 环境准备与依赖安装

先搞定依赖。我推荐新建干净环境,避免依赖冲突:

python -m venv venv source venv/bin/activate # Windows上执行 venv\Scripts\activate pip install langgraph langchain-openai

版本说明:LangGraph更新很快,API有小幅变动,跑示例时如果报错先看模块名字是不是变了。搜索服务的具体封装各家不一样,核心是把网络请求的返回结果统一成字典结构保存进State。

3.4 从0到1实现:每一步的完整代码

先定义搜索工具。为了示例完整,我写一个模拟工具,你的实现换成真实搜索API就行:

import requests import os SEARCH_API_KEY = os.getenv("SEARCH_API_KEY") def search_web(query: str, top_k: int = 5) -> list: # 这里接任意搜索服务 # 返回: [{"title": ..., "link": ..., "snippet": ...}] return search_api_call(query=query, top_k=top_k)

再定义LLM调用函数,假设用OpenAI兼容接口:

from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, api_key=os.getenv("OPENAI_API_KEY"), )

接下来按节点实现编排。把每个节点定义成普通函数,加上清晰注释:

from typing_extensions import TypedDict class AgentState(TypedDict): question: str plan: list findings: dict iteration: int max_iterations: int report: str def plan_node(state: AgentState): """基于问题生成子问题列表""" prompt = f"针对问题【{state['question']}】,列出3-5个需要调研的子问题,输出JSON数组。" resp = llm.invoke(prompt) try: plan = json.loads(resp.content) except Exception: plan = [state["question"]] return {"plan": plan} def search_node(state: AgentState): """遍历计划,执行搜索,把结果累加到findings""" queries = state["plan"] new_findings = {} for q in queries: new_findings[q] = search_web(q, top_k=3) return {"findings": new_findings}

到这里,同步的搜索是慢的,每条搜索都要等网络。这还只是五六个查询,如果扩展到几十条,整套流程能跑几分钟。所以我实际项目里,搜索节点用的是异步实现,一会儿单独讲。

主循环的"提炼信息"节点,把原始搜索结果交给LLM提取要点,省得报告阶段处理海量噪音:

def analyze_node(state: AgentState): """把搜索到的原始信息压缩成要点""" findings = state["findings"] summary = {} for q, results in findings.items(): prompt = f"基于以下搜索结果,提炼关于问题的要点,不超过150字:\n{results}" summary[q] = llm.invoke(prompt).content return {"summaries": summary}

最后是"判断是否继续"的条件边逻辑:

def should_keep_searching(state: AgentState): """条件边:根据迭代次数和已收集信息决定下一步""" if state["iteration"] >= state["max_iterations"]: return "report" # 还可以加:关键信息缺失判断 if len(state.get("findings", {})) < 2: return "search_improved" return "analyze"

完整编译图的操作如下:

from langgraph.graph import StateGraph, START, END builder = StateGraph(AgentState) builder.add_node("plan", plan_node) builder.add_node("search", search_node) builder.add_node("analyze", analyze_node) builder.add_node("report", report_node) builder.add_edge(START, "plan") builder.add_edge("plan", "search") builder.add_edge("search", "analyze") builder.add_conditional_edges( "analyze", should_keep_searching, { "report": "report", "search_improved": "search", "analyze": "analyze", }, ) builder.add_edge("report", END) graph = builder.compile(checkpointer=MemorySaver())

运行整个流程:

config = {"configurable": {"thread_id": "research-001"}} initial_state = {"question": "如何优化本地大模型的推理延迟?", "plan": [], "findings": {}, "iteration": 0, "max_iterations": 3} result = graph.invoke(initial_state, config) print(result["report"])

这里的thread_id就是一个会话ID,checkpointer靠它区分不同会话的状态。同一个thread_id多次调用,流程可以接着上次的状态继续跑,这对长任务非常重要。

3.5 状态累积与循环控制的细节

我在搭建这个流程时遇到一个隐蔽问题:每次search节点返回 {"findings": new_findings},默认覆盖了之前的findings,导致上一轮的搜索成果直接被清空。如果你希望多轮调研的结果累积起来,就得靠reducer。

这里放一个实际可用的reducer写法:

from typing import Annotated def merge_dict(left: dict, right: dict) -> dict: if not left: return right merged = left.copy() for k, v in right.items(): if k in merged: # 如果同一问题的结果重复调研,合并列表并去重 merged[k] = merged[k] + v else: merged[k] = v return merged class AgentState(TypedDict): question: str plan: list findings: Annotated[dict, merge_dict] # 关键在这行 iteration: int max_iterations: int report: str

加了Annotated后,每次search节点返回的findings会自动和旧值合并,不再是覆盖。这个细节,新手十有八九会踩坑。

循环计数也有讲究。iteration字段在节点里负责自增,初始值0,plan_revision节点每次自增1,条件边根据iteration判断是否超限。记住:LangGraph自身没有内置循环限制,能不能正常退出全看你条件边的终止判断写没写对。为了防止意外死循环,我在启动参数里固定max_iterations,默认3轮,够用且安全。

4. 进阶能力:并行执行、检查点恢复与人在回路

4.1 异步节点实现并行搜索调用

前面提到,多个搜索之间互不依赖,串行跑太浪费时间。LangGraph原生支持异步节点,把节点函数定义成async def,invoke改成ainvoke即可。

import asyncio async def search_node_async(state: AgentState): queries = state["plan"] new_findings = await asyncio.gather( *(smart_search(q) for q in queries) ) return {"findings": dict(zip(queries, new_findings))}

smart_search里面用你选型的异步客户端发请求,结果一次性收集。我实测过,把8个搜索任务并行化后,整轮耗时从40秒压到10秒左右,用户体验完全不一样。DeepResearch这种任务,搜索越密集,收益越大。

异步节点在图里使用方式和同步节点完全相同,add_node的逻辑一致,编译阶段会帮你处理好事件循环。有一条要注意:如果你的LLM调用用了同步客户端,而搜索用了异步,建议把LLM调用也切成异步版本,或者干脆用asyncio.to_thread包装,避免事件循环被阻塞。

4.2 设置断点恢复:让人工确认Agent的中间结果

在生产环境里,让Agent完全自主跑完整个流程风险很大。最起码的保障,是在关键节点插入"人工确认"。

LangGraph实现这个非常直接,用interrupt_before参数让图在指定节点前暂停:

config = { "configurable": {"thread_id": "research-002"}, } graph.invoke(initial_state, config, interrupt_before=["report"])

流程执行到report节点前会暂停,返回当前状态。用户检查收集的信息够不够,不够就通过update_state补充指令让它继续,够了再resume:

user_feedback = "重点补充一下模型量化方案的对比信息" graph.update_state(config, {"user_feedback": user_feedback}) graph.invoke(None, config) # 继续执行

这个能力用来做"人工确认最终报告质量"、"确认是否追加预算查询"这类闸门,非常合适。生产级Agent很难一步到位全自动,"人在回路"是多数真实系统上线的必要条件。

4.3 用持久化Checkpointer实现任务断点续跑

MemorySaver适合调试,但生产环境进程一重启就全丢了。LangGraph官方提供了SQLite和Postgres等持久化适配器,我分享一个SQLite的简单接法:

from langgraph.checkpoint.sqlite import SqliteSaver import sqlite3 conn = sqlite3.connect("agent_state.db", check_same_thread=False) checkpointer = SqliteSaver(conn) graph = workflow.compile(checkpointer=checkpointer)

这样即便跑了一半由于网络错误崩溃,重启之后拿到相同的thread_id,直接从最后一个完成的节点继续,不用从头跑一遍。调研类任务动不动几十步,断点续跑的价值不用多说。

4.4 Stream模式实时观测Agent内部步骤

调试时我最常开的功能是stream模式。在LangGraph里,用stream代替invoke,就能像看直播一样看到每一步的输入输出:

for event in graph.stream(initial_state, config=config, stream_mode="updates"): for node_name, node_output in event.items(): print(f"节点 [{node_name}] 输出: {node_output}")

初期调试Agent,别急着看最终报告,先看每个节点喂进去什么、吐出来什么。很多问题就是这一步暴露出来的,比如搜索结果解析失败、LLM返回格式不对、字段没更新。stream模式就是Agent系统的调试器,学会用它,排查问题能省一半时间。

5. 常见坑与排查技巧:实测汇总

5.1 字段覆盖与状态合并异常

这个我前面说过了,是最高频的坑。症状:循环了几轮,findings始终只有最后一轮的数据。原因就是默认覆盖式更新。解决办法就是上Annotated+reducer。排查方法很简单,在流程中打印节点返回,发现返回里字段值不是累积的,基本就是没加reducer。

5.2 条件边路由名字不匹配

LangGraph的条件边映射表,键是条件函数的返回值,值是节点的名字。拼写不一致或者引用了不存在的节点名,编译时能通过但运行时会报错。我建议把节点名抽成常量:

NODE_PLAN = "plan" NODE_SEARCH = "search" NODE_ANALYZE = "analyze" NODE_REPORT = "report"

所有add_node、add_edge、映射表全用常量引用。字符串一多,人眼根本盯不住,常量至少能让IDE帮你查错。

5.3 LLM返回格式不稳定导致节点崩掉

LLM输出JSON时偶尔会有额外的解释文本,导致json.loads直接抛异常。我的习惯是在LLM调用时强制指定输出格式,或者做好异常兜底。前面plan_node里那个try/except就是兜底,解析失败就退回用原始问题当单一调研项。生产环境建议加个"重试一次"的逻辑,效果更好。

5.4 长流程超时与重试策略

DeepResearch跑几十步,任何一步网络抖动都会中断。我的建议分三层:

  • 给每次LLM调用设置合理的timeout参数。
  • 对搜索请求做重试包装,最多重试2次,间隔1秒。
  • 检查点设置好,任务中断后从最近节点恢复。

5.5 并行节点操作共享状态的注意点

当一个节点里用asyncio.gather并行执行多个任务时,注意这些任务内部不要直接修改State对象。并行任务只负责返回自己的数据,最终由节点统一组装成返回字典。多个写操作同时进行,LangGraph的Reducer也不是线程安全的,最好只在主节点函数里更新状态。并行改造前,先把功能跑通,再加并行。

5.6 Token消耗控制:降低生产成本的实战技巧

DeepResearch最烧钱的地方是每轮循环都要多次调用LLM。我的优化套路:主要流程只用轻量模型(如带mini后缀的),报告生成阶段才换强模型。搜索结果的"提炼要点"用便宜模型足够了,最终写报告再用贵模型,这样效果和成本平衡得很理想。再比如限制每轮最大搜索数、控制上下文的温度参数,这些细节叠加起来成本差异非常明显。我实际项目里,单次调研成本从几块降到几毛,就是因为做了轻量模型前置过滤。

6. 面向未来的扩展:让DeepResearch更强

6.1 接入RAG与本地知识库

搜索外部网络不是唯一的信息来源。企业内部的文档、本地知识库,同样应该成为DeepResearch的调研范围。在搜索节点里加一个判断:如果查询命中了本地知识库,先把本地结果和网络结果合并,再发给LLM提炼。LangGraph的节点是普通函数,加这样一道逻辑几乎不费事。

6.2 引入反射与自我修正

目前这套结构的自我纠错能力有限——信息不够只是多搜几轮,但不会主动反思"搜索策略是否有问题"。可以加一个reflection节点,每轮调研后让LLM评估当前信息是否能回答问题,输出评估意见和改进建议。条件边根据评估结果决定进入下一轮搜索还是写报告。这种反思机制能显著减少无效搜索。DeepResearch的核心目标不是多搜几轮,而是"给出的报告真的能解决疑问",有了反射节点,这个目标更容易逼近。

6.3 多Agent协作架构演进

复杂项目里,拆成多个专职协作的Agent可能更合适:一个Researcher Agent负责全网搜索,一个Reviewer Agent负责交叉验证信息,一个Writer Agent负责最终撰写。LangGraph对多Agent也很友好——每个Agent本身是一个节点,它们之间通过消息通信,在同一个图里协作,共享同一个State。这个方案比单体Step实现更容易维护,也更贴近真实团队分工。如果你的流程复杂到单Agent代码膨胀严重,这就是下一步该走的方向。

6.4 长期记忆与用户画像整合

对话型DeepResearch要保留用户的使用记录、偏好领域、历史关注话题。结合我们前面用的checkpointer机制,在State里增加memory字段,让Agent在调研前先读历史交互,提供更精准的调研方向。长期记忆引入还能让后续轮次的多轮对话更自然,这也是很多生产级Agent系统的标配能力。

写在最后:我的实际体会

如果非让我说这条技术路线里最重要的一句话,我会说:先用图想清楚Agent的流程,再动笔写代码。LangGraph最大的价值不是性能优化,也不是某个API有多好用,而是逼着你在动手之前把Agent的行为边界、分支逻辑、终止条件都想清楚。流程设计好了,代码只是顺着图填肉而已,小白也能顺畅写完;流程没想清楚,换什么框架都救不了你。

DeepResearch只是Agent应用的一种场景,但我强烈建议你把这个实战流程跑通一遍。这个项目麻雀虽小、五脏俱全——有条件边、有循环、有状态聚合、有断点恢复、有人工干预。这套组合拳打一遍,你对Agent编排的理解会上一个台阶。之后遇到任何复杂的Agent需求,你自然知道该从哪里下手。我个人踩过最深的坑就是早期过度追求node数量,最后维护成本压垮了项目;返璞归真,把节点按"一次任务、一次决策、一次LLM调用"划分之后,代码清爽度直线上升。希望这篇实战笔记能让你少走这些弯路,把绝大多数踩坑时间省下来,直接去做真正有价值的事情。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 8:59:53

从低功耗视频到端侧AI:芯片技术基因的延续

低功耗视频芯片这个赛道&#xff0c;放在五年前看&#xff0c;很多人会觉得是一桩“苦生意”&#xff1a;利润薄、周期长、技术门槛还高。但这两年端侧AI爆发&#xff0c;我才回过味来——当年那些愿意啃视频编码、功耗控制硬骨头的厂商&#xff0c;反而成了最有机会吃到智能AI…

作者头像 李华
网站建设 2026/10/22 8:59:45

深度学习驱动的公路隧道衬砌缺陷探地雷达智能识别

公路隧道衬砌检测这个方向&#xff0c;这几年在交通运维圈子里讨论热度一直不低。探地雷达本身不是什么新设备&#xff0c;很多检测单位早就在用&#xff0c;但真正卡脖子的环节一直是图像判读——波形剖面图摆在那&#xff0c;能看懂的人少&#xff0c;能一天看几百米测线还不…

作者头像 李华
网站建设 2026/10/22 8:59:36

AI赋能跨境电商:48小时产出高转化商品素材实战指南

各位跨境卖家、独立站操盘手&#xff0c;还有刚进场的外贸新人们&#xff0c;今天聊聊一个非常实际的话题&#xff1a;怎么用AI在48小时内&#xff0c;赶出一套让海外用户看了就想下单的商品素材。先说清楚&#xff0c;这不是什么玄学教程&#xff0c;也不是空中楼阁的理论。我…

作者头像 李华
网站建设 2026/10/22 8:58:32

嵌入式Linux上的.NET 10 Native AOT:从交叉编译到网关部署全记录

三年前我接了一个边缘采集网关的选型评估&#xff0c;硬件是某ARM Cortex-A53工业级板卡&#xff0c;存储和内存都只有512MB&#xff0c;原计划用C。当时这事儿在团队里争议不小&#xff0c;因为.NET在这个场景几乎算禁忌——传统方式把.NET runtime塞进去就要占上百MB&#xf…

作者头像 李华
网站建设 2026/10/22 8:58:31

AI赋能毕业论文全流程:从选题到定稿的完整工作流

临近毕业季&#xff0c;朋友圈里几乎每天都能刷到同一条求助&#xff1a;"论文写到一半写不动了""开题报告改了八遍还没过""降重降到怀疑人生"。我自己从去年开始深度使用一套代号为PaperXie的AI赋能学术写作全流程方案&#xff0c;从选题一路跑…

作者头像 李华
网站建设 2026/10/22 8:58:31

NPU推理框架集成实战:图执行扩展模块的设计与踩坑总结

最近小半年&#xff0c;我基本都在做同一件事&#xff1a;把第三方推理框架接入我们自研的NPU图执行扩展模块&#xff0c;也就是 npugraph_ex。这个模块名字里的 ex 是 extension 的意思&#xff0c;负责把框架侧动态执行的模型捕获成静态计算图&#xff0c;再做算子映射、内存…

作者头像 李华