Phi-3.5 三个 demo notebook 差在哪:微软生成式 AI 入门课第 19 课

2026-08-18

翻到 generative-ai-for-beginners 第 19 课时,很多人会直接照着 19-slm/README.md 里「Running Phi-3/3.5 Locally」那一节点三个链接:19-slm/python/phi35-instruct-demo.ipynbphi35-vision-demo.ipynbphi35_moe_demo.ipynb。三份都是 Hugging Face Transformers 的本地推理演示,开头几个 cell 的写法也高度相似,粗看像同一份模板换了个模型名——于是按第一份的经验去改第二份,改到一半发现变量对不上。

这三份文件的差别其实全写在代码里。下面把它们放在一起对读,重点是各自加载什么、输入长什么样、依赖差在哪。

一、先分清这三份走的不是 provider 路线

课程仓的云端 provider 配置总说明在 00-course-setup/03-providers.md。那一页写明:需要特定 provider 的作业,会在文件名里带上标签——oai 要 OpenAI 的 endpoint 与 key,aoai 要 Azure OpenAI 的,hf 要 Hugging Face token,githubmodels 要对应的 endpoint 与 key;凭据统一写进从 .env.copy 复制出来的 .env第 19 课这三份 notebook 一个标签都没带:它们不读环境变量里的 key,而是直接把本地目录路径传给 from_pretrained

同目录下第四份 Phi-3-Vision-Nividia-NIM.ipynb 才是走 HTTP API 的形态——它里面是 invoke_urlrequests.post,header 写 "Authorization": "Bearer Your Nvidia NIM API Key",图片转成 base64 塞进 message 内容里,还有一行

assert len(image_b64) < 180_000, \
  "To upload larger images, use the assets API (see docs)"

这是那份 notebook 里的断言原文,180_000 是它写死的值。搞混这两类,你会拿着 API key 去找本地权重目录,或者反过来。

二、前置条件:仓库根的 requirements 装完并不够

00-course-setup/02-setup-local.md 的 Step 2 / Step 3 建环境:

python -m venv .venv          # make one
source .venv/bin/activate     # macOS / Linux
.\.venv\Scripts\activate      # Windows PowerShell
pip install -r requirements.txt

以上两段是该文件里的原文。Windows 用户注意第二行和第三行是两套写法,别在 PowerShell 里去 source。同一文件的 Troubleshooting 表里还单列了一条 Windows 条目:pip cannot build wheels (Windows),给的处置是 pip install --upgrade pip setuptools wheel then retry。

关键在于:仓库根 requirements.txtpyproject.tomldependencies 里都没有 torch、transformers、accelerate、Pillow、opencv-python。而 instruct 那份的第一个 cell 就是 import torchfrom transformers import AutoModelForCausalLM, AutoTokenizer, pipeline,MoE 那份是 from torch import bfloat16import transformers;vision 那份没有单独 import torch,但它同样从 transformers 里取 AutoModelForCausalLMAutoProcessor,加载时还传了 torch_dtype_attn_implementation='flash_attention_2'(后者对应的 flash-attn 也不在课程依赖里,MoE 那份把它的安装命令留在注释状态的 cell 里)。第 19 课目录下也没有自己的依赖清单文件——19-slm/ 下只有 README.mdimg/python/ 三项。所以装完课程依赖再打开这三份 notebook,缺的东西要你自己补。

另一件事是权重。三份的模型路径分别写死为 "../phi-3-instruct""../Phi3Vision""../Phi3MOE"。注意 ../ 是相对 notebook 所在的 19-slm/python/,指向的是 19-slm/ 本身,不是仓库根;而这三个目录仓库里并不存在,要你自己把权重放进去或改成别的路径。

19-slm/README.md 在介绍这条路线时写明它需要 GPU 加速。至于具体硬件要求,仓库里没有找到进一步的说明。

还有一点值得先看清:这三份 notebook 只是同一节课里「本地跑」的其中一条路子。README 在它们下面还并列了另外几条,命令都是原文给的,Windows 侧尤其省事——Ollama 那条只有一行 ollama run phi3.5;Foundry Local 那条给的是

winget install Microsoft.FoundryLocal
foundry model run phi-3.5-mini

