系列「企业级 AI Agent 实现拆解」E31 篇,Part 9 起步篇第一章。前面 30 篇拆解了 Eino 和 DeepFlux 的内部机制。从这里开始换个姿态:从零动手,把 Agent 真的跑起来。
读完这篇你会得到
- 一个能跑的 Go Agent 项目:30 行核心代码
- ChatModel + Tool + ReAct 三者怎么串在一起
- 用 DeepSeek(国内可用、最便宜)作为模型入口
- 看懂终端输出:Agent 在"想"什么
准备工作
你需要:
- Go 1.21 及以上(
go version验证) - DeepSeek API Key:去 platform.deepseek.com 注册,新用户有免费额度
- 一个能联网的终端
为什么选 DeepSeek? 它的 API 兼容 OpenAI 格式,只需换一个 BaseURL,价格比 GPT-4o 便宜 10 倍以上,特别适合调试。
第一步:初始化项目
mkdir my-first-agent && cd my-first-agent
go mod init my-first-agent
安装依赖:
go get github.com/cloudwego/eino@latest
go get github.com/cloudwego/eino-ext/components/model/openai@latest
eino 是核心框架,eino-ext 里装了各个模型的驱动,这里用 openai 包驱动 DeepSeek(两者 API 格式相同)。
第二步:写一个 Tool
Tool 是 Agent 能调用的"工具"——类似函数,但 Agent 会根据任务自己决定要不要调它、怎么调。
新建 main.go:
package main
import (
"context"
"fmt"
"io"
"os"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/components/tool/utils"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/flow/agent/react"
"github.com/cloudwego/eino/schema"
)
// 工具的输入参数:用 struct 定义,字段注释变成 JSON Schema 描述
type WeatherParams struct {
City string `json:"city" jsonschema_description:"城市名,如 北京、上海"`
}
// 工具的执行逻辑:返回 (结果字符串, error)
func getWeather(_ context.Context, p *WeatherParams) (string, error) {
// 这里可以调真实天气 API,演示先硬编码
return fmt.Sprintf("%s 今天晴,气温 25°C,适合出行。", p.City), nil
}
工具定义只需要三样东西:输入结构体、执行函数、一个名字和描述。
第三步:创建 ChatModel
func newChatModel(ctx context.Context) *openai.ChatModel {
cm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
BaseURL: "https://api.deepseek.com", // DeepSeek 的 OpenAI 兼容地址
Model: "deepseek-chat",
APIKey: os.Getenv("DEEPSEEK_API_KEY"), // 从环境变量读,别写死在代码里
})
if err != nil {
panic(err)
}
return cm
}
openai.NewChatModel 本来是给 GPT 用的,但因为 DeepSeek 的接口格式一样,换个 BaseURL 就能用。这就是"接口兼容"带来的好处。
第四步:把 ChatModel + Tool 组成 Agent
func main() {
ctx := context.Background()
// 1. 把函数包成 Tool
weatherTool, err := utils.InferTool(
"get_weather", // 工具名(Agent 会看到这个名字)
"查询指定城市的天气", // 工具描述(Agent 用它决定什么时候调)
getWeather,
)
if err != nil {
panic(err)
}
// 2. 创建 ChatModel
chatModel := newChatModel(ctx)
// 3. 用 react.NewAgent 把两者组装成一个 ReAct Agent
agent, err := react.NewAgent(ctx, &react.AgentConfig{
ToolCallingModel: chatModel,
ToolsConfig: compose.ToolsNodeConfig{
Tools: []tool.BaseTool{weatherTool},
},
})
if err != nil {
panic(err)
}
// 4. 发一条消息,用 Stream 得到流式回复
sr, err := agent.Stream(ctx, []*schema.Message{
{Role: schema.User, Content: "北京今天天气怎么样?适合去颐和园吗?"},
})
if err != nil {
panic(err)
}
defer sr.Close()
// 5. 逐 token 打印(打字机效果)
for {
msg, err := sr.Recv()
if err == io.EOF {
break
}
if err != nil {
panic(err)
}
fmt.Print(msg.Content) // 不换行,token 一个个蹦出来
}
fmt.Println()
}
运行
export DEEPSEEK_API_KEY="你的 key"
go run main.go
你会看到类似这样的输出:
北京今天晴,气温 25°C,适合出行。颐和园户外游览非常适合,
建议上午前往,避开正午高温。记得带遮阳帽和防晒霜……
发生了什么?
这 30 行代码背后,Agent 完成了一次完整的 ReAct 循环:
用户消息
↓
[思考] LLM 判断:需要查天气才能回答
↓
[行动] 调用 get_weather(city="北京")
↓
[观察] 工具返回:北京今天晴,25°C
↓
[思考] LLM 结合观察生成最终回复
↓
流式输出给你
react.NewAgent 把这个循环封装好了。你给它 ChatModel 和 Tool,它处理"什么时候调工具、调什么、把结果填回上下文再想一轮"这些细节。
utils.InferTool 做了什么
你写的是一个普通 Go 函数 func(context.Context, *WeatherParams) (string, error),utils.InferTool 把它变成 Agent 认识的 tool.InvokableTool:
- 用反射读取
WeatherParams的字段和jsonschema_descriptiontag,生成 JSON Schema - 把这个 Schema 告诉 LLM:"这个工具接受
{city: string}参数" - 当 LLM 决定调工具时,把 LLM 返回的 JSON 参数反序列化成
*WeatherParams,调你的函数
你只需要关心业务逻辑,不需要手写 JSON Schema。
三行改动就能换模型
想换成 GPT-4o?
BaseURL: "", // 删掉 BaseURL,用默认
Model: "gpt-4o",
APIKey: os.Getenv("OPENAI_API_KEY"),
想换成 Claude?后面会专门讲。想本地跑 Llama?E42 会讲 Ollama 接入。模型切换不影响工具和 Agent 逻辑。
完整代码清单
my-first-agent/
├── go.mod
└── main.go ← 全部 70 行(含注释)
不需要 YAML,不需要配置框架,不需要数据库。这就是 Eino 的最小可运行形态。
小结
一个可用的 AI Agent = ChatModel(大脑) + Tool(手) + ReAct 循环(神经)。
Eino 把 ReAct 循环封在 react.NewAgent 里,你只需要:
openai.NewChatModel接上模型utils.InferTool包一个函数成工具react.NewAgent组装起来agent.Stream发消息、收回复
下一篇讲 Go 项目怎么组织——从脚本式的单文件,到 DDD 4 层结构,什么时候该分层、什么时候过度设计。