step7_langgraph_travel_homework.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. """
  2. 第 7 步:完整作业版。
  3. 目标:
  4. 用 LangGraph 构建一个多角色协作的旅行规划 Agent,
  5. 并通过 MCP 动态调用本地工具,生成包含交通、住宿、景点、预算的旅行方案。
  6. 运行:
  7. python step7_langgraph_travel_homework.py
  8. """
  9. from __future__ import annotations
  10. import asyncio
  11. import os
  12. import sys
  13. from pathlib import Path
  14. from typing import TypedDict
  15. from dotenv import load_dotenv
  16. from langchain.agents import create_agent
  17. from langchain_mcp_adapters.client import MultiServerMCPClient
  18. from langchain_openai import ChatOpenAI
  19. from langgraph.graph import END, START, StateGraph
  20. class TravelState(TypedDict, total=False):
  21. """
  22. 这是整个 LangGraph 的状态。
  23. 你可以把它理解成一张“协作记录表”:
  24. 每个角色完成自己的部分后,都把结果写进这张表。
  25. """
  26. user_request: str
  27. transport_plan: str
  28. hotel_plan: str
  29. attraction_plan: str
  30. budget_plan: str
  31. final_answer: str
  32. def build_qwen_model() -> ChatOpenAI:
  33. """
  34. 创建 Qwen 模型。
  35. 这里使用的是 DashScope 的 OpenAI 兼容接口,
  36. 所以可以用 LangChain 的 ChatOpenAI 来调用 Qwen。
  37. """
  38. load_dotenv()
  39. api_key = os.getenv("DASHSCOPE_API_KEY")
  40. if not api_key:
  41. raise RuntimeError("请先在 .env 里填写 DASHSCOPE_API_KEY。")
  42. return ChatOpenAI(
  43. api_key=api_key,
  44. base_url=os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
  45. model=os.getenv("QWEN_MODEL", "qwen-plus"),
  46. temperature=0.2,
  47. )
  48. async def build_mcp_tools():
  49. """
  50. 连接本地 MCP Server,并动态发现工具。
  51. 这一步体现 MCP 的核心能力:
  52. Agent 主程序没有手写 search_hotels、search_transport 等函数,
  53. 而是通过 MCP Client 从 Server 获取工具列表。
  54. """
  55. server_file = Path(__file__).with_name("my_mcp_demo.py").resolve()
  56. client = MultiServerMCPClient(
  57. {
  58. "travel-tools": {
  59. "transport": "stdio",
  60. "command": sys.executable,
  61. "args": [str(server_file)],
  62. }
  63. }
  64. )
  65. return await client.get_tools()
  66. async def call_role(model: ChatOpenAI, tools, system_prompt: str, user_request: str) -> str:
  67. """
  68. 创建一个角色 Agent。
  69. 每个角色都能看到同一组 MCP 工具,
  70. 但 system_prompt 会限制它只做自己的专业工作。
  71. """
  72. agent = create_agent(
  73. model=model,
  74. tools=tools,
  75. system_prompt=system_prompt,
  76. )
  77. result = await agent.ainvoke(
  78. {
  79. "messages": [
  80. {
  81. "role": "user",
  82. "content": user_request,
  83. }
  84. ]
  85. }
  86. )
  87. return result["messages"][-1].content
  88. async def main() -> None:
  89. model = build_qwen_model()
  90. tools = await build_mcp_tools()
  91. print("完整作业版可用 MCP 工具:", [tool.name for tool in tools])
  92. async def transport_node(state: TravelState) -> TravelState:
  93. answer = await call_role(
  94. model=model,
  95. tools=tools,
  96. system_prompt=(
  97. "你是交通规划师。"
  98. "你只负责城市间交通建议。"
  99. "必须优先调用 search_transport 工具。"
  100. "输出推荐方式、时间、费用和选择理由。"
  101. ),
  102. user_request=state["user_request"],
  103. )
  104. return {"transport_plan": answer}
  105. async def hotel_node(state: TravelState) -> TravelState:
  106. answer = await call_role(
  107. model=model,
  108. tools=tools,
  109. system_prompt=(
  110. "你是住宿规划师。"
  111. "你只负责住宿区域、晚数和住宿预算。"
  112. "必须优先调用 search_hotels 工具。"
  113. "输出住宿区域建议和预算。"
  114. ),
  115. user_request=state["user_request"],
  116. )
  117. return {"hotel_plan": answer}
  118. async def attraction_node(state: TravelState) -> TravelState:
  119. answer = await call_role(
  120. model=model,
  121. tools=tools,
  122. system_prompt=(
  123. "你是景点体验规划师。"
  124. "你只负责景点、美食、夜景和每日体验安排。"
  125. "必须优先调用 recommend_attractions 工具。"
  126. "注意不要把行程排得太满。"
  127. ),
  128. user_request=state["user_request"],
  129. )
  130. return {"attraction_plan": answer}
  131. async def budget_node(state: TravelState) -> TravelState:
  132. answer = await call_role(
  133. model=model,
  134. tools=tools,
  135. system_prompt=(
  136. "你是预算规划师。"
  137. "你只负责估算总预算,并拆分交通、住宿、餐饮、市内交通、门票等项目。"
  138. "必须优先调用 estimate_budget 工具。"
  139. ),
  140. user_request=state["user_request"],
  141. )
  142. return {"budget_plan": answer}
  143. async def final_node(state: TravelState) -> TravelState:
  144. prompt = f"""
  145. 你是总规划师。请把四个角色的结果整合成一份可交作业的旅行规划方案。
  146. 用户需求:
  147. {state["user_request"]}
  148. 交通规划师结果:
  149. {state["transport_plan"]}
  150. 住宿规划师结果:
  151. {state["hotel_plan"]}
  152. 景点体验规划师结果:
  153. {state["attraction_plan"]}
  154. 预算规划师结果:
  155. {state["budget_plan"]}
  156. 请按下面格式输出:
  157. 一、需求理解
  158. 二、交通方案
  159. 三、住宿方案
  160. 四、每日行程安排
  161. 五、预算估算
  162. 六、注意事项
  163. 七、技术说明:本项目如何体现 MCP 和 LangGraph 多角色协作
  164. 技术说明部分必须明确写出:
  165. - MCP Server 暴露了哪些工具
  166. - MCP Client 如何动态发现工具
  167. - Function Calling 和 MCP 分别负责什么
  168. - LangGraph 中每个角色节点负责什么
  169. """
  170. response = await model.ainvoke(prompt)
  171. return {"final_answer": response.content}
  172. graph = StateGraph(TravelState)
  173. graph.add_node("transport_planner", transport_node)
  174. graph.add_node("hotel_planner", hotel_node)
  175. graph.add_node("attraction_planner", attraction_node)
  176. graph.add_node("budget_planner", budget_node)
  177. graph.add_node("final_planner", final_node)
  178. graph.add_edge(START, "transport_planner")
  179. graph.add_edge("transport_planner", "hotel_planner")
  180. graph.add_edge("hotel_planner", "attraction_planner")
  181. graph.add_edge("attraction_planner", "budget_planner")
  182. graph.add_edge("budget_planner", "final_planner")
  183. graph.add_edge("final_planner", END)
  184. app = graph.compile()
  185. result = await app.ainvoke(
  186. {
  187. "user_request": (
  188. "我想 2026-08-15 从成都去重庆玩 3 天 2 晚,"
  189. "1 个人,预算控制在 1500 元以内。"
  190. "我喜欢美食、夜景和轻松一点的城市漫步,"
  191. "请生成交通、住宿、景点和预算方案。"
  192. )
  193. }
  194. )
  195. print("\n========== 完整作业最终结果 ==========\n")
  196. print(result["final_answer"])
  197. if __name__ == "__main__":
  198. asyncio.run(main())