一个最小 Agent 是怎么跑起来的:Agent Loop 与工具使用全链路

从零实现 Agent Harness 系列 · 第 01 篇

很多人第一次看 Agent 代码,会先被一堆名词砸到:tool calling、memory、planner、MCP、multi-agent。

但如果把外层都剥掉,Agent 最核心的骨架其实很简单:模型决定下一步,程序执行动作,再把结果还给模型。

前言

很多人会把 Agent 理解成一种"更聪明的聊天模型"。

这个说法不能算错,但不够准确。普通聊天模型能理解语言、组织答案、做推理,但它不会直接读取你的文件系统,不会直接运行终端命令,不会直接修改代码------它只能根据你给它的上下文说话。

Agent 不一样。Agent 的核心,不是模型突然多了超能力,而是我们给它搭了一个循环:

text 复制代码
模型负责决定下一步
程序负责执行真实动作
执行结果再回到模型上下文里

一句话说:

Agent 不是"模型自己会做事",而是"模型在循环里指挥程序做事"。

一、Agent Loop 是怎么跑起来的

假设用户说:"帮我看看这个项目的 README 写了什么"。

如果没有工具,模型只能靠猜,或者靠你手动把 README 内容贴给它。如果有 Agent Loop,事情就变成:

text 复制代码
模型:我需要读取 README
程序:执行 read_file("README.md")
程序:把文件内容返回
模型:基于真实文件内容回答用户

再比如用户说:"帮我跑一下测试,看看哪里失败了":

text 复制代码
模型:调用 bash("pytest")
程序:真的去跑 pytest
程序:把测试输出返回
模型:根据失败日志继续分析

这就是 Agent 和普通聊天最大的区别:

text 复制代码
聊天模型主要处理"你给我的文本"
Agent 还可以主动观察和操作外部世界

最小的心智模型

最小的 Agent Loop,可以只记住这六步:

text 复制代码
1. 用户输入任务
2. 程序把当前 messages 发给模型
3. 模型决定:直接回答,或调用某个工具
4. 如果模型要调用工具,程序执行工具
5. 工具结果以 tool message 形式回填到 messages
6. 再次调用模型,直到它不再要工具

画成一条线:

text 复制代码
用户任务 -> 模型 -> tool call -> 程序执行工具 -> tool result -> 模型继续判断 -> 最终回答

很多复杂 Agent 系统看起来很花,但骨架通常还是这条线。

二、工具使用全链路

但这里有一个很多人第一次写 Agent 会踩的坑:

text 复制代码
我已经写好了 read_file、bash、write_file
那模型是不是就会自动调用它们?

不是。模型不会直接看到你的本地函数,也不会自动理解哪些函数可用、每个函数接收什么参数。工具真正能被模型使用,要经过一整条链路。

2.1 四个角色:实现、schema、模型、runtime

一条完整的工具调用链路里,一共有四个角色。

工具实现: 也就是你的 Python 函数,比如 run_bashrun_readrun_writerun_edit。它们负责和真实环境交互。

工具schema: 暴露给模型看的"工具说明书",描述工具名、用途、参数结构、参数类型。模型看见的是这份说明书,不是你的 Python 源码。

模型: 模型不是直接执行工具,而是根据当前上下文判断:这一步要不要调工具?调哪个?参数是什么?它做的是"提案",不是"执行"。

Agent runtime: 也就是你写的程序主循环。它负责把工具 schema 传给模型、接住模型返回的 tool_calls、执行本地工具、把结果写回消息历史。

text 复制代码
工具实现负责做事
schema 负责让模型理解"能做什么"
模型负责决定"现在要不要做"
runtime 负责真正把调用跑起来

2.2 模型看到的是 schema,不是 Python 函数

程序里可能有这样一个函数:

python 复制代码
def run_bash(command: str) -> str:
    ...

但模型并不会看到这段 Python 源码。模型看到的是一份结构化描述:

json 复制代码
{
  "type": "function",
  "function": {
    "name": "bash",
    "description": "Run a shell command in workspace",
    "parameters": {
      "type": "object",
      "properties": {
        "command": {"type": "string"}
      },
      "required": ["command"]
    }
  }
}

这份描述就是 tool schema。它的作用不是执行,而是让模型理解"我现在有什么能力、调用它需要什么参数"。

可以这样理解:

text 复制代码
本地函数是发动机
tool schema 是操作说明书
模型只看到说明书

程序每次调模型时,都会把当前消息和工具列表一起传过去:

python 复制代码
response = client.chat.completions.create(
    model=model_name,
    messages=messages,
    tools=TOOLS,
)

这里的关键不是 create() 调用本身,而是:模型每一轮能调用什么,取决于这一轮请求里 Agent runtime 把哪些 tool schema 传给了模型。后面看 MCP 时,这句话会反复出现。

2.3 模型返回的是调用请求,程序负责执行

模型收到工具列表后,如果觉得需要某个工具,它不会自己执行。它只会返回一个结构化请求:

text 复制代码
我要调用 read_file
参数是 {"path": "README.md"}

也就是常说的 tool_call------这是"调用意图",不是"执行结果"。

