LangGraph破局指南,打造具备长期记忆与人工可控的高阶AI智能体

在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智能体能够更好地应对复杂的实际应用场景。比如在长期科研辅助中,智能体可以记住多个研究阶段的历史数据,持续推进研究任务;在客户咨询场景中,智能体可以衔接多轮对话,无需用户重复说明需求;在复杂任务编排中,人工可以在关键节点介入决策,避免错误执行,提升任务成功率。

相关推荐
不爱学英文的码字机器2 小时前
基于昇腾 NPU 部署 Llama-3-8B 实战教程:从环境搭建到构建昇腾问答智能体
人工智能·pytorch·llama
week_泽2 小时前
1、OpenCV 特征检测入门笔记
人工智能·笔记·opencv
车队老哥记录生活2 小时前
强化学习 RL 基础 3:随机近似方法 | 梯度下降
人工智能·算法·机器学习·强化学习
线束线缆组件品替网2 小时前
工业防水接口标准解析:Amphenol CONEC 圆形线缆技术详解
人工智能·汽车·电脑·硬件工程·材料工程
熬夜敲代码的小N2 小时前
2026 职场生存白皮书:Gemini Pro 实战使用指南
人工智能·python·ai·职场和发展
独自归家的兔2 小时前
AI 原生应用开发框架深度解析:从单智能体到多智能体协同开发
人工智能
ArkAPI2 小时前
腾讯AI基础设施的系统论:从推理框架的算子融合到智能体的任务分解
人工智能·ai·google·aigc·腾讯·多模态处理·arkapi
Godspeed Zhao2 小时前
自动驾驶中的传感器技术83——Sensor Fusion(6)
人工智能·机器学习·自动驾驶
semantist@语校3 小时前
第五十八篇|从城市节律到制度密度:近畿日本语学院的数据建模与关西语校结构工程
大数据·服务器·数据库·人工智能·百度·ai·知识图谱