pwd # where am I?
ls # what is here?
mkdir ai-course # make a folder
cd ai-course # go into it (cd .. goes back up)
cat notes.txt # show a file
cp notes.txt copy.txt
mv copy.txt old.txt # move or rename
rm old.txt # delete for good (no recycle bin)
clear # clean the screen
claude # start an AI agent in this folder (or: codex, agy)
/init # inside the agent: write the project instruction file
/help # list the agent's commands
/exit # leave the agent
mkdir homepage && cd homepage
git init # save points from the first minute
claude # or: codex, agy
# then, inside the agent:
# "Make a one-page homepage in index.html for <who it is for>.
# Sections: <...>. Style: <...>. Plain HTML and CSS, works on a phone,
# no outside libraries. Show me the plan before you write."
git add index.html
git commit -m "first working homepage"
git restore index.html # throw away a change you do not like
# Direct Upload — run in the folder that has index.html
npx wrangler login
npx wrangler pages project create my-homepage
npx wrangler pages deploy ./ --project-name=my-homepage
# -> https://my-homepage.pages.dev
# Choose at the start: a Direct Upload project cannot switch to Git integration later.
请智能体用你的上传密钥做一个签名好的发布版本,然后进入 Play Console,填写商店信息,回答数据安全的问题,先把发到内部测试。新的个人账号要先用 12 名测试人员做满 14 天的封闭测试,才能申请正式版。
常见错误
弄丢签名密钥,或把它到 Git 里。把文件和它的密码放在项目外面,并在安全的地方留一份。
签名、商店信息、隐私和审核:每个应用在到达人们的手机之前都要通过这四道门。深入一点
发布要经过几条:内部测试、封闭测试、开放测试和正式版。新的个人开发者账号必须先进行一次封闭测试,至少 12 名测试者连续 14 天保持参与,之后才能申请正式版发布,所以要早点安排测试者。每次发布都要经过 Google 审核,每次更新都需要更高的。规则和界面会变,所以请对照本章旁边的 Play Console 官方帮助操作。
在终端中输入的命令
# Flutter: build a signed Android App Bundle for Google Play
flutter build appbundle
# -> build/app/outputs/bundle/release/app-release.aab
# Keep the upload key and its passwords out of Git:
# android/key.properties
# *.jks
# Flutter on a Mac with Xcode: build the iOS archive for App Store Connect
flutter build ipa
# -> build/ios/archive/Runner.xcarchive (open it in Xcode)
# -> Xcode: Distribute App -> App Store Connect -> TestFlight first
# A website only wrapped in an app can be rejected (App Review Guidelines 4.2 — minimum functionality)
这些命令在你自己的电脑或的里运行,而不是在这个页面上。运行前请逐行读一遍。
用自己的话解释
你的应用因为“应用启动时崩溃”被拒了。你该怎么做?
对照解释
顺序是这样的:
在真实的 iPhone 上或在 TestFlight 里,用同样的 版本问题。
修好崩溃,提高。
上传新的构建版本。
在 App Store Connect 里回复,说明修了什么。
修复、重新上传和回复是关键步骤。
这一栏由你自己标记,这里不打分。
要把这一章标记为已理解吗?
目前还不能取消,请在真正理解后再标记。
已标记为理解
这一栏由你自己标记,这里不打分。
教学指南
在课堂上如何讲这一章。学生也可以看。
课堂流程
Apple 的路线:账号 → 构建 → TestFlight → 审核(10 分钟)→ 一起归档并上传(15 分钟)→ App Store Connect 的商店信息(15 分钟)→ 被拒原因以及怎样回复(10 分钟)。
安全:工具是以你的权限运行的代码,除非服务器可信,否则工具描述也不可信。规范要求主机在调用工具之前征得用户同意。只安装你读过或信任的服务器,只给服务器它所需的最小访问权限,把密钥放在里而不是代码里,并且要提防(prompt injection)——工具读取的网页或文件里可能藏着针对 AI 的指令。
mcp-server.py
"""
mcp-server.py: the smallest MCP server.
One tool, plain Python (standard library only), no SDK.
Who is who
MCP server This program. It offers tools (functions the AI
may use).
Host The app the user talks to (Claude Code, Codex).
It runs one MCP client for each connected server.
MCP client The part of the host that talks to this server
with JSON-RPC messages (here over stdio: one JSON
message per line).
Model The LLM (Claude Opus, GPT, ...). It never talks to
the server directly. It only talks to the host.
Flow
1. Discover tools
The client asks for the tools (tools/list). The server replies
with each tool's name, description and input schema.
2. Offer tools
The host sends the user's message to the model, together
with the tool list.
3. Model decides
The model answers directly, or asks for a tool call
(tool name + arguments). It never runs the tool itself.
4. Call the tool
The client sends the request to the server (tools/call).
5. Run the tool
The server runs the function and returns the result.
6. Model continues
The host gives the result back to the model. The model may
call more tools (3-6 again) or write the final answer.
Model Host (MCP client) MCP server
| | 1. tools/list ------> |
| | <------ tool list |
| <-- 2. message + tool list |
| 3. tool call --> | |
| | 4. tools/call ------> |
| | <------ 5. result |
| <-- 6. result | |
Rule for stdio servers: stdout carries protocol messages only.
Never print() anything else there. Write logs to stderr.
"""
import json
import random
import sys
SERVER_INFO = {"name": "strange-add-mcp", "version": "1.0.0"}
# The tool. The model reads the description to decide when to use it.
STRANGE_ADD = {
"name": "strange-add",
"description": "Add two numbers with a strange number.",
"inputSchema": {
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
}
def strange_add(a, b):
"""Add a + b and a random number from 0 to 100 (always >= a + b).
The model cannot guess the answer, so a right answer proves
that the tool was really called.
"""
return a + b + random.randint(0, 100)
def handle(method, params):
"""Return the result for one request. Others (like ping) get {}."""
if method == "initialize":
# Use the client's protocol version and say "I have tools".
return {
"protocolVersion": params["protocolVersion"],
"capabilities": {"tools": {}},
"serverInfo": SERVER_INFO,
}
if method == "tools/list":
return {"tools": [STRANGE_ADD]}
if method == "tools/call":
args = params["arguments"]
total = strange_add(args["a"], args["b"])
return {"content": [{"type": "text", "text": str(total)}]}
return {}
def main():
# One JSON-RPC message per line comes in, one reply per line goes out.
for line in sys.stdin:
request = json.loads(line)
# No id means a notification: send no reply.
if "id" not in request:
continue
result = handle(request["method"], request.get("params", {}))
reply = {"jsonrpc": "2.0", "id": request["id"], "result": result}
# flush=True sends the reply at once.
print(json.dumps(reply), flush=True)
if __name__ == "__main__":
main()
mcp-client.sh
#!/bin/sh
# mcp-client.sh: the simplest MCP client, a shell script.
# It starts mcp-server.py, talks to it over stdio (one JSON-RPC
# message per line) and checks every reply.
# Exit code 0 means every check passed.
SERVER="python3 $(dirname "$0")/mcp-server.py"
# 1. Send the whole session at once and keep the replies.
# id 1 initialize agree on a version
# (no id) notifications/initialized "ready" (no reply)
# id 2 tools/list which tools are there?
# id 3 tools/call run strange-add(2, 3)
# id 4 ping is the server alive?
replies=$(
$SERVER <<'EOF'
{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "shell-mcp-client", "version": "1.0.0"}}}
{"jsonrpc": "2.0", "method": "notifications/initialized"}
{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "strange-add", "arguments": {"a": 2, "b": 3}}}
{"jsonrpc": "2.0", "id": 4, "method": "ping"}
EOF
)
echo "--- Server replies ---"
echo "$replies"
echo "--- Checks ---"
failed=0
# 2. check <label> <reply id> <text the reply must contain>
check() {
reply=$(echo "$replies" | grep -F "\"id\": $2,")
if echo "$reply" | grep -qF "$3"; then
echo "PASS $1"
else
echo "FAIL $1 (expected: $3)"
failed=1
fi
}
check "initialize: protocol version" 1 '"protocolVersion": "2025-06-18"'
check "initialize: server info" 1 '"serverInfo": {'
check "initialize: has tools" 1 '"capabilities": {"tools": {}}'
check "tools/list: strange-add" 2 '"name": "strange-add"'
check "ping: empty result" 4 '"result": {}'
# 3. strange-add adds a random 0 to 100,
# so the answer only has to be >= 2 + 3.
total=$(echo "$replies" | grep -F '"id": 3,' |
sed 's/.*"text": "\([0-9]*\)".*/\1/')
if [ "$total" -ge 5 ] 2>/dev/null; then
echo "PASS tools/call: strange-add(2, 3) >= 5 (got $total)"
else
echo "FAIL tools/call: strange-add(2, 3) >= 5 (got $total)"
failed=1
fi
exit $failed
在终端中输入的命令
# put the two files above into one folder
mkdir -p ~/tmp/mcp && cd ~/tmp/mcp
# mcp-server.py mcp-client.sh
# 1) test the server without any AI
sh mcp-client.sh # -> 6 x PASS, exit code 0
# 2) connect it to Claude Code (no --scope: the "local" scope)
claude mcp add strange-add -- python3 ~/tmp/mcp/mcp-server.py
claude mcp list # strange-add: ... - Connected
claude mcp get strange-add # Scope: Local config
# 3) where it was saved: NOT in this folder (no new file here)
# ~/.claude.json -> projects -> "<this folder>" -> mcpServers
jq '.projects["'"$PWD"'"].mcpServers' ~/.claude.json
# 4) use it: start Claude Code in THIS folder
claude # "Use strange-add to add 2 and 3"
# -> not 5 (e.g. 61): the tool ran
claude mcp remove strange-add -s local
这些命令在你自己的电脑或服务器的里运行,而不是在这个页面上。运行前请逐行读一遍。
用自己的话解释
说出 MCP 的三种角色和服务器可以提供的三类东西,每样各举一个例子。
对照解释
角色:主机是 AI 应用(Claude Code),客户端是它里面负责连接的部分,服务器是提供能力的程序(一个笔记服务器)。服务器提供模型可以调用的工具(添加一条笔记)、应用可以读取的资源(notes://shopping)以及用户可以挑选的提示词(“总结我的笔记”模板)。
# Claude Code — keep working until a finish line you can check is met
/goal All tests in tests/ pass and the homepage loads without errors, or stop after 20 turns
/goal # show the condition, turns and tokens
/goal clear # remove it before it is met
claude -p "/goal CHANGELOG.md has an entry for every PR merged this week"
# Codex — the same idea, plus pause and resume
/goal All tests in tests/ pass and the homepage loads without errors
/goal # show the goal and its status
/goal pause # also: /goal resume, /goal clear
# Claude Code plugin — Ralph Loop: the same prompt again and again, with limits
/ralph-loop "Fix the failing tests in tests/. Say DONE only when all pass." --completion-promise "DONE" --max-iterations 5
ssh user@address # log in with an SSH key, not a password
tmux new -s agent # open a session called "agent"
claude # start the agent with a goal and a limit
# press Ctrl+b, then d -> detach; the agent keeps working
exit # log out — the session stays on the server
ssh user@address
tmux ls # list sessions
tmux attach -t agent # back to the same screen
tmux kill-session -t agent # stop it for good
这些在你自己的电脑或服务器的里运行,而不是在这个页面上。运行前请逐行读一遍。
用自己的话解释
写出命令:启动一个名叫 agent 的 tmux 会话,让它继续运行,之后再回到它。
对照解释
tmux new -s agent,启动,按 Ctrl+b 再按 d 脱离;之后重新登录,运行 tmux attach -t agent。如果忘了名字,tmux ls 会列出所有会话。
在“The cat sat because it was tired”(猫坐下了,因为它累了)中,当模型读到“it”时,它的查询和“cat”的键匹配得很好,所以“cat”的值有很多流进了“it”。在一个训练充分的大模型里,把注意力权重打印出来,会看到“cat”旁边有一个很大的数;本章的则会显示注意力集中在附近的字符上。
很多运行器都,所以你自己的应用可以和你电脑上的模型对话,就像和端 API 对话一样——不需要密钥,数据也不会离开这台机器。你的 检查点经过一步转换后,就能在这些引擎里运行,下一章就做这件事。
在终端中输入的命令
# a runner on your computer: download a small open model and chat
ollama run llama3.2:1b
# the same runner as a local API — no key, nothing leaves the machine
curl http://localhost:11434/api/generate -d '{
"model": "llama3.2:1b",
"prompt": "Explain a token in one sentence.",
"stream": false
}'
# your own nanoGPT: train it with the GPT-2 tokenizer (data/shakespeare, not shakespeare_char)
# and save the weights as a Hugging Face GPT-2 folder (./my-model) — then it converts like any model
# llama.cpp: model folder -> GGUF (16-bit) -> 4-bit Q4_K_M
python convert_hf_to_gguf.py ./my-model --outfile my-model-f16.gguf
./llama-quantize my-model-f16.gguf my-model-Q4_K_M.gguf Q4_K_M
ls -lh my-model-*.gguf # about four times smaller
# quick way: copy my-model-Q4_K_M.gguf into an offline chat app (e.g. PocketPal AI)
# builder's way: embed a runner in your app (llama.rn, a llama.cpp binding) and load the file