当模型返回 name = "read_file",程序还需要把这个名字映射到真实实现。这就是 TOOL_HANDLERS 的作用:

python 复制代码
TOOL_HANDLERS = {
    "bash": run_bash,
    "read_file": run_read,
    "write_file": run_write,
    "edit_file": run_edit,
}

这层很朴素:模型说要调哪个工具,程序就按名字找到对应 handler,真的去执行。它把"模型世界"和"程序世界"接起来了。

2.4 工具结果为什么要回填到 messages

模型调用工具后,程序执行完,不能只把结果打印到终端就结束。

更重要的是:把结果重新放回消息历史,再发给模型。

比如用户说"帮我看看 README 里有没有安装说明",一轮正确的流程是:

text 复制代码
模型:调用 read_file("README.md")
程序:读取 README
程序:把 README 内容作为 tool result 回填
模型:基于文件内容回答"有/没有安装说明"

如果不回填,模型只知道自己发起过一次调用,但不知道真实结果是什么。

所以工具调用真正完整的闭环是:

text 复制代码
schema -> tool_call -> handler -> tool_result -> 再次推理

2.5 工具设计:清楚比数量重要

工具好不好用,不只取决于函数本身,也取决于 schema 写得清不清楚。

比如同样一个读文件工具,差一点的描述可能是"read file",好一点的描述会明确:"Read a file from workspace. Use this when you need actual file contents instead of guessing from memory. Optionally limit returned lines."

这会直接影响模型的决策,因为模型用工具,是通过当前上下文、tool name、tool description、参数 schema 来推断自己该怎么行动。很多工具调用问题,本质不是模型笨,而是工具接口没讲清楚。

同样,一开始不要急于给 Agent 塞很多工具。工具多了,选择成本更高,边界变模糊,错误调用概率上升。早期更重要的不是工具数量,而是工具边界。一个好工具通常名字直白、描述清楚、参数少而准、返回值稳定、失败方式可预测。

三、代码与工程意识

在这套教学代码里,最小版本对应的是 harness_agent/01_agent_loop.py。如果你第一次读这类代码,不要上来逐行抠,先抓三层:

text 复制代码
工具实现层:bash、read_file、write_file、edit_file ------ 真正和环境交互的部分
工具注册层:TOOLS 和 TOOL_HANDLERS ------ 把工具的名字、参数和用途暴露给模型
循环调度层:Agent Loop 本体 ------ 调模型、检查 tool_calls、执行、回填、再调

虽然这个文件很小,但里面已经埋了两个很重要的工程意识。

**第一,模型可以提议动作,但程序要设边界。**比如文件路径不能越界(safe_path)、危险命令要拦截。它还不是生产级沙箱,但已经体现出一个原则:模型负责提议,程序负责兜底。

**第二,工具结果不是附属品,而是下一轮上下文。**很多初学者会把 tool result 理解成"打印给终端看看",但它真正的价值是会重新进入 messages,成为下一轮模型推理的输入。这意味着 Agent 的能力不只来自模型本身,还来自工具设计得好不好、结果返回得清不清楚、Loop 有没有把上下文接稳。

四、为什么这一层是基础

因为后面几乎所有高级能力,都是在这个 loop 和工具链路上加出来的:

  • Todo:让 Loop 有任务状态
  • Skills:让 Loop 能按需加载知识
  • Context Compact:让 Loop 不被长上下文压垮
  • Background Tasks:让 Loop 不被长命令阻塞
  • Multi-Agent:让一个 Loop 可以派出别的 Loop
  • MCP:让工具来源不只限于本地函数

如果最小 Loop 和工具链路没理解,后面的功能会看起来都像"又多了一堆机制"。但如果理解了,后面其实都是在回答同一个问题:

text 复制代码
怎样让这个循环和工具链更强、更稳、更可控

小结

Agent 的本质不是"模型自己会干活",而是"模型在一个循环里决定动作,程序替它执行动作,再把结果还给模型"。

工具使用,就是这条循环里最核心的链路:模型看见的是 tool schema,返回的是 tool call,程序负责执行并把结果还给模型。

先把这个最小闭环吃透,后面再看计划、记忆、MCP 和多 Agent,主线就会很清楚。

相关推荐
Keven_111 小时前
算法札记:二分
算法·二分
TCW11211 小时前
AI底层系列:用C++实现线性代数的公式推导与算法设计-6.线性方程组的解集
c++·人工智能·算法
luoyayun3612 小时前
从零实现 EBU R128 LUFS 响度分析:K-weighting 滤波、双门限算法
算法·lufs响度分析
小糯米6012 小时前
JS 数组
数据结构·算法·排序算法
拳里剑气2 小时前
C++算法:链表
c++·算法·链表
凌波粒2 小时前
LeetCode--90.子集II(回溯算法)
数据结构·算法·leetcode
旖-旎2 小时前
《LeetCode 417 太平洋大西洋水流问题 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill
凌波粒2 小时前
LeetCode--46.全排列(回溯算法)
数据结构·算法·leetcode
海砥装备HardAus2 小时前
大载重工业无人机动力容错控制:单电机失效下的应急重构算法设计
算法·重构·嵌入式·无人机