我最近在思考一个很实际的问题:如果 Go 和 AI Agent 都刚开始学,能不能把两件事合成一个项目,从零做出自己的 Agent?
答案是可以,而且这可能比“先学完 Go,再研究 Agent”更有效。Go 的语法并不算庞大,Agent 的核心结构也没有想象中神秘。只要项目边界足够小,就能让每一个新知识点马上变成看得见的功能。
但这里有一个重要前提:第一版不要做万能助手,也不要从多 Agent 开始。
这篇文章选择“个人任务助理”作为贯穿全程的项目。它从一个只会保存待办事项的命令行程序开始,逐步加入模型对话、工具调用、状态存储、HTTP 接口和安全控制。六周后得到的不只是一个演示,而是一套能继续扩展的 Agent 后端骨架。
先确定最终要做什么
这个项目最终支持三类对话:
“帮我添加一个任务:周五前写完项目总结”
“列出还没有完成的任务”
“把任务 3 标记为已完成”
对用户而言,它像一个简单的聊天助手;对程序而言,它有三个明确工具:
create_task
list_tasks
complete_task
模型不直接修改数据库。它只负责理解用户意图并生成结构化的工具调用,Go 程序负责校验参数、执行操作和返回真实结果。
完整数据流如下:
用户输入
↓
模型判断下一步
├─ 不需要工具 → 直接回答
└─ 需要工具 → 返回工具名称与 JSON 参数
↓
Go 校验并执行
↓
工具结果返回模型
↓
继续判断或最终回答
这就是最小 Agent 循环。关于模型、工具、状态和循环的完整拆解,可以先阅读 AI Agent 第一章:从对话到行动。
为什么这个项目适合同时学习 Go 和 Agent
个人任务助理的业务不复杂,却刚好覆盖了 Go 后端和 Agent 开发最重要的基础能力。
| 项目功能 | 对应的 Go 知识 | 对应的 Agent 知识 |
|---|---|---|
| 定义任务 | struct、切片、方法 |
结构化业务状态 |
| 保存任务 | 文件、SQLite、错误处理 | 长期状态 |
| 接收对话 | JSON、net/http |
用户输入边界 |
| 调用模型 | HTTP 客户端、context |
Prompt、Responses API |
| 执行工具 | 接口、map、JSON 解码 |
Tool Calling、工具注册 |
| 限制行为 | 超时、校验、测试 | 最大步数、审批、安全边界 |
项目会迫使我们在真实场景中使用这些知识,而不是只记语法。例如,学到 context.Context 时,不只是知道它能取消请求,而是让用户断开连接后,模型调用和工具执行也能一起停止。
开始前需要学到什么程度
不需要先学完 Go。开始项目之前,只要能看懂下面这些概念:
- 变量、函数、
if和for; struct、切片和map;- 函数可以返回多个值;
- Go 使用
error显式处理错误; - 包、模块和
go run的基本用法。
goroutine、Channel、反射和复杂设计模式都可以以后再学。第一版 Agent 是串行循环,暂时不需要并发编排。
开发环境可以这样确认:
go version
mkdir go-task-agent
cd go-task-agent
go mod init example.com/go-task-agent
建议每个阶段都保持以下命令可通过:
gofmt -w .
go test ./...
go vet ./...
go run ./cmd/agent
第一周:先写一个没有 AI 的任务程序
第一周完全不调用模型。目标是用 Go 写出一个可靠的命令行任务管理器。
先定义最小数据结构:
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}
type TaskStore interface {
Create(title string) (Task, error)
List() ([]Task, error)
Complete(id int) (Task, error)
}
第一版可以用内存切片保存任务。这个阶段重点不是数据库,而是理解:
- 什么时候用结构体;
- 值接收者和指针接收者有什么区别;
- 接口如何隔离业务逻辑和存储方式;
- 错误如何从底层逐层返回;
- 如何为
Create、List和Complete写测试。
第一周的验收标准很简单:程序能添加、列出和完成任务,非法 ID 会返回明确错误,核心逻辑有单元测试。
第二周:整理成可维护的 Go 项目
功能跑通后,再把代码从 main.go 中拆出来:
go-task-agent/
├── cmd/agent/main.go
├── internal/agent/
├── internal/tools/
├── internal/store/
│ ├── store.go
│ └── memory.go
├── go.mod
└── README.md
cmd/agent 只负责组装依赖和启动程序;store 管理任务数据;tools 负责把业务能力包装成 Agent 可调用的工具;agent 最终负责运行循环。
这一周需要补上三个工程能力:
使用 context.Context
模型请求、数据库调用和外部 HTTP 请求都应该接收同一个 context.Context。不要为了方便在业务函数里重新使用 context.Background(),否则上层取消无法继续向下传播。
包装错误
使用 %w 保留底层原因:
task, err := store.Complete(id)
if err != nil {
return Task{}, fmt.Errorf("complete task %d: %w", id, err)
}
用接口替换实现
内存存储和未来的 SQLite 存储都实现 TaskStore。业务层依赖接口,就能在测试时使用内存版本,在运行时切换成数据库版本。
第三周:接入模型,但先不要加工具
接下来只做一轮普通对话,先打通 Go 到模型 API 的链路。
OpenAI 当前提供官方 Go SDK,但官方文档仍将它标注为 beta。Go 目前没有与 Python、TypeScript 对等的官方 Agents SDK,因此这条路线使用 Go SDK 调用 Responses API,并由我们自己控制 Agent 循环。
安装 SDK:
go get github.com/openai/openai-go/v3
export OPENAI_API_KEY="你的 API Key"
最小调用如下:
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := client.Responses.New(
ctx,
responses.ResponseNewParams{
Model: "gpt-5.6",
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("请用一句话解释什么是任务优先级"),
},
},
)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.OutputText())
}
API Key 只能放在服务端环境变量或密钥管理系统中,不能写进源码、前端代码或 Git 仓库。
这一阶段只验证四件事:SDK 初始化、请求超时、API 错误处理和文本输出。暂时不要同时引入流式响应、历史消息和工具调用,否则一旦失败,很难判断是哪一层出了问题。
第四周:实现第一个 Agent 工具循环
模型能回答问题之后,再把任务操作包装成工具。一个工具至少要有:
- 稳定且清楚的名称;
- 告诉模型何时调用的描述;
- JSON Schema 参数定义;
- Go 参数结构体;
- 实际执行函数。
以创建任务为例:
type CreateTaskArgs struct {
Title string `json:"title"`
}
type ToolResult struct {
OK bool `json:"ok"`
Message string `json:"message"`
}
工具收到 JSON 后要再次严格解码和校验。模型生成的参数不能被当成可信输入:
func decodeCreateTask(raw []byte) (CreateTaskArgs, error) {
var args CreateTaskArgs
decoder := json.NewDecoder(bytes.NewReader(raw))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&args); err != nil {
return args, fmt.Errorf("decode arguments: %w", err)
}
args.Title = strings.TrimSpace(args.Title)
if args.Title == "" {
return args, errors.New("title is required")
}
return args, nil
}
Agent Runner 的职责可以概括成下面这段伪代码:
for step := 0; step < maxSteps; step++ {
reply := callModel(messages, toolDefinitions)
if reply.HasFinalText() {
return reply.FinalText()
}
for _, call := range reply.ToolCalls() {
result := executeTool(call.Name, call.Arguments)
messages = append(messages, toolResult(call.ID, result))
}
}
return errors.New("agent exceeded maximum steps")
第一版把 maxSteps 设置为 5 就够了。最大步数不是优化项,而是防止重复工具调用失控的基本边界。
工具实现还需要超时、错误分类、输出裁剪和高风险操作确认。这部分可以继续阅读 AI Agent 第二章:可靠工具调用。
第五周:加入 HTTP 接口和持久化状态
命令行版本稳定后,再增加一个最小 HTTP 服务:
POST /chat
GET /healthz
POST /chat 接收:
{
"session_id": "user-123",
"message": "把写周报加入任务"
}
返回:
{
"reply": "已添加任务:写周报"
}
这一阶段学习 net/http、JSON 编解码、状态码、请求大小限制和服务端超时。先使用标准库即可,不需要急着引入 Web 框架。
任务数据可以从内存迁移到 SQLite,但要把两类状态分开:
- 业务状态:任务本身,必须可靠持久化;
- 对话状态:用户和模型的历史,需要裁剪、摘要或按需加载。
不要把全部历史无限追加到每次请求中。上下文越长,调用成本和噪声越高。对于任务助理,数据库中的真实任务才是事实来源,对话历史只用于理解最近几轮表达。
第六周:补上安全、评测和可观测性
能运行不等于可靠。最后一周不再增加显眼功能,而是让已有能力可以被验证。
给工具分级
读取类:list_tasks 可以自动执行
写入类:create_task 执行后明确告知用户
敏感类:delete_all_tasks 必须人工确认
模型的 Prompt 只能提供行为建议,不能代替程序权限控制。删除数据、付款、发送外部消息等操作,应由 Go 代码暂停执行并等待明确审批。
建立评测集
准备至少 30 条固定输入,覆盖:
- 正常创建、查询和完成任务;
- 含糊表达和缺少参数;
- 不存在的任务 ID;
- 诱导模型绕过权限;
- 工具超时和数据库失败;
- 连续调用导致的最大步数限制。
每次修改 Prompt、工具描述或模型后,都重新运行同一批用例。不要只凭几次手工对话判断 Agent 是否变好了。
记录必要信息
至少记录请求 ID、会话 ID、模型耗时、工具名称、工具耗时、结果状态和总步数。日志中不要保存 API Key、完整鉴权头和未经处理的隐私数据。
每天应该怎么学
可以把每天一到两个小时拆成三个部分:
20 分钟:学习当天需要的 Go 概念
60 分钟:把概念放进项目并运行
20 分钟:写测试、整理错误和学习记录
遇到问题时,先判断它属于哪一层:
Go 编译错误?
JSON 结构错误?
HTTP 请求错误?
模型没有选择工具?
工具选择正确但执行失败?
工具成功但结果没有回传?
这种分层排查比反复修改 Prompt 更有效。Agent 系统的大部分问题并不都来自模型,很多时候只是 JSON 字段不匹配、上下文没有继续传递,或者工具错误被吞掉了。
六周验收清单
完成下面这些项目,就可以认为第一阶段学习闭环已经建立:
- 能解释 Chatbot 与 Agent 的区别;
- 能独立写出 Go 结构体、接口和错误处理;
- 能使用
context.Context传递超时和取消; - 能通过官方 Go SDK 调用 Responses API;
- 能注册并执行至少三个工具;
- 能把工具结果交还模型继续运行;
- 能限制 Agent 最大执行步数;
- 能把任务保存到 SQLite;
- 能提供一个最小
/chatHTTP 接口; - 能为工具写单元测试和失败用例;
- 能让危险操作等待人工确认;
- 能通过日志看清一次运行调用了哪些工具。
最容易走偏的几个地方
一开始就做多 Agent
单 Agent 的状态、工具和错误还没理清时,多 Agent 只会让问题更难定位。只有不同任务确实需要不同工具、权限或专业指令时,再拆分角色。
把所有逻辑都写进 Prompt
Prompt 不是权限系统,也不是参数校验器。能由 Go 类型、Schema、超时和权限代码确定的规则,就不应该只依赖模型遵守。
给 Agent 一个无限制 Shell
学习阶段应该从三个业务工具开始,而不是直接允许任意命令执行。工具权限越小,行为越容易理解和测试。
过早引入框架和向量数据库
任务助理的事实在 SQLite 中,不需要先做知识库。等项目真正需要检索大量文档时,再增加文件搜索或向量检索。
只验证“它回答了”
真正需要验证的是:它是否选择了正确工具、参数是否准确、失败是否可恢复、危险操作是否被阻止,以及运行是否在预算内停止。
接下来怎么继续
走完这条路线后,可以沿三个方向扩展:
- 产品方向:流式响应、登录、Web UI、消息平台接入;
- 工程方向:PostgreSQL、Redis、任务队列、追踪和部署;
- Agent 方向:知识检索、MCP、可恢复审批和多 Agent 分工。
顺序仍然应该是:先让单个工具可靠,再让单个 Agent 可靠,最后才讨论多个 Agent 如何协作。
OpenAI 官方文档也给出了相同的路线边界:使用 Responses API 时,应用自己管理工具调用和循环;使用 Agents SDK 时,SDK 代为管理循环。目前 Go 更适合前一种方式。可以参考 OpenAI Go SDK 和 Responses API 与 Agents SDK 的区别。
从零开始并不意味着先把所有知识学完。更可行的方法是先做一个小闭环,让每一周都产生可运行、可测试、可以继续扩展的结果。六周只是节奏参考,真正重要的是始终保持项目边界清楚,并且每一步都有证据证明它确实工作。