langgraph_practice.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. from langgraph.graph import START, END, StateGraph, MessagesState
  2. from typing import TypedDict, Annotated
  3. from operator import add
  4. from langchain_openai import ChatOpenAI
  5. from dotenv import load_dotenv
  6. import os
  7. load_dotenv(override=True)
  8. deepseek_base_url = os.getenv("DEEPSEEK_BASE_URL")
  9. deepseek_base_key = os.getenv("DEEPSEEK_BASE_KEY")
  10. deepseek_base_name = os.getenv("DEEPSEEK_BASE_NAME")
  11. print("1. 三点===============================================================================================================================================================================================================")
  12. class CounterState1(TypedDict):
  13. count: int
  14. def incr_node_1(state: CounterState1)-> dict:
  15. count = state["count"] + 1
  16. return {"count": count}
  17. def incr_node_2(state: CounterState1)-> dict:
  18. count = state["count"] * 2
  19. return {"count": count}
  20. build = StateGraph(CounterState1)
  21. build.add_node("incr_node_1", incr_node_1)
  22. build.add_node("incr_node_2", incr_node_2)
  23. build.add_edge(START, "incr_node_1")
  24. build.add_edge("incr_node_1", "incr_node_2")
  25. build.add_edge("incr_node_2", END)
  26. graph = build.compile()
  27. result = graph.invoke({"count": 1})
  28. print(result)
  29. print("2. 条件边===============================================================================================================================================================================================================")
  30. class CounterState2(TypedDict):
  31. count: int
  32. def sub_node_1(state: CounterState2) -> dict:
  33. count = state["count"] - 1
  34. return {"count": count}
  35. def sub_node_2(state: CounterState2) -> dict:
  36. count = state["count"] - 2
  37. return {"count": count}
  38. def condition_edge(state: CounterState2) -> str:
  39. if state["count"] > 5:
  40. return "sub_node_1"
  41. return "sub_node_2"
  42. build = StateGraph(CounterState2)
  43. build.add_node("sub_node_1", sub_node_1).add_node("sub_node_2", sub_node_2)
  44. build.add_edge(START, "sub_node_1")
  45. build.add_conditional_edges(
  46. "sub_node_1",
  47. condition_edge,
  48. {"sub_node_1": "sub_node_1", "sub_node_2": "sub_node_2"}
  49. )
  50. build.add_edge("sub_node_2", END)
  51. graph = build.compile()
  52. result = graph.invoke({"count": 20})
  53. print(result)
  54. print("3. 覆盖===============================================================================================================================================================================================================")
  55. class OverwriteState(TypedDict):
  56. arr: list[str]
  57. def node_1(state: OverwriteState)->dict:
  58. return {"arr":["a", "b"]}
  59. def node_2(state: OverwriteState)->dict:
  60. return {"arr":["c", "d"]}
  61. build = StateGraph(OverwriteState)
  62. build.add_node("node_1", node_1).add_node("node_2", node_2)
  63. build.add_edge(START, "node_1")
  64. build.add_edge("node_1", "node_2")
  65. build.add_edge("node_2", END)
  66. graph = build.compile()
  67. result = graph.invoke({"arr":["e", "f"]})
  68. print(result)
  69. print("4. 追加===============================================================================================================================================================================================================")
  70. class AppendState(TypedDict):
  71. arr: Annotated[list[str], add]
  72. def node_1(state: AppendState)->dict:
  73. return {"arr":["aa", "bb"]}
  74. def node_2(state: AppendState)->dict:
  75. return {"arr":["cc", "dd"]}
  76. build = StateGraph(AppendState)
  77. build.add_node("node_1", node_1).add_node("node_2", node_2)
  78. build.add_edge(START, "node_1")
  79. build.add_edge("node_1", "node_2")
  80. build.add_edge("node_2", END)
  81. graph = build.compile()
  82. result = graph.invoke({"arr":["ee", "ff"]})
  83. print(result)
  84. print("5. API===============================================================================================================================================================================================================")
  85. # llm = ChatOpenAI(
  86. # base_url= deepseek_base_url,
  87. # api_key= deepseek_base_key,
  88. # model= deepseek_base_name
  89. # )
  90. # def chat_llm(state: MessagesState)->dict:
  91. # response = llm.invoke("你是谁")
  92. # return {"messages": [response]}
  93. # build = StateGraph(MessagesState)
  94. # build.add_node("chat", chat_llm)
  95. # build.add_edge(START, "chat")
  96. # build.add_edge("chat", END)
  97. # graph = build.compile()
  98. # result = graph.invoke({"messages": ["多学多问"]})
  99. # print(result)
  100. print("6. 自定义===============================================================================================================================================================================================================")
  101. class ReplaceableList(list):
  102. def __init__(self, *args, replace: bool = False, **kwargs):
  103. super().__init__(*args, **kwargs)
  104. self.replace = replace
  105. def smart_merge(current: list, incom: list)->list:
  106. if isinstance(incom, ReplaceableList) and getattr(incom, "replace", False):
  107. return list(incom)
  108. return current + incom
  109. class CostomState(TypedDict):
  110. arr: Annotated[list[str], smart_merge]
  111. def append(state: CostomState)->dict:
  112. return {"arr": ["b", "c"]}
  113. def overwrite(state: CostomState)->dict:
  114. new_list = ReplaceableList(["reset_data"])
  115. new_list.replace = True
  116. return {"arr": new_list}
  117. build = StateGraph(CostomState)
  118. build.add_node("append", append)
  119. build.add_node("overwrite", overwrite)
  120. build.add_edge(START, "overwrite")
  121. build.add_edge("overwrite", "append")
  122. build.add_edge("append", END)
  123. graph = build.compile()
  124. result = graph.invoke({"arr": ["a"]})
  125. print(result)
  126. print("7. 子图===============================================================================================================================================================================================================")
  127. class DrinkState(TypedDict):
  128. logs: Annotated[list[str], add]
  129. class MakeState(TypedDict):
  130. sub_logs: Annotated[list[str], add]
  131. def add_coffee(state: MakeState) -> dict:
  132. return {"sub_logs": ["添加咖啡"]}
  133. def add_sugar(state: MakeState) -> dict:
  134. return {"sub_logs": ["添加粮"]}
  135. def do_make_coffee(state: MakeState) -> dict:
  136. return {"sub_logs": ["咖啡制作中..."]}
  137. makeBuild = StateGraph(MakeState)
  138. makeBuild.add_node("add_coffee", add_coffee)
  139. makeBuild.add_node("add_sugar", add_sugar)
  140. makeBuild.add_node("do_make_coffee", do_make_coffee)
  141. makeBuild.add_edge(START, "add_coffee")
  142. makeBuild.add_edge("add_coffee", "add_sugar")
  143. makeBuild.add_edge("add_sugar", "do_make_coffee")
  144. makeBuild.add_edge("do_make_coffee", END)
  145. makeGraph = makeBuild.compile()
  146. def order_coffee(state: DrinkState) -> dict:
  147. return {"logs": ["点咖啡"]}
  148. def make_coffee(state: DrinkState) -> dict:
  149. result = makeGraph.invoke({"sub_logs": ["开始制作咖啡"]})
  150. return {"logs": result["sub_logs"]}
  151. def drink_coffee(state: DrinkState) -> dict:
  152. return {"logs": ["喝咖啡"]}
  153. build = StateGraph(DrinkState)
  154. build.add_node("order_coffee", order_coffee)
  155. build.add_node("make_coffee", make_coffee)
  156. build.add_node("drink_coffee", drink_coffee)
  157. build.add_edge(START, "order_coffee")
  158. build.add_edge("order_coffee", "make_coffee")
  159. build.add_edge("make_coffee", "drink_coffee")
  160. build.add_edge("drink_coffee", END)
  161. graph = build.compile()
  162. result = graph.invoke({"logs": ["喝咖啡"]})
  163. print(result)
  164. print("8. Send===============================================================================================================================================================================================================")
  165. """
  166. 图的拓扑结构:
  167. START
  168. [parse_query] ← 节点:解析用户输入,提取品牌列表
  169. route_to_research ← 条件边路由函数:返回 N 个 Send
  170. ├─ Send("research_brand", {brand: "苹果"})
  171. ├─ Send("research_brand", {brand: "华为"}) ← 三个并行
  172. └─ Send("research_brand", {brand: "小米"})
  173. ▼ (并行执行)
  174. [research_brand] × 3 ← 每个品牌独立研究
  175. ▼ (全部完成后汇总)
  176. [summarize] ← 节点:汇总所有报告,输出对比
  177. END
  178. """
  179. from typing import Annotated, TypedDict
  180. from operator import add
  181. from langgraph.graph import StateGraph, START, END
  182. from langgraph.types import Send
  183. # ══════════════════════════════════════════════════════════════
  184. # State 定义
  185. # ══════════════════════════════════════════════════════════════
  186. class CompareState(TypedDict):
  187. """手机对比任务的 State"""
  188. query: str # 用户的原始问题
  189. brands: list[str] # 从 query 中解析出的品牌列表
  190. reports: Annotated[list[str], add] # 每个品牌的研究报告(add reducer 自动合并)
  191. comparison: str # 最终的对比总结
  192. # ══════════════════════════════════════════════════════════════
  193. # 节点函数
  194. # ══════════════════════════════════════════════════════════════
  195. def parse_query(state: CompareState) -> dict:
  196. """
  197. 节点 ①:解析用户输入,提取出要对比的品牌列表。
  198. 实际场景中这里会用 LLM 做实体提取,这里直接硬编码模拟。
  199. """
  200. query = state["query"]
  201. print(f"[parse] 收到问题: {query}")
  202. # 模拟 LLM 解析:从自然语言中提取品牌名称
  203. brands = ["苹果", "华为", "小米"]
  204. print(f"[parse] 解析出 {len(brands)} 个品牌: {brands}")
  205. return {"brands": brands}
  206. def research_brand(state: CompareState) -> dict:
  207. """
  208. 节点 ②:研究单个品牌(被 Send 并行调用的目标节点)。
  209. 它不知道总共有几个品牌,也不关心其他品牌在做什么——
  210. 只专注于自己拿到的这一个。
  211. 实际场景中这里会调搜索引擎 API 或 LLM,这里用数据模拟。
  212. """
  213. brand = state["brands"][0] # Send 保证这里只有一个品牌
  214. # 模拟:查询该品牌的旗舰机信息
  215. phone_db = {
  216. "苹果": "iPhone 16 Pro Max | A18 Pro 芯片 | 6.9吋 OLED | 4K 120fps 视频 | iOS 18",
  217. "华为": "Mate 70 Pro+ | 麒麟 9100 | 6.8吋 OLED | 物理可变光圈 | 鸿蒙 NEXT",
  218. "小米": "小米 15 Ultra | 骁龙 8 Gen4 | 6.73吋 AMOLED | 徕卡光学 | 澎湃 OS 2.0",
  219. }
  220. info = phone_db.get(brand, f"{brand}旗舰机信息暂缺")
  221. report = f"【{brand}】{info}"
  222. print(f" [research] 并行查询 [{brand}] -> {info}")
  223. return {"reports": [report]} # add reducer 自动拼到总 reports 里
  224. def summarize(state: CompareState) -> dict:
  225. """
  226. 节点 ③:所有品牌都研究完了,汇总生成对比结论。
  227. 实际场景中这里会把所有 reports 喂给 LLM,让它生成对比分析。
  228. """
  229. print(f"\n[summarize] 开始汇总 {len(state['reports'])} 份报告...")
  230. # 模拟 LLM 生成的对比总结
  231. comparison = (
  232. "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
  233. " >> 旗舰手机对比总结\n"
  234. "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
  235. )
  236. for report in state["reports"]:
  237. comparison += f" {report}\n"
  238. comparison += (
  239. "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
  240. "[总结] 三款旗舰各有千秋 --\n"
  241. " 苹果 iPhone 16 Pro Max:视频拍摄王者,生态闭环体验最佳\n"
  242. " 华为 Mate 70 Pro+:影像系统物理可变光圈独树一帜\n"
  243. " 小米 15 Ultra:徕卡光学加持,性价比旗舰首选\n"
  244. "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
  245. )
  246. return {"comparison": comparison}
  247. # ══════════════════════════════════════════════════════════════
  248. # 路由函数 —— 核心!返回 Send 列表而不是 dict
  249. # ══════════════════════════════════════════════════════════════
  250. def route_to_research(state: CompareState):
  251. """
  252. ★ Send API 的核心:条件边路由函数 ★
  253. 关键规则:
  254. - 这个函数只能放在 add_conditional_edges 里,不能放进 add_node
  255. - 普通节点返回 dict(更新 state)
  256. - 路由函数返回 Send 列表(创建并行任务)或字符串(END / 节点名)
  257. 思想:几个品牌 → 发几个 Send → 启动几次 research_brand
  258. return [
  259. Send("research_brand", {"brands": ["苹果"]}), ─┐
  260. Send("research_brand", {"brands": ["华为"]}), ─┤ 全部并行执行
  261. Send("research_brand", {"brands": ["小米"]}), ─┘
  262. ]
  263. 每个 Send 的两个参数:
  264. 参数 1: 目标节点名 —— "派给谁做"
  265. 参数 2: 此分支专属的 state —— "这个任务要什么数据"
  266. """
  267. if not state["brands"]:
  268. return END
  269. return [
  270. Send("research_brand", {"brands": [brand]})
  271. for brand in state["brands"]
  272. ]
  273. # ══════════════════════════════════════════════════════════════
  274. # 构图
  275. # ══════════════════════════════════════════════════════════════
  276. builder = StateGraph(CompareState)
  277. builder.add_node("parse_query", parse_query)
  278. builder.add_node("research_brand", research_brand)
  279. builder.add_node("summarize", summarize)
  280. # 边
  281. builder.add_edge(START, "parse_query")
  282. builder.add_conditional_edges("parse_query", route_to_research) # 路由 → 动态 fan-out
  283. builder.add_edge("research_brand", "summarize") # 所有并行结果汇聚到 summarize
  284. builder.add_edge("summarize", END)
  285. graph = builder.compile()
  286. # ══════════════════════════════════════════════════════════════
  287. # 运行
  288. # ══════════════════════════════════════════════════════════════
  289. result = graph.invoke({
  290. "query": "帮我对比一下苹果、华为和小米的旗舰机型",
  291. "brands": [],
  292. "reports": [],
  293. "comparison": "",
  294. })
  295. print("======")
  296. print(result["brands"])
  297. print(result["comparison"])