Learn Claude Code:CodeAgent 的最小架构——工具+权限+Hook扩展钩子
一个 Coding Agent 的最小内核:循环、工具、权限与 Hooks
一个 Coding Agent 的核心循环可能不到几十行,但一个可用系统绝不只是 while True。
这一篇从最底层开始,整理我对 Agent Loop、工具系统、权限系统和 Hooks 的理解。
一、Agent Loop:持续行动的最小闭环
普通大模型调用只有一次输入和一次输出:
用户问题 → 模型回答 → 结束
Agent Loop 则会识别模型是否请求调用工具:
用户问题
→ 模型请求 read_file
→ Harness 读取文件
→ 文件内容返回模型
→ 模型请求 edit_file
→ Harness 修改文件
→ 修改结果返回模型
→ 模型给出最终回答
伪代码如下:
def agent_loop(messages):
while True:
response = llm(messages=messages, tools=TOOLS)
messages.append(to_assistant_message(response))
tool_calls = extract_tool_calls(response)
if not tool_calls:
return extract_text(response)
tool_results = []
for call in tool_calls:
output = execute_tool(call)
tool_results.append(
make_tool_result(call.id, output)
)
messages.append({
"role": "user",
"content": tool_results,
})
这里最关键的不是循环语法,而是协议闭环:
tool_use_id <——> tool_result.tool_use_id
每个工具请求必须有对应结果,否则模型无法知道某次调用是否完成。
生产环境中,是否继续循环最好依据响应里是否真的出现 tool_use 块,而不是只相信某个停止原因字段,因为流式响应的状态可能尚未完全更新。
二、工具系统:工具本质上是带 Schema 的函数
工具通常包含两部分。
第一部分提供给模型:
{
"name": "read_file",
"description": "Read a UTF-8 text file from the workspace.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}
第二部分提供给 Harness:
def read_file(path: str) -> str:
...
二者通过分发表关联:
TOOL_HANDLERS = {
"read_file": read_file,
"write_file": write_file,
"edit_file": edit_file,
"bash": run_bash,
}
执行时不需要写大量 if-else:
handler = TOOL_HANDLERS[call.name]
output = handler(**call.input)
这种设计的关键价值是:新增工具不需要修改 Agent Loop。
新增一个工具只需:
- 添加工具 Schema;
- 注册处理函数。
循环、权限、日志和错误恢复逻辑都可以复用。
三、工具描述会直接影响 Agent 稳定性
工具不仅要能执行,还必须让模型容易理解。
不好的工具描述:
process_file:处理文件
模型不知道它是读取、修改、删除还是转换。
更好的描述:
edit_file:在指定 UTF-8 文本文件中,将唯一出现的 old_text
替换为 new_text;若 old_text 不存在或出现多次则返回错误。
一个工具应尽量具备:
- 单一职责;
- 清晰输入;
- 明确副作用;
- 可预测输出;
- 可验证错误;
- 尽可能少的隐式状态。
工具太粗,模型难以控制;工具太碎,调用次数和上下文成本又会迅速增加。工具粒度需要围绕任务场景权衡。
四、多工具调用不能简单地全部并发
模型可能在一次响应中请求多个工具:
read_file(a.py)
read_file(b.py)
write_file(result.md)
前两个读取可以并发,但写入必须等待读取结果和模型的下一步决策。更一般地说:
- 只读工具通常可以并发;
- 修改文件、数据库和外部状态的工具应谨慎串行;
- 相互有依赖的工具必须保留顺序。
一种较合理的做法是:
按原始顺序扫描调用
→ 将连续的并发安全工具组成 batch
→ batch 内并发
→ batch 之间串行
而不是粗暴地把所有调用扔进线程池。
同时,每个工具在执行前还应经过验证管线:
Schema 验证
→ 工具自己的参数校验
→ PreToolUse Hooks
→ 权限检查
→ 真正执行
五、权限系统:安全不能靠模型自觉
文件工具可以限制在工作目录内,但 bash 天然拥有更大的破坏能力。模型可能因为误解任务而执行:
rm -rf ...
sudo ...
git reset --hard
安全设计应在工具执行前完成,而不是在执行后补救。
可以将权限判断理解为三层闸门:
硬拒绝
→ 规则判断
→ 用户审批
硬拒绝
无论上下文如何都不能执行的行为,例如明显的系统破坏命令。
规则判断
根据工具、路径和参数判断风险,例如:
- 写入工作区外;
- 删除文件;
- 修改受保护配置;
- 调用带破坏性的 MCP 工具。
用户审批
规则判断认为需要确认时,暂停当前工具调用,让用户明确允许或拒绝。
生产系统的权限结果通常不只有 allow/deny,还可能包含:
allow 直接允许
deny 直接拒绝
ask 请求人工确认
passthrough 当前规则不处理,交给下一层
这里的重要原则是:
用户扩展的 Hook 不应绕过系统级 deny 规则。
即使某个 Hook 总是返回允许,系统的硬拒绝和安全规则仍应继续生效。
六、Hooks:扩展行为,但不污染核心循环
如果每增加一个功能都直接修改循环:
log_tool_call()
check_permission()
notify_user()
auto_git_add()
collect_metrics()
循环很快会变得无法维护。
Hooks 的作用是提供稳定扩展点:
UserPromptSubmit
PreToolUse
PostToolUse
Stop
实现可以非常简单:
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:
return result
常见用途如下:
| Hook | 典型用途 |
|---|---|
| UserPromptSubmit | 输入审计、补充上下文 |
| PreToolUse | 权限检查、参数修正、日志 |
| PostToolUse | 结果检查、指标统计 |
| Stop | 清理资源、生成会话摘要 |
Hooks 的设计价值不在于“回调函数”本身,而在于让核心循环保持稳定。
七、必须防止 Hook 自己制造死循环
例如 Stop Hook 认为任务还没完成,向模型追加一条消息要求继续。如果下一轮仍触发相同 Stop Hook,就可能无限续跑。
因此需要显式状态:
stop_hook_active = true
当系统已经因为 Stop Hook 重入循环时,后续 Stop Hook 应识别该状态,避免再次触发同一种续跑逻辑。
任何可以“阻止停止”或“重新注入任务”的扩展点,都应配置:
- 最大触发次数;
- 重入标志;
- 超时;
- 可审计日志。
八、总结
一个最小 Coding Agent 可以由一个循环和一个 Bash 工具构成,但真正可用的内核至少需要:
Agent Loop
+ Tool Registry
+ Input Validation
+ Concurrency Policy
+ Permission Pipeline
+ Hook System
其中最重要的设计原则是:
- 循环保持简单稳定;
- 能力通过工具扩展;
- 安全通过确定性代码实现;
- 横切逻辑通过 Hooks 挂载;
- 任何自动续跑机制都必须有边界。
理解这一层后,后续的记忆、任务、多 Agent 和 MCP 都只是继续挂载在同一个循环周围。
更多推荐

所有评论(0)