Prechádzať zdrojové kódy

新增 MCP 作业代码

邹超强 1 mesiac pred
rodič
commit
7a6c3160fd

+ 13 - 0
04_mcp_homework/.env.example

@@ -0,0 +1,13 @@
+# 复制本文件为 .env,然后填入你的 Qwen API Key。
+# 不要把真实 Key 发给别人,也不要提交到 GitHub。
+DASHSCOPE_API_KEY=k-e0317b1c22e1408895964cc8d63d3b16
+
+# Qwen OpenAI 兼容模式地址。
+# 中国大陆常用这个地址;如果你用的是国际站或其他区域,以控制台文档为准。
+QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+
+# 先用 qwen-plus,稳定、省心。
+QWEN_MODEL=qwen-plus
+
+# 如果你后来注册了高德地图 Key,可以再填这个。
+AMAP_API_KEY=你的_高德地图_key_可选

+ 3 - 0
04_mcp_homework/.gitignore

@@ -0,0 +1,3 @@
+.env
+__pycache__/
+.codex_tmp/

+ 189 - 0
04_mcp_homework/MCP旅行规划作业教程.md

@@ -0,0 +1,189 @@
+# MCP 旅行规划作业教程
+
+这份教程对应课件最后的作业:使用 LangGraph 构建一个多角色协作旅行规划 Agent,并通过 MCP 动态调用工具,生成包含行程、住宿和景点推荐的个性化旅行方案。
+
+## 你最终要交什么
+
+建议交三部分:
+
+1. 一段说明:解释你的 Agent 架构、MCP 工具来源、每个角色负责什么。
+2. 一份运行截图:展示 MCP 工具被发现、Agent 输出最终旅行方案。
+3. 代码文件:`travel_mcp_server.py`、`travel_agent_langgraph.py`、`.env.example`、`requirements.txt`。
+
+## 先把 MCP 和 Function Calling 分清
+
+一句话:
+
+- Function Calling 解决的是:模型怎么表达“我想调用哪个工具、参数是什么”。
+- MCP 解决的是:工具怎么被发现、怎么被调用、结果怎么通过标准协议返回。
+
+可以把它们放到一条链路里看:
+
+```text
+用户提问
+→ 大模型判断需要工具
+→ Function Calling 生成工具调用意图
+→ MCP Client 把这个意图发给 MCP Server
+→ MCP Server 真正执行工具
+→ 工具结果返回给模型
+→ 模型组织自然语言答案
+```
+
+所以 MCP 不是 Function Calling 的替代品。它们是上下游协作关系:Function Calling 更靠近模型,MCP 更靠近工具基础设施。
+
+## 本作业的推荐实现路线
+
+因为你还没有注册高德 API Key,第一版不要卡在高德那里。我们先做一个本地模拟旅行 MCP Server:
+
+- `travel_mcp_server.py`:提供交通、酒店、景点、预算 4 个 MCP Tool。
+- `travel_agent_langgraph.py`:用 LangGraph 编排 4 个角色:
+  - 交通规划师
+  - 住宿规划师
+  - 景点体验规划师
+  - 预算规划师
+  - 最后由总规划师汇总
+
+这样能完整体现:
+
+- MCP Server 暴露工具
+- MCP Client 动态发现工具
+- LangGraph 组织多角色协作
+- 大模型根据角色目标调用 MCP 工具
+
+## 第 1 步:安装依赖
+
+在当前目录执行:
+
+```powershell
+python -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+```
+
+如果你使用 uv:
+
+```powershell
+uv pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+```
+
+## 第 2 步:配置大模型 Key
+
+复制 `.env.example` 为 `.env`:
+
+```powershell
+Copy-Item .env.example .env
+```
+
+然后打开 `.env`,填写:
+
+```text
+DEEPSEEK_API_KEY=你的 DeepSeek API Key
+```
+
+注意,这里需要的是 DeepSeek 的大模型 Key,不是高德地图 Key。
+
+## 第 3 步:理解本地 MCP Server
+
+打开 `travel_mcp_server.py`。核心是:
+
+```python
+mcp = FastMCP(name="travel-tools")
+```
+
+然后用:
+
+```python
+@mcp.tool()
+def search_transport(...):
+    ...
+```
+
+把普通 Python 函数注册成 MCP Tool。
+
+这个 Server 最后用 stdio 启动:
+
+```python
+mcp.run(transport="stdio")
+```
+
+stdio 的好处是:不需要你单独打开一个服务窗口,Client 会自动启动它。
+
+## 第 4 步:运行作业主程序
+
+执行:
+
+```powershell
+python travel_agent_langgraph.py
+```
+
+正常情况下你会先看到:
+
+```text
+已从 MCP Server 动态发现工具: ['search_transport', 'search_hotels', ...]
+```
+
+这就说明 MCP Client 已经从 Server 拉到了工具列表。
+
+然后程序会输出最终旅行方案。
+
+## 第 5 步:换成你自己的题目
+
+在 `travel_agent_langgraph.py` 里找到:
+
+```python
+user_request = (
+    "我想从成都出发去重庆玩 3 天 2 晚..."
+)
+```
+
+你可以改成:
+
+```python
+user_request = (
+    "我想从上海出发去杭州玩 2 天 1 晚,2 个人,预算 1800 元以内。"
+    "喜欢自然风景、拍照和慢节奏城市漫步。"
+)
+```
+
+如果你换了代码里没有收录的城市,工具会给出通用策略;这不是错误,而是模拟数据不完整。
+
+## 第 6 步:作业说明怎么写
+
+你可以这样写:
+
+```text
+本项目实现了一个基于 LangGraph 的多角色旅行规划 Agent。
+系统包含交通规划师、住宿规划师、景点体验规划师、预算规划师四个角色,
+每个角色都通过 MCP Client 动态获取 travel-tools MCP Server 暴露的工具。
+
+MCP Server 提供 search_transport、search_hotels、recommend_attractions、
+estimate_trip_budget 等工具。Agent 不需要在代码里手写工具 schema,
+而是在运行时通过 MCP 动态发现工具,再由大模型通过 Function Calling 判断何时调用。
+
+最终,总规划师节点汇总各角色结果,生成包含行程、住宿、景点和预算的个性化旅行方案。
+```
+
+## 后续升级:接入高德或 ModelScope MCP
+
+等你注册高德 API Key 后,可以把本地模拟工具替换为高德官方 MCP。
+
+常见的高德 MCP Streamable HTTP 地址格式是:
+
+```text
+https://mcp.amap.com/mcp?key=你的高德key
+```
+
+如果高德或 ModelScope 服务页面给了不同 endpoint,以服务页面为准。对应配置参考 `travel_agent_amap_example.py`。
+
+老师给的 ModelScope MCP 广场地址是:
+
+```text
+https://modelscope.cn/mcp
+```
+
+你可以在里面搜索地图、搜索、酒店、火车票等 MCP 服务。如果服务页面提供 SSE 或 HTTP Endpoint,就能放进 `MultiServerMCPClient`。
+
+## 你交作业时可以强调的亮点
+
+- 我没有直接在 Agent 里写死工具,而是通过 MCP 动态发现工具。
+- 工具运行在独立 MCP Server 中,Agent 只负责调用。
+- LangGraph 把任务拆成多个角色节点,最后统一汇总。
+- Function Calling 负责模型的调用决策,MCP 负责工具发现和执行链路。

