Переглянути джерело

Merge branch 'main' of https://git.miaoatech.cn/zouchaoqiang/AI-agent

邹超强 1 тиждень тому
батько
коміт
09794c4e22
40 змінених файлів з 4726 додано та 0 видалено
  1. 661 0
      01_sql_homework/sql_agent_test.ipynb
  2. 1095 0
      01_sql_homework/test.ipynb
  3. BIN
      02_rag_homework/car_info.pdf
  4. BIN
      02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/data_level0.bin
  5. BIN
      02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/header.bin
  6. BIN
      02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/length.bin
  7. 0 0
      02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/link_lists.bin
  8. BIN
      02_rag_homework/chroma_db1/chroma.sqlite3
  9. BIN
      02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/data_level0.bin
  10. BIN
      02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/header.bin
  11. BIN
      02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/index_metadata.pickle
  12. BIN
      02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/length.bin
  13. BIN
      02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/link_lists.bin
  14. BIN
      02_rag_homework/my_knowledge_db/chroma.sqlite3
  15. 721 0
      02_rag_homework/rag.ipynb
  16. 146 0
      02_rag_homework/rag_demo.py
  17. 40 0
      03_langgraph_homework/Plan-and-Execute和ReAct的区别.md
  18. 17 0
      03_langgraph_homework/langgraph.ipynb
  19. 106 0
      03_langgraph_homework/react.ipynb
  20. 13 0
      04_mcp_homework/.env.example
  21. 3 0
      04_mcp_homework/.gitignore
  22. 189 0
      04_mcp_homework/MCP旅行规划作业教程.md
  23. 6 0
      04_mcp_homework/main.py
  24. 100 0
      04_mcp_homework/mcp.ipynb
  25. 117 0
      04_mcp_homework/my_mcp_demo.py
  26. 92 0
      04_mcp_homework/my_mcp_server.py
  27. 8 0
      04_mcp_homework/requirements.txt
  28. 26 0
      04_mcp_homework/step0_plain_function.py
  29. 39 0
      04_mcp_homework/step1_list_tools.py
  30. 51 0
      04_mcp_homework/step2_call_tool.py
  31. 61 0
      04_mcp_homework/step3_call_multiple_tools.py
  32. 43 0
      04_mcp_homework/step4_qwen_hello.py
  33. 90 0
      04_mcp_homework/step5_qwen_agent_use_mcp.py
  34. 189 0
      04_mcp_homework/step6_langgraph_two_roles.py
  35. 246 0
      04_mcp_homework/step7_langgraph_travel_homework.py
  36. 38 0
      04_mcp_homework/test_mcp.py
  37. 353 0
      04_mcp_homework/从零开始理解MCP作业.md
  38. 123 0
      04_mcp_homework/生成结果.txt
  39. 102 0
      05_skill_homework/SKILL.md
  40. 51 0
      05_skill_homework/assets/monthly-summary-template.md

Різницю між файлами не показано, бо вона завелика
+ 661 - 0
01_sql_homework/sql_agent_test.ipynb


Різницю між файлами не показано, бо вона завелика
+ 1095 - 0
01_sql_homework/test.ipynb


BIN
02_rag_homework/car_info.pdf


BIN
02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/data_level0.bin


BIN
02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/header.bin


BIN
02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/length.bin


+ 0 - 0
02_rag_homework/chroma_db1/23de5044-545b-4846-8357-bb443762e11f/link_lists.bin


BIN
02_rag_homework/chroma_db1/chroma.sqlite3


BIN
02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/data_level0.bin


BIN
02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/header.bin


BIN
02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/index_metadata.pickle


BIN
02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/length.bin


BIN
02_rag_homework/my_knowledge_db/c5345d55-c0a0-4330-b980-552e17804ab9/link_lists.bin


BIN
02_rag_homework/my_knowledge_db/chroma.sqlite3


+ 721 - 0
02_rag_homework/rag.ipynb

