在AI智能体的开发浪潮中,能够完成基础任务调用的智能体早已不是终点。当我们将智能体投入实际应用场景,比如长期科研辅助、多轮客户咨询、复杂任务编排时,很快就会发现两个关键痛点:一是智能体"记不住事",每次会话都是全新开始,无法衔接历史上下文;二是智能体"太自主",执行过程不透明,遇到错误无法及时干预,只能被动接受最终结果。
LangGraph作为LangChain生态中专注于流程编排的核心框架,给出了极具针对性的解决方案。通过其内置的断点恢复机制和状态流式控制能力,我们能够轻松为智能体赋予"长期记忆"和"可控执行"的核心属性。本文将基于实战视角,从环境搭建到核心功能落地,再到进阶的人工介入设计,完整拆解如何打造一个有记忆、能暂停、可插手的AI智能体,让技术落地更贴合实际应用需求。
在正式开始前,我们先明确核心目标。相较于基础的LangGraph智能体,本次进阶实战需要实现三大突破:首先是对话状态的持久化,让智能体能够跨会话保存历史轨迹,中断后可恢复;其次是执行过程的可视化,实时追踪每一步的状态流转,清晰掌握智能体的思考与行动逻辑;最后是人工介入机制,在关键节点介入决策,避免智能体陷入错误执行路径。这三大能力的结合,将让智能体从"能干活"升级为"会干活、好掌控"。

