news 2026/6/26 7:00:19

【CC】Learn Claude Code s01-s04学习笔记

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【CC】Learn Claude Code s01-s04学习笔记

本文参考github项目:Learn Claude Code – 真正的 Agent Harness 工程

Agent 工具与执行系统

本文是 learn-claude-code 课程第二到第五节的笔记,覆盖 Agent 从「能跑起来」到「能安全地跑起来」的四个核心机制:Agent Loop → Tool Use → Permission → Hooks

这四个机制逐层递进:先有一个最小可运行的循环,再让模型能调用多种工具,接着加上安全闸门防止危险操作,最后用 hook 把逻辑从循环里解耦出去。读完本文,你应该能理解一个 AI Agent 的「执行层」是如何工作的。

Agent Loop:最小可运行的 Agent 内核

Agent Loop 是让模型能持续行动的最小运行框架。职责分工很清晰:

  • 模型负责决策:要不要调工具、调哪个
  • harness负责执行:调了就跑、结果喂回去

模型的stop_reason有两种关键状态:

stop_reason含义
"tool_use"模型说:“我需要执行某个工具才能继续”
"end_turn"模型说:“我回答完了,不需要再做任何操作”

核心代码

defagent_loop(messages):whileTrue:# 1. 调用 API 获取模型响应response=client.messages.create(model=MODEL,system=SYSTEM,messages=messages,tools=TOOLS,max_tokens=8000,)# 2. 将模型回复加入历史messages.append({"role":"assistant","content":response.content})# 3. 判断是否需要工具调用ifresponse.stop_reason!="tool_use":return# 正常结束,返回回复# 4. 执行模型要求的工具,收集结果results=[]forblockinresponse.content:ifblock.type=="tool_use":output=run_bash(block.input["command"])results.append({"type":"tool_result","tool_use_id":block.id,"content":output,})# 5. 将工具执行结果加入历史messages.append({"role":"user","content":results})
  • 代码位置:s01 的 agent loop 主循环
  • 关键点messages中有 3 类内容——用户原始 message + 模型回复的内容 +(如果调了工具)工具执行结果
  • 终止条件:当stop_reason不是"tool_use"时循环退出,模型给了最终回答

实际运行观察

试试这些 prompt:

  1. Create a file called hello.py that prints "Hello, World!"
  2. List all Python files in this directory
  3. What is the current git branch?

观察重点:模型什么时候调用工具(循环继续),什么时候不调用(循环结束)?

下面是一次实际运行的输出:

s01 >> Create a file called hello.py that prints "Hello, World!" $ echo 'print("Hello, World!")' > hello.py (no output) $ cat hello.py && python hello.py 'cat' 不是内部或外部命令,也不是可运行的程序或批处理文件。 $ type hello.py && python hello.py 'print("Hello, World!")' $ python hello.py (no output) $ more hello.py 'print("Hello, World!")' $ echo print("Hello, World!") > hello.py (no output) $ more hello.py print("Hello, World!") $ python hello.py Hello, World! 文件 `hello.py` 已创建,运行输出结果为 `Hello, World!`。

每一行$开头的黄色字就是模型调用tool_use(要求执行 bash),共8 次

Windows 适配:模型一开始用了cat(Unix 命令),在 Windows 上失败后自动切换成moretype,最终完成了任务。这就是 agent loop 的价值——失败 → 观察错误 → 换方式重试,和人调试的过程一样。

为什么第一轮echo没成功:第一次echo 'print("Hello, World!")' > hello.py带了单引号,文件里写进去的是字面量'print("Hello, World!")'(包含引号),所以python hello.py没有输出。模型后来用more查看文件内容发现了问题,改用不带引号的echo print("Hello, World!") > hello.py重写,才正确输出。

整个流程体现了 agent loop 的三个关键能力:

  1. 执行——调 bash 做事
  2. 观察——命令失败或结果不对时读输出/文件内容
  3. 修正——根据反馈换方式,直到成功

第一轮尝试:

第二轮修正:

  • 模型能看到完整历史,包括它自己犯过的错误(cat失败、引号问题)
  • 这个累积的上下文就是模型的"记忆"——它从错误中学习,换命令、查内容、修正,直到成功
  • 最终messages回到history,下次用户提问时继续追加,所以多轮对话的记忆也在

Tool Use:从一把刀到工具箱