@@ -0,0 +1,721 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "a435d3d6",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "C:\\Users\\29448\\AppData\\Local\\Temp\\ipykernel_31076\\1036803851.py:1: DeprecationWarning: `langchain-community` is being sunset and is no longer actively maintained. See https://github.com/langchain-ai/langchain-community/issues/674 for details and migration guidance toward standalone integration packages.\n",
+      "  from langchain_community.document_loaders import PyMuPDFLoader\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "文档类型:<class 'list'>\n",
+      "PDF 共354页\n"
+     ]
+    }
+   ],
+   "source": [
+    "from langchain_community.document_loaders import PyMuPDFLoader\n",
+    "\n",
+    "#创建加载器实例,传入 PDF 文件路径\n",
+    "\n",
+    "pdf_loader = PyMuPDFLoader(\"./car_info.pdf\")\n",
+    "\n",
+    "#调用 load() 方法。 返回一个Document 列表 (每页一个 Document)\n",
+    "pdf_pages = pdf_loader.load()\n",
+    "\n",
+    "#看看加载结果\n",
+    "\n",
+    "print(f\"文档类型:{type(pdf_pages)}\")\n",
+    "print(f\"PDF 共{len(pdf_pages)}页\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "0e957443",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "元数据:{'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'creator': 'PTC Arbortext Publishing Engine', 'creationdate': '2023-06-16T15:35:59+08:00', 'source': './car_info.pdf', 'file_path': './car_info.pdf', 'total_pages': 354, 'format': 'PDF 1.7', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'moddate': '2023-06-16T17:41:45+08:00', 'trapped': '', 'modDate': \"D:20230616174145+08'00'\", 'creationDate': \"D:20230616153559+08'00'\", 'page': 0}\n",
+      "内容预览:欢迎\n",
+      "感谢您选择了具有优良安全性、舒适性、动力性和经济性的Lynk & Co领克汽车。\n",
+      "首次使用前请仔细、完整地阅读本手册内容,将有助于您更好地了解和使用车辆。\n",
+      "本手册中的所有资料均为出版时的最新资料,但本公司将对产品进行不断的改进和优化,您所购的车辆可能与本手册中的描述有所不同,请以实际\n",
+      "接收的车辆为准。\n",
+      "如您有任何问题,或需要预约服务,请拨打电话4006-010101 联系我们。您也可以开车前\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 查看第一页的内容和元数据\n",
+    "first_page = pdf_pages[0]\n",
+    "print(f\"元数据:{first_page.metadata}\")\n",
+    "print(f\"内容预览:{first_page.page_content[:200]}\") #截取200个字符"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "5d6fe44c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import re \n",
+    "\n",
+    "# ---- 问题1:PDF 解析产生的多余换行符 ----\n",
+    "# 现象:一句话被拆成多行,中间插了 \\n\n",
+    "# 例如:\"领克汽车\\n车顶行李架\\n最大载荷\"\n",
+    "# 解决:用正则匹配并删除非中文字符之间的换行符\n",
+    "raw_text = \"领克汽车\\n车顶行李架\\n最大载荷。\"\n",
+    "pattern = re.compile(r'[^一](\\n)[^一]', re.DOTALL)\n",
+    "clean_text = re.sub(pattern, lambda m: m.group(0).replace('\\n', ''), raw_text)\n",
+    "\n",
+    "# ---- 问题2:特殊符号干扰 ----\n",
+    "# 现象:PDF 中的项目符号 • 、多余空格等\n",
+    "clean_text = clean_text.replace('•', '')\n",
+    "clean_text = clean_text.replace('  ', ' ')  # 合并多余空格\n",
+    "# ---- 问题3:页眉页脚噪声 ----\n",
+    "# 现象:每页都有 \"第X页\"、\"公司名称\" 等重复内容\n",
+    "# 解决:根据元数据中的页码信息,过滤掉固定位置的噪声文本\n",
+    "\n",
+    "def clean_pdf_text(text: str) -> str:\n",
+    "    \"\"\"清洗 PDF 解析出的文本,去除常见噪声\"\"\"\n",
+    "    import re\n",
+    "\n",
+    "    # 删除非中文字符之间的换行符\n",
+    "    text = re.sub(r'[^一](\\n)[^一]', lambda m: m.group(0).replace('\\n', ''), text)\n",
+    "\n",
+    "    # 删除项目符号和多余空格\n",
+    "    text = text.replace('•', '').replace('  ', ' ')\n",
+    "\n",
+    "    # 删除连续的换行符(保留一个)\n",
+    "    text = re.sub(r'\\n{2,}', '\\n', text)\n",
+    "    return text.strip()\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "28f9e688",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "---块1---\n",
+      "Langchain 是一个让你的 LLM 变得更强大的开源框架\n",
+      "\n",
+      "---块2---\n",
+      "你想开发一个基于 LLM 的应用,需要什么组件他都有,直接使用就行\n",
+      "\n",
+      "---块3---\n",
+      "甚至针对常规的应用流程,它利用 Chain 这个概念已经内置标准化方案了\n",
+      "\n",
+      "---块4---\n",
+      "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 手动实现固定字符分块\n",
+    "from langchain_text_splitters import CharacterTextSplitter\n",
+    "\n",
+    "# 1. 定义分块器\n",
+    "text_splitter = CharacterTextSplitter(\n",
+    "    separator=\"。\",   #指定分隔符,默认为 “\\n\\n”\n",
+    "    chunk_size=50,     # 每个块的最大字符数\n",
+    "    chunk_overlap=10,    # 每个块之间的重叠字符数\n",
+    "    length_function=len,  # 计算字符数的函数\n",
+    "    is_separator_regex=False # 是否将分隔符视为正则表达式\n",
+    ")\n",
+    "\n",
+    "# 2. 执行分块,返回字符串列表\n",
+    "text = \"\"\"\n",
+    "Langchain 是一个让你的 LLM 变得更强大的开源框架。\n",
+    "你想开发一个基于 LLM 的应用,需要什么组件他都有,直接使用就行。\n",
+    "甚至针对常规的应用流程,它利用 Chain 这个概念已经内置标准化方案了。\n",
+    "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
+    "\"\"\"\n",
+    "chunks = text_splitter.split_text(text)\n",
+    "for i, chunk in enumerate(chunks):\n",
+    "    print(f\"---块{i+1}---\\n{chunk}\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "863080a2",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "--- 块1 ---\n",
+      "LangChain 是一个让你的 LLM 变得更强大的开源框架。\n",
+      "\n",
+      "--- 块2 ---\n",
+      "你想开发一个基于 LLM 的应用,需要什么组件它都有,直接使用就行。\n",
+      "\n",
+      "--- 块3 ---\n",
+      "甚至针对常规的应用流程,它利用 Chain 这个概念已经内置标准化方案了。\n",
+      "\n",
+      "--- 块4 ---\n",
+      "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "''' \n",
+    "* RecursiveCharacterTextSplitter 递归字符文本分割\n",
+    "RecursiveCharacterTextSplitter 将按不同的字符递归地分割(按照这个优先级[\"\\n\\n\", \"\\n\", \" \", \"\"]),\n",
+    "    这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置\n",
+    "RecursiveCharacterTextSplitter需要关注的是4个参数:\n",
+    "\n",
+    "* separators - 分隔符字符串数组\n",
+    "* chunk_size - 每个文档的字符数量限制\n",
+    "* chunk_overlap - 两份文档重叠区域的长度\n",
+    "* length_function - 长度计算函数\n",
+    "'''\n",
+    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
+    "\n",
+    "# 创建递归字符分割器\n",
+    "text_splitter = RecursiveCharacterTextSplitter(\n",
+    "    # 分隔符优先级:段落 → 换行 → 句号 → 空格 → 硬切\n",
+    "    separators=[\"\\n\\n\", \"\\n\", \"。\", \"!\", \"?\", \" \", \"\"],\n",
+    "\n",
+    "    # 每个块最大 50 字符\n",
+    "    chunk_size = 50,\n",
+    "    # 相邻块重叠 10 字符 (chunk_size 的 20%)\n",
+    "    chunk_overlap = 10,\n",
+    "    # 长度计算函数\n",
+    "    length_function=len\n",
+    ")\n",
+    "\n",
+    "# 对单段文本进行分割\n",
+    "text = \"\"\"\n",
+    "LangChain 是一个让你的 LLM 变得更强大的开源框架。\n",
+    "你想开发一个基于 LLM 的应用,需要什么组件它都有,直接使用就行。\n",
+    "甚至针对常规的应用流程,它利用 Chain 这个概念已经内置标准化方案了。\n",
+    "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
+    "\"\"\"\n",
+    "chunks = text_splitter.split_text(text)\n",
+    "for i, chunk in enumerate(chunks):\n",
+    "    print(f\"--- 块{i+1} ---\\n{chunk}\\n\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "174c088a",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "切分后的文件数量:4243\n",
+      "切分后的字符数(可以用来大致评估 token 数):148617\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 对整个文件进行切割\n",
+    "split_docs = text_splitter.split_documents(pdf_pages)\n",
+    "print(f\"切分后的文件数量:{len(split_docs)}\")\n",
+    "print(f\"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "119ef139",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "有效块数量:4243\n",
+      "总字符数(可大致评估 Token 数):148617\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 过滤掉 page_content 为空或仅含空白的文档\n",
+    "valid_docs = [\n",
+    "    doc for doc in split_docs \n",
+    "    if doc.page_content and doc.page_content.strip()\n",
+    "]\n",
+    "\n",
+    "print(f\"有效块数量:{len(valid_docs)}\")\n",
+    "print(f\"总字符数(可大致评估 Token 数):{sum(len(d.page_content) for d in valid_docs)}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "579fe5e6",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "向量维度:1024\n",
+      "前5个值:[-0.013686168007552624, 0.015642698854207993, -0.052721332758665085, 0.033709585666656494, -0.08253694325685501]\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 安装 dashscope 依赖:uv pip install dashscope -i https://mirrors.aliyun.com/pypi/simple\n",
+    "\n",
+    "from langchain_community.embeddings import DashScopeEmbeddings\n",
+    "from dotenv import load_dotenv\n",
+    "import os\n",
+    "\n",
+    "load_dotenv()\n",
+    "api_key = os.getenv('QWEN_API_KEY')\n",
+    "\n",
+    "# 初始化 Embedding 模型\n",
+    "# 需要先在 https://dashscope.console.aliyun.com/ 获取 API Key\n",
+    "embedding_model = DashScopeEmbeddings(\n",
+    "    model=\"text-embedding-v3\",        # 模型名称\n",
+    "    dashscope_api_key=api_key   # 替换为你的真实 Key\n",
+    ")\n",
+    "\n",
+    "# 单条文本向量化\n",
+    "text = \"RAG系统搭建实战\"\n",
+    "embedding = embedding_model.embed_query(text)\n",
+    "print(f\"向量维度:{len(embedding)}\")\n",
+    "print(f\"前5个值:{embedding[:5]}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "3951a993",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "生成了 4 个向量\n",
+      "每个向量维度:1024\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 批量文本向量化\n",
+    "texts = [\n",
+    "    \"hello world\",\n",
+    "    \"什么是大语言模型\",\n",
+    "    \"RAG的概念和原理\",\n",
+    "    \"牛顿第一定律是什么\",\n",
+    "]\n",
+    "\n",
+    "embeddings = embedding_model.embed_documents(texts)\n",
+    "print(f\"生成了 {len(embeddings)} 个向量\")\n",
+    "print(f\"每个向量维度:{len(embeddings[0])}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "5bc26184",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "匹配内容:王老师教学认真负责,是公认的好老师\n"
+     ]
+    }
+   ],
+   "source": [
+    "from langchain_community.vectorstores import Chroma\n",
+    "\n",
+    "# 准备一些测试数据\n",
+    "datas = [\n",
+    "    \"小明特别喜欢吃脆甜多汁的苹果\",\n",
+    "    \"小红对榴莲那独特的味道情有独钟\",\n",
+    "    \"小明和小丽是一对甜蜜的情侣\",\n",
+    "    \"王老师教学认真负责,是公认的好老师\",\n",
+    "    \"小李每天都要吃一根香蕉\",\n",
+    "    \"小王的男朋友长得阳光帅气,是大家公认的大帅哥\"\n",
+    "]\n",
+    "\n",
+    "# 创建向量数据库并持久化(会同时把文本和对应的向量存入数据库)\n",
+    "db = Chroma.from_texts(\n",
+    "    texts=datas,\n",
+    "    embedding=embedding_model,\n",
+    "    collection_metadata={\"hnsw:space\": \"cosine\"},  # 关键配置:指定距离算法为余弦相似度\n",
+    "    persist_directory=\"./chroma_db1\"  # 数据库存储路径\n",
+    ")\n",
+    "\n",
+    "# 查询相似文本\n",
+    "# 相似度检索\n",
+    "query = \"谁是老师\"\n",
+    "results = db.similarity_search(query, k=1)  # 返回最相似的 1 条\n",
+    "\n",
+    "for doc in results:\n",
+    "    print(f\"匹配内容:{doc.page_content}\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "7e905aae",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "内容:王老师教学认真负责,是公认的好老师 | 相似度分数:0.3909\n",
+      "内容:王老师教学认真负责,是公认的好老师 | 相似度分数:0.3909\n",
+      "内容:王老师教学认真负责,是公认的好老师 | 相似度分数:0.3909\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 带分数的相似度检索(分数越低越相似,0 表示完全匹配)\n",
+    "query = \"谁是老师\"\n",
+    "results = db.similarity_search_with_score(query, k=3)\n",
+    "\n",
+    "for doc, score in results:\n",
+    "    print(f\"内容:{doc.page_content} | 相似度分数:{score:.4f}\")\n",
+    "    "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "7e7240cc",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "C:\\Users\\29448\\AppData\\Local\\Temp\\ipykernel_31076\\632587025.py:12: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the `langchain-chroma package and should be used instead. To use it run `pip install -U `langchain-chroma` and import as `from `langchain_chroma import Chroma``.\n",
+      "  vectordb = Chroma(\n"
+     ]
+    }
+   ],
+   "source": [
+    "from langchain_community.vectorstores import Chroma\n",
+    "\n",
+    "# 从文档创建向量库(写数据)\n",
+    "# vectorstore = Chroma.from_documents(\n",
+    "#     documents=valid_docs,\n",
+    "#     embedding=embedding_model,\n",
+    "#     collection_metadata={\"hnsw:space\": \"cosine\"},\n",
+    "#     persist_directory=\"./my_knowledge_db\"\n",
+    "# )\n",
+    "\n",
+    "# 直接加载已持久化的数据库(无需再次添加文档或持久化)\n",
+    "vectordb = Chroma(\n",
+    "    persist_directory=\"./my_knowledge_db\",\n",
+    "    embedding_function=embedding_model\n",
+    ")\n",
+    "\n",
+    "# 创建检索器,设置返回 Top-3 最相关文档\n",
+    "retriever = vectordb.as_retriever(search_kwargs={\"k\": 3})"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "b3f89995",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "--- 结果1 ---\n",
+      "内容:说明!\n",
+      "□当车速在0 – 40km/h的范围内,且车辆在陡坡上低速下坡行驶\n",
+      "时,HDC才会激活。...\n",
+      "来源:{'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'keywords': '', 'page': 160, 'format': 'PDF 1.7', 'total_pages': 354, 'creationdate': '2023-06-16T15:35:59+08:00', 'author': '', 'subject': '', 'trapped': '', 'source': './car_info.pdf', 'title': '', 'modDate': \"D:20230616174145+08'00'\", 'file_path': './car_info.pdf', 'creator': 'PTC Arbortext Publishing Engine', 'moddate': '2023-06-16T17:41:45+08:00', 'creationDate': \"D:20230616153559+08'00'\"}\n",
+      "\n",
+      "--- 结果2 ---\n",
+      "内容:说明!\n",
+      "□当车速在0 – 40km/h的范围内,且车辆在陡坡上低速下坡行驶\n",
+      "时,HDC才会激活。...\n",
+      "来源:{'keywords': '', 'trapped': '', 'file_path': './car_info.pdf', 'page': 160, 'total_pages': 354, 'creationDate': \"D:20230616153559+08'00'\", 'modDate': \"D:20230616174145+08'00'\", 'moddate': '2023-06-16T17:41:45+08:00', 'subject': '', 'source': './car_info.pdf', 'creationdate': '2023-06-16T15:35:59+08:00', 'format': 'PDF 1.7', 'creator': 'PTC Arbortext Publishing Engine', 'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'title': '', 'author': ''}\n",
+      "\n",
+      "--- 结果3 ---\n",
+      "内容:□当车速在40-60 km/h范围内无法激活HDC功能,车速超过60 km/...\n",
+      "来源:{'total_pages': 354, 'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'source': './car_info.pdf', 'page': 160, 'creationdate': '2023-06-16T15:35:59+08:00', 'creator': 'PTC Arbortext Publishing Engine', 'author': '', 'subject': '', 'file_path': './car_info.pdf', 'keywords': '', 'trapped': '', 'moddate': '2023-06-16T17:41:45+08:00', 'creationDate': \"D:20230616153559+08'00'\", 'title': '', 'modDate': \"D:20230616174145+08'00'\", 'format': 'PDF 1.7'}\n",
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 测试检索\n",
+    "query = \"在多少速度范围内,HDC才会激活?\"\n",
+    "relevant_docs = retriever.invoke(query)\n",
+    "\n",
+    "for i, doc in enumerate(relevant_docs):\n",
+    "    print(f\"--- 结果{i+1} ---\")\n",
+    "    print(f\"内容:{doc.page_content[:100]}...\")\n",
+    "    print(f\"来源:{doc.metadata}\")\n",
+    "    print()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "ce141dc1",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from langchain_core.prompts import ChatPromptTemplate\n",
+    "\n",
+    "# 构建 Prompt 模板\n",
+    "prompt = ChatPromptTemplate.from_template(\"\"\"\n",
+    "你是一个专业的知识库助手。请根据以下检索到的上下文回答用户问题。\n",
+    "\n",
+    "**规则:**\n",
+    "- 只基于提供的上下文回答,不要编造\n",
+    "- 如果上下文中没有相关信息,直接说「根据现有资料,我找不到这个问题的答案」\n",
+    "- 回答要简洁直接,引用原文时用引号\n",
+    "\n",
+    "**检索到的上下文:**\n",
+    "{context}\n",
+    "\n",
+    "**用户问题:**\n",
+    "{question}\n",
+    "\"\"\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "id": "745939b1",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "在0 – 40km/h的范围内,HDC才会激活。\n"
+     ]
+    }
+   ],
+   "source": [
+    "from langchain_community.chat_models import ChatTongyi\n",
+    "from langchain_core.output_parsers import StrOutputParser\n",
+    "\n",
+    "# 初始化大模型(这里用通义千问,也可以换成 DeepSeek)\n",
+    "llm = ChatTongyi(\n",
+    "    model=\"qwen-plus\",                 # 模型名称\n",
+    "    dashscope_api_key=api_key   # 替换为你的真实 Key\n",
+    ")\n",
+    "\n",
+    "# 拼装上下文\n",
+    "context_text = \"\\n\\n---\\n\\n\".join([doc.page_content for doc in relevant_docs])\n",
+    "\n",
+    "# 构建 Chain 并调用\n",
+    "chain = prompt | llm | StrOutputParser()\n",
+    "\n",
+    "response = chain.invoke({\n",
+    "    \"context\": context_text,\n",
+    "    \"question\": query\n",
+    "})\n",
+    "\n",
+    "print(response)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "id": "96862cbb",
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "开始生成评估数据...\n",
+      "处理第1/4个问题:车顶行李架最大载荷是多少?\n",
+      "处理第2/4个问题:自适应巡航系统的工作速度范围是多少?\n",
+      "处理第3/4个问题:车辆推荐的轮胎胎压是多少(空载前轮)?\n",
+      "处理第4/4个问题:保养周期是多少公里或多长时间?\n",
+      "\n",
+      "开始评估...\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Evaluating: 100%|██████████| 16/16 [03:49<00:00, 14.37s/it]"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "评估结果: {'faithfulness': 0.8333, 'answer_relevancy': 0.5317, 'context_precision': 0.7500, 'context_recall': 0.7500}\n"
+     ]
+    },
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "# 使用 ragas 0.4.x 的 legacy metrics(兼容 LangChain LLM)\n",
+    "# 注意:ragas 0.4.x 的 collections metrics 需要 InstructorBaseRagasLLM,\n",
+    "# 而 legacy metrics 仍支持 LangChain LLM,通过 evaluate() 自动注入\n",
+    "from ragas.metrics._faithfulness import faithfulness\n",
+    "from ragas.metrics._answer_relevance import answer_relevancy\n",
+    "from ragas.metrics._context_precision import context_precision\n",
+    "from ragas.metrics._context_recall import context_recall\n",
+    "\n",
+    "from ragas import evaluate, RunConfig\n",
+    "from datasets import Dataset\n",
+    "from langchain_community.embeddings import DashScopeEmbeddings\n",
+    "from langchain_community.chat_models import ChatTongyi\n",
+    "\n",
+    "# 初始化模型\n",
+    "embedding_model = DashScopeEmbeddings(\n",
+    "    model=\"text-embedding-v3\",\n",
+    "    dashscope_api_key=api_key\n",
+    ")\n",
+    "llm = ChatTongyi(model=\"qwen-plus\", api_key=api_key)\n",
+    "\n",
+    "# ---- 准备测试数据 ----\n",
+    "test_questions = [\n",
+    "    {\n",
+    "        \"question\": \"车顶行李架最大载荷是多少?\",\n",
+    "        \"ground_truth\": \"车顶行李架最大载荷为70kg\"\n",
+    "    },\n",
+    "    {\n",
+    "        \"question\": \"自适应巡航系统的工作速度范围是多少?\",\n",
+    "        \"ground_truth\": \"自适应巡航系统(ACC)可以在0-150km/h范围内工作\"\n",
+    "    },\n",
+    "    {\n",
+    "        \"question\": \"车辆推荐的轮胎胎压是多少(空载前轮)?\",\n",
+    "        \"ground_truth\": \"空载时前轮推荐胎压为230 kPa\"\n",
+    "    },\n",
+    "    {\n",
+    "        \"question\": \"保养周期是多少公里或多长时间?\",\n",
+    "        \"ground_truth\": \"保养周期为15,000公里或12个月(以先到者为准)\"\n",
+    "    }\n",
+    "]\n",
+    "\n",
+    "# ---- 构建评估数据集 ----\n",
+    "evaluation_data = {\n",
+    "    \"question\": [],\n",
+    "    \"answer\": [],\n",
+    "    \"contexts\": [],\n",
+    "    \"ground_truth\": []\n",
+    "}\n",
+    "print(\"开始生成评估数据...\")\n",
+    "for i, item in enumerate(test_questions, 1):\n",
+    "    question = item[\"question\"]\n",
+    "    ground_truth = item[\"ground_truth\"]\n",
+    "\n",
+    "    print(f\"处理第{i}/{len(test_questions)}个问题:{question}\")\n",
+    "\n",
+    "    # 1.检索相关文档\n",
+    "    retriever = vectordb.as_retriever(search_kwargs={\"k\": 3})\n",
+    "    relevant_docs = retriever.invoke(question)\n",
+    "    contexts = [doc.page_content for doc in relevant_docs]\n",
+    "\n",
+    "    # 2.用大模型生成答案\n",
+    "    context_text = \"\\n\\n\".join(contexts)\n",
+    "    prompt = f\"\"\"基于以下上下文回答问题。如果没有相关信息,请说\"无法从提供的信息中回答\"。\n",
+    "\n",
+    "    上下文:{context_text}\n",
+    "\n",
+    "    问题:{question}\n",
+    "    答案:\"\"\"\n",
+    "    answer = llm.invoke(prompt).content\n",
+    "\n",
+    "    # 3. 保存评估数据\n",
+    "    evaluation_data[\"question\"].append(question)\n",
+    "    evaluation_data[\"answer\"].append(answer)\n",
+    "    evaluation_data[\"contexts\"].append(contexts)\n",
+    "    evaluation_data[\"ground_truth\"].append(ground_truth)\n",
+    "\n",
+    "# ---- 执行评估 ----\n",
+    "dataset = Dataset.from_dict(evaluation_data)\n",
+    "\n",
+    "# 配置评估运行参数\n",
+    "run_config = RunConfig(\n",
+    "    timeout=600,       # 单次操作超时时间(秒)\n",
+    "    max_workers=1,     # 并发数,本地模型建议设为1\n",
+    "    max_retries=10,    # 最大重试次数\n",
+    "    max_wait=120,      # 重试间隔上限(秒)\n",
+    ")\n",
+    "\n",
+    "# 定义要使用的评估指标(legacy metrics,已实例化的单例对象)\n",
+    "metrics = [\n",
+    "    faithfulness,        # 忠实度:答案是否基于上下文\n",
+    "    answer_relevancy,    # 答案相关性:答案是否与问题相关\n",
+    "    context_precision,   # 上下文精确度:检索到的上下文是否相关\n",
+    "    context_recall       # 上下文召回率:是否检索到所有必要信息\n",
+    "]\n",
+    "\n",
+    "print(\"\\n开始评估...\")\n",
+    "result = evaluate(\n",
+    "    dataset,\n",
+    "    metrics=metrics,\n",
+    "    embeddings=embedding_model,\n",
+    "    llm=llm,\n",
+    "    run_config=run_config\n",
+    ")\n",
+    "\n",
+    "# ---- 输出评估结果 ----\n",
+    "print(\"评估结果:\", result)"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3.11 (shuheAI)",
+   "language": "python",
+   "name": "shuheai"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.11.14"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}

