【AI Agent 与 Super Agent 构建实战】第 02 篇:Go Agent 开发环境搭建与 LLM 接入

本系列定位:用 Go 语言从零构建各类 AI Agent,覆盖 ReAct、Plan-and-Execute、Multi-Agent、Super Agent 等核心设计模式的完整工程实现。


本篇你将学到

  • 搭建 Go Agent 开发环境(go-openai + Ollama)
  • 实现多 Provider 统一抽象层(OpenAI / Anthropic / Ollama 一套接口)
  • 流式输出(Streaming)的完整实现
  • AgentForge 项目骨架搭建与配置驱动架构

学完本篇,你将拥有一个能接入任意 LLM 的 Go 项目骨架,后续所有 Agent 功能都在此基础上扩展。


一、技术栈与环境准备

1.1 整体架构

LLM Provider

AgentForge v0.1 骨架

CLI 入口

配置管理
config.yaml

Provider 抽象层

会话管理

OpenAI API
GPT-4 / GPT-4o

Ollama 本地
Llama / Qwen

Anthropic API
Claude

1.2 环境要求

组件 版本要求 说明
Go 1.23+ 需要 generics 支持
go-openai latest OpenAI API Go SDK,兼容所有 OpenAI 格式端点
Ollama 0.5+ 本地推理引擎(可选,用于离线开发)
操作系统 Linux / macOS / Windows 跨平台支持

1.3 项目初始化

mkdir agentforge
cd agentforge
go mod init github.com/yourname/agentforge

# 核心依赖
go get github.com/sashabaranov/go-openai
go get github.com/spf13/viper   # 配置管理
go get github.com/spf13/cobra   # CLI 框架

项目目录结构:

agentforge/
├── cmd/
│   └── root.go          # CLI 入口
├── internal/
│   ├── config/          # 配置管理
│   ├── provider/        # LLM Provider 抽象层
│   ├── session/         # 会话管理
│   └── agent/           # Agent 核心逻辑(后续扩展)
├── config.yaml          # 配置文件
├── go.mod
└── main.go

二、配置驱动架构

2.1 配置文件设计

# config.yaml
provider:
  # 默认使用的 Provider
  default: openai

  # OpenAI 配置(也兼容第三方 OpenAI 格式 API)
  openai:
    api_key: "sk-xxxxxxxx"
    base_url: "https://api.openai.com/v1"
    model: "gpt-4o"
    max_tokens: 4096
    temperature: 0.7

  # Ollama 本地配置
  ollama:
    base_url: "http://localhost:11434/v1"
    api_key: "ollama"           # Ollama 不需要真实 key
    model: "qwen2.5:14b"
    max_tokens: 4096
    temperature: 0.7

  # Anthropic 配置
  anthropic:
    api_key: "sk-ant-xxxxxxxx"
    base_url: "https://api.anthropic.com/v1"
    model: "claude-3-5-sonnet"
    max_tokens: 4096
    temperature: 0.7

session:
  system_prompt: "你是一个有帮助的 AI 助手。"
  max_history: 20               # 保留最近 20 条消息

2.2 配置加载代码

// internal/config/config.go
package config

import (
	"fmt"
	"os"

	"github.com/spf13/viper"
)

// AppConfig 应用全局配置
type AppConfig struct {
	Provider ProviderConfig `mapstructure:"provider"`
	Session  SessionConfig  `mapstructure:"session"`
}

// ProviderConfig 包含所有 Provider 的配置
type ProviderConfig struct {
	Default   string         `mapstructure:"default"` // openai / ollama / anthropic
	OpenAI    ProviderParams `mapstructure:"openai"`
	Ollama    ProviderParams `mapstructure:"ollama"`
	Anthropic ProviderParams `mapstructure:"anthropic"`
}

// ProviderParams 单个 Provider 的连接参数
type ProviderParams struct {
	APIKey      string  `mapstructure:"api_key"`
	BaseURL     string  `mapstructure:"base_url"`
	Model       string  `mapstructure:"model"`
	MaxTokens   int     `mapstructure:"max_tokens"`
	Temperature float32 `mapstructure:"temperature"`
}

// SessionConfig 会话配置
type SessionConfig struct {
	SystemPrompt string `mapstructure:"system_prompt"`
	MaxHistory   int    `mapstructure:"max_history"`
}

// Load 加载配置文件
func Load(path string) (*AppConfig, error) {
	viper.SetConfigFile(path)
	viper.AutomaticEnv() // 环境变量覆盖

	if err := viper.ReadInConfig(); err != nil {
		return nil, fmt.Errorf("读取配置失败: %w", err)
	}

	var cfg AppConfig
	if err := viper.Unmarshal(&cfg); err != nil {
		return nil, fmt.Errorf("解析配置失败: %w", err)
	}

	// 环境变量优先:API_KEY 可通过环境变量注入(生产安全)
	if key := os.Getenv("OPENAI_API_KEY"); key != "" {
		cfg.Provider.OpenAI.APIKey = key
	}

	return &cfg, nil
}

