从零搭建 Agent Harness 系列(三)极简工具与物理交互原则

之前的博客我们已经通过ReAct的架构为Agent赋予了一个大脑,但是只有大脑是不够的,也就是说现在的Agent只能做token的预测,但是还需要一些感官,能够将现实世界中与完成任务有关的各种信息整理成token,以及能够将token重新变成能够影现实世界的操作,这些操作就是我们这篇博客的重点——tool

我们将亲手用 Go 语言构建一个强扩展、高内聚的 Tool Registry.

架构设计:为什么需要 Tool Registry?

在 Harness(驾驭工程)的理念中,Main Loop 永远是“瞎子”和“聋子”。它不应该知道 bash 命令怎么调用,也不应该知道 read_file 需要什么参数格式。它只负责维护上下文,并将模型吐出来的 JSON 字符串丢给执行层。

因此,Tool Registry 扮演了一个极其关键的“集线器(Hub)”和“路由器(Router)”的角色。它的核心职责有三:

  1. 动态挂载(Register):允许开发者在引擎启动时,随时随地向系统插拔新的工具实现(在 Go 中,其本质上是实现了特定 Go 接口的结构体)。

  2. 描述暴露(Expose Schema):在每次向大模型发起推理前,Registry 负责把当前所有已挂载工具的名称、描述以及 JSON Schema 打包成列表,交给 Provider 翻译给大模型听。

  3. 路由分发与执行(Dispatch & Execute):当大模型决定调用某个工具,并吐出一串 JSON 参数(ToolCall)时,Registry 负责找到对应的 Go 函数,把 JSON 丢给它执行,最后将结果封装成统一的 ToolResult 返回给 Main Loop。

Tool Registry

定义BaseTool接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// internal/tools/registry.go
package tools

import (
"context"
"encoding/json"
"fmt"
"log"

"github.com/yourname/go-tiny-claw/internal/schema"
)

// BaseTool 是所有具体工具必须实现的通用接口
type BaseTool interface {
// Name 返回工具的全局唯一名称 (大模型通过这个名字调用它)
Name() string

// Definition 返回用于提交给大模型的工具元信息和参数 JSON Schema
Definition() schema.ToolDefinition

// Execute 接收大模型吐出的 JSON 参数,执行具体业务逻辑
// 注意:参数是 json.RawMessage,反序列化由各个具体工具内部自行处理
Execute(ctx context.Context, args json.RawMessage) (string, error)
}

实现 Registry 的路由与分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// internal/tools/registry.go (续)

// Registry 定义了工具的注册与分发接口
type Registry interface {
// Register 挂载一个新的工具到系统中
Register(tool BaseTool)

// GetAvailableTools 返回当前系统挂载的所有工具的 Schema,供 Main Loop 交给 Provider
GetAvailableTools() []schema.ToolDefinition

// Execute 实际路由并执行模型请求的工具调用
Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult
}

// registryImpl 是 Registry 接口的默认实现
type registryImpl struct {
// 使用 map 以工具的 Name 作为 Key 进行快速 O(1) 路由查找
tools map[string]BaseTool
}

func NewRegistry() Registry {
return &registryImpl{
tools: make(map[string]BaseTool),
}
}

func (r *registryImpl) Register(tool BaseTool) {
name := tool.Name()
if _, exists := r.tools[name]; exists {
log.Printf("[Warning] 工具 '%s' 已经被注册,将被覆盖。\n", name)
}
r.tools[name] = tool
log.Printf("[Registry] 成功挂载工具: %s\n", name)
}

func (r *registryImpl) GetAvailableTools() []schema.ToolDefinition {
var defs []schema.ToolDefinition
for _, tool := range r.tools {
defs = append(defs, tool.Definition())
}
return defs
}

func (r *registryImpl) Execute(ctx context.Context, call schema.ToolCall) schema.ToolResult {
// 1. 路由查找:如果在注册表中找不到该工具,这是模型产生了幻觉,直接向模型抛出错误
tool, exists := r.tools[call.Name]
if !exists {
errMsg := fmt.Sprintf("Error: 系统中不存在名为 '%s' 的工具。", call.Name)
return schema.ToolResult{
ToolCallID: call.ID,
Output: errMsg,
IsError: true, // 标记为错误,模型看到后会尝试纠正
}
}

// 2. 执行工具逻辑:将原始的 JSON 字节流直接丢给具体工具
output, err := tool.Execute(ctx, call.Arguments)

// 3. 封装结果:将执行结果或底层物理错误封装后返回给 Main Loop
if err != nil {
errMsg := fmt.Sprintf("Error executing %s: %v", call.Name, err)
return schema.ToolResult{
ToolCallID: call.ID,
Output: errMsg,
IsError: true,
}
}

return schema.ToolResult{
ToolCallID: call.ID,
Output: output,
IsError: false,
}
}

