2026年6月6日 2 分钟阅读

从零构建 AI Agent CLI:150 行代码揭示工具调用四大组件

tinyash 0 条评论

问题:如何让 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),
)

这里做了两件事:

  1. Provider 选择:Anthropic、OpenAI、Gemini、Groq、Mistral——统一的 Model 接口,切换只需改一个字符串
  2. 执行链路挂载:当 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 行。

为什么这么短?

能如此精简的原因在于框架的设计选择

  1. 服务自描述:Handler 的 doc comment 自动成为工具描述,@example 标签给 LLM 提供使用提示。你不需要手写工具 Schema。
  2. Provider 统一:Anthropic、OpenAI、Gemini 等全部实现了同一个 Model 接口,切换只需改一个字符串。
  3. 执行自动挂载:工具定义和执行在同一个对象上,减少了胶水代码。

如果你不是用微服务框架,而是裸 HTTP API,只需要额外 50 行:一个枚举端点的函数、一个按名称调用端点的函数。其他代码完全不变。

扩展方向

150 行是一个起点,你可以在此基础上增加:

  • 确认步骤:在破坏性操作(删除记录)前,要求用户确认
  • 审计日志:记录每次工具调用到日志系统或可观测性平台
  • 工具过滤:限制 Agent 只能看到某些服务,实现权限控制
  • 输入源替换:把 REPL 换成 Slack Bot——同一个 ask 函数,不同的输入管道
  • 事件触发:将 stdin 输入替换为事件驱动——这正是 Workflow 引擎做的事

总结

构建一个工具调用 Agent 并不需要复杂的框架。理解四个核心组件——工具发现、模型创建、对话记忆、主循环——你就可以用 150 行代码从零构建一个可用的 AI Agent CLI。核心思路是:让框架做自动化的部分(注册、Schema 提取、执行路由),你只需要写胶水代码(枚举、调用、展示)

如果你正在开发自己的 Agent 工具链,不妨先从这个最小可行实现开始,逐步扩展。

发表评论

你的邮箱地址不会被公开,带 * 的为必填项。