+ 6 - 0
04_mcp_homework/main.py

@@ -0,0 +1,6 @@
+def main():
+    print("Hello from 06-mcp!")
+
+
+if __name__ == "__main__":
+    main()

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 100 - 0
04_mcp_homework/mcp.ipynb


+ 117 - 0
04_mcp_homework/my_mcp_demo.py

@@ -0,0 +1,117 @@
+from fastmcp import FastMCP
+
+# 创建一个 MCP Server。
+# 你可以把它理解成:创建一个“旅行工具箱”。
+mcp = FastMCP(name="travel-tools")
+
+@mcp.tool()
+def search_hotels(city: str, nights: int = 2, budget_per_night: int = 400) -> str:
+    """
+    查询目的地住宿区域建议。
+
+    Args:
+        city: 目的地城市,比如 重庆、杭州、西安。
+        nights: 入住晚数,默认 2 晚。
+        budget_per_night: 每晚住宿预算,单位元,默认 400 元。
+    """
+    return f"{city} 住 {nights} 晚,建议预算 {budget_per_night} 元/晚。"
+
+
+@mcp.tool()
+def search_transport(origin: str, destination: str, travel_date: str = "未指定") -> str:
+    """
+    查询两个城市之间的交通建议。
+
+    Args:
+        origin: 出发城市,比如 成都、上海、北京。
+        destination: 目的地城市,比如 重庆、杭州、西安。
+        travel_date: 出行日期,比如 2026-08-15。如果用户没说,可以用默认值。
+    """
+    if origin == "成都" and destination == "重庆":
+        return (
+            f"{travel_date} 从成都到重庆,推荐优先选择高铁:"
+            "成都东站到重庆北站约 1.5-2 小时,二等座约 150 元左右。"
+            "如果多人同行也可以考虑自驾,但重庆市区停车和路况会更麻烦。"
+        )
+
+    if origin == "上海" and destination == "杭州":
+        return (
+            f"{travel_date} 从上海到杭州,推荐高铁:"
+            "上海虹桥到杭州东约 45-70 分钟,班次多,适合周末短途游。"
+        )
+
+    return (
+        f"暂时没有收录 {origin} 到 {destination} 的固定交通数据。"
+        "建议优先比较高铁、飞机和自驾三种方式,再根据时间、预算和同行人数选择。"
+    )
+
+
+@mcp.tool()
+def recommend_attractions(city: str, preferences: str = "美食、夜景、城市漫步") -> str:
+    """
+    根据目的地城市和用户偏好推荐景点。
+
+    Args:
+        city: 目的地城市,比如 重庆、杭州、西安。
+        preferences: 用户偏好,比如 美食、夜景、历史、亲子、拍照。
+    """
+    if city == "重庆":
+        return (
+            f"根据你的偏好「{preferences}」,重庆推荐这些安排:"
+            "洪崖洞适合看夜景,但人多,建议晚上错峰;"
+            "李子坝轻轨站适合体验山城立体交通;"
+            "山城步道适合城市漫步;"
+            "解放碑和八一路好吃街适合美食打卡;"
+            "鹅岭二厂适合拍照和咖啡休息。"
+        )
+
+    if city == "杭州":
+        return (
+            f"根据你的偏好「{preferences}」,杭州推荐西湖、灵隐寺、龙井村、京杭大运河。"
+            "整体节奏可以放慢,适合自然风景和城市漫步。"
+        )
+
+    return (
+        f"暂时没有收录 {city} 的详细景点数据。"
+        f"建议围绕「{preferences}」选择 3-5 个核心景点,每天不要超过 3 个。"
+    )
+
+
+@mcp.tool()
+def estimate_budget(
+    destination: str,
+    days: int = 3,
+    people: int = 1,
+    hotel_budget_per_night: int = 400,
+    round_trip_transport_budget: int = 300,
+) -> str:
+    """
+    估算旅行预算。
+
+    Args:
+        destination: 目的地城市,比如 重庆。
+        days: 旅行天数。
+        people: 出行人数。
+        hotel_budget_per_night: 每晚住宿预算,单位元。
+        round_trip_transport_budget: 每人往返大交通预算,单位元。
+    """
+    nights = max(days - 1, 1)
+    hotel_total = nights * hotel_budget_per_night
+    transport_total = people * round_trip_transport_budget
+    food_total = people * days * 120
+    local_transport_total = people * days * 50
+    ticket_total = people * 100
+    total = hotel_total + transport_total + food_total + local_transport_total + ticket_total
+
+    return (
+        f"{destination} {days} 天 {people} 人预算估算:"
+        f"往返交通约 {transport_total} 元;"
+        f"住宿约 {hotel_total} 元;"
+        f"餐饮约 {food_total} 元;"
+        f"市内交通约 {local_transport_total} 元;"
+        f"门票/体验预留约 {ticket_total} 元;"
+        f"合计约 {total} 元。"
+    )
+
+if __name__ == "__main__":
+    mcp.run(transport="stdio")

