Artifacts:结构化内容生成

当 Agent 的输出不仅仅是给人类阅读的对话文本,而是需要被下游系统解析、渲染或执行的机器可读内容时,“Artifacts” 这一概念就变得至关重要。Artifacts 是 ADK Go 中用于生成结构化、类型化、可复用内容输出的核心机制。它让 Agent 能够产出代码片段、JSON 配置、HTML 页面、Markdown 文档等具有明确格式和语义的数据,而不是混杂在自由文本中的"代码块"。

从自由文本到结构化输出:为什么需要 Artifacts

在传统的 LLM 交互中,如果你要求模型"写一个 Go 函数来处理 HTTP 请求",它可能会在回复中这样输出:

好的,这是一个处理 HTTP 请求的 Go 函数:

```go
func handleRequest(w http.ResponseWriter, r *http.Request) {
    // ...
}

你可以把它用在 main.go 中。记得导入 net/http 包。


这种输出虽然对人类友好,但对程序来说提取其中的代码块需要额外的解析逻辑,且容易出错(例如代码块中可能包含解释性文字)。Artifacts 机制通过让模型明确标注"这是一个独立的、可提取的内容单元",解决了这一问题。

从架构角度看,Artifacts 实现了 LLM 输出从"非结构化叙事"到"结构化数据"的跃迁。这使得:

1. **前端渲染优化**:UI 可以针对不同类型的 Artifact 使用专门的渲染器(代码高亮、图表绘制、表格展示)
2. **下游系统集成**:生成的 JSON 可以直接被 API 消费,生成的 SQL 可以直接被执行
3. **版本控制与追踪**:每个 Artifact 有独立的标识符,可以单独追踪、更新和回滚
4. **多模态工作流**:一个 Artifact 可以是代码,另一个可以是该代码的测试用例,它们之间可以建立引用关系

## ADK Go 中的 Artifacts 架构

ADK Go 的 Artifacts 系统由三个核心组件构成:

**Artifact 定义**:描述内容的类型、标题、语言等元数据
**Artifact 内容**:实际的数据负载(字符串或二进制)
**Artifact 处理器**:负责将 Artifact 转换为最终输出格式

用户请求 ↓ [Agent 推理] —— 模型决定需要生成哪些 Artifacts ↓ [Artifact 生成] —— 每个 Artifact 独立生成,带有类型标识 ↓ [Artifact 处理] —— 根据类型进行渲染、验证或转换 ↓ [输出组装] —— 将 Artifacts 与叙事文本组合为最终响应


与简单的"在 Markdown 中嵌入代码块"不同Artifacts 在协议层面就是一等公民这意味着客户端可以精确地知道"这里有一个类型为 `application/vnd.go` 的 Artifact,标题为 `handler.go`"

## 基础使用启用 Artifacts 生成

在 ADK Go 中启用 Artifacts 能力需要在 Agent 配置中显式声明以下是一个完整的生产级配置示例

```go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "google.golang.org/adk/agent"
    "google.golang.org/adk/artifact"
    "google.golang.org/adk/llm"
)

func main() {
    ctx := context.Background()

    model, err := llm.NewGeminiModel(ctx, llm.GeminiConfig{
        APIKey: os.Getenv("GOOGLE_API_KEY"),
        Model:  "gemini-2.0-pro",
    })
    if err != nil {
        log.Fatalf("模型初始化失败: %v", err)
    }

    // 注册支持的 Artifact 类型
    registry := artifact.NewRegistry()
    registry.Register(artifact.TypeCode, artifact.CodeHandler{
        Languages: []string{"go", "python", "javascript", "sql"},
    })
    registry.Register(artifact.TypeMarkdown, artifact.MarkdownHandler{})
    registry.Register(artifact.TypeJSON, artifact.JSONHandler{
        SchemaValidator: true,  // 启用 JSON Schema 验证
    })
    registry.Register(artifact.TypeHTML, artifact.HTMLHandler{
        Sanitize: true,  // 对 HTML 进行 XSS 过滤
    })

    agent, err := agent.New(agent.Config{
        Name:  "code-generator",
        Model: model,
        Instruction: `你是一个全栈开发助手当用户请求代码文档或配置时
你必须将生成的结构化内容以 Artifact 形式输出

Artifact 使用规范
1. 每个 Artifact 必须有明确的类型和标题
2. 代码 Artifact 必须指定编程语言
3. 多个相关文件应作为独立的 Artifacts 输出
4. 在 Artifact 前后提供简要的说明文字`,
        Artifacts: artifact.Config{
            Enabled:   true,
            Registry:  registry,
            MaxCount:  10,  // 单次响应最多 10 个 Artifacts
            MaxSize:   64 * 1024,  // 单个 Artifact 最大 64KB
        },
    })
    if err != nil {
        log.Fatalf("Agent 创建失败: %v", err)
    }

    resp, err := agent.Run(ctx, "帮我写一个 Go HTTP 服务,包含用户注册和登录接口")
    if err != nil {
        log.Fatalf("执行失败: %v", err)
    }

    // 遍历响应中的 Artifacts
    for _, art := range resp.Artifacts {
        fmt.Printf("Artifact: %s (%s)\n", art.Title, art.Type)
        fmt.Println("---")
        fmt.Println(art.Content)
        fmt.Println("---")
    }
}