// GetProviderParams 根据名称获取 Provider 参数
func (c *AppConfig) GetProviderParams(name string) (ProviderParams, error) {
	switch name {
	case "openai":
		return c.Provider.OpenAI, nil
	case "ollama":
		return c.Provider.Ollama, nil
	case "anthropic":
		return c.Provider.Anthropic, nil
	default:
		return ProviderParams{}, fmt.Errorf("未知 provider: %s", name)
	}
}

设计要点:API Key 支持环境变量注入,避免硬编码在配置文件中。生产环境应使用密钥管理服务。


三、Provider 抽象层设计

这是本篇的核心——一个统一的 LLM 接口,屏蔽不同 Provider 的差异。

3.1 抽象接口定义

实现类

统一接口

LLMProvider 接口
Chat() / StreamChat()

OpenAIProvider
兼容 OpenAI 格式

OllamaProvider
本地推理

AnthropicProvider
Claude 系列

// internal/provider/provider.go
package provider

import (
	"context"

	"github.com/yourname/agentforge/internal/config"
)

// Message 统一消息格式
type Message struct {
	Role    string `json:"role"`    // system / user / assistant / tool
	Content string `json:"content"` // 消息内容
}

// ChatRequest 聊天请求
type ChatRequest struct {
	Messages    []Message
	Model       string
	MaxTokens   int
	Temperature float32
}

// ChatResponse 聊天响应
type ChatResponse struct {
	Content    string // 回复文本
	TokensUsed int    // 消耗的 Token 数
	Model      string // 实际使用的模型
}

// LLMProvider LLM 提供者抽象接口
type LLMProvider interface {
	// Chat 同步聊天
	Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
	// StreamChat 流式聊天,通过 channel 逐块返回
	StreamChat(ctx context.Context, req ChatRequest) (<-chan string, error)
}

// New 根据配置创建 Provider 实例(工厂模式)
func New(name string, cfg *config.AppConfig) (LLMProvider, error) {
	params, err := cfg.GetProviderParams(name)
	if err != nil {
		return nil, err
	}

	switch name {
	case "openai", "ollama":
		// Ollama 兼容 OpenAI API 格式,共用同一个实现
		return NewOpenAICompatible(params), nil
	case "anthropic":
		return NewAnthropicProvider(params), nil
	default:
		return nil, fmt.Errorf("不支持的 provider: %s", name)
	}
}

3.2 OpenAI 兼容实现(同时支持 Ollama)

由于 Ollama 提供了 OpenAI 兼容端点,一个实现即可同时支持 OpenAI 和 Ollama:

// internal/provider/openai.go
package provider

import (
	"context"
	"fmt"

	"github.com/sashabaranov/go-openai"
	"github.com/yourname/agentforge/internal/config"
)

// OpenAICompatible 兼容 OpenAI API 格式的 Provider
type OpenAICompatible struct {
	client *openai.Client
	config config.ProviderParams
}

// NewOpenAICompatible 创建 OpenAI 兼容 Provider
func NewOpenAICompatible(cfg config.ProviderParams) *OpenAICompatible {
	// 自定义 BaseURL(用于 Ollama 或第三方 API)
	clientConfig := openai.DefaultConfig(cfg.APIKey)
	if cfg.BaseURL != "" {
		clientConfig.BaseURL = cfg.BaseURL
	}

	return &OpenAICompatible{
		client: openai.NewClientWithConfig(clientConfig),
		config: cfg,
	}
}

// Chat 同步聊天
func (p *OpenAICompatible) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
	// 转换消息格式
	var msgs []openai.ChatCompletionMessage
	for _, m := range req.Messages {
		msgs = append(msgs, openai.ChatCompletionMessage{
			Role:    m.Role,
			Content: m.Content,
		})
	}

	// 构建请求
	model := req.Model
	if model == "" {
		model = p.config.Model
	}
	maxTokens := req.MaxTokens
	if maxTokens == 0 {
		maxTokens = p.config.MaxTokens
	}
	temp := req.Temperature
	if temp == 0 {
		temp = p.config.Temperature
	}

	resp, err := p.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
		Model:       model,
		Messages:    msgs,
		MaxTokens:   maxTokens,
		Temperature: temp,
	})
	if err != nil {
		return nil, fmt.Errorf("LLM 调用失败: %w", err)
	}

	return &ChatResponse{
		Content:    resp.Choices[0].Message.Content,
		TokensUsed: resp.Usage.TotalTokens,
		Model:      resp.Model,
	}, nil
}