+ 146 - 0
02_rag_homework/rag_demo.py

@@ -0,0 +1,146 @@
+from langchain_community.document_loaders import PyMuPDFLoader
+from langchain_text_splitters import RecursiveCharacterTextSplitter
+from langchain_community.embeddings import DashScopeEmbeddings
+from langchain_community.vectorstores import Chroma
+from langchain_core.prompts import ChatPromptTemplate
+from langchain_community.chat_models import ChatTongyi
+from langchain_core.output_parsers import StrOutputParser
+from dotenv import load_dotenv
+import re
+import os
+
+load_dotenv()
+api_key = os.getenv('QWEN_API_KEY')
+
+# ============================================================
+# 1. 加载 PDF 文档
+# ============================================================
+loader = PyMuPDFLoader("D:/code/shuheAI/02_RAG/华为擎云W585X 用户指南-(PGUX,KOS&UOS_02,zh-cn).pdf")
+pdf_pages = loader.load()
+
+print(f"文档类型:{type(pdf_pages)}")
+print(f"PDF 共{len(pdf_pages)}页")
+
+# 查看第一页的内容和元数据
+first_page = pdf_pages[0]
+print(f"元数据:{first_page.metadata}")
+print(f"内容预览:{first_page.page_content[:200]}")
+
+# ============================================================
+# 2. 清洗 PDF 文本
+# ============================================================
+def clean_pdf_text(text: str) -> str:
+    """清洗 PDF 解析出的文本,去除常见噪声"""
+    # 删除非中文字符之间的换行符
+    text = re.sub(r'[^一](\n)[^一]', lambda m: m.group(0).replace('\n', ''), text)
+    # 删除项目符号和多余空格
+    text = text.replace('•', '').replace('  ', ' ')
+    # 删除连续的换行符(保留一个)
+    text = re.sub(r'\n{2,}', '\n', text)
+    return text.strip()
+
+# 对所有页面清洗
+for page in pdf_pages:
+    page.page_content = clean_pdf_text(page.page_content)
+
+# ============================================================
+# 3. 文本分块
+# ============================================================
+splitter = RecursiveCharacterTextSplitter(
+    separators=["\n\n", "\n", "。", "!", "?", " ", ""],
+    chunk_size=50,
+    chunk_overlap=10,
+    length_function=len
+)
+
+split_docs = splitter.split_documents(pdf_pages)
+print(f"切分后的文件数量:{len(split_docs)}")
+print(f"切分后的字符数(可以用来大致评估 token 数):{sum([len(doc.page_content) for doc in split_docs])}")
+
+# 过滤掉 page_content 为空或仅含空白的文档
+valid_docs = [
+    doc for doc in split_docs
+    if doc.page_content and doc.page_content.strip()
+]
+print(f"有效块数量:{len(valid_docs)}")
+print(f"总字符数(可大致评估 Token 数):{sum(len(d.page_content) for d in valid_docs)}")
+
+# ============================================================
+# 4. 初始化 Embedding 模型
+# ============================================================
+embedding_model = DashScopeEmbeddings(
+    model="text-embedding-v3",
+    dashscope_api_key=api_key
+)
+
+# 单条文本向量化
+text = "RAG系统搭建实战"
+embedding = embedding_model.embed_query(text)
+print(f"向量维度:{len(embedding)}")
+print(f"前5个值:{embedding[:5]}")
+
+# ============================================================
+# 5. 向量化 + 存入向量库
+# ============================================================
+persist_dir = "./my_knowledge_db"
+
+if os.path.exists(persist_dir) and os.listdir(persist_dir):
+    print(f"加载已有向量库:{persist_dir}")
+    vectordb = Chroma(
+        persist_directory=persist_dir,
+        embedding_function=embedding_model
+    )
+else:
+    print(f"创建新向量库:{persist_dir}")
+    vectordb = Chroma.from_documents(
+        documents=valid_docs,
+        embedding=embedding_model,
+        collection_metadata={"hnsw:space": "cosine"},  # 余弦相似度
+        persist_directory=persist_dir
+    )
+
+# 创建检索器,设置返回 Top-3 最相关文档
+retriever = vectordb.as_retriever(search_kwargs={"k": 3})
+
+# ============================================================
+# 6. 提问
+# ============================================================
+query = "如何进入BIOS设置?"
+relevant_docs = retriever.invoke(query)
+
+for i, doc in enumerate(relevant_docs):
+    print(f"--- 结果{i+1} ---")
+    print(f"内容:{doc.page_content[:100]}...")
+    print(f"来源:{doc.metadata}")
+    print()
+
+# 带分数的相似度检索(分数越低越相似,0 表示完全匹配)
+results = vectordb.similarity_search_with_score(query, k=3)
+for doc, score in results:
+    print(f"内容:{doc.page_content[:100]}... | 相似度分数:{score:.4f}")
+
+# ============================================================
+# 7. 生成回答
+# ============================================================
+context = "\n\n---\n\n".join([d.page_content for d in relevant_docs])
+
+prompt = ChatPromptTemplate.from_template("""
+你是一个专业的知识库助手。请根据以下上下文回答问题。
+
+**规则:**
+- 只基于提供的上下文回答,不要编造
+- 如果上下文中没有相关信息,直接说「根据现有资料,我找不到这个问题的答案」
+- 回答要简洁直接,引用原文时用引号
+
+**上下文:**
+{context}
+
+**问题:**
+{question}
+""")
+
+llm = ChatTongyi(model="qwen-plus", dashscope_api_key=api_key)
+chain = prompt | llm | StrOutputParser()
+
+answer = chain.invoke({"context": context, "question": query})
+print(answer)

