前言 最近在给自己的平台 TapeCode 做后台升级,想让 AI 真正参与到平台运营
用 Go 从零实现一个 AI Agent 后台系统
发布时间: 2026-06-23 (a month ago)
GOAgent

前言

最近在给自己的平台 TapeCode 做后台升级,想让 AI 真正参与到平台运营里来,而不仅仅是一个漂亮的网页聊天框。
普通的 AI 对话能回答问题,但它做不到:

  • “去数据库里查一下这个用户买了什么课。”
  • “把这个在评论区刷单或辱骂的用户封禁 7 天。”
    AI Agent + Function Calling 就是解决这个问题的金钥匙。
    这篇文章,我会结合自己项目里的真实代码,从头讲清楚:
  1. 什么是 Function Calling,为什么它让 AI 能“干活”?
  2. 如何抽象出一个通用的 Go 语言 Agent 运行引擎?
  3. 如何在一个真实的在线教育平台里落地两种实际 Agent。

一、 普通 LLM 调用 vs Agent 调用

先明确一个认知差距:

  • 普通 LLM 调用:你发一段话,它回一段话,属于简单的问答。
    • :“这个用户买了哪些课?”
    • AI:“我不知道,我没有访问你数据库的权限和能力。”
  • Agent + Function Calling 调用:你告诉 LLM“你可以使用这些特定的工具”,它会根据上下文自主决定要不要调用、调用哪个、以及传什么参数。最后由系统执行工具并把结果返回给它,它再给出最终答案。
    sequenceDiagram actor User as 运营/管理员 participant System as Go 业务系统 (TapeCode) participant LLM as 豆包大模型 (Doubao) User->>System: 查一下 ID 为 42 的用户买了什么课? System->>LLM: 携带可用工具 [query_user_orders] 请求 Note over LLM: 决策: 需调用 query_user_orders<br/>参数: user_id=42 LLM-->>System: 触发 ToolCall System->>System: 查库 ToolQueryUserOrders(42) System->>LLM: 反馈工具执行结果 [订单数据] LLM-->>System: 综合结果,翻译成自然语言 System-->>User: 用户 42 购买了《Go 入门》和《Docker 实战》,共两门课程。

这才是真正意义上的 AI 融入业务系统。

二、 LLM 客户端封装

项目使用的是字节跳动旗下的豆包大模型(Doubao),通过火山引擎 ARK Runtime Go SDK 接入。

2.1 基础对话(流式接收返回完整结果)

go 复制代码
// SendContent 发送内容给 LLM(内部走流式,返回完整结果)
func SendContent(systemPrompt string, history []Message) (string, error) {
    client := NewLLMClient()
    
    // 构造消息列表,包含 System Prompt 和历史对话
    reqMessages := []*model.ChatCompletionMessage{
        {
            Role:    model.ChatMessageRoleSystem,
            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(systemPrompt)},
        },
    }
    for _, msg := range history {
        reqMessages = append(reqMessages, &model.ChatCompletionMessage{
            Role:    msg.Role,
            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(msg.Content)},
        })
    }
    req := model.CreateChatCompletionRequest{
        Model:    modelID,
        Messages: reqMessages,
        Stream:   volcengine.Bool(true),
    }
    
    stream, err := client.CreateChatCompletionStream(context.Background(), req)
    if err != nil {
        return "", err
    }
    defer stream.Close()
    var fullContent strings.Builder
    for {
        resp, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return "", err
        }
        delta := resp.Choices[0].Delta.Content
        if delta != "" {
            fullContent.WriteString(delta)
        }
    }
    return fullContent.String(), nil
}

2.2 流式推送(用于 WebSocket 实时输出)

通过回调函数 StreamCallback 实时把 Token 推送给前端,实现类似 ChatGPT 的打字机体验。

go 复制代码
type StreamCallback func(string)
// SendStreamContent 每收到一个 delta 就触发回调
func SendStreamContent(systemPrompt string, history []Message, onData StreamCallback) (string, error) {
    client := NewLLMClient()
    // ... 构造 reqMessages 逻辑同上 ...
    
    req := model.CreateChatCompletionRequest{
        Model:    modelID,
        Messages: reqMessages,
        Stream:   volcengine.Bool(true),
    }
    stream, err := client.CreateChatCompletionStream(context.Background(), req)
    if err != nil {
        return "", err
    }
    defer stream.Close()
    var fullContent strings.Builder
    for {
        resp, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return "", err
        }
        delta := resp.Choices[0].Delta.Content
        if delta != "" {
            fullContent.WriteString(delta)
            onData(delta) // 实时推送给前端通道/WebSocket
        }
    }
    return fullContent.String(), nil
}

2.3 携带工具的对话(Agent 核心)

go 复制代码
// ChatWithTools 携带工具定义调用 LLM
func ChatWithTools(messages []*model.ChatCompletionMessage, tools []*model.Tool) (*model.ChatCompletionResponse, error) {
    client := NewLLMClient()
    req := model.CreateChatCompletionRequest{
        Model:    modelID,
        Messages: messages,
        Tools:    tools, // 关键:把工具列表和参数描述告诉 LLM
    }
    resp, err := client.CreateChatCompletion(context.Background(), req)
    if err != nil {
        return nil, err
    }
    return &resp, nil
}

三、 通用 Agent 运行引擎

为了优雅地复用 Agent 逻辑,避免为每一个功能写一坨冗余的判断代码,我们抽象了一个 AgentConfig 结构体和通用的 RunAgent 引擎:

go 复制代码
// AgentConfig 定义一个“技能 Agent”的配置与能力
type AgentConfig struct {
    Name         string
    SystemPrompt string
    Tools        []*model.Tool
    ToolMap      map[string]func(string) string // 工具执行器:函数名 -> 本地具体执行函数
}

通用运行引擎 RunAgent 协调处理整个双轮调用流程:

go 复制代码
// RunAgent 通用 Agent 运行引擎
func RunAgent(agent AgentConfig, userQuery string) string {
    // Step 1: 构造对话,注入系统 Prompt 与用户提问
    messages := []*model.ChatCompletionMessage{
        {
            Role:    model.ChatMessageRoleSystem,
            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(agent.SystemPrompt)},
        },
        {
            Role:    model.ChatMessageRoleUser,
            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(userQuery)},
        },
    }
    // Step 2: 第一轮调用,LLM 决定是否使用工具
    resp, err := ChatWithTools(messages, agent.Tools)
    if err != nil {
        return fmt.Sprintf("Error in LLM calling: %v", err)
    }
    msg := resp.Choices[0].Message
    // Step 3: 如果不需要调用工具,说明 AI 可以直接回答,直接返回内容
    if len(msg.ToolCalls) == 0 {
        return *msg.Content.StringValue
    }
    // Step 4: LLM 发出 ToolCalls 意图,Go 在本地反射/映射执行真实操作
    messages = append(messages, &msg)
    for _, toolCall := range msg.ToolCalls {
        fname := toolCall.Function.Name
        args  := toolCall.Function.Arguments
        var result string
        if handler, ok := agent.ToolMap[fname]; ok {
            result = handler(args) // 调用对应的本地业务函数
        } else {
            result = "Error: Tool not found"
        }
        // 把工具的真实执行结果作为 "Tool" 角色的消息附加到上下文中
        messages = append(messages, &model.ChatCompletionMessage{
            Role:       model.ChatMessageRoleTool,
            Content:    &model.ChatCompletionMessageContent{StringValue: volcengine.String(result)},
            ToolCallID: toolCall.ID,
        })
    }
    // Step 5: 第二轮调用,LLM 结合第一轮上下文及刚刚获取的工具执行结果,输出自然语言
    finalResp, err := ChatWithTools(messages, agent.Tools)
    if err != nil {
        return fmt.Sprintf("Error in final LLM calling: %v", err)
    }
    return *finalResp.Choices[0].Message.Content.StringValue
}

四、 两个真实的业务 Agent

在 TapeCode 平台中,目前落地了两个业务场景 Agent:

4.1 DataAgent:数据查询专家

  • 职责:供运营团队快速查询用户信息及购买记录。
  • 定义的函数/工具
    • query_user_info:根据 ID 或昵称查询用户基础信息。
    • query_user_orders:查询用户最近的已支付订单记录。
go 复制代码
func NewDataAgent() AgentConfig {
    return AgentConfig{
        Name:         "DataStats",
        SystemPrompt: cfg.DataAgentPrompt, // 从配置读取
        Tools: []*model.Tool{
            {
                Type: model.ToolTypeFunction,
                Function: &model.FunctionDefinition{
                    Name:        "query_user_info",
                    Description: "查询用户的基本信息,如ID、昵称、邮箱等。输入可以是用户ID或昵称。",
                    Parameters: map[string]any{
                        "type": "object",
                        "properties": map[string]any{
                            "keyword": map[string]any{
                                "type":        "string",
                                "description": "用户的ID或者昵称",
                            },
                        },
                        "required": []string{"keyword"},
                    },
                },
            },
        },
        ToolMap: map[string]func(string) string{
            "query_user_info": func(argsStr string) string {
                var args struct{ Keyword string `json:"keyword"` }
                _ = json.Unmarshal([]byte(argsStr), &args)
                return ToolQueryUserInfo(args.Keyword)
            },
        },
    }
}
// 具体的底层数据库查询函数
func ToolQueryUserInfo(keyword string) string {
    var user models.UserModel
    if err := global.DB.Preload("Roles").Where("id = ? OR nickname = ?", keyword, keyword).First(&user).Error; err != nil {
        return "未找到对应用户信息"
    }
    return fmt.Sprintf("ID: %d\n昵称: %s\n邮箱: %s\n注册时间: %s",
        user.ID, user.Nickname, user.Email, user.CreatedAt.Format("2006-01-02"))
}