s01 的 Agent 只有一个 bash 工具。读文件要cat,写文件要echo "..." > file.py,改文件要sed

问题在于:模型想的是"读这个文件",却要拼出cat path/to/file。多了一层翻译,浪费 token,还容易拼错。让工具语义更贴近模型意图,是这个阶段的改进目标。

组件之前 (s01)之后 (s02)
工具数量1 (bash)5 (+read, write, edit, glob)
工具执行硬编码run_bash()TOOL_HANDLERS 查表分发
路径安全safe_path 校验(仅 file tools)
循环while True+stop_reason与 s01 完全一致

唯一的变动在工具执行那 1 行:run_bash()替换为TOOL_HANDLERS[block.name]()查表分发。循环结构完全不变——这是设计上的关键点:新增能力不改循环逻辑

工具定义

TOOLS=[{"name":"bash","description":"Run a shell command.",...},{"name":"read_file","description":"Read file contents.",...},{"name":"write_file","description":"Write content to file.",...},{"name":"edit_file","description":"Replace text in file once.",...},{"name":"glob","description":"Find files by pattern.",...},]

每个工具有自己的实现函数:

defrun_read(path,limit=None):lines=safe_path(path).read_text().splitlines()iflimit:lines=lines[:limit]return"\n".join(lines)defrun_write(path,content):safe_path(path).write_text(content)returnf"Wrote{len(content)}bytes to{path}"defrun_edit(path,old_text,new_text):text=safe_path(path).read_text()ifold_textnotintext:return"Error: text not found"safe_path(path).write_text(text.replace(old_text,new_text,1))returnf"Edited{path}"defrun_glob(pattern):importglobasgreturn"\n".join(g.glob(pattern,root_dir=WORKDIR))
  • 关键点safe_path确保文件操作不会逃出工作目录
  • edit 的安全性:先检查old_text确实存在才替换,避免误改

工具分发

TOOL_HANDLERS={"bash":run_bash,"read_file":run_read,"write_file":run_write,"edit_file":run_edit,"glob":run_glob,}# 循环里只改了一行——从硬编码 run_bash 变成查表:forblockinresponse.content:ifblock.type=="tool_use":handler=TOOL_HANDLERS[block.name]# 查表output=handler(**block.input)# 调用results.append(...)

加一个工具 = 在TOOLS数组加一条 + 在TOOL_HANDLERS字典加一行。循环不变。

试试这些 prompt:

  1. Read the file README.md and tell me what this project is about
  2. Create a file called test.py that prints "hello", then read it back
  3. Find all Python files in this directory
  4. Read both README.md and requirements.txt, then create a summary file

观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?

Permission:安全不能靠信任,要靠代码

s02 的 Agent 有 5 个工具。file tools 受safe_path保护,但 bash 不受限制。让它"清理一下项目",可能执行rm -rf /

安全不能靠信任模型,要靠代码——在工具执行之前做判断。

每个工具调用经过三道闸门,顺序固定:硬拒绝优先,软询问次之,都没命中就放行。

