""" 第 7 步:完整作业版。 目标: 用 LangGraph 构建一个多角色协作的旅行规划 Agent, 并通过 MCP 动态调用本地工具,生成包含交通、住宿、景点、预算的旅行方案。 运行: python step7_langgraph_travel_homework.py """ from __future__ import annotations import asyncio import os import sys from pathlib import Path from typing import TypedDict from dotenv import load_dotenv from langchain.agents import create_agent from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langgraph.graph import END, START, StateGraph class TravelState(TypedDict, total=False): """ 这是整个 LangGraph 的状态。 你可以把它理解成一张“协作记录表”: 每个角色完成自己的部分后,都把结果写进这张表。 """ user_request: str transport_plan: str hotel_plan: str attraction_plan: str budget_plan: str final_answer: str def build_qwen_model() -> ChatOpenAI: """ 创建 Qwen 模型。 这里使用的是 DashScope 的 OpenAI 兼容接口, 所以可以用 LangChain 的 ChatOpenAI 来调用 Qwen。 """ load_dotenv() api_key = os.getenv("DASHSCOPE_API_KEY") if not api_key: raise RuntimeError("请先在 .env 里填写 DASHSCOPE_API_KEY。") return ChatOpenAI( api_key=api_key, base_url=os.getenv("QWEN_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"), model=os.getenv("QWEN_MODEL", "qwen-plus"), temperature=0.2, ) async def build_mcp_tools(): """ 连接本地 MCP Server,并动态发现工具。 这一步体现 MCP 的核心能力: Agent 主程序没有手写 search_hotels、search_transport 等函数, 而是通过 MCP Client 从 Server 获取工具列表。 """ server_file = Path(__file__).with_name("my_mcp_demo.py").resolve() client = MultiServerMCPClient( { "travel-tools": { "transport": "stdio", "command": sys.executable, "args": [str(server_file)], } } ) return await client.get_tools() async def call_role(model: ChatOpenAI, tools, system_prompt: str, user_request: str) -> str: """ 创建一个角色 Agent。 每个角色都能看到同一组 MCP 工具, 但 system_prompt 会限制它只做自己的专业工作。 """ agent = create_agent( model=model, tools=tools, system_prompt=system_prompt, ) result = await agent.ainvoke( { "messages": [ { "role": "user", "content": user_request, } ] } ) return result["messages"][-1].content async def main() -> None: model = build_qwen_model() tools = await build_mcp_tools() print("完整作业版可用 MCP 工具:", [tool.name for tool in tools]) async def transport_node(state: TravelState) -> TravelState: answer = await call_role( model=model, tools=tools, system_prompt=( "你是交通规划师。" "你只负责城市间交通建议。" "必须优先调用 search_transport 工具。" "输出推荐方式、时间、费用和选择理由。" ), user_request=state["user_request"], ) return {"transport_plan": answer} async def hotel_node(state: TravelState) -> TravelState: answer = await call_role( model=model, tools=tools, system_prompt=( "你是住宿规划师。" "你只负责住宿区域、晚数和住宿预算。" "必须优先调用 search_hotels 工具。" "输出住宿区域建议和预算。" ), user_request=state["user_request"], ) return {"hotel_plan": answer} async def attraction_node(state: TravelState) -> TravelState: answer = await call_role( model=model, tools=tools, system_prompt=( "你是景点体验规划师。" "你只负责景点、美食、夜景和每日体验安排。" "必须优先调用 recommend_attractions 工具。" "注意不要把行程排得太满。" ), user_request=state["user_request"], ) return {"attraction_plan": answer} async def budget_node(state: TravelState) -> TravelState: answer = await call_role( model=model, tools=tools, system_prompt=( "你是预算规划师。" "你只负责估算总预算,并拆分交通、住宿、餐饮、市内交通、门票等项目。" "必须优先调用 estimate_budget 工具。" ), user_request=state["user_request"], ) return {"budget_plan": answer} async def final_node(state: TravelState) -> TravelState: prompt = f""" 你是总规划师。请把四个角色的结果整合成一份可交作业的旅行规划方案。 用户需求: {state["user_request"]} 交通规划师结果: {state["transport_plan"]} 住宿规划师结果: {state["hotel_plan"]} 景点体验规划师结果: {state["attraction_plan"]} 预算规划师结果: {state["budget_plan"]} 请按下面格式输出: 一、需求理解 二、交通方案 三、住宿方案 四、每日行程安排 五、预算估算 六、注意事项 七、技术说明:本项目如何体现 MCP 和 LangGraph 多角色协作 技术说明部分必须明确写出: - MCP Server 暴露了哪些工具 - MCP Client 如何动态发现工具 - Function Calling 和 MCP 分别负责什么 - LangGraph 中每个角色节点负责什么 """ response = await model.ainvoke(prompt) return {"final_answer": response.content} graph = StateGraph(TravelState) graph.add_node("transport_planner", transport_node) graph.add_node("hotel_planner", hotel_node) graph.add_node("attraction_planner", attraction_node) graph.add_node("budget_planner", budget_node) graph.add_node("final_planner", final_node) graph.add_edge(START, "transport_planner") graph.add_edge("transport_planner", "hotel_planner") graph.add_edge("hotel_planner", "attraction_planner") graph.add_edge("attraction_planner", "budget_planner") graph.add_edge("budget_planner", "final_planner") graph.add_edge("final_planner", END) app = graph.compile() result = await app.ainvoke( { "user_request": ( "我想 2026-08-15 从成都去重庆玩 3 天 2 晚," "1 个人,预算控制在 1500 元以内。" "我喜欢美食、夜景和轻松一点的城市漫步," "请生成交通、住宿、景点和预算方案。" ) } ) print("\n========== 完整作业最终结果 ==========\n") print(result["final_answer"]) if __name__ == "__main__": asyncio.run(main())