游乐游手机版
首页/AI教程/文章详情

从零到一Agent系列第4篇:工具契约之Tool接口与注册表

时间:2026-07-21 18:33
从零到一手撸 Agent 系列 — 第 4 篇:工具的契约 — Tool 接口与注册表 上一篇我们搞定了 Agent 的主循环——能请求 LLM、解析 tool_call 的返回值、调用工具,再把结果传回去。但问题来了:工具本身还是空的。`registry Get(name)` 返回的是 nil,A

从零到一手撸 Agent 系列 — 第 4 篇:工具的契约 — Tool 接口与注册表

上一篇我们搞定了 Agent 的主循环——能请求 LLM、解析 tool_call 的返回值、调用工具,再把结果传回去。但问题来了:工具本身还是空的。`registry.Get(name)` 返回的是 nil,Agent 只能干瞪眼,什么都做不了。

从零到一手撸 Agent 系列 — 第 4 篇:工具的契约 — Tool 接口与注册表

所以这一篇的任务很明确:给 Agent 装上"手"——也就是工具系统。这其实是 Agent 和纯聊天机器人之间的分水岭。有了工具,Agent 才能读文件、写代码、跑命令。咱们先定义工具的契约,也就是 Tool 接口,再实现注册表,最后落地几个基础工具。

一、Tool 接口:六个方法,一个契约

所有工具都要遵循同一个接口,这是整个工具系统的基石:

type Tool interface {
Name() string // 工具名,如 "read_file"
Description() string // 功能描述(给 LLM 看的)
Schema() json.RawMessage // 参数 JSON Schema(告诉 LLM 怎么调)
Execute(ctx context.Context, args map[string]any) (string, error) // 执行
ReadOnly() bool // 是否只读(决定能否并行)
}

五个方法,各司其职,具体来说:

方法 谁用 作用
Name() Registry + System Prompt 工具的唯一标识,LLM 回复里的 tool_calls[].name 对应的就是它
Description() System Prompt 告诉 LLM 这个工具能干什么,描述写得好不好直接影响 LLM 调用的准确率
Schema() Provider → LLM JSON Schema 格式的参数定义,LLM 据此生成合法的参数 JSON
Execute() Agent 主循环 真正"干活"的地方——传入参数 map,返回字符串结果
ReadOnly() executeBatch 决定是否可并行:true 的连续调用可以并发,false 必须串行

这里有个细节值得聊一下:为什么 Execute 的入参是 map[string]any 而不是结构化的类型?因为工具是运行时动态注册的,编译期根本不知道有哪些工具会上场。在 Go 里,map[string]any 是唯一能表示"任意 JSON 对象"的类型。每个工具内部会用 decodeArgs 把这个 map 转换成自己的参数结构体。

返回值的设计也很关键——固定是 (string, error)。不管是成功还是失败,都返回字符串:成功了返回工具的输出,失败了返回 "Error: ..."。这样一来,调用方不需要区分"工具报错"和"工具正常返回了一句话",统一当作文本追加到对话历史就行。

二、Registry:线程安全 + 排序 + 过滤

工具注册表本质上就是一个并发安全的 map:

type Registry struct {
mu *sync.RWMutex
tools map[string]Tool
}
func (r *Registry) Register(tool Tool) { /* 加锁写 */ }
func (r *Registry) Unregister(name string) { /* 加锁删 */ }
func (r *Registry) Get(name string) Tool { /* 读锁查 */ }

这里用的是 RWMutex 而不是 Mutex——原因很简单,Get 的调用频率远高于 Register,读锁不互斥,并发查询就不会阻塞。

还有个 List() 方法,它返回按名称排序的切片:

func (r *Registry) List() []Tool {
tools := make([]Tool, 0, len(r.tools))
for _, tool := range r.tools {
tools = append(tools, tool)
}
sort.Slice(tools, func(i, j int) bool {
return tools[i].Name() < tools[j].Name()
})
return tools
}