README 自述 Foundry Local 会自动挑可用的执行 provider 并暴露一个 OpenAI 兼容端点;再往下还有 ONNX Runtime for GenAI 那条,装的是 onnxruntimeonnxruntime-genai。如果你的目的只是让 Phi 在本机出字,未必要从这三份 notebook 开始;它们的价值在于把 Transformers 这一层的每个环节摊开给你看。

三、三份的差别,落到字段上

instructvisionMoE
模型路径"../phi-3-instruct"model_id = "../Phi3Vision"model_id = "../Phi3MOE"
加载类AutoModelForCausalLMAutoModelForCausalLM + AutoProcessortransformers.AutoModelForCausalLM
dtypetorch_dtype="auto"torch_dtype="auto"torch_dtype=bfloat16
devicedevice_map="cuda"device_map="cuda",输入 .to("cuda:0")device_map='auto'
分词/预处理AutoTokenizerAutoProcessor(..., num_crops=4)AutoTokenizer
输入手拼字符串字符串 + PIL 图片列表函数拼字符串
生成pipeline("text-generation")model.generate(...)pipeline("text-generation")

instruct 这份最短。 它的输入不是 messages 列表——列表写法在 cell 里被注释掉了,实际用的是一整根手拼字符串,特殊 token 直接写在里面:

messages = "<|system|>\n 你是我的人工智能助手,协助我用中文解答问题.\n<|end|><|user|>\n 你知道长沙吗?? \n<|end|><|assistant|>"

然后 pipeline("text-generation", model=model, tokenizer=tokenizer)generation_args 里给的是 max_new_tokens: 1024return_full_text: Falsetemperature: 0.3do_sample: False,最后取 output[0]['generated_text']。开头还有一句 torch.random.manual_seed(0)。这些都是仓库里的示例值。

vision 这份的输入是两路。 它多了一个 AutoProcessor

processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True, num_crops=4)

模型加载那行还多了 _attn_implementation='flash_attention_2'。prompt 不再手写,而是

prompt = processor.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = processor(prompt, images, return_tensors="pt").to("cuda:0")

图片从哪来?这份 notebook 前四个 cell(全部处于注释状态)是一段 OpenCV 抽帧代码:cv2.VideoCapture 逐帧读,算直方图后用 cv2.compareHist(hist, next_hist, cv2.HISTCMP_CORREL) 比相似度,similarity < 0.9 就把这一帧存成 keyframe_{i}.jpg,调用写的是 save_keyframes('../video/copilot.mp4', '../output')。也就是说 vision demo 的真实输入形态是「一段视频先落成一堆 jpg」。接着的取图 cell 是

for i in range(1,22):

22 是写死的上限,对应作者当时抽出的那批帧,同时它决定了 placeholder 里拼出多少个 <|image_i|> 占位符。换成你自己的视频,这个数不改就对不上。

顺带一提,这份 notebook 取图之前那个 import cell 写的是 from PIL import Imageimport requests, base64,但后面走本地路径的代码里并没有再用到 requestsbase64——这两个名字在同目录那份走 HTTP API 的 NIM notebook 里才是主角。照抄 import 行时可以少装一点东西。

生成环节它也不走 pipeline,而是直接 model.generate(**inputs, eos_token_id=processor.tokenizer.eos_token_id, **generation_args),然后

generate_ids = generate_ids[:, inputs['input_ids'].shape[1]:]
response = processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]

前一行按输入长度切掉 prompt 部分,这件事 instruct 那份是靠 return_full_text: False 交给 pipeline 做的——同一个目的,两处写法完全不同。

MoE 这份多了一层 prompt 组装。 dtype 显式写成 from torch import bfloat16 再传 torch_dtype=bfloat16device_map='auto',加载后多一句 model.eval()。拼 prompt 用的是自定义函数:

def instruction_format(sys_message: str, query: str):
    # note, don't "</s>" to the end
    return f'<|system|> {sys_message} <|end|>\n<|user|> {query} <|end|>\n<|assistant|>'

它的 sys_msg 里写了一套 JSON 工具协议,要求模型用 "tool_name""input" 键值对回话,最终输出再带上 "agentid""output",可用工具列了 Blog 和 Translate 两个。generation_argsmax_new_tokens 是 512,不是 instruct 那份的 1024。此外它还单独有一个 cell 做 torch.cuda.empty_cache() 并设置 os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True "(原文末尾带一个空格)。

