{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 第3章项目：建立一套可交接的AI学习工作台\n",
    "\n",
    "这不是安装比赛，也不是Python语法考试。本实验只验证一件事：你能否把一次模型问答变成**可用、可复现、可确认**的学习证据。\n",
    "\n",
    "完成链路：`环境自检 → 选择mock/local/cloud → 最小问答 → 人工复核 → 故障模拟 → 导出状态页`。\n",
    "\n",
    "> 第一次运行保持 `MODE = \"mock\"`。它不联网、不读取密钥，任何计算机都应能得到相同的课程回答。真实模型路径由教师确认后再切换。"
   ],
   "id": "ch03-environment-check-01"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 0. 先预测，再运行\n",
    "\n",
    "在运行代码前，先回答：\n",
    "\n",
    "1. 如果模型服务未启动，你预计会看到什么？\n",
    "2. 你的第一步是重装软件，还是保存错误并核对服务状态？为什么？\n",
    "3. AI若要修改文件，应该先向你说明哪些信息？\n",
    "\n",
    "本章的动作边界：只读本章资源、运行安全自检、保存到 `ch03_outputs/`。不得查找密钥、读取无关目录、删除、覆盖、外发或连接真实账号。"
   ],
   "id": "ch03-environment-check-02"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "parameters",
     "student-edit"
    ]
   },
   "outputs": [],
   "source": [
    "# ===== 学生配置区：第一次只修改 STUDENT_RULE 和 STUDENT_PREDICTION =====\n",
    "MODE = \"mock\"                 # 可选：mock / local / cloud\n",
    "BASE_URL = \"\"                 # 只填课程提供的地址；mock保持空白\n",
    "MODEL = \"qwen-course-mock\"    # 真实路径按课程模型卡填写\n",
    "API_KEY_ENV = \"AI_API_KEY\"    # 这里只写环境变量名，绝不能写密钥值\n",
    "FAULT = \"none\"                # 可选：none / service_offline / wrong_model\n",
    "\n",
    "# 学生必改点1：补充一条你自己的Cline安全规则。\n",
    "STUDENT_RULE = \"【请修改】任何改动先说明文件、理由和验证方法，等我确认。\"\n",
    "\n",
    "# 学生必改点2：在故障模拟前写下预测。\n",
    "STUDENT_PREDICTION = \"【请修改】服务未启动时会返回连接类错误；我会先保留现场并核对服务状态。\"\n",
    "\n",
    "assert MODE in {\"mock\", \"local\", \"cloud\"}\n",
    "assert FAULT in {\"none\", \"service_offline\", \"wrong_model\"}\n",
    "assert not BASE_URL.startswith(\"sk-\"), \"BASE_URL中不应出现密钥\"\n",
    "print(\"配置已读取。当前路径：\", MODE, \"；故障开关：\", FAULT)"
   ],
   "id": "ch03-environment-check-03"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. 找到本章模块\n",
    "\n",
    "下面的代码只在课程目录内查找 `preflight.py`，不会扫描整个计算机。Notebook和可复用函数分开存放，是因为后续HTML报告和自动测试也要调用同一套规则。完整函数代码在 `chapters/ch03-environment/preflight.py`，可在VS Code中逐行查看。"
   ],
   "id": "ch03-environment-check-04"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "import html\n",
    "import json\n",
    "import sys\n",
    "\n",
    "cwd = Path.cwd().resolve()\n",
    "candidates = [\n",
    "    cwd / \"chapters\" / \"ch03-environment\",                  # 从teaching_resources运行\n",
    "    cwd.parent / \"chapters\" / \"ch03-environment\",           # 从notebooks运行\n",
    "    cwd / \"publisher_clean\" / \"chapters_v1.7\" / \"teaching_resources\" / \"chapters\" / \"ch03-environment\",\n",
    "]\n",
    "CHAPTER_DIR = next((p for p in candidates if (p / \"preflight.py\").is_file()), None)\n",
    "if CHAPTER_DIR is None:\n",
    "    raise FileNotFoundError(\"找不到preflight.py。请在teaching_resources或notebooks目录启动Notebook。\")\n",
    "RESOURCE_ROOT = CHAPTER_DIR.parents[1]\n",
    "OUTPUT_DIR = CHAPTER_DIR / \"ch03_outputs\"\n",
    "if str(CHAPTER_DIR) not in sys.path:\n",
    "    sys.path.insert(0, str(CHAPTER_DIR))\n",
    "\n",
    "from preflight import DEFAULT_PROMPT, build_evidence, call_model, classify_fault, preflight\n",
    "\n",
    "print(\"本章目录：\", CHAPTER_DIR)\n",
    "print(\"输出目录：\", OUTPUT_DIR)"
   ],
   "id": "ch03-environment-check-05"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Preflight：先证明工作区本身可用\n",
    "\n",
    "环境自检只读取Python版本、操作系统、当前课程路径和必要文件是否存在。它不会读取浏览器记录、个人文档或密钥。先预测哪些项可能失败，再运行。"
   ],
   "id": "ch03-environment-check-06"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "baseline = preflight()\n",
    "print(\"总体状态：\", \"通过\" if baseline[\"passed\"] else \"需要排查\")\n",
    "for item in baseline[\"checks\"]:\n",
    "    symbol = \"[OK]\" if item[\"status\"] == \"pass\" else \"[!]\"\n",
    "    print(f\"{symbol} {item['name']}: {item['detail']}\")"
   ],
   "id": "ch03-environment-check-07"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 模型连通：三条路径使用同一个输入和同一种结果结构\n",
    "\n",
    "`call_model()`接收模式、提示词、服务地址、模型名和故障开关。mock直接返回课程固定回答；local/cloud则把同一提示词组装成OpenAI兼容的JSON请求，发送到 `/chat/completions`，再读取 `choices[0].message.content`。\n",
    "\n",
    "这层统一接口很重要：教学任务不依赖某个聊天窗口。更换模型路径时，提示词、复核方法和证据格式不变。密钥只允许从 `API_KEY_ENV` 指定的环境变量读取，不进入Notebook。"
   ],
   "id": "ch03-environment-check-08"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PROMPT = DEFAULT_PROMPT\n",
    "result = call_model(\n",
    "    mode=MODE,\n",
    "    prompt=PROMPT,\n",
    "    base_url=BASE_URL,\n",
    "    model=MODEL,\n",
    "    api_key_env=API_KEY_ENV,\n",
    "    fault=FAULT,\n",
    ")\n",
    "\n",
    "print(\"成功：\", result.ok)\n",
    "print(\"路径：\", result.mode)\n",
    "print(\"模型：\", result.model)\n",
    "print(\"耗时(ms)：\", result.latency_ms)\n",
    "if result.ok:\n",
    "    print(\"\\n模型回答：\\n\" + result.answer)\n",
    "else:\n",
    "    print(\"\\n故障类型：\", result.error_type)\n",
    "    print(\"故障现象：\", result.error_message)\n",
    "    print(\"建议顺序：\", \" → \".join(classify_fault(result)))"
   ],
   "id": "ch03-environment-check-09"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 看懂请求的关键部分（不需要背代码）\n",
    "\n",
    "真实路径在 `preflight.py` 中构造的核心数据如下。模型接收的不是“一个神秘按钮”，而是一组有角色的消息：system规定伴学规则，user给出本次问题。\n",
    "\n",
    "```python\n",
    "body = {\n",
    "    \"model\": selected_model,\n",
    "    \"messages\": [\n",
    "        {\"role\": \"system\", \"content\": \"先给结论，再解释，最后列核验点\"},\n",
    "        {\"role\": \"user\", \"content\": prompt},\n",
    "    ],\n",
    "    \"temperature\": 0.2,\n",
    "    \"stream\": False,\n",
    "}\n",
    "```\n",
    "\n",
    "HTTP成功只说明服务返回了数据，不说明内容一定正确；所以还必须人工复核。"
   ],
   "id": "ch03-environment-check-10"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 人工复核：代码只能找线索，不能替你判断事实\n",
    "\n",
    "下面的关键词检查只能提示回答是否**提到**了某些概念。出现“GPU”不代表相关解释正确。你必须在下一格写出自己的判断和核验办法。"
   ],
   "id": "ch03-environment-check-11"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "answer = result.answer if result.ok else \"\"\n",
    "clues = {\n",
    "    \"CPU/GPU分工线索\": (\"CPU\" in answer and \"GPU\" in answer),\n",
    "    \"内存或显存线索\": (\"内存\" in answer or \"显存\" in answer),\n",
    "    \"模型大小取舍线索\": (\"越大\" in answer or \"模型大小\" in answer or \"参数\" in answer),\n",
    "    \"人工核验线索\": (\"核验\" in answer or \"查阅\" in answer or \"确认\" in answer),\n",
    "}\n",
    "for name, found in clues.items():\n",
    "    print((\"[找到]\" if found else \"[未找到]\"), name)\n",
    "print(\"\\n注意：[找到]只代表找到文字线索，不代表事实已经核验。\")"
   ],
   "id": "ch03-environment-check-12"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "student-edit"
    ]
   },
   "outputs": [],
   "source": [
    "# ===== 学生填写区：不能原样保留占位内容 =====\n",
    "TRUSTED_POINT = \"【请填写】回答中哪一点基本可信？\"\n",
    "VERIFY_POINT = \"【请填写】哪一点仍需查证？\"\n",
    "VERIFY_METHOD = \"【请填写】你准备查看本机信息、课程模型卡还是权威文档？\"\n",
    "FINAL_JUDGMENT = \"【请填写】核验后全部/部分/不采纳，并说明原因。\"\n",
    "\n",
    "review = {\n",
    "    \"trusted_point\": TRUSTED_POINT,\n",
    "    \"verify_point\": VERIFY_POINT,\n",
    "    \"verify_method\": VERIFY_METHOD,\n",
    "    \"final_judgment\": FINAL_JUDGMENT,\n",
    "    \"student_rule\": STUDENT_RULE,\n",
    "    \"student_prediction\": STUDENT_PREDICTION,\n",
    "}\n",
    "print(json.dumps(review, ensure_ascii=False, indent=2))"
   ],
   "id": "ch03-environment-check-13"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. 安全故障实验：改一个变量，比较结果\n",
    "\n",
    "回到最上方配置，把 `FAULT` 从 `none` 改成 `service_offline`，重新运行模型单元格。记录故障类型和第一步。然后改成 `wrong_model` 再运行一次。\n",
    "\n",
    "保持不变的量：模式、提示词、地址、模型配置。唯一改变的是故障类型。这样才能判断不同现象来自哪里。完成后把 `FAULT` 恢复为 `none`。\n",
    "\n",
    "如果当前 `FAULT` 仍为 `none`，下格会用独立变量安全演示两类故障，不接触真实服务。"
   ],
   "id": "ch03-environment-check-14"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fault_comparison = []\n",
    "for simulated_fault in [\"service_offline\", \"wrong_model\"]:\n",
    "    demo = call_model(mode=\"mock\", fault=simulated_fault)\n",
    "    row = {\n",
    "        \"fault\": simulated_fault,\n",
    "        \"error_type\": demo.error_type,\n",
    "        \"message\": demo.error_message,\n",
    "        \"first_step\": classify_fault(demo)[0],\n",
    "    }\n",
    "    fault_comparison.append(row)\n",
    "    print(f\"{simulated_fault}: {demo.error_type} → {classify_fault(demo)[0]}\")\n",
    "\n",
    "assert fault_comparison[0][\"error_type\"] != fault_comparison[1][\"error_type\"]"
   ],
   "id": "ch03-environment-check-15"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. 导出学习证据与状态页\n",
    "\n",
    "下格只在本章的 `ch03_outputs/` 中创建新文件，不覆盖教材模板。报告不保存密钥，也不保存完整服务地址。运行后打开 `environment_report.html`，让同伴判断是否能据此复现。"
   ],
   "id": "ch03-environment-check-16"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "OUTPUT_DIR.mkdir(exist_ok=True)\n",
    "evidence = build_evidence(result, baseline)\n",
    "evidence[\"review\"] = review\n",
    "evidence[\"fault_comparison\"] = fault_comparison\n",
    "\n",
    "evidence_path = OUTPUT_DIR / \"environment_evidence.json\"\n",
    "evidence_path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n",
    "\n",
    "status = \"可用\" if result.ok and baseline[\"passed\"] else \"需要排查\"\n",
    "answer_html = html.escape(result.answer or result.error_message)\n",
    "report_html = f\"\"\"<!doctype html>\n",
    "<html lang='zh-CN'><meta charset='utf-8'><meta name='viewport' content='width=device-width'>\n",
    "<title>第3章环境状态页</title>\n",
    "<style>body{{font:16px/1.7 system-ui;margin:0;background:#f4f7f5;color:#17342d}}main{{max-width:820px;margin:auto;padding:40px 20px}}section{{background:white;border:1px solid #dce7e1;border-radius:16px;padding:24px;margin:16px 0}}.ok{{color:#08785e}}code{{background:#edf3f0;padding:2px 6px;border-radius:5px}}pre{{white-space:pre-wrap}}</style>\n",
    "<main><p>第3章 · AI学习工作台</p><h1 class='ok'>{html.escape(status)}</h1>\n",
    "<section><h2>环境</h2><p>路径：<code>{html.escape(result.mode)}</code>　模型：<code>{html.escape(result.model)}</code>　耗时：{result.latency_ms} ms</p></section>\n",
    "<section><h2>模型结果</h2><pre>{answer_html}</pre></section>\n",
    "<section><h2>人的判断</h2><p>{html.escape(FINAL_JUDGMENT)}</p><p><strong>学生规则：</strong>{html.escape(STUDENT_RULE)}</p></section>\n",
    "<section><h2>交接提醒</h2><p>从Notebook开始；先运行preflight；真实服务不可用时切回mock；任何修改先说明、确认，再执行。</p></section></main></html>\"\"\"\n",
    "report_path = OUTPUT_DIR / \"environment_report.html\"\n",
    "report_path.write_text(report_html, encoding=\"utf-8\")\n",
    "\n",
    "print(\"已生成：\", evidence_path)\n",
    "print(\"已生成：\", report_path)"
   ],
   "id": "ch03-environment-check-17"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. 交接验收\n",
    "\n",
    "请同伴只根据你的记录回答：用了哪个Notebook？走哪条模型路径？成功标志是什么？回答的哪一点经过核验？服务未启动时先做什么？Cline能否越出本章目录？\n",
    "\n",
    "若有一项答不出，回到记录补充证据。最后把填写后的三份Markdown记录、Notebook运行记录、`environment_evidence.json`和`environment_report.html`作为本章交付物。\n",
    "\n",
    "**原理结论**：模型服务提供能力，VS Code组织工程，Notebook保存实验，Cline提出计划并辅助修改，人决定是否授权、是否采纳和是否交付。"
   ],
   "id": "ch03-environment-check-18"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
