第九章:实战项目 - 智能代码审查助手

这是本教程的收官之作。前面八章学的所有知识点,我们在这一章全部用上,做一个真正能跑、真正有用的项目。


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?

代码审查天然就是一个有先后顺序的流程:

  1. 你得先分析代码的基本结构(有多少行、多少函数、嵌套深不深),才能给后面的 Agent 提供上下文。
  2. Bug 检测需要结合代码结构分析的结果,才能更精准地定位问题。
  3. 风格检查独立于 Bug 检测,但也需要知道代码的基本信息。
  4. 最后的报告生成需要把前面三步的结果全部汇总。

这正是 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. 你定义一个输入结构体(带 jsondescription 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)
}

然后你会看到流水线开始工作:

  1. CodeAnalyzer 先调用 count_linescheck_complexity 工具,输出代码结构分析
  2. BugDetector 拿到分析结果,开始找 Bug(这段代码里有不少问题)
  3. StyleChecker 检查代码风格
  4. 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. 加回调做审查指标统计

可以用 BeforeAgentCallbackAfterAgentCallback 记录每个 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_linescheck_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 的世界里玩得开心!