Forge 开发笔记 02:最小 Coding Agent 是什么

Posted on Jun 21, 2026

如果要从零解释 Coding Agent,我不会先讲模型,也不会先讲 prompt。我会先讲循环。

因为 Agent 最容易被误解的地方,是把它看成“一次更聪明的模型调用”。但一个 Coding Agent 真正开始工作,是从循环开始的:整理上下文,请模型决定下一步,执行工具,把工具结果放回上下文,然后再次请模型决定下一步。

Forge 的 v0.1 就是为了展示这个最小闭环。

源码版本:arjenzhou/forge@6aa96ff,对应 v0.1.0-mvp

最小闭环的设计思路

最小 Coding Agent 至少要回答五个问题。

第一,谁来推进任务?模型只负责给建议,不能自己控制循环,所以需要一个 runner。

第二,模型每一轮能看到什么?用户目标、system prompt、历史工具结果都要被整理成 message,所以需要 context。

第三,模型如何行动?它不能直接读写文件,只能请求调用工具,所以需要 tool registry。

第四,系统如何知道发生过什么?Agent 行为不是一次函数返回,而是一串可复盘步骤,所以需要 trace。

第五,什么时候结束?最小版本里可以先让模型通过“没有 tool calls”表达结束意图,后面的 verifier 再把结束判断收紧。

这五个问题推导出 v0.1 的基本形状:runner 推进循环,context 提供输入,model 产生决策,tools 执行动作,trace 记录过程。

代码落点

理解这个分工以后,再看源码就清楚很多:

examples/demo_mock.py  # 最小 demo 入口
forge/runner.py        # agent loop
forge/tools.py         # 7 个核心编码工具
forge/context.py       # message history
forge/trace.py         # 执行记录

Runner 只是调度者

AgentRunner 初始化时并没有很多神秘对象。它拿到模型、系统提示、workspace、verifier、sandbox 和工具表:

class AgentRunner:
    def __init__(self, model, system_prompt=None, workspace_dir=".",
                 test_command=None, tool_registry=None, model_lock=None,
                 tool_lock=None):
        self.model = model
        self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
        self.workspace_dir = os.path.abspath(workspace_dir)
        self.verifier = Verifier(workspace_dir=self.workspace_dir,
                                 test_command=test_command)
        self.sandbox = LocalRestrictedSandbox(self.workspace_dir)
        self.tool_registry = tool_registry or registry

这里最重要的不是“模型”,而是职责边界:

  • 模型负责决定下一步;
  • runner 负责推进循环;
  • context 负责保存消息;
  • tool registry 负责执行动作;
  • verifier 负责判断完成;
  • sandbox 负责限制边界。

一个最小 Agent,不是一个大函数,而是一组角色分明的模块。

一轮循环发生了什么

run() 里,runner 每一轮都会做三件事:编译上下文、调用模型、处理工具或结束请求。

关键代码可以压成这样看:

messages = context.get_messages()
tool_definitions = self.tool_registry.tool_definitions

content, tool_calls = self.model.generate(messages, tool_definitions)
step.model_text_response = content

if tool_calls:
    context.add_assistant(content, tool_calls)
    result = self.tool_registry.execute(func_name, args,
                                        sandbox=self.sandbox,
                                        runner=self)
    context.add_tool_result(tc_id, func_name, result)
else:
    is_passed, report = self.verifier.verify()

这就是 Coding Agent 和普通聊天的根本区别。

普通聊天到 model.generate() 就差不多结束了;Coding Agent 还要处理 tool_calls。模型不能直接改文件,它只能声明“我要调用哪个工具,参数是什么”。真正的文件读写、命令执行、patch 应用,都在工具层完成。

为什么是 7 个工具

v0.1 的核心工具都在 forge/tools.py

list_files
search_code
read_file
apply_patch
edit_file_block
run_command
git_diff

这 7 个工具其实对应了一个开发者修 bug 的基本动作。

先看目录,找到可能相关的文件;再搜索关键词,定位代码位置;读文件理解上下文;修改代码;运行测试或命令验证;最后看 diff,确认自己改了什么。

工具注册也很直接。@tool 装饰器把 Python 函数放进 registry,registry 再从函数签名和 docstring 生成给模型看的 schema:

def register(self, func):
    name = func.__name__
    self.tools[name] = func

    sig = inspect.signature(func)
    properties = {}
    required = []

    for param_name, param in sig.parameters.items():
        if param_name in ("self", "cls", "sandbox", "runner"):
            continue
        properties[param_name] = {"type": "string"}

注意这里跳过了 sandboxrunner。模型不需要知道这些内部依赖,但工具执行时系统会注入它们。这为后面的 sandbox 和 subagent 铺了路。

跑 demo 会看到什么

运行:

python examples/demo_mock.py

你会看到 runner 一轮一轮推进:

[Runner] === Iteration 1 ===
[Runner] Requesting Tool (Sequential): list_files

[Runner] === Iteration 2 ===
[Runner] Requesting Tool (Sequential): read_file

[Runner] === Iteration 3 ===
[Runner] Requesting Tool (Sequential): apply_patch

这比“模型会写代码”具体得多。Forge 展示的是:模型在每一轮只能通过有限工具观察世界、改变文件、验证结果。Agent 的能力不是凭空来的,而是由 loop、context、tool 和 trace 共同组成的。

最小不是简陋

v0.1 的 Forge 很小,但它已经包含了 Agent 的几个关键边界:模型只负责决策,工具负责行动,runner 负责循环,context 负责记忆,trace 负责观察。

后面所有版本,都是在这个骨架上补能力:验证、评估、恢复、规划、压缩、安全、扩展和协作。

所以理解 v0.1 的意义很重要。因为一旦看懂了这个最小闭环,再复杂的 Agent 系统都可以被拆回这几个问题:

谁在推进循环?谁在整理上下文?谁在执行动作?谁在判断完成?谁在记录过程?

Comments: