第九章:实战项目 - 智能代码审查助手
这是本教程的收官之作。前面八章学的所有知识点,我们在这一章全部用上,做一个真正能跑、真正有用的项目。
9.1 我们要做什么?
想象一下:你写了一段 Go 代码,丢给一个 AI 助手,它帮你从头到尾做一次完整的代码审查(Code Review),最后给你出一份专业的审查报告。
这不是一个简单的"帮我看看代码"的对话。我们要搭一条审查流水线,让代码依次经过四个专业 Agent 的检查:
用户提交代码 --> CodeReviewPipeline (SequentialAgent)
|
|-- Step 1: CodeAnalyzer (分析代码结构,统计行数、复杂度)
|-- Step 2: BugDetector (检测潜在 Bug,找安全隐患)
|-- Step 3: StyleChecker (检查代码风格,是否符合 Go 惯例)
|-- Step 4: ReportGenerator (汇总所有结果,生成最终报告)
每个 Agent 各司其职,前一个的输出会自动传给后一个。最终你拿到的是一份结构清晰、评分明确的审查报告。
这个项目用到了哪些 ADK 知识?
| ADK 概念 | 在项目中的作用 |
|---|---|
FunctionTool |
代码行数统计工具、复杂度检查工具 |
LLMAgent |
四个专业 Agent,各负责一个审查环节 |
SequentialAgent |
把四个 Agent 串成流水线,按顺序执行 |
OutputKey |
每个 Agent 把结果存到 session state |
指令模板 {key} |
后续 Agent 通过模板读取前面 Agent 的输出 |
Launcher |
一键启动,支持 console / web 等多种模式 |
可以说,这个项目就是前面所有章节知识的综合运用。做完这个,你就算是把 ADK-Go 的核心玩法全掌握了。
9.2 项目架构详解
在动手写代码之前,我们先把架构想清楚。
为什么用 SequentialAgent?
代码审查天然就是一个有先后顺序的流程:
- 你得先分析代码的基本结构(有多少行、多少函数、嵌套深不深),才能给后面的 Agent 提供上下文。
- Bug 检测需要结合代码结构分析的结果,才能更精准地定位问题。
- 风格检查独立于 Bug 检测,但也需要知道代码的基本信息。
- 最后的报告生成需要把前面三步的结果全部汇总。
这正是 SequentialAgent 的用武之地 -- 按顺序执行子 Agent,每一步的输出通过 OutputKey 存到 session state,后续 Agent 通过 {key} 模板自动读取。
数据流向
用户输入代码
|
v
[CodeAnalyzer] --调用工具--> count_lines, check_complexity
| 输出存到 state["code_analysis"]
v
[BugDetector] --读取--> {code_analysis}
| 输出存到 state["bug_report"]
v
[StyleChecker] --读取用户原始代码-->
| 输出存到 state["style_report"]
v
[ReportGenerator] --读取--> {code_analysis}, {bug_report}, {style_report}
| 生成最终报告
v
用户看到完整的审查报告
为什么需要自定义工具?
LLM 很擅长理解代码逻辑、发现潜在问题,但它不擅长精确计数。让 LLM 数代码有多少行、多少个函数,它经常数错。所以我们用 FunctionTool 来做这些精确计算的活儿,让 LLM 专注于它擅长的分析和推理。
9.3 项目初始化
先创建项目目录,初始化 Go module:
mkdir code-review-assistant && cd code-review-assistant
go mod init code-review-assistant
go get google.golang.org/adk
go get google.golang.org/genai
项目结构很简单,就一个文件:
code-review-assistant/
|-- go.mod
|-- go.sum
|-- main.go <-- 所有代码都在这里
别看只有一个文件,内容可不少。但好处是你可以直接 go run . 就跑起来,不用操心包结构。
9.4 完整代码
下面是完整的 main.go,可以直接复制运行。我会在代码之后逐段讲解。
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/agent/workflowagents/sequentialagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
)
// ============================================================
// 第一部分:工具定义
// ============================================================
// 这些工具负责"精确计算"的活儿,LLM 不擅长数数,
// 但我们的 Go 函数可以精确地统计代码信息。
// CountLinesInput 是代码行数统计工具的输入参数
type CountLinesInput struct {
Code string `json:"code" description:"要统计行数的代码文本"`
}
// CountLinesOutput 是代码行数统计工具的输出结果
type CountLinesOutput struct {
TotalLines int `json:"total_lines" description:"总行数"`
CodeLines int `json:"code_lines" description:"代码行数"`
CommentLines int `json:"comment_lines" description:"注释行数"`
BlankLines int `json:"blank_lines" description:"空行数"`
}
// countLines 统计代码的行数信息
// 它会把代码按行拆开,逐行判断是代码行、注释行还是空行
func countLines(ctx tool.Context, input CountLinesInput) (CountLinesOutput, error) {
lines := strings.Split(input.Code, "\n")
output := CountLinesOutput{TotalLines: len(lines)}
// 标记是否在多行注释块中
inBlockComment := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 处理多行注释块
if inBlockComment {
output.CommentLines++
if strings.Contains(trimmed, "*/") {
inBlockComment = false
}
continue
}
switch {
case trimmed == "":
// 空行
output.BlankLines++
case strings.HasPrefix(trimmed, "//"):
// 单行注释
output.CommentLines++
case strings.HasPrefix(trimmed, "/*"):
// 多行注释开始
output.CommentLines++
if !strings.Contains(trimmed, "*/") {
inBlockComment = true
}
default:
// 代码行
output.CodeLines++
}
}
return output, nil
}
// CheckComplexityInput 是复杂度检查工具的输入参数
type CheckComplexityInput struct {
Code string `json:"code" description:"要检查复杂度的代码文本"`
}
// CheckComplexityOutput 是复杂度检查工具的输出结果
type CheckComplexityOutput struct {
Functions int `json:"functions" description:"函数数量"`
MaxNesting int `json:"max_nesting" description:"最大嵌套深度"`
Warnings []string `json:"warnings" description:"警告信息列表"`
}
// checkComplexity 做一个简单的代码复杂度检查
// 它会统计函数数量、计算最大嵌套深度,并给出相应的警告
func checkComplexity(ctx tool.Context, input CheckComplexityInput) (CheckComplexityOutput, error) {
output := CheckComplexityOutput{
Warnings: make([]string, 0),
}
lines := strings.Split(input.Code, "\n")
currentNesting := 0
for _, line := range lines {
trimmed := strings.TrimSpace(line)
// 统计函数定义(简单匹配 func 关键字开头)
if strings.HasPrefix(trimmed, "func ") {
output.Functions++
}
// 通过花括号计算嵌套深度
// 虽然简单粗暴,但对大多数 Go 代码够用了
openBraces := strings.Count(line, "{")
closeBraces := strings.Count(line, "}")
currentNesting += openBraces - closeBraces
if currentNesting > output.MaxNesting {
output.MaxNesting = currentNesting
}
}
// 根据统计结果给出警告
if output.MaxNesting > 4 {
output.Warnings = append(output.Warnings,
fmt.Sprintf("嵌套层级过深(当前最大 %d 层),建议重构,提取子函数", output.MaxNesting))
}
if output.Functions > 20 {
output.Warnings = append(output.Warnings,
fmt.Sprintf("函数数量过多(当前 %d 个),建议拆分到多个文件", output.Functions))
}
if output.Functions == 0 {
output.Warnings = append(output.Warnings,
"没有检测到函数定义,请确认提交的是否为完整代码")
}
return output, nil
}
// ============================================================
// 第二部分:主函数 - 组装 Agent 流水线
// ============================================================
func main() {
ctx := context.Background()
// --------------------------------------------------------
// 第 1 步:创建 LLM 模型
// --------------------------------------------------------
// 所有 LLMAgent 共享同一个模型实例。
// 你可以换成其他模型,比如 "gemini-2.5-pro"。
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// --------------------------------------------------------
// 第 2 步:创建工具
// --------------------------------------------------------
// 把我们上面定义的 Go 函数包装成 ADK 工具,
// 这样 LLM 就能在需要的时候调用它们。
// 代码行数统计工具
countLinesTool, err := functiontool.New(functiontool.Config{
Name: "count_lines",
Description: "统计代码的行数信息,区分代码行、注释行和空行,返回各项精确数字",
}, countLines)
if err != nil {
log.Fatalf("创建 count_lines 工具失败: %v", err)
}
// 复杂度检查工具
complexityTool, err := functiontool.New(functiontool.Config{
Name: "check_complexity",
Description: "检查代码的复杂度信息,统计函数数量和最大嵌套深度,给出改进警告",
}, checkComplexity)
if err != nil {
log.Fatalf("创建 check_complexity 工具失败: %v", err)
}
// --------------------------------------------------------
// 第 3 步:创建四个专业 Agent
// --------------------------------------------------------
// === Agent 1: 代码结构分析师 ===
// 它的任务是调用工具,统计代码的基本信息,
// 然后总结出一份结构分析报告。
codeAnalyzer, err := llmagent.New(llmagent.Config{
Name: "CodeAnalyzer",
Model: model,
Description: "分析代码结构和基本统计信息",
Instruction: `你是一个代码结构分析师。你的任务是分析用户提交的代码,给出精确的结构信息。
请按以下步骤操作:
1. 使用 count_lines 工具统计代码行数
2. 使用 check_complexity 工具检查代码复杂度
3. 阅读代码,总结其主要功能和结构
请严格按照以下格式输出:
## 代码结构分析
### 基本统计
- 总行数:(填入工具返回的数字)
- 代码行:(填入工具返回的数字)
- 注释行:(填入工具返回的数字)
- 空行:(填入工具返回的数字)
### 复杂度信息
- 函数数量:(填入工具返回的数字)
- 最大嵌套深度:(填入工具返回的数字)
- 复杂度警告:(列出工具返回的警告,没有则写"无")
### 代码概述
用 2-3 句话简要描述这段代码的主要功能、使用了哪些包、代码的整体组织方式。
注意:数字必须使用工具返回的精确值,不要自己估算。`,
Tools: []tool.Tool{countLinesTool, complexityTool},
OutputKey: "code_analysis",
})
if err != nil {
log.Fatalf("创建 CodeAnalyzer 失败: %v", err)
}
// === Agent 2: Bug 猎手 ===
// 它会结合代码分析的结果,深入审查代码中的潜在问题。
// 注意指令中的 {code_analysis} —— 这就是指令模板,
// ADK 会自动把 session state 中的 code_analysis 值替换进来。
bugDetector, err := llmagent.New(llmagent.Config{
Name: "BugDetector",
Model: model,
Description: "检测代码中的潜在 Bug 和安全隐患",
Instruction: `你是一个经验丰富的 Bug 猎手,专门找 Go 代码里的隐藏问题。
前面的同事已经做了代码结构分析,结果如下:
{code_analysis}
现在轮到你了。请仔细审查用户提交的代码,重点关注以下类型的问题:
1. **空指针/nil 引用风险** - 是否有未检查 nil 就直接使用的地方?
2. **资源泄漏** - 文件、数据库连接、HTTP 响应体等是否正确关闭?有没有用 defer?
3. **并发安全** - 是否有竞态条件、死锁风险?共享变量有没有加锁?
4. **错误处理** - 是否有被忽略的 error 返回值?错误信息是否有意义?
5. **边界条件** - 切片越界、整数溢出、空切片/空 map 操作等。
6. **逻辑错误** - 条件判断是否正确、循环是否有 off-by-one 错误等。
请严格按照以下格式输出:
## Bug 检测报告
(对每个发现的问题,按以下格式列出)
### [严重/中等/轻微] 问题标题
- **位置**:具体说明在代码的哪一部分
- **问题描述**:详细说明问题是什么
- **风险**:可能导致什么后果
- **修复建议**:给出具体的修复方案
---
如果没有发现明显的 Bug,请输出:
## Bug 检测报告
经过仔细审查,未发现明显的 Bug 或安全隐患。代码质量良好。`,
OutputKey: "bug_report",
})
if err != nil {
log.Fatalf("创建 BugDetector 失败: %v", err)
}
// === Agent 3: 代码风格专家 ===
// 专注于 Go 社区的编码规范和最佳实践。
styleChecker, err := llmagent.New(llmagent.Config{
Name: "StyleChecker",
Model: model,
Description: "检查代码风格和 Go 语言最佳实践",
Instruction: `你是一个 Go 语言风格专家,深谙 Go 社区的编码规范和最佳实践。
你熟悉 Effective Go、Go Code Review Comments、Go Proverbs 等经典指南。
请审查用户提交的代码,从以下几个维度进行检查:
1. **命名规范**
- 变量、函数、类型的命名是否符合 Go 惯例?
- 缩写是否恰当(比如 URL 而不是 Url)?
- 导出与非导出的命名是否合理?
2. **代码格式**
- 缩进、空行的使用是否恰当?
- 代码是否整洁,没有多余的空行或过长的行?
3. **注释质量**
- 导出的函数和类型是否有文档注释?
- 注释是否清晰、有价值(不是废话注释)?
- 包级别的文档是否存在?
4. **Go 惯用模式**
- 是否使用了 Go 惯用的错误处理方式(if err != nil)?
- 是否合理使用了 defer、interface、goroutine 等?
- 有没有"用其他语言的思维写 Go"的情况?
5. **包组织**
- import 是否按标准库、第三方库、内部包分组?
- 依赖是否合理?
请严格按照以下格式输出:
## 代码风格检查
(对每个问题,按以下格式列出)
- **[命名/格式/注释/惯用模式/包组织]** 问题描述 --> 建议的改进方式
### 风格评分
给出一个 1-10 分的整体风格评分,并简要说明理由。
- 1-3 分:风格问题严重,需要大幅改进
- 4-6 分:有一些问题,但基本可读
- 7-8 分:风格良好,有小改进空间
- 9-10 分:优秀,符合 Go 社区最佳实践`,
OutputKey: "style_report",
})
if err != nil {
log.Fatalf("创建 StyleChecker 失败: %v", err)
}
// === Agent 4: 报告生成器 ===
// 汇总前面三个 Agent 的所有结果,生成最终的审查报告。
// 注意它的指令中引用了三个 state key:
// {code_analysis}、{bug_report}、{style_report}
reportGenerator, err := llmagent.New(llmagent.Config{
Name: "ReportGenerator",
Model: model,
Description: "汇总所有审查结果,生成最终的代码审查报告",
Instruction: `你是一个代码审查报告撰写专家。你的任务是把前面三位同事的分析结果,汇总成一份简洁、专业、可执行的代码审查总报告。
以下是三位同事的分析结果:
### 一、代码结构分析
{code_analysis}
### 二、Bug 检测报告
{bug_report}
### 三、代码风格检查
{style_report}
请根据以上信息,生成最终报告。严格按照以下格式输出:
# 代码审查报告
## 概要
用一段话(3-5 句)总结这段代码的整体质量。要客观、具体,不要说空话。
## 评分
| 维度 | 评分(1-10) | 说明 |
|------|-----------|------|
| 代码质量 | x | 简要说明 |
| 安全性 | x | 简要说明 |
| 可维护性 | x | 简要说明 |
| 代码风格 | x | 简要说明 |
| **综合评分** | **x** | 综合评价 |
## 关键问题(必须修复)
列出最重要的、必须修复的问题。如果没有关键问题,写"无关键问题,代码质量良好"。
每条用编号列出,包含问题描述和具体修复建议。
## 改进建议(建议修复)
列出值得改进但不紧急的问题。
每条用编号列出,包含问题描述和建议的做法。
## 亮点
列出代码中做得好的地方,给开发者正向反馈。
如果没有特别的亮点,可以指出代码中合理的设计决策。
注意:
- 不要重复前面已有的详细内容,要做提炼和总结。
- 评分要基于实际发现的问题,不要凭感觉打分。
- 报告要对开发者有实际帮助,不要说套话。`,
OutputKey: "final_report",
})
if err != nil {
log.Fatalf("创建 ReportGenerator 失败: %v", err)
}
// --------------------------------------------------------
// 第 4 步:组装流水线
// --------------------------------------------------------
// 用 SequentialAgent 把四个 Agent 串起来,
// 它们会按照放入 SubAgents 的顺序依次执行。
pipeline, err := sequentialagent.New(sequentialagent.Config{
AgentConfig: agent.Config{
Name: "CodeReviewPipeline",
Description: "智能代码审查流水线:分析结构 --> 检测 Bug --> 检查风格 --> 生成报告",
SubAgents: []agent.Agent{
codeAnalyzer, // 第一步:分析代码结构
bugDetector, // 第二步:检测潜在 Bug
styleChecker, // 第三步:检查代码风格
reportGenerator, // 第四步:生成审查报告
},
},
})
if err != nil {
log.Fatalf("创建 CodeReviewPipeline 失败: %v", err)
}
// --------------------------------------------------------
// 第 5 步:启动应用
// --------------------------------------------------------
// 使用 full.NewLauncher() 全家桶启动器,
// 支持 console(命令行对话)、web(Web UI)等多种模式。
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(pipeline),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v\n\n%s", err, l.CommandLineSyntax())
}
}
9.5 代码逐段解读
代码虽然一口气写在一个文件里,但逻辑分成了清晰的五个部分。我们逐个来聊。
9.5.1 工具定义(FunctionTool)
type CountLinesInput struct {
Code string `json:"code" description:"要统计行数的代码文本"`
}
func countLines(ctx tool.Context, input CountLinesInput) (CountLinesOutput, error) {
// ...
}
为什么需要自定义工具?
你可能会想:"让 LLM 自己数代码行数不就行了?"
事实是,LLM 数数非常不靠谱。你给它一段 50 行的代码,它可能告诉你有 47 行或者 53 行。但我们的审查报告需要精确的数字,这就是工具的价值所在。
functiontool.New 的工作原理很简单:
1. 你定义一个输入结构体(带 json 和 description tag)
2. 你定义一个输出结构体
3. 你写一个处理函数,签名是 func(tool.Context, Input) (Output, error)
4. ADK 会自动从结构体的 tag 推断出 JSON Schema,告诉 LLM 这个工具的参数格式
LLM 在需要的时候会自动调用这些工具。比如 CodeAnalyzer 的指令里说"使用 count_lines 工具统计代码行数",LLM 就会生成一个工具调用请求,ADK 框架负责执行工具并把结果返回给 LLM。
两个工具的分工:
| 工具 | 功能 | 为什么需要 |
|---|---|---|
count_lines |
统计总行数、代码行、注释行、空行 | LLM 数不准行数 |
check_complexity |
统计函数数量、最大嵌套深度、给出警告 | LLM 数不准花括号嵌套 |
9.5.2 创建模型
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
这行代码创建了一个 Gemini 模型实例。几个要点:
- 模型名用
"gemini-2.5-flash",速度快、成本低,适合做代码审查这种文本密集型任务 - 如果你想要更强的分析能力,可以换成
"gemini-2.5-pro" - API Key 从环境变量读取,不要硬编码在代码里
- 四个 Agent 共享同一个模型实例,这是完全可以的,不需要为每个 Agent 创建不同的模型
9.5.3 四个 Agent 的设计
这是项目最核心的部分。每个 Agent 都是一个 llmagent.New 创建的 LLMAgent,配置了不同的指令(Instruction)。
CodeAnalyzer -- 代码结构分析师
codeAnalyzer, err := llmagent.New(llmagent.Config{
Name: "CodeAnalyzer",
Instruction: `你是一个代码结构分析师...`,
Tools: []tool.Tool{countLinesTool, complexityTool},
OutputKey: "code_analysis",
})
关键点:
- 只有它配了 Tools,因为只有它需要调用统计工具
- OutputKey: "code_analysis" 表示它的输出会被存到 session state 的 code_analysis 键下
- 指令里明确告诉它要先调用工具,再做总结
BugDetector -- Bug 猎手
bugDetector, err := llmagent.New(llmagent.Config{
Name: "BugDetector",
Instruction: `...前面的同事已经做了代码结构分析,结果如下:
{code_analysis}
...`,
OutputKey: "bug_report",
})
关键点:
- 指令中的 {code_analysis} 是模板语法,ADK 会在运行时自动替换成 session state 中 code_analysis 的值
- 这就是 Agent 之间"传话"的方式 -- 前一个 Agent 通过 OutputKey 存结果,后一个 Agent 通过 {key} 模板读结果
- 它没有配工具,纯靠 LLM 的分析能力来找 Bug
StyleChecker -- 代码风格专家
styleChecker, err := llmagent.New(llmagent.Config{
Name: "StyleChecker",
Instruction: `你是一个 Go 语言风格专家...`,
OutputKey: "style_report",
})
关键点:
- 它也没有工具,纯靠 LLM 对 Go 语言规范的理解
- 它直接审查用户原始提交的代码,不需要读取前面 Agent 的分析结果
- 输出存到 style_report,供最后的报告生成器使用
ReportGenerator -- 报告生成器
reportGenerator, err := llmagent.New(llmagent.Config{
Name: "ReportGenerator",
Instruction: `...
{code_analysis}
...
{bug_report}
...
{style_report}
...`,
OutputKey: "final_report",
})
关键点:
- 它是整条流水线的"汇总者",通过模板引用了前面三个 Agent 的所有输出
- 它的指令里详细规定了报告格式,包括评分表格、关键问题、改进建议等
- OutputKey: "final_report" 虽然这是最后一个 Agent 了,但设置 OutputKey 是个好习惯,万一以后你想在流水线后面再加一步呢
9.5.4 OutputKey 和状态传递机制
这是理解整个流水线的关键。让我们拆解一下这个机制:
Agent A (OutputKey: "code_analysis")
--> LLM 回复了一段文本
--> ADK 自动把这段文本存到 session.State["code_analysis"]
Agent B (Instruction 里有 {code_analysis})
--> ADK 渲染指令时,发现 {code_analysis} 模板
--> 从 session.State["code_analysis"] 取值
--> 替换到指令中
--> 把完整指令发给 LLM
就这么简单。不需要你手动传参、不需要写胶水代码,ADK 框架帮你搞定了 Agent 之间的数据流转。
关于指令模板,有几个规则:
- 占位符格式是 {key_name},key 必须匹配 ^[a-zA-Z_][a-zA-Z0-9_]*$
- 如果 state 里找不到对应的 key,Agent 会报错
- 如果你想让某个 key 可选(找不到也别报错),可以用 {key_name?} 加问号
9.5.5 SequentialAgent 组装
pipeline, err := sequentialagent.New(sequentialagent.Config{
AgentConfig: agent.Config{
Name: "CodeReviewPipeline",
Description: "智能代码审查流水线",
SubAgents: []agent.Agent{
codeAnalyzer,
bugDetector,
styleChecker,
reportGenerator,
},
},
})
SequentialAgent 的配置很简洁:
- 把子 Agent 放进 SubAgents 切片,顺序就是执行顺序
- 它会依次执行每个子 Agent,等一个完成了再执行下一个
- 不需要你写任何循环或编排逻辑
在底层,SequentialAgent 其实是一个 MaxIterations=1 的 LoopAgent,也就是说它只循环一次,把所有子 Agent 跑一遍就结束。
9.5.6 Launcher 启动
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(pipeline),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v\n\n%s", err, l.CommandLineSyntax())
}
full.NewLauncher() 是 ADK 的"全家桶启动器",它支持多种运行模式:
- console - 命令行交互模式,适合本地调试
- web - Web 服务模式,支持 Web UI 和 API
- a2a - Agent-to-Agent 协议模式
你通过命令行参数来选择模式。比如 go run . console 就是命令行模式。
9.6 运行项目
环境准备
先设置好 API Key:
export GOOGLE_API_KEY="your-api-key-here"
启动项目
用 console 模式启动:
go run . console
你会看到一个交互式命令行界面。
示例对话
输入一段 Go 代码让它审查:
> 请帮我审查以下Go代码:
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func fetchData(url string) string {
resp, err := http.Get(url)
if err != nil {
fmt.Println("error:", err)
}
body, _ := ioutil.ReadAll(resp.Body)
return string(body)
}
func processItems(items []string) {
for i := 0; i <= len(items); i++ {
fmt.Println(items[i])
}
}
func main() {
data := fetchData("https://api.example.com/data")
fmt.Println(data)
items := []string{"a", "b", "c"}
processItems(items)
}
然后你会看到流水线开始工作:
- CodeAnalyzer 先调用
count_lines和check_complexity工具,输出代码结构分析 - BugDetector 拿到分析结果,开始找 Bug(这段代码里有不少问题)
- StyleChecker 检查代码风格
- ReportGenerator 汇总生成最终报告
最终你会看到类似这样的输出:
# 代码审查报告
## 概要
这段代码存在多个严重问题,包括空指针引用风险、资源泄漏和数组越界错误。
代码功能简单但实现不够健壮,需要重点关注错误处理和资源管理。
## 评分
| 维度 | 评分(1-10) | 说明 |
|------|-----------|------|
| 代码质量 | 3 | 存在明显逻辑错误和空指针风险 |
| 安全性 | 2 | HTTP 响应体未关闭,错误被忽略 |
| 可维护性 | 4 | 代码简短但缺乏错误处理 |
| 代码风格 | 5 | 基本格式可以,但使用了废弃的包 |
| **综合评分** | **3** | 需要较大改进 |
## 关键问题(必须修复)
1. resp.Body 未关闭,每次调用都会泄漏资源
2. http.Get 出错后仍然访问 resp.Body,会导致空指针 panic
3. processItems 循环条件 i <= len(items) 导致数组越界
4. ioutil.ReadAll 的 error 被忽略
## 改进建议(建议修复)
1. 使用 io 包替代已废弃的 ioutil 包
2. fetchData 应该返回 (string, error) 而不是遇错忽略
3. 添加必要的函数文档注释
## 亮点
- import 分组合理
- 函数职责划分清晰(获取数据和处理数据分开)
这份报告精准地找出了代码中的关键问题:资源泄漏、空指针风险、数组越界,还给出了具体的修复建议。
9.7 扩展思路
这个项目已经是一个完整可用的代码审查助手了,但还有很多有意思的方向可以继续扩展:
1. 加个 REST API,给团队用
目前只有 console 模式,如果你想让团队其他人也能用,可以加一个 HTTP API:
// 不需要改 Agent 代码,只需要换一种启动方式
// go run . web
full.NewLauncher() 已经内置了 web 模式的支持,加上 web 参数就能启动 HTTP 服务。如果你需要更灵活的控制,也可以参考第三章的 REST 服务模式自己搭。
2. 加记忆功能,记住过去的审查
可以接入 memory.Service,让 Agent 记住之前审查过的代码。这样它可以:
- 对比前后版本的变化
- 识别反复出现的问题模式
- 给出"和上次相比改进了哪些"的反馈
3. 接入更多工具
目前只有行数统计和复杂度检查两个工具,你可以加更多:
- go vet 集成:调用
go vet命令做静态分析 - go test 集成:运行测试,检查覆盖率
- golangci-lint 集成:接入 linter 工具链
- git diff 工具:只审查变更的部分,而不是整个文件
4. 用 ParallelAgent 加速
目前 BugDetector 和 StyleChecker 是串行的,但其实它们互不依赖。你可以把架构改成:
CodeAnalyzer (串行第一步)
|
v
ParallelAgent (并行执行)
|-- BugDetector
|-- StyleChecker
|
v
ReportGenerator (串行最后一步)
这需要把中间的并行部分用 parallelagent.New 包一层,然后放进一个更大的 SequentialAgent 里。
5. 加回调做审查指标统计
可以用 BeforeAgentCallback 和 AfterAgentCallback 记录每个 Agent 的执行时间、token 消耗等:
// 思路示例:在 Agent 执行前后记录时间
codeAnalyzer, err := llmagent.New(llmagent.Config{
// ... 其他配置 ...
BeforeAgentCallbacks: []agent.BeforeAgentCallback{
func(ctx agent.CallbackContext) ([]genai.Part, error) {
log.Printf("[%s] 开始执行...", ctx.AgentName())
return nil, nil
},
},
})
6. 支持多语言
目前指令是针对 Go 代码写的。你可以:
- 让用户指定语言,动态切换指令
- 用 InstructionProvider 根据代码内容自动检测语言并生成对应的审查指令
9.8 常见问题
Q: 流水线执行很慢,要等很久?
四个 Agent 串行执行,每个都要调用一次 LLM,确实会比单次对话慢。几个优化思路:
- 用 gemini-2.5-flash 而不是 pro,速度快很多
- 精简指令,减少不必要的输出要求
- 如前面提到的,把独立的步骤改成并行
Q: Agent 有时候不调用工具怎么办?
在 CodeAnalyzer 的指令里明确说"必须使用 xxx 工具",通常 LLM 就会乖乖调用。如果还不行,可以调整模型温度:
codeAnalyzer, err := llmagent.New(llmagent.Config{
// ...
GenerateContentConfig: &genai.GenerateContentConfig{
Temperature: genai.Ptr(float32(0.1)), // 降低温度,让 LLM 更听话
},
})
Q: 模板变量 {code_analysis} 报错说找不到?
确保前一个 Agent 确实设置了对应的 OutputKey,并且 SequentialAgent 中 Agent 的顺序是对的(使用 OutputKey 的 Agent 必须在设置 OutputKey 的 Agent 之后)。
Q: 能不能审查多个文件?
当前版本一次只审查用户输入的一段代码。要支持多文件,你可以: - 让用户把多个文件内容一起贴进来 - 或者加一个"读文件"工具,让 Agent 从磁盘读取文件
9.9 项目小结
恭喜你完成了这个实战项目!让我们回顾一下用到的所有 ADK 核心概念:
| 概念 | 用在哪里 | 作用 |
|---|---|---|
| FunctionTool | count_lines、check_complexity |
把 Go 函数包装成 LLM 可调用的工具 |
| LLMAgent | 四个专业 Agent | 每个 Agent 有独立的指令和职责 |
| SequentialAgent | CodeReviewPipeline |
把四个 Agent 串成按顺序执行的流水线 |
| OutputKey | 每个 Agent 都设置了 | 把 Agent 的输出存到 session state |
| 指令模板 {key} | BugDetector、ReportGenerator 的指令 | 引用前面 Agent 的输出 |
| Launcher | full.NewLauncher() |
一键启动,支持多种运行模式 |
这些就是 ADK-Go 最核心的构建模块。掌握了它们,你可以搭建出各种各样的 AI Agent 应用。
从这个项目出发,你可以继续探索: - 更复杂的 Agent 编排(并行、循环、条件分支) - 外部工具集成(MCP 协议、数据库、API) - 生产化部署(Web 服务、可观测性、会话持久化)
祝你在 ADK-Go 的世界里玩得开心!