Notebook

第3章项目:建立一套可交接的AI学习工作台

这不是安装比赛,也不是Python语法考试。本实验只验证一件事:你能否把一次模型问答变成可用、可复现、可确认的学习证据。

完成链路:环境自检 → 选择mock/local/cloud → 最小问答 → 人工复核 → 故障模拟 → 导出状态页。

第一次运行保持 MODE = "mock"。它不联网、不读取密钥,任何计算机都应能得到相同的课程回答。真实模型路径由教师确认后再切换。

0. 先预测,再运行

在运行代码前,先回答:

  1. 如果模型服务未启动,你预计会看到什么?
  2. 你的第一步是重装软件,还是保存错误并核对服务状态?为什么?
  3. AI若要修改文件,应该先向你说明哪些信息?

本章的动作边界:只读本章资源、运行安全自检、保存到 ch03_outputs/。不得查找密钥、读取无关目录、删除、覆盖、外发或连接真实账号。

# ===== 学生配置区:第一次只修改 STUDENT_RULE 和 STUDENT_PREDICTION =====
MODE = "mock"                 # 可选:mock / local / cloud
BASE_URL = ""                 # 只填课程提供的地址;mock保持空白
MODEL = "qwen-course-mock"    # 真实路径按课程模型卡填写
API_KEY_ENV = "AI_API_KEY"    # 这里只写环境变量名,绝不能写密钥值
FAULT = "none"                # 可选:none / service_offline / wrong_model

# 学生必改点1:补充一条你自己的Cline安全规则。
STUDENT_RULE = "【请修改】任何改动先说明文件、理由和验证方法,等我确认。"

# 学生必改点2:在故障模拟前写下预测。
STUDENT_PREDICTION = "【请修改】服务未启动时会返回连接类错误;我会先保留现场并核对服务状态。"

assert MODE in {"mock", "local", "cloud"}
assert FAULT in {"none", "service_offline", "wrong_model"}
assert not BASE_URL.startswith("sk-"), "BASE_URL中不应出现密钥"
print("配置已读取。当前路径:", MODE, ";故障开关:", FAULT)

1. 找到本章模块

下面的代码只在课程目录内查找 preflight.py,不会扫描整个计算机。Notebook和可复用函数分开存放,是因为后续HTML报告和自动测试也要调用同一套规则。完整函数代码在 chapters/ch03-environment/preflight.py,可在VS Code中逐行查看。

from pathlib import Path
import html
import json
import sys

cwd = Path.cwd().resolve()
candidates = [
    cwd / "chapters" / "ch03-environment",                  # 从teaching_resources运行
    cwd.parent / "chapters" / "ch03-environment",           # 从notebooks运行
    cwd / "publisher_clean" / "chapters_v1.7" / "teaching_resources" / "chapters" / "ch03-environment",
]
CHAPTER_DIR = next((p for p in candidates if (p / "preflight.py").is_file()), None)
if CHAPTER_DIR is None:
    raise FileNotFoundError("找不到preflight.py。请在teaching_resources或notebooks目录启动Notebook。")
RESOURCE_ROOT = CHAPTER_DIR.parents[1]
OUTPUT_DIR = CHAPTER_DIR / "ch03_outputs"
if str(CHAPTER_DIR) not in sys.path:
    sys.path.insert(0, str(CHAPTER_DIR))

from preflight import DEFAULT_PROMPT, build_evidence, call_model, classify_fault, preflight

print("本章目录:", CHAPTER_DIR)
print("输出目录:", OUTPUT_DIR)

2. Preflight:先证明工作区本身可用

环境自检只读取Python版本、操作系统、当前课程路径和必要文件是否存在。它不会读取浏览器记录、个人文档或密钥。先预测哪些项可能失败,再运行。

baseline = preflight()
print("总体状态:", "通过" if baseline["passed"] else "需要排查")
for item in baseline["checks"]:
    symbol = "[OK]" if item["status"] == "pass" else "[!]"
    print(f"{symbol} {item['name']}: {item['detail']}")

3. 模型连通:三条路径使用同一个输入和同一种结果结构

call_model()接收模式、提示词、服务地址、模型名和故障开关。mock直接返回课程固定回答;local/cloud则把同一提示词组装成OpenAI兼容的JSON请求,发送到 /chat/completions,再读取 choices[0].message.content。

这层统一接口很重要:教学任务不依赖某个聊天窗口。更换模型路径时,提示词、复核方法和证据格式不变。密钥只允许从 API_KEY_ENV 指定的环境变量读取,不进入Notebook。

PROMPT = DEFAULT_PROMPT
result = call_model(
    mode=MODE,
    prompt=PROMPT,
    base_url=BASE_URL,
    model=MODEL,
    api_key_env=API_KEY_ENV,
    fault=FAULT,
)

print("成功:", result.ok)
print("路径:", result.mode)
print("模型:", result.model)
print("耗时(ms):", result.latency_ms)
if result.ok:
    print("\n模型回答:\n" + result.answer)
else:
    print("\n故障类型:", result.error_type)
    print("故障现象:", result.error_message)
    print("建议顺序:", " → ".join(classify_fault(result)))

看懂请求的关键部分(不需要背代码)

真实路径在 preflight.py 中构造的核心数据如下。模型接收的不是“一个神秘按钮”,而是一组有角色的消息:system规定伴学规则,user给出本次问题。