// StreamChat 流式聊天
func (p *OpenAICompatible) StreamChat(ctx context.Context, req ChatRequest) (<-chan string, error) {
	var msgs []openai.ChatCompletionMessage
	for _, m := range req.Messages {
		msgs = append(msgs, openai.ChatCompletionMessage{
				Role:    m.Role,
			Content: m.Content,
		})
	}

	model := req.Model
	if model == "" {
		model = p.config.Model
	}

	stream, err := p.client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
		Model:       model,
		Messages:    msgs,
		MaxTokens:   p.config.MaxTokens,
		Temperature: p.config.Temperature,
	})
	if err != nil {
		return nil, fmt.Errorf("创建流式请求失败: %w", err)
	}

	ch := make(chan string, 100)

	go func() {
		defer close(ch)
		defer stream.Close()

		for {
			resp, err := stream.Recv()
			if err != nil {
				if err.Error() == "EOF" {
					return
				}
				ch <- fmt.Sprintf("\n[错误] %v", err)
				return
			}
			if len(resp.Choices) > 0 {
				delta := resp.Choices[0].Delta.Content
				if delta != "" {
					ch <- delta
				}
			}
		}
	}()

	return ch, nil
}

3.3 流式输出效果演示

// 流式输出的使用方式
func main() {
	provider, _ := provider.New("ollama", cfg)

	ch, _ := provider.StreamChat(ctx, provider.ChatRequest{
		Messages: []provider.Message{
			{Role: "user", Content: "用三句话解释什么是 AI Agent"},
		},
	})

	// 逐块打印,实现打字机效果
	for chunk := range ch {
		fmt.Print(chunk)
	}
}

运行效果(文字逐块出现):

AI Agent 是一个能够自主感知环境、做出决策并采取行动来达成目标的人工智能系统。
它由大语言模型(LLM)作为大脑,通过调用外部工具来执行实际操作。
与普通聊天机器人不同,Agent 具备自主循环能力,能多步推理和行动直到任务完成。

四、会话管理

4.1 Session 设计

LLM Provider Session 用户 LLM Provider Session 用户 发送消息 "你好" 追加 user 消息到历史 Chat(全部历史消息) API 调用 回复 ChatResponse 追加 assistant 消息到历史 显示回复
// internal/session/session.go
package session

import (
	"context"
	"fmt"

	"github.com/yourname/agentforge/internal/provider"
)

// Session 管理一次对话的完整上下文
type Session struct {
	provider     provider.LLMProvider
	systemPrompt string
	maxHistory   int
	messages     []provider.Message
}

// New 创建新会话
func New(p provider.LLMProvider, systemPrompt string, maxHistory int) *Session {
	s := &Session{
		provider:     p,
		systemPrompt: systemPrompt,
		maxHistory:   maxHistory,
		messages:     []provider.Message{},
	}
	// System Prompt 作为第一条消息
	if systemPrompt != "" {
		s.messages = append(s.messages, provider.Message{
			Role:    "system",
			Content: systemPrompt,
		})
	}
	return s
}

// Chat 发送消息并获取回复
func (s *Session) Chat(ctx context.Context, userInput string) (string, error) {
	// 追加用户消息
	s.messages = append(s.messages, provider.Message{
		Role:    "user",
		Content: userInput,
	})

	// 调用 LLM
	resp, err := s.provider.Chat(ctx, provider.ChatRequest{
		Messages: s.messages,
	})
	if err != nil {
		return "", err
	}

	// 追加助手回复
	s.messages = append(s.messages, provider.Message{
		Role:    "assistant",
		Content: resp.Content,
	})

	// 历史消息超出上限时截断(保留 system prompt)
	s.truncate()

	return resp.Content, nil
}

// StreamChat 流式发送消息
func (s *Session) StreamChat(ctx context.Context, userInput string) (<-chan string, error) {
	s.messages = append(s.messages, provider.Message{
		Role:    "user",
		Content: userInput,
	})

	ch, err := s.provider.StreamChat(ctx, provider.ChatRequest{
		Messages: s.messages,
	})
	if err != nil {
		return nil, err
	}

	// 包装 channel:流结束后收集完整回复存入历史
	wrappedCh := make(chan string, 100)
	go func() {
		defer close(wrappedCh)
		var fullContent string
		for chunk := range ch {
			fullContent += chunk
			wrappedCh <- chunk
		}
		s.messages = append(s.messages, provider.Message{
			Role:    "assistant",
			Content: fullContent,
		})
		s.truncate()
	}()

	return wrappedCh, nil
}