极简工具法则与YOLO执行哲学

既然拥有了如此强大的动态注册机制,按照常规的开发惯例,下一步我们是不是应该开始疯狂地给 Agent 编写各种专用的业务工具了?比如:写一个 git_commit 工具,写一个 npm_install 工具,写一个 grep_search 工具,或者引入当下大火的 MCP(Model Context Protocol)协议,把几百个第三方 API 一股脑地挂载给模型?

如果你真的打算这么做,那么你的 Agent 离“智障”和“破产”也就不远了。今天这一讲,我们将探讨 Harness Engineering(驾驭工程)中最核心、也最反直觉的设计哲学:极简主义与 YOLO(You Only Live Once)模式

我们将从底层逻辑剖析,为什么顶尖的开源 Coding Agent(如 OpenClaw)坚决抵制过度封装,最终只保留了几个最核心的工具。随后,我们将用 Go 语言补齐这块拼图,打造出驾驭工程中的“终极武器”——bash 工具

警惕 Context Bloat:为什么工具越多,Agent 越笨

在当前的 AI Agent 开发圈子里,有一种普遍的迷思:“只要我给模型的工具足够多,它的能力就越强。

”这导致了大量臃肿的框架和 MCP Server 的诞生。比如,一个标准的 GitHub MCP 可能包含 20 多个工具,消耗上万个 Token;一个 Playwright MCP 也会塞入几十个页面操作原语。如果你在引擎启动时加载了这些工具,会发生什么?

大模型的每一次思考(每一次 Turn 发起请求时),都必须把这些极其冗长的工具描述(JSON Schema)全部阅读一遍。这在业界被称为 Context Bloat(上下文膨胀)。

这会带来三个致命的后果:

  1. 极高的成本与延迟:仅仅为了问一句“帮我看看 main.go 的代码”,你就要向大模型发送 3 万个 Token 的前置工具描述。每次 API 请求的时间和金钱成本呈指数级上升。

  2. 注意力分散:这是最致命的。大模型的核心机制是注意力(Attention)。工具描述越多,大模型对核心任务指令的注意力就越弱。它非常容易发生幻觉(Hallucination),在几十个长得差不多的工具中调用了错误的那一个。

  3. 无尽的适配维护:你每加一个特定的专用工具(比如 search_jira_ticket),就要在 Go 引擎里维护一套繁琐的反序列化和 API 请求代码。一旦第三方接口变更,Agent 直接罢工

大道至简:图灵完备的 4 大原语

顶尖的 Harness 驾驭工程师是如何解决这个问题的?答案是:回归操作系统的本质。

其实本质上还是,还是避免提供与当前任务无关的信息,比如大量的Tool,绝大多数当前用不到,提供了就是噪音。

既然我们把 Agent 当作一个跑在本地工作区(Workspace)的工作流助手,那么它面对的环境就是操作系统的终端和文件系统。我们完全不需要为 git、grep、npm 单独写工具,因为操作系统里已经有了一个终极接口——Shell(Bash)。

在 OpenClaw / pi 的极简哲学中,仅需为大模型提供 4 个基础工具:

  1. read:读取文件内容(获取环境信息)。

  2. write:创建新文件或完全覆盖文件。

  3. edit:精准的局部代码替换(外科手术式修改。由于其具备多级降级的复杂性,我们将在下一讲专门实现)。

  4. bash:在当前工作区执行任意 Shell 命令(终极执行器)。

Agent 系统元语

YOLO 哲学:放弃本地“安全剧场”

Harness 驾驭工程的实战经验告诉我们:对于在本地开发者机器上运行的 Agent,过度前置的安全校验往往是“安全剧场(Security Theater)”。

注:安全剧场(Security Theater)指一些安全措施主要停留在形式层面——例如做了很多看起来很严格的校验 / 流程,但这些手段对真实风险的降低帮助有限,或无法有效覆盖攻击的关键路径。其效果更像“展示安全姿态”,而不是实质性地提升安全性。

只要你允许 Agent 运行代码,它总能找到方法绕过静态的黑名单,比如把 rm 拆成变量拼接执行,或者写一个带有恶意逻辑的 Python 脚本再去运行它。为了防范这种小概率事件而引入极其复杂的权限控制,只会让 Agent 在日常开发中变得愚蠢且处处受限。

因此,在基础开发阶段,OpenClaw 奉行 YOLO 模式:默认全权信任,直接在工作区(WorkDir)中执行。

如果真的出了错,交给 Git 去回滚。