body = {
    "model": selected_model,
    "messages": [
        {"role": "system", "content": "先给结论,再解释,最后列核验点"},
        {"role": "user", "content": prompt},
    ],
    "temperature": 0.2,
    "stream": False,
}

HTTP成功只说明服务返回了数据,不说明内容一定正确;所以还必须人工复核。

4. 人工复核:代码只能找线索,不能替你判断事实

下面的关键词检查只能提示回答是否提到了某些概念。出现“GPU”不代表相关解释正确。你必须在下一格写出自己的判断和核验办法。

answer = result.answer if result.ok else ""
clues = {
    "CPU/GPU分工线索": ("CPU" in answer and "GPU" in answer),
    "内存或显存线索": ("内存" in answer or "显存" in answer),
    "模型大小取舍线索": ("越大" in answer or "模型大小" in answer or "参数" in answer),
    "人工核验线索": ("核验" in answer or "查阅" in answer or "确认" in answer),
}
for name, found in clues.items():
    print(("[找到]" if found else "[未找到]"), name)
print("\n注意:[找到]只代表找到文字线索,不代表事实已经核验。")
# ===== 学生填写区:不能原样保留占位内容 =====
TRUSTED_POINT = "【请填写】回答中哪一点基本可信?"
VERIFY_POINT = "【请填写】哪一点仍需查证?"
VERIFY_METHOD = "【请填写】你准备查看本机信息、课程模型卡还是权威文档?"
FINAL_JUDGMENT = "【请填写】核验后全部/部分/不采纳,并说明原因。"

review = {
    "trusted_point": TRUSTED_POINT,
    "verify_point": VERIFY_POINT,
    "verify_method": VERIFY_METHOD,
    "final_judgment": FINAL_JUDGMENT,
    "student_rule": STUDENT_RULE,
    "student_prediction": STUDENT_PREDICTION,
}
print(json.dumps(review, ensure_ascii=False, indent=2))

5. 安全故障实验:改一个变量,比较结果

回到最上方配置,把 FAULT 从 none 改成 service_offline,重新运行模型单元格。记录故障类型和第一步。然后改成 wrong_model 再运行一次。

保持不变的量:模式、提示词、地址、模型配置。唯一改变的是故障类型。这样才能判断不同现象来自哪里。完成后把 FAULT 恢复为 none。

如果当前 FAULT 仍为 none,下格会用独立变量安全演示两类故障,不接触真实服务。

fault_comparison = []
for simulated_fault in ["service_offline", "wrong_model"]:
    demo = call_model(mode="mock", fault=simulated_fault)
    row = {
        "fault": simulated_fault,
        "error_type": demo.error_type,
        "message": demo.error_message,
        "first_step": classify_fault(demo)[0],
    }
    fault_comparison.append(row)
    print(f"{simulated_fault}: {demo.error_type} → {classify_fault(demo)[0]}")

assert fault_comparison[0]["error_type"] != fault_comparison[1]["error_type"]

6. 导出学习证据与状态页

下格只在本章的 ch03_outputs/ 中创建新文件,不覆盖教材模板。报告不保存密钥,也不保存完整服务地址。运行后打开 environment_report.html,让同伴判断是否能据此复现。

OUTPUT_DIR.mkdir(exist_ok=True)
evidence = build_evidence(result, baseline)
evidence["review"] = review
evidence["fault_comparison"] = fault_comparison

evidence_path = OUTPUT_DIR / "environment_evidence.json"
evidence_path.write_text(json.dumps(evidence, ensure_ascii=False, indent=2), encoding="utf-8")

status = "可用" if result.ok and baseline["passed"] else "需要排查"
answer_html = html.escape(result.answer or result.error_message)
report_html = f"""<!doctype html>
<html lang='zh-CN'><meta charset='utf-8'><meta name='viewport' content='width=device-width'>
<title>第3章环境状态页</title>
<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>
<main><p>第3章 · AI学习工作台</p><h1 class='ok'>{html.escape(status)}</h1>
<section><h2>环境</h2><p>路径:<code>{html.escape(result.mode)}</code> 模型:<code>{html.escape(result.model)}</code> 耗时:{result.latency_ms} ms</p></section>
<section><h2>模型结果</h2><pre>{answer_html}</pre></section>
<section><h2>人的判断</h2><p>{html.escape(FINAL_JUDGMENT)}</p><p><strong>学生规则:</strong>{html.escape(STUDENT_RULE)}</p></section>
<section><h2>交接提醒</h2><p>从Notebook开始;先运行preflight;真实服务不可用时切回mock;任何修改先说明、确认,再执行。</p></section></main></html>"""
report_path = OUTPUT_DIR / "environment_report.html"
report_path.write_text(report_html, encoding="utf-8")

print("已生成:", evidence_path)
print("已生成:", report_path)

7. 交接验收

请同伴只根据你的记录回答:用了哪个Notebook?走哪条模型路径?成功标志是什么?回答的哪一点经过核验?服务未启动时先做什么?Cline能否越出本章目录?

若有一项答不出,回到记录补充证据。最后把填写后的三份Markdown记录、Notebook运行记录、environment_evidence.json和environment_report.html作为本章交付物。

原理结论:模型服务提供能力,VS Code组织工程,Notebook保存实验,Cline提出计划并辅助修改,人决定是否授权、是否采纳和是否交付。