+ 40 - 0
03_langgraph_homework/Plan-and-Execute和ReAct的区别.md

@@ -0,0 +1,40 @@
+# ReAct 与 Plan-and-Execute 的区别
+
+## 一、ReAct
+
+我的核心理解是:Agent 不会提前制定一个完整的执行计划,而是根据当前任务和上一步的执行结果,动态决定下一步应该做什么。
+
+例如,用户要求查询天气并计算一个数学问题,Agent 可以先调用天气工具,得到结果后,再根据当前任务决定是否调用计算器工具。
+
+它的基本流程是:**思考 → 行动 → 观察 → 再思考 → 再行动 ……**
+
+ReAct 更像是 **"边思考、边执行、边根据结果调整"**。
+
+它的优点是灵活,适合执行路径不确定的任务。
+
+---
+
+## 二、Plan-and-Execute
+
+Plan-and-Execute 的核心理解是:Agent 先对整个任务进行规划,再按照计划逐步执行,并根据执行结果调整计划。
+
+也就是:**先规划,再执行,执行过程中根据结果进行检查和调整。**
+
+它的基本流程是:**规划 → 执行 → 检查 → 调整 → 再执行 ……**
+
+Plan-and-Execute 更像是 **"先规划整体路线,再按照路线执行"**。
+
+---
+
+## 三、我认为两者最本质的区别
+
+**ReAct 关注"下一步做什么",Plan-and-Execute 关注"整个任务怎么完成"。**
+
+ReAct 是"边走边决定下一步",而 Plan-and-Execute 是"先规划整体路线,再按照路线执行"。
+
+---
+
+## 四、适用场景
+
+- **ReAct** 更适合简单、动态、实时的任务。它的特点是边思考边行动,根据当前环境和工具返回结果动态决定下一步。
+- **Plan-and-Execute** 则是先对整个任务进行规划,再由 Executor 按步骤执行,并通过 Replanner 根据执行结果调整计划。它更适合复杂的多步骤任务,尤其是步骤之间存在依赖关系的场景。

