从零到一手撸 Agent 系列 — 第 1 篇:一个 Coding Agent 是什么?

从零到一手撸 Agent 系列 --- 第 1 篇:一个 Coding Agent 是什么?

系列:从零到一手撸 Agent 系列

难度:🟢 入门

对应源码:cmd/cmd/cli/


开篇:你每天写代码,但你想过让 AI 帮你写吗?

2025 年,GitHub Copilot 已经拥有超过 200 万付费用户,Cursor 的估值突破百亿美元。AI 编码助手不再是科幻小说里的概念------它们就在你的编辑器里,每当你敲下几行注释,一个完整的函数就自动浮现出来。

但你有没有想过:这些东西,到底是怎么工作的?

它们不是魔法。拆开来,是一个"AI 大脑" + "一套工具手" + "一个沟通循环"。理解这些,你不仅能用好 AI 编码工具,还能自己动手造一个。

这个系列的目标,就是带你从零构建一个终端 AI 编码助手 ------用 Go 语言。代码和思路都来自一个真实可运行的开源项目 coding-agent。每篇文章会在上一篇的基础上增加一个新能力,到第 3 篇你就能拥有一个真正"能对话、能干活"的 Agent。

本篇我们先放眼全局:理解什么是 Coding Agent,拆解它的架构,再搭好项目的骨架。


一、Coding Agent 到底是什么?

先区分几个容易混淆的概念:

类型 代表产品 工作方式
代码补全 GitHub Copilot、Codeium 在编辑器里,根据上下文续写下一段代码。通常只做"填空"。
AI 编辑器 Cursor、Windsurf 把 AI 嵌入整个编辑流程:可以修改多文件、运行终端命令、查看 lint 错误。
Coding Agent Claude Code、aider、本项目 对话驱动:你说"帮我重构这个模块",Agent 自主决定读哪些文件、运行哪些命令、怎么修改。它像一个能操作你电脑的程序员搭档。

一个 Coding Agent 的核心工作流程:

关键在这:Agent 不只是"回答问题",而是"主动调用工具、多轮迭代、最终产出结果"


二、架构全景:一个 Coding Agent 由哪些部分组成?

用一张图看清本项目 coding-agent 的全貌:

模块清单及本系列对应文章:

模块 职责 对应文章
CLI 命令行入口、子命令分发 本篇
Provider 封装 LLM API 调用(OpenAI/Anthropic) 第 2 篇
Agent 核心 主循环、消息管理、System Prompt 第 3 篇
工具系统 文件读写、Shell 执行、搜索等 23 个工具 第 4--5 篇
权限管线 三级安全检查(黑名单/确认/目录边界) 第 6 篇
JSON-RPC 自研 RPC 传输层(供 LSP/MCP 共享) 第 7 篇
LSP 客户端 代码智能(跳转定义、查找引用等) 第 8 篇
子代理 派生子 Agent 处理复杂子任务 第 9 篇
记忆系统 Session 持久化 + BM25 长期记忆 第 10 篇
上下文压缩 三层递进压缩(soft/compact/snip) 第 11 篇
TUI Bubble Tea 全屏终端界面 第 12 篇
MCP 外部工具协议集成 第 13 篇
Skill & Hook 可复用技能 + Shell 扩展钩子 第 14 篇

不用被这张图吓到------我们不会一开始就塞进所有东西。实际上,本篇只搭一个空壳 CLI,后面的文章一块一块地填进来。


三、选型:为什么用 Go?

你可能在犹豫:写 LLM 应用不是该用 Python 吗?

Python 生态确实丰富(langchain、llama-index 等),但 Go 有几个独特优势很适合做一个终端 agent:

  1. 单二进制分发go build 直接出一个可执行文件,用户不用装 Python 解释器、不用折腾虚拟环境
  2. 并发模型优秀:Agent 经常需要同时做多件事(流式读 LLM 响应 + 执行后台工具 + 渲染 UI),Go 的 goroutine + channel 非常自然
  3. 启动快、内存低:终端工具要求"秒开",Go 编译后启动几乎没有延迟
  4. 交叉编译简单GOOS=linux GOARCH=amd64 go build 一行命令就能编译出 Linux 二进制

而且你会发现:Go 写 LLM 应用的代码量并没有想象中多。这个项目的 Provider 层、工具系统、Agent 循环,都是用 Go 标准库 + 极少量第三方依赖实现的。本系列在涉及 Go 语法的地方都会加注释说明,所以你不需要是 Go 专家也能看懂。


四、搭骨架:项目初始化

好,现在打开终端,我们从零开始。

4.1 创建项目

bash 复制代码
mkdir coding-agent
cd coding-agent
go mod init github.com/你的用户名/coding-agent

4.2 安装依赖

bash 复制代码
go get github.com/spf13/cobra@v1.9.1
go get github.com/joho/godotenv@v1.5.1

我们只用两个第三方库:

  • cobra :Go 生态里最流行的 CLI 框架,负责子命令(chat/once/tui)和 flag 管理
  • godotenv :从 .env 文件加载环境变量,让 API Key 不需要每次都手敲