+ 92 - 0
04_mcp_homework/my_mcp_server.py

@@ -0,0 +1,92 @@
+# my_mcp_server.py —— 一个极简的自定义 MCP 服务
+from mcp.server.fastmcp import FastMCP
+from datetime import datetime
+from urllib.parse import unquote
+
+# ============================================================
+# 第一步:创建 FastMCP 实例,相当于初始化一个 Server 框架
+# ============================================================
+mcp = FastMCP(name="my-tools")
+
+
+# ============================================================
+# 第二步:用 @mcp.tool() 装饰器注册工具
+# 装饰器会自动提取函数的 type hints 和 docstring 作为工具描述
+# ============================================================
+
+@mcp.tool()
+def get_current_time() -> str:
+    """
+    获取当前日期和时间,返回格式友好的字符串。
+
+    适用场景:当用户问"现在几点"或"今天几号"时调用。
+    """
+    now = datetime.now()
+    return f"现在是 {now.year}年{now.month}月{now.day}日,{now.strftime('%H:%M:%S')}"
+
+
+@mcp.tool()
+def calculate_bmi(weight_kg: float, height_cm: float) -> str:
+    """
+    根据体重(公斤)和身高(厘米)计算 BMI 指数。
+
+    参数:
+    - weight_kg: 体重,单位公斤
+    - height_cm: 身高,单位厘米
+
+    返回:BMI 数值 + 健康评级
+    """
+    height_m = height_cm / 100
+    bmi = weight_kg / (height_m ** 2)
+
+    # 按中国标准分级
+    if bmi < 18.5:
+        level = "偏瘦,多吃点"
+    elif bmi < 24:
+        level = "正常,继续保持"
+    elif bmi < 28:
+        level = "偏胖,注意饮食"
+    else:
+        level = "该运动了兄弟"
+
+    return f"BMI = {bmi:.1f},{level}"
+
+
+# ============================================================
+# 第三步:用 @mcp.resource() 注册静态资源
+# 资源是"模板化"的数据端点,适合暴露文章、配置、文件内容等
+# URI 中用 {变量名} 定义路径参数
+# ============================================================
+
+# 模拟一个简易知识库
+KNOWLEDGE_BASE = {
+    "请假流程": "OA 系统提交 → 直属领导审批 → HR 备案。年假需提前 3 天申请。",
+    "报销标准": "市内交通实报实销(上限 100/天),住宿标准 350/晚(一线城市 450/晚)。",
+    "入职材料": "身份证原件 + 复印件、学历学位证、体检报告、离职证明、银行卡。",
+}
+
+@mcp.resource("knowledge://{topic}")
+def query_knowledge(topic: str) -> str:
+    """
+    查询公司内部知识库,返回指定主题的说明文档。
+
+    调用方式(Client 端):session.read_resource("knowledge://报销标准")
+    """
+    # MCP 协议中 URI 的非 ASCII 字符会被 Pydantic AnyUrl 自动
+    # URL 编码(如 "请假流程" → "%E8%AF%B7%E5%81%87%E6%B5%81%E7%A8%8B"),
+    # 但 FastMCP 的 ResourceTemplate.matches() 提取参数时不会自动解码,
+    # 所以这里需要手动 unquote。
+    topic = unquote(topic)
+    print(f"查询知识库:{topic}")
+    result = KNOWLEDGE_BASE.get(topic, f"没找到关于「{topic}」的记录,请联系 HR 更新知识库。")
+    return result
+
+
+# ============================================================
+# 第四步:启动 Server
+# ============================================================
+if __name__ == "__main__":
+    # SSE 模式:作为独立 HTTP 服务运行,适合远程调用和调试
+    mcp.settings.host = "0.0.0.0"
+    mcp.settings.port = 8000
+    mcp.run(transport="sse")

