1. 项目概述为什么我们需要一个“代码优先”的AI Agent框架如果你最近在尝试用Go语言构建AI智能体可能会发现一个尴尬的局面市面上很多流行的Agent框架比如LangChain的Go版本或者一些基于OpenAI Assistant API封装的库用起来总感觉有点“隔靴搔痒”。它们要么抽象层级太高把底层逻辑藏得太深你想自定义一个工具Tool或者调整Agent的决策流程得翻半天文档最后可能还得去改框架源码要么就是设计上太“Pythonic”在Go这种强类型、编译型的语言环境里用起来不够顺手性能和并发优势也没完全发挥出来。这就是Google开源的Agent Development Kit for Go (ADK-Go)要解决的核心问题。它不是一个试图把所有功能都封装好的“黑盒”而是一个代码优先Code-First的工具包。你可以把它理解为一套精心设计的“乐高积木”和“搭建手册”。它提供了构建智能体所需的核心组件——比如与LLM大语言模型对话的抽象、工具的定义与调用、多智能体协作的编排逻辑——但如何组装这些组件实现怎样的业务逻辑控制权完全在你手里。这种设计哲学特别适合我们这些有工程背景的开发者。我们习惯了用代码明确地定义行为、编写单元测试、进行版本控制。ADK-Go让你能用写业务代码的方式去构建Agent而不是在配置文件或者UI界面上拖拽。举个例子当你定义一个“查询数据库”的工具时你就是在写一个标准的Go函数它的输入输出类型、可能发生的错误、需要哪些外部依赖都在代码里一目了然。这种透明度和可控性对于构建需要投入生产环境的、复杂的多智能体系统至关重要。2. 核心设计理念与架构拆解2.1 “代码优先”意味着什么“代码优先”是ADK-Go区别于许多其他框架的核心理念。它主要体现在以下几个方面逻辑即代码Agent的思考流程、工具调用策略、多Agent间的通信协议都不是通过配置文件或魔法字符串来定义而是通过实现具体的Go接口Interface和结构体Struct来完成。这意味着你的Agent逻辑可以享受Go语言的所有好处静态类型检查、IDE的智能补全和跳转、清晰的错误处理。极致的可测试性因为核心组件都是接口你可以轻松地为你的Agent编写单元测试和集成测试。你可以Mock LLM的响应模拟工具调用的结果从而在完全可控的环境下验证Agent在各种边界条件下的行为。这对于保证复杂系统的可靠性是不可或缺的。无缝集成现有代码库你的业务逻辑、数据访问层、第三方服务客户端都可以直接作为工具Tool被Agent调用。不需要为了适配框架而进行额外的、别扭的封装。ADK-Go的设计鼓励你将Agent能力作为应用程序的一个自然组成部分而不是一个孤立的、难以维护的“AI模块”。2.2 核心架构组件ADK-Go的架构非常清晰主要围绕几个核心接口展开。理解这些接口就掌握了使用它的钥匙。Agent接口这是智能体的心脏。一个Agent的核心是它的Run方法它接收一个上下文Context和一次对话轮次Turn然后决定下一步做什么——是直接回复用户还是调用某个工具或者是把任务交给另一个Agent。你需要实现这个接口来定义你专属的Agent行为。Tool接口工具是Agent延伸的手脚。一个工具本质上是一个具有明确输入输出描述的Go函数。ADK-Go通过Tool接口将其标准化使得LLM能够理解这个工具能做什么、需要什么参数。框架内置了一些常用工具如网络搜索、代码执行但更重要的是你可以用几行代码就把任何现有的函数“包装”成一个工具。Model接口这是与LLM交互的抽象层。虽然ADK-Go由Google出品天然对Gemini系列模型有很好的支持但它通过Model接口保持了模型无关性。理论上你可以为其实现OpenAI、Claude或其他任何模型的适配器让你自由选择最适合的后端。Orchestrator当你需要多个Agent协同工作时编排器就登场了。它负责管理Agent之间的对话流、任务分配和状态同步。ADK-Go提供了基础的编排模式也允许你实现更复杂的自定义工作流比如基于竞争或协作的多Agent系统。这种基于接口的松耦合设计使得每个组件都可以独立替换和扩展极大地提升了框架的灵活性。2.3 与生态的融合MCP与云原生ADK-Go的另一个亮点是它对现代开发生态的拥抱。模型上下文协议MCP集成MCP是一种新兴的协议旨在标准化服务器如数据库、API向AI客户端如Agent暴露其能力和上下文的方式。ADK-Go对MCP的支持意味着你的Agent可以动态地发现并利用那些实现了MCP协议的外部资源极大地扩展了其能力边界而无需硬编码。云原生基因这个项目源自Google因此对云原生部署有着天生的亲和力。你的Agent应用可以很容易地被封装成Docker容器部署到Google Cloud Run、Kubernetes等环境中。框架内部对并发、超时、重试等问题的处理也符合云原生应用的最佳实践。这对于需要弹性伸缩、高可用的生产级Agent服务来说是一个巨大的优势。3. 从零开始构建你的第一个智能体理论说得再多不如动手写一行代码。让我们从一个最简单的例子开始构建一个能查询天气的Agent。这个Agent只做一件事当用户询问天气时调用一个模拟的天气查询工具。3.1 环境准备与项目初始化首先确保你的Go版本在1.21或以上。然后创建一个新项目并引入ADK-Gomkdir my-first-agent cd my-first-agent go mod init my-first-agent go get google.golang.org/adk现在创建一个main.go文件。3.2 第一步定义工具Tool工具是Agent能力的基石。我们先定义一个模拟的天气查询工具。package main import ( context fmt time google.golang.org/adk/tools ) // WeatherTool 是一个模拟的天气查询工具 type WeatherTool struct{} // 实现 tools.Definition 接口描述这个工具 func (w WeatherTool) Definition() tools.Definition { return tools.Definition{ Name: get_weather, Description: 获取指定城市的当前天气情况。, InputSchema: tools.InputSchema{ type: object, properties: { city: { type: string, description: 城市名称例如北京上海San Francisco, }, }, required: [city], }, } } // 实现 tools.Tool 接口的 Execute 方法这是工具的实际逻辑 func (w WeatherTool) Execute(ctx context.Context, input map[string]any) (map[string]any, error) { // 从输入中获取城市参数 city, ok : input[city].(string) if !ok { return nil, fmt.Errorf(invalid city parameter) } // 这里是模拟的天气数据。真实场景中你会在这里调用一个天气API。 // 为了演示我们返回一个固定的响应。 weatherInfo : fmt.Sprintf(%s的天气晴朗温度22°C湿度65%%东南风2级。数据更新时间%s, city, time.Now().Format(2006-01-02 15:04:05)) return map[string]any{ weather: weatherInfo, }, nil }关键点解析Definition()方法返回一个JSON Schema它用LLM能理解的语言精确描述了工具的用途、所需参数及其类型。这是Agent能正确调用工具的前提。Execute()方法是工具的实际执行体。注意其输入是map[string]any输出也是。ADK-Go负责在LLM的调用和你的Go函数之间进行数据格式的转换。这里的错误处理也很重要任何执行失败都应该返回error以便Agent能进行后续处理如重试或向用户报错。3.3 第二步创建智能体Agent接下来我们创建一个最简单的Agent它只做一件事使用我们刚定义的天气工具。package main import ( context fmt log google.golang.org/adk google.golang.org/adk/agents google.golang.org/adk/tools ) // MyFirstAgent 结构体它将持有模型和工具列表 type MyFirstAgent struct { model adk.Model tools []tools.Tool } // 实现 agents.Agent 接口的 Run 方法 func (a *MyFirstAgent) Run(ctx context.Context, turn *agents.Turn) (*agents.Turn, error) { // 1. 从本轮对话中获取用户消息 userMsg : turn.Last() if userMsg nil || userMsg.Author ! agents.AuthorUser { return nil, fmt.Errorf(no user message found in this turn) } // 2. 这里是一个简化的逻辑如果用户输入包含“天气”关键词就调用天气工具。 // 在实际的Agent中这个决策应该由LLM根据对话历史和工具描述来做出。 // 为了简化示例我们直接进行字符串匹配。 userText : userMsg.Text if containsWeatherQuery(userText) { // 假设我们从用户消息中提取了城市这里简单写死为“北京”。 // 真实场景下这个信息也应该由LLM来提取。 toolInput : map[string]any{city: 北京} // 3. 执行工具 weatherTool : a.tools[0] // 我们知道只有一个工具 result, err : weatherTool.Execute(ctx, toolInput) if err ! nil { // 工具执行出错返回错误信息给用户 turn.Add(agents.NewTextMessage(agents.AuthorAssistant, fmt.Sprintf(查询天气时出错%v, err))) return turn, nil } // 4. 将工具执行结果构造成Agent的回复 weather, _ : result[weather].(string) reply : fmt.Sprintf(根据查询结果%s, weather) turn.Add(agents.NewTextMessage(agents.AuthorAssistant, reply)) return turn, nil } // 5. 如果不匹配返回一个默认回复 turn.Add(agents.NewTextMessage(agents.AuthorAssistant, 您好我是一个简单的天气查询助手请尝试问我关于天气的问题。)) return turn, nil } // 一个简单的辅助函数 func containsWeatherQuery(text string) bool { // 简单的关键词匹配实际应用需要更智能的自然语言理解 return containsAny(text, []string{天气, weather, 下雨, 晴天}) } func containsAny(s string, substrs []string) bool { for _, sub : range substrs { // 这里应该做更精细的中英文分词处理仅为示例 if strings.Contains(strings.ToLower(s), strings.ToLower(sub)) { return true } } return false }注意事项 这个Agent的实现非常“原始”它用硬编码的规则字符串匹配来决定行为。这展示了Agent接口的灵活性——你可以从任何逻辑开始。但在真实场景中Run方法的核心应该是将当前的对话状态Turn和可用的工具列表Tools提交给LLMModel让LLM决定下一步行动。我们下一步就来引入LLM让Agent真正“智能”起来。3.4 第三步引入LLM与完成智能决策现在我们升级Agent让它利用Gemini模型来理解用户意图并自动决定是否调用工具。package main import ( context fmt log strings google.golang.org/adk google.golang.org/adk/agents google.golang.org/adk/models/gemini // 引入Gemini模型包 google.golang.org/adk/tools ) func main() { ctx : context.Background() // 1. 初始化Gemini模型客户端 // 你需要设置环境变量 GOOGLE_API_KEY 或通过其他方式提供API密钥 model, err : gemini.NewModel(ctx, gemini.Config{ ModelName: gemini-2.0-flash-exp, // 选择一个合适的Gemini模型 }) if err ! nil { log.Fatalf(Failed to create model: %v, err) } // 2. 准备工具列表 toolList : []tools.Tool{WeatherTool{}} // 3. 使用ADK提供的标准“工具调用”Agent // 这个 agents.ToolCalling 是一个高级封装它内部会 // a. 将工具描述和对话历史发送给LLM。 // b. 解析LLM的响应判断是生成文本还是调用工具。 // c. 如果调用工具则执行工具并将结果再次发送给LLM形成多轮对话。 agent : agents.NewToolCalling(model, toolList...) // 4. 模拟一次对话 turn : agents.NewTurn() turn.Add(agents.NewTextMessage(agents.AuthorUser, 今天北京天气怎么样)) // 5. 运行Agent resultTurn, err : agent.Run(ctx, turn) if err ! nil { log.Fatalf(Agent run failed: %v, err) } // 6. 打印Agent的最终回复 for _, msg : range resultTurn.Messages { if msg.Author agents.AuthorAssistant { fmt.Printf(Agent: %s\n, msg.Text) } } }核心机制解析agents.NewToolCalling是ADK-Go提供的一个非常强大的“开箱即用”的Agent实现。它封装了与LLM协作进行工具调用的完整循环规划将当前对话历史和所有可用工具的描述来自Tool.Definition()发送给LLM。决策LLM分析后会返回一个结构化的响应指明下一步是“直接回答”还是“调用某个工具及其参数”。执行如果决定调用工具Agent就执行对应的Tool.Execute()方法。观察与再规划将工具执行的结果作为新的上下文再次发送给LLM让LLM生成面向用户的最终回答或决定下一步行动。 这个过程可能会循环多次直到LLM认为可以给出最终答案。这一切都在agent.Run()的一次调用中自动完成你无需手动管理这个循环。4. 进阶实战构建一个多智能体协作系统单一Agent的能力是有限的。真正的威力在于让多个各有所长的Agent协同工作。假设我们要构建一个“旅行规划助手”它由两个Agent组成一个信息搜集Agent负责查询航班、酒店、天气一个行程规划Agent负责根据信息制定合理的行程草案。4.1 设计多Agent架构我们将使用ADK-Go的agents.Group来实现一个简单的顺序工作流。Group允许你将多个Agent串联起来前一个Agent的输出作为后一个Agent的输入。package main import ( context fmt log google.golang.org/adk/agents google.golang.org/adk/models/gemini google.golang.org/adk/tools ) func main() { ctx : context.Background() model, _ : gemini.NewModel(ctx, gemini.Config{ModelName: gemini-2.0-flash-exp}) // 1. 创建信息搜集Agent及其工具 infoGathererTools : []tools.Tool{ FlightSearchTool{}, HotelSearchTool{}, WeatherTool{}, // 复用之前的天气工具 } infoGathererAgent : agents.NewToolCalling(model, infoGathererTools...) // 2. 创建行程规划Agent它可能不需要外部工具主要靠LLM推理 // 我们可以为它定制一个特殊的“提示词”让它专注于规划和整合。 plannerAgent : agents.NewToolCalling(model) // 没有工具纯LLM // 3. 使用Group编排两个Agent // Group会按顺序执行每个Agent并将上一个Agent的完整对话历史传递给下一个。 travelPlannerGroup : agents.NewGroup( agents.WithName(信息搜集员), infoGathererAgent, agents.WithName(行程规划师), plannerAgent, ) // 4. 运行这个多Agent系统 turn : agents.NewTurn() turn.Add(agents.NewTextMessage(agents.AuthorUser, 帮我规划一个下周末从上海到北京的三天两夜旅行预算中等。)) resultTurn, err : travelPlannerGroup.Run(ctx, turn) if err ! nil { log.Fatal(err) } // 打印最终结果 fmt.Println( 旅行规划结果 ) for _, msg : range resultTurn.Messages { if msg.Author agents.AuthorAssistant { fmt.Println(msg.Text) } } }在这个设计中信息搜集员Agent会首先运行。当用户提出旅行规划请求时这个Agent内部的LLM会分析请求并可能依次调用航班查询、酒店查询、天气查询等工具与用户进行多轮对话以收集必要信息如具体日期、偏好等。当信息收集得差不多时Group会将包含所有这些对话历史的Turn传递给行程规划师Agent。这个Agent看到完整的历史记录包括工具调用的结果就能基于这些信息生成一份详细的行程草案。4.2 实现自定义工具与复杂交互上面的FlightSearchTool和HotelSearchTool需要我们自己实现。这里以FlightSearchTool为例展示一个更接近真实场景的工具它可能需要调用外部API。type FlightSearchTool struct { apiClient *SomeFlightAPIClient // 假设的航班API客户端 } func (f *FlightSearchTool) Definition() tools.Definition { return tools.Definition{ Name: search_flights, Description: 根据出发地、目的地、日期搜索航班信息。, InputSchema: tools.InputSchema{ type: object, properties: { departure_city: {type: string, description: 出发城市}, arrival_city: {type: string, description: 到达城市}, departure_date: {type: string, description: 出发日期 (YYYY-MM-DD)}, return_date: {type: string, description: 返程日期 (YYYY-MM-DD)可选}, }, required: [departure_city, arrival_city, departure_date], }, } } func (f *FlightSearchTool) Execute(ctx context.Context, input map[string]any) (map[string]any, error) { // 1. 参数验证与提取 depCity, _ : input[departure_city].(string) arrCity, _ : input[arrival_city].(string) depDate, _ : input[departure_date].(string) retDate, ok : input[return_date].(string) // 2. 调用外部API这里用模拟数据代替 // 真实情况下这里会有HTTP请求、错误处理、重试逻辑等。 flights : []Flight{ {Airline: 东方航空, FlightNo: MU5101, Departure: 08:00, Arrival: 10:20, Price: 1200}, {Airline: 中国国航, FlightNo: CA1501, Departure: 14:30, Arrival: 16:55, Price: 1100}, } // 3. 格式化结果使其对LLM友好 resultStr : fmt.Sprintf(从%s到%s在%s的航班搜索结果\n, depCity, arrCity, depDate) for i, f : range flights { resultStr fmt.Sprintf(%d. %s %s: %s - %s, 价格: %d\n, i1, f.Airline, f.FlightNo, f.Departure, f.Arrival, f.Price) } if ok retDate ! { resultStr fmt.Sprintf(\n注已包含返程日期%s的查询条件, retDate) } return map[string]any{flights: resultStr}, nil } type Flight struct { Airline string FlightNo string Departure string Arrival string Price int }实操心得工具设计的艺术描述要精准Description和InputSchema是LLM理解工具的“说明书”。描述要简洁明确参数定义要详细包括类型、是否必填、示例。好的描述能极大提高工具调用的准确率。结果要结构化但友好工具返回给LLM的数据最好是一种易于LLM理解和引用的格式。简单的字符串描述如上面的resultStr可以但更复杂的场景可以考虑返回结构化的JSON让LLM能精确提取其中的字段。错误处理要健壮工具执行可能失败网络超时、API限流、参数无效。必须在Execute方法中妥善处理这些错误并返回清晰的错误信息这样上层的Agent或编排器才能决定是重试、换用备用方案还是向用户报错。5. 部署、测试与问题排查5.1 将Agent部署为云服务ADK-Go构建的Agent应用本质上是一个标准的Go HTTP服务。你可以使用任何你熟悉的Web框架如Gin, Echo来包装它。这里提供一个使用标准库net/http的极简示例package main import ( encoding/json net/http google.golang.org/adk/agents // ... 其他导入 ) func main() { // 初始化你的Agent myAgent : setupMyAgent() http.HandleFunc(/chat, func(w http.ResponseWriter, r *http.Request) { var req struct { Message string json:message } if err : json.NewDecoder(r.Body).Decode(req); err ! nil { http.Error(w, Invalid request, http.StatusBadRequest) return } turn : agents.NewTurn() turn.Add(agents.NewTextMessage(agents.AuthorUser, req.Message)) resultTurn, err : myAgent.Run(r.Context(), turn) if err ! nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // 提取Agent的最后一条回复 var lastReply string for _, msg : range resultTurn.Messages { if msg.Author agents.AuthorAssistant { lastReply msg.Text } } json.NewEncoder(w).Encode(map[string]string{reply: lastReply}) }) // 部署时监听端口由云平台环境变量决定如Cloud Run的PORT port : os.Getenv(PORT) if port { port 8080 } log.Printf(Server listening on port %s, port) log.Fatal(http.ListenAndServe(:port, nil)) }将这个应用容器化编写Dockerfile后就可以轻松部署到Google Cloud Run、Kubernetes或其他容器平台。ADK-Go本身无状态非常适合水平扩展。5.2 单元测试与集成测试“代码优先”带来的最大好处之一就是可测试性。测试一个Agent或工具就像测试任何Go函数一样简单。测试一个工具func TestWeatherTool_Execute(t *testing.T) { ctx : context.Background() tool : WeatherTool{} // 测试正常输入 result, err : tool.Execute(ctx, map[string]any{city: 北京}) assert.NoError(t, err) assert.Contains(t, result[weather], 北京) // 测试缺少必要参数 _, err tool.Execute(ctx, map[string]any{}) assert.Error(t, err) }模拟LLM测试Agent逻辑你可以创建一个实现了adk.Model接口的Mock对象来模拟LLM的响应从而在不调用真实API的情况下测试Agent的决策逻辑。type mockModel struct { responses []string // 预设的LLM回复序列 index int } func (m *mockModel) GenerateContent(ctx context.Context, req *adk.GenerateContentRequest) (*adk.GenerateContentResponse, error) { // 返回预设的响应模拟LLM决定调用工具或直接回复 resp : m.responses[m.index] m.index // ... 将resp解析成adk.GenerateContentResponse结构 return parsedResponse, nil } // ... 实现其他必要方法 func TestMyAgent_Run(t *testing.T) { mockLLM : mockModel{responses: []string{{tool_calls: [{name: get_weather, args: {city: 北京}}]}}} agent : MyFirstAgent{model: mockLLM, tools: []tools.Tool{WeatherTool{}}} // ... 运行Agent并断言其行为 }5.3 常见问题与排查技巧在实际开发中你可能会遇到以下典型问题问题1LLM总是不调用我期望的工具。排查首先检查工具的Definition()方法。Description是否清晰说明了工具的用途和适用场景InputSchema是否准确描述了每个参数LLM完全依赖这些描述来做决策。尝试将描述写得更具体、更场景化。技巧在给LLM的System Prompt或对话历史中可以加入一些“少样本示例”Few-shot Examples直接展示在什么情况下应该调用哪个工具以及如何填写参数。问题2工具调用结果被LLM忽略或误解。排查检查工具Execute()返回的数据格式。它是否过于复杂或冗长尝试将结果总结成更简洁、关键的几点或者用更清晰的标记如使用项目符号-来格式化。技巧ADK-Go的agents.ToolCallingAgent内部会处理工具调用循环。确保你使用的是最新的版本因为它可能包含了更好的结果格式化逻辑。问题3多Agent协作时信息传递丢失。排查在使用agents.Group或自定义编排器时确认Turn对象包含完整的对话历史被正确地从一个Agent传递到下一个Agent。每个Agent的Run方法都应该基于传入的Turn进行操作并将新的消息添加进去。技巧可以在关键节点打印Turn的内容 (fmt.Printf(%v\n, turn))查看消息历史是否如预期般增长。问题4性能问题响应慢。排查工具延迟用工具执行时间。是否是某个外部API调用过慢考虑增加超时、实现缓存或寻找替代API。LLM延迟检查使用的模型。更小、更快的模型如gemini-2.0-flash通常比超大模型如gemini-2.0-ultra响应快得多且对于许多任务来说精度足够。不必要的循环Agent是否陷入了“调用工具 - 得到结果 - 再次调用相同工具”的死循环这可能是工具描述不清或LLM理解有误导致的。需要优化提示词或增加循环次数限制。问题5如何处理长上下文和Token消耗策略对于长对话Token消耗会快速增长。ADK-Go的Turn对象存储了所有消息。你需要实现一个“上下文窗口管理”策略。例如可以创建一个自定义的Agent在Run方法中在将Turn发送给LLM前主动截断或总结早期的对话历史只保留最相关的部分。这是一种高级但非常实用的优化技巧。从我个人的使用经验来看ADK-Go最大的优势在于它把控制权交还给了开发者。你不会被框死在某种固定的“Agent模式”里。无论是构建一个简单的客服机器人还是一个由数十个专业Agent组成的复杂决策系统你都可以用清晰的Go代码来定义它们的行为和交互。这种自由度和工程上的严谨性正是它在众多Agent框架中脱颖而出的原因。开始用它来构建你的下一个AI应用吧你会发现用代码“编织”智能体的过程既充满挑战又极具成就感。