从零构建 AI Agent CLI:150 行代码揭示工具调用四大组件
问题:如何让 AI 理解你的服务?
你有一堆服务——创建用户、发送邮件、查询订单。你希望用自然语言说出需求,AI 自动找到正确的服务并调用它。这个场景在今天已经非常普遍,但很多开发者面对「工具调用(Tool Calling)」时,第一反应是引入一个重型框架。
其实,一个工具调用 Agent 的核心,只有四个组件。当你理解了这四个组件,就能用 150 行左右代码,从零构建一个可用的 AI Agent CLI。
组件一:工具发现(Tool Discovery)
Agent 要调用你的服务,首先需要知道「有哪些工具可用」。
核心思路:将每个服务端点转化为一个工具描述,包含三个字段:
- 名称:唯一标识,如
users_Users_Create - 描述:这个服务做什么(从 Handler 的文档注释提取)
- 参数 Schema:需要哪些输入(从 Request 结构体的字段派生)
在 Go Micro 的实现中,这一步是自动化的——服务注册时已经包含了端点和元数据,只需一行代码即可发现所有工具:
tools := ai.NewTools(reg, ai.ToolClient(client)) discovered, err := tools.Discover()
如果你是裸 API,自己写一个枚举函数,把每个 HTTP 端点或 gRPC 方法映射为 {name, description, parameters} 三元组即可。这一步的产出一份 JSON 工具清单,直接喂给 LLM。
组件二:模型创建(Model Setup)
有了工具清单后,需要创建一个 LLM 模型实例,把工具挂载上去。
关键设计:工具描述和工具执行是一体两面——同样的 Tools 对象,既能通过 Discover() 构建工具列表,也能在处理 Tool Call 时将请求路由到正确的处理器。
m := ai.New("anthropic",
ai.WithAPIKey(apiKey),
ai.WithTools(tools),
)
这里做了两件事:
- Provider 选择:Anthropic、OpenAI、Gemini、Groq、Mistral——统一的
Model接口,切换只需改一个字符串 - 执行链路挂载:当 LLM 返回
call users_Users_Create with args时,自动调用对应的 RPC 或 HTTP 端点
你不需要写任何「如果用户想要发邮件,就调用邮件服务」的逻辑——LLM 从工具描述的语义中自主推理出应该调用哪个服务。
组件三:对话记忆(Conversation Memory)
工具调用不是一次性的——用户可能在第一次查询后追问细节。要支持多轮对话,需要一个简单的消息累加器。
hist := ai.NewHistory(50)
核心设计:
- 消息列表:按顺序存储
user/assistant/tool消息 - 容量限制:超出限制时丢弃最早的上下文,避免 Token 溢出
- 追加模式:每个用户输入和模型回复都追加到列表,下次请求时一起发送
你不需要向量数据库或 RAG。对于 CLI 场景,一个简单的环形缓冲区 History 再加一个 Reset() 方法就足够了。消息列表的结构是标准的 OpenAI/Anthropic ChatML 格式,三行代码就能实现。
组件四:主循环(The Loop)
把前面三个组件串联起来,形成一个完整的 Agent 交互循环:
func ask(ctx context.Context, m ai.Model, hist *ai.History, tools []ai.Tool, prompt string) error {
// 1. 记录用户输入
hist.Add("user", prompt)
// 2. 调用模型:传入提示词 + 系统指令 + 工具清单 + 历史上下文
resp, err := m.Generate(ctx, &ai.Request{
Prompt: prompt,
SystemPrompt: systemPrompt,
Tools: tools,
Messages: hist.Messages(),
})
// 3. 打印文本回复
if resp.Reply != "" {
hist.Add("assistant", resp.Reply)
fmt.Println(resp.Reply)
}
// 4. 展示被调用的工具
for _, tc := range resp.ToolCalls {
args, _ := json.Marshal(tc.Input)
fmt.Printf(" → called %s(%s)\n", tc.Name, args)
}
// 5. 打印工具执行后的最终答案
if resp.Answer != "" {
hist.Add("assistant", resp.Answer)
fmt.Println(resp.Answer)
}
return nil
}
再包一个 REPL(Read-Eval-Print Loop),就有了一个完整的聊天 CLI:
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
if !scanner.Scan() { return }
line := strings.TrimSpace(scanner.Text())
switch line {
case "exit", "quit": return
case "reset": hist.Reset(); continue
default: ask(ctx, m, hist, discovered, line)
}
}
四个组件加在一起,核心逻辑只有约 40 行。加上 CLI 参数解析、环境变量读取、帮助文本,总共约 150 行。
为什么这么短?
能如此精简的原因在于框架的设计选择:
- 服务自描述:Handler 的 doc comment 自动成为工具描述,
@example标签给 LLM 提供使用提示。你不需要手写工具 Schema。 - Provider 统一:Anthropic、OpenAI、Gemini 等全部实现了同一个
Model接口,切换只需改一个字符串。 - 执行自动挂载:工具定义和执行在同一个对象上,减少了胶水代码。
如果你不是用微服务框架,而是裸 HTTP API,只需要额外 50 行:一个枚举端点的函数、一个按名称调用端点的函数。其他代码完全不变。
扩展方向
150 行是一个起点,你可以在此基础上增加:
- 确认步骤:在破坏性操作(删除记录)前,要求用户确认
- 审计日志:记录每次工具调用到日志系统或可观测性平台
- 工具过滤:限制 Agent 只能看到某些服务,实现权限控制
- 输入源替换:把 REPL 换成 Slack Bot——同一个
ask函数,不同的输入管道 - 事件触发:将 stdin 输入替换为事件驱动——这正是 Workflow 引擎做的事
总结
构建一个工具调用 Agent 并不需要复杂的框架。理解四个核心组件——工具发现、模型创建、对话记忆、主循环——你就可以用 150 行代码从零构建一个可用的 AI Agent CLI。核心思路是:让框架做自动化的部分(注册、Schema 提取、执行路由),你只需要写胶水代码(枚举、调用、展示)。
如果你正在开发自己的 Agent 工具链,不妨先从这个最小可行实现开始,逐步扩展。