LangGraph学习

  

什么是LangGraph?

在建立了一点LangChain和LLM的基础上可以进行延申,它是一个用来搭建AI Agent的框架。

通俗一点也就是说它很像一个流程图/状态图(自称),因为我可以去规定该图的每一个节点,每一个节点状态,以及每一条边,最后形成一套完整的状态图。

所以我认为,它主要解决整个流程的编排问题,而我们要去实现每一个小的模块,就需要去了解LangChain,因为LangChain相当于是一个个的小零件,去完成每个节点该做的事情。

常用头文件

import os
from dotenv import load_dotenv
from typing_extensions import TypedDict,NotRequired

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage,SystemMessage,AIMessage
from langchain_tavily import TavilySearch

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command

LangGraph需要了解的基本名词

1.Agent

大白话就是能自己思考每一步干什么的AI,比如你让它“你知道AI圈最新的消息嘛?”,他会自己决策,决定要不要去搜索,然后总结果。

2.Workflow

这里也就是指的工作流程,比如说我需要规定,输入问题 -> 分类 -> 搜索 -> 总结 -> 输出。

3.Graph

这里指的就是状态图,每个步骤都是一个节点,通过边进行链接,invoke的内容按状态图进行传播,而langChain就是用管道|链接,形成一条流水线传播。

3.Node Edge State

这三个都是LangGraph的核心,会先确定每一个节点的State是什么样子,可以自定义或者使用常用的MessageState,然后创建每一个节点Node,最后连上边Edge,检查的时候只需要从头到尾看看创建有没有漏掉,或者边有没有少连。

最核心的用法就是创建一个builder = StateGraph(状态类型),然后调用方builder.add_node或者builder.add_edge,特殊用法就是条件边builder.add_conditional_edge。

LangGraph最简版手搓

1.定义节点类state

class QAstate(TypedDict):
   question: str
   answer: str

2.创建builder

builder = StateGraph(State)

3.调用大模型API

load_dotenv()

llm = ChatOpenAI(
   model=os.getenv("DASHSCOPE_MODEL"),
   api_key=os.getenv("DASHSCOPE_API_KEY"),
   base_url=os.getenv("DASHSCOPE_BASE_URL"),
)
#这里使用阿里百炼 读者可自行切换

4.定义节点node

def question_answer(state: QAstate):
   question = state["question"]

   response=llm.invoke([
       ("system",
'''
你是一个专业的名词解释助手,
你会详细的解释我给出的每一个名词,
并且用我奶奶都能听懂的话,
简洁的解释给我听懂
'''),
       ("human", question)
   ])

   return {
       "answer": response.content
   }

5.在状态图上创建节点node

builder.add_node("question_answer",question_answer)

6.在状态图上创建边

builder.add_edge(START,"question_answer")
builder.add_edge("question_answer",END)

7.编译状态图

graph = builder.compile()

8.创建程序入口及接收返回

if __name__ == '__main__':
    result = graph.invoke(
        {
            "question": "帮我解释什么是LLM?"
        }
    )

    print(result["question"])
    print(result["answer"])

读到这里其实已经学会了最简单的LangGraph实现一个AI Agent的问答流程,可以完成一个自己手写的问答。

#完整代码
import os
from dotenv import load_dotenv
from typing_extensions import TypedDict,NotRequired

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_tavily import TavilySearch

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition

class QAstate(TypedDict):
    question: str
    answer: str

load_dotenv()

