从零到一手撸 Agent 系列 — 第 1 篇:一个 Coding Agent 是什么?
开篇:你每天写代码,但你想过让 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:
- 单二进制分发:
go build直接出一个可执行文件,用户不用装 Python 解释器、不用折腾虚拟环境 - 并发模型优秀:Agent 经常需要同时做多件事(流式读 LLM 响应 + 执行后台工具 + 渲染 UI),Go 的 goroutine + channel 非常自然
- 启动快、内存低:终端工具要求"秒开",Go 编译后启动几乎没有延迟
- 交叉编译简单:
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 会被所有子命令(once、chat、tui)自动继承。这样用户无论用哪个子命令,都能统一指定 --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 同样地,放一个占位的 chat 和 tui
// 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.go 的 PersistentFlags |
.env 环境变量加载策略 | main.go 的 parseEnvFlag + 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 都是持续更新的动力~