一、实战基础:环境准备与核心代码铺垫
本次进阶实战基于上一期的基础LangGraph智能体框架展开,因此我们先完成环境配置和基础代码的梳理,确保后续功能升级能够平稳衔接。对于尚未接触过基础框架的读者,也可以通过本节的代码快速搭建起可运行的基础环境。
首先是环境安装。我们需要借助Anaconda创建独立的虚拟环境,避免依赖冲突。打开终端输入以下命令,完成环境搭建和核心依赖安装:
conda create -n langgraph python=3.12 -y
conda activate langgraph
pip install langgraph langchain langchain-openai langchain-community openai pygraphviz langgraph-checkpoint-sqlite
这里需要特别说明,langgraph-checkpoint-sqlite是本次断点恢复功能的核心依赖,用于将状态数据持久化到SQLite数据库;pygraphviz则用于可视化智能体的执行流程,方便我们梳理节点关系。
接下来是基础智能体框架的核心代码。该框架包含工具定义、状态定义、Agent实现、模型配置四大核心模块,能够实现基础的工具调用能力。具体代码如下,关键部分已添加注释说明:
python
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated, Type
import operator
from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field, PrivateAttr
from langchain_core.tools import BaseTool
import requests
# 工具定义:博查搜索工具,用于获取实时信息
class BoChaSearchInput(BaseModel):
query: str = Field(..., description="搜索的查询内容")
class BoChaSearchResults(BaseTool):
name: str = "bocha_web_search"
description: str = "使用博查API进行网络搜索,可用来查找实时信息或新闻"
args_schema: Type[BaseModel] = BoChaSearchInput
_api_key: str = PrivateAttr()
_count: int = PrivateAttr()
_summary: bool = PrivateAttr()
_freshness: str = PrivateAttr()
def __init__(self, api_key: str, count: int = 5, summary: bool = True, freshness: str = "noLimit", **kwargs):
super().__init__(**kwargs)
self._api_key = api_key
self._count = count
self._summary = summary
self._freshness = freshness
def _run(self, query: str) -> str:
url = "https://api.bochaai.com/v1/web-search"
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json"
}
payload = {
"query": query,
"summary": self._summary,
"freshness": self._freshness,
"count": self._count
}
try:
response = requests.post(url, headers=headers, json=payload, timeout=10)
response.raise_for_status()
data = response.json()
results = data.get("data", {}).get("webPages", {}).get("value", [])
if not results:
return f"未找到相关内容。\n[DEBUG] 返回数据:{data}"
output = ""
for i, item in enumerate(results[:self._count]):
title = item.get("name", "无标题")
snippet = item.get("snippet", "无摘要")
url = item.get("url", "")
output += f"{i+1}. {title}\n{snippet}\n链接:{url}\n\n"
return output.strip()
except Exception as e:
return f"搜索失败:{e}"
# 智能体状态定义:存储对话消息,通过operator.add实现消息累加
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], operator.add]
# Agent实现:核心流程编排,包含LLM调用和工具执行两个节点
class Agent:
def __init__(self, model, tools, system=""):
self.system = system
graph = StateGraph(AgentState)
graph.add_node("llm", self.call_openai) # LLM思考节点
graph.add_node("action", self.take_action) # 工具执行节点
# 条件边:根据LLM是否产生工具调用,决定进入工具执行节点或结束流程
graph.add_conditional_edges("llm", self.exists_action, {True: "action", False: END})
graph.add_edge("action", "llm") # 工具执行后返回LLM继续思考
graph.set_entry_point("llm") # 流程入口:LLM节点
self.graph = graph.compile() # 编译流程
self.tools = {t.name: t for t in tools}
self.model = model.bind_tools(tools) # 为模型绑定工具
# 判断LLM是否产生工具调用
def exists_action(self, state: AgentState):
result = state['messages'][-1]
return len(result.tool_calls) > 0
# 调用LLM生成响应
def call_openai(self, state: AgentState):
messages = state['messages']
if self.system:
messages = [SystemMessage(content=self.system)] + messages
message = self.model.invoke(messages)
return {'messages': [message]}
# 执行工具调用
def take_action(self, state: AgentState):
tool_calls = state['messages'][-1].tool_calls
results = []
for t in tool_calls:
print(f"调用工具:{t}")
if not t['name'] in self.tools:
print("\n ....工具名称错误....")
result = "工具名称错误,请重试"
else:
result = self.tools[t['name']].invoke(t['args'])
results.append(ToolMessage(tool_call_id=t['id'], name=t['name'], content=str(result)))
print("返回LLM继续思考!")
return {'messages': results}
# 模型和启动配置
prompt = """
你是一名聪明的科研助理,可以使用搜索引擎查找信息。你可以多次调用搜索,既可以一次性调用,也可以分步骤调用。只有当你确切知道要找什么时,才去检索信息。如果在提出后续问题之前需要先检索信息,你也可以这样做!
"""
# 配置通义千问模型(需替换为自己的API Key)
aliyun_api_key = '你的api_key'
model = ChatOpenAI(
model="qwen-plus",
openai_api_key=aliyun_api_key,
openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
# 初始化工具和Agent
tool = BoChaSearchResults(api_key="你的api_key", count=4)
abot = Agent(model, [tool], system=prompt)
基础框架搭建完成后,我们可以通过简单的测试验证其功能。输入查询指令,智能体将调用博查搜索工具获取信息并返回结果。但正如我们开篇所说,这个基础框架存在明显的局限性,一旦程序中断,所有对话历史都会丢失,且执行过程完全黑盒,无法干预。接下来,我们将针对这两个问题,逐步实现断点恢复和流式控制能力。
二、断点恢复:给智能体装上"长期记忆"
断点恢复的核心价值在于实现对话状态的持久化保存,让智能体能够"记住"历史对话轨迹和中间执行步骤。即使程序中途中断,再次启动时也能从断点继续运行,这对于长周期任务和多轮对话场景至关重要。LangGraph通过Checkpoint系统实现这一功能,配合SQLite数据库,能够轻松构建起"线程级"的长期记忆机制。
2.1 核心原理:Checkpoint与状态持久化
LangGraph的Checkpoint系统本质上是一种状态快照机制,能够在智能体执行流程的关键节点,将当前的AgentState(对话消息、执行位置等)保存到指定的存储介质中。当需要恢复时,系统通过读取存储的快照数据,还原之前的执行状态,让流程从断点继续推进。
本次实战我们选择SQLite作为存储介质,原因在于其轻量易用,无需额外部署数据库服务,适合快速开发和测试。LangGraph提供了专门的langgraph-checkpoint-sqlite库,用于实现Checkpoint与SQLite的对接。
2.2 实战落地:配置SqliteSaver
首先,我们需要导入SqliteSaver并初始化数据库连接。这里有两种常见的配置方式,分别适用于测试环境和生产环境,我们逐一说明。
第一种是内存数据库配置,仅适用于测试场景。这种方式创建的数据库存储在内存中,程序退出后数据会全部丢失,优点是无需占用磁盘空间,运行速度快。代码如下:
python
from langgraph.checkpoint.sqlite import SqliteSaver
# 创建内存数据库
memory = SqliteSaver.from_conn_string(":memory:")
第二种是本地磁盘数据库配置,适用于生产环境。这种方式将数据库文件保存到本地磁盘,程序退出后数据依然保留,能够实现真正的长期记忆。代码如下:
python
from langgraph.checkpoint.sqlite import SqliteSaver
import os
# 设置数据库保存路径:当前目录下的checkpoints.db文件
db_path = os.path.abspath("checkpoints.db")
db_uri = f"sqlite:///{db_path}"
# 初始化本地磁盘数据库
memory = SqliteSaver.from_conn_string(db_uri)
这里需要注意一个常见的坑。在实际运行过程中,直接使用from_conn_string方法可能会出现"AttributeError: '_GeneratorContextManager' object has no attribute 'get_next_version'"错误。这是因为早期版本的langgraph-checkpoint-sqlite中,from_conn_string是一个上下文管理器,返回的不是SqliteSaver实例,而是_GeneratorContextManager对象,缺少LangGraph所需的方法。
解决这个问题的方法是手动构造SqliteSaver实例,通过直接创建sqlite3连接来避免上下文管理器的问题。修改后的代码如下:
python
import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
import os
db_path = os.path.abspath("checkpoints.db")
# 创建SQLite连接,设置check_same_thread=False允许跨线程访问
conn = sqlite3.connect(db_path, check_same_thread=False)
memory = SqliteSaver(conn)
这样就能成功创建具备完整功能的SqliteSaver实例,为后续的状态持久化做好准备。
2.3 集成到Agent框架
数据库配置完成后,我们需要将其集成到之前的Agent框架中。核心步骤是在Agent类的初始化方法中添加checkpointer参数,并将其传入graph.compile()方法,让流程编译时启用Checkpoint功能。
修改Agent类的__init__方法如下:
python
class Agent:
def __init__(self, model, tools, checkpointer, system=""):
self.system = system
graph = StateGraph(AgentState)
graph.add_node("llm", self.call_openai)
graph.add_node("action", self.take_action)
graph.add_conditional_edges("llm", self.exists_action, {True: "action", False: END})
graph.add_edge("action", "llm")
graph.set_entry_point("llm")
# 编译时传入checkpointer,启用断点恢复
self.graph = graph.compile(checkpointer=checkpointer)
self.tools = {t.name: t for t in tools}
self.model = model.bind_tools(tools)
修改完成后,创建Agent实例时需要传入之前初始化的memory对象:
python
abot = Agent(model, [tool], system=prompt, checkpointer=memory)
到这里,Agent框架的断点恢复功能已经配置完成。但此时直接运行测试代码,还会遇到新的错误。
2.4 关键配置:thread_id的作用与使用
当我们使用基础测试代码调用智能体时:
python
messages = [HumanMessage(content="What is the weather in sf?")]
result = abot.graph.invoke({"messages": messages})
print(result['messages'][-1].content)
会出现"ValueError: Checkpointer requires one or more of the following 'configurable' keys: thread_id, checkpoint_ns, checkpoint_id"错误。这个错误的核心原因是,Checkpoint系统需要一个唯一标识符来区分不同的会话或任务,否则无法确定将状态快照归档到哪里。
在这三个可选的标识符中,thread_id是最常用的,它可以理解为"会话身份证",每个thread_id对应一个独立的对话线程,状态快照会按thread_id分类存储。我们只需要在调用graph.invoke()时,通过config参数传入thread_id即可解决问题。
修改后的测试代码如下:
python
messages = [HumanMessage(content="What is the weather in sf?")]
# 传入thread_id,标识当前对话线程
result = abot.graph.invoke(
{"messages": messages},
config={"configurable": {"thread_id": "user-001"}}
)
print(result['messages'][-1].content)
此时再次运行代码,就能正常执行并生成checkpoints.db文件。这个文件中存储了当前对话线程的所有状态信息,包括历史消息、执行位置等。
为了验证断点恢复和长期记忆功能,我们可以进行一个简单的测试。首先运行上述代码,询问旧金山的天气;然后修改查询内容,不提及"天气"关键词,仅询问"广州呢?",并使用相同的thread_id:
python
# 第二次询问,不提及天气关键词
messages = [HumanMessage(content="What about Guangzhou?")]
result = abot.graph.invoke(
{"messages": messages},
config={"configurable": {"thread_id": "user-001"}}
)
print(result['messages'][-1].content)
运行结果会发现,智能体依然调用了博查搜索工具,查询广州的天气信息。这说明智能体通过thread_id关联了历史对话,"记住"了上一轮的查询主题是天气。如果我们更换一个新的thread_id,比如"user-002",再执行同样的查询,智能体就会询问具体需要了解广州的什么信息,而不会主动搜索天气。
这个测试充分验证了断点恢复机制的有效性。通过thread_id,我们可以实现不同对话线程的隔离和状态持久化,让智能体具备了真正的"长期记忆"。
三、状态流式控制:看清智能体的"思考过程"
有了长期记忆后,我们还需要能够清晰地看到智能体的执行过程,了解每一步的思考和行动逻辑。这不仅有助于调试复杂的流程,还能为后续的人工介入提供基础。LangGraph提供的graph.stream()方法,能够实时追踪流程的执行状态,将每个节点的输出逐步打印出来,实现状态的可视化。
3.1 graph.stream()的核心作用
graph.stream()是LangGraph中用于流式处理状态的核心方法,与graph.invoke()的"一次性返回最终结果"不同,它会将流程执行过程中的每个节点输出作为一个事件,逐步返回。通过遍历这些事件,我们可以实时查看智能体的思考过程、工具调用细节、工具返回结果等关键信息,实现执行过程的完全透明化。
3.2 实战使用:实时追踪执行状态
使用graph.stream()的方法非常简单,只需要将之前的graph.invoke()调用替换为循环遍历graph.stream()的输出即可。具体代码如下:
python
messages = [HumanMessage(content="What is the weather in sf?")]
# 定义对话线程配置
thread = {"configurable": {"thread_id": "user-003"}}
# 流式遍历执行过程中的每个事件
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
运行这段代码,终端会逐步输出三个关键信息:一是LLM生成的工具调用指令,包含要调用的工具名称和查询参数;二是工具执行的详细过程和返回结果;三是LLM根据工具返回结果生成的最终回复。
例如,输出内容会包含类似这样的片段:
AIMessage(content='', additional_kwargs={'tool_calls': \[{'id': 'call_d142822160cc463ea23890', 'function': {'arguments': '{"query": "current weather in San Francisco"}', 'name': 'bocha_web_search'}, 'type': 'function', 'index': 0}\]...})
调用工具:{'name': 'bocha_web_search', 'args': {'query': 'current weather in San Francisco'}, 'id': 'call_d142822160cc463ea23890', 'type': 'tool_call'}
返回LLM继续思考!
ToolMessage(content='1. San Francisco, California, United States current weather conditions and forecast\\n47°F / 8°C Feels like 44.10°F / 7°C Wind: West at 6 mph / 9 km/h Humidity: 86% Pressure: 30 inches /\\n链接: https://www.worldtimeserver.com/weather_in_US-CA.aspx?forecastid=gn5391959...')
通过这些输出,我们能够清晰地看到智能体从接收问题、生成工具调用、执行工具到生成回复的完整流程。这种可视化能力对于调试复杂的Agent流程至关重要,能够帮助我们快速定位问题所在。
四、人工介入:让智能体"听指挥"
状态可视化之后,下一步就是实现人工介入,让我们能够在智能体执行关键步骤前进行干预,避免错误的工具调用或执行路径。LangGraph通过在流程编译时设置中断节点,实现"执行前暂停,确认后继续"的人工介入机制,也就是常说的Human in the Loop(人机协同)。
4.1 基础实现:关键节点中断
人工介入的核心是在流程编译时,通过interrupt_before参数指定需要中断的节点。例如,我们希望在智能体执行工具调用前暂停,等待人工确认后再继续,只需要修改Agent类中graph.compile()的调用,添加interrupt_before=["action"]参数:
python
self.graph = graph.compile(
checkpointer=checkpointer,
interrupt_before=["action"] # 在action节点执行前中断
)
修改完成后,我们使用graph.stream()调用智能体:
python
messages = [HumanMessage(content="Whats the weather in SF?")]
thread = {"configurable": {"thread_id": "user-004"}}
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
此时运行代码,终端会输出LLM生成的工具调用指令后停止,不再继续执行工具调用。这是因为流程在进入action节点前被中断了。
要查看当前的流程状态,我们可以使用graph.get_state()方法,获取指定thread_id的状态快照:
python
print(abot.graph.get_state(thread))
输出结果中会包含当前的历史消息、下一步要执行的节点(next=('action',))等信息。通过abot.graph.get_state(thread).next,我们可以直接获取下一步的执行节点,确认流程确实中断在了action节点前。
如果需要继续执行流程,我们只需要再次调用graph.stream(),并传入None作为输入(表示无需添加新消息),同时指定相同的thread_id:
python
# 继续执行中断的流程,传入None表示无新消息
for event in abot.graph.stream(None, thread):
for v in event.values():
print(v)
这样流程就会从断点继续执行,调用工具并完成后续的思考和回复生成。
4.2 进阶实现:终端交互确认
基础的中断和恢复功能需要手动修改代码执行,不够灵活。我们可以通过添加终端交互逻辑,让智能体在中断时询问用户是否继续执行,输入"y"则继续,否则终止流程。具体代码如下:
python
messages = [HumanMessage(content="Whats the weather in LA?")]
thread = {"configurable": {"thread_id": "user-005"}}
# 首次调用,触发流程中断
for event in abot.graph.stream({"messages": messages}, thread):
for v in event.values():
print(v)
# 循环检查是否有未完成的节点,实现交互确认
while abot.graph.get_state(thread).next:
print("\n当前流程状态:", abot.graph.get_state(thread), "\n")
_input = input("是否继续执行?(y/n)")
if _input != "y":
print("流程终止。")
break
# 继续执行流程
for event in abot.graph.stream(None, thread):
for v in event.values():
print(v)
运行这段代码,当流程中断时,终端会输出当前的状态信息,并询问是否继续。输入"y"后,流程继续执行;输入其他内容,流程则直接终止。这种方式让人工介入更加灵活,能够有效避免智能体执行错误的工具调用。
4.3 高级思路:工具化人工介入
上述两种人工介入方式都需要在流程编译时预设中断节点,不够智能。更优雅的方式是将"人工介入"设计为一个独立的工具,当智能体在执行过程中遇到无法确定的问题时,主动调用该工具向人类请求帮助。
具体实现思路是,创建一个"请求人工协助"的工具,当大模型判断当前任务需要人类补充信息或确认决策时,自动调用该工具。工具被调用后,暂停流程并向用户询问信息,用户输入后将信息作为工具返回结果,继续流程执行。
这种方式的核心优势在于,人工介入的触发由大模型自主决策,而不是预设的节点,能够适应更复杂的场景。实现这一功能的关键在于提升大模型的理解和决策能力,让其能够准确判断何时需要人类协助。这也是为什么各大厂商都在全力打造更强大的基座模型,因为基础模型的能力直接决定了智能体的协同效率。
五、总结与进阶方向
通过本次实战,我们完成了LangGraph智能体的两大核心能力升级,让智能体从"能干活"升级为"会干活、好掌控"。首先是通过Checkpoint系统和SqliteSaver实现了状态持久化,让智能体具备了长期记忆,能够跨会话衔接上下文,中断后可恢复;其次是通过graph.stream()实现了状态流式可视化,配合人工介入机制,让智能体的执行过程完全透明,可干预、可控制。
这两大能力的结合,让LangGraph智能体能够更好地应对复杂的实际应用场景。比如在长期科研辅助中,智能体可以记住多个研究阶段的历史数据,持续推进研究任务;在客户咨询场景中,智能体可以衔接多轮对话,无需用户重复说明需求;在复杂任务编排中,人工可以在关键节点介入决策,避免错误执行,提升任务成功率。