最近我在用 Go 写一个叫 AgentHub 的项目,想把 AI Agent 从概念变成自己能看懂、能运行的程序。

第一阶段的目标很具体:给程序一个自然语言任务,让模型选择工具,程序执行工具,再把结果交回模型,最后得到回答。最开始用 Echo 验证流程,接着把已有的图片处理服务接进来。

这篇记录的是 2026 年 9 月 10 日的开发过程:项目骨架、图片工具注册,以及随后对忽略规则的修正。

先把模型、工具和循环分开

项目刚开始时,我把主要代码分成了三部分:

部分负责什么
模型客户端把消息发给 GLM,取回模型回复
工具执行具体操作,比如回显文本、缩放图片
Agent 内核维护消息历史,解析回复,选择工具并控制循环

这样分的直接好处是,图片处理的细节可以留在图片工具里。Agent 内核只需要知道工具叫什么、能做什么,以及怎么调用它。

工具接口很小:

1
2
3
4
5
type Tool interface {
Name() string
Description() string
Execute(ctx context.Context, input string) (string, error)
}

Name 用于查找工具,Description 会进入提示词,Execute 才真正执行操作。模型根据描述提出工具调用,Go 程序根据名字查表,再调用对应的 Execute。

这也让我把职责想清楚了:模型输出工具名和参数之后,还需要程序完成解析、分发、执行和结果回传。

ReAct 在代码里是一段有限循环

这一版 AgentHub 使用文本协议。模型需要调用工具时,输出 Action 和 Action Input;能够回答时,输出 Final Answer。

例如,下面是一段用于说明协议的文本,并非某次运行日志:

1
2
Action: image_scaling
Action Input: ./picture/window.jpg 256

内核的执行顺序是:

1
2
3
4
5
6
7
8
9
10
11
准备系统提示词和用户问题
↓
调用模型,把回复追加到消息历史
↓
解析回复
├─ 有最终答案:返回答案
└─ 有工具请求:查找并执行工具
↓
把 Observation 追加到消息历史
↓
再次调用模型

这里有一个很容易忽略的地方:第二次调用模型时,要把之前的过程一起发过去。

如果只发原始问题,模型就看不到工具刚刚返回的结果。因此 messages 会不断追加模型回复和工具反馈,让下一轮有依据继续处理。

当前这套文本协议把工具结果作为带有 Observation: 前缀的 user 消息回传,每轮最多处理一个工具请求。它的实现比较直接,适合先跟清楚调用链;后续如果迁移到原生结构化工具调用,还需要调整协议和消息表示。

从 Echo 走到真正的图片处理

最开始用 Echo,是因为它可以把输入原样返回。这样我能先确认工具名称分发和结果回传是否连得起来。

接下来注册 image_scaling,让 Agent 调用本地图片处理服务。用户传入的是这样的任务:

1
把 ./picture/window.jpg 缩放到宽度 256

模型需要把自然语言转换成工具接受的参数。图片工具再依次完成这些工作:

  1. 拆出图片路径和目标宽度,检查字段数量及宽度范围。
  2. 根据文件扩展名检查是否为支持的 JPEG 或 PNG。
  3. 打开本地文件,把图片和宽度组装为上传请求。
  4. 请求图片服务的 /resize 接口。
  5. 读取返回的图片字节,保存文件并返回输出路径。

这样,一次自然语言任务就和已有的 HTTP 服务连接起来了。Agent 内核仍然只调用 Execute,上传和保存文件的细节由工具负责。

接口能接住对象,还要看对象有没有准备好

接图片工具时,我也碰到了 Go 里一个很实际的问题:实现了接口,并不意味着这个类型可以直接用零值运行。

Echo 本身不依赖外部配置,用 Echo{} 就能工作。图片工具需要服务地址和 HTTP 客户端,所以要通过构造函数创建:

1
imageprocessing.NewImageScaling("http://localhost:8080/resize")

这里还涉及接收者类型。图片工具的方法使用指针接收者,放进工具列表的是 *ImageScaling;Echo 使用值接收者,可以直接放入 Echo{}。

以后再接工具,我会同时看两件事:类型是否满足接口,以及执行时依赖的状态是否已经初始化。

把失败也放进循环里考虑

这版循环除了正常调用,还有几条失败路径:

  • 模型请求失败:返回错误,结束本次运行。
  • 模型指定了不存在的工具:把错误写成 Observation,交回模型继续处理。
  • 工具执行失败:把工具错误交回模型,允许下一轮根据反馈调整。
  • 循环次数耗尽:返回“循环耗尽,未能得到最终答案”。

命令行入口将最大循环次数设为 5,整个任务使用 60 秒的超时上下文。这样至少能给一次运行设置明确的次数和时间边界。

不过,5 轮只是停止条件,不能证明任务完成。一次成功的图片实验,也不能说明这些失败路径都已经被充分验证。第一阶段还没有自动化测试,这些都需要后面继续补。

一个和 Agent 无关、但很容易踩的坑

同一天,我还修正了 .gitignore。

原文件的规则带有前导空格,一些说明文字也没有写成注释。修复时把规则顶格书写、给说明行加上 #,并统一了换行格式。

这个问题提醒我,配置文件写得看起来整齐,并不能证明规则实际生效。尤其是 .env 这样的文件,应该用 git check-ignore 检查匹配结果。也要记住,忽略规则不会自动移除已经被 Git 跟踪的文件。

这一阶段留下的问题

到这里,AgentHub 已经能够接收命令行任务,通过模型选择工具,调用图片服务,再根据工具结果生成最终回答。

但当时还有一个明显的问题:运行结束后主要看到的是最终答案,中间到底调用了什么、传了什么参数、哪里慢了,都不够清楚。

如果输出不符合预期,我需要区分是模型回复格式出了问题,还是解析器、工具或外部服务出了问题。