后面会陆续引入 Bubble Tea(TUI)、Chroma(语法高亮)等,但现在只需要这些。

4.3 目录结构

go 复制代码
coding-agent/
├── cmd/
│   ├── main.go          ← 入口:加载 .env → 启动 Cobra
│   └── cli/
│       ├── root.go      ← Cobra rootCmd + 全局 flags
│       ├── once.go      ← "once" 子命令(一次性对话)
│       ├── chat.go      ← "chat" 子命令(REPL 交互)
│       └── tui.go       ← "tui" 子命令(全屏界面)
├── internal/            ← 所有核心逻辑(后续文章逐步填充)
├── .env                 ← API Key 等私密配置
├── go.mod
└── go.sum

cmd/ 只做"组装"------把 internal/ 里的各个模块拼起来。internal/ 里才是真正的业务逻辑。这是 Go 项目的惯用分层。


五、写代码:CLI 骨架

5.1 入口文件:cmd/main.go

go 复制代码
package main

import (
    "flag"
    "fmt"
    "os"

    "github.com/joho/godotenv"
    "github.com/你的用户名/coding-agent/cmd/cli"
)

func main() {
    // 第 1 步:用标准库 flag 抢在 Cobra 之前解析 --env
    envPath, rest, err := parseEnvFlag(os.Args[1:])
    if err != nil {
        fmt.Fprintln(os.Stderr, "Error:", err)
        os.Exit(2)
    }
    os.Args = append([]string{os.Args[0]}, rest...)

    // 第 2 步:加载 .env 文件
    if err := loadDotenv(envPath); err != nil {
        fmt.Fprintln(os.Stderr, "Error:", err)
        os.Exit(1)
    }

    // 第 3 步:启动 Cobra
    cli.Execute()
}

// parseEnvFlag 用标准库 flag 抢出 --env/-e,剩余参数透传给 Cobra
func parseEnvFlag(args []string) (envPath string, rest []string, err error) {
    fs := flag.NewFlagSet("coding-agent", flag.ContinueOnError)
    fs.StringVar(&envPath, "env", ".env", "dotenv 文件路径")
    fs.StringVar(&envPath, "e", ".env", "dotenv 文件路径(简写)")
    if err := fs.Parse(args); err != nil {
        return "", nil, err
    }
    return envPath, fs.Args(), nil
}

// loadDotenv 加载指定的 .env 文件(找不到不报错,允许只用系统环境变量)
func loadDotenv(envPath string) error {
    if envPath == "-" { // --env - 表示禁用 .env
        return nil
    }
    err := godotenv.Load(envPath)
    if err == nil || os.IsNotExist(err) {
        return nil // 文件不存在:静默跳过
    }
    return fmt.Errorf("加载 %s 失败: %w", envPath, err)
}

两个设计要点:

  • 为什么用标准库 flag 而不是 Cobra 解析 --env 因为 .env 必须在 Cobra 初始化之前加载------Cobra 的 flag 依赖环境变量提供默认值。
  • 找不到 .env 不报错。 用户可能只用 shell 环境变量(export OPENAI_API_KEY=...),不应该因为没有 .env 文件就拒绝启动。

5.2 Cobra 根命令:cmd/cli/root.go

go 复制代码
package cli

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "coding-agent",
    Short: "一个终端 AI 编码助手",
    Long: `coding-agent 是一个 Go 实现的 Coding Agent,
支持 REPL 和全屏 TUI 两种交互模式。

子命令:
  once  一次性对话
  chat  交互式 REPL
  tui   Bubble Tea 全屏界面`,
    SilenceUsage:  true, // 出错时不打印 usage(太啰嗦)
    SilenceErrors: true, // 我们自己处理错误输出
}

func init() {
    // 全局 flag:所有子命令都继承
    rootCmd.PersistentFlags().StringP("provider", "P", "",
        "LLM Provider: openai 或 anthropic")
    rootCmd.PersistentFlags().StringP("model", "M", "",
        "模型名(如 gpt-4o、claude-sonnet-4-20250514)")
    rootCmd.PersistentFlags().StringP("base-url", "u", "",
        "API Base URL(用 DeepSeek 等第三方 API 时指定)")
    rootCmd.PersistentFlags().StringP("workdir", "w", "",
        "工作目录(默认当前目录)")
    rootCmd.PersistentFlags().IntP("max-turns", "t", 0,
        "最大对话轮数(默认 100)")
    rootCmd.PersistentFlags().BoolP("trust", "Y", false,
        "信任模式:自动批准所有权限请求")
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, "Error:", err)
        os.Exit(1)
    }
}

PersistentFlags() 注册的 flag 会被所有子命令(oncechattui)自动继承。这样用户无论用哪个子命令,都能统一指定 --model--provider 等参数。

5.3 占位子命令:cmd/cli/once.go

本篇不会真的调用 LLM------我们先放一个占位实现,只要能打印两行日志就行。这很关键:让我们尽早验证"项目能编译、能跑起来"

go 复制代码
package cli

import (
    "fmt"
    "github.com/spf13/cobra"
)