+ 8 - 0
04_mcp_homework/requirements.txt

@@ -0,0 +1,8 @@
+langchain
+langgraph
+langchain-openai
+langchain-deepseek
+langchain-mcp-adapters
+fastmcp
+mcp
+python-dotenv

+ 26 - 0
04_mcp_homework/step0_plain_function.py

@@ -0,0 +1,26 @@
+"""
+第 0 步:先写一个普通 Python 函数。
+
+这一文件不涉及 MCP、不涉及 Agent,只是为了让你先理解:
+工具的本质 = 一个有输入、有输出的函数。
+
+运行:
+python step0_plain_function.py
+"""
+
+
+def search_hotels(city: str, nights: int = 2, budget_per_night: int = 400) -> str:
+    """
+    查询目的地住宿区域建议。
+
+    Args:
+        city: 目的地城市,比如 重庆、杭州、西安。
+        nights: 入住晚数,默认 2 晚。
+        budget_per_night: 每晚住宿预算,单位元,默认 400 元。
+    """
+    return f"{city} 住 {nights} 晚,建议预算 {budget_per_night} 元/晚。"
+
+
+if __name__ == "__main__":
+    result = search_hotels(city="重庆", nights=2, budget_per_night=400)
+    print(result)

+ 39 - 0
04_mcp_homework/step1_list_tools.py

@@ -0,0 +1,39 @@
+"""
+第 1 步测试:连接本地 MCP Server,并列出它提供的工具。
+
+运行:
+python step1_list_tools.py
+"""
+
+from __future__ import annotations
+
+import asyncio
+import sys
+from pathlib import Path
+
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+
+async def main() -> None:
+    server_file = Path(__file__).with_name("my_mcp_demo.py").resolve()
+
+    # MCP Client 会自动启动 my_mcp_demo.py,然后询问它有哪些工具。
+    client = MultiServerMCPClient(
+        {
+            "travel-tools": {
+                "transport": "stdio",
+                "command": sys.executable,
+                "args": [str(server_file)],
+            }
+        }
+    )
+
+    tools = await client.get_tools()
+
+    print("MCP Client 发现了这些工具:")
+    for tool in tools:
+        print(f"- {tool.name}: {tool.description}")
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 51 - 0
04_mcp_homework/step2_call_tool.py

@@ -0,0 +1,51 @@
+"""
+第 2 步测试:连接本地 MCP Server,并直接调用 MCP 工具。
+
+这一步还不接大模型,只验证:
+1. MCP Client 能发现工具
+2. MCP Client 能调用工具
+3. MCP Server 能返回结果
+
+运行:
+python step2_call_tool.py
+"""
+
+from __future__ import annotations
+
+import asyncio
+import sys
+from pathlib import Path
+
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+
+async def main() -> None:
+    server_file = Path(__file__).with_name("my_mcp_demo.py").resolve()
+
+    client = MultiServerMCPClient(
+        {
+            "travel-tools": {
+                "transport": "stdio",
+                "command": sys.executable,
+                "args": [str(server_file)],
+            }
+        }
+    )
+
+    tools = await client.get_tools()
+    hotel_tool = next(tool for tool in tools if tool.name == "search_hotels")
+    # 调用这个mcp工具
+    result = await hotel_tool.ainvoke(
+        {
+            "city": "重庆",
+            "nights": 2,
+            "budget_per_night": 400,
+        }
+    )
+
+    print("MCP 工具调用结果:")
+    print(result)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 61 - 0
04_mcp_homework/step3_call_multiple_tools.py

@@ -0,0 +1,61 @@
+"""
+第 3 步测试:一个 MCP Server 暴露多个工具,并分别调用它们。
+
+运行:
+python step3_call_multiple_tools.py
+"""
+
+from __future__ import annotations
+
+import asyncio
+import sys
+from pathlib import Path
+
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+
+async def main() -> None:
+    server_file = Path(__file__).with_name("my_mcp_demo.py").resolve()
+
+    client = MultiServerMCPClient(
+        {
+            "travel-tools": {
+                "transport": "stdio",
+                "command": sys.executable,
+                "args": [str(server_file)],
+            }
+        }
+    )
+
+    tools = await client.get_tools()
+    tool_map = {tool.name: tool for tool in tools}
+
+    print("MCP Client 发现了这些工具:")
+    for tool_name in tool_map:
+        print(f"- {tool_name}")
+
+    hotel_result = await tool_map["search_hotels"].ainvoke(
+        {
+            "city": "重庆",
+            "nights": 2,
+            "budget_per_night": 400,
+        }
+    )
+
+    transport_result = await tool_map["search_transport"].ainvoke(
+        {
+            "origin": "成都",
+            "destination": "重庆",
+            "travel_date": "2026-08-15",
+        }
+    )
+
+    print("\n住宿工具返回:")
+    print(hotel_result)
+
+    print("\n交通工具返回:")
+    print(transport_result)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 43 - 0
04_mcp_homework/step4_qwen_hello.py

