前言
最近在给自己的平台 TapeCode 做后台升级,想让 AI 真正参与到平台运营里来,而不仅仅是一个漂亮的网页聊天框。
普通的 AI 对话能回答问题,但它做不到:
- “去数据库里查一下这个用户买了什么课。”
- “把这个在评论区刷单或辱骂的用户封禁 7 天。”
AI Agent + Function Calling 就是解决这个问题的金钥匙。
这篇文章,我会结合自己项目里的真实代码,从头讲清楚:
- 什么是 Function Calling,为什么它让 AI 能“干活”?
- 如何抽象出一个通用的 Go 语言 Agent 运行引擎?
- 如何在一个真实的在线教育平台里落地两种实际 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 充当“预处理者”或“草拟者”。
五、 设计亮点总结
- System Prompt 配置化:我们将 Agent 的 SystemPrompt 从代码中剥离,存入配置文件中。无需重新编译部署,即可动态调整 AI 的工作边界和“性格特征”。
- Tool 参数的 JSON Schema 严格定义:大模型并不懂你的代码签名,它判断参数类型和传递规范的唯一依据就是我们填写的
JSON Schema。给每个 Tool 加清晰的description是高精度调用的基础。 - 两轮调用的标准范式:
- 第一轮:将请求传给大模型,模型决策是否调用工具、调用哪一个、参数是什么。
- 第二轮:系统在本地完成调用,将结果返回给模型,由模型做二次加工后用自然语言输出。
- 单例客户端优化:
使用sync.Once确保全局只建立一个大模型 RPC 客户端,避免频繁发起 HTTP 连接请求:govar ( 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 构建智能后台系统,希望这篇文章对你有所启发!