先想一个场景:你正埋头写代码,双手按在键盘上,屏幕上那只桌面牛突然开始“哞哞”叫。或者你正吃着外卖,腾不出手去点鼠标,桌宠却在一个劲播放音效。这时候你唯一想要的能力,就是张嘴喊一句「牛来」,让它立刻闭嘴。听起来像整活,但这件事的技术含量并不低:它意味着一个桌面宠物具备了完整的“麦克风采集 → 本地语音识别 → 指令执行”闭环,而且整个过程不需要云服务器参与。
这篇文章要讲的,就是把这个闭环跑通的具体方法。我会从方案选型开始,给出一套可以直接运行的 Python 实现,让「牛来」这句话从声波变成一次桌宠动作触发。整个方案不依赖 GPU、不依赖付费 API、也不需要联网。做完之后你会得到一个判断:本地语音识别并没有想象中那么高不可攀,真正有门槛的反而在于如何把音频流、识别引擎和应用逻辑稳定地黏合在一起,而桌面宠物恰好是这个链路成本最低、效果最直观的试验场。
如果你关注桌面宠物开发、本地 AI 部署,或者只是想给自己的小工具加一个“语音入口”,这篇会比较对味。文章涉及的关键技术包括:本地离线语音识别(Vosk)、麦克风音频流采集(sounddevice)、关键词/唤醒词识别,以及用本地 HTTP 服务把识别结果转成动作。这套代码稍加改造,就能复用到语音关机、语音控制播放器、本地会议记录这类桌面端场景。
1. 为什么给桌宠加语音识别,值得单独写一篇
桌宠这类应用的交互方式,长期停留在“鼠标点击”和“键盘快捷键”上。你会点它、拖它、喂它,但它本质上是被动等待输入的装饰品。语音控制出现后,交互模型发生了变化:用户不再需要把手挪到鼠标上,桌宠可以随时响应一句话。这种体验上的差异,正是语音助手类产品让人“上瘾”的原因。
但从技术角度看,桌宠又是一个非常特殊的语音识别场景。它不需要听懂长篇大论,不需要纠正说话人的语法错误,甚至不需要理解自然语言的整句含义。它只需要在有限的指令集里做判断:这句话里有没有「牛来」,命中了就执行闭嘴动作。因此它属于典型的“封闭场景、有限指令集”语音交互,这大大降低了识别引擎的选择难度。
这也是我认为这件事值得单独写一篇的原因:桌面宠物是一个“小玩具”,但它是学习本地语音交互链路的最佳载体。它实时性要求不高、动作语义单一、失败成本低、用户容忍度高。你不需要先搭一个完整的智能音箱系统,再回来研究语音识别;你可以从让桌宠听懂口令开始,把采集、识别、控制这三段链路全部打通。
还有一个更现实的理由:语音识别放回本机,是整个本地 AI 部署趋势的一部分。就像本地部署大模型解决的是数据隐私和调用成本问题一样,本地语音识别把音频数据留在设备内,不依赖网络,也不按调用次数计费。对于桌面这种轻量场景,云 API 的“高准确率”优势并不明显,反而是“必须联网”“按次计费”“音频上传”这些缺点被放大了。
所以这篇文章的核心判断是:给桌宠加语音控制,最适合的第一步不是接入云端语音 API,而是用本地离线识别跑通最小闭环。先解决采集和控制,再去纠结模型精度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 语音识别选型:云端 API 和本地识别的真实差距
很多开发者听到“语音识别”,第一反应是接入各家云平台的语音 API。这些 API 确实准确率高、接入简单,但用到桌面宠物这种场景里,会有几个绕不开的问题。
| 对比维度 | 云端语音识别 API | 本地离线识别(Vosk / sherpa-onnx / FunASR) |
|---|---|---|
| 网络依赖 | 必须联网,断网即失效 | 完全离线运行 |
| 调用成本 | 按调用时长或次数计费 | 一次性下载模型,运行不花钱 |
| 端到端延迟 | 录音上传 + 服务端计算 + 结果回传 | 本地推理,关键词场景通常在几百毫秒内 |
| 隐私 | 音频内容上传到服务商 | 音频不出本机 |
| 中文支持 | 通常较强,长句听写优势明显 | 小模型够用,复杂长句弱于大模型 |
| 资源占用 | 客户端本身负担小 | CPU 占用随模型大小和持续监听方式变化 |
如果只是做一个“听懂一句话就闭嘴”的桌宠,云端 API 的准确率优势基本发挥不出来。反而延迟和稳定性会拖后腿:你喊完「牛来」,语音要经过录音结束、上传、服务器解码、回传这一整条链路,体验上往往比本地识别慢半拍。桌宠这种需要“即时反应”的场景,本地识别在体感上更自然。
目前比较主流的本地/离线识别方案有几类:
- Vosk:轻量级离线识别工具包,提供 Python、Java、C++ 等绑定,支持中文模型,还支持把识别词表限制在指定关键词集合内。这一点对唤醒词场景非常关键。
- sherpa-onnx:支持 Paraformer、Zipformer、Whisper 等多种模型,擅长嵌入式与移动端场景,适合以后往 Jetson、RK3588 这类设备上迁移。
- FunASR:阿里开源的语音识别工具包,中文识别效果好,但模型相对更大,更适合放在本地服务器上做批量转写。
- faster-whisper:在 Whisper 基础上做了推理加速,通用语言能力强,中文效果不错,但资源要求比 Vosk 小模型高不少。
选型时不要只看“谁的识别率最高”,而是看“在有限指令集与有限算力的条件下,谁能更稳定地命中关键词”。桌宠不需要听写整段话,它只需要在有限的词表里做判断。因此 Vosk 这类支持“语法约束”的引擎,反而是最贴合的选择:它可以把搜索空间缩小到「牛来」「闭嘴」这几句话上,既降低 CPU 开销,也降低误识别概率。
3. 核心概念:从一声「牛来」到一次系统调用
在写代码之前,需要先理清一段语音从麦克风到动作触发,中间经历了什么。理解这条链路,排错时会从容很多。
第一阶段是音频采集。麦克风采集到的模拟信号会被声卡转换为数字信号,通常以 PCM 格式输出。语音识别引擎普遍要求 16kHz 采样率、单声道、16 bit 位深,因为绝大多数语音模型就是在这样的数据上训练的。16kHz 意味着每秒采集 16000 个采样点,每个采样点用 2 字节表示,那么一秒音频就是 32000 字节。这个数字在后面设置音频缓冲区时很实用。
第二阶段是端点检测与分帧。音频流是连续的,但识别引擎需要按“块”处理。实践中常用的做法是设置一个固定 blocksize,让声卡每采集满一定字节就回调一次,把原始 PCM 数据塞进一个队列,识别主循环再从队列里取。桌面场景里一次回调通常对应 0.1 到 0.5 秒的音频。
第三阶段是特征提取与解码。音频块进入识别引擎后,会被转换成声学特征,再通过声学模型和语言模型解码成文本。这就是 Vosk 这类引擎内部做的事情,使用者只需要把 PCM 数据交给它,然后读取输出的文本。
第四阶段是语义映射。识别出来的文本不直接等于动作。你需要定义“哪些文本命中后触发哪个动作”,比如文本里包含「牛来」就发送“闭嘴”指令给桌宠进程。这个映射可以做得非常简陋,也可以做成带置信度判断的规则引擎。
最后是动作执行。桌宠本身是一个独立应用,语音识别模块是一个独立进程。两个进程之间需要通信。最简单的做法是桌宠开一个本地 HTTP 服务,识别模块用 requests 发一条 POST 请求。通信协议完全由你控制,这比把识别逻辑硬塞进桌宠内部要灵活得多。
这里还要区分两个概念:连续听写和关键词识别。连续听写是“把用户说的整段话全部转成文字”,关键词识别是“只判断会话中是否出现指定词语”。桌宠适合关键词识别:识别范围小、速度快、不容易因为整句噪声产生误触发。Vosk 的 KaldiRecognizer 支持两种用法:不传语法时是全量识别,传入 JSON 语法时会把识别结果限制在指定短语集合里。后一种方式明显的优势是 CPU 开销低、命中准确率高,缺点是扩展指令时需要同步更新词表。
4. 环境准备与前置条件
本节的代码基于 Python,桌面环境为主。如果你用 Windows 或 macOS,直接跑即可;如果用 Linux,需要确保系统里有音频设备,并安装 PortAudio 的运行库。
推荐环境:
- 操作系统:Windows 10/11、macOS、Ubuntu 20.04 及以上
- Python 版本:3.9 到 3.11 均可,具体以依赖库兼容性为准
- 麦克风:笔记本内置麦克风或 USB 麦克风均可
安装依赖命令如下:
bash复制python -m pip install --upgrade pip
pip install vosk sounddevice requests flask
说明一下这几个库的角色:
vosk是离线识别引擎,负责把 PCM 音频转成文本。sounddevice是 PortAudio 的 Python 绑定,负责从麦克风采集原始音频。requests用于识别进程向桌宠服务发送 HTTP 指令。flask用于在桌宠进程里开一个本地控制接口,方便其他进程调用。
接着下载中文语言模型。从 Vosk 官方模型列表选择一个小体积中文模型即可,例如 vosk-model-small-cn-0.22(模型版本以官方列表为准)。下载后解压,得到一个模型目录。我把这个目录改名为 model-cn-small,后面的代码会用到。这个模型文件只保存在本地,识别过程不依赖任何外部服务。
安装完成后,可以先验证库是否可用:
bash复制python -c "import vosk; print(vosk.__version__)"
python -c "import sounddevice; print(sounddevice.__version__)"
如果 sounddevice 导入失败,在 Ubuntu 上通常需要先安装 PortAudio 系统库:
bash复制sudo apt-get install libportaudio2
到这里环境就绪。下面开始写代码。
5. 完整示例:让桌宠听懂「牛来」
我会按四步拆解:先确认麦克风可用,再跑通最小识别循环,然后把识别范围收敛到关键词,最后接入桌宠控制服务。每一步都可以独立运行和验证。
5.1 第一步:确认麦克风设备
先写一个脚本列出系统里的音频设备,这一步能提前排除大部分采集问题。
python复制# voice_pet/check_audio.py
import sounddevice as sd
# 打印音频输入输出设备列表
print(sd.query_devices())
print("默认输入设备编号:", sd.default.device)
bash复制python voice_pet/check_audio.py
正常输出里能看到输入设备的名称、默认采样率和最大输入通道数。如果设备列表为空,说明系统没有可用的音频输入,后面所有步骤都无法进行,需要先检查麦克风驱动。
5.2 第二步:最小识别循环
这一版代码不做任何动作控制,只负责把麦克风采集到的音频送入识别引擎,并打印识别出来的文本。
python复制# voice_pet/listen_keyword.py
import json
import queue
import sys
import sounddevice as sd
import vosk
MODEL_PATH = "model-cn-small" # 下载并解压后的模型目录
SAMPLE_RATE = 16000 # 16kHz、单声道、int16 是 Vosk 的标准输入格式
model = vosk.Model(MODEL_PATH)
audio_queue = queue.Queue()
def audio_callback(indata, frames, time_info, status):
"""麦克风回调:把原始 PCM 数据放进队列,避免阻塞采集线程。"""
if status:
print("[audio] 采集状态异常:", status, file=sys.stderr)
audio_queue.put(bytes(indata))
def main():
rec = vosk.KaldiRecognizer(model, SAMPLE_RATE)
with sd.RawInputStream(
samplerate=SAMPLE_RATE,
blocksize=8000,
channels=1,
dtype="int16",
callback=audio_callback,
):
print("开始监听,请说出「牛来」…… Ctrl+C 退出")
while True:
data = audio_queue.get()
if rec.AcceptWaveform(data):
result = json.loads(rec.Result())
text = result.get("text", "").replace(" ", "")
print("[识别]", text)
if "牛来" in text:
print(">>> 命中口令:牛来")
else:
# 未形成完整结果时,可以观察中间结果用于调试
partial = json.loads(rec.PartialResult()).get("partial", "")
if partial:
print("[半截]", partial)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("已退出")
代码逻辑并不复杂,但有几个细节值得注意。
sd.RawInputStream 的参数必须和 Vosk 的输入要求对齐:16kHz、单声道、int16。blocksize=8000 表示每次回调处理 8000 个采样点,也就是 0.5 秒的音频。这个值越小,响应越及时,但回调频率越高;越大则相反,实际项目可以按需调整。
audio_queue 是采集线程和识别主循环之间的缓冲。采集回调会高频触发,绝对不能在里面做耗时操作,所以只做入队,识别逻辑放在主循环里读队列。
rec.AcceptWaveform(data) 的返回值表示“这一块音频是否已经构成一个完整的识别结果”。为 True 时,rec.Result() 返回最终文本;为 False 时,可以用 rec.PartialResult() 获取中间结果。桌宠场景里只取最终结果,能减少半截词触发误动作的概率。
这里有一个 Vosk 的常见细节:中文识别结果里词与词之间会带空格,例如 牛 来,所以代码里做了 replace(" ", "") 处理后再判断是否包含「牛来」。
5.3 第三步:用语法约束缩小识别范围
全量识别会尝试识别所有可能的中文语句,这不是桌宠需要的。Vosk 的 KaldiRecognizer 支持传入一个 JSON 格式的语法列表,把识别结果限制在指定短语内。这样 CPU 开销更低,关键词命中也更稳定。
python复制# voice_pet/listen_wake.py
import json
import queue
import sys
import sounddevice as sd
import vosk
MODEL_PATH = "model-cn-small"
SAMPLE_RATE = 16000
model = vosk.Model(MODEL_PATH)
audio_queue = queue.Queue()
keywords = ["牛来", "闭嘴", "牛来闭嘴"]
def audio_callback(indata, frames, time_info, status):
audio_queue.put(bytes(indata))
def main():
# 传入语法后,识别结果会被限制在这几个短语里
rec = vosk.KaldiRecognizer(model, SAMPLE_RATE, json.dumps(keywords))
with sd.RawInputStream(
samplerate=SAMPLE_RATE,
blocksize=8000,
channels=1,
dtype="int16",
callback=audio_callback,
):
print("监听关键词:", keywords)
while True:
data = audio_queue.get()
if rec.AcceptWaveform(data):
text = json.loads(rec.Result()).get("text", "").replace(" ", "")
if not text:
continue
print("[关键词命中]", text)
if "牛来" in text:
print(">>> 触发桌宠闭嘴动作")
和第二步的差别只有两处:keywords 列表定义了允许识别的短语,KaldiRecognizer 构造时传入了 json.dumps(keywords)。其余代码完全一样。这一步跑通后,你已经有了一个本地离线关键词识别器,它现在只听得懂词表里的几句话。
5.4 第四步:把「闭嘴」动作绑定到桌宠进程
桌宠本体通常是带有动画渲染的独立进程。为了让语音识别模块和桌宠进程解耦,我给桌宠写一个最小 HTTP 控制服务,模拟“接收指令并改变牛的状态”。实际项目里,这个服务内部可以调用 Electron 的 IPC、pygame 的动画控制器,或者任何桌宠框架提供的接口。
python复制# voice_pet/pet_server.py
import argparse
from flask import Flask, jsonify, request
app = Flask(__name__)
# 模拟桌宠内部状态,实际项目里应替换为桌宠控制对象的引用
pet_state = {
"mooing": True, # 是否在播放叫声
"mood": "happy",
}
@app.route("/command", methods=["POST"])
def command():
payload = request.get_json(force=True)
cmd = payload.get("cmd", "")
if cmd == "shutup":
pet_state["mooing"] = False
print("[桌宠] 停止叫声")
elif cmd == "moo":
pet_state["mooing"] = True
print("[桌宠] 开始叫")
else:
return jsonify({"ok": False, "message": "unknown command"}), 400
return jsonify({"ok": True, "state": pet_state})
@app.route("/state", methods=["GET"])
def state():
return jsonify(pet_state)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=5000)
args = parser.parse_args()
# 只监听本机回环地址,不要暴露到局域网
app.run(host="127.0.0.1", port=args.port, debug=False)
然后修改识别脚本,让识别的关键词命中时,向这个服务发一条 POST 请求。
python复制# voice_pet/listen_wake_http.py
import json
import queue
import sys
import requests
import sounddevice as sd
import vosk
MODEL_PATH = "model-cn-small"
SAMPLE_RATE = 16000
PET_URL = "http://127.0.0.1:5000/command"
model = vosk.Model(MODEL_PATH)
audio_queue = queue.Queue()
keywords = ["牛来", "闭嘴", "牛来闭嘴"]
def audio_callback(indata, frames, time_info, status):
audio_queue.put(bytes(indata))
def send_pet_command(cmd: str) -> bool:
try:
resp = requests.post(PET_URL, json={"cmd": cmd}, timeout=2)
return resp.ok
except requests.RequestException as exc:
print("[pet] 命令发送失败:", exc)
return False
def main():
rec = vosk.KaldiRecognizer(model, SAMPLE_RATE, json.dumps(keywords))
with sd.RawInputStream(
samplerate=SAMPLE_RATE,
blocksize=8000,
channels=1,
dtype="int16",
callback=audio_callback,
):
print("监听关键词:", keywords, "| 桌宠服务:", PET_URL)
while True:
data = audio_queue.get()
if rec.AcceptWaveform(data):
text = json.loads(rec.Result()).get("text", "").replace(" ", "")
if not text:
continue
print("[关键词命中]", text)
if "牛来" in text:
if send_pet_command("shutup"):
print(">>> 桌宠已闭嘴")
else:
print(">>> 命令已识别,但桌宠服务未响应")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("已退出")
到这里,从声音到动作的完整链路已经打通:麦克风采集到「牛来」→ Vosk 本地识别出文本 → HTTP 请求发送 shutdown → 桌宠服务把 mooing 置为 False。
6. 运行结果与效果验证
验证时需要开两个终端。
终端一启动桌宠控制服务:
bash复制python voice_pet/pet_server.py --port 5000
预期看到 Flask 输出:
code复制 * Running on http://127.0.0.1:5000
先用 curl 手动测试一下 HTTP 接口,确认服务本身工作正常:
bash复制curl -X POST http://127.0.0.1:5000/command \
-H "Content-Type: application/json" \
-d '{"cmd": "shutup"}'
预期返回:
json复制{"ok": true, "state": {"mooing": false, "mood": "happy"}}
这表示接口能正常执行截停动作。
终端二启动语音识别监听:
bash复制python voice_pet/listen_wake_http.py
对着麦克风清楚地说出「牛来」,终端二预期输出:
code复制[关键词命中] 牛来
>>> 桌宠已闭嘴
同时终端一会打印:
code复制[桌宠] 停止叫声
如果你看到这两行输出,说明整个闭环已经成功。如果没有任何反映,先按这个顺序排查:终端一的 pet_server.py 是否还在运行;用 python voice_pet/check_audio.py 确认有没有默认输入设备;确认模型目录名与 MODEL_PATH 一致;最后再观察识别脚本里是否打印了 [半截],确认麦克风确实有声音进入。
7. 常见问题与排查思路
本地语音识别链路虽然不长,但出问题时,现象往往分布在采集、识别、通信三个环节。下面整理几类高频问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 启动时 sounddevice 报错找不到设备 | 系统没有默认输入设备,或麦克风驱动未安装 | 运行 python check_audio.py 查看设备列表 |
安装/启用麦克风驱动;在 RawInputStream 中显式指定 device 参数 |
| 识别结果总是为空 | 采集到静音、声道数或采样率不符、麦克风被占 | 打开 PartialResult() 打印中间结果 |
检查 16kHz、int16、channels=1;增大麦克风音量;关闭占用麦克风的应用 |
| 「牛来」被识别成其他词 | 发音不够清晰、口音差异、模型词表受限 | 观察 PartialResult() 中的中间结果 |
放慢语速;把同音/近音词加入语法词表;换更大的中文模型 |
| 桌宠收不到命令 | pet_server.py 未启动、端口冲突、URL 写错 |
先用 curl 手动请求接口 | 确认服务先启动;更换端口;检查 PET_URL |
| CPU 占用偏高 | 全量识别模型过大,或监听循环一直满载运行 | 观察任务管理器 CPU 曲线 | 改用 small 模型;启用语法约束;调大 blocksize |
| 语音响应有 1 到 2 秒延迟 | 音频缓冲过大、模型推理慢、打印日志过多 | 减少 blocksize,观察命中耗时 |
小模型 + 语法约束;去掉日志输出后再测 |
| 连续喊话时重复触发 | 一句话被识别成多次完整结果,没有做冷却 | 给命中逻辑加冷却时间 | 命中后 sleep 或记录时间戳,0.5 到 1 秒内不再触发 |
| 出现「牛来」「牛来」之间有空格导致没命中 | Vosk 中文结果默认带词间空格 | 打印原始 Result() 文本 |
对结果执行 replace(" ", "") 后再匹配 |
排错时最忌讳跳过采集环节直接怀疑模型。先确认声卡确实把音频送进来了,再确认识别脚本有文本输出,最后才排查通信。三个环节分开验证,问题定位会快很多。
8. 最佳实践与工程建议
跑通 demo 只是第一步。如果想把「牛来桌宠」做成一个日常可用、不烦人的工具,下面这些工程细节值得提前考虑。
关于唤醒词,建议别用太长的短语,也不要选日常对话里出现频率过高的词。唤醒词的本质是一个窄带分类问题,2 到 4 个字、发音区分度高、不容易和周围环境音混淆,是更稳的选择。「牛来」这个口令的辨识度就比“好的”“播放”这类高频词好得多。
关于识别策略,桌宠场景要明确区分“关键词识别”和“连续听写”。如果你只需要听懂几个动作,就不要让识别引擎跑全量中文模型。用语法约束把搜索空间缩小到指令集,既省 CPU,又降低误触发概率。后续新增指令时,只需同步更新 keywords 列表和动作映射表。
关于进程解耦,建议保持语音识别进程和桌宠进程互相独立。识别进程崩溃恢复时,桌宠动画不会跟着挂掉;桌宠更换框架时,语音识别部分完全不用改。HTTP 接口是目前最简单的解耦方式,也可以替换为 WebSocket、Unix socket 或消息队列,通信协议本身不影响识别逻辑。
关于安全边界,pet_server.py 这种控制服务必须绑定 127.0.0.1,不要设置为 0.0.0.0。即使只在本机使用,也建议在接口层限制请求体大小,并可选择性校验一个简单的 token。任何能控制桌宠动作的接口,理论上都存在被局域网内其他设备调用的风险,绑定回环地址是最基本的防护。
关于防抖和去重,语音识别不是一次性的。你喊一声「牛来」,可能连续几个音频块都包含该词,导致重复触发。在动作执行层加一个冷却时间,比如 0.5 到 1 秒内忽略同指令,能显著提升体验。
关于日志,开发时输出 PartialResult() 调试中间结果,观察系统把你说的话识别成了什么;生产环境只输出命中事件和异常,避免日志刷屏占用磁盘。
关于开机自启,桌宠通常希望常驻桌面。Windows 可以用任务计划程序,macOS 可以用 launchd,Linux 可以用 systemd user service。语音识别模块同样可以设置为开机自启,但建议加一个异常退出自动重启机制,避免麦克风被占用一次就永久失效。
关于模型文件,不要把解压后的模型提交进 Git 仓库。模型文件体积不小,且很容易重新下载,正确做法是提供一个下载脚本,让使用者自行准备模型目录。
9. 总结与后续学习方向
到这里,你已经完成了一个完整的本地语音交互项目:从麦克风采集,到本地离线识别,再到桌宠动作执行。这里面真正铺垫的关键能力,不是“让牛闭嘴”,而是你从此拥有了一套可复用的桌面端语音链路。换个词表、换一个动作映射,它就能变成语音关机、语音点歌、语音打卡工具。
如果还想往深走,有三个方向值得继续投入。第一是给桌宠加上本地语音合成(TTS),让它不只会听话,还会用语音回应你,这就构成完整的语音交互体验。第二是换用更轻量的离在线一体识别框架,比如 sherpa-onnx,把整套逻辑迁移到 Jetson 或 RK3588 这类嵌入式设备上,甚至可以接上本地部署的大语言模型,让桌宠从“听懂口令”升级为“能陪你聊天”。第三是继续优化识别体验,包括端点检测、噪声抑制、多语种识别,这些都是本地语音产品走向实用的关键环节。
把 AI 能力放回设备端,不是一个新概念,但它在桌面小工具上的落地才刚刚开始。让桌宠听懂「牛来」,就是这条路上最小、但最完整的一步。代码已经摆在上面了,接下来你可以先跑通它,然后想想你桌面上还有什么东西,值得喊一嗓子就听话。