四、边界:几处照抄前得先改的地方

trust_remote_code=True 三份都开了,vision 那份连 processor 也开。这个开关意味着会执行权重目录里自带的 Python 代码,别对来路不明的目录打开它——这是通用做法,不是课程里的说明。

temperaturedo_sample: False 同时出现。 instruct 写 0.3、vision 写 0.0、MoE 写 0.3,而三份的 do_sample 都是 False。vision 与 MoE 这两份 notebook 保存的单元格输出里,留着 transformers 的 UserWarning 原文,说 do_sample 被设为 False,而 temperature 这个 flag 只在基于采样的生成模式下才起作用。两处白纸黑字放一起就是:照抄这段的话,那个温度值在这套参数下不生效。

有一条 deprecated 提示。 vision 那份加载 processor 的 cell 保存输出里是一条 FutureWarning,原文写 image_processor_class 参数已废弃、将在 v4.42 移除,建议改用 slow_image_processor_classfast_image_processor_class。这条是当时那个 transformers 版本打出来的,仓库里留着。

notebook 里混着裸 pip vision 那份有两个 cell 直接写 pip install transformers -Upip install jinja2 -U,没有 ! 也没有 %,而且位置排在模型已经加载之后。MoE 那份的四个 pip cell 则全是注释状态,其中两条名字还不一样:# pip install flash-attn --no-build-isolation# ! pip install flash_attn -U。这几行怎么在 Windows 上装,仓库里没有找到相关说明。

MoE 的 system prompt 自己前后不一致。 工具清单只给了 Blog 和 Translate,但示例文字写的是 “you must use the calculator tool like so”,紧跟的示例块用的却是 Blog;再往下演示翻译时,tool_name 又写成了 "Search"——这三个名字都不在工具清单里。直接照抄这段当模板,等于给模型一份自相矛盾的说明。

运行环境标注对不上。 这三份 notebook 的 metadata 里,kernelspec 显示名是 AzureML 的 Python 3.8,language_info 记的版本是 3.9.19;而仓库根 pyproject.toml 写的是 requires-python = ">=3.10"。这是仓库当前的值,随版本可能变动,但说明 notebook 是在另一套环境里存下来的,别把里面的日志当成你环境的预期。

五、怎么验证配对了

按这个顺序确认,比一路 Run All 省事:

  1. 只跑第一个 import cell。 报缺包就说明第二节那批依赖还没补上,跟模型路径无关。
  2. 确认相对路径。19-slm/python/ 下打开 notebook 时,../phi-3-instruct 指的是 19-slm/phi-3-instruct。先把这一层排除掉,再去怀疑权重本身。
  3. vision 先对帧数。 确认 ../output/ 下的 keyframe_1.jpgkeyframe_21.jpg 编号连续,再看 placeholder<|image_i|> 的个数和 images 列表长度是否一致。这两处对不上,问题出在抽帧那一步,不在模型。
  4. 打印拼好的 prompt 再送进去。 MoE 那份本身就有一个只输出 input_prompt 的 cell,可以照这个习惯办:
print(repr(input_prompt))
print(len(images), placeholder.count("<|image_"))

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。

  1. 核对加载到的类名。 MoE 那份 model.eval() 之后保存的输出里,打印出来的顶层类名是 PhiMoEForCausalLM;你本地加载完打印 model,类名对不上就说明指到了别的权重目录。

三份文件的共同点只有一句:路径、prompt 拼法、生成参数全是写死的示例值,没有一处从配置里读。把它们当成三份需要改的样例来读,比当成能直接跑的脚本来读要顺得多。


本文依据 github.com/microsoft/generative-ai-for-beginners 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的课程正文与代码示例。我们没有跑过文中涉及的代码, 因此不涉及运行结果、耗时与生成质量的任何描述。 该课程持续更新,文中涉及的文件路径、依赖与接口写法随版本变动,请以仓库最新内容为准。 文中涉及的云端服务调用会产生费用并可能上传数据,请自行评估密钥与数据边界。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。