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

12 阅读9分钟

从零到一手撸 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 AgentClaude Code、aider、本项目对话驱动:你说"帮我重构这个模块",Agent 自主决定读哪些文件、运行哪些命令、怎么修改。它像一个能操作你电脑的程序员搭档。

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

agent-workflow.png

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


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

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

architecture.png

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

模块职责对应文章
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 篇
TUIBubble 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 创建项目

mkdir coding-agent
cd coding-agent
go mod init github.com/你的用户名/coding-agent

4.2 安装依赖

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 目录结构

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

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

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——我们先放一个占位实现,只要能打印两行日志就行。这很关键:让我们尽早验证"项目能编译、能跑起来"

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

// 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)
}
// 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 文件:

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

编译并运行:

# 编译
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

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

[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 都是持续更新的动力~