s01 Agent Loop --- 一个循环就是全部
"One loop & Bash is all you need" --- 一个工具 + 一个循环 = 一个 Agent。
- Harness 层:The Loop(模型与现实世界的第一座桥)
- 前序:无(起点)
一、要解决的问题
模型能"输出"一条 bash 命令,但输出完就停了:它不会自己执行,也不会根据执行结果继续推理。于是人被迫当中间层------手动跑、手动回贴结果。本章就是把这段"人工中转"自动化。
二、核心机制
一个 while True:模型要工具就继续,不要就停。判据是响应里有没有 tool_use 内容块。
| 信号 | 含义 | 循环动作 |
|---|---|---|
含 tool_use 块 |
模型请求调工具 | 执行 → 回灌结果 → 继续 |
不含 tool_use 块 |
模型没调工具 | 退出循环 |
三、关键代码(约 30 行)
python
def agent_loop(messages):
while True:
response = client.messages.create(
model=MODEL, system=SYSTEM, messages=messages,
tools=TOOLS, max_tokens=8000)
messages.append({"role": "assistant", "content": response.content})
tool_calls = [b for b in response.content if b.type == "tool_use"]
if not tool_calls: # 没有任何工具调用 → 本轮结束
return
results = []
for block in tool_calls:
output = run_bash(block.input["command"])
results.append({"type": "tool_result",
"tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results}) # 结果回灌
- 工具只有一个
bash:subprocess.run(shell=True, timeout=120),输出截断 50000 字符。 - 危险命令直接拦:
rm -rf /、sudo、shutdown、reboot、> /dev/。
四、关键洞察
- 判据落在"内容块"上 :只看
tool_use是否存在,因此循环永远不会追加一条空的 tool-result 消息。 - 模型决定(调不调工具、调哪个),Harness 执行(调工具、把结果追加为新消息)。
- 这 30 行不是智能本身,而是"让模型能持续行动"的最小运行框架。后面 16 章都在这个循环上加机制,循环本身永不变。
五、一句话总结
模型是司机,Harness 是车;s01 造出了车的底盘------一个让模型能"踩油门---看结果---再决策"的循环。
s02 Tool Use --- 加一个工具,只加一个 handler
"Add a tool, add just one handler" --- 循环不变,把新工具注册进调度表即可。
- Harness 层:Tool Dispatch(扩展模型能触达的范围)
- 前序:s01(循环不变)
一、要解决的问题
只有 bash 时,读文件要 cat、写文件要 echo >、改文件要 sed。模型想的是"读这个文件",却要自己翻译成命令------多一层翻译,既费 token 又易错。
二、核心机制
s01 循环一行不改 。唯一变化是工具执行那一行:从硬编码 run_bash() 换成调度查表 TOOL_HANDLERS[block.name](**block.input)。
加一个工具只需两步:
- 定义工具 :
TOOLS数组加一条(JSON schema,告诉模型"我能做什么")。 - 注册 handler :
TOOL_HANDLERS字典加一条映射。
三、关键代码
python
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.", ...},
]
TOOL_HANDLERS = {
"bash": run_bash, "read_file": run_read, "write_file": run_write,
"edit_file": run_edit, "glob": run_glob,
}
for block in tool_calls:
handler = TOOL_HANDLERS[block.name] # 查表
output = handler(**block.input) # 调用
- 文件工具引入
safe_path(限制在工作目录内),bash 仍不受限。 - 模型一次可能返回多个
tool_use------按response.content中的原始顺序逐个执行。
四、与 s01 的变化
| 组件 | s01 | s02 |
|---|---|---|
| 工具数 | 1(bash) | 5(+read/write/edit/glob) |
| 工具执行 | 硬编码 run_bash() |
TOOL_HANDLERS 查表 |
| 路径安全 | 无 | safe_path(仅文件工具) |
| 循环 | while True + tool_use | 与 s01 完全一致 |
五、关键洞察
- "加工具 = 加一条 schema + 一条映射" 是整套 Harness 的扩展范式,后续所有章节都沿用。
- 抽象点只有一处(调度表),没有为"未来可能的工具"做过度设计。
- bash 未被约束:
rm -rf /仍能跑------这正好引出 s03。
六、一句话总结
用一张 名字 → 函数 的调度表,把"单工具 Agent"升级为"多工具 Agent",而主循环纹丝不动。
s03 Permission --- 执行前先过权限闸门
"Check permissions before executing" --- 权限管线决定哪些操作需要审批。
- Harness 层:Permission(工具执行前的一道闸门)
- 前序:s02(循环不变)
一、要解决的问题
s02 有 5 个工具:文件工具有 safe_path 保护,bash 完全不受限 。让它"清理下项目",它可能真的跑 rm -rf /。安全不能靠"信任模型",必须靠代码------每次工具执行前做一次检查。
二、核心机制
s02 循环完全保留,只在工具执行前插入 check_permission()。每次工具调用按固定顺序过三道闸:
| 闸门 | 作用 | 命中后 |
|---|---|---|
| 1. 拒绝清单 | 永久禁止的操作(rm -rf /、sudo) |
直接拒绝,不执行 |
| 2. 规则匹配 | 依赖上下文(越界读写、rm 文件) |
交给闸门 3 |
| 3. 用户审批 | 闸门 2 命中后暂停等确认 | 用户决定允许/拒绝 |
三道都没命中 → 直接执行(绝大多数常规操作走这条路)。
三、关键代码
python
DENY_LIST = ["rm -rf /", "sudo", "shutdown", "reboot", "mkfs", "dd if=", "> /dev/sda"]
PERMISSION_RULES = [
{"tools": ["read_file", "write_file", "edit_file"],
"check": lambda args: not (WORKDIR / args.get("path","")).resolve()
.is_relative_to(WORKDIR),
"message": "Access outside workspace"},
{"tools": ["bash"],
"check": lambda args: contains_destructive_command(args.get("command","")),
"message": "Potentially destructive command"},
]
def check_permission(block) -> bool:
if block.name == "bash" and check_deny_list(block.input.get("command","")):
return False # 闸门 1
reason = check_rules(block.name, block.input)
if reason and ask_user(...) == "deny": # 闸门 2 → 3
return False
return True
- 破坏性命令用词边界正则 识别(
(?:^|[;&|()\n])\s*(?:rm|del)(?=\s|$|[;&|()])),所以model、delimiter、echo del test.txt不会误伤。
四、与 s02 的变化
| 组件 | s02 | s03 |
|---|---|---|
| 安全模型 | 无(信任模型) | 三闸权限管线 |
| 新函数 | --- | check_deny_list / check_rules / ask_user / check_permission |
| 循环 | 直接执行所有工具 | 执行前插入 check_permission() |
五、关键洞察
- 安全必须是代码,不能是信任:提示词约束挡不住模型。
- 章节自陈边界:拒绝清单用简单字符串匹配 ,只用于演示权限闸门的位置,不是完整的安全边界。
- 设计取"三闸固定顺序":硬拒绝优先,其次软询问,最后放行------越危险的判断越靠前。
六、一句话总结
在"模型说要执行"和"真的执行"之间插一道闸门;但演示级实现不等于生产级安全边界。
s04 Hooks --- 挂在循环上,别写进循环里
"Hang on the loop, don't write into it" --- 钩子在工具执行前后注入扩展逻辑。
- Harness 层:Hooks(不侵入循环的扩展点)
- 前序:s03(循环不变)
一、要解决的问题
s03 有了权限检查。但每加一个需求------"记录每次 bash 调用""写完后自动 git add"------都得改 agent_loop。循环会迅速膨胀成:
python
log_to_file(block); check_permission(block); notify_slack(block)
output = execute(block); auto_git_add(block) # 循环已面目全非
**想扩展的是 Agent 行为,改的却是循环本身。**循环应是稳定内核,扩展应挂在外面。
二、核心机制
s03 循环与权限逻辑完全保留,唯一变化:把 check_permission() 从循环体挪到一个钩子上。循环不再直接调任何检查函数,只调 trigger_hooks("PreToolUse", block),由注册表决定跑什么。
四个事件覆盖一个完整 Agent 周期:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| UserPromptSubmit | 用户输入后、进 LLM 前 | 输入校验、上下文注入 |
| PreToolUse | 工具执行前 | 权限检查、日志 |
| PostToolUse | 工具执行后 | 副作用(自动 git add)、输出检查 |
| Stop | 循环即将退出 | 清理、决定是否继续 |
三、关键代码
python
HOOKS = {"UserPromptSubmit": [], "PreToolUse": [], "PostToolUse": [], "Stop": []}
def register_hook(event, callback): HOOKS[event].append(callback)
def trigger_hooks(event, *args):
for callback in HOOKS[event]:
result = callback(*args)
if result is not None: # 返回值 ≠ None → 该钩子说"停"
return result
return None
控制流语义:
- PreToolUse 返回非 None → 本次工具调用被拦。
- Stop 返回非 None → 循环继续(返回值作为新消息注入)。
- UserPromptSubmit / PostToolUse 的返回值不影响控制流。
循环里只改一行:
python
blocked = trigger_hooks("PreToolUse", block) # s03: if not check_permission(block)
if blocked:
results.append({... "content": str(blocked)}); continue
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
trigger_hooks("PostToolUse", block, output)
四、与 s03 的变化
| 组件 | s03 | s04 |
|---|---|---|
| 扩展方式 | check_permission() 硬编码在循环 |
HOOKS 注册表 + trigger_hooks() |
| 循环 | 直接调 check_permission |
调 trigger_hooks("PreToolUse", ...) |
| 退出控制 | 无 | Stop 钩子可阻止退出 |
| 输入拦截 | 无 | UserPromptSubmit 可注入上下文 |
五、关键洞察
- 观察者模式:循环只发事件,回调自己决定行为,实现"扩展而不侵入"。
- 权限检查从"循环的一行"变成"钩子的一个回调"------职责归位:循环管调度,策略挂钩子。
- 四个钩子点正好是 Agent 周期的四个关键节点:输入 → 执行前 → 执行后 → 退出。
六、一句话总结
给循环留 4 个扩展点,让所有"想加的行为"以回调形式挂在外面,循环始终干净。
s05 TodoWrite --- 没有计划的 Agent 会跑偏
"An agent without a plan goes wherever the wind blows" --- 先列步骤再执行,复杂任务不易漏步。
- Harness 层:Planning(让 Agent 想清楚再动手)
- 前序:s04(复用工具调度 + 权限 + 钩子)
一、要解决的问题
给一个复杂任务:"把所有 Python 文件改成 snake_case,跑测试,修失败。"Agent 改 3 个文件 → 跑测试 → 发现 2 个失败 → 开始修。修着修着就忘了原始目标是"改名",测试失败占满了全部注意力。
对话越长越糟:工具结果不断填充上下文,稀释系统提示。10 步重构,走到第 3 步就开始即兴发挥,因为第 4--10 步已被挤出注意力。
二、核心机制
保留 s04 的工具调度、权限、钩子,新增 todo_write 工具 + 一个提醒计数器。
todo_write只更新计划状态,真正干活仍是已有工具。- 新工具走同一个
TOOL_HANDLERS[block.name]调度路径。 - 连续 3 轮 tool-use 没用
todo_write,Harness 在该轮结果里追加一条提醒。
三、关键代码
python
class TodoManager:
def __init__(self): self.items = []
def update(self, todos): # 校验后整体替换
validated = [...] # ≤20 项;content 非空;仅 1 项 in_progress
self.items = validated
return self.render() # [ ] pending / [>] in_progress / [x] completed
TODO = TodoManager()
def run_todo_write(todos):
out = TODO.update(todos); print(out); return out
TOOLS.append({"name": "todo_write", "description": "Create and manage a task list ..."})
TOOL_HANDLERS["todo_write"] = run_todo_write
提醒逻辑:
python
rounds_since_todo = 0 if used_todo else rounds_since_todo + 1
if rounds_since_todo >= 3:
results.append({"type": "text", "text": "<reminder>Update your todos.</reminder>"})
rounds_since_todo = 0
典型流程:列全部步骤(pending)→ 挑一步设 in_progress → 做完设 completed → 看下一个 pending → 继续。
四、与 s04 的变化
| 组件 | s04 | s05 |
|---|---|---|
| 工具数 | 5 | 6(+todo_write) |
| 计划 | 无 | 有状态 TODO 列表 + 提醒 |
| 循环 | 调度 + 钩子 | 同一调度路径,另加计数器与提醒注入 |
五、关键洞察
- todo_write 没有给 Agent 任何新的"执行能力",它加的是"计划能力"。 这是本章最核心的一句。
- 提醒是软约束(往结果里塞文本),不是硬拦截------把"该做计划了"作为一种上下文信号交给模型。
- 计划驻留内存,随会话结束消失------这正好引出 s10 的持久化任务图。
六、一句话总结
用一个 todo_write 工具把"先计划"变成模型可调用的动作,再靠轻量提醒维持它,从而对抗长任务中的注意力漂移。