闸门作用命中后
1. 拒绝列表永远禁止的操作(rm -rf /sudo直接拒绝,不执行
2. 规则匹配取决于上下文的操作(写工作区外、rm文件)交给闸门 3
3. 用户审批闸门 2 命中后,暂停等用户确认用户决定允许或拒绝

三道都没命中 → 直接执行。大部分日常操作走这条路。

闸门 1:硬拒绝列表

先查,命中就返回阻止信息:

DENY_LIST=["rm -rf /","sudo","shutdown","reboot","mkfs","dd if=","> /dev/sda",]defcheck_deny_list(command:str)->str|None:forpatterninDENY_LIST:ifpatternincommand:returnf"Blocked: '{pattern}' is on the deny list"returnNone
  • 关键点:纯字符串匹配,不依赖模型自觉。黑名单里的操作永远不可能执行

闸门 2:规则匹配

描述"什么时候需要问用户"。每条规则指定工具和检查条件:

PERMISSION_RULES=[{"tools":["write_file","edit_file"],"check":lambdaargs:not(WORKDIR/args.get("path","")).resolve().is_relative_to(WORKDIR),"message":"Writing outside workspace",},{"tools":["bash"],"check":lambdaargs:any(kwinargs.get("command","")forkwin["rm ","> /etc/","chmod 777"]),"message":"Potentially destructive command",},]defcheck_rules(tool_name:str,args:dict)->str|None:forruleinPERMISSION_RULES:iftool_nameinrule["tools"]andrule["check"](args):returnrule["message"]returnNone
  • 关键点:规则是声明式的,每条规则描述"什么情况下需要问"。新增危险场景只需加一条规则
  • 路径检查is_relative_to(WORKDIR)确保写文件不逃逸到工作区外

闸门 3:用户审批

规则命中后,暂停等用户输入:

defask_user(tool_name:str,args:dict,reason:str)->str:print(f"\n⚠{reason}")print(f" Tool:{tool_name}({args})")choice=input(" Allow? [y/N] ").strip().lower()return"allow"ifchoicein("y","yes")else"deny"

三道闸门串在一起,插在工具执行之前:

defcheck_permission(block)->bool:# 闸门 1: 硬拒绝ifblock.name=="bash":reason=check_deny_list(block.input.get("command",""))ifreason:print(f"\n⛔{reason}")returnFalse# 闸门 2 + 3: 规则匹配 → 用户审批reason=check_rules(block.name,block.input)ifreason:decision=ask_user(block.name,block.input,reason)ifdecision=="deny":returnFalsereturnTrue# 在 agent_loop 中——s02 的循环只加了一行:forblockinresponse.content:ifblock.type=="tool_use":ifnotcheck_permission(block):# ← 新增results.append({..."content":"Permission denied."})continueoutput=TOOL_HANDLERS[block.name](**block.input)# s02 原有results.append(...)
  • 关键点:权限检查插在"模型要求执行"和"实际执行"之间。被拒绝的工具返回"Permission denied.",模型可以看到这个反馈并尝试其他方式

Hooks:把逻辑从循环里解耦出去

s03 的权限检查、日志、大文件提醒都硬编码在循环里。每加一个新能力就要改循环,循环越来越胖。

Hooks 解决的就是这个问题:把"什么时候做什么"定义在循环外,循环只负责在关键节点触发

Hook 注册表

一个字典,事件名映射到回调列表:

HOOKS={"UserPromptSubmit":[],"PreToolUse":[],"PostToolUse":[],"Stop":[],}defregister_hook(event:str,callback):HOOKS[event].append(callback)deftrigger_hooks(event:str,*args):forcallbackinHOOKS[event]:result=callback(*args)ifresultisnotNone:# 返回值 ≠ None → hook 说"停"returnresultreturnNone
  • 关键约定:返回值None= 放行/继续;非None= 拦截/阻止
  • PreToolUse的非 None 返回值会阻止本次工具执行
  • Stop的非 None 返回值会强制续跑(注入一条消息让模型继续)

四个 Hook 节点

UserPromptSubmit:用户输入提交后、进入 LLM 前触发。可以拦截或修改输入,教学版只做日志演示:

defcontext_inject_hook(query:str)->str|None:"""Inject current working directory info into every prompt."""print(f"\033[90m[HOOK] UserPromptSubmit: working in{WORKDIR}\033[0m")returnNone# return None = no modification, let prompt throughregister_hook("UserPromptSubmit",context_inject_hook)

在主循环中,用户输入后立即触发:

query=input("s04 >> ")trigger_hooks("UserPromptSubmit",query)# ← 进入 LLM 之前history.append({"role":"user","content":query})agent_loop(history)

PreToolUse / PostToolUse:工具执行前后。s03 的权限检查现在包装成 PreToolUse hook,再加一个日志 hook 和一个大输出提醒:

# PreToolUse: 权限检查(s03 的逻辑,从循环移到 hook)defpermission_hook(block):ifblock.name=="bash":forpatterninDENY_LIST:ifpatterninblock.input.get("command",""):return"Permission denied by deny list"ifblock.namein("write_file","edit_file"):path=block.input.get("path","")ifnot(WORKDIR/path).resolve().is_relative_to(WORKDIR):choice=input(" Allow? [y/N] ").strip().lower()ifchoicenotin("y","yes"):return"Permission denied by user"returnNone# PreToolUse: 日志deflog_hook(block):print(f"[HOOK]{block.name}(...)")# PostToolUse: 大文件提醒deflarge_output_hook(block,output):iflen(str(output))>100000:print(f"[HOOK] ⚠ Large output from{block.name}")register_hook("PreToolUse",permission_hook)register_hook("PreToolUse",log_hook)register_hook("PostToolUse",large_output_hook)
  • 要点:权限检查从循环里的if not check_permission(block)变成了permission_hook回调。循环不知道"权限"的存在——它只管调用trigger_hooks

Stop:循环即将退出时触发(stop_reason != "tool_use")。教学版用于打印收尾统计:

defsummary_hook(messages:list)->str|None:"""Print a summary when the loop is about to stop."""tool_count=sum(1forminmessagesforbin(m.get("content")ifisinstance(m.get("content"),list)else[])ifisinstance(b,dict)andb.get("type")=="tool_result")print(f"\033[90m[HOOK] Stop: session used{tool_count}tool calls\033[0m")returnNone# return None = allow stop, return string = force continuationregister_hook("Stop",summary_hook)

在 agent_loop 中,退出前触发:

ifresponse.stop_reason!="tool_use":force=trigger_hooks("Stop",messages)# ← 退出之前ifforce:# hook returned a message → inject it and continuemessages.append({"role":"user","content":force})continuereturn

循环里只改了一处

s03 直接调用check_permission(block),s04 改为trigger_hooks("PreToolUse", block)

forblockinresponse.content:ifblock.type!="tool_use":continue# s03: if not check_permission(block): ...# s04: hook 替代硬编码blocked=trigger_hooks("PreToolUse",block)ifblocked:results.append({"type":"tool_result","tool_use_id":block.id,"content":str(blocked)})continuehandler=TOOL_HANDLERS.get(block.name)output=handler(**block.input)ifhandlerelsef"Unknown:{block.name}"trigger_hooks("PostToolUse",block,output)results.append({"type":"tool_result","tool_use_id":block.id,"content":output})

四个 hook 覆盖了 agent cycle 的关键节点:输入 → 执行前 → 执行后 → 退出。循环只负责调用trigger_hooks(),具体逻辑全在 hook 回调里。


总结

回顾四个阶段的演进,核心设计原则是清晰的:

阶段解决的问题设计原则
Agent Loop模型需要持续行动循环 + stop_reason 控制生命周期
Tool Use一个 bash 工具不够用查表分发,循环不变
Permission模型可能执行危险操作三道闸门,硬拒绝优先
Hooks循环越来越臃肿观察者模式,循环只管触发

每加一层能力,循环本身几乎不动——变动都在循环外面:

  • Tool Use:循环里只改一行(run_bash()TOOL_HANDLERS[...]()
  • Permission:循环里只加一行(check_permission(block)
  • Hooks:循环里只改一行(check_permission()trigger_hooks("PreToolUse")

这就是一个好的 harness 架构:核心循环保持简单,能力通过外部机制叠加。后续的 Sub-Agent、MCP 等特性,也遵循同样的设计思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 6:57:27

【计算机毕业设计案例】基于 SpringBoot 的书籍拍卖订单管理系统设计与实现 微信端图书拍卖交易运维管理系统设计与实现(程序+文档+讲解+定制)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

作者头像 李华
网站建设 2026/6/26 6:56:53

VBA技术资料500_VBA_将文件保存为最新版本

我给VBA的定义:VBA是个人小型自动化处理的有效工具。利用好了,可以大大提高自己的工作效率,而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套,分为初级、中级、高级三大部分,教程是对VBA的系统讲解&#…

作者头像 李华
网站建设 2026/6/26 6:54:30

扁线电机的 NVH:为什么它比圆线电机更安静,但依然有麻烦

导读:扁线电机在新能源汽车中快速普及,"NVH 表现更好"是常被提到的卖点。但这句话只说对了一半——扁线确实在某些维度上降低了噪声,也带来了新的麻烦。这篇文章从电磁力波、槽极配合、转矩脉动几个维度,把扁线电机 NVH…

作者头像 李华
网站建设 2026/6/26 6:51:52

融合梯度下降与区间算术的复杂约束求解框架设计与实践

1. 项目概述与核心思路拆解看到“基于Oracle梯度下降与区间算术的随机约束求解框架”这个标题,很多朋友可能会觉得它融合了数据库、优化算法和数学计算,听起来有点“缝合怪”的感觉。但在我实际接触和构建类似系统的经验里,这种组合恰恰是为了…

作者头像 李华