LangGraph实现可以查快递的聊天机器人

LangGraph实现可以查快递的聊天机器人

简介

本文主要基于LangGraph官网中的QuickStart示例代码,通过Streamlit来实现一个支持工具调用的聊天机器人。通过这个实现,主要可以看下LangGraph实现的ChatBot和用LangChain的区别在哪里?有哪些优势?后续我会基于streamlit可视化聊天界面继续探究LangGraph的其他特性,比如CRAG、数据库查询助手等。

准备

  1. 国内大语言模型,本文用的是通义千问,参考地址:https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key?spm=a2c4g.11186623.0.0.2f1e65c2CBUPJk
  2. 聚合数据的快递查询API,参考地址:https://www.juhe.cn/docs/api/id/43,首次开通1分钱可以调用100次

代码

创建streamlit

创建一个streamlit页面,设置标题,宽屏布局

import streamlit as st

st.set_page_config(page_title="Langgraph chatbot", layout="wide")
st.title("Langgraph chatbot")
实现加载历史聊天记录

使用streamlit的session_state实现首次问候语,并加载历史聊天记录。

# 初始化历史聊天记录
if st.sidebar.button("清空聊天记录") or "langgraph_messages" not in st.session_state:
    st.session_state["langgraph_messages"] = [
        {"role": "assistant", "content": "您好,我是langgraph实现的聊天机器人"}]

# 加载历史聊天记录
for msg in st.session_state.langgraph_messages:
    st.chat_message(msg["role"]).write(msg["content"])
实现Langgraph Chatbot

按照官方教程,创建graph第一步首先是创建state,这里创建一个自定义State

class State(TypedDict):
    # messages属性
    messages: Annotated[list, add_messages]

graph_builder = StateGraph(State)

定义大模型,这里使用阿里通义千问

# 定义大模型
llm = ChatTongyi()

创建工具

from langchain_core.tools import tool


@tool
def get_logistics_info(no: str) -> str:
    """根据快递单号查询快递信息"""

    # 基本参数配置
    api_url = 'http://v.juhe.cn/exp/index'  # 接口请求URL
    api_key = '你的APIKEY'  # 在个人中心->我的数据,接口名称上方查看

	# 快递单号前两位和api里面的公司代码映射,这里只是简单判断,实际需要调用网站提供的api来实时查询
    express_no_map = {
        "jt": "jtexpress",  # 极兔速递
        "yt": "yt",  # 圆通
        "sf": "sf",  # 顺丰
    }

	# 取快递单号前两位字母,不一定所有快递单号都是这个规则,需要考虑具体实现。
    express_com = express_no_map[no[:2].lower()]

    # 接口请求入参配置
    request_params = {
        'key': api_key,
        'com': express_com,
        'no': no,  # 快递单号
        # 'senderPhone': '',
        # 'receiverPhone': '',
        'dtype': 'json',
    }

    # 发起接口网络请求
    response = requests.get(api_url, params=request_params)

    # 解析响应结果
    if response.status_code == 200:
        response_json = response.json()
        return response_json
    else:
        # 网络异常等因素,解析结果异常。可依据业务逻辑自行处理。
        print('请求异常')

绑定工具

tools = [get_logistics_info]

llm_with_tools = llm.bind_tools(tools)

实现chatbot节点

def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}

实现工具节点,考虑到工具会有多个,langgraph官网是实现了BaseToolNode类,支持传入具体的工具列表实例化工具节点,这里要注意下content = json.dumps(tool_result, ensure_ascii=False)​这一行,官网没加ensure_ascii=False​参数,这里记得加下。如果工具返回包含中文字符会导致编码问题,大模型不认识unicode字符,会解读错误工具的返回信息。

