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 Map)
s02 和 s04 是同构的:
| 主循环不认识什么 | 靠哪张表 | |
|---|---|---|
| s02 | 任何具体工具 | TOOL_HANDLERS |
| s04 | 任何具体钩子 | 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:response=llm(messages)# 第 2 步messages.append({"role":"assistant","content":response["content"]})# 第 3 步ifresponse["stop_reason"]!="tool_use":return# ← 唯一出口results=[]# 第 4 步forblockinresponse["content"]:ifblock["type"]=="tool_use":output=run_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无限增长,Ctrl+C才能停。亲眼看到"没有退出条件"有多可怕。 - 把工具的返回文本改成
"rm -rf test"相关 → 观察[权限拒绝]如何被当作正常结果回填,循环并不中断。
1.6 一个关键细节
block.input["command"]说明模型输出的是结构化参数,不是纯文本。这就是为什么第 2 步要传tools=TOOLS——模型得先知道每个工具的参数 schema,才能填对。
二、s02 Tool Use:工具注册表
2.1 它要解决的问题
模型说"我要调用get_time",程序怎么知道该执行哪段代码?
错误答案(很多人第一反应):写一串 if/elif。
ifname=="run_bash":run_bash(**params)elifname=="get_time":get_time(**params)# 加一个工具就要改一次循环 ← 灾难正确答案:查表。
2.2 核心代码
TOOL_HANDLERS={"run_bash":run_bash,"get_time":get_time,"count_lines":count_lines,}defrun_tool(name,params):handler=TOOL_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:return"allow","只读工具,无副作用"ifname=="run_bash":low=str(params.get("command","")).lower()ifany(dinlowfordinDANGEROUS):return"deny","包含破坏性操作(不可逆)"ifany(low.startswith(s)orf"{s}"inlowforsinSAFE):return"allow","只读命令,安全"return"ask","未识别命令,需要用户确认"return"ask","未知工具,默认需要确认"| 决策 | 含义 | 例子 |
|---|---|---|
allow | 放行 | dir /b、get_time |
deny | 直接拦死,连问都不问 | rm -rf、format、shutdown |
ask | 交给用户拍板 | curl、pip install、未识别的命令 |
为什么必须三态?因为很多操作既不该无条件放行,也不该一刀切拦死。curl联网:可能是正当需求,也可能在偷传数据——只有用户能判断。三态设计把决定权交给了最终用户,而不是写代码的人。
3.3 三个硬性原则
① 权限门必须在 handler 执行【之前】
defrun_tool(name,params):decision,reason=check_permission(name,params)# 先判断ifdecision=="deny":returnf"[权限拒绝]{reason}"# ... 放行之后才轮到执行handler=TOOL_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)response=llm(messages)fire("after_llm",response)# ...fire("before_tool",name,params)result=handler(**params)fire("after_tool",name,params,result)主循环不认识任何一个具体钩子。加钩子只改register那一行。
4.3 钩子的三种能力
| 能力 | 例子 | 实现协议 |
|---|---|---|
| 观察 | 审计日志、上下文监控 | 做点事,return None |
| 改写 | 参数规范化(ls→dir /b) | 原地修改params字典 |
| 拦截 | 敏感信息保护(.env) | return一个字符串= 直接短路 |
“返回非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_HANDLERS |
| s04 | 任何具体钩子 | HOOKS |
同一个思想,用了两遍。理解了 s02,s04 就只是换个对象重来一次。
五、我的薄弱点清单(错题本)
这一章是本文最有价值的部分。以下是我在学习过程中确实答错、或掌握不牢的知识点,按重要性排序。
薄弱点 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:'NoneType'objectisnotsubscriptable从报错倒推根因的链条(必须形成肌肉记忆):
报错最后一行 → 定位到 agent_loop 的 response["content"] → 说明 response 是 None → response 来自 llm(messages) → 模型函数为什么返回 None? → 所有 if 都没命中 → 少了兜底 return30 秒定位根因,比记住任何语法都重要。
薄弱点 3(中等):替换代码块时,连带删掉了相邻的兜底逻辑
具体经过:把mock_llm的if turn == 2:块替换成turn == 2+turn == 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(权限)、s04(Hooks)、s11(错误恢复)全都变得理所当然。
薄弱点自评总表
| # | 薄弱点 | 严重度 | 已掌握? | 验证方式 |
|---|---|---|---|---|
| 1 | 把主体性/智能安到程序身上 | 高 | 待用实验自证 | 删turn == 3段落后运行 |
| 2 | .get()vs[],None的两面性 | 中 | 已理解 | 删注册表某行后运行 |
| 3 | 替换代码块时删掉兜底逻辑 | 中 | 已修复 | 重跑不再报NoneType |
| 4 | 模拟模式 vs 真实模式差异 | 低 | 已理解 | 对比--real行为 |
| 5 | 权限拒绝后"谁在换方案" | 高 | 待巩固 | 用自己的话复述一遍 |
六、四章横向对比速查表
6.1 四课解决的问题
| 课 | 核心问题 | 一句话答案 | 主循环改了吗 |
|---|---|---|---|
| s01 | 模型怎么"做事"? | 一个循环,靠stop_reason退出 | —— |
| s02 | 模型说调工具,怎么找到代码? | 查TOOL_HANDLERS注册表 | 没改 |
| s03 | 凭什么让它执行? | 执行前加一道三态权限门 | 没改 |
| s04 | 怎么在各节点加动作? | 主循环留 4 个插座,钩子可插拔 | 没改 |
注意最后一列的"没改"—— 这正是这四课最漂亮的地方:每一课都在不碰主循环的前提下,给 Agent 加上一层能力。
6.2 "错误即数据"四次出现的位置
| 位置 | 返回的文本 | 谁写的 |
|---|---|---|
| 权限拒绝 | [权限拒绝] ... | run_tool |
| 未知工具 | [错误] 未知工具: xxx | run_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.py | s01 | 五步循环全流程打印,观察messages增长 |
s02_tool_use.py | s02 | 工具注册表、未知工具兜底、工具异常兜底 |
s03_permission.py | s03 | 三态权限门、参数级粒度、模型被拒后自行换方案 |
s04_hooks.py | s04 | 四个钩子插座 + 钩子三能力(观察/改写/拦截) |
运行方式(零依赖、无需 API Key):
python s01_agent_loop.py# 模拟模式python s03_permission.py--ask# 交互式权限询问python s04_hooks.py--real# 真实模式(需 ANTHROPIC_API_KEY)结语
「工具与执行」这四课,表面上是四个功能,实际上是同一个思想的四次变体:
把会变动的东西从主循环里搬出去,放进一张表;把所有失败都变成可读的信息,回填给模型。
主循环从头到尾没为任何工具、任何权限规则、任何钩子改过一行——这就是这套架构最漂亮的地方。
而你这四课最大的收获,可能不是这四段代码,而是一个认知上的转变:
Agent 里,程序不是大脑,是血管和栏杆。智能在模型那一侧。