排序不是为了好看——System Prompt 的顺序固定了,LLM 的 prompt cache 才能命中。如果每次生成的 prompt 中工具顺序都是随机的,cache 就废了。

还有一个重要方法:

func FilterRegistry(parent *Registry, exclude ...string) *Registry {
child := NewRegistry()
for name, tool := range parent.tools {
if !excluded(name) {
child.tools[name] = tool
}
}
return child
}

这个有什么用?子 Agent(第 9 篇会讲到)不能调用 tasktodo_write 这类"元工具"——否则子 Agent 会再派生出子子 Agent,无限递归下去。FilterRegistry 就是用来创建一个排除特定工具的副本。

三、decodeArgs:map → 结构体的通用桥梁

func decodeArgs(args map[string]any, target any) error {
raw, _ := json.Marshal(args) // map → JSON 字节
return json.Unmarshal(raw, target) // JSON 字节 → 结构体
}

别看就三行代码,每个工具的 Execute 都依赖它。这种 map[string]anyjson.Marshaljson.Unmarshal 的"绕一圈"做法,比直接用反射更稳健——它利用了 Go 的 json tag 来完成字段映射,类型不匹配时还能给出清晰的错误信息。

四、实战:ReadFileTool — 一个完整的工具实现

咱们以 read_file 为例,走一遍从接口到实现的完整流程。

4.1 结构体 + 构造函数

type ReadFileTool struct {
AllowedDirs []string // 白名单目录,为空表示不限制
MaxBytes int // 单次读取上限,默认 10MB
}
func NewReadFileTool(workdir string) *ReadFileTool {
return &ReadFileTool{
AllowedDirs: allowedDirsFromWorkdir(workdir),
MaxBytes: 10 * 1024 * 1024,
}
}

AllowedDirs 是安全边界:如果设置了 /home/user/project,LLM 就不能读 /etc/passwd。空切片代表不限制——在本地开发场景下够用,后面第 6 篇会有更精细的权限控制。

4.2 五个接口方法

func (t *ReadFileTool) Name() string { return "read_file" }
func (t *ReadFileTool) ReadOnly() bool { return true } // 纯读,可并行
func (t *ReadFileTool) Description() string {
return "读取文本文件,返回带行号的输出..." // 用英文描述,LLM 原生语言
}
func (t *ReadFileTool) Schema() json.RawMessage {
// 返回 {"type":"object","properties":{"path":...},"required":["path"]}
}

Schema() 硬编码返回 JSON,虽然手写 JSON 看起来有点丑,但它的职责很单一——告诉 LLM 参数格式。一旦定义好了,基本就不会动。

4.3 Execute:核心逻辑

func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) (string, error) {
// 1. 解码参数
var p struct {
Path string `json:"path"`
Offset int `json:"offset"`
Limit int `json:"limit"`
}
decodeArgs(args, &p)
// 2. 校验路径
t.checkPath(p.Path)
// 3. 二进制检测:前 8KB 有 NUL 字节 → 判定为二进制,拒绝读取
peek := make([]byte, 8192)
f.Read(peek)
if bytes.IndexByte(peek, 0) >= 0 {
return "", errors.New("可能是二进制文件")
}
// 4. 读取并格式化:每行前缀 " 42→..."
lines := strings.SplitAfter(string(content), "\n")
for i, line := range lines[offset : offset+limit] {
fmt.Fprintf(&b, "%*d→%s\n", padWidth, offset+i+1, line)
}
return b.String(), nil
}

这里有两个设计亮点值得注意。

行号输出格式 42→... 不是随便定的。LLM 读到 42→package main 就知道这是第 42 行,后续调用 edit_file 替换时可以直接引用行号——read_fileedit_file 是配套设计的。

二进制检测:简单的 NUL 字节检查就能覆盖 99% 的场景。比"检查文件扩展名"更可靠——.gitignore 没有扩展名但它是文本,比"检查完整 MIME type"更轻量。

