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可与之对比,除这份代码上述代码均为手搓:

该代码的整体逻辑如图 参考后更易懂
#注意这里补充代码并非手搓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大家可以自行探索哦,本篇只作为基础了解~
本文来自博客园,作者:LiYellowDuck,转载请注明原文链接:https://www.cnblogs.com/LiYellowDuck/p/19969479,YellowDuck热爱手搓!!!

浙公网安备 33010602011771号