一个 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。

新增一个工具只需:

  1. 添加工具 Schema;
  2. 注册处理函数。

循环、权限、日志和错误恢复逻辑都可以复用。

三、工具描述会直接影响 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

其中最重要的设计原则是:

  1. 循环保持简单稳定;
  2. 能力通过工具扩展;
  3. 安全通过确定性代码实现;
  4. 横切逻辑通过 Hooks 挂载;
  5. 任何自动续跑机制都必须有边界。

理解这一层后,后续的记忆、任务、多 Agent 和 MCP 都只是继续挂载在同一个循环周围。

更多推荐