class BasicToolNode:
    def __init__(self, tools: list) -> None:
        # 创建一个以工具名称为KEY,工具对象为Value的MAP
        self.tools_by_name = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")
        outputs = []

        for tool_call in message.tool_calls:
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call['args']
            )
            # json中包含中文一定要加上ensure_ascii=False参数,否则输出ascii编码,大模型不认识。
            content = json.dumps(tool_result, ensure_ascii=False)
            outputs.append(
                ToolMessage(
                    content=content,
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
            return {"messages": outputs}

# 创建工具节点
tool_node = BasicToolNode(tools=tools)

有了chabot节点和工具节点,然后把节点加入图,然后需要实现条件边,也就是要实现当用户提问不涉及工具调用的时候,大模型就是一个普通聊天,当用户的提示词涉及工具调用,大模型会自己调用工具,并通过工具的返回结果进行回复。整个图的构造:

image

  1. __start__节点接收用户的初始输入
  2. state传递给chabot,chatbot基于用户提示词判断是否需要调用tools,如果不需要,则直接回复,回复后就进入__end__节点,结束本轮聊天
  3. 如果调用了tools,那么tools节点执行完成之后,需要传递给chabot进行回复。
  4. chabot到tools在一轮对话过程中可能会有多次交互,直到不存在tool_call之后,进入__end__节点

以下是chabot到tools的路由逻辑代码实现

# 创建工具路由
def route_tools(state: State) -> Literal["tools", "__end__"]:
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return "__end__"

开始构建图

这里通过SqliteSaver实现了聊天记忆

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)

graph_builder.add_edge(START, "chatbot")

# chatbot->tools边
graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    {"tools": "tools", "__end__": "__end__"},

# tools->chatbot边
graph_builder.add_edge("tools", "chatbot")

# 创建记忆
memory = SqliteSaver.from_conn_string("agents/stores/memory.db")

# 编译
graph = graph_builder.compile(checkpointer=memory)

实现图的调用

# 创建聊天输入框
user_query = st.chat_input(placeholder="开始聊天吧!")
config = {"configurable": {"thread_id": "thread-1"}}
if user_query:
    st.session_state.langgraph_messages.append({"role": "user", "content": user_query})
    st.chat_message("user").write(user_query)

    # 打印所有step输出,包括ToolMessage和AIMessage
    with st.chat_message("assistant"):
        for event in graph.stream({"messages": ("user", user_query)}, config):
            for value in event.values():
                response = value["messages"][-1].content
                st.write(response)
                st.session_state.langgraph_messages.append({"role": "assistant", "content": response})

# 把每个step的state数据显示在expander里面
with view_messages:
    state_his = list(graph.get_state_history(config))
    for v_state in state_his:
        st.write(str(v_state))
实现效果

初始加载页面

image

随便聊一聊,试下非工具调用下的效果

image

问下他会不会查快递,试试工具调用

image

已经识别了,但是因为我们没给他快递单号,所以chatbot需要让我们进一步提供快递单号。到这里,如果我下一步直接输入快递单号的话,如果没实现记忆的话,大模型可能无法识别,但是我这里实现了记忆,所以直接提供一个快递单号给他就行,不要再说其他的。

image

可以看到,大模型调用了快递查询接口,并根据接口返回,整理并回复了快递最新物流信息。

以下是完整代码

from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
import streamlit as st
from langchain_core.tools import tool
import requests
import json
from langchain_core.messages import ToolMessage
from typing import Literal
from langgraph.checkpoint.sqlite import SqliteSaver

st.set_page_config(page_title="Langgraph chatbot", layout="wide")
st.title("Langgraph chatbot")

# 定义一个state
class State(TypedDict):
    # messages属性
    messages: Annotated[list, add_messages]

graph_builder = StateGraph(State)


@tool
def get_logistics_info(no: str) -> str:
    """根据快递单号查询快递信息"""

    # 基本参数配置
    api_url = 'http://v.juhe.cn/exp/index'  # 接口请求URL
    api_key = 'your api key'  # 在个人中心->我的数据,接口名称上方查看

    express_no_map = {
        "jt": "jtexpress",  # 极兔速递
        "yt": "yt",  # 圆通
        "sf": "sf",  # 顺丰
    }

    express_com = express_no_map[no[:2].lower()]

    # 接口请求入参配置
    request_params = {
        'key': api_key,
        'com': express_com,
        'no': no,  # 快递单号
        # 'senderPhone': '',
        # 'receiverPhone': '',
        'dtype': 'json',
    }

    # 发起接口网络请求
    response = requests.get(api_url, params=request_params)

    # 解析响应结果
    if response.status_code == 200:
        response_json = response.json()
        return response_json
    else:
        # 网络异常等因素,解析结果异常。可依据业务逻辑自行处理。
        print('请求异常')


# 定义大模型
llm = ChatTongyi()

tools = [get_logistics_info]

llm_with_tools = llm.bind_tools(tools)


def chatbot(state: State):
    return {"messages": [llm_with_tools.invoke(state["messages"])]}


class BasicToolNode:
    def __init__(self, tools: list) -> None:
        # 创建一个以工具名称为KEY,工具对象为Value的MAP
        self.tools_by_name = {tool.name: tool for tool in tools}

    def __call__(self, inputs: dict):
        if messages := inputs.get("messages", []):
            message = messages[-1]
        else:
            raise ValueError("No message found in input")
        outputs = []

        for tool_call in message.tool_calls:
            tool_result = self.tools_by_name[tool_call["name"]].invoke(
                tool_call['args']
            )
            # json中包含中文一定要加上ensure_ascii=False参数,否则输出ascii编码,大模型不认识。
            content = json.dumps(tool_result, ensure_ascii=False)
            outputs.append(
                ToolMessage(
                    content=content,
                    name=tool_call["name"],
                    tool_call_id=tool_call["id"],
                )
            )
            return {"messages": outputs}


tool_node = BasicToolNode(tools=tools)


def route_tools(state: State) -> Literal["tools", "__end__"]:
    if isinstance(state, list):
        ai_message = state[-1]
    elif messages := state.get("messages", []):
        ai_message = messages[-1]
    else:
        raise ValueError(f"No messages found in input state to tool_edge: {state}")
    if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0:
        return "tools"
    return "__end__"


graph_builder.add_node("chatbot", chatbot)
graph_builder.add_node("tools", tool_node)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_conditional_edges(
    "chatbot",
    route_tools,
    {"tools": "tools", "__end__": "__end__"},
)
graph_builder.add_edge("tools", "chatbot")

memory = SqliteSaver.from_conn_string("agents/stores/memory.db")
graph = graph_builder.compile(checkpointer=memory)

# 把当前构建的图显示在侧边栏
graph_image = st.sidebar.image(graph.get_graph().draw_mermaid_png())

# 为了方便查看state数据,用一个expander显示state数据
view_messages = st.expander("查看State")

# 初始化历史聊天记录
if st.sidebar.button("清空聊天记录") or "langgraph_messages" not in st.session_state:
    st.session_state["langgraph_messages"] = [
        {"role": "assistant", "content": "您好,我是langgraph实现的聊天机器人"}]

# 加载历史聊天记录
for msg in st.session_state.langgraph_messages:
    st.chat_message(msg["role"]).write(msg["content"])

# 创建聊天输入框
user_query = st.chat_input(placeholder="开始聊天吧!")
config = {"configurable": {"thread_id": "thread-1"}}
if user_query:
    st.session_state.langgraph_messages.append({"role": "user", "content": user_query})
    st.chat_message("user").write(user_query)

    # 打印所有step输出,包括ToolMessage和AIMessage
    with st.chat_message("assistant"):
        for event in graph.stream({"messages": ("user", user_query)}, config):
            for value in event.values():
                response = value["messages"][-1].content
                st.write(response)
                st.session_state.langgraph_messages.append({"role": "assistant", "content": response})

with view_messages:
    state_his = list(graph.get_state_history(config))
    for v_state in state_his:
        st.write(str(v_state))

总结

  1. 用langgraph实现的聊天机器人,在工具调用上会比langchain更灵活,整体表现也更自然
  2. langchain之前在复杂的工具调用上实现记忆是比较复杂的,现在在langgraph只需要创建一个checkpoint即可自动支持记忆
  3. 官网示例中还有HIL(Human In Loop)的实现,这也是langgraph的核心理念之一,即我在之前文章提到的人机协同,官网实现的不是很直观,后续有时间考虑下怎么在streamlit上实现。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值