LangGraph 从零搭建 AI Agent:状态图 + 工具调用 + 多步推理,一个真实案例讲透
这篇不是"hello world"教程——你会跟着搭一个能自动爬取论文、翻译摘要、生成中文综述报告的完整 Agent,全程可运行。
一、先搞清楚:为什么不是 LangChain,而是 LangGraph?
LangChain 解决的是"把 LLM 和各种工具串起来"的问题——一条线性的 Chain。但真实场景的 Agent 工作流从来不是线性的:
- 翻译失败 → 重试还是换引擎?
- 爬虫没抓到数据 → 跳过还是报错?
- 中间结果质量太低 → 让 LLM 自己 review 一遍?
LangChain 的 Chain 写分支逻辑很痛苦。LangGraph 用状态图(State Graph)解决——每一步都是一个节点,节点间用条件边连接,Agent 自己决定下一步走哪。
┌─────────────┐
│ 开始 │
└──────┬──────┘
│
┌──────▼──────┐
│ 爬取论文 │
└──────┬──────┘
│
┌──────▼──────┐
┌───────│ 判断结果 │───────┐
│ 失败 └─────────────┘ 成功 │
│ │
┌───────▼──────┐ ┌───────▼──────┐
│ 重试/跳过 │ │ 翻译摘要 │
└───────┬──────┘ └───────┬──────┘
│ │
└──────────┬───────────────────┘
│
┌──────▼──────┐
│ 生成综述 │
└──────┬──────┘
│
┌──────▼──────┐
│ 输出结果 │
└─────────────┘
LangGraph 三个核心概念(5 分钟搞懂)
| 概念 | 你的理解 | 代码对应 |
|---|---|---|
| State | Agent 的"记忆"——每一步都能读写 | 一个 TypedDict,记录当前数据、决策、错误 |
| Node | 一个执行单元——爬虫、翻译、审核 | 一个 Python 函数,输入 State 输出更新后的 State |
| Edge | 节点间的路——正常走还是岔路走 | add_edge 是直走,add_conditional_edges 是分岔 |
这三个概念就够搭出 90% 的 Agent。
二、实战:搭一个论文综述 Agent
场景:用户输入一个研究领域关键词 → Agent 自动找相关论文 → 翻译摘要 → 生成中文综述报告。
2.1 环境准备
pip install langgraph langchain-openai langchain-community arxiv httpx
2.2 定义 State:Agent 的共享记忆
from typing import TypedDict, List, Annotated
import operator
class ResearchState(TypedDict):
# 用户输入
query: str # 搜索关键词,如 "large language model agent"
max_papers: int # 最多爬几篇论文
# 中间结果
papers: List[dict] # 爬到的论文列表
errors: List[str] # 出错记录
# 翻译结果
translated_abstracts: List[str] # 翻译后的中文摘要
# 最终产出
final_report: str # 生成的中文综述
# 控制流
retry_count: int # 重试次数
next_step: str # 下一步走哪
2.3 写节点函数:每个节点做一件事
节点 1:爬取论文(带重试)
import arxiv
def fetch_papers(state: ResearchState) -> ResearchState:
"""从 arxiv 搜索论文,支持重试"""
print(f"🔍 正在搜索 arxiv: {state['query']}")
try:
client = arxiv.Client()
search = arxiv.Search(
query=state["query"],
max_results=state["max_papers"],
sort_by=arxiv.SortCriterion.Relevance
)
papers = []
for result in client.results(search):
papers.append({
"title": result.title,
"authors": [a.name for a in result.authors],
"published": result.published.strftime("%Y-%m-%d"),
"summary": result.summary.replace("\n", " "),
"url": result.entry_id,
"pdf_url": result.pdf_url
})
if not papers:
state["errors"].append(f"未找到与 '{state['query']}' 相关的论文")
else:
print(f"✅ 找到 {len(papers)} 篇论文")
state["papers"] = papers
state["retry_count"] = 0
except Exception as e:
state["errors"].append(f"爬取失败: {str(e)}")
state["retry_count"] += 1
return state
节点 2:翻译摘要
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
def translate_abstracts(state: ResearchState) -> ResearchState:
"""用 LLM 翻译每篇论文的摘要为中文"""
print(f"🌐 正在翻译 {len(state['papers'])} 篇论文摘要...")
translated = []
for i, paper in enumerate(state["papers"]):
prompt = f"""请将以下英文学术论文摘要翻译为中文,要求:
1. 保留专业术语的英文原文并用括号标注中文
2. 保持学术风格,不添加解释
3. 如果摘要过长,恰当分段
英文摘要:
{paper['summary']}
"""
response = llm.invoke(prompt)
translated.append(response.content)
print(f" [{i+1}/{len(state['papers'])}] ✅ {paper['title'][:50]}...")
state["translated_abstracts"] = translated
return state
节点 3:生成综述报告
def generate_report(state: ResearchState) -> ResearchState:
"""基于翻译后的摘要生成中文综述报告"""
print("📝 正在生成综述报告...")
# 组装上下文
papers_context = ""
for i, paper in enumerate(state["papers"]):
papers_context += f"""
### 论文 {i+1}:{paper['title']}
- 作者:{', '.join(paper['authors'])}
- 发表时间:{paper['published']}
- 链接:{paper['url']}
**中文摘要**:
{state['translated_abstracts'][i]}
---
"""
prompt = f"""你是一位资深学术研究者。请基于以下 {len(state['papers'])} 篇关于"{state['query']}"的论文,撰写一份中文综述报告。
{papers_context}
要求:
1. 开头概述该领域的研究现状和趋势(200字左右)
2. 按主题或方法对论文进行分类介绍,每组2-3篇
3. 指出各方法的优缺点和适用场景
4. 结尾给出未来研究方向建议
5. 每篇论文至少引用一次,标注为 [论文N]
6. 总字数控制在 1500-2500 字
请直接输出 Markdown 格式的综述报告。"""
response = llm.invoke(prompt)
state["final_report"] = response.content
print(f"✅ 综述报告生成完成({len(response.content)} 字符)")
return state
三、组装 Graph:让节点连起来
3.1 定义路由逻辑:Agent 自己决定走哪条路
def should_retry_fetch(state: ResearchState) -> str:
"""爬虫节点之后的路由决策"""
if state["papers"]:
return "translate" # 有数据 → 去翻译
elif state["retry_count"] < 3:
return "fetch_papers" # 没数据 + 还没重试够 → 再爬一次
else:
return "end" # 重试 3 次全败 → 放弃
def should_review(state: ResearchState) -> str:
"""翻译之后:要不要让 LLM 自审?"""
# 策略:如果翻译结果中有明显的"未翻译"标记,触发自审
for abstract in state["translated_abstracts"]:
if "Error" in abstract or "failed" in abstract.lower():
return "retry_translate"
return "generate_report"
3.2 构建 Graph
from langgraph.graph import StateGraph, END
# 创建状态图
workflow = StateGraph(ResearchState)
# 添加节点
workflow.add_node("fetch_papers", fetch_papers)
workflow.add_node("translate_abstracts", translate_abstracts)
workflow.add_node("generate_report", generate_report)
# 设置入口
workflow.set_entry_point("fetch_papers")
# 添加条件边:爬虫 → 翻译 or 重试 or 结束
workflow.add_conditional_edges(
"fetch_papers",
should_retry_fetch,
{
"translate": "translate_abstracts",
"fetch_papers": "fetch_papers", # 重试回到自己
"end": END
}
)
# 翻译 → 生成报告(直连)
workflow.add_edge("translate_abstracts", "generate_report")
# 报告 → 结束
workflow.add_edge("generate_report", END)
# 编译
app = workflow.compile()
3.3 运行
result = app.invoke({
"query": "AI agent tool use LLM",
"max_papers": 5,
"papers": [],
"errors": [],
"translated_abstracts": [],
"final_report": "",
"retry_count": 0,
"next_step": "fetch_papers"
})
# 结果在 result["final_report"] 里
print(result["final_report"])
四、进阶:给 Agent 装上外部工具
纯 LLM 推理的 Agent 能力有限。LangGraph 的杀手锏是把工具调用也建模为节点。
4.1 定义工具节点
from langchain.tools import tool
from langgraph.prebuilt import ToolNode
@tool
def search_web(query: str) -> str:
"""搜索网络获取最新信息"""
# 实际项目中接入 SerpAPI / Tavily / Brave Search
import httpx
response = httpx.get(
"https://api.duckduckgo.com/",
params={"q": query, "format": "json"}
)
data = response.json()
snippets = [item.get("Text", "") for item in data.get("RelatedTopics", [])[:3]]
return "\n---\n".join(snippets) if snippets else "无搜索结果"
@tool
def save_to_file(content: str, filename: str) -> str:
"""将内容保存到本地文件"""
with open(filename, "w", encoding="utf-8") as f:
f.write(content)
return f"✅ 已保存到 {filename}({len(content)} 字符)"
tools = [search_web, save_to_file]
4.2 扩展现有 Agent:加上联网搜索
from langgraph.prebuilt import ToolNode
# 在原有 workflow 上加一个新节点
tool_node = ToolNode(tools)
workflow.add_node("search_web", tool_node)
# 修改论文爬虫后的路由:如果 arxiv 没数据,自动切到 web 搜索
def smart_fetch_router(state: ResearchState) -> str:
if state["papers"]:
return "translate"
elif state["retry_count"] >= 2:
# arxiv 两轮都没数据,切 web 搜索
return "search_web"
else:
return "fetch_papers"
workflow.add_conditional_edges(
"fetch_papers",
smart_fetch_router,
{
"translate": "translate_abstracts",
"fetch_papers": "fetch_papers",
"search_web": "search_web"
}
)
4.3 加一个"最终输出"节点:自动保存报告
def save_report(state: ResearchState) -> ResearchState:
filename = f"综述_{state['query'].replace(' ', '_')}.md"
with open(filename, "w", encoding="utf-8") as f:
f.write(state["final_report"])
print(f"📄 报告已保存至 {filename}")
return state
workflow.add_node("save_report", save_report)
workflow.add_edge("generate_report", "save_report")
workflow.add_edge("save_report", END)
五、可视化你的 Agent Graph
LangGraph 内置了 Mermaid 可视化——尤其当你开始写几十个节点的复杂 Agent 时,这个救命:
from langgraph.graph import END
from IPython.display import Image, display
# 生成 Mermaid 图
mermaid_png = app.get_graph().draw_mermaid_png()
with open("agent_graph.png", "wb") as f:
f.write(mermaid_png)
display(Image(mermaid_png))
输出就是一张完整的流程图,所有节点和条件边一目了然。
六、生产环境三个踩坑经验
1. State 不要太大
Agent 每走一步,整个 State 都会传给 LLM。你的 translated_abstracts 如果包含 10 篇论文的中文摘要,一次请求可能吃掉几万 token。解决:节点间传递摘要而非全文,最终生成报告时才用完整数据。
2. 加 human-in-the-loop 断点
LangGraph 支持在任何节点前暂停,等你人工确认:
# 编译时加 interrupt_before
app = workflow.compile(interrupt_before=["generate_report"])
# 运行时会在 generate_report 前暂停
result = app.invoke(input_data)
# 你看完中间结果后,手动批准继续
result = app.invoke(None, config) # None = 从断点继续
这个功能在"Agent 要发邮件 / 要花钱调 API"的场景下是硬需求。
3. 用 Checkpointer 持久化状态
默认 State 在内存里,App 挂了全丢。加一个 SQLite checkpointer:
from langgraph.checkpoint.sqlite import SqliteSaver
with SqliteSaver.from_conn_string("agent_state.db") as checkpointer:
app = workflow.compile(checkpointer=checkpointer)
# 每次 invoke 传入 thread_id
result = app.invoke(input_data, {"configurable": {"thread_id": "session-1"}})
中断后恢复、长时间运行的批量任务,靠它续命。
七、LangGraph vs 其他 Agent 框架
| 框架 | 设计哲学 | 适合场景 | 学习曲线 |
|---|---|---|---|
| LangGraph | 显式状态图,节点+边 | 复杂的多步推理、需要精确流程控制 | 🟡 中 |
| CrewAI | 角色扮演,多 Agent 对话 | 团队协作模拟 | 🟢 低 |
| AutoGen | 对话驱动,消息传递 | 多 Agent 对话式协作 | 🟡 中 |
| OpenAI Swarm | 轻量级,handoff 模式 | 简单的任务分发 | 🟢 低 |
| 手写 Agent | 自己写所有逻辑 | 简单场景、有特殊需求 | 🔴 高 |
LangGraph 的不可替代性:你要的是一个稳定的、可测试的、能精确控制每一步决策的 Agent,而不是让 LLM 在"黑盒里自由发挥"。当你需要"第 3 步失败就回退到第 1 步"这种精确控制时,其他框架都会让你抓狂。
八、总结
这篇你 get 了:
- LangGraph 三个核心概念(State / Node / Edge)就能搭 Agent
- 一个真实可运行的论文综述 Agent 完整代码
- 工具节点(ToolNode)的集成方式
- 三个生产环境必须注意的坑
下一步建议:把这两个连起来——用 LangGraph 编排一个 Agent,用 MCP 协议暴露文件系统和数据库工具给这个 Agent。爬虫 Agent 从 arxiv 抓论文 → 翻译 Agent 处理摘要 → 报告 Agent 生成综述 → 最后通过 MCP Server 把报告写入本地文件。全链路自动化,一篇新的硬核文章就有了。
更多推荐
所有评论(0)