
Learn Claude Code s01–s04 学习笔记与薄弱点复盘学习对象learnclaudecode 渐进式教程「工具与执行」板块s01 Agent Loop / s02 Tool Use / s03 Permission / s04 Hooks学习方式不逐行读代码而是「跑通 → 改坏 → 回看」三步法每课都配一份零依赖、无需 API Key 的可运行 demo先看它怎么跑再动手改坏它最后回看教程原始代码。本文既是四课的知识总结也是一份自我错题本——最后两章专门记录我答错和掌握不牢的地方。目录零、全局心智模型最重要的一章一、s01 Agent Loop循环二、s02 Tool Use工具注册表三、s03 Permission权限门控四、s04 Hooks可插拔钩子五、我的薄弱点清单错题本六、四章横向对比速查表七、自测题附答案零、全局心智模型最重要的一章这四课真正的收获不是四段代码而是一个心智模型。它是理解后面全部 16 课的地基。0.1 一句话哲学程序只做三件事判断、执行、回填。决策权永远在模型手里。很多人包括我在学这四课时会本能地把智能和主体性安到程序身上觉得程序会自己想办法“自动降级”“重试”。这是最大的误区。真相是Agent 程序是一根管道 一串关卡它没有任何想办法的能力。所有聪明的行为都发生在模型那一侧。0.2 三份契约模型侧 vs 程序侧位置内容谁在用TOOLS定义工具说明书名字、描述、参数结构模型读据此决定调什么TOOL_HANDLERS名字 → 函数 的字典程序查表用工具函数本体真正干活的代码程序执行三者解耦但必须对齐。少任何一份工具就不工作——而且失败方式完全不同见 五、薄弱点。0.3 贯穿四课的设计哲学① 错误即数据Error as Data四课里出现了四次几乎一模一样的写法returnf[权限拒绝] ...# s01 / s03 —— 命令被拦returnf[错误] 未知工具:{name}# s02 —— 工具不存在returnf[工具执行异常] ...# s02 —— 工具抛异常returnf[钩子拦截] ...# s04 —— 被钩子拦下它们全都返回一句字符串而不是抛异常。为什么因为抛异常会中断循环而返回字符串会被包装成tool_result回填进messages模型下一轮就能读到我被拒了然后自己决定怎么办。Agent 的健壮性不来自提前想到所有失败而来自把所有失败都变成可读的信息。② 注册表思想Registry / Dispatch Maps02 和 s04 是同构的主循环不认识什么靠哪张表s02任何具体工具TOOL_HANDLERSs04任何具体钩子HOOKS共同点把会变动的东西从主循环里搬出去放进一张表。这是软件设计里开闭原则的具体落地——对修改关闭对扩展开放。Claude Code 有几十个工具、无数钩子主循环从没为任何单个工具或钩子改过。一、s01 Agent Loop循环1.1 它要解决的问题模型只能回答不能做事。怎么让它能查文件、跑命令、多轮推理答案一个循环。让模型一边做事一边把结果看回来。1.2 五步流程① 把用户问题作为第一条消息 messages [{role: user, content: query}] ② 把【整个 messages】 工具定义一起发给模型 ③ 追加模型回答检查 stop_reason 不是 tool_use → return循环结束唯一出口 是 tool_use → 继续 ④ 执行模型要求的工具收集结果 ⑤ 把结果包装成 tool_result以 user 身份追加回 messages → 回到 ②1.3 核心代码defagent_loop(query,llm):messages[{role:user,content:query}]# 第 1 步whileTrue:responsellm(messages)# 第 2 步messages.append({role:assistant,content:response[content]})# 第 3 步ifresponse[stop_reason]!tool_use:return# ← 唯一出口results[]# 第 4 步forblockinresponse[content]:ifblock[type]tool_use:outputrun_bash(block[input][command])results.append({type:tool_result,tool_use_id:block[id],content:output,})messages.append({role:user,content:results})# 第 5 步1.4 知识点messages列表就是 Agent 的全部记忆。唯一的可变状态。每轮 append 两样assistant 回答 工具结果。读代码时盯着它的每次 append就抓住了循环的心跳。循环没有任务完成的概念。它唯一的刹车是stop_reason ! tool_use。模型一直要工具它就一直转。所以真实产品必须有 max turns、上下文压缩等机制兜底。每轮把整个列表发出去。所以模型永远知道之前发生了什么——这是多轮的实现方式代价是 token 持续增长s08 的主题。工具结果用role: user回填。反直觉但很重要在 API 的消息协议里工具结果不是模型的回答而是环境对模型的输入。1.5 动手验证把mock_llm里if turn 1:改成if turn 1:→ 循环永不停机messages无限增长CtrlC才能停。亲眼看到没有退出条件有多可怕。把工具的返回文本改成rm -rf test相关 → 观察[权限拒绝]如何被当作正常结果回填循环并不中断。1.6 一个关键细节block.input[command]说明模型输出的是结构化参数不是纯文本。这就是为什么第 2 步要传toolsTOOLS——模型得先知道每个工具的参数 schema才能填对。二、s02 Tool Use工具注册表2.1 它要解决的问题模型说我要调用get_time程序怎么知道该执行哪段代码错误答案很多人第一反应写一串 if/elif。ifnamerun_bash:run_bash(**params)elifnameget_time:get_time(**params)# 加一个工具就要改一次循环 ← 灾难正确答案查表。2.2 核心代码TOOL_HANDLERS{run_bash:run_bash,get_time:get_time,count_lines:count_lines,}defrun_tool(name,params):handlerTOOL_HANDLERS.get(name)# ← 关键.get() 而不是 []ifhandlerisNone:returnf[错误] 未知工具:{name}可用工具:{list(TOOL_HANDLERS)}try:returnhandler(**params)# ← 关键** 解包字典为关键字参数exceptExceptionase:returnf[工具执行异常]{type(e).__name__}:{e}2.3 知识点① 函数是一等公民本课最重要的 Python 概念{get_time: get_time}—— 右边那个get_time不带引号它是函数对象本身被当作值存进了字典。理解这一点你就理解了为什么工具系统能设计得这么干净。函数可以像数字、字符串一样被传递和存储这是 Python 的灵魂也是所有插件系统的实现基础。② 加一个新工具 改三处主循环零改动改哪里作用谁看TOOLS列表加一条工具说明书模型写一个def xxx()具体实现程序TOOL_HANDLERS加一行注册程序主循环agent_loop一个字都不用改。③.get()与[]的区别易错点写法键不存在时后果TOOL_HANDLERS[name]抛KeyError程序直接崩TOOL_HANDLERS.get(name)返回None可以走if handler is None分支把错误变成数据一个中断程序一个变成可处理的数据。这个区别在写任何健壮代码时都会反复出现。④**params解包handler(**params)把{path: .}自动变成handler(path.)。字典与函数参数之间的无缝转换。对比handler(*params)是位置参数解包字典会被拆成 key 传给函数基本必然报错。⑤ 参数级错误处理工具炸了不能炸掉循环try/except包住handler(**params)异常转成字符串回填。这是错误即数据的第二次出现。2.4 动手验证加一个自己的工具如roll_dice三处各改一处跑通。然后确认主循环真的没动。把TOOL_HANDLERS里某一行删掉但TOOLS里定义还在 → 模型仍然会要求调用它程序返回未知工具。这就是三份契约必须对齐的实证。三、s03 Permission权限门控3.1 它要解决的问题模型要执行rm -rf /凭什么让它执行答案在工具执行【之前】加一道门所有工具共用。3.2 决策是三态不是布尔defcheck_permission(name,params):ifnameinREADONLY_TOOLS:returnallow,只读工具无副作用ifnamerun_bash:lowstr(params.get(command,)).lower()ifany(dinlowfordinDANGEROUS):returndeny,包含破坏性操作不可逆ifany(low.startswith(s)orf{s}inlowforsinSAFE):returnallow,只读命令安全returnask,未识别命令需要用户确认returnask,未知工具默认需要确认决策含义例子allow放行dir /b、get_timedeny直接拦死连问都不问rm -rf、format、shutdownask交给用户拍板curl、pip install、未识别的命令为什么必须三态因为很多操作既不该无条件放行也不该一刀切拦死。curl联网可能是正当需求也可能在偷传数据——只有用户能判断。三态设计把决定权交给了最终用户而不是写代码的人。3.3 三个硬性原则① 权限门必须在 handler 执行【之前】defrun_tool(name,params):decision,reasoncheck_permission(name,params)# 先判断ifdecisiondeny:returnf[权限拒绝]{reason}# ... 放行之后才轮到执行handlerTOOL_HANDLERS.get(name)returnhandler(**params)先问再干不是干完再问。如果判断放在执行之后命令已经跑完了才告诉你被拒绝了——文件已经删了。② 权限粒度是【参数级】的同一个run_bash工具rm -rf build→DENYdir /b→ALLOW所以权限不能只按工具名配必须能看到参数内容。这是本课最核心的认知。③ 判断与执行分离check_permission()不执行任何东西它只返回一个决策字符串。这个拆分让权限逻辑可以被单独测试、单独审计。3.4 错误即数据的第三次出现deny不抛异常而是return [权限拒绝]...。最精彩的观察demo 里模型被拒三次后自己改用了dir /b完成了任务。是程序让它换方案的吗不是。程序只是把三句被拒绝了回填进messages。模型下一轮读到这些文本自己决定删不了那就看看目录吧。你不需要写任何失败了该怎么办的逻辑。你只要把不行说清楚模型自己会绕路。3.5 动手验证--ask交互模式跑一次亲手输入y/n体会ask把决定权交给用户。把rm 从DANGEROUS挪到NEED_CONFIRM→ 从拦死变问你。思考真实产品里哪个更合理Claude Code 默认对删除操作是询问而非拦死因为用户有时确实想删东西。把check_permission挪到handler()后面 → 命令已执行完才说被拒。四、s04 Hooks可插拔钩子4.1 它要解决的问题想在 Agent 执行的各个节点加点动作——记日志、改参数、拦特殊命令、改结果——难道每次都要去改主循环答案主循环只预留插座具体插什么设备随时加。4.2 核心代码HOOKS{before_llm:[],# 调模型之前after_llm:[],# 调模型之后before_tool:[],# 执行工具之前after_tool:[],# 执行工具之后}defregister(hook_point,name,fn):HOOKS[hook_point].append((name,fn))注册钩子register(before_llm,上下文监控,context_watch)register(before_tool,审计日志,audit_hook)register(before_tool,参数规范化,normalize_hook)register(before_tool,敏感信息保护,secret_guard)register(after_tool,输出整理,tidy_result)主循环里只有四个触发点fire(before_llm,messages)responsellm(messages)fire(after_llm,response)# ...fire(before_tool,name,params)resulthandler(**params)fire(after_tool,name,params,result)主循环不认识任何一个具体钩子。加钩子只改register那一行。4.3 钩子的三种能力能力例子实现协议观察审计日志、上下文监控做点事return None改写参数规范化ls→dir /b原地修改params字典拦截敏感信息保护.envreturn一个字符串 直接短路“返回非None即拦截”——和 s02 的未知工具、s03 的权限拒绝是同一个协议。这是第四次见到它。4.4 两个精妙细节① 钩子改参数后权限检查看到的是【改写之后】的参数执行顺序参数规范化先把ls改成dir /b然后权限门判断dir /b。如果允许钩子改参数安全检查就【必须】放在改写之后—— 否则钩子可以把危险命令伪装成安全的绕过权限。真实系统里这个顺序是安全底线。② 钩子之间可以共享状态before_tool记开始时间after_tool算耗时——两个独立函数怎么传数据用模块级变量。这是钩子系统里的经典模式TIMING{}defstart_timer(name,params):TIMING[name]time.time()defreport_timer(name,params,result):print(f{name}耗时{time.time()-TIMING[name]:.2f}s)4.5 与 s02 的对称性本课最大的认知收获主循环不认识什么靠哪张表s02任何具体工具TOOL_HANDLERSs04任何具体钩子HOOKS同一个思想用了两遍。理解了 s02s04 就只是换个对象重来一次。五、我的薄弱点清单错题本这一章是本文最有价值的部分。以下是我在学习过程中确实答错、或掌握不牢的知识点按重要性排序。薄弱点 1严重把主体性和智能安到程序身上踩坑次数3 次。场景我的错误回答正确理解s01 命令被权限拦截“程序自己换个方案继续执行”程序只return一句拒绝文本换方案的是模型s02 调用不存在的工具“程序可能会去自己查询吧查完之后再运行”程序只查表 返回错误字符串它连查询这个动作都没有s03 模型改用安全命令“重试机制干的”代码里根本没有重试机制是模型的剧本在换方案为什么我会反复踩这个坑因为我们平时用的软件浏览器、Word、资源管理器都是程序在替你做决定——点删除程序自己就把文件删了。这个经验让我形成直觉“程序是能动的主体”。但 Agent 是个新物种程序退化成了管道 关卡智能全在模型那一侧。正确的锚定句请背下来程序只做三件事判断、执行、回填。凡是程序做的代码里一定能找到对应那几行找不到就是模型做的。自证实验我要亲手做一次删掉mock_llm里if turn 3:那一整段就是要求dir /b的那段重新运行。如果程序真有重试/降级机制那么删除后它应该自己去执行dir /b因为任务还没完成嘛。但它不会——它安安静静地结束了。这证明换成dir /b的是模型的剧本不是程序。这个知识点为什么关键s04 Hooks 是在判断前后加观察点s11 Error Recovery 会学错误重试但那是在程序里显式写的循环不是凭空长出来的s17 Autonomous Agents 的自主循环同理。薄弱点 2中等.get()与[]的区别以及None的两面性我的回答“.get() 返回 None所以保证了程序继续执行” —— 方向对但没说到根上。准确表述写法键不存在时后果TOOL_HANDLERS[name]抛KeyError程序直接崩TOOL_HANDLERS.get(name)返回None走if handler is None分支把错误变成数据关键词是KeyErrorvsNone—— 一个中断程序一个变成可处理的数据。反向联想也是我的坑函数没有return时默认返回None读它的属性就会炸TypeError:NoneTypeobjectisnotsubscriptable从报错倒推根因的链条必须形成肌肉记忆报错最后一行 → 定位到 agent_loop 的 response[content] → 说明 response 是 None → response 来自 llm(messages) → 模型函数为什么返回 None → 所有 if 都没命中 → 少了兜底 return30 秒定位根因比记住任何语法都重要。薄弱点 3中等替换代码块时连带删掉了相邻的兜底逻辑具体经过把mock_llm的if turn 2:块替换成turn 2turn 3时把末尾那个end_turn兜底return也一起删了导致程序在 turn 4 崩溃。教训替换代码块前先扫一眼上下文边界确认相邻结构兜底 return、else分支、循环结束语句是否该保留。这个兜底return的双重身份它既是防御性编程防止函数返回None也真实地扮演了模型说我干完了的角色—— 循环正是从这里正常退出的。薄弱点 4易忽略模拟模式和真实模式的行为差异我的困惑明明在TOOLS里加了roll_dice定义为什么跑起来毫无变化原因模拟模式下的模型根本不读TOOLS。mock_llm就是一段写死的 if/else 剧本。模拟模式真实模式模型响应脚本预设真实推理读TOOLS定义吗不读读新增工具后要改哪前 3 处 mock_llm剧本只改前 3 处主要用途零成本观察循环结构验证真实行为关键区分给模型看的说明书TOOLS和程序里的实现函数 注册表是两份独立的东西。漏注册表 → 报未知工具漏函数 → 报错漏TOOLS定义 → 真实模式下模型根本不知道有这个工具。薄弱点 5概念模糊对权限拒绝与循环继续的关系理解不清我的初答“工具被拒绝后程序的处理方式是自己换个方案继续执行。”正确理解程序里没有任何换个方案的代码。翻遍run_bash和agent_loop找不到一句try: other_plan()。它只做了两件事return[权限拒绝]...# 返回一句文本messages.append(...)# 回填continue# 循环继续是模型在下一轮看到我被拒了自己决定换方案。结论控制权始终在模型手里程序只负责传话和设栏杆。想明白这一点s03权限、s04Hooks、s11错误恢复全都变得理所当然。薄弱点自评总表#薄弱点严重度已掌握验证方式1把主体性/智能安到程序身上高待用实验自证删turn 3段落后运行2.get()vs[]None的两面性中已理解删注册表某行后运行3替换代码块时删掉兜底逻辑中已修复重跑不再报NoneType4模拟模式 vs 真实模式差异低已理解对比--real行为5权限拒绝后谁在换方案高待巩固用自己的话复述一遍六、四章横向对比速查表6.1 四课解决的问题课核心问题一句话答案主循环改了吗s01模型怎么做事一个循环靠stop_reason退出——s02模型说调工具怎么找到代码查TOOL_HANDLERS注册表没改s03凭什么让它执行执行前加一道三态权限门没改s04怎么在各节点加动作主循环留 4 个插座钩子可插拔没改注意最后一列的没改—— 这正是这四课最漂亮的地方每一课都在不碰主循环的前提下给 Agent 加上一层能力。6.2 错误即数据四次出现的位置位置返回的文本谁写的权限拒绝[权限拒绝] ...run_tool未知工具[错误] 未知工具: xxxrun_tool工具异常[工具执行异常] ...run_tool钩子拦截[钩子拦截] ...before_tool钩子四处的写法几乎完全一样。这就是整个 Agent Harness 的核心哲学。6.3 决策流程全景一次工具调用的完整生命周期模型返回 tool_use(name, params) ↓ 【s04】before_tool 钩子依次执行 ├─ 改写参数原地改 params 字典 └─ 返回字符串 → 拦截直接结束跳过下面全部 ↓ 【s03】check_permission(name, params) —— 检查的是【改写后】的参数 ├─ deny → return [权限拒绝] ← 结束 ├─ ask → 问用户被拒 → return ← 结束 └─ allow → 继续 ↓ 【s02】TOOL_HANDLERS.get(name) ├─ None → return [错误] 未知工具 ← 结束 └─ 有 → handler(**params) ├─ 抛异常 → return [工具执行异常] ← 结束 └─ 正常 → result ↓ 【s04】after_tool 钩子依次改写 result ↓ 包装成 tool_result → messages.append → 下一轮循环这张图就是 s01–s04 的全部成果。后面 16 课都是在它的某些节点上继续加东西。七、自测题附答案题目Agent 循环的退出条件是什么这个条件由谁决定给 Agent 加一个新工具需要改哪几处主循环要不要动模型调用一个注册表里不存在的工具从模型说要调到循环继续中间程序做了哪几步为什么TOOL_HANDLERS.get(name)不能写成TOOL_HANDLERS[name]权限决策为什么设计成三态allow/deny/ask而不是两态权限检查代码必须放在什么位置为什么rm -rf build被 DENY、dir /b被 ALLOW这说明权限的什么特性模型被拒绝三次后改用安全命令完成的任务。换方案是谁做的主循环预留了几个钩子插座分别在哪钩子拦截的实现协议是什么为什么权限检查必须发生在钩子改写参数【之后】在TOOLS里加了工具定义但模拟模式下毫无变化为什么答案点击展开stop_reason ! tool_use。由模型决定——程序只是读到这个值然后return。三处TOOLS加定义、写实现函数、TOOL_HANDLERS加注册。主循环不用动。真实模式自动生效模拟模式还要改mock_llm剧本。①TOOL_HANDLERS.get(name)查表 → ② 返回None→ ③ 命中if handler is None→ ④ 返回[错误] 未知工具...字符串 → ⑤由agent_loop包装成tool_result回填messages→ ⑥ 循环继续。[]在键不存在时抛KeyError直接中断程序.get()返回None才能走兜底分支把错误变成可处理的数据。因为很多操作既不该无条件放行也不该一刀切拦死。ask把决定权交给最终用户而不是写代码的人。必须放在handler()执行之前。放在之后会导致命令已经执行完了才报告被拒绝——破坏已经发生了。权限粒度是参数级的不能只看工具名必须能看到参数内容。模型自己。程序只把三句拒绝信息回填进messages模型读到后自己换方案。程序只会三件事判断、执行、回填。四个before_llm/after_llm/before_tool/after_tool。钩子返回一个字符串即表示拦截该字符串直接作为工具结果返回None表示放行。因为钩子能改参数。如果权限检查在改写之前钩子就能把危险命令伪装成安全的绕过权限。这是安全底线。因为模拟模式的mock_llm根本不读TOOLS它只是一段写死的 if/else 剧本。只有真实模式下模型才会读工具定义。附四份可运行 Demo文件对应课程演示内容s01_agent_loop.pys01五步循环全流程打印观察messages增长s02_tool_use.pys02工具注册表、未知工具兜底、工具异常兜底s03_permission.pys03三态权限门、参数级粒度、模型被拒后自行换方案s04_hooks.pys04四个钩子插座 钩子三能力观察/改写/拦截运行方式零依赖、无需 API Keypython s01_agent_loop.py# 模拟模式python s03_permission.py--ask# 交互式权限询问python s04_hooks.py--real# 真实模式需 ANTHROPIC_API_KEY结语「工具与执行」这四课表面上是四个功能实际上是同一个思想的四次变体把会变动的东西从主循环里搬出去放进一张表把所有失败都变成可读的信息回填给模型。主循环从头到尾没为任何工具、任何权限规则、任何钩子改过一行——这就是这套架构最漂亮的地方。而你这四课最大的收获可能不是这四段代码而是一个认知上的转变Agent 里程序不是大脑是血管和栏杆。智能在模型那一侧。