// truncate 截断历史消息(保留 system prompt + 最近 N 条)
func (s *Session) truncate() {
	// 保留第一条 system prompt + 最近 maxHistory 条
	if len(s.messages) <= s.maxHistory+1 {
		return
	}
	systemMsgs := []provider.Message{}
	otherMsgs := []provider.Message{}
	for _, m := range s.messages {
		if m.Role == "system" {
			systemMsgs = append(systemMsgs, m)
		} else {
			otherMsgs = append(otherMsgs, m)
		}
	}
	// 截断非 system 消息
	start := len(otherMsgs) - s.maxHistory
	if start < 0 {
		start = 0
	}
	s.messages = append(systemMsgs, otherMsgs[start:]...)
}

// History 获取当前对话历史
func (s *Session) History() []provider.Message {
	return s.messages
}

五、CLI 入口整合

5.1 完整的 main.go

// main.go
package main

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"strings"

	"github.com/spf13/cobra"
	"github.com/yourname/agentforge/internal/config"
	"github.com/yourname/agentforge/internal/provider"
	"github.com/yourname/agentforge/internal/session"
)

func main() {
	var configFile string

	rootCmd := &cobra.Command{
		Use:   "agentforge",
		Short: "AgentForge - AI Agent 锻造平台",
		Long:  "从零构建的 AI Agent 框架,本篇是 v0.1 基础对话引擎",
		Run: func(cmd *cobra.Command, args []string) {
			runChat(configFile)
		},
	}

	rootCmd.Flags().StringVarP(&configFile, "config", "c", "config.yaml", "配置文件路径")
	rootCmd.Execute()
}

func runChat(configFile string) {
	// 加载配置
	cfg, err := config.Load(configFile)
	if err != nil {
		fmt.Printf("配置加载失败: %v\n", err)
		os.Exit(1)
	}

	// 创建 Provider
	providerName := cfg.Provider.Default
	p, err := provider.New(providerName, cfg)
	if err != nil {
		fmt.Printf("Provider 创建失败: %v\n", err)
		os.Exit(1)
	}

	// 创建会话
	sess := session.New(p, cfg.Session.SystemPrompt, cfg.Session.MaxHistory)

	fmt.Printf("🤖 AgentForge v0.1 已启动 (Provider: %s)\n", providerName)
	fmt.Println("输入消息开始对话,输入 /quit 退出")
	fmt.Println(strings.Repeat("-", 50))

	scanner := bufio.NewScanner(os.Stdin)
	ctx := context.Background()

	for {
		fmt.Print("\n你 > ")
		if !scanner.Scan() {
			break
		}
		input := strings.TrimSpace(scanner.Text())
		if input == "/quit" {
			break
		}
		if input == "" {
			continue
		}
		if input == "/history" {
			for _, m := range sess.History() {
				fmt.Printf("  [%s] %s\n", m.Role, truncate(m.Content, 60))
			}
			continue
		}

		// 流式输出
		fmt.Print("AI > ")
		ch, err := sess.StreamChat(ctx, input)
		if err != nil {
			fmt.Printf("错误: %v\n", err)
			continue
		}
		for chunk := range ch {
			fmt.Print(chunk)
		}
		fmt.Println()
	}
	fmt.Println("再见!")
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}

5.2 运行效果

$ go run main.go

🤖 AgentForge v0.1 已启动 (Provider: ollama)
输入消息开始对话,输入 /quit 退出
--------------------------------------------------

你 > 我叫张三,记住我的名字
AI > 你好张三!很高兴认识你,我已经记住了你的名字。

你 > 我叫什么?
AI > 你叫张三。

你 > /history
  [system] 你是一个有帮助的 AI 助手。
  [user] 我叫张三,记住我的名字
  [assistant] 你好张三!很高兴认识你,我已经记住了你的名字。
  [user] 我叫什么?
  [assistant] 你叫张三。

本篇小结

知识点 核心内容
Provider 抽象层 定义 LLMProvider 接口,屏蔽 OpenAI / Ollama / Anthropic 差异
OpenAI 兼容策略 Ollama 兼容 OpenAI API 格式,一个实现覆盖两个 Provider
流式输出 StreamChat() 通过 channel 逐块返回,实现打字机效果
会话管理 Session 维护消息历史,支持截断和持久化
配置驱动 YAML 配置 + 环境变量覆盖,API Key 不硬编码
AgentForge v0.1 完整的 CLI 对话工具,支持多 Provider 切换

下篇预告

第 03 篇:LLM 核心机制回顾 — 提示工程与上下文窗口
深入理解 Prompt 结构化设计、Token 计费机制、Context Window 管理策略——这些是后续 Agent 设计的底层基础。


如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