内置 Artifact 类型详解

类型标识MIME 类型适用场景特殊处理
codeapplication/vnd.code源代码文件支持语法高亮、语言标识
markdowntext/markdown文档、说明支持 Markdown 渲染
jsonapplication/json配置文件、数据支持 Schema 验证
htmltext/html前端页面、邮件模板支持 XSS 过滤
svgimage/svg+xml矢量图形支持渲染预览
mermaidtext/x-mermaid流程图、架构图支持图表渲染

高级模式:自定义 Artifact 类型与处理器

生产环境中,内置的 Artifact 类型往往不足以覆盖所有业务需求。ADK Go 允许注册自定义 Artifact 类型和处理器。以下是一个为"基础设施即代码"(IaC)场景设计的自定义 Artifact 示例:

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "regexp"

    "google.golang.org/adk/agent"
    "google.golang.org/adk/artifact"
)

// TerraformArtifact 表示一个 Terraform 配置文件
type TerraformArtifact struct {
    Title       string
    Content     string
    Provider    string  // aws, gcp, azure
    Resources   []string
}

// TerraformHandler 处理 Terraform Artifact 的验证和转换
type TerraformHandler struct {
    // 可以注入 Terraform 二进制路径进行语法验证
    TerraformPath string
}

func (h *TerraformHandler) Type() string {
    return "application/vnd.terraform"
}

func (h *TerraformHandler) Validate(art *artifact.Artifact) error {
    // 基础语法检查:确保是有效的 HCL 格式
    if !regexp.MustCompile(`^\s*(resource|data|module|variable|output|provider)\s+"`).MatchString(art.Content) {
        return fmt.Errorf("无效的 Terraform 配置:缺少资源定义")
    }
    
    // 生产环境建议:调用 terraform validate 进行完整验证
    // 这里为了示例简化处理
    return nil
}

func (h *TerraformHandler) Render(art *artifact.Artifact) (string, error) {
    // 返回带语法高亮的 HTML 或纯文本
    return fmt.Sprintf("```hcl\n%s\n```", art.Content), nil
}

func (h *TerraformHandler) Transform(art *artifact.Artifact, target string) (*artifact.Artifact, error) {
    // 支持转换为其他格式,例如 JSON(terraform show -json 的等价物)
    switch target {
    case "json":
        // 调用 hcl2json 或类似工具进行转换
        return nil, fmt.Errorf("JSON 转换尚未实现")
    default:
        return nil, fmt.Errorf("不支持的转换目标: %s", target)
    }
}

func main() {
    registry := artifact.NewRegistry()
    registry.Register("application/vnd.terraform", &TerraformHandler{
        TerraformPath: "/usr/local/bin/terraform",
    })

    // ... 创建 Agent 并使用 registry
}

自定义 Artifact 处理器的核心价值在于将领域特定的验证和转换逻辑封装在类型系统中。例如,对于 SQL Artifact,处理器可以:

  • 使用 SQL 解析器检查语法正确性
  • 识别潜在的危险操作(DROP、DELETE without WHERE)
  • 格式化 SQL 以符合团队规范
  • 生成对应的 ORM 代码作为关联 Artifact

生产实践:多文件项目生成

在实际开发中,Agent 经常需要生成由多个文件组成的完整项目结构。Artifacts 机制天然支持这种模式。以下是一个生成 Go Web 项目完整脚手架的示例:

func generateProject(ctx context.Context, agent *agent.Agent, description string) (*Project, error) {
    resp, err := agent.Run(ctx, fmt.Sprintf(`
请根据以下描述生成一个完整的 Go Web 项目:
%s

要求:
1. 生成 main.go、handlers.go、models.go、go.mod 四个文件
2. 每个文件作为一个独立的 Artifact 输出
3. 包含适当的错误处理和日志记录
4. 使用标准库 net/http,不引入外部依赖
`, description))
    if err != nil {
        return nil, err
    }

    proj := &Project{Files: make(map[string]string)}
    for _, art := range resp.Artifacts {
        if art.Type != "application/vnd.code" {
            continue
        }
        // 假设标题就是文件名
        proj.Files[art.Title] = art.Content
    }

    // 验证项目完整性
    requiredFiles := []string{"main.go", "handlers.go", "models.go", "go.mod"}
    for _, f := range requiredFiles {
        if _, ok := proj.Files[f]; !ok {
            return nil, fmt.Errorf("生成的项目缺少必需文件: %s", f)
        }
    }

    return proj, nil
}

type Project struct {
    Files map[string]string
}

func (p *Project) WriteToDisk(basePath string) error {
    for filename, content := range p.Files {
        path := filepath.Join(basePath, filename)
        if err := os.WriteFile(path, []byte(content), 0644); err != nil {
            return fmt.Errorf("写入 %s 失败: %w", filename, err)
        }
    }
    return nil
}