五、其他基础工具一览

5.1 write_file — 写文件

type WriteFileTool struct {
AllowedDirs []string
}
func (t *WriteFileTool) ReadOnly() bool { return false } // 写操作,不可并行

核心逻辑就三步:os.MkdirAll(filepath.Dir(path), 0755) 创建父目录 → os.Create 打开文件 → file.WriteString(content)。还支持 append=true 追加模式。

5.2 edit_file — 精确替换

type EditFileTool struct {
AllowedDirs []string
}
type editFileArgs struct {
Path string `json:"path"`
OldText string `json:"old_text"` // 必须在文件中精确匹配(包括空白字符)
NewText string `json:"new_text"`
All bool `json:"all"` // true=替换全部匹配,false=只替换唯一匹配
}

关键设计:默认只替换唯一匹配。如果 old_text 在文件中间出现多次且 all=false,工具会返回错误。这强制 LLM 给出足够的上下文来唯一定位——比如替换的不是 fmt 而是 fmt.Sprintf("user: %s", name)

5.3 glob_file — 文件发现

func (t *GlobFileTool) Name() string { return "glob_file" }
func (t *GlobFileTool) ReadOnly() bool { return true }

内部调用 filepath.Glob(pattern)。Agent 在"找文件"时不用让 LLM 猜路径,直接 glob_file("**/*.go") 一条命令就搞定了。

5.4 grep — 搜索内容

type GrepTool struct {
AllowedDirs []string
}
func (t *GrepTool) ReadOnly() bool { return true }

核心实现:regexp.Compile(pattern)filepath.Walk 递归遍历 → 逐行匹配 → 返回 path:line:text 格式。跳过 .gitnode_modules 和隐藏文件。最多 200 条结果,可配置超时(默认 30 秒)。

5.5 bash — Shell 执行

type BashTool struct {
DefaultTimeout time.Duration // 默认 60s
MaxOutputBytes int // 默认 1MB
}
func (b *BashTool) ReadOnly() bool { return false }

跨平台适配:Windows 用 cmd /C,类 Unix 用 sh -cExecute 的核心是 exec.CommandContext——用 context 控制超时,cmd.CombinedOutput() 合并 stdout + stderr。

六、DefaultRegistry:一键装配

我们当然不希望每次创建 Agent 都要手动注册十几个工具。所以提供一个工厂函数:

func DefaultRegistry(workdir string) *Registry {
r := NewRegistry()
r.Register(NewBashTool(workdir))
r.Register(NewReadFileTool(workdir))
r.Register(NewWriteFileTool(workdir))
r.Register(NewEditFileTool(workdir))
r.Register(NewGlobFileTool(workdir))
r.Register(NewGrepTool(workdir))
r.Register(NewWebFetchTool())
r.Register(NewTodoWriteTool())
r.Register(NewCompleteStepTool())
// ... 更多工具
return r
}

调用方只需一行 registry := tools.DefaultRegistry(workdir),就能得到一个预装好全部基础工具的注册表。如果想加自定义工具,registry.Register(myTool) 追加即可;想删掉某个工具,registry.Unregister("bash") 即可。

七、工具执行全景:从 LLM 提议到结果回传

把工具系统和 Agent 主循环串联起来,看完整链路:

1. Agent 构建请求 → 遍历 registry.List() 生成 tools 数组 → 发给 LLM
2. LLM 返回 tool_calls: [ {id:"call_1", name:"read_file", arguments:'{"path":"main.go"}'}, {id:"call_2", name:"glob_file", arguments:'{"pattern":"*.go"}'}, {id:"call_3", name:"write_file", arguments:'{"path":"out.txt","content":"..."}'} ]
3. partitionToolCalls 按 ReadOnly 分组: [{read_file, glob_file} 并行] → [{write_file} 串行]
4. 每个工具依次执行 invokeTool → registry.Get(name) → tool.Execute(args)
5. 所有结果作为 tool 消息追加到对话历史: [ {role:"tool", tool_call_id:"call_1", content:"1→package main\n..."}, {role:"tool", tool_call_id:"call_2", content:"main.go\n..."}, {role:"tool", tool_call_id:"call_3", content:"写入成功"}, ]
6. 循环回到 loopStep → 再次请求 LLM(带工具结果)