var onceCmd = &cobra.Command{
    Use:   "once",
    Short: "一次性对话:执行一次后退出",
    RunE:  runOnce,
}

var onceMessage string

func init() {
    rootCmd.AddCommand(onceCmd)
    onceCmd.Flags().StringVarP(&onceMessage, "message", "m", "",
        "用户输入(必填)")
    onceCmd.MarkFlagRequired("message")
}

func runOnce(cmd *cobra.Command, args []string) error {
    // 本期只是占位,打印参数确认能跑通
    fmt.Printf("[coding-agent] once 模式已启动\n")
    fmt.Printf("[coding-agent] 你的输入: %s\n", onceMessage)
    fmt.Printf("[coding-agent] Provider: %s\n",
        cmd.Flag("provider").Value.String())
    fmt.Printf("[coding-agent] 模型: %s\n",
        cmd.Flag("model").Value.String())
    fmt.Println("[coding-agent] (下篇开始接入 LLM,敬请期待)")
    return nil
}

5.4 同样地,放一个占位的 chattui

go 复制代码
// cmd/cli/chat.go
package cli

import (
    "fmt"
    "github.com/spf13/cobra"
)

var chatCmd = &cobra.Command{
    Use:   "chat",
    Short: "交互式 REPL(开发中)",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("[coding-agent] REPL 模式将在后续文章实现")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(chatCmd)
}
go 复制代码
// cmd/cli/tui.go
package cli

import (
    "fmt"
    "github.com/spf13/cobra"
)

var tuiCmd = &cobra.Command{
    Use:   "tui",
    Short: "Bubble Tea 全屏界面(开发中)",
    RunE: func(cmd *cobra.Command, args []string) error {
        fmt.Println("[coding-agent] TUI 模式将在第 12 篇实现")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(tuiCmd)
}

六、运行验证

配一个 .env 文件:

bash 复制代码
OPENAI_API_KEY=sk-your-key-here
# 如果用的是 DeepSeek 等兼容 API:
# OPEN_BASE_URL=https://api.deepseek.com/v1

编译并运行:

bash 复制代码
# 编译
go build -o coding-agent ./cmd

# 单次对话模式(目前只是占位输出)
./coding-agent once -m "hello world"

# 查看帮助
./coding-agent --help
./coding-agent once --help

# REPL 和 TUI 目前只打印提示
./coding-agent chat
./coding-agent tui

如果一切正常,你会看到类似输出:

csharp 复制代码
[coding-agent] once 模式已启动
[coding-agent] 你的输入: hello world
[coding-agent] Provider: openai
[coding-agent] 模型: gpt-4o-mini
[coding-agent] (下篇开始接入 LLM,敬请期待)

小结

这篇文章我们做了一件事:搭好了项目的骨架。你现在有一个能编译、能跑、有三个子命令的 CLI 工具。虽然它还不会"思考",但它的目录结构和模块边界已经被清晰地定义出来了。

你学到了什么 对应代码
Coding Agent 的概念和架构全景 第二节的架构图
Go 项目初始化与目录结构约定 go mod init + 目录树
Cobra CLI 框架的基本用法 root.goPersistentFlags
.env 环境变量加载策略 main.goparseEnvFlag + loadDotenv
全局 flag 的继承机制 PersistentFlags() 让所有子命令自动继承

下篇预告 :我们将实现 Provider 抽象层------让项目真正接入 LLM。你会学到 OpenAI API 的 Chat Completions 协议、流式传输的 SSE 解析、以及如何用策略模式同时支持 OpenAI 和 Anthropic 两个 Provider。届时,你的 Agent 将第一次"开口说话"。


关联源码cmd/main.go · cmd/cli/root.go · cmd/cli/once.go


🐙 本教程所有源码均来自开源项目 github.com/wsx864321/c...,欢迎 Star ⭐ 支持!你的每一个 Star 都是持续更新的动力~

相关推荐
名侦探7222 小时前
Agent 的工作原理与理解方式
agent
JaydenAI2 小时前
[Loop Engineering在MAF中的实现-04]利用AIJudgeLoopEvaluator让LLM当评委
ai·agent·maf·loopagent·循环工程
AINative软件工程2 小时前
LLM 并发工具调用的 5 个生产陷阱:幂等性、竞态与失败补偿
llm·agent·ai编程
leeyi2 小时前
Graph 执行引擎源码:Eino 的节点调度是怎么工作的(第56篇-E42)
agent·ai编程·graphql
TrisighT2 小时前
让 Claude 自己管自己:Hooks 拦截 rm -rf 的那一次救场
aigc·agent·ai编程
潘锦3 小时前
从碎片检索到深度推理:如何重塑 AI Agent 的知识引擎
agent
带刺的坐椅4 小时前
用 Solon TeamAgent 落地供应链异常管理与自动补货
java·ai·llm·agent·solon·供应链
深海鱼肝油ya14 小时前
小说自动生成系统(二)
人工智能·大模型·agent·智能体·自动化编程·小说生成系统
小林ixn14 小时前
会议纪要自动化:用 Claude Skill 把重复工作“卷”成一行命令
agent·自动化运维