@@ -0,0 +1,43 @@
+"""
+第 4 步:先测试 Qwen 大模型能不能正常调用。
+
+这一步还不接 MCP。
+目标只有一个:确认你的 Qwen API Key、base_url、模型名都没问题。
+
+运行前:
+1. 复制 .env.example 为 .env
+2. 在 .env 里填 DASHSCOPE_API_KEY
+3. 运行:python step4_qwen_hello.py
+"""
+
+from __future__ import annotations
+
+import os
+
+from dotenv import load_dotenv
+from langchain_openai import ChatOpenAI
+
+
+def build_qwen_model() -> ChatOpenAI:
+    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,
+    )
+
+
+def main() -> None:
+    model = build_qwen_model()
+    response = model.invoke("用一句话介绍你自己。")
+    print(response.content)
+
+
+if __name__ == "__main__":
+    main()

+ 90 - 0
04_mcp_homework/step5_qwen_agent_use_mcp.py

@@ -0,0 +1,90 @@
+"""
+第 5 步:让 Qwen Agent 自己决定是否调用 MCP 工具。
+
+前面 step2/step3 是我们在代码里手动调用工具:
+    tool_map["search_hotels"].ainvoke(...)
+
+这一步变成:
+    用户用自然语言提问
+    Qwen 判断该调用 search_transport 还是 search_hotels
+    MCP Server 执行工具
+    Qwen 根据工具结果回答
+
+运行:
+python step5_qwen_agent_use_mcp.py
+"""
+
+from __future__ import annotations
+
+import asyncio
+import os
+import sys
+from pathlib import Path
+
+from dotenv import load_dotenv
+from langchain.agents import create_agent
+from langchain_mcp_adapters.client import MultiServerMCPClient
+from langchain_openai import ChatOpenAI
+
+
+def build_qwen_model() -> ChatOpenAI:
+    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 main() -> None:
+    server_file = Path(__file__).with_name("my_mcp_demo.py").resolve()
+
+    client = MultiServerMCPClient(
+        {
+            "travel-tools": {
+                "transport": "stdio",
+                "command": sys.executable,
+                "args": [str(server_file)],
+            }
+        }
+    )
+
+    tools = await client.get_tools()
+    print("Qwen Agent 可用的 MCP 工具:", [tool.name for tool in tools])
+
+    model = build_qwen_model()
+
+    agent = create_agent(
+        model=model,
+        tools=tools,
+        system_prompt=(
+            "你是一个旅行助手。"
+            "当用户询问住宿时,优先使用 search_hotels 工具;"
+            "当用户询问城市间交通时,优先使用 search_transport 工具;"
+            "拿到工具结果后,用中文自然语言回答。"
+        ),
+    )
+
+    result = await agent.ainvoke(
+        {
+            "messages": [
+                {
+                    "role": "user",
+                    "content": "我想 2026-08-15 从成都去重庆玩 3 天 2 晚,帮我看看交通和住宿怎么安排。",
+                }
+            ]
+        }
+    )
+
+    print("\nQwen Agent 最终回答:")
+    print(result["messages"][-1].content)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 189 - 0
04_mcp_homework/step6_langgraph_two_roles.py

