从零实现 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_bash、run_read、run_write、run_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,主线就会很清楚。