至此,你的 Agent 真正拥有了"动手能力"——能读能写能搜能跑命令。它不再是一个只会说话的 LLM wrapper,而是一个能主动操作代码库的编码助手。

小结

你学到了什么 对应代码
Tool 接口的五方法契约 tool.go
Registry:RWMutex 并发安全 + 排序保 cache + FilterRegistry 排除 registry.go
decodeArgs:map → 结构体的 JSON 桥接 decode.go
ReadFileTool 的完整实现(行号格式、二进制检测、白名单) files.go
write_file/edit_file/glob_file/grep/bash 的关键设计 files.gogrep.gobash.go
DefaultRegistry 工厂函数 preset.go
工具执行全景链路:LLM 提议 → 分区 → 执行 → 回传 第七节

下篇预告:有了工具,Agent 能做的事暴增——但也能搞破坏。我们将给 Agent 系上"安全带"——安全权限管线。你会看到如何用 DenyList(黑名单)+ BashAsk(命令确认)+ WorkdirBoundary(目录边界)三级检查,让 Agent 既能干活又不会删掉你的系统文件。

来源:https://juejin.cn/post/7663813816637833262
上一篇自主引擎六器:ReAct、MCP、多Agent、Workflow、沙箱、护栏 下一篇款MCP工具助AI深度理解业务
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
TalkVisions实时视频翻译应用,消除语言障碍
AI教程 · 2026-07-25

TalkVisions实时视频翻译应用,消除语言障碍

TalkVisions是一款实时视频翻译应用,能将视频中的口语实时转录为文本并翻译成用户所选语言,以字幕形式叠加在画面上,支持多语言、低延迟,还可保存录制视频,有效消除跨语言沟通障碍。

AI驱动的日历管理工具Ipso
AI教程 · 2026-07-25

AI驱动的日历管理工具Ipso

IpsoAI是一款专为专业人士及助手打造的AI日历管理工具,能够自动协调多方日程、智能草拟邮件,并通过快速安排会议、提供智能建议及自动化工作流程,显著减少琐碎操作,帮助用户高效管理时间、提升工作效率。

Spectate企业级专业高效监控与事故管理一体化平台
AI教程 · 2026-07-25

Spectate企业级专业高效监控与事故管理一体化平台

Spectate是一款高效监控和事故管理工具,能在30秒内检测故障并推送告警。它支持Slack、PagerDuty等主流集成,提供自定义状态页面和全球性能监控。系统自动更新状态并推送修复建议,帮助团队减少沟通成本,快速解决问题。

阿里云通义千问2.5大模型发布 多项能力赶超GPT-4
AI教程 · 2026-07-25

阿里云通义千问2.5大模型发布 多项能力赶超GPT-4

通义千问2 5大模型发布,多项能力宣称赶超GPT-4,中文语境下文本理解、生成、知识问答等表现优异。相比2 1版本,理解提升9%、逻辑推理提升16%、指令遵循提升19%。开源1100亿参数模型超越Llama-3-70B,获评开源最强。已服务超9万家企业,与小米、微博等达成合作。

万知个人AI工作站:一站式智能阅读创作分享平台
AI教程 · 2026-07-25

万知个人AI工作站:一站式智能阅读创作分享平台

万知是集成多种AI能力的个人工作站,支持自然语言交互、文档快速阅读与摘要生成、PPT自动设计与优化,覆盖学术研究、商务报告、写作辅助及日常问答等场景,全方位提升工作效率。