Різницю між файлами не показано, бо вона завелика
+ 17 - 0
03_langgraph_homework/langgraph.ipynb


Різницю між файлами не показано, бо вона завелика
+ 106 - 0
03_langgraph_homework/react.ipynb


+ 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()

Різницю між файлами не показано, бо вона завелика
+ 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 负责工具发现、工具调用和结果返回。
+```
+

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

@@ -0,0 +1,123 @@
+(.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> 
+(.venv) PS D:\code\shuheAI\04_MCP> python .\step7_langgraph_travel_homework.py
+完整作业版可用 MCP 工具: ['search_hotels', 'search_transport', 'recommend_attractions', 'estimate_budget']
+
+========== 完整作业最终结果 ==========
+
+# 🌆 成都→重庆 3天2晚单人旅行规划方案(2026.08.15-08.17)
+
+### 一、需求理解
+- **出行时间**:2026年8月15日出发,3天2晚(8.15-8.17)
+- **出行人员**:1人
+- **预算上限**:≤ 1500元
+- **核心偏好**:地道美食、城市夜景、轻松节奏的城市漫步
+- **规划策略**:以高铁往返保障时效,住宿选址地铁沿线经济型酒店/青旅,景点以免费/低价核心地标为主,餐饮主打社区老店与小吃,全程控制日均步行强度,确保“松弛感”与预算双达标。
+
+---
+
+### 二、交通方案
+- **城际大交通**:成都东站 ↔ 重庆北站/重庆西站(高铁/动车)
+  - 耗时:单程约 1.5~2 小时
+  - 费用:二等座约 150元/单程,往返合计 **300元**
+  - 建议:提前 3~5 天在12306购票,避开周末临时溢价;优先选择重庆北站(距市区核心商圈更近)。
+- **市内交通**:地铁+公交为主,步行串联
+  - 费用预留:**120元**(单程2~7元,开通支付宝/微信“重庆公共交通乘车码”即可扫码通行)
+  - 提示:重庆核心景点地铁覆盖率达90%,山城地形多坡坎,建议穿舒适平底鞋,非必要不打车。
+
+---
+
+### 三、住宿方案
+- **预算控制**:250元/晚 × 2晚 = **500元**(严格匹配总预算,兼顾便利与舒适)
+- **推荐区域**:
+  1. **解放碑/较场口片区**:步行5分钟可达八一路好吃街与洪崖洞,地铁1/2/6号线交汇,美食与夜景动线最短。
+  2. **观音桥片区**:本地人高频生活圈,餐饮密集、夜生活丰富,地铁3/9号线直达,同价位房型空间更大。
+- **预订建议**:选择连锁快捷酒店(如汉庭、如家基础房型)或评分4.5+的独立青旅单间/民宿。避开“江景房”溢价标签,实际体验差异不大,且均位于地铁站步行10分钟范围内。
+
+---
+
+### 四、每日行程安排
+**Day 1(8.15)|抵达 + 城市初印象 + 江风夜景**
+- **下午**:抵达重庆,地铁直达酒店办理入住,稍作休整。
+- **傍晚**:步行至**解放碑步行街** → **八一路好吃街**(酸辣粉、山城小汤圆、烤脑花,人均30-40元吃饱)。
+- **晚上**:20:00后错峰前往**千厮门大桥**或**大剧院江滩**,隔江拍摄洪崖洞全景。随后沿**南滨路**慢走吹江风,看两江交汇夜景。
+- *节奏提示*:全程步行+短途地铁,以“吃+看”为主,不赶场。
+
+**Day 2(8.16)|山城漫步 + 立体交通 + 文艺休憩**
+- **上午**:**山城步道(第三步道)**。老重庆石板路、吊脚楼遗址、江景平台,适合慢走拍照(免费)。
+- **中午**:步道出口附近吃地道**重庆小面/豌杂面**(约15-20元)。
+- **下午**:地铁至**李子坝轻轨站**观景台看“轻轨穿楼”,随后步行至**鹅岭二厂**。找家天台/江景咖啡馆休息,感受山城文艺氛围(避开午后高温)。
+- **晚上**:返回市区,在**戴家巷崖壁步道**或**北滨路**散步,轻松赏夜景。
+- *节奏提示*:上午漫步+下午咖啡放空,体力分配合理,避开人流高峰。
+
+**Day 3(8.17)|市井烟火 + 轻松返程**
+- **上午**:睡到自然醒,前往酒店周边社区寻找**老火锅/串串香**(单人小锅或拼桌,约60-80元)。
+- **中午**:逛逛**观音桥好吃街**或**北仓文创街区**,购买伴手礼(陈麻花、火锅底料)。
+- **下午**:根据高铁时间提前1小时前往重庆北站,结束行程。
+- *节奏提示*:休闲收尾,预留充足时间前往车站,不安排高强度步行。
+
+---
+
+### 五、预算估算
+| 项目 | 费用(元) | 说明 |
+|------|------------|------|
+| 往返高铁交通 | 300 | 成都东↔重庆北 二等座 |
+| 住宿(2晚) | 500 | 250元/晚,经济型连锁/青旅单间 |
+| 餐饮(3天) | 350 | 日均约115元,主打小吃+1顿火锅+咖啡 |
+| 市内交通 | 120 | 地铁+公交+少量应急打车 |
+| 门票/体验预留 | 110 | 长江索道/文创/弹性支出(多数景点免费) |
+| **合计** | **1380** | **严格控制在1500元内,余120元备用** |
+
+---
+
+### 六、注意事项
+1. **天气与装备**:8月重庆高温多雨,务必携带晴雨伞、高倍防晒霜、驱蚊液及透气速干衣物。
+2. **错峰策略**:洪崖洞亮灯约19:30-23:00,建议20:30后前往对岸或高处观景,避开内部拥挤人流与排队。
+3. **美食避坑**:避开景区正门口“网红店”,多走两步进居民区小巷,味道更地道且价格亲民。
+4. **体力管理**:重庆“爬坡上坎”多,每日仅安排1个核心步行区+1个休息点,穿舒适平底鞋,遵循“轻松漫步”原则。
+5. **交通提示**:提前开通公共交通乘车码,地铁覆盖90%核心动线;若遇突发降雨,可灵活将户外漫步替换为室内文创街区或防空洞书店。
+
+---
+
+### 七、技术说明:本项目如何体现 MCP 和 LangGraph 多角色协作
+- **MCP Server 暴露了哪些工具**:MCP Server 封装了标准化旅行数据与计算工具,包括 `search_transport_routes`(查询城际/市内交通班次与实时票价)、`query_accommodation`(按区域/预算/评分检索酒店与青旅)、`fetch_attraction_info`(获取景点开放时间/门票/人流热力与步行强度)、`calculate_budget`(动态核算各项支出并校验预算上限)。
+- **MCP Client 如何动态发现工具**:LangGraph 的 Agent 节点在初始化阶段,通过 MCP Client 向 Server 发送 `tools/list` 请求,自动拉取当前可用工具清单及其 JSON Schema。系统无需硬编码工具定义,即可根据用户实时输入(如“预算1500”“喜欢夜景”)动态匹配、按需加载并调用最相关的工具,实现高扩展性。
+- **Function Calling 和 MCP 分别负责什么**:`Function Calling` 负责 LLM 的意图解析、参数提取与路由决策,将自然语言需求转化为结构化调用指令;`MCP` 负责提供标准化的工具执行环境、安全沙箱与数据管道,确保外部 API 调用的可观测性、权限控制与跨平台兼容。两者协同实现“LLM 负责思考与决策,MCP 负责安全执行与数据返回”的解耦架构。
+- **LangGraph 中每个角色节点负责什么**:系统采用有向图(Graph)编排多智能体协作流。`交通规划师节点` 调用交通工具输出最优动线;`住宿规划师节点` 基于动线与预算筛选住宿区域与房型;`景点体验规划师节点` 结合偏好与体力消耗生成每日松弛行程;`预算规划师节点` 作为全局校验器,汇总前三者输出进行成本核算,若超支则触发反馈边(Feedback Edge)要求降级或调整;最终由 `总规划师节点` 进行冲突消解、格式统一与方案输出,形成闭环工作流。
+(.venv) PS D:\code\shuheAI\04_MCP> 
+

+ 102 - 0
05_skill_homework/SKILL.md

@@ -0,0 +1,102 @@
+---
+name: monthly-learning-summary
+description: 当用户需要生成学生月度学习总结、月报、月底学习汇报、本月学习复盘时使用。适用于用户说“写月报”“生成月度总结”“本月学习总结”“月底汇报”“帮我整理这个月学了什么”等场景,用于生成面向导师或老师的结构化学习总结。不适用于周报、年终总结、单纯项目进度报告、简历或营销文案。
+---
+
+# 生成月度学习总结
+
+## 目标
+
+根据用户提供的简要学习情况,生成一份结构清晰、语气正式自然、适合提交给导师或老师的月度学习总结。
+
+生成时必须基于用户提供的信息组织内容,不能编造项目、成绩、证书、比赛结果或导师评价。
+
+## 执行步骤
+
+1. 确认汇报月份。
+   - 如果用户明确给出月份,使用用户指定的月份。
+   - 如果用户只说“这个月”或“本月”,使用当前月份。
+   - 如果月份不清楚,先询问用户要写哪个月份。
+
+2. 收集必要信息。
+   - 如果用户没有说明每周学习内容,询问本月每周大概学了什么。
+   - 如果用户没有说明完成的作业或项目,询问本月是否有作业、练习项目或课堂项目。
+   - 如果用户没有说明下月计划,询问下月准备继续学习什么。
+   - 一次最多问三个问题,避免让用户觉得复杂。
+
+3. 按周整理学习内容。
+   - 如果用户已经按周说明,保留用户原有顺序。
+   - 如果用户只给出一串知识点,就按照学习顺序整理成第1周到第4周,并注明“按学习顺序整理”。
+   - 如果用户明确提到五个学习阶段,可以增加第5周。
+
+4. 读取模板文件:
+   - 使用 `{baseDir}/assets/monthly-summary-template.md`
+   - 按模板结构填充内容。
+
+5. 生成总结内容。
+   - 本月学习概览:概括本月学习主题和整体进度。
+   - 按周学习内容:分周列出核心知识点。
+   - 完成的作业或项目:说明项目名称、完成情况和练习价值。
+   - 学习成果与收获:总结掌握了什么、能力有什么提升。
+   - 遇到的问题与解决情况:只写用户提供的问题,不能编造。
+   - 下月学习计划与目标:写成具体、可执行的目标。
+
+6. 输出前检查。
+   - 是否包含按周学习内容。
+   - 是否列出作业或项目;如果没有,明确写“暂无明确作业或项目记录”。
+   - 是否列出问题与解决方式;如果没有,明确写“本月未提供具体问题”。
+   - 是否包含下月学习计划。
+   - 是否保持模板结构完整。
+
+## 输出要求
+
+- 默认使用中文输出。
+- 语气要正式、自然,像学生向导师汇报。
+- 保持模板标题结构不变。
+- 用户没提供的信息不能编造,可以用“待补充”标记。
+- 多使用具体表达,例如“学习了”“完成了”“掌握了”“梳理了”“实践了”。
+- 如果用户提供的信息很少,也要先生成可用草稿,并提示哪些地方需要补充。
+
+## 边界情况处理
+
+### 用户信息很少
+
+如果用户只说:“这个月学了 HTML、CSS、JavaScript 和 React。”
+
+可以按学习顺序整理为:
+
+- 第1周:HTML 与 CSS 基础
+- 第2周:JavaScript 基础
+- 第3周:React 核心概念
+- 第4周:综合练习或复习巩固
+
+同时说明:以上周次是根据学习顺序整理,用户可以继续调整真实时间安排。
+
+### 用户没有提供问题
+
+不能替用户编造问题。应写:
+
+“本月未提供具体问题,后续可补充学习中遇到的卡点、报错或理解难点。”
+
+### 用户没有提供项目
+
+不能编造项目。应写:
+
+“暂无明确作业或项目记录,可补充练习项目、课堂作业或代码实践。”
+
+### 学习内容跨月
+
+如果用户描述的学习周期跨越两个月,只整理目标月份内的内容。另一个月份的内容可以放到“衔接说明”里简单说明。
+
+## 示例
+
+用户输入:
+
+“帮我写一份本月学习总结,这个月学了 HTML、CSS、JavaScript 和 React,做了一个 TodoList。”
+
+处理方式:
+
+1. 使用本 Skill。
+2. 按学习顺序整理每周内容。
+3. 把 TodoList 写入“完成的作业或项目”。
+4. 如果用户没有提供下月计划,可以生成“待补充”或先追问。

+ 51 - 0
05_skill_homework/assets/monthly-summary-template.md

@@ -0,0 +1,51 @@
+# {year}年{month}月学习总结
+
+**姓名:** {name}
+**汇报对象:** {mentor}
+**汇报周期:** {start_date} 至 {end_date}
+
+## 一、本月学习概览
+
+{monthly_overview}
+
+## 二、按周学习内容
+
+### 第1周:{week_1_title}
+
+{week_1_content}
+
+### 第2周:{week_2_title}
+
+{week_2_content}
+
+### 第3周:{week_3_title}
+
+{week_3_content}
+
+### 第4周:{week_4_title}
+
+{week_4_content}
+
+{week_5_optional}
+
+## 三、完成的作业或项目
+
+{assignments_and_projects}
+
+## 四、本月学习成果与收获
+
+{learning_outcomes}
+
+## 五、遇到的问题与解决情况
+
+| 问题 | 解决方式 | 当前状态 |
+|:-----|:---------|:---------|
+{problems_table}
+
+## 六、下月学习计划与目标
+
+{next_month_plan}
+
+## 七、衔接说明
+
+{transition_notes}

Деякі файли не було показано, через те що забагато файлів було змінено