从零构建LangGraph智能体:基于Ollama本地模型与工具调用的AI应用开发实战
1. 项目概述:从零构建你的第一个LangGraph智能体
最近在AI应用开发领域,LangGraph这个框架的热度越来越高。很多朋友在后台问我,看了官方文档感觉概念很多,但真到自己动手想搭一个能实际跑起来的智能体(Agent)时,却不知道从哪里开始。这感觉就像拿到了一盒乐高,说明书只告诉你每个零件叫什么,却没教你怎么拼成一艘飞船。
我花了一些时间,深入研究了社区里一个叫 LangGraph-learn 的开源项目。这个项目本身是一个学习仓库,旨在帮助理解其配套的后端项目 LangGraph-GUI-backend 。但在我看来,它的价值远不止于此。它更像是一个精心设计的“脚手架”和“案例库”,通过一系列由简到繁的示例,清晰地揭示了如何将LangChain、LangGraph这些工具组合起来,构建出真正能处理复杂逻辑的AI工作流。今天,我就结合这个项目的思路和我自己的实操经验,带你彻底搞懂LangGraph的核心,并手把手带你从环境搭建到完成第一个具备记忆和工具调用能力的智能体。
简单来说,我们将要构建的,是一个可以和你进行多轮对话、能根据你的指令去执行特定操作(比如计算、搜索)的AI程序。它不再是简单的一问一答,而是一个有“状态”、能“思考”的循环系统。无论你是想开发一个自动化的客服助手、一个智能数据分析工具,还是一个个性化的内容生成引擎,这套方法论都是通用的基础。
2. 核心概念与工具选型解析
在动手写代码之前,我们必须先理清几个核心概念和为什么选择这些工具。很多教程直接扔代码,但如果不明白背后的“为什么”,一旦需求变化或者出了问题,你依然会束手无策。
2.1 LangChain vs. LangGraph:角色与定位
首先,最常被混淆的就是LangChain和LangGraph。你可以这样理解:
-
LangChain 是一个 工具箱 。它提供了连接大语言模型(LLM)、各种数据源(文档、数据库)、以及外部工具(搜索引擎、API)的标准接口和通用组件。比如,你想让LLM读取一个PDF文件然后回答问题,LangChain提供了
DocumentLoader、TextSplitter、RetrievalQA链等现成的模块,让你像搭积木一样快速组装出一个流程。它的核心是“链”(Chain),即一个预设的、线性的执行顺序。 -
LangGraph 是一个 流程控制器 。它建立在LangChain之上,专门用于构建 有状态、可循环、可分支 的复杂工作流。当你的智能体需要根据中间结果决定下一步做什么(比如,用户问题模糊时需要反问,计算完成后需要判断是否继续),或者需要维护一个跨多轮对话的“记忆”时,简单的链就不够用了。LangGraph引入了“图”(Graph)的概念,节点(Node)是执行单元,边(Edge)定义了流转逻辑,让你能够清晰地设计出智能体的“决策回路”。
一个生活化的类比 :假设你要组装一台电脑。
- LangChain就像是京东,提供了CPU、内存、显卡等所有标准化的配件(工具和组件)。
- 而LangGraph则是一张详细的 装机流程图 ,告诉你先装CPU到主板,然后涂硅脂、装散热器,接着插内存……它定义了这些配件组装起来的 顺序和条件 。没有LangGraph,你有一堆零件但可能装错;没有LangChain,你有流程图但找不到零件。
在这个项目中,我们同时需要两者:用LangChain来提供与大模型对话、调用工具的能力;用LangGraph来编排这些能力的执行顺序和循环逻辑。
2.2 为什么选择本地模型Ollama?
项目示例中大量使用了Ollama来运行本地大模型,如 llama3.2 、 qwen2.5 等。这背后有非常实际的考量:
- 成本与可控性 :对于学习和开发阶段,频繁调用GPT-4、Claude等闭源API会产生可观费用。使用本地模型,一次下载,无限次调用,成本为零,非常适合实验和迭代。
- 数据隐私与安全 :所有计算都在本地完成,敏感数据无需出域,这对于处理企业内部数据或隐私要求高的场景至关重要。
- 网络与延迟 :不依赖外部API,没有网络波动的影响,响应速度更稳定。
- 模型定制化 :Ollama支持量化模型,可以在消费级硬件(甚至带Apple Silicon芯片的Mac)上流畅运行足够聪明的模型,平衡了性能与资源消耗。
当然,本地模型的极限能力可能不及顶尖的闭源模型,但对于理解智能体原理、构建原型和许多实际应用来说,已经完全足够。在后续的扩展中,你可以轻松地将 ChatOllama 节点替换为 ChatOpenAI ,从而切换到云端模型。
2.3 关键组件拆解:State、Node、Edge
LangGraph的核心抽象是三个概念,理解了它们,就理解了整个框架。
-
State(状态) :这是一个贯穿整个工作流的共享字典。它定义了智能体在每一步“知道”什么。通常,我们会定义一个
TypedDict来规范状态的结构,例如:from typing import TypedDict, Annotated import operator class AgentState(TypedDict): # 当前的用户输入 messages: Annotated[list, operator.add] # 智能体思考的中间步骤(用于ReAct模式等) scratchpad: Annotated[list, operator.add] # 其他自定义字段,如已访问的网页、查询次数等 count: int这里的
Annotated[list, operator.add]是LangGraph的一个妙处,它声明这个字段是一个列表,并且当多个节点修改它时,采用“追加”(add)的方式合并,而不是覆盖。这完美契合了对话历史不断增长的需求。 -
Node(节点) :节点是一个普通的Python函数,它接收当前的
State,执行一些操作(如调用LLM、运行工具),然后返回一个包含更新内容的字典。这个字典会与旧状态按照Annotated的规则合并。例如,一个“调用模型”的节点函数,它读取state[‘messages’],发给LLM,然后将LLM的回复追加到messages中。 -
Edge(边) :边决定了流程的走向。分为两种:
- 起始边(Entry Point) :指定从哪个节点开始。
- 条件边(Conditional Edge) :这是实现智能的关键。它根据某个节点的输出,决定下一步该去哪个节点。例如,在“模型调用”节点之后,我们需要判断模型返回的是“最终答案”还是一个“工具调用请求”。如果是工具调用,就流向“执行工具”节点;如果是最终答案,就流向“结束”。
通过组合这些元素,我们就能画出一张控制智能体行为的“思维导图”。
3. 环境搭建与基础配置实操
理论清晰后,我们进入实战环节。一个稳定、可复现的环境是成功的第一步。我将以 LangGraph-learn 项目为蓝本,并结合更稳健的实践来讲解。
3.1 Python环境与依赖管理
强烈建议使用 conda 或 venv 创建独立的Python环境,避免包冲突。
# 使用conda(推荐)
conda create -n langgraph-learn python=3.11
conda activate langgraph-learn
# 或者使用venv
python3.11 -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
接下来安装核心依赖。 LangGraph-learn 的README给出了一个通用命令,但我们可以更精确一些。创建一个 requirements.txt 文件是更专业的做法。
# requirements.txt
langchain>=0.1.0
langchain-community>=0.0.10
langchain-core>=0.1.0
langgraph>=0.0.50
chromadb>=0.4.22 # 用于后续的RAG示例
httpx>=0.25.0
tenacity<8.4.0 # 注意版本限制,新版本可能有不兼容变更
然后安装:
pip install -r requirements.txt
注意 :Python包生态日新月异,尤其是AI领域。如果安装过程中出现版本冲突,可以尝试先安装核心框架,再按需安装其他。
tenacity是一个重试库,某些旧版本代码依赖其特定API,所以这里锁定了版本。
3.2 Ollama本地模型的部署与验证
- 安装Ollama :前往 Ollama官网 下载对应操作系统的安装包,一键安装。
- 拉取模型 :打开终端,运行以下命令拉取一个适合你硬件的中等规模模型。对于入门,
llama3.2:3b或qwen2.5:7b是不错的选择,对内存要求较低。ollama pull llama3.2:3b # 或 ollama pull qwen2.5:7b - 验证模型运行 :运行以下命令进行交互式测试,确保模型正常工作。
在提示符后输入“Hello”,看是否能得到正常回复。按ollama run llama3.2:3bCtrl+D退出。
3.3 编写你的第一个LangGraph智能体
现在,我们来构建一个最简单的、能进行多轮对话的智能体。这个智能体还没有工具调用能力,但已经具备了记忆功能。
# basic_agent.py
from typing import TypedDict, Annotated, Sequence
import operator
from langchain_community.chat_models import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import StateGraph, END
# 1. 定义状态
class AgentState(TypedDict):
# 对话历史。operator.add 表示节点更新时,消息是追加的。
messages: Annotated[Sequence[HumanMessage | AIMessage], operator.add]
# 2. 初始化模型
llm = ChatOllama(model="llama3.2:3b", temperature=0)
# 3. 定义节点函数
def call_model(state: AgentState):
"""节点:调用语言模型"""
print(f"[DEBUG] 调用模型,当前历史消息数:{len(state['messages'])}")
# 将历史消息直接发送给模型
response = llm.invoke(state["messages"])
# 返回一个字典,其中包含要更新到状态中的内容
return {"messages": [response]}
# 4. 构建图
workflow = StateGraph(AgentState)
# 添加节点,命名为“model”
workflow.add_node("model", call_model)
# 设置入口点:从“model”节点开始
workflow.set_entry_point("model")
# 设置边:执行完“model”节点后,流程结束。
# 注意:这是一个最简单的线性流,没有循环。
workflow.add_edge("model", END)
# 5. 编译图,生成可执行对象
app = workflow.compile()
# 6. 运行智能体
if __name__ == "__main__":
# 初始状态,只有一条用户消息
initial_state = {"messages": [HumanMessage(content="你好,请介绍一下你自己。")]}
# 运行图,传入初始状态
final_state = app.invoke(initial_state)
# 打印最终的所有消息
for msg in final_state["messages"]:
print(f"{type(msg).__name__}: {msg.content}")
运行这个脚本 python basic_agent.py ,你会看到模型进行了回复。但你会发现,它只进行了一轮对话。因为我们只定义了一条从 model 到 END 的边。要实现多轮对话,我们需要引入 循环 。
4. 实现循环与条件逻辑:打造有“思考”能力的智能体
一个真正的智能体应该能持续接收用户输入。我们需要修改图的结构,使其能够循环。
4.1 构建交互式循环智能体
我们将创建一个 __main__ 循环,每次迭代都调用图。同时,我们需要一个节点来处理用户输入。
# interactive_agent.py
from typing import TypedDict, Annotated, Sequence
import operator
from langchain_community.chat_models import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
llm = ChatOllama(model="llama3.2:3b", temperature=0)
def call_model(state: AgentState):
"""调用模型节点"""
response = llm.invoke(state["messages"])
return {"messages": [response]}
def human_input_node(state: AgentState):
"""人工输入节点:模拟用户输入"""
# 在实际应用中,这里可能从WebSocket、API或命令行获取输入
# 这里我们简单地从控制台读取
user_input = input("\n[用户]:")
human_message = HumanMessage(content=user_input)
return {"messages": [human_message]}
# 构建图
workflow = StateGraph(AgentState)
workflow.add_node("human_input", human_input_node)
workflow.add_node("model", call_model)
# 设置流程:开始 -> 等待用户输入 -> 调用模型 -> 结束
workflow.set_entry_point("human_input")
workflow.add_edge("human_input", "model")
workflow.add_edge("model", END) # 注意,这里还是直接结束了
app = workflow.compile()
if __name__ == "__main__":
# 初始化空对话
config = {"configurable": {"thread_id": "thread_1"}}
state = {"messages": []}
print("智能体已启动,输入‘退出’或‘quit’结束对话。")
while True:
# 调用图,它会执行 human_input -> model -> END
result = app.invoke(state, config=config)
# 更新状态为最新结果
state = result
# 打印模型的最新回复
last_msg = state["messages"][-1]
if isinstance(last_msg, AIMessage):
print(f"[助手]:{last_msg.content}")
# 检查用户是否想退出(这里需要改进,因为输入在节点内)
# 更好的方式是在`human_input_node`中判断并修改状态,通过条件边结束。
这个版本实现了多轮,但每次都需要重新 invoke ,并且退出逻辑不优雅。真正的LangGraph循环应该在图内部完成。
4.2 引入条件边实现图内循环
这才是LangGraph的精华所在。我们修改图的结构,让 model 节点之后不直接结束,而是根据模型输出的内容来决定下一步:是继续等待用户输入,还是结束。
为了实现这个,我们需要:
- 让模型以特定格式输出,便于我们解析其“意图”。
- 定义一个路由函数,根据模型输出决定下一个节点。
我们先给模型提供一个系统提示词,让它以 Action: 和 Final Answer: 的格式思考(这是ReAct模式的简化)。
# agent_with_cycle.py
from typing import TypedDict, Annotated, Sequence, Literal
import operator
import re
from langchain_community.chat_models import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langgraph.graph import StateGraph, END
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
llm = ChatOllama(model="llama3.2:3b", temperature=0)
# 更清晰的系统提示,指导模型格式化输出
system_prompt = SystemMessage(content="""你是一个有帮助的助手。请用以下格式回应:
思考:首先,思考用户的问题。
行动:如果需要使用工具,请输出‘Action: 工具名称’,然后在下一行‘Action Input: 输入内容’。
最终答案:如果可以直接回答,请输出‘Final Answer: 你的回答’。
""")
def call_model(state: AgentState):
"""调用模型节点,并注入系统提示"""
# 确保系统提示在消息列表开头(通常只需一次)
# 简单实现:每次都把系统提示放在最前面
messages_with_system = [system_prompt] + state["messages"]
response = llm.invoke(messages_with_system)
return {"messages": [response]}
def human_input_node(state: AgentState):
user_input = input("\n[用户]:")
if user_input.lower() in ["退出", "quit", "exit"]:
# 返回一个特殊信号,我们将通过状态传递给路由函数
return {"messages": [HumanMessage(content="用户请求退出。")], "should_exit": True}
return {"messages": [HumanMessage(content=user_input)]}
def should_continue(state: AgentState) -> Literal["human_input", "__end__"]:
"""路由函数:决定下一步是继续对话还是结束"""
# 检查是否有退出信号
if state.get("should_exit"):
return "__end__"
last_message = state["messages"][-1]
if isinstance(last_message, AIMessage):
content = last_message.content
# 简单解析模型输出,判断是否是最终答案
if "Final Answer:" in content:
print(f"[助手最终答案]:{content.split('Final Answer:')[-1].strip()}")
# 输出最终答案后,流程回到等待用户输入
return "human_input"
elif "Action:" in content:
# 这里本应去执行工具,我们先打印并假设工具执行完成,返回继续
print(f"[助手计划行动]:{content}")
# 模拟工具执行结果,并添加到消息历史
tool_result = f"工具执行结果:模拟完成了‘{content.split('Action:')[-1].split('\\n')[0].strip()}’操作。"
state["messages"].append(AIMessage(content=tool_result))
# 工具执行后,需要模型再次思考,所以返回‘model’节点(本例未实现,先回human)
return "human_input"
# 默认情况,继续等待用户输入
return "human_input"
# 构建图
workflow = StateGraph(AgentState)
workflow.add_node("human_input", human_input_node)
workflow.add_node("model", call_model)
workflow.set_entry_point("human_input")
# 关键:使用条件边
workflow.add_conditional_edges(
"model",
should_continue, # 路由函数
{
"human_input": "human_input",
"__end__": END
}
)
workflow.add_edge("human_input", "model")
app = workflow.compile()
if __name__ == "__main__":
print("智能体已启动(带循环)。输入‘退出’结束。")
# 使用configurable的thread_id来持久化图的状态
config = {"configurable": {"thread_id": "demo_thread_1"}}
# 初始状态包含系统提示(可选,也可在call_model中加)
initial_state = {"messages": []}
# 只需调用一次,图会根据条件边自动循环
# 但为了演示,我们这里用一个外部循环来模拟多次用户触发
# 实际上,更高级的用法是图在内部通过‘human_input’ -> ‘model’ -> (条件判断) -> ‘human_input’ 形成闭环
# 上述图结构实现了:用户输入 -> 模型思考 -> 判断 -> 若需继续则等待新输入。
# 但‘human_input’节点需要被持续触发。一个更闭环的设计是设置一个‘主循环’节点。
# 由于篇幅,我们先运行一个简化版:手动循环调用。
for _ in range(5): # 限制对话轮数
user_input = input("\n[用户]:")
if user_input.lower() in ["退出", "quit", "exit"]:
break
# 每次调用,都是从human_input节点开始
result = app.invoke({"messages": [HumanMessage(content=user_input)]}, config=config)
# 打印模型输出
for msg in result['messages'][-2:]: # 取最后两条(可能包含我们的模拟工具消息)
if isinstance(msg, AIMessage):
print(f"[助手]:{msg.content[:200]}...") # 截断显示
这个示例展示了条件边的威力。虽然为了简化,工具调用是模拟的,但整个“思考-判断-行动”的循环骨架已经搭建起来了。 should_continue 函数就是这个智能体的大脑前额叶,负责决策。
5. 集成工具调用与RAG:赋予智能体“手脚”和“记忆”
一个只会空想的智能体是不实用的。接下来,我们为其集成真实的工具调用和基于检索增强生成(RAG)的外部知识库。
5.1 为智能体添加计算器工具
我们使用LangChain的 @tool 装饰器来轻松创建工具。
# agent_with_tool.py
from typing import TypedDict, Annotated, Sequence, Literal
import operator
import json
from langchain_community.chat_models import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolExecutor, ToolInvocation
import langchain_core.messages as messages
# 1. 定义工具
@tool
def calculator(expression: str) -> str:
"""计算一个数学表达式。支持加减乘除(+-*/)和括号。"""
# 警告:实际生产环境应用使用安全评估,如`ast.literal_eval`或专用库
# 此处为演示,简单使用eval(仅用于受信任的输入演示)
try:
result = eval(expression)
return f"计算结果:{expression} = {result}"
except Exception as e:
return f"计算错误:{e}"
tools = [calculator]
tool_executor = ToolExecutor(tools)
# 2. 增强系统提示,告诉模型可用的工具
system_prompt = SystemMessage(content=f"""你是一个有帮助的助手,可以调用工具。
你可以使用的工具:
- calculator: 计算数学表达式。输入应为一个字符串,如“3 + 5 * 2”。
请严格按以下格式回应:
思考:首先分析问题,决定是否需要使用工具。
行动:如果需要,输出一个JSON对象,格式如下:
```json
{{
"action": "工具名称",
"action_input": "工具输入"
}}
最终答案:如果不需要工具或得到工具结果后,输出‘Final Answer: 你的回答’。 """)
class AgentState(TypedDict): messages: Annotated[Sequence[BaseMessage], operator.add]
llm = ChatOllama(model="llama3.2:3b", temperature=0)
将工具绑定到LLM,让它知道如何调用
llm_with_tools = llm.bind_tools(tools)
def call_model(state: AgentState): """调用绑定了工具的模型""" # 只取最近几条消息以避免上下文过长,实际项目可优化 recent_messages = state["messages"][-10:] response = llm_with_tools.invoke([system_prompt] + recent_messages) return {"messages": [response]}
def tool_node(state: AgentState): """工具执行节点""" last_message = state["messages"][-1] # 检查最后一条消息是否包含工具调用 if not last_message.tool_calls: # 没有工具调用,直接返回,不修改状态 return {"messages": []}
tool_calls = last_message.tool_calls
outputs = []
for tc in tool_calls:
try:
# 执行工具
result = tool_executor.invoke(tc)
outputs.append(ToolMessage(content=str(result), tool_call_id=tc['id']))
except Exception as e:
outputs.append(ToolMessage(content=f"工具调用错误:{e}", tool_call_id=tc['id']))
# 将工具执行结果作为消息追加
return {"messages": outputs}
def should_continue(state: AgentState) -> Literal["tools", " end "]: """路由函数:根据模型输出决定下一步""" last_message = state["messages"][-1] if isinstance(last_message, AIMessage) and last_message.tool_calls: # 模型要求调用工具 return "tools" # 否则,流程结束(因为我们假设一次调用只处理一个用户问题) return " end "
构建图
workflow = StateGraph(AgentState) workflow.add_node("agent", call_model) # 改名为agent更贴切 workflow.add_node("tools", tool_node)
workflow.set_entry_point("agent") workflow.add_conditional_edges( "agent", should_continue, {"tools": "tools", " end ": END} ) workflow.add_edge("tools", "agent") # 工具执行后,返回agent节点重新思考
app = workflow.compile()
if name == " main ": print("智能计算助手已启动。输入数学问题,如‘计算(15+27)/3的值’。") config = {"configurable": {"thread_id": "tool_thread_1"}}
while True:
user_input = input("\n[用户]:")
if user_input.lower() in ["退出", "quit", "exit"]:
break
initial_state = {"messages": [HumanMessage(content=user_input)]}
# 流式输出步骤,更直观
for event in app.stream(initial_state, config=config, stream_mode="values"):
for msg in event["messages"]:
if isinstance(msg, AIMessage) and msg.content:
print(f"[思考]:{msg.content}")
elif isinstance(msg, AIMessage) and msg.tool_calls:
print(f"[行动]:计划调用工具 {[tc['name'] for tc in msg.tool_calls]}")
elif isinstance(msg, ToolMessage):
print(f"[工具结果]:{msg.content}")
# 打印最终答案
final_state = app.get_state(config)
final_messages = final_state.values.get("messages", [])
if final_messages and isinstance(final_messages[-1], AIMessage) and final_messages[-1].content:
print(f"\n[最终答案]:{final_messages[-1].content}")
现在,你的智能体已经拥有了一个计算器工具!当你问它“123乘以456等于多少”时,它会先思考,然后输出一个结构化的工具调用请求,`tool_node`会捕获这个请求并执行计算,将结果返回给模型,模型再生成最终答案。这就是智能体“思考-行动-观察”的完整循环。
### 5.2 集成RAG(检索增强生成)
RAG能让智能体根据你提供的私有文档来回答问题。我们使用ChromaDB作为向量数据库,LangChain的文本分割器和检索器。
```python
# agent_with_rag.py (部分关键代码,集成到上述智能体中)
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
# 1. 准备知识库文档(示例:加载一个txt文件)
loader = TextLoader("./my_document.txt") # 你的文档路径
documents = loader.load()
# 2. 分割文本
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
docs = text_splitter.split_documents(documents)
# 3. 创建向量存储
embeddings = OllamaEmbeddings(model="nomic-embed-text") # 需要先拉取 embedding 模型: `ollama pull nomic-embed-text`
vectorstore = Chroma.from_documents(documents=docs, embedding=embeddings, persist_directory="./chroma_db")
retriever = vectorstore.as_retriever()
# 4. 定义一个RAG查询工具
@tool
def search_knowledge_base(query: str) -> str:
"""在内部知识库中搜索与问题相关的信息。"""
relevant_docs = retriever.invoke(query)
content = "\n\n---\n\n".join([doc.page_content for doc in relevant_docs[:3]]) # 取前三段
return f"根据知识库,相关信息如下:\n{content}"
# 5. 将 search_knowledge_base 工具添加到工具的列表中
tools.append(search_knowledge_base)
# 重新绑定工具到LLM
llm_with_tools = llm.bind_tools(tools)
# 更新系统提示,加入新工具的描述...
将RAG工具集成进去后,当用户问到“我们公司的项目截止日期是什么时候?”(假设 my_document.txt 中包含了公司信息),智能体会先调用 search_knowledge_base 工具,检索出相关文档片段,再结合这些信息生成最终答案。
6. 调试、优化与常见问题排查
在实际开发中,你一定会遇到各种问题。这里分享几个我踩过的坑和解决技巧。
6.1 调试与状态可视化
LangGraph提供了很好的状态查看功能。在 app.invoke 或 app.stream 过程中,可以打印状态变化。
# 在调用时,使用debug模式或手动打印
for event in app.stream(initial_state, stream_mode="values"):
print(f"当前节点状态: {event.keys()}")
if 'messages' in event:
last_msg = event['messages'][-1]
print(f"最新消息类型: {type(last_msg).__name__}")
更有效的是使用LangGraph的内置检查点(Checkpoint)和记忆(Memory)功能来持久化对话状态,这对于构建聊天机器人至关重要。 configurable 参数中的 thread_id 就是用于区分不同对话线程的。
6.2 常见错误与解决方案
-
TypeError: Object of type ‘AIMessage’ is not JSON serializable- 原因 :尝试将LangChain的Message对象直接存入JSON或传递给某些不支持序列化的接口。
- 解决 :在需要序列化时(如存入数据库、通过API传输),使用
message.model_dump()将其转换为字典,或message.model_dump_json()转换为JSON字符串。读取时再用AIMessage.model_validate_json()还原。
-
模型不遵循工具调用格式
- 原因 :小模型或提示词(Prompt)不够清晰。
- 解决 :
- 优化系统提示词,给出更精确的示例(Few-shot Prompting)。
- 使用
llm.bind_tools(tools, tool_choice=“auto”/“required”)来强制或引导模型使用工具。 - 考虑使用更大或更擅长工具调用的模型(如
qwen2.5:14b)。
-
图编译或运行时报错
KeyError- 原因 :状态(State)的键定义与节点返回的键不匹配,或使用了未定义的节点名。
- 解决 :仔细检查
TypedDict的定义、每个节点函数返回的字典键名,以及add_node和add_edge时使用的名称是否完全一致。LangGraph对类型和名称检查比较严格。
-
对话历史过长导致模型性能下降或API超时
- 原因 :
messages列表无限制增长。 - 解决 :实现一个“记忆管理”节点。可以定期总结对话历史,或将过旧的消息移除。一种常见策略是只保留最近N轮对话,或者将长对话总结成一个“系统记忆”片段。
- 原因 :
6.3 性能优化建议
- 模型选择 :在开发测试阶段使用小模型(如
llama3.2:3b),上线前根据需求评估换用更大模型或云端API。 - 异步处理 :如果智能体需要同时处理多个请求或调用耗时的外部API,使用
asyncio和LangGraph的异步接口(如ainvoke,astream)可以大幅提升吞吐量。 - 缓存 :对于重复的检索(RAG)或计算,可以引入缓存机制,例如使用
langchain.cache。 - 流式输出 :对于需要长时间思考的复杂任务,使用
app.stream将中间思考过程(AIMessage.content)流式返回给前端,用户体验会好很多。
构建LangGraph智能体是一个迭代的过程。从最简单的线性链开始,逐步添加循环、条件判断、工具调用和外部知识,最终形成一个功能强大的自主系统。 LangGraph-learn 项目提供的正是这样一个循序渐进的路径图。希望这篇结合了项目精髓和个人经验的指南,能帮你顺利踏上AI智能体开发的实践之路。记住,最好的学习方式就是动手,遇到问题就去翻阅官方文档和社区讨论,你会发现这片新大陆的生态正在飞速成长。
更多推荐



所有评论(0)