4.2 OpsAgent:运维操作专家

  • 职责:自动提取信息并执行平台管控操作,比如封禁违规用户。
  • 工具banned_user(需要提供用户 ID、封禁原因和天数)。
go 复制代码
func NewOpsAgent() AgentConfig {
    return AgentConfig{
        Name:         "SysAdmin",
        SystemPrompt: cfg.OpsAgentPrompt,
        Tools: []*model.Tool{
            {
                Type: model.ToolTypeFunction,
                Function: &model.FunctionDefinition{
                    Name:        "banned_user",
                    Description: "封禁违规用户,需要提供用户ID、封禁原因和封禁天数",
                    Parameters: map[string]any{
                        "type": "object",
                        "properties": map[string]any{
                            "user_id": map[string]any{"type": "string", "description": "要封禁的用户ID"},
                            "reason":  map[string]any{"type": "string", "description": "封禁原因"},
                            "days":    map[string]any{"type": "integer", "description": "封禁天数"},
                        },
                        "required": []string{"user_id", "reason", "days"},
                    },
                },
            },
        },
        ToolMap: map[string]func(string) string{
            "banned_user": func(argsStr string) string {
                var args struct {
                    UserID string `json:"user_id"`
                    Reason string `json:"reason"`
                    Days   int    `json:"days"`
                }
                _ = json.Unmarshal([]byte(argsStr), &args)
                userID, _ := strconv.ParseUint(args.UserID, 10, 32)
                return ToolBannedUser(uint(userID), args.Reason, args.Days)
            },
        },
    }
}
func ToolBannedUser(userID uint, reason string, days int) string {
    var user models.UserModel
    if err := global.DB.First(&user, userID).Error; err != nil {
        return "封禁操作失败:未找到对应用户"
    }
    if user.IsBanned {
        return fmt.Sprintf("用户 %s 已经被封禁过了", user.Nickname)
    }
    expiry := time.Now().AddDate(0, 0, days)
    global.DB.Model(&user).Updates(map[string]any{
        "is_banned":  true,
        "banned_at":  time.Now(),
        "ban_reason": reason,
        "ban_expiry": expiry,
    })
    return fmt.Sprintf("已成功封禁用户 %s,原因:%s,期限:%d天", user.Nickname, reason, days)
}

[!CAUTION]
安全警示(Human-in-the-Loop):涉及到“封禁”、“删除”、“数据导出”或“支付”等高风险操作时,千万不要把最终决定权完全放给 AI。建议在 ToolMap 执行前增加一层人工确认按钮,由人做最终确认,让 AI 充当“预处理者”或“草拟者”。


五、 设计亮点总结

  1. System Prompt 配置化:我们将 Agent 的 SystemPrompt 从代码中剥离,存入配置文件中。无需重新编译部署,即可动态调整 AI 的工作边界和“性格特征”。
  2. Tool 参数的 JSON Schema 严格定义:大模型并不懂你的代码签名,它判断参数类型和传递规范的唯一依据就是我们填写的 JSON Schema。给每个 Tool 加清晰的 description 是高精度调用的基础。
  3. 两轮调用的标准范式
    • 第一轮:将请求传给大模型,模型决策是否调用工具、调用哪一个、参数是什么
    • 第二轮:系统在本地完成调用,将结果返回给模型,由模型做二次加工后用自然语言输出。
  4. 单例客户端优化
    使用 sync.Once 确保全局只建立一个大模型 RPC 客户端,避免频繁发起 HTTP 连接请求:
    go 复制代码
    var (
        once   sync.Once
        client *arkruntime.Client
    )
    func NewLLMClient() *arkruntime.Client {
        once.Do(func() {
            client = arkruntime.NewClientWithApiKey(cfg.ArkApiKey, ...)
        })
        return client
    }

六、 核心安全机制

大模型虽强,但在生产环境中必须保持绝对的敬畏:

  • 最小权限原则:Agent 专用的数据库账号应当是严格隔离的。例如:DataAgent 只能使用读写分离的只读库账号,防止幻觉引发的写脏数据。
  • 完备的操作日志审计:每一条 Agent 触发的工具调用,都必须在数据库中写下完整的 Audit Log(包括原始提问、大模型决策提取的参数、以及实际执行结果),以便后续追责与逻辑回溯。
  • 请求频率与限制(Rate Limiting):防止大模型在 ReAct(思考链)模式下陷入无限循环或遭受外部输入攻击,短时间内对数据库服务造成恶意写冲击。

七、 结语

Function Calling 的核心思想其实很简单:你教 AI 认识你的本地工具,AI 自己决定什么时候使用。
而 Go 语言天生适合做这类工程结构性的封装:类型安全、并发结构清晰、且极易融入微服务和 GORM 等企业框架。
如果你也在用 Go 构建智能后台系统,希望这篇文章对你有所启发!