Artifact 间的引用关系

复杂项目中,Artifact 之间可能存在依赖关系。例如,main.go 导入了 handlers 包。ADK Go 支持通过 Dependencies 字段显式声明这种关系:

// 在 Artifact 元数据中声明依赖
art := &artifact.Artifact{
    Title: "main.go",
    Type:  "application/vnd.code",
    Content: mainGoContent,
    Metadata: map[string]interface{}{
        "language": "go",
        "dependencies": []string{"handlers.go", "models.go"},
    },
}

这使得下游系统可以:

  • 按依赖顺序处理 Artifacts(先处理模型,再处理处理器,最后处理入口)
  • 构建 Artifact 依赖图,检测循环依赖
  • 当某个 Artifact 更新时,识别所有受影响的下游 Artifact

安全考量:不可信内容的处理

当 Agent 生成 HTML、JavaScript 或 SQL 等内容时,安全风险显著增加。Artifacts 系统提供了多层防护机制:

1. HTML/SVG 的 XSS 防护

htmlHandler := artifact.HTMLHandler{
    Sanitize: true,
    // 使用 bluemonday 等库进行严格的 HTML 过滤
    AllowedTags: []string{"p", "br", "strong", "em", "code", "pre"},
    AllowedAttrs: map[string][]string{
        "a": {"href"},
    },
}

2. SQL 注入防护

对于 SQL Artifact,永远不要直接执行 Agent 生成的 SQL。正确的做法是:

func safeExecuteSQL(db *sql.DB, sqlArt *artifact.Artifact) error {
    // 1. 使用 SQL 解析器分析查询类型
    stmt, err := sqlparser.Parse(sqlArt.Content)
    if err != nil {
        return fmt.Errorf("SQL 解析失败: %w", err)
    }

    // 2. 只允许白名单中的操作类型
    switch stmt.(type) {
    case *sqlparser.Select:
        // SELECT 是安全的(只读)
    default:
        return fmt.Errorf("不允许执行非 SELECT 语句")
    }

    // 3. 在只读副本上执行
    rows, err := db.Query(sqlArt.Content)
    // ...
}

3. 代码执行隔离

如果系统需要执行 Agent 生成的代码(如 Python 脚本),必须在沙箱环境中运行:

func executeInSandbox(code string, timeout time.Duration) (*SandboxResult, error) {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    cmd := exec.CommandContext(ctx, "firejail", "--noprofile", "python3", "-c", code)
    cmd.Env = []string{}  // 清空环境变量
    
    output, err := cmd.CombinedOutput()
    // ...
}

调试与质量保障

Artifacts 的调试比纯文本输出更复杂,因为需要验证结构正确性、语义一致性和跨 Artifact 协调性。建议实施以下质量保障措施:

自动化验证流水线

func validateArtifacts(artifacts []artifact.Artifact) []ValidationError {
    var errors []ValidationError

    for _, art := range artifacts {
        handler := registry.Get(art.Type)
        if handler == nil {
            errors = append(errors, ValidationError{
                Artifact: art.Title,
                Message:  fmt.Sprintf("未知的 Artifact 类型: %s", art.Type),
            })
            continue
        }

        if err := handler.Validate(&art); err != nil {
            errors = append(errors, ValidationError{
                Artifact: art.Title,
                Message:  err.Error(),
            })
        }
    }

    // 跨 Artifact 验证:检查引用一致性
    errors = append(errors, validateCrossReferences(artifacts)...)

    return errors
}

版本控制与 Diff

将生成的 Artifacts 纳入版本控制,可以追踪 Agent 输出随时间的变化:

func diffArtifacts(old, new []artifact.Artifact) []ArtifactDiff {
    // 按标题匹配,计算文本差异
    // 使用 github.com/sergi/go-diff/diffmatchpatch 等库
}

性能优化

大规模使用 Artifacts 时需要注意以下性能问题:

  1. Token 消耗:Artifacts 通常比普通文本更结构化,可能包含更多冗余字符(缩进、括号等)。监控 Token 使用情况,必要时对大型 Artifact 进行压缩或分片。

  2. 渲染延迟:前端渲染复杂 Artifact(如大型 Mermaid 图表)可能阻塞 UI。使用 Web Worker 或虚拟列表进行优化。

  3. 存储开销:如果持久化存储所有历史 Artifacts,考虑使用对象存储(S3、GCS)而非关系型数据库。

下一步

Artifacts 解决了 Agent"如何输出"的问题——通过结构化、类型化的内容生成,使 Agent 的输出可以被程序可靠地消费和处理。接下来我们将探讨 Skills——如何给 Agent 挂载预设的专业能力模块,让它在特定领域表现得更加专业。

Grounding | Skills for Agents →


想跟着学更多 Go ADK 实战?关注「全栈之巅-梦兽编程」公众号,每周更新 Go / AI 编程实战干货。