[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-47":3},{"id":4,"title":5,"title_en":6,"abstract":7,"abstract_en":8,"content":9,"content_en":10,"category":11,"banner_id":12,"banner_path":13,"tags":14,"is_recommend":17,"prev_article":18,"next_article":22,"created_at":26},47,"用 Go 从零实现一个 AI Agent 后台系统","Implement an AI Agent backend system from scratch using Go","## 前言\n最近在给自己的平台 **TapeCode** 做后台升级，想让 AI 真正参与到平台运营","Recently, I've been upgrading the backend of my platform **TapeCode** to enable AI to truly participate in platform operations, rather than just acting as a fancy chat box on a webpage.","## 前言\n最近在给自己的平台 **TapeCode** 做后台升级，想让 AI 真正参与到平台运营里来，而不仅仅是一个漂亮的网页聊天框。\n普通的 AI 对话能回答问题，但它做不到：\n* “去数据库里查一下这个用户买了什么课。”\n* “把这个在评论区刷单或辱骂的用户封禁 7 天。”\n**AI Agent + Function Calling** 就是解决这个问题的金钥匙。\n这篇文章，我会结合自己项目里的真实代码，从头讲清楚：\n1. 什么是 Function Calling，为什么它让 AI 能“干活”？\n2. 如何抽象出一个通用的 Go 语言 Agent 运行引擎？\n3. 如何在一个真实的在线教育平台里落地两种实际 Agent。\n---\n## 一、 普通 LLM 调用 vs Agent 调用\n先明确一个认知差距：\n* **普通 LLM 调用**：你发一段话，它回一段话，属于简单的问答。\n  * **你**：“这个用户买了哪些课？”\n  * **AI**：“我不知道，我没有访问你数据库的权限和能力。”\n* **Agent + Function Calling 调用**：你告诉 LLM“你可以使用这些特定的工具”，它会根据上下文自主决定要不要调用、调用哪个、以及传什么参数。最后由系统执行工具并把结果返回给它，它再给出最终答案。\n  ```mermaid\n  sequenceDiagram\n      actor User as 运营\u002F管理员\n      participant System as Go 业务系统 (TapeCode)\n      participant LLM as 豆包大模型 (Doubao)\n      User->>System: 查一下 ID 为 42 的用户买了什么课？\n      System->>LLM: 携带可用工具 [query_user_orders] 请求\n      Note over LLM: 决策: 需调用 query_user_orders\u003Cbr\u002F>参数: user_id=42\n      LLM-->>System: 触发 ToolCall\n      System->>System: 查库 ToolQueryUserOrders(42)\n      System->>LLM: 反馈工具执行结果 [订单数据]\n      LLM-->>System: 综合结果，翻译成自然语言\n      System-->>User: 用户 42 购买了《Go 入门》和《Docker 实战》，共两门课程。\n  ```\n这才是真正意义上的 AI 融入业务系统。\n---\n## 二、 LLM 客户端封装\n项目使用的是字节跳动旗下的**豆包大模型（Doubao）**，通过火山引擎 **ARK Runtime Go SDK** 接入。\n### 2.1 基础对话（流式接收返回完整结果）\n```go\n\u002F\u002F SendContent 发送内容给 LLM（内部走流式，返回完整结果）\nfunc SendContent(systemPrompt string, history []Message) (string, error) {\n    client := NewLLMClient()\n    \n    \u002F\u002F 构造消息列表，包含 System Prompt 和历史对话\n    reqMessages := []*model.ChatCompletionMessage{\n        {\n            Role:    model.ChatMessageRoleSystem,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(systemPrompt)},\n        },\n    }\n    for _, msg := range history {\n        reqMessages = append(reqMessages, &model.ChatCompletionMessage{\n            Role:    msg.Role,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(msg.Content)},\n        })\n    }\n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: reqMessages,\n        Stream:   volcengine.Bool(true),\n    }\n    \n    stream, err := client.CreateChatCompletionStream(context.Background(), req)\n    if err != nil {\n        return \"\", err\n    }\n    defer stream.Close()\n    var fullContent strings.Builder\n    for {\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            return \"\", err\n        }\n        delta := resp.Choices[0].Delta.Content\n        if delta != \"\" {\n            fullContent.WriteString(delta)\n        }\n    }\n    return fullContent.String(), nil\n}\n```\n### 2.2 流式推送（用于 WebSocket 实时输出）\n通过回调函数 `StreamCallback` 实时把 Token 推送给前端，实现类似 ChatGPT 的打字机体验。\n```go\ntype StreamCallback func(string)\n\u002F\u002F SendStreamContent 每收到一个 delta 就触发回调\nfunc SendStreamContent(systemPrompt string, history []Message, onData StreamCallback) (string, error) {\n    client := NewLLMClient()\n    \u002F\u002F ... 构造 reqMessages 逻辑同上 ...\n    \n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: reqMessages,\n        Stream:   volcengine.Bool(true),\n    }\n    stream, err := client.CreateChatCompletionStream(context.Background(), req)\n    if err != nil {\n        return \"\", err\n    }\n    defer stream.Close()\n    var fullContent strings.Builder\n    for {\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            return \"\", err\n        }\n        delta := resp.Choices[0].Delta.Content\n        if delta != \"\" {\n            fullContent.WriteString(delta)\n            onData(delta) \u002F\u002F 实时推送给前端通道\u002FWebSocket\n        }\n    }\n    return fullContent.String(), nil\n}\n```\n### 2.3 携带工具的对话（Agent 核心）\n```go\n\u002F\u002F ChatWithTools 携带工具定义调用 LLM\nfunc ChatWithTools(messages []*model.ChatCompletionMessage, tools []*model.Tool) (*model.ChatCompletionResponse, error) {\n    client := NewLLMClient()\n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: messages,\n        Tools:    tools, \u002F\u002F 关键：把工具列表和参数描述告诉 LLM\n    }\n    resp, err := client.CreateChatCompletion(context.Background(), req)\n    if err != nil {\n        return nil, err\n    }\n    return &resp, nil\n}\n```\n---\n## 三、 通用 Agent 运行引擎\n为了优雅地复用 Agent 逻辑，避免为每一个功能写一坨冗余的判断代码，我们抽象了一个 `AgentConfig` 结构体和通用的 `RunAgent` 引擎：\n```go\n\u002F\u002F AgentConfig 定义一个“技能 Agent”的配置与能力\ntype AgentConfig struct {\n    Name         string\n    SystemPrompt string\n    Tools        []*model.Tool\n    ToolMap      map[string]func(string) string \u002F\u002F 工具执行器：函数名 -> 本地具体执行函数\n}\n```\n通用运行引擎 `RunAgent` 协调处理整个双轮调用流程：\n```go\n\u002F\u002F RunAgent 通用 Agent 运行引擎\nfunc RunAgent(agent AgentConfig, userQuery string) string {\n    \u002F\u002F Step 1: 构造对话，注入系统 Prompt 与用户提问\n    messages := []*model.ChatCompletionMessage{\n        {\n            Role:    model.ChatMessageRoleSystem,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(agent.SystemPrompt)},\n        },\n        {\n            Role:    model.ChatMessageRoleUser,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(userQuery)},\n        },\n    }\n    \u002F\u002F Step 2: 第一轮调用，LLM 决定是否使用工具\n    resp, err := ChatWithTools(messages, agent.Tools)\n    if err != nil {\n        return fmt.Sprintf(\"Error in LLM calling: %v\", err)\n    }\n    msg := resp.Choices[0].Message\n    \u002F\u002F Step 3: 如果不需要调用工具，说明 AI 可以直接回答，直接返回内容\n    if len(msg.ToolCalls) == 0 {\n        return *msg.Content.StringValue\n    }\n    \u002F\u002F Step 4: LLM 发出 ToolCalls 意图，Go 在本地反射\u002F映射执行真实操作\n    messages = append(messages, &msg)\n    for _, toolCall := range msg.ToolCalls {\n        fname := toolCall.Function.Name\n        args  := toolCall.Function.Arguments\n        var result string\n        if handler, ok := agent.ToolMap[fname]; ok {\n            result = handler(args) \u002F\u002F 调用对应的本地业务函数\n        } else {\n            result = \"Error: Tool not found\"\n        }\n        \u002F\u002F 把工具的真实执行结果作为 \"Tool\" 角色的消息附加到上下文中\n        messages = append(messages, &model.ChatCompletionMessage{\n            Role:       model.ChatMessageRoleTool,\n            Content:    &model.ChatCompletionMessageContent{StringValue: volcengine.String(result)},\n            ToolCallID: toolCall.ID,\n        })\n    }\n    \u002F\u002F Step 5: 第二轮调用，LLM 结合第一轮上下文及刚刚获取的工具执行结果，输出自然语言\n    finalResp, err := ChatWithTools(messages, agent.Tools)\n    if err != nil {\n        return fmt.Sprintf(\"Error in final LLM calling: %v\", err)\n    }\n    return *finalResp.Choices[0].Message.Content.StringValue\n}\n```\n---\n## 四、 两个真实的业务 Agent\n在 TapeCode 平台中，目前落地了两个业务场景 Agent：\n### 4.1 DataAgent：数据查询专家\n* **职责**：供运营团队快速查询用户信息及购买记录。\n* **定义的函数\u002F工具**：\n  * `query_user_info`：根据 ID 或昵称查询用户基础信息。\n  * `query_user_orders`：查询用户最近的已支付订单记录。\n```go\nfunc NewDataAgent() AgentConfig {\n    return AgentConfig{\n        Name:         \"DataStats\",\n        SystemPrompt: cfg.DataAgentPrompt, \u002F\u002F 从配置读取\n        Tools: []*model.Tool{\n            {\n                Type: model.ToolTypeFunction,\n                Function: &model.FunctionDefinition{\n                    Name:        \"query_user_info\",\n                    Description: \"查询用户的基本信息，如ID、昵称、邮箱等。输入可以是用户ID或昵称。\",\n                    Parameters: map[string]any{\n                        \"type\": \"object\",\n                        \"properties\": map[string]any{\n                            \"keyword\": map[string]any{\n                                \"type\":        \"string\",\n                                \"description\": \"用户的ID或者昵称\",\n                            },\n                        },\n                        \"required\": []string{\"keyword\"},\n                    },\n                },\n            },\n        },\n        ToolMap: map[string]func(string) string{\n            \"query_user_info\": func(argsStr string) string {\n                var args struct{ Keyword string `json:\"keyword\"` }\n                _ = json.Unmarshal([]byte(argsStr), &args)\n                return ToolQueryUserInfo(args.Keyword)\n            },\n        },\n    }\n}\n\u002F\u002F 具体的底层数据库查询函数\nfunc ToolQueryUserInfo(keyword string) string {\n    var user models.UserModel\n    if err := global.DB.Preload(\"Roles\").Where(\"id = ? OR nickname = ?\", keyword, keyword).First(&user).Error; err != nil {\n        return \"未找到对应用户信息\"\n    }\n    return fmt.Sprintf(\"ID: %d\\n昵称: %s\\n邮箱: %s\\n注册时间: %s\",\n        user.ID, user.Nickname, user.Email, user.CreatedAt.Format(\"2006-01-02\"))\n}\n```\n### 4.2 OpsAgent：运维操作专家\n* **职责**：自动提取信息并执行平台管控操作，比如封禁违规用户。\n* **工具**：`banned_user`（需要提供用户 ID、封禁原因和天数）。\n```go\nfunc NewOpsAgent() AgentConfig {\n    return AgentConfig{\n        Name:         \"SysAdmin\",\n        SystemPrompt: cfg.OpsAgentPrompt,\n        Tools: []*model.Tool{\n            {\n                Type: model.ToolTypeFunction,\n                Function: &model.FunctionDefinition{\n                    Name:        \"banned_user\",\n                    Description: \"封禁违规用户，需要提供用户ID、封禁原因和封禁天数\",\n                    Parameters: map[string]any{\n                        \"type\": \"object\",\n                        \"properties\": map[string]any{\n                            \"user_id\": map[string]any{\"type\": \"string\", \"description\": \"要封禁的用户ID\"},\n                            \"reason\":  map[string]any{\"type\": \"string\", \"description\": \"封禁原因\"},\n                            \"days\":    map[string]any{\"type\": \"integer\", \"description\": \"封禁天数\"},\n                        },\n                        \"required\": []string{\"user_id\", \"reason\", \"days\"},\n                    },\n                },\n            },\n        },\n        ToolMap: map[string]func(string) string{\n            \"banned_user\": func(argsStr string) string {\n                var args struct {\n                    UserID string `json:\"user_id\"`\n                    Reason string `json:\"reason\"`\n                    Days   int    `json:\"days\"`\n                }\n                _ = json.Unmarshal([]byte(argsStr), &args)\n                userID, _ := strconv.ParseUint(args.UserID, 10, 32)\n                return ToolBannedUser(uint(userID), args.Reason, args.Days)\n            },\n        },\n    }\n}\nfunc ToolBannedUser(userID uint, reason string, days int) string {\n    var user models.UserModel\n    if err := global.DB.First(&user, userID).Error; err != nil {\n        return \"封禁操作失败：未找到对应用户\"\n    }\n    if user.IsBanned {\n        return fmt.Sprintf(\"用户 %s 已经被封禁过了\", user.Nickname)\n    }\n    expiry := time.Now().AddDate(0, 0, days)\n    global.DB.Model(&user).Updates(map[string]any{\n        \"is_banned\":  true,\n        \"banned_at\":  time.Now(),\n        \"ban_reason\": reason,\n        \"ban_expiry\": expiry,\n    })\n    return fmt.Sprintf(\"已成功封禁用户 %s，原因：%s，期限：%d天\", user.Nickname, reason, days)\n}\n```\n> [!CAUTION]\n> **安全警示（Human-in-the-Loop）**：涉及到“封禁”、“删除”、“数据导出”或“支付”等高风险操作时，千万不要把最终决定权完全放给 AI。建议在 `ToolMap` 执行前增加一层**人工确认按钮**，由人做最终确认，让 AI 充当“预处理者”或“草拟者”。\n---\n## 五、 设计亮点总结\n1. **System Prompt 配置化**：我们将 Agent 的 SystemPrompt 从代码中剥离，存入配置文件中。无需重新编译部署，即可动态调整 AI 的工作边界和“性格特征”。\n2. **Tool 参数的 JSON Schema 严格定义**：大模型并不懂你的代码签名，它判断参数类型和传递规范的唯一依据就是我们填写的 `JSON Schema`。给每个 Tool 加清晰的 `description` 是高精度调用的基础。\n3. **两轮调用的标准范式**：\n   * 第一轮：将请求传给大模型，模型决策**是否调用工具、调用哪一个、参数是什么**。\n   * 第二轮：系统在本地完成调用，将结果返回给模型，由模型做二次加工后用**自然语言**输出。\n4. **单例客户端优化**：\n   使用 `sync.Once` 确保全局只建立一个大模型 RPC 客户端，避免频繁发起 HTTP 连接请求：\n   ```go\n   var (\n       once   sync.Once\n       client *arkruntime.Client\n   )\n   func NewLLMClient() *arkruntime.Client {\n       once.Do(func() {\n           client = arkruntime.NewClientWithApiKey(cfg.ArkApiKey, ...)\n       })\n       return client\n   }\n   ```\n---\n## 六、 核心安全机制\n大模型虽强，但在生产环境中必须保持绝对的敬畏：\n* **最小权限原则**：Agent 专用的数据库账号应当是严格隔离的。例如：`DataAgent` 只能使用读写分离的**只读库账号**，防止幻觉引发的写脏数据。\n* **完备的操作日志审计**：每一条 Agent 触发的工具调用，都必须在数据库中写下完整的 Audit Log（包括原始提问、大模型决策提取的参数、以及实际执行结果），以便后续追责与逻辑回溯。\n* **请求频率与限制（Rate Limiting）**：防止大模型在 ReAct（思考链）模式下陷入无限循环或遭受外部输入攻击，短时间内对数据库服务造成恶意写冲击。\n---\n## 七、 结语\nFunction Calling 的核心思想其实很简单：**你教 AI 认识你的本地工具，AI 自己决定什么时候使用。**\n而 Go 语言天生适合做这类工程结构性的封装：类型安全、并发结构清晰、且极易融入微服务和 GORM 等企业框架。\n如果你也在用 Go 构建智能后台系统，希望这篇文章对你有所启发！\n","# Transforming LLMs from \"Chatting Tools\" into Real Working \"System Employees\": Go + Function Calling in Practice\n\n## Preface\nRecently, I've been upgrading the backend of my platform **TapeCode** to enable AI to truly participate in platform operations, rather than just acting as a fancy chat box on a webpage.\n\nOrdinary AI conversations can answer questions, but they cannot do the following:\n* \"Go to the database and check what courses this user has purchased.\"\n* \"Ban this user who is farming orders or using abusive language in the comment area for 7 days.\"\n\n**AI Agent + Function Calling** is the golden key to solve this problem.\n\nIn this article, combining real code from my project, I will clearly explain from scratch:\n1. What is Function Calling, and why does it make AI capable of \"doing work\"?\n2. How to abstract a general Go Agent execution engine?\n3. How to implement two practical Agents in a real online education platform.\n\n---\n\n## I. Ordinary LLM Call vs. Agent Call\n\nFirst, clarify a cognitive gap:\n* **Ordinary LLM Call**: You send a sentence, and it replies with a sentence. It belongs to simple Q&A.\n  * **You**: \"Which courses did this user buy?\"\n  * **AI**: \"I don't know. I don't have the permission or capability to access your database.\"\n* **Agent + Function Calling Call**: You tell the LLM “you can use these specific tools.” The LLM will autonomously decide whether to call them, which one to call, and what parameters to pass based on the context. Finally, the system executes the tool and returns the result to the LLM, which then gives the final answer.\n  ```mermaid\n  sequenceDiagram\n      actor User as Operator\u002FAdmin\n      participant System as Go Backend (TapeCode)\n      participant LLM as Doubao LLM (Doubao)\n      User->>System: Check what courses user with ID 42 bought?\n      System->>LLM: Request with available tools [query_user_orders]\n      Note over LLM: Decision: Call query_user_orders\u003Cbr\u002F>Parameters: user_id=42\n      LLM-->>System: Trigger ToolCall\n      System->>System: Query DB: ToolQueryUserOrders(42)\n      System->>LLM: Return tool execution results [Order Data]\n      LLM-->>System: Synthesize results and translate to natural language\n      System-->>User: User 42 bought \"Go Programming Introduction\" and \"Docker in Action\", a total of two courses.\n  ```\n\nThis is what it truly means to integrate AI into a business system.\n\n---\n\n## II. LLM Client Wrapper\n\nThe project uses the **Doubao LLM** under ByteDance, accessed via the Volcano Engine **ARK Runtime Go SDK**.\n\n### 2.1 Basic Conversation (Streaming In, Returning Full Result)\n```go\n\u002F\u002F SendContent sends content to LLM (internally using stream, returning full result)\nfunc SendContent(systemPrompt string, history []Message) (string, error) {\n    client := NewLLMClient()\n    \n    \u002F\u002F Construct message list, including System Prompt and history conversation\n    reqMessages := []*model.ChatCompletionMessage{\n        {\n            Role:    model.ChatMessageRoleSystem,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(systemPrompt)},\n        },\n    }\n    for _, msg := range history {\n        reqMessages = append(reqMessages, &model.ChatCompletionMessage{\n            Role:    msg.Role,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(msg.Content)},\n        })\n    }\n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: reqMessages,\n        Stream:   volcengine.Bool(true),\n    }\n    \n    stream, err := client.CreateChatCompletionStream(context.Background(), req)\n    if err != nil {\n        return \"\", err\n    }\n    defer stream.Close()\n    var fullContent strings.Builder\n    for {\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            return \"\", err\n        }\n        delta := resp.Choices[0].Delta.Content\n        if delta != \"\" {\n            fullContent.WriteString(delta)\n        }\n    }\n    return fullContent.String(), nil\n}\n```\n\n### 2.2 Streaming Push (Used for real-time WebSocket output)\nUsing the callback function `StreamCallback` to push tokens to the frontend in real-time, achieving a typewriter experience similar to ChatGPT.\n```go\ntype StreamCallback func(string)\n\u002F\u002F SendStreamContent triggers a callback every time a delta is received\nfunc SendStreamContent(systemPrompt string, history []Message, onData StreamCallback) (string, error) {\n    client := NewLLMClient()\n    \u002F\u002F ... Construct reqMessages logic same as above ...\n    \n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: reqMessages,\n        Stream:   volcengine.Bool(true),\n    }\n    stream, err := client.CreateChatCompletionStream(context.Background(), req)\n    if err != nil {\n        return \"\", err\n    }\n    defer stream.Close()\n    var fullContent strings.Builder\n    for {\n        resp, err := stream.Recv()\n        if err == io.EOF {\n            break\n        }\n        if err != nil {\n            return \"\", err\n        }\n        delta := resp.Choices[0].Delta.Content\n        if delta != \"\" {\n            fullContent.WriteString(delta)\n            onData(delta) \u002F\u002F Push to frontend channel\u002FWebSocket in real-time\n        }\n    }\n    return fullContent.String(), nil\n}\n```\n\n### 2.3 Conversation with Tools (Agent Core)\n```go\n\u002F\u002F ChatWithTools calls LLM with tool definitions\nfunc ChatWithTools(messages []*model.ChatCompletionMessage, tools []*model.Tool) (*model.ChatCompletionResponse, error) {\n    client := NewLLMClient()\n    req := model.CreateChatCompletionRequest{\n        Model:    modelID,\n        Messages: messages,\n        Tools:    tools, \u002F\u002F Key: Tell LLM the tools list and parameter descriptions\n    }\n    resp, err := client.CreateChatCompletion(context.Background(), req)\n    if err != nil {\n        return nil, err\n    }\n    return &resp, nil\n}\n```\n\n---\n\n## III. General Agent Engine\n\nIn order to reuse Agent logic elegantly and avoid writing a batch of redundant checking code for every function, we abstracted an `AgentConfig` struct and a general `RunAgent` engine:\n```go\n\u002F\u002F AgentConfig defines the configuration and capabilities of a \"Skill Agent\"\ntype AgentConfig struct {\n    Name         string\n    SystemPrompt string\n    Tools        []*model.Tool\n    ToolMap      map[string]func(string) string \u002F\u002F Tool executor: function name -> local execution handler\n}\n```\n\nThe general execution engine `RunAgent` orchestrates the entire two-round call process:\n```go\n\u002F\u002F RunAgent general Agent running engine\nfunc RunAgent(agent AgentConfig, userQuery string) string {\n    \u002F\u002F Step 1: Construct conversation, inject System Prompt and user query\n    messages := []*model.ChatCompletionMessage{\n        {\n            Role:    model.ChatMessageRoleSystem,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(agent.SystemPrompt)},\n        },\n        {\n            Role:    model.ChatMessageRoleUser,\n            Content: &model.ChatCompletionMessageContent{StringValue: volcengine.String(userQuery)},\n        },\n    }\n    \u002F\u002F Step 2: First round call, LLM decides whether to use tools\n    resp, err := ChatWithTools(messages, agent.Tools)\n    if err != nil {\n        return fmt.Sprintf(\"Error in LLM calling: %v\", err)\n    }\n    msg := resp.Choices[0].Message\n    \u002F\u002F Step 3: If no tool call is needed, it means AI can reply directly, return content\n    if len(msg.ToolCalls) == 0 {\n        return *msg.Content.StringValue\n    }\n    \u002F\u002F Step 4: LLM emits ToolCalls intent, Go maps\u002Freflects it locally to perform real action\n    messages = append(messages, &msg)\n    for _, toolCall := range msg.ToolCalls {\n        fname := toolCall.Function.Name\n        args  := toolCall.Function.Arguments\n        var result string\n        if handler, ok := agent.ToolMap[fname]; ok {\n            result = handler(args) \u002F\u002F Call corresponding local business function\n        } else {\n            result = \"Error: Tool not found\"\n        }\n        \u002F\u002F Append the actual execution result of the tool to the context as a \"Tool\" role message\n        messages = append(messages, &model.ChatCompletionMessage{\n            Role:       model.ChatMessageRoleTool,\n            Content:    &model.ChatCompletionMessageContent{StringValue: volcengine.String(result)},\n            ToolCallID: toolCall.ID,\n        })\n    }\n    \u002F\u002F Step 5: Second round call, LLM combines the first round context and the newly obtained tool results to output natural language\n    finalResp, err := ChatWithTools(messages, agent.Tools)\n    if err != nil {\n        return fmt.Sprintf(\"Error in final LLM calling: %v\", err)\n    }\n    return *finalResp.Choices[0].Message.Content.StringValue\n}\n```\n\n---\n\n## IV. Two Real Business Agents\n\nIn the TapeCode platform, two business scenario Agents are currently implemented:\n\n### 4.1 DataAgent: Data Query Expert\n* **Responsibility**: Provides rapid queries of user information and purchase records for the operations team.\n* **Defined Functions\u002FTools**:\n    * `query_user_info`: Query basic user information by ID or nickname.\n    * `query_user_orders`: Query the user's recent paid order records.\n```go\nfunc NewDataAgent() AgentConfig {\n    return AgentConfig{\n        Name:         \"DataStats\",\n        SystemPrompt: cfg.DataAgentPrompt, \u002F\u002F Read from config\n        Tools: []*model.Tool{\n            {\n                Type: model.ToolTypeFunction,\n                Function: &model.FunctionDefinition{\n                    Name:        \"query_user_info\",\n                    Description: \"Query basic user information such as ID, nickname, email, etc. Input can be User ID or Nickname.\",\n                    Parameters: map[string]any{\n                        \"type\": \"object\",\n                        \"properties\": map[string]any{\n                            \"keyword\": map[string]any{\n                                \"type\":        \"string\",\n                                \"description\": \"User ID or Nickname\",\n                            },\n                        },\n                        \"required\": []string{\"keyword\"},\n                    },\n                },\n            },\n        },\n        ToolMap: map[string]func(string) string{\n            \"query_user_info\": func(argsStr string) string {\n                var args struct{ Keyword string `json:\"keyword\"` }\n                _ = json.Unmarshal([]byte(argsStr), &args)\n                return ToolQueryUserInfo(args.Keyword)\n            },\n        },\n    }\n}\n\u002F\u002F Concrete underlying database query function\nfunc ToolQueryUserInfo(keyword string) string {\n    var user models.UserModel\n    if err := global.DB.Preload(\"Roles\").Where(\"id = ? OR nickname = ?\", keyword, keyword).First(&user).Error; err != nil {\n        return \"User information not found\"\n    }\n    return fmt.Sprintf(\"ID: %d\\nNickname: %s\\nEmail: %s\\nRegistered At: %s\",\n        user.ID, user.Nickname, user.Email, user.CreatedAt.Format(\"2006-01-02\"))\n}\n```\n\n### 4.2 OpsAgent: Operations\u002FOps Expert\n* **Responsibility**: Automatically extract information and execute platform control operations, such as banning violating users.\n* **Tool**: `banned_user` (requires user ID, ban reason, and ban days).\n```go\nfunc NewOpsAgent() AgentConfig {\n    return AgentConfig{\n        Name:         \"SysAdmin\",\n        SystemPrompt: cfg.OpsAgentPrompt,\n        Tools: []*model.Tool{\n            {\n                Type: model.ToolTypeFunction,\n                Function: &model.FunctionDefinition{\n                    Name:        \"banned_user\",\n                    Description: \"Ban a violating user, requires User ID, ban reason, and ban days\",\n                    Parameters: map[string]any{\n                        \"type\": \"object\",\n                        \"properties\": map[string]any{\n                            \"user_id\": map[string]any{\"type\": \"string\", \"description\": \"User ID to ban\"},\n                            \"reason\":  map[string]any{\"type\": \"string\", \"description\": \"Ban reason\"},\n                            \"days\":    map[string]any{\"type\": \"integer\", \"description\": \"Ban days\"},\n                        },\n                        \"required\": []string{\"user_id\", \"reason\", \"days\"},\n                    },\n                },\n            },\n        },\n        ToolMap: map[string]func(string) string{\n            \"banned_user\": func(argsStr string) string {\n                var args struct {\n                    UserID string `json:\"user_id\"`\n                    Reason string `json:\"reason\"`\n                    Days   int    `json:\"days\"`\n                }\n                _ = json.Unmarshal([]byte(argsStr), &args)\n                userID, _ := strconv.ParseUint(args.UserID, 10, 32)\n                return ToolBannedUser(uint(userID), args.Reason, args.Days)\n            },\n        },\n    }\n}\nfunc ToolBannedUser(userID uint, reason string, days int) string {\n    var user models.UserModel\n    if err := global.DB.First(&user, userID).Error; err != nil {\n        return \"Ban operation failed: User not found\"\n    }\n    if user.IsBanned {\n        return fmt.Sprintf(\"User %s has already been banned\", user.Nickname)\n    }\n    expiry := time.Now().AddDate(0, 0, days)\n    global.DB.Model(&user).Updates(map[string]any{\n        \"is_banned\":  true,\n        \"banned_at\":  time.Now(),\n        \"ban_reason\": reason,\n        \"ban_expiry\": expiry,\n    })\n    return fmt.Sprintf(\"Successfully banned user %s, Reason: %s, Period: %d days\", user.Nickname, reason, days)\n}\n```\n\n> [!CAUTION]\n> **Safety Warning (Human-in-the-Loop)**: When dealing with high-risk operations such as \"banning\", \"deleting\", \"exporting data\", or \"payment\", never completely hand over the final decision-making power to AI. It is highly recommended to add a **human confirmation button** before `ToolMap` execution, letting humans make the final decision and having AI act as a \"pre-processor\" or \"drafter\".\n\n---\n\n## V. Design Highlights Summary\n1. **System Prompt Configuration**: We decouple the Agent's SystemPrompt from the code and store it in a configuration file. You can dynamically adjust the AI's work boundaries and \"personality traits\" without compiling and redeploying.\n2. **Strict Tool Parameters JSON Schema Definition**: LLMs do not understand your code signatures; the only basis for them to judge parameter types and delivery specifications is the `JSON Schema` we fill in. Adding a clear `description` to each Tool is the foundation for high-precision calls.\n3. **Standard Paradigm of Two-Round Calls**:\n    * First round: Pass the request to LLM, and the model decides **whether to use tools, which one to use, and what parameters to pass**.\n    * Second round: The system executes the call locally, returns the results to the model, and the model post-processes them to output **natural language**.\n4. **LLM Client Singleton Optimization**:\n   Use `sync.Once` to ensure only one LLM RPC client is built globally, avoiding repeated HTTP connection requests under high concurrency:\n   ```go\n   var (\n       once   sync.Once\n       client *arkruntime.Client\n   )\n   func NewLLMClient() *arkruntime.Client {\n       once.Do(func() {\n           client = arkruntime.NewClientWithApiKey(cfg.ArkApiKey, ...)\n       })\n       return client\n   }\n   ```\n\n---\n\n## VI. Core Security Mechanisms\nAlthough LLMs are powerful, we must maintain absolute respect in production environments:\n* **Principle of Least Privilege**: The database account dedicated to the Agent should be strictly isolated. For example: `DataAgent` should only use a **read-only database account** under read-write separation to prevent dirty data caused by hallucinations.\n* **Complete Operation Audit Log**: Every tool call triggered by an Agent must write a complete Audit Log in the database (including original question, parameters extracted by LLM decision, and actual execution results) for subsequent accountability and logical back-tracing.\n* **Rate Limiting**: Prevent the LLM from falling into infinite loops in ReAct (Chain of Thought) mode or being subjected to malicious input attacks, causing malicious write shocks to the database service in a short period of time.\n\n---\n\n## VII. Conclusion\nThe core idea of Function Calling is actually very simple: **You teach the AI to know your local tools, and the AI itself decides when to use them.**\nAnd Go is naturally suitable for this kind of engineering-structural encapsulation: type safety, clear concurrency model, and easy integration into microservices and enterprise frameworks like GORM.\nIf you are also building smart backends with Go, hope this article inspires you!\n","AI",21,"https:\u002F\u002Fblog4-1316398321.cos.ap-nanjing.myqcloud.com\u002Fblog5\u002F20250823013114__【哲风壁纸】清晨-雪山.png",[15,16],"GO","Agent",false,{"id":19,"title":20,"title_en":21},46,"深度实战：基于 Go + Gemma 4 打造自研私有化 AI 通讯中台","In-depth combat: Build a self-developed and privatized AI communication center platform based on Go + Gemma 4",{"id":23,"title":24,"title_en":25},48,"我是如何用 Go 语言造出一个\"虚拟世界\"的——从死锁地狱到事件驱动架构","How I Built a \"Virtual World\" in Go: From Deadlock Hell to Event-Driven Architecture","2026-06-23T13:03:54.047252+08:00"]