llm = ChatOpenAI(
    model=os.getenv("DASHSCOPE_MODEL"),
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

def question_answer(state: QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的名词解释助手,你会详细的解释我给出的每一个名词,并且用我奶奶都能听懂的话,简洁的解释给我听懂"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

builder = StateGraph(QAstate)

builder.add_node("question_answer",question_answer)
builder.add_edge(START,"question_answer")
builder.add_edge("question_answer",END)

graph = builder.compile()

if __name__ == '__main__':
    result = graph.invoke(
        {
            "question": "帮我解释什么是LLM?"
        }
    )

    print(result["question"])
    print(result["answer"])

返回结果:忘记保存了...

但是仅仅做到这样其实不太够,比如有时候我们的某宝客服机器人,我们给他提问有时候是售前提问,有时候是售后提问,有时候又是技术问题,这样三个类别,我如果system的prompt都是一套,“你是一个专业的xxx助手,你会xxx,并且xxx,最后xxxx”,明显不可能三个问题全部那么对口,那么我们如何解决呢?那么我们就需要用到条件分支!

LangGraph条件分支

用户问题
↓
判断类型
├── 售前问题
├── 售后问题
├── 技术问题
└── 普通问题
#具体分类

1.创建带问题目录的节点

class QAstate(TypedDict):
    question: str
    answer: NotRequired[str]
    category: NotRequired[str] #用于区分到底是哪一类

2.创建分类节点

#在这里根据不同的关键字进行分类 分类后确定到底是哪一类问题
classification(state:QAstate):
    question = state["question"]

    keywords_sell_advance = ["价格","样品","均价"]
    keywords_sell_after = ["退货","退款","不想要","退换","下错单","售后","保障"]
    keywords_tech_question = ["如何使用","操作","放置","解决","问题","技术","说明","清洗"]

    if any(word in question for word in keywords_sell_advance):
        return {
            "category": "advance"
        }
    if any(word in question for word in keywords_sell_after):
        return {
            "category": "after"
        }
    if any(word in question for word in keywords_tech_question):
        return {
            "category": "tech"
        }

    return {
        "category": "normal"
    }

2.创建路由

#前面根据关键字已经给category字段赋值,根据不同值返回路由
def route_node(state:QAstate):
    if state["category"] == "advance":
        return "advance"
    if state["category"] == "after":
        return "after"
    if state["category"] == "tech":
        return "tech"
    if state["category"] == "normal":
        return "normal"

3.创建不同的回答节点

#这里是每一类不同的处理方法 分为售前 售后 技术 一般
def advance_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的售前营销员,你会用精湛的话术,保证用户的体验,让用户对售前产品放心"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def after_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的售后营销员,你会用精湛的话术,保证用户的体验,让用户对售后的产品服务感到满足"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def tech_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的技术员工,你会用严谨的语言,保证解决用户的问题,让用户在使用产品的过程中没有障碍"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def normal_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的常见问题回答者,你会用巧妙的语言,保证解决用户的问题"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

4.条件边的添加方法

#这里只做了条件边添加方法 其他节点或者普通边添加方法同上 不在叙述
builder.add_conditional_edges(
    "classification",
    route_node,
    {
        "advance": "advance_answer",
        "after": "after_answer",
        "tech": "tech_answer",
        "normal": "normal_answer",
    },
)

通过以上节点的修改和条件边的添加,也就是条件分支,就可以解决以上多种情况提问的有效应对,附上完整代码。

import os
from dotenv import load_dotenv
from typing_extensions import TypedDict,NotRequired

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage,SystemMessage,AIMessage
from langchain_tavily import TavilySearch

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition

class QAstate(TypedDict):
    question: str
    answer: NotRequired[str]
    category: NotRequired[str] 

load_dotenv()

llm = ChatOpenAI(
    model=os.getenv("DASHSCOPE_MODEL"),
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

def classification(state:QAstate):
    question = state["question"]

    keywords_sell_advance = ["价格","样品","均价"]
    keywords_sell_after = ["退货","退款","不想要","退换","下错单","售后","保障"]
    keywords_tech_question = ["如何使用","操作","放置","解决","问题","技术","说明","清洗"]

    if any(word in question for word in keywords_sell_advance):
        return {
            "category": "advance"
        }
    if any(word in question for word in keywords_sell_after):
        return {
            "category": "after"
        }
    if any(word in question for word in keywords_tech_question):
        return {
            "category": "tech"
        }

    return {
        "category": "normal"
    }

def route_node(state:QAstate):
    if state["category"] == "advance":
        return "advance"
    if state["category"] == "after":
        return "after"
    if state["category"] == "tech":
        return "tech"
    if state["category"] == "normal":
        return "normal"

def advance_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的售前营销员,你会用精湛的话术,保证用户的体验,让用户对售前产品放心"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def after_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的售后营销员,你会用精湛的话术,保证用户的体验,让用户对售后的产品服务感到满足"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def tech_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的技术员工,你会用严谨的语言,保证解决用户的问题,让用户在使用产品的过程中没有障碍"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def normal_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的常见问题回答者,你会用巧妙的语言,保证解决用户的问题"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

builder = StateGraph(QAstate)

builder.add_node("classification",classification)
builder.add_node("advance_answer",advance_answer)
builder.add_node("after_answer",after_answer)
builder.add_node("tech_answer",tech_answer)
builder.add_node("normal_answer",normal_answer)

builder.add_edge(START, "classification")
builder.add_conditional_edges(
    "classification",
    route_node,
    {
        "advance": "advance_answer",
        "after": "after_answer",
        "tech": "tech_answer",
        "normal": "normal_answer",
    },
)

builder.add_edge("advance_answer",END)
builder.add_edge("after_answer",END)
builder.add_edge("tech_answer",END)
builder.add_edge("normal_answer",END)

graph = builder.compile()

if __name__ == '__main__':
    result1 = graph.invoke(
        {
            "question": "你们的这个电脑价格怎么样?"
        }
    )

    print(result1["answer"])

    result2 = graph.invoke(
        {
            "question": "你们的这个电脑售后有什么保障?"
        }
    )

    print(result2["answer"])

    result3 = graph.invoke(
        {
            "question": "你们的这个电脑怎么使用啊,建议进行自己手动清洗嘛?"
        }
    )

    print(result3["answer"])

    
返回输出:    
您好!感谢您对我们产品的关注!关于电脑的价格,我非常理解您的关心。我们始终坚持“高性价比+高品质”的理念,确保每一分钱都花在刀刃上。

我们的电脑根据不同配置(如处理器、内存、硬盘、显卡等)会有相应的梯度定价,满足从日常办公、学习到专业设计、游戏娱乐等多种需求。比如:

- **轻办公系列**:价格亲民,3000-5000元区间,轻薄便携,续航出色,非常适合学生和上班族;
- **全能性能款**:5000-8000元,搭载高性能处理器和独立显卡,应对多任务、剪辑、编程毫无压力;
- **旗舰电竞/创作本**:8000元以上,顶级配置,专为设计师、视频创作者和硬核玩家打造,流畅运行大型软件和3A游戏。

最重要的是——  
✅ 所有机型均通过严格品控,整机质保2年,7×24小时技术支持;  
✅ 现在下单还享限时优惠,部分型号直降500元,赠送原厂背包+鼠标;  
✅ 支持七天无理由退换,30天价格保护,让您买得安心、用得放心!



如果您方便的话,我可以根据您的使用场景(比如是办公、学习、游戏还是设计),为您精准推荐几款高性价比的型号,帮您省时又省钱!您平时主要用电脑做什么呢?😊
您好,感谢您对我们产品的关注!关于电脑的售后服务,我们始终坚持以客户满意为核心,为您提供全方位、安心可靠的保障服务。具体来说,我们的售后保障主要包括以下几个方面:

1. **全国联保,便捷无忧**  
   我们提供全国范围内的联保服务,无论您在哪个城市购买,都可以在全国授权服务网点享受统一标准的维修与技术支持,异地同样有效,出行出差也不用担心。

2. **7×24小时在线技术支持**  
   遇到使用问题?别担心!我们的专业客服团队全年无休,随时通过电话、在线客服或微信小程序为您提供远程协助,快速排查常见问题,多数情况下无需送修即可解决。

3. **7天包退,15天包换**  
   自签收之日起,若产品出现非人为的质量问题,支持7天内免费退货,15天内免费换新机,让您购机零风险。

4. **整机一年保修,主要部件两年保障**  
   整机享受一年免费保修服务,CPU、主板、内存、硬盘、电源等核心部件更享两年质保,使用寿命更安心。

5. **上门服务(部分城市支持)**  
   在北上广深等一线及重点城市,我们提供预约上门维修服务,足不出户就能解决问题,省时又省心。

6. **延保与意外保障可选**  
   如果您希望获得更长期的保护,我们还提供延长保修、意外损坏(如进水、跌落)等增值服务,全面守护您的设备安全。

    

更重要的是,我们的服务不只是“修好”,而是“让您满意”。每一次维修都会进行完整检测与清洁保养,并提供使用建议,真正实现“一次服务,长久安心”。

如果您已经购买或正在考虑,我可以根据您的所在城市和具体机型,为您进一步确认服务细节,确保您全程无忧。您看方便告诉我您关心的具体型号或所在地区吗?我来为您精准匹配服务方案 😊
您好,感谢您的提问。不过需要澄清的是:我们提供的产品是人工智能服务,并不涉及实体电脑设备的销售或维护。因此您提到的“这个电脑”可能有所误解。

如果您是在使用某台电脑时遇到操作问题,建议您:

1. **确认设备品牌与型号**:查看电脑机身上的标识(如联想、戴尔、华为等),然后访问该品牌的官方网站,查阅对应的产品使用手册或支持页面。

2. **关于清洗电脑**:
   - **不建议用户自行拆机清洗**,尤其是对主板、风扇等内部元件进行操作,可能导致硬件损坏或失去保修资格。
   - 如需清洁,推荐使用干燥的压缩空气清除键盘和散热口灰尘,用微湿的无绒布擦拭外壳。
   - 若设备积尘严重或出现过热问题,建议送至官方授权服务中心进行专业清理。

3. **获取技术支持**:请提供具体的电脑品牌和问题现象(例如无法开机、运行缓慢等),我可以为您提供更详细的指导。

如有其他使用疑问,欢迎继续咨询,我会尽力协助您安全、正确地操作设备。

读到这里你已经学会了根据不同场景,每一个场景做出一套合适的提示词,可以应用LangGraph的条件分支,但是还是存在问题,有些问题比如“某个产品和市场当天的平均水平相比如何呢?”你可能会想,我们直接分类甩给ai管他这么多,但是大多时候ai的数据库是更新不到如此迅速,每一天的消息都能精确把握,那么这里就需要去调用Tools工具,去查询网页。所以Tools到底是什么?我们如何使用呢?

Tool Calling

什么是Tool?

顾名思义,Tool就是工具,但是对于我们人来说工具是一个个实体,我们可以用来为我们要达成的目的服务,比如我要修东西,我空手很难做到,那我怎么办,就去使用螺丝刀,那么对ai,假如有人问我特定时间的情况,我可能数据库没有,那我怎么办,就去调用工具检索。

1.最简单的工具

@Tool
def calculate(a,b):
  return a + b

2.封装好的工具

获取API_KEY->Tavily官网:https://www.tavily.com/

#这里不需要加上@Tool装饰器 因为已经封装好 系统默认知道这是可调用工具
#注意需要在.env文件加入TAVIL_API_KEY后方可 工具调用
search_tool = TavilySearch(
    max_results=5,
    topic="general",
    search_depth="basic",
)

3.自定义调用API工具

获取API_KEY->openweather官网:https://openweathermap.org/

#这里展示调用openWeather的API去查询天气
#同样需要配置API_KEY
@tool
def get_current_weather(city: str) -> str:
    """查询指定城市的当前天气。适合用户询问今天、现在、当前的天气、温度、湿度、风速等信息。参数 city 应该是城市名,例如 Beijing、Shanghai、Guangzhou。"""
    api_key = os.getenv("OPENWEATHER_API_KEY")

    if not api_key:
        return "没有找到 OPENWEATHER_API_KEY,请先在 .env 文件中配置 OpenWeather API Key。"

    url = "https://api.openweathermap.org/data/2.5/weather"

    params = {
        "q": city,
        "appid": api_key,
        "units": "metric",
        "lang": "zh_cn",
    }

    try:
        response = requests.get(url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
    except requests.exceptions.HTTPError as exc:
        return f"天气查询失败:HTTP 错误,可能是城市名错误或 API Key 无效。错误信息:{exc}"
    except requests.exceptions.RequestException as exc:
        return f"天气查询失败:网络请求异常:{exc}"

    city_name = data.get("name", city)
    country = data.get("sys", {}).get("country", "")
    weather = data.get("weather", [{}])[0].get("description", "未知")
    temp = data.get("main", {}).get("temp")
    feels_like = data.get("main", {}).get("feels_like")
    humidity = data.get("main", {}).get("humidity")
    pressure = data.get("main", {}).get("pressure")
    wind_speed = data.get("wind", {}).get("speed")

    return (
        f"{city_name}, {country} 当前天气:{weather}\n"
        f"温度:{temp}°C\n"
        f"体感温度:{feels_like}°C\n"
        f"湿度:{humidity}%\n"
        f"气压:{pressure} hPa\n"
        f"风速:{wind_speed} m/s"
    )

4.模型绑定工具(以TavilSearch(search_tool)为例)

#创建Tools工具列表
tools = [search_tool]

#模型定义同上
llm = ChatOpenAI(
    model=os.getenv("DASHSCOPE_MODEL"),
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

#把可用的工具绑定给模型 以便模型后续自行决定是否使用
llm_with_tools = llm.bind_tools(tools)

5.创建call_model

这一步流程简单 只是细节复杂 可以选择暂且记住模式 后续进行理解 主要理解:

#state:MessageState
class MessagesState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

# MessagesState
# class MessagesState(TypedDict):
#    messages: Annotated[list[AnyMessage], add_messages]
# Annotated类型标注增强 也就是说后续的message2不会覆盖原来的message1,而是向后添加
#保证了 对话 和 调用返回结果

'''类型结构
{
  "message":[
      SystemMessage(content="...")
      HumanMessage(content="...")
      ...
  ]#注意这里是列表
}
'''
#call_model是让模型自己确定是否要调用工具
def call_model(state: MessagesState):
    response = llm_with_tools.invoke(state["messages"]) 
#这里返回的是类似前面的HumanMessage,这里为AIMessage

    return {
        "messages": [response]
    }


# call_model 会让模型看这句话,然后生成一个回复
# 如果模型觉得不需要工具,可能直接返回:
# AIMessage(content="LangGraph 是一个用于构建 Agent 工作流的框架...")
# 如果模型觉得需要搜索工具,可能返回:
# AIMessage(
#     content="",
#     tool_calls=[
#         {
#             "name": "web_search",
#             "args": {"query": "LangGraph latest changes today"},
#             "id": "call_123"
#         }
#     ]
# )

# 用户问题
#   ↓
# call_model 第一次调用模型
#   ↓
# AIMessage(content="", tool_calls=[...])
#   ↓
# ToolNode 执行检索工具
#   ↓
# ToolMessage(content="检索到的文本...")
#   ↓
# call_model 第二次调用模型
#   ↓
# AIMessage(content="根据检索结果生成的最终回答")

'''情况 1:模型不需要工具
如果用户问:
你好

模型可能直接返回:
AIMessage(
    content="你好!有什么我可以帮你的吗?",
    tool_calls=[]
)
这时:
return {"messages": [response]}

返回的是一条普通 AI 回复。
然后:
tools_condition
检查到没有 tool_calls,流程结束:
llm → END

情况 2:模型需要工具
如果用户问:
今天 LangGraph 最新版本是什么?

模型可能返回:
AIMessage(
    content="",
    tool_calls=[
        {
            "name": "web_search",
            "args": {
                "query": "LangGraph latest version today"
            },
            "id": "call_123"
        }
    ]
)
这时 content 可能是空的,但 tool_calls 里有内容。
然后:
tools_condition
会发现:
response.tool_calls
不是空的,于是流程走到:
llm → tools
'''

5.构建状态/流程图

builder = StateGraph(MessagesState)
#这里的MessageState不需要重新用类别定义 从头文件引入即可

builder.add_node("llm", call_model)
builder.add_node("tools", ToolNode(tools))
#这里的工具节点依然是封装好的直接用即可

builder.add_edge(START, "llm")

builder.add_conditional_edges(
    "llm",
    tools_condition,
    #稍微不同这里不需要放入route_map因为工具会自动映射
)

builder.add_edge("tools", "llm") 
#关键传回模型 决定是否继续搜索或者直接输出

graph = builder.compile()

6.程序入口

if __name__ == "__main__":
    result = graph.invoke({
        "messages": [
            HumanMessage(content="今天关于 LangGraph 最新版本有什么变化?")
            # role = human
            # content = 今天 LangGraph 有什么最新变化?
            # 固定用法
            
            # SystemMessage:系统规则
            # HumanMessage:用户消息
            # AIMessage:模型回复
            # ToolMessage:工具返回结果
        ]
    })

    print(result["messages"][-1].content)

读到此处,已经学会了核心的两个用法一个是条件分支,一个是工具调用

#完整代码
#基于条件分支进行修改 直接调用工具返回没有采取工具节点的方法
import os
from dotenv import load_dotenv
from typing_extensions import TypedDict,NotRequired

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage,SystemMessage,AIMessage
from langchain_tavily import TavilySearch

from langgraph.graph import StateGraph, START, END, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition

class QAstate(TypedDict):
    question: str
    answer: NotRequired[str]
    category: NotRequired[str] 

load_dotenv()

llm = ChatOpenAI(
    model=os.getenv("DASHSCOPE_MODEL"),
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url=os.getenv("DASHSCOPE_BASE_URL"),
)

search_tool = TavilySearch(
    max_results=5,
    topic="general",
    search_depth="basic",
)


def classification(state:QAstate):
    question = state["question"]

    keywords_sell_advance = ["价格","样品","均价"]
    keywords_sell_after = ["退货","退款","不想要","退换","下错单","售后","保障"]
    keywords_tech_question = ["如何使用","操作","放置","解决","问题","技术","说明","清洗"]

    if any(word in question for word in keywords_sell_advance):
        return {
            "category": "advance"
        }
    if any(word in question for word in keywords_sell_after):
        return {
            "category": "after"
        }
    if any(word in question for word in keywords_tech_question):
        return {
            "category": "tech"
        }

    return {
        "category": "normal"
    }

def route_node(state:QAstate):
    if state["category"] == "advance":
        return "advance"
    if state["category"] == "after":
        return "after"
    if state["category"] == "tech":
        return "tech"
    if state["category"] == "normal":
        return "normal"

def advance_answer(state:QAstate):
    question = state["question"]

    search_result = search_tool.invoke(
        {
            "query": "question"
        }
    )
    response=llm.invoke([
        ("system", '''你是一个专业的售前营销员,你会用精湛的话术,保证用户的体验,让用户对售前产品放心'''),
        ("human",
        f"""
        用户问题:{question}

        网页查询结果:{search_result}


        请根据以上 网页查询结果,回答用户问题
        """
        )

    ])

    return {
        "answer": response.content
    }

def after_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的售后营销员,你会用精湛的话术,保证用户的体验,让用户对售后的产品服务感到满足"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def tech_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的技术员工,你会用严谨的语言,保证解决用户的问题,让用户在使用产品的过程中没有障碍"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

def normal_answer(state:QAstate):
    question = state["question"]

    response=llm.invoke([
        ("system", "你是一个专业的常见问题回答者,你会用巧妙的语言,保证解决用户的问题"),
        ("human", question)
    ])

    return {
        "answer": response.content
    }

builder = StateGraph(QAstate)

builder.add_node("classification",classification)
builder.add_node("advance_answer",advance_answer)
builder.add_node("after_answer",after_answer)
builder.add_node("tech_answer",tech_answer)
builder.add_node("normal_answer",normal_answer)

builder.add_edge(START, "classification")
builder.add_conditional_edges(
    "classification",
    route_node,
    {
        "advance": "advance_answer",
        "after": "after_answer",
        "tech": "tech_answer",
        "normal": "normal_answer",
    },
)

builder.add_edge("advance_answer",END)
builder.add_edge("after_answer",END)
builder.add_edge("tech_answer",END)
builder.add_edge("normal_answer",END)

graph = builder.compile()

if __name__ == '__main__':
    result1 = graph.invoke(
        {
            "question": "你们的这个电脑价格怎么样?与市场最新的价格相比呢"
        }
    )

    print(result1["answer"])

    # result2 = graph.invoke(
    #     {
    #         "question": "你们的这个电脑售后有什么保障?"
    #     }
    # )

    # print(result2["answer"])

    # result3 = graph.invoke(
    #     {
    #         "question": "你们的这个电脑怎么使用啊,建议进行自己手动清洗嘛?"
    #     }
    # )

    # print(result3["answer"])

返回结果:
    
您好,感谢您的提问!

关于您关心的电脑价格问题,目前我们的产品定价是基于最新的市场行情、硬件配置以及售后服务综合制定的,具有非常强的竞争力。我们主打高性价比路线,在确保使用最新一代处理器、高速内存和固态硬盘等优质配置的同时,通过优化供应链和精简渠道成本,让利给消费者。

与市面上同配置机型相比,我们的电脑价格普遍低**10%-15%**,部分热销型号甚至有更大优惠。比如搭载Intel i7/AMD Ryzen 7处理器、16GB内存、512GB SSD的主流轻薄本,市场均价在6999元左右,而我们同配置机型仅售**5999元起**,还额外赠送两年上门保修服务。

更重要的是,我们承诺“**价格保护机制**”——若您购买后30天内发现官方降价,我们将双倍返还差价,让您买得安心、用得放心。

如果您方便提供具体想了解的型号或使用需求(如办公、设计、游戏等),我可以为您精准推荐最适合的配置和优惠方案,还能申请专属购机礼包哦! 😊

这里还存在一些小问题,比如我们正常与常用的ai对话,他会有上下文的记忆,它会有些情景需要人工手动确定,以及输出一个一个字蹦出来的,那么下面介绍一下,我学习到的几个方法,实现以上功能。

Memory记忆

说人话就是AI不是只记得当前的对话,还能联系到之前的对话

memory = InMemorySaver() #核心 
#比如你的 AI 对话流程跑到哪一步了、前面用户说过什么、当前状态是什么,它都可以先记下来。
#但注意:它是存在内存里,不是数据库里。程序一关,这些记忆通常就没了。
#所以它更适合学习、测试、调试,不太适合正式上线长期保存。

graph = builder.compile(checkpointer=memory) #核心
#告诉这个流程图:你每跑一步,都把当前状态记到 memory 里

config = {
    "configurable": {
        "thread_id": "richard_chat_1"
    }
}
#给一个配置 每一段对话都传入config 同一个thread就是用一个对话
result1 = graph.invoke(
    {
        "messages": [
            HumanMessage(content="我叫 Richard")
        ]
    },
    config=config, 
    #仅仅新增config即可,其他同上
)

print(result1["messages"][-1].content)


result2 = graph.invoke(
    {
        "messages": [
            HumanMessage(content="我叫什么?")
        ]
    },
    config=config,
)

print(result2["messages"][-1].content)
----------------------------------------------------
#另外的持久化存储方法
#SQLite 保存:程序关了也还能记住 SQLite本地的小数据库
from langgraph.checkpoint.sqlite import SqliteSaver

with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
    graph = builder.compile(checkpointer=memory)

    config = 
    {
        "configurable": {
            "thread_id": "user_001"
        }
    }

    graph.invoke(
        {
          "messages": [
              HumanMessage(content="我叫张三")
          ]
        },
        config=config
    )

#注意:如果想要更抗造 更工程化可以换更强的数据库 这里只做两个小型参考

附上完整代码

#完整代码
#上下文记忆 短记忆 程序结束记忆消失
from typing import Annotated

from langchain_core.messages import AnyMessage, HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import InMemorySaver
from langchain_openai import ChatOpenAI


llm = ChatOpenAI(
    model="...",
    api_key="...",
    base_url="...",
)


def call_model(state: MessagesState):
    system_message = SystemMessage(
        content="你是一个中文助手,请根据当前对话上下文回答。"
    )

    response = llm.invoke(
        [system_message] + state["messages"]
    )

    return {
        "messages": [response]
    }


builder = StateGraph(MessagesState)

builder.add_node("llm", call_model)
builder.add_edge(START, "llm")

memory = InMemorySaver() #核心
graph = builder.compile(checkpointer=memory) #核心


config = {
    "configurable": {
        "thread_id": "richard_chat_1"
    }
}


result1 = graph.invoke(
    {
        "messages": [
            HumanMessage(content="我叫 Richard")
        ]
    },
    config=config,
)

print(result1["messages"][-1].content)


result2 = graph.invoke(
    {
        "messages": [
            HumanMessage(content="我叫什么?")
        ]
    },
    config=config,
)

print(result2["messages"][-1].content)

Huamn-in-the-loop人为确认

#流程示意
AI 想发邮件
↓
先暂停
↓
让人确认
↓
确认后再发送
#由于较为好理解直接附上完整代码和注释
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import interrupt, Command


class State(TypedDict):
    email_draft: str
    approved: bool
    result: str


def write_email(state: State) -> dict:
    return {
        "email_draft": "你好,这是一封由 AI 生成的邮件草稿。"
    }


def human_review(state: State) -> dict:
    approved = interrupt({
        "question": "是否批准发送这封邮件?",
        "draft": state["email_draft"],
        "options": ["approve", "reject"]
    })
    #重点在于这里 一串会被直接打印出来 同时在这里中断 通过后续command可以resume 

    return {
        "approved": approved == "approve"
    }


def send_email(state: State) -> dict:
    return {
        "result": f"邮件已发送:{state['email_draft']}"
    }


def cancel_email(state: State) -> dict:
    return {
        "result": "邮件已取消发送。"
    }


def route_after_review(state: State) -> str:
    if state["approved"]:
        return "send_email"
    return "cancel_email"


builder = StateGraph(State)

builder.add_node("write_email", write_email)
builder.add_node("human_review", human_review)
builder.add_node("send_email", send_email)
builder.add_node("cancel_email", cancel_email)

builder.add_edge(START, "write_email")
builder.add_edge("write_email", "human_review")

builder.add_conditional_edges(
    "human_review",
    route_after_review,
    {
        "send_email": "send_email",
        "cancel_email": "cancel_email",
    },
)

builder.add_edge("send_email", END)
builder.add_edge("cancel_email", END)

memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)


config = {
    "configurable": {
        "thread_id": "email_review_1"
    }
}


result = graph.invoke({}, config=config)

print(result["__interrupt__"])

final_result = graph.invoke(Command(resume="approve"), config=config) #在这里选择继续即可

print(final_result["result"])

Streaming输出

说人话:不是等 AI 全写完才给你,而是一边想一边显示,就像 ChatGPT 打字一样,一个字一个字出来。

#重点在于输出其他基本同上 这里给出主程序输出展示
# 主程序
if __name__ == "__main__":
    user_input = input("你:")

    inputs = {
        "messages": [
            HumanMessage(content=user_input)
        ]
    }

    print("AI:", end="", flush=True)

    # stream_mode="messages"
    # 说人话:让 AI 像 ChatGPT 一样,一个字一个字吐出来
    for msg, metadata in graph.stream(
        inputs,
        stream_mode="messages",
    ):
        if isinstance(msg, AIMessageChunk):
            print(msg.content, end="", flush=True)

    print()

这里附上AI生成学习顺序表:

阶段 学什么 目标
1 Python 基础 看懂代码
2 LangChain 基础 知道模型、提示词、工具
3 LangGraph 核心概念 搞懂 State、Node、Edge
4 简单 StateGraph 写出第一个流程
5 条件分支 让流程会判断路线
6 工具调用 让 AI 会用外部工具
7 Memory / Persistence 让流程能记住和恢复
8 Human-in-the-loop 让人能审批关键操作
9 Streaming 做出实时输出体验
10 Multi-Agent 多个 AI 分工协作
11 部署调试 做成可上线项目

可以根据这个顺序进行学习,本篇博客为YellowDuck第一篇博客,纯新手小白,有问题请见谅,也可以选择告诉YellowDuck,同时本篇也是从零学起,理解不深入或者有错误反馈给我即可,谢谢阅读~

ps:补一份完整代码以及流程图,主要是为了区别LangChain,后续会发一篇关于LangChain可与之对比,除这份代码上述代码均为手搓:
ea1a763d535515d1b26c3e5013412cdb
该代码的整体逻辑如图 参考后更易懂

#注意这里补充代码并非手搓AI生成仅作参考
from __future__ import annotations

from typing import Literal, TypedDict, Optional
from uuid import uuid4

from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command, interrupt, RetryPolicy


# =========================
# 1. 定义 State:流程的小本子
# =========================

class Flight(TypedDict):
    id: str
    depart_time: str
    arrive_time: str
    arrive_hour: int
    price: int
    note: str


class Hotel(TypedDict):
    id: str
    name: str
    level: str
    total_price: int


class TravelState(TypedDict, total=False):
    user_need: str

    origin: str
    destination: str
    start_date: str
    end_date: str
    nights: int
    travelers: int

    budget: int
    avoid_red_eye: bool

    # 规划偏好
    force_non_red_eye: bool
    hotel_level: str
    replan_count: int

    # 查询结果
    flight_options: list[Flight]
    hotel_options: list[Hotel]

    selected_flight: Flight
    selected_hotel: Hotel

    total_price: int
    problems: list[str]

    # 人类确认
    approved: bool

    # 下单结果
    booking_id: str
    status: str


# =========================
# 2. 假接口:真实项目里换成真实 API
# =========================

hotel_api_called_times = 0
booking_api_called_times = 0


def fake_flight_api(origin: str, destination: str) -> list[Flight]:
    """
    假航班接口。
    真实项目里这里可以换成:
    - Amadeus
    - 携程内部接口
    - 航司 API
    - 你自己的聚合服务
    """
    return [
        {
            "id": "F_RED_EYE",
            "depart_time": "2026-06-01 20:30",
            "arrive_time": "2026-06-01 23:50",
            "arrive_hour": 23,
            "price": 1800,
            "note": "便宜,但到达太晚",
        },
        {
            "id": "F_DAYTIME",
            "depart_time": "2026-06-01 09:30",
            "arrive_time": "2026-06-01 14:10",
            "arrive_hour": 14,
            "price": 2600,
            "note": "白天到达,体验更好",
        },
    ]


def fake_hotel_api(destination: str, nights: int) -> list[Hotel]:
    """
    假酒店接口。
    故意第一次失败,模拟接口 503。
    LangGraph 会根据 RetryPolicy 自动重试。
    """
    global hotel_api_called_times
    hotel_api_called_times += 1

    if hotel_api_called_times == 1:
        raise ConnectionError("酒店接口临时失败:503 Service Unavailable")

    return [
        {
            "id": "H_STANDARD",
            "name": f"{destination} Central Hotel",
            "level": "standard",
            "total_price": 4200,
        },
        {
            "id": "H_BUDGET",
            "name": f"{destination} Budget Inn",
            "level": "budget",
            "total_price": 2800,
        },
    ]


def fake_booking_api(flight: Flight, hotel: Hotel, idempotency_key: str) -> str:
    """
    假下单接口。
    故意第一次失败,模拟支付/下单接口偶发失败。

    idempotency_key 是“防重复下单钥匙”。
    简单说:同一把钥匙重复请求,后端应该识别为同一单,不要重复扣钱。
    """
    global booking_api_called_times
    booking_api_called_times += 1

    if booking_api_called_times == 1:
        raise ConnectionError("下单接口临时失败:payment gateway timeout")

    return f"BOOKING-{idempotency_key[:8]}"


# =========================
# 3. 节点:每个函数就是流程的一步
# =========================

def understand_need(state: TravelState) -> TravelState:
    """
    理解用户需求。
    真实项目里这里可以调用 LLM,把自然语言拆成结构化字段。
    这里为了演示,直接填一些默认值。
    """
    user_need = state["user_need"]

    return {
        "origin": state.get("origin", "Beijing"),
        "destination": state.get("destination", "Tokyo"),
        "start_date": state.get("start_date", "2026-06-01"),
        "end_date": state.get("end_date", "2026-06-04"),
        "nights": state.get("nights", 3),
        "travelers": state.get("travelers", 1),
        "budget": state.get("budget", 6000),
        "avoid_red_eye": "不要红眼" in user_need or "不要太晚" in user_need,
        "force_non_red_eye": state.get("force_non_red_eye", False),
        "hotel_level": state.get("hotel_level", "standard"),
        "replan_count": state.get("replan_count", 0),
        "status": "understood_need",
    }


def search_flights(state: TravelState) -> TravelState:
    """
    查航班。
    默认先选最便宜的,所以可能选到红眼航班。
    如果前面发现时间冲突,就会设置 force_non_red_eye=True,再重新查。
    """
    flights = fake_flight_api(state["origin"], state["destination"])

    if state.get("force_non_red_eye"):
        candidates = [
            f for f in flights
            if f["arrive_hour"] < 22
        ]
    else:
        candidates = flights

    selected = min(candidates, key=lambda f: f["price"])

    return {
        "flight_options": flights,
        "selected_flight": selected,
        "status": "selected_flight",
    }


def search_hotels(state: TravelState) -> TravelState:
    """
    查酒店。
    如果预算超了,前面会把 hotel_level 改成 budget,然后这里就会选便宜酒店。
    """
    hotels = fake_hotel_api(state["destination"], state["nights"])
    wanted_level = state.get("hotel_level", "standard")

    candidates = [
        h for h in hotels
        if h["level"] == wanted_level
    ]

    if not candidates:
        candidates = hotels

    selected = min(candidates, key=lambda h: h["total_price"])

    return {
        "hotel_options": hotels,
        "selected_hotel": selected,
        "status": "selected_hotel",
    }


def evaluate_plan(state: TravelState) -> TravelState:
    """
    判断预算和时间有没有问题。
    """
    flight = state["selected_flight"]
    hotel = state["selected_hotel"]

    total_price = flight["price"] + hotel["total_price"]

    problems: list[str] = []

    if state.get("avoid_red_eye") and flight["arrive_hour"] >= 22:
        problems.append("time_conflict")

    if total_price > state["budget"]:
        problems.append("over_budget")

    return {
        "total_price": total_price,
        "problems": problems,
        "status": "evaluated_plan",
    }


def route_after_evaluation(
    state: TravelState,
) -> Literal["change_time", "replan_budget", "ask_approval", "cancel"]:
    """
    这里是 LangGraph 的关键:
    不是固定下一步,而是根据 State 决定走哪条路。
    """
    problems = state.get("problems", [])

    if "time_conflict" in problems:
        return "change_time"

    if "over_budget" in problems:
        if state.get("replan_count", 0) >= 2:
            return "cancel"
        return "replan_budget"

    return "ask_approval"


def change_time(state: TravelState) -> TravelState:
    """
    时间冲突:比如用户说不要红眼,但选到了半夜到的航班。
    解决办法:强制换成非红眼航班,然后回去重新查航班。
    """
    return {
        "force_non_red_eye": True,
        "status": "changed_time_preference",
    }


def replan_budget(state: TravelState) -> TravelState:
    """
    超预算:降低酒店档次,再回去重新查酒店。
    """
    return {
        "hotel_level": "budget",
        "replan_count": state.get("replan_count", 0) + 1,
        "status": "replanned_budget",
    }


def ask_approval(
    state: TravelState,
) -> Command[Literal["book_trip", "cancel"]]:
    """
    下单前暂停,问用户是否确认。
    interrupt 就是“先别继续,等人点头”。
    """
    answer = interrupt({
        "question": "这个旅行方案可以下单吗?",
        "flight": state["selected_flight"],
        "hotel": state["selected_hotel"],
        "total_price": state["total_price"],
        "budget": state["budget"],
    })

    if answer is True:
        return Command(
            update={
                "approved": True,
                "status": "approved_by_user",
            },
            goto="book_trip",
        )

    return Command(
        update={
            "approved": False,
            "status": "cancelled_by_user",
        },
        goto="cancel",
    )


def book_trip(state: TravelState) -> TravelState:
    """
    真正下单。
    注意:真实下单一定要用 idempotency_key,防止失败重试时重复扣钱。
    """
    idempotency_key = str(uuid4())

    booking_id = fake_booking_api(
        flight=state["selected_flight"],
        hotel=state["selected_hotel"],
        idempotency_key=idempotency_key,
    )

    return {
        "booking_id": booking_id,
        "status": "booked",
    }


def cancel(state: TravelState) -> TravelState:
    return {
        "status": state.get("status", "cancelled"),
    }


# =========================
# 4. 搭图:把节点用箭头连起来
# =========================

builder = StateGraph(TravelState)

builder.add_node("understand_need", understand_need)
builder.add_node("search_flights", search_flights)

# 查酒店接口可能失败,所以加重试
builder.add_node(
    "search_hotels",
    search_hotels,
    retry_policy=RetryPolicy(
        max_attempts=3,
        retry_on=ConnectionError,
    ),
)

builder.add_node("evaluate_plan", evaluate_plan)
builder.add_node("change_time", change_time)
builder.add_node("replan_budget", replan_budget)
builder.add_node("ask_approval", ask_approval)

# 下单接口也可能失败,所以也加重试
builder.add_node(
    "book_trip",
    book_trip,
    retry_policy=RetryPolicy(
        max_attempts=3,
        retry_on=ConnectionError,
    ),
)

builder.add_node("cancel", cancel)

builder.add_edge(START, "understand_need")
builder.add_edge("understand_need", "search_flights")
builder.add_edge("search_flights", "search_hotels")
builder.add_edge("search_hotels", "evaluate_plan")

builder.add_conditional_edges(
    "evaluate_plan",
    route_after_evaluation,
    {
        "change_time": "change_time",
        "replan_budget": "replan_budget",
        "ask_approval": "ask_approval",
        "cancel": "cancel",
    },
)

# 时间冲突后,回去重新查航班
builder.add_edge("change_time", "search_flights")

# 超预算后,回去重新查酒店
builder.add_edge("replan_budget", "search_hotels")

builder.add_edge("book_trip", END)
builder.add_edge("cancel", END)


# =========================
# 5. 编译图:加 checkpointer,才能暂停/恢复
# =========================

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)


# =========================
# 6. 运行 Demo
# =========================

if __name__ == "__main__":
    config = {
        "configurable": {
            # 同一个 thread_id 就是同一条旅行规划流程
            # 你可以理解成“存档编号”
            "thread_id": "trip-demo-001",
        }
    }

    first_result = graph.invoke(
        {
            "user_need": "我想从北京去东京玩 3 晚,预算 6000,不要红眼航班,不要太晚到。",
            "budget": 6000,
        },
        config=config,
    )

    print("\n=== 第一次运行结果 ===")
    print(first_result)

    if "__interrupt__" in first_result:
        interrupt_payload = first_result["__interrupt__"][0].value

        print("\n=== 需要用户确认 ===")
        print(interrupt_payload)

        # 模拟用户点击“确认下单”
        final_result = graph.invoke(
            Command(resume=True),
            config=config,
        )

        print("\n=== 用户确认后的最终结果 ===")
        print(final_result)

Tips:这里后续10 11大家可以自行探索哦,本篇只作为基础了解~

posted @ 2026-05-04 18:51  LiYellowDuck  阅读(69)  评论(2)    收藏  举报