@@ -0,0 +1,189 @@
+"""
+第 6 步:用 LangGraph 做一个最小多角色旅行规划 Agent。
+
+这一版先只做两个角色:
+1. 交通规划师:负责交通
+2. 住宿规划师:负责住宿
+3. 总结节点:把两个角色结果合成一段旅行建议
+
+运行:
+python step6_langgraph_two_roles.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 里的 state,可以理解成“多个角色之间传递的工作记录”。
+    """
+
+    user_request: str
+    transport_plan: str
+    hotel_plan: str
+    final_answer: str
+
+
+def build_qwen_model() -> ChatOpenAI:
+    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():
+    """
+    连接 my_mcp_demo.py 这个本地 MCP 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 工具。
+    """
+    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("LangGraph 可用 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 final_node(state: TravelState) -> TravelState:
+        """
+        第三个节点:总规划师。
+        它不直接调 MCP 工具,只负责汇总前面两个角色的结果。
+        """
+        prompt = f"""
+请把下面两个角色的结果整合成一份简洁旅行建议。
+
+用户需求:
+{state["user_request"]}
+
+交通规划师结果:
+{state["transport_plan"]}
+
+住宿规划师结果:
+{state["hotel_plan"]}
+
+请输出:
+1. 交通建议
+2. 住宿建议
+3. 一句话总结
+"""
+        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("final_planner", final_node)
+
+    graph.add_edge(START, "transport_planner")
+    graph.add_edge("transport_planner", "hotel_planner")
+    graph.add_edge("hotel_planner", "final_planner")
+    graph.add_edge("final_planner", END)
+
+    app = graph.compile()
+
+    result = await app.ainvoke(
+        {
+            "user_request": (
+                "我想 2026-08-15 从成都去重庆玩 3 天 2 晚,"
+                "预算控制在 1500 元以内,帮我安排交通和住宿。"
+            )
+        }
+    )
+
+    print("\n========== LangGraph 最终结果 ==========\n")
+    print(result["final_answer"])
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 246 - 0
04_mcp_homework/step7_langgraph_travel_homework.py

@@ -0,0 +1,246 @@
+"""
+第 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())

+ 38 - 0
04_mcp_homework/test_mcp.py

@@ -0,0 +1,38 @@
+"""
+同时获取多个MCP工具
+使用获取资源的函数
+使用自己的MCP stdio 与 sse
+"""
+
+import asyncio
+from langchain_mcp_adapters.client import MultiServerMCPClient
+
+#1. 配置MCP客户端
+mcp_config = {
+    "amap": {
+        "url": "https://mcp.api-inference.modelscope.net/6ae8935632f641/sse",
+        "transport": "sse",
+    },
+    "mcp_tools": {
+        "url": "http://localhost:8000/sse",   # 指向上面启动的 FastMCP 服务
+        "transport": "sse",
+    },
+}
+
+client = MultiServerMCPClient(mcp_config)
+
+async def get_mcp_tools():
+        # 获取MCP工具
+        tools = await client.get_tools()
+        print(f"当前可用工具:{[t.name for t in tools]}")
+        # 从指定 MCP Server 的 Resource 中读取数据
+        # 每个 Resource 通过 URI 定位,就像访问一个 RESTful 端点
+        async with client.session("mcp_tools") as session:
+            # 拉取三条公司制度作为增强上下文
+            leave_policy = await session.read_resource("knowledge://请假流程")
+            expense_policy = await session.read_resource("knowledge://报销标准")
+            print(f"[知识库] 请假流程: {leave_policy.contents[0].text}")
+            print(f"[知识库] 报销标准: {expense_policy.contents[0].text}")
+    
+if __name__ == "__main__":
+   asyncio.run(get_mcp_tools())

+ 353 - 0
04_mcp_homework/从零开始理解MCP作业.md

@@ -0,0 +1,353 @@
+# 从零开始理解 MCP 作业
+
+这份文档不是“直接给你一坨代码”,而是带你从最小概念开始,一层一层把 MCP 作业搭起来。
+
+你现在先记住一句话:
+
+> MCP 就是把你写好的 Python 函数,包装成一个标准服务,让 Agent 可以自动发现、自动调用。
+
+## 0. 我们最终要做什么
+
+老师的作业是:
+
+用 LangGraph 做一个多角色旅行规划 Agent,通过 MCP 调用工具,生成旅行方案。
+
+听起来很大,其实拆开只有 5 步:
+
+```text
+1. 先写普通 Python 函数
+2. 把普通函数注册成 MCP Tool
+3. 启动 MCP Server
+4. MCP Client 获取工具列表
+5. 把工具交给 Agent / LangGraph 使用
+```
+
+## 1. 先不谈 MCP:什么是“工具”?
+
+假设我们有一个普通 Python 函数:
+
+```python
+def calculate_bmi(weight_kg: float, height_cm: float) -> str:
+    height_m = height_cm / 100
+    bmi = weight_kg / (height_m ** 2)
+    return f"BMI = {bmi:.1f}"
+```
+
+这个函数本质上就是一个“工具”:
+
+- 输入:体重、身高
+- 处理:计算 BMI
+- 输出:计算结果
+
+如果你自己调用它,就是:
+
+```python
+print(calculate_bmi(100, 192))
+```
+
+但 Agent 不会直接知道这个函数存在。你得告诉 Agent:
+
+“我这里有个工具,名字叫 calculate_bmi,它需要 weight_kg 和 height_cm 两个参数。”
+
+以前我们会用 LangChain 的 `@tool` 手动注册。
+
+MCP 的做法是:把这些工具放进一个 MCP Server 里,让客户端自动发现。
+
+## 2. MCP Server 是什么?
+
+你可以把 MCP Server 理解成“工具盒子”。
+
+这个工具盒子里可以放很多工具:
+
+- 查交通
+- 查酒店
+- 推荐景点
+- 估算预算
+
+在代码里,创建 MCP Server 的方式是:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="travel-tools")
+```
+
+这句话的意思是:
+
+“我要创建一个 MCP 工具服务,名字叫 travel-tools。”
+
+## 3. 怎么把普通函数封装成 MCP 工具?
+
+关键只有一个装饰器:
+
+```python
+@mcp.tool()
+```
+
+例如:
+
+```python
+@mcp.tool()
+def search_hotels(city: str, nights: int = 2, budget_per_night: int = 400) -> str:
+    """
+    查询目的地住宿区域建议。
+
+    Args:
+        city: 目的地城市。
+        nights: 入住晚数。
+        budget_per_night: 每晚住宿预算,单位元。
+    """
+    return f"{city} 住 {nights} 晚,建议预算 {budget_per_night} 元/晚。"
+```
+
+这里发生了三件事:
+
+1. `@mcp.tool()` 把函数注册成 MCP 工具。
+2. 函数名 `search_hotels` 会变成工具名。
+3. 参数类型和注释会变成工具说明,方便模型理解什么时候该调用它。
+
+所以你写 MCP 工具时,要特别注意两点:
+
+- 参数类型要写清楚,比如 `city: str`
+- docstring 要写清楚,告诉模型这个工具干什么
+
+## 4. 为什么一定要写参数类型?
+
+因为 MCP Client 要把你的工具转换成模型能理解的工具 schema。
+
+比如这个函数:
+
+```python
+def search_hotels(city: str, nights: int = 2) -> str:
+```
+
+MCP 大概能理解成:
+
+```text
+工具名:search_hotels
+参数:
+- city:字符串
+- nights:整数,默认 2
+返回:字符串
+```
+
+如果你不写类型,模型就更容易传错参数。
+
+## 5. Tool 和 Resource 有什么区别?
+
+刚开始你只需要这样记:
+
+```text
+Tool:让模型自动调用,用来“做事”
+Resource:让程序手动读取,用来“提供资料”
+```
+
+例子:
+
+```text
+查酒店、查交通、算预算 → Tool
+旅行规划原则、公司制度、固定说明文档 → Resource
+```
+
+我们的作业里:
+
+- `search_transport` 是 Tool
+- `search_hotels` 是 Tool
+- `recommend_attractions` 是 Tool
+- `estimate_trip_budget` 是 Tool
+- `travel_policy://planning_rules` 是 Resource
+
+## 6. stdio 模式是什么意思?
+
+我们的 MCP Server 最后有一句:
+
+```python
+mcp.run(transport="stdio")
+```
+
+`stdio` 可以理解成“本地进程通信”。
+
+也就是说:
+
+```text
+Agent 程序启动
+→ 自动启动 travel_mcp_server.py
+→ 通过标准输入/输出传 JSON-RPC 消息
+→ 调用工具
+→ 拿回结果
+```
+
+stdio 的好处:
+
+- 不需要你手动开网页服务
+- 不需要端口
+- 适合本地开发和课程作业
+
+## 7. MCP Client 是什么?
+
+Server 是工具盒子。
+
+Client 是去连接工具盒子的人。
+
+代码里是:
+
+```python
+client = MultiServerMCPClient(
+    {
+        "travel-tools": {
+            "transport": "stdio",
+            "command": sys.executable,
+            "args": [str(server_path)],
+        }
+    }
+)
+```
+
+这段意思是:
+
+“我要连接一个叫 travel-tools 的 MCP Server。它是 stdio 模式。启动命令是当前 Python 解释器,启动文件是 travel_mcp_server.py。”
+
+然后:
+
+```python
+tools = await client.get_tools()
+```
+
+意思是:
+
+“去 MCP Server 问一下:你有哪些工具?”
+
+这一步就是 MCP 的动态发现。
+
+## 8. Function Calling 在哪里?
+
+MCP Client 拿到工具以后,会把工具交给大模型 Agent:
+
+```python
+agent = create_agent(
+    model=model,
+    tools=tools,
+    system_prompt="你是交通规划师..."
+)
+```
+
+当用户问:
+
+```text
+我想从成都去重庆玩 3 天,帮我规划交通。
+```
+
+模型会判断:
+
+```text
+我需要调用 search_transport
+参数 origin = 成都
+参数 destination = 重庆
+```
+
+这一步就是 Function Calling。
+
+然后真正执行工具的是 MCP。
+
+所以再记一次:
+
+```text
+Function Calling:模型决定调哪个工具
+MCP:负责找到工具、执行工具、返回结果
+```
+
+## 9. LangGraph 多角色是怎么来的?
+
+LangGraph 可以把一个大任务拆成多个节点。
+
+我们的作业拆成:
+
+```text
+transport_planner   交通规划师
+hotel_planner       住宿规划师
+attraction_planner  景点规划师
+budget_planner      预算规划师
+final_planner       总规划师
+```
+
+每个节点做一件事。
+
+例如交通节点:
+
+```python
+async def transport_node(state):
+    content = await ask_role(
+        model,
+        tools,
+        "你是交通规划师。必须优先调用 MCP 交通工具...",
+        state["user_request"],
+    )
+    return {"transport_plan": content}
+```
+
+这个节点的意思是:
+
+“让交通规划师 Agent 读取用户需求,调用 MCP 工具,输出交通方案。”
+
+最后总规划师节点把前面几个角色的结果汇总成完整方案。
+
+## 10. 你应该按什么顺序学习代码?
+
+不要一上来就看 `travel_agent_langgraph.py`,会晕。
+
+建议顺序:
+
+1. 先看 `travel_mcp_server.py` 里的 `search_hotels`
+2. 再看 `search_transport`
+3. 再看最下面的 `mcp.run(transport="stdio")`
+4. 然后看 `travel_agent_langgraph.py` 里的 `build_mcp_client`
+5. 再看 `tools = await client.get_tools()`
+6. 最后再看 LangGraph 那几个 node
+
+## 11. 你真正需要掌握的最小代码模板
+
+以后你自己封装 MCP 工具,最小模板就是:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="my-tools")
+
+
+@mcp.tool()
+def my_tool(name: str) -> str:
+    """
+    说明这个工具是干什么的。
+
+    Args:
+        name: 参数说明。
+    """
+    return f"你好,{name}"
+
+
+if __name__ == "__main__":
+    mcp.run(transport="stdio")
+```
+
+你可以把它想象成:
+
+```text
+普通函数 + @mcp.tool() + mcp.run()
+```
+
+这就是最小 MCP Server。
+
+## 12. 本作业你怎么讲给老师听?
+
+你可以这样说:
+
+```text
+我先用 FastMCP 封装了一个本地旅行工具服务,里面有交通查询、酒店查询、景点推荐和预算估算四个 Tool。
+然后在 LangGraph 主程序里用 MultiServerMCPClient 连接这个 MCP Server,并通过 get_tools 动态获取工具列表。
+接着我把这些工具交给不同角色的 Agent,包括交通规划师、住宿规划师、景点规划师和预算规划师。
+每个角色根据用户需求调用 MCP 工具,最后由总规划师节点汇总成完整旅行方案。
+
+在这个流程里,Function Calling 负责让模型判断要调用哪个工具和传什么参数;
+MCP 负责工具发现、工具调用和结果返回。
+```
+

+ 38 - 0
04_mcp_homework/生成结果.txt

@@ -0,0 +1,38 @@
+(.venv) PS D:\code\shuheAI\04_MCP> python .\step4_qwen_hello.py
+我是通义千问,由阿里巴巴通义实验室研发的大语言模型,致力于为你提供高效、可靠的智能协助。
+(.venv) PS D:\code\shuheAI\04_MCP> python .\step5_qwen_agent_use_mcp.py
+Qwen Agent 可用的 MCP 工具: ['search_hotels', 'search_transport']
+
+Qwen Agent 最终回答:
+为您规划2026年8月15日从成都出发前往重庆的3天2晚行程,交通与住宿建议如下:
+
+**🚄 交通安排**
+- **首选高铁**:从成都东站乘坐高铁至重庆北站,车程约1.5-2小时,二等座票价约150元/人。班次非常密集,准点率高,是短途出行最省心高效的选择。
+- **自驾备选**:全程约3-3.5小时。但重庆市区地形立体、单行道多且核心景区停车较难,建议抵达后尽量依赖地铁、公交或网约车,体验会更轻松。
+
+**🏨 住宿安排**
+- **预算参考**:按2晚行程,建议每晚预算400元左右。该价位在重庆可预订到位置便利、设施齐全的舒适型酒店或设计感民宿。
+- **区域推荐**:
+  - **解放碑/洪崖洞片区**:绝对核心,步行可达多数地标,餐饮和夜生活极其丰富,适合第一次来重庆的游客。
+  - **观音桥片区**:本地潮流商圈,地铁3/9号线交汇,交通四通八达,酒店性价比普遍较高。
+  - **南滨路/弹子石片区**:一线江景房集中,视野开阔,适合喜欢安静、偏爱夜景的游客。
+
+**💡 行程小贴士**
+8月中旬重庆正值盛夏,气温较高且偶有阵雨,建议备好防晒霜、晴雨伞和透气衣物。3天2晚可轻松覆盖“山城步道+两江夜景+轻轨穿楼+地道火锅”等经典体验。
+
+需要我帮您细化每日游玩路线,或根据具体偏好(如亲子/情侣/预算浮动)推荐几家具体酒店吗?
+(.venv) PS D:\code\shuheAI\04_MCP> 
+(.venv) PS D:\code\shuheAI\04_MCP> 
+(.venv) PS D:\code\shuheAI\04_MCP> 
+(.venv) PS D:\code\shuheAI\04_MCP> python .\step6_langgraph_two_roles.py
+LangGraph 可用 MCP 工具: ['search_hotels', 'search_transport']
+
+========== LangGraph 最终结果 ==========
+
+1. **交通建议**:优先选择高铁往返(成都东站⇌重庆北站)。单程约1.5-2小时,二等座约150元,往返总计约300元。班次密集、准点率高,可避开重庆复杂路况与停车难题,省时省力且性价比最优。
+
+2. **住宿建议**:推荐入住解放碑或洪崖洞商圈附近,预算控制在400元/晚,2晚共计800元。该区域为重庆核心地段,公共交通发达,步行即可覆盖主要景点与地道美食,游玩动线最便捷。
+
+3. **一句话总结**:高铁出行+核心商圈住宿总花费约1100元,严格控制在1500元预算内,兼顾高效通勤与沉浸式山城体验。
+(.venv) PS D:\code\shuheAI\04_MCP> 
+

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov