OPALL

OPALL-F-023 · STUDY · 已整理 ORGANIZED · 前置:F-001 · F-006

手把手:从 0 搭一个最小 Agent

Build your first agent from scratch

读懂 F-001 的循环、F-006 的工具边界之后,这一篇带你把它们从空文件夹亲手搭出来。这里给的是骨架和思路——每步为什么这么写、关键代码长什么样、跑通的标志是什么;完整可跑的成品在 P-006,想要逐行讲解 + 陪跑答疑看 教学包。零依赖、不用 key。

01要搭的是什么GOAL

一个最小 Agent = 一个循环:观察当前状态 → 决策下一步 → 执行(调工具)→ 更新状态,不断重复,直到任务完成、或撞上停止条件。它不需要大模型也能跑——先用一个"确定性决策器"(写死规则)把骨架跑通,理解了再换成真模型。这正是 P-006 的做法:默认零依赖、无需 key。

先跑通这个,你就亲手理解了 Agent 的本质;后面换模型、加工具都是在这个骨架上长出来的。

02目录骨架STRUCTURE

先想清楚每个文件负责什么,再动手。最小拆法:

my-agent/
├── run.py        # 入口:读任务、建隔离工作区、跑循环、写结果
├── agent.py      # 核心:观察→决策→执行→更新的循环 + 停止条件
├── decider.py    # 决策器:给状态、返回下一步动作(先写死规则版)
├── tools.py      # 工具:Agent 能调的动作(读/写文件),每个都带边界校验
├── trace.py      # 留痕:把每一步逐条写进 JSONL
└── verify.py     # 验证:独立核对结果对不对(不信 Agent 自己报"我做完了")

为什么这么分?因为每个文件对应 Agent 的一个"关节":循环、决策、能力、留痕、验证。分开写,后面想换决策器或加工具,只动一个文件。

03写循环(agent.py)THE LOOP

循环是心脏。关键不在"跑起来",在三个显式的停止条件——不然它会空转或失控烧钱:

def run(task, decider, tools, max_steps=12):
    state = {"task": task, "history": [], "done": False}
    fails = 0
    for step in range(max_steps):          # 停止条件①:步数上限
        obs = observe(state)               # 观察:整理现状给决策器
        action = decider.decide(obs)       # 决策:返回 {"type": ..., ...}
        if action["type"] == "finish":     # 停止条件②:任务完成
            state["done"] = True
            break
        try:
            result = execute(action, tools)
            fails = 0
        except ToolError as e:             # 只接可预期的工具错误
            result = {"error": str(e)}
            fails += 1
            if fails >= 3:                 # 停止条件③:连续失败熔断
                break
        state["history"].append({"step": step, "action": action, "result": result})
    return state

注意两点:一是"决策"和"执行"分开(决策器只决定做什么、不碰真实世界,方便测试和替换);二是只捕获可预期ToolError,别用一个 except 吞掉所有异常——那会把真正的 bug 藏起来。

04加工具 = 加边界(tools.py)TOOLS = LIMITS

工具就是 Agent 的"手"。给它一个工具,就是给它一份权力——所以每个工具都要自带边界,别让它能碰不该碰的:

from pathlib import Path

WORKSPACE = Path("runs/current").resolve()   # 只允许在这个目录内活动

def _resolve(name):
    p = (WORKSPACE / name).resolve()
    if not p.is_relative_to(WORKSPACE):       # 防"../../etc/passwd"越界
        raise ToolError(f"越界访问被拒: {name}")
    return p

def read_file(name):
    return _resolve(name).read_text(encoding="utf-8")

def write_file(name, content):
    _resolve(name).write_text(content, encoding="utf-8")
    return "ok"

TOOLS = {"read_file": read_file, "write_file": write_file}  # 只注册这两个

这就是 F-012 讲的"工具即权力边界"落到代码:白名单注册(只有 TOOLS 里的能被调)、路径校验(is_relative_to 挡越界)。想让它更安全,就把边界收得更紧,而不是加更多工具。

05留 trace + 独立验证TRACE & VERIFY

这两步是"判断/证据"意识落到动手,也是这个站反复强调、但别处教程常省掉的:

  • trace:把每步的动作和结果逐条追加进一个 JSONL 文件。一份带失败记录的 trace,就是你项目"经得起追问"的证据(见 N-007)。
  • 独立验证:任务完成后,不要信 Agent 自己说的"我做完了"。用一段独立代码去实际核对结果(比如它说写了 5 行,你就真去数那个文件有没有 5 行)。verify 和 agent 分开,才防得住"自报家门"。
# verify.py — 独立核对,不信自报
def verify(task, workspace):
    # 例:任务要求生成的文件至少 5 行
    out = (workspace / task["output"]).read_text()
    actual = len(out.splitlines())
    return actual >= task["done_when"]["min_lines"]   # 逐项核对,返回真假

06跑通 + 三级改动练习RUN & PRACTICE

跑通的标志:python3 run.py 后,你能看到 trace 里一步步的动作、最后 verify 返回"通过",且结果文件真的在。到这里,你就有了一个能追问的最小项目。

然后别停在"跑通"——按 F-008 的三级台阶,在这个骨架上练"改",每次改完记一行台账(动了什么、崩在哪、为什么):

  • 改输入:换一个不同的任务,看它在循环里怎么走、哪一步变了。
  • 改约束:把 max_steps 调到 3,或给工具加一条新限制,看它怎么被逼着变聪明或失败。
  • 改失败:故意让某个工具抛错,验证熔断真的触发、trace 里能看到失败——把报错从噪音读成信息。

能改三处、每处说得清为什么,这个项目才真正属于你——这正是 F-010 说的"经得起追问"。

  • 你的循环有三个显式停止条件(完成/步数/熔断)吗?
  • 你的工具有边界校验、只注册了该有的吗?
  • 你有一份能打开的 trace,和一段不信自报的独立验证吗?

完整可跑的参考实现:P-006 Minimal Agent Loop(克隆即跑)。想要逐行讲解、三级改动的完整练习和陪跑答疑 → 教学包