手撸一个AI Agent 对话机器人

准备工作

申请大模型API key(如gpt-4o、qwen-max、deepseek-v3)
enter image description here

  • Demo curl:其中$DASHSCOPE_API_KEY就是上面申请的API KEY

    curl -X POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions \
    -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "model": "qwen-max",
        "messages": [
            {
                "role": "system",
                "content": "是一名专业的气象学专家"
            },
            {
                "role": "user", 
                "content": "今天的天气如何?"
            },
    		{
    			"role": "assistant",
    			"content": "",
    			"tool_call_id": "",
    			"tool_calls": [
    				{
    					"id": "call_f1f5e214aff84e16a32bf9",
    					"type": "function",
    					"index": 0,
    					"function": {
    						"name": "get_current_time",
    						"arguments": "{}"
    					}
    				}
    			]
             },
    		 {
    			"role": "tool",
    			"content": "当前时间是:2025-05-16 10:57:45",
    			"tool_call_id": "call_f1f5e214aff84e16a32bf9",
    			"tool_calls": null
             }
        ]
    }'

对话消息类型

  • 系统消息

一般放在调用接口的第一个,模型的目标或角色,是用于大模型输出的提示词
{
	"role": "system",
	"content": "是一名专业的气象学专家"
}
  • 用户消息

用户输入的提问内容
{
	"role": "user",
	"content": "今天的天气如何?"
}
  • 助手消息

大模型回复的消息内容
{
	"role": "assistant",
	"content": "今天的天气如何?"
}
  • 工具消息

工具函数执行的输出结果
{
	"role": "tool",
	"content": "当前时间是:2025-05-16 10:57:45",
	"tool_call_id": "call_f1f5e214aff84e16a32bf9",
}

 

Function calling原理图

用户程序代码工具大模型API定义工具函数代码(出入参、描述等)整合用户文本和工具列表大模型输出整理后的结果大模型输出结果alt[需要调用工具][不需要调用工具]用户输入提问内容1用户消息&工具描述请求大模型API2返回需要调用的工具列表&所需参数3调用工具函数4返回工具函数执行结果值5用户消息&工具执行结果再次请求大模型API6返回大模型输出结果7返回输出内容给用户8用户程序代码工具大模型API

核心逻辑说明

初始化消息

enter image description here

工具函数说明

enter image description here

首次调用大模型

enter image description here

处理大模型返回的工具列表

enter image description here
enter image description here
enter image description here
enter image description here
enter image description here
enter image description here

返回数据

{
    "code": 0,
    "msg": "success",
    "data": {
        "model": "qwen-max",
        "messages": [
            {
                "role": "system",
                "content": "你是著名的气象学专家,请根据用户的问题给出非常专业的回答。",
                "tool_call_id": "",
                "tool_calls": null
            },
            {
                "role": "user",
                "content": "明天天气如何?",
                "tool_call_id": "",
                "tool_calls": null
            },
            {
                "role": "assistant",
                "content": "",
                "tool_call_id": "",
                "tool_calls": [
                    {
                        "id": "call_0ed2ac6a239b4ad98c76b6",
                        "type": "function",
                        "index": 0,
                        "function": {
                            "name": "get_current_time",
                            "arguments": "{}"
                        }
                    }
                ]
            },
            {
                "role": "tool",
                "content": "当前时间是:2025-05-16 20:28:39",
                "tool_call_id": "call_0ed2ac6a239b4ad98c76b6",
                "tool_calls": null
            },
            {
                "role": "assistant",
                "content": "",
                "tool_call_id": "",
                "tool_calls": [
                    {
                        "id": "call_10724f37620842da8a185b",
                        "type": "function",
                        "index": 0,
                        "function": {
                            "name": "get_current_weather",
                            "arguments": "{\"date\": \"2025-05-17\"}"
                        }
                    }
                ]
            },
            {
                "role": "tool",
                "content": "当前2025-05-17天气为晴,温度为28度",
                "tool_call_id": "call_10724f37620842da8a185b",
                "tool_calls": null
            },
            {
                "role": "assistant",
                "content": "明天的天气预报显示天气将为晴朗,温度大约在28摄氏度左右。请根据实际需要做好防晒和保持水分。",
                "tool_call_id": "",
                "tool_calls": null
            }
        ],
        "tools": [
            {
                "type": "function",
                "function": {
                    "name": "get_current_weather",
                    "description": "用于获取某一天的天气",
                    "parameters": {
                        "properties": {
                            "date": {
                                "description": "时间,如2025-01-01",
                                "type": "string"
                            }
                        },
                        "required": [
                            "date"
                        ],
                        "type": "object"
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "get_current_time",
                    "description": "用于获取当前时间",
                    "parameters": {
                        "properties": {},
                        "type": "object"
                    }
                }
            }
        ],
        "stream": false
    }
}

手撸代码

  • main.go

    package main
    
    import (
        "fmt"
        "go_chat_mcp/app/controller/chat"
        "log"
        "net/http"
    )
    
    func main() {
        http.HandleFunc("/chat/send", McpSend)
        fmt.Println("服务器启动在 http://localhost:8080")
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    func McpSend(w http.ResponseWriter, r *http.Request) {
        send := chat.NewSend()
        send.Send(w, r)
    }

    enter image description here

  • send.go

    package chat
    
    import (
        "encoding/json"
        "fmt"
        "go_chat_mcp/app/common/proto"
        "go_chat_mcp/app/model"
        "io"
        "net/http"
    )
    
    type Send struct {
    }
    
    func NewSend() *Send {
        return &Send{}
    }
    
    func (s *Send) Send(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    
        var sendRequest proto.SendRequest
        err = json.Unmarshal(body, &sendRequest)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    
        if sendRequest.MsgId == "" || sendRequest.Msg == "" {
            http.Error(w, "msg_id or msg is empty", http.StatusBadRequest)
            return
        }
        sendResp, err := model.NewModel(sendRequest).Send()
        if err != nil {
            fmt.Println(err.Error())
            return
        }
    
        resp := proto.Resp{
            Code:    0,
            Message: "success",
            Data:    sendResp,
        }
        respBytes, err := json.Marshal(resp)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        w.WriteHeader(http.StatusOK)
        w.Write(respBytes)
    }

    enter image description here

  • model.go

    package model
    
    import (
        "go_chat_mcp/app/common/proto"
        gpt4o "go_chat_mcp/app/gpt-4o"
        "go_chat_mcp/app/qwen"
    )
    
    type Model interface {
        Send() (resp proto.ModelMsgs, err error)
    }
    
    func NewModel(param proto.SendRequest) Model {
        switch param.Model {
        case "gpt-4o":
            return gpt4o.NewGpt4o(param)
        case "qwen3-235b-a22b":
            return qwen.NewQwen(param)
        case "qwen-max":
            return qwen.NewQwen(param)
        }
        return nil
    }

    enter image description here

  • qwen.go

    package qwen
    
    import (
        "encoding/json"
        "fmt"
        "go_chat_mcp/app/common/proto"
        "sync"
    )
    
    type Qwen struct {
        Msgs  proto.QwenMsgs
        MsgId string
    }
    
    const (
        QwenUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"
    )
    
    var APIKEYs = []string{
        "sk-4cd2daxxxxxxxxxxxxxaxxxxxxc1d42fdc",
    }
    
    var qwenMap map[string]proto.QwenMsgs
    
    var once sync.Once
    
    func init() {
        once.Do(func() {
            qwenMap = make(map[string]proto.QwenMsgs)
        })
    }
    
    func NewQwen(param proto.SendRequest) *Qwen {
        msgId := param.MsgId
        qwen := &Qwen{MsgId: msgId}
        userMsg := proto.Messages{
            Role:    "user",
            Content: param.Msg,
        }
        if qwenMsgs, ok := qwenMap[msgId]; ok {
            qwen.Msgs = qwenMsgs
            qwen.Msgs.Messages = append(qwen.Msgs.Messages, userMsg)
        } else { // 初始化会话
            qwen.Msgs = proto.QwenMsgs{
                Model:          param.Model,
                Messages:       append(SystemMsg, userMsg),
                Tools:          qwenTools,
                Stream:         false,
                EnableThinking: false,
            }
        }
    
        qwenMap[msgId] = qwen.Msgs
        return qwen
    }
    
    func (d *Qwen) Send() (resp proto.ModelMsgs, err error) {
        //记录会话
        /**************调用qwen3 API******************/
        send1, err := d.PushToQwen()
        if err != nil {
            fmt.Println("err 1111", err)
            return
        }
    
        if len(send1.Choices) == 0 {
            fmt.Println("err 2222", err)
            return
        }
        d.AddAssistantMessage(send1.Choices[0].Message.Content, send1.Choices[0].Message.Role, send1.Choices[0].Message.ToolCalls)
        // 处理工具调用
        toolCalls1 := send1.Choices[0].Message.ToolCalls
        fmt.Println("toolCalls", toolCalls1)
        if len(toolCalls1) > 0 {
            for _, v := range toolCalls1 {
                d.AddToolMessage(v)
            }
            send2 := proto.SendResponse{}
            send2, err = d.PushToQwen()
            if err != nil {
                fmt.Println("err 3333", err)
            }
    
            if len(send2.Choices) == 0 {
                fmt.Println("err 4444", err)
                return
            }
            d.AddAssistantMessage(send2.Choices[0].Message.Content, send2.Choices[0].Message.Role, send2.Choices[0].Message.ToolCalls)
    
            toolCalls2 := send2.Choices[0].Message.ToolCalls
            if len(toolCalls2) > 0 {
                for _, v := range toolCalls2 {
                    d.AddToolMessage(v)
                }
            }
            fmt.Println("d.Msgs 88888888", d.GetMsgJson())
            //再次调用大模型 API
            send3 := proto.SendResponse{}
            send3, err = d.PushToQwen()
            if err != nil {
                fmt.Println("err 4444", err)
                return
            }
    
            if len(send3.Choices) == 0 {
                fmt.Println("err 4444", err)
                return
            }
            fmt.Println("d.Msgs 999999999", d.GetMsgJson())
            d.AddAssistantMessage(send3.Choices[0].Message.Content, send3.Choices[0].Message.Role, send3.Choices[0].Message.ToolCalls)
        }
    
        fmt.Println("d.Msgs 1000000000", d.GetMsgJson())
        modelMsgs := proto.ModelMsgs{
            Model:    d.Msgs.Model,
            Messages: d.Msgs.Messages,
            Tools:    d.Msgs.Tools,
            Stream:   d.Msgs.Stream,
        }
        return modelMsgs, nil
    }
    
    func (d *Qwen) PushToQwen() (resp proto.SendResponse, err error) {
        var respBody []byte
    
        //再次调用qwen3 API
        respBody, err = d.HttpQwen(d.Msgs)
        if err != nil {
            return resp, err
        }
        fmt.Println("respBody 1111", string(respBody))
        var sendResponse proto.SendResponse
        err = json.Unmarshal(respBody, &sendResponse)
        if err != nil {
            return sendResponse, err
        }
    
        return sendResponse, nil
    }
    
    func (d *Qwen) AddToolMessage(toolCall proto.ToolCall) {
        toolResult := ""
        if tool, ok := ToolsMap[toolCall.Function.Name]; ok {
            toolResult = tool(toolCall.Function.Arguments)
        }
        // 将工具返回结果添加到会话中
        d.Msgs.Messages = append(d.Msgs.Messages, proto.Messages{
            Role:       "tool",
            Content:    toolResult,
            ToolCallId: toolCall.Id,
        })
    
        // 更新会话
        qwenMap[d.MsgId] = d.Msgs
    }
    
    func (d *Qwen) AddSystemMessage(msg, role string) {
        // 将工具返回结果添加到会话中
        d.Msgs.Messages = append(d.Msgs.Messages, proto.Messages{
            Role:    role,
            Content: msg,
        })
    
        // 更新会话
        qwenMap[d.MsgId] = d.Msgs
    }
    
    func (d *Qwen) AddAssistantMessage(msg, role string, toolCall []proto.ToolCall) {
        // 将工具返回结果添加到会话中
        d.Msgs.Messages = append(d.Msgs.Messages, proto.Messages{
            Role:      role,
            Content:   msg,
            ToolCalls: toolCall,
        })
    
        // 更新会话
        qwenMap[d.MsgId] = d.Msgs
    }
    
    func (d *Qwen) GetMsgJson() string {
        msgBytes, _ := json.Marshal(d.Msgs)
        return string(msgBytes)
    }

    enter image description here

  • common.go

    package qwen
    
    import (
        "encoding/json"
        "fmt"
        "go_chat_mcp/app/common/proto"
        "io"
        "net/http"
        "strings"
        "time"
    )
    
    func (d *Qwen) HttpQwen(qwenMsgs proto.QwenMsgs) (respBody []byte, err error) {
        // 确保非流式调用时EnableThinking为false
        if !qwenMsgs.Stream {
            // qwenMsgs.EnableThinking = false
        }
    
        body, err := json.Marshal(qwenMsgs)
        if err != nil {
            fmt.Println(err)
            return
        }
    
        httpReq, err := http.NewRequest("POST", QwenUrl, strings.NewReader(string(body)))
        if err != nil {
            fmt.Println(err)
            return
        }
    
        timeUnix := time.Now().Unix()
        // 轮询APIKEY 防止被封号 或被限流
        APIKey := APIKEYs[timeUnix%int64(len(APIKEYs))]
    
        // 设置请求头
        httpReq.Header.Add("Content-Type", "application/json")
        httpReq.Header.Add("Authorization", "Bearer "+APIKey)
    
        client := &http.Client{}
        httpResp, err := client.Do(httpReq)
        if err != nil {
            return nil, err
        }
        defer httpResp.Body.Close()
    
        respBody, err = io.ReadAll(httpResp.Body)
        if err != nil {
            return nil, err
        }
        return respBody, nil
    }

    enter image description here

  • system.go

    package qwen
    
    import "go_chat_mcp/app/common/proto"
    
    var SystemMsg = []proto.Messages{
        {
            Role:    "system",
            Content: "你是著名的气象学专家,请根据用户的问题给出非常专业的回答。",
        },
    }

    enter image description here

  • tools.go

    package qwen
    
    import (
        "encoding/json"
        "fmt"
        "go_chat_mcp/app/common/proto"
        "time"
    )
    
    var qwenTools = []proto.Tool{
        {
            Type: "function",
            Function: proto.ToolFunction{
                Name:        GetCurrentWeatherToolName,
                Description: "用于获取某一天的天气",
                Parameters: proto.ToolParameters{
                    "type": "object",
                    "properties": map[string]interface{}{
                        "date": map[string]interface{}{
                            "type":        "string",
                            "description": "时间,如2025-01-01",
                        },
                    },
                    "required": []string{"date"},
                },
            },
        },
        {
            Type: "function",
            Function: proto.ToolFunction{
                Name:        GetCurrentTimeToolName,
                Description: "用于获取当前时间",
                Parameters: proto.ToolParameters{
                    "type":       "object",
                    "properties": map[string]interface{}{},
                },
            },
        },
    }
    
    const (
        GetCurrentWeatherToolName = "get_current_weather"
        GetCurrentTimeToolName    = "get_current_time"
    )
    
    var ToolsMap = map[string]func(paramsJson string) string{
        GetCurrentWeatherToolName: GetCurrentWeather,
        GetCurrentTimeToolName:    GetCurrentTime,
    }
    
    type GetCurrentWeatherParams struct {
        Date string `json:"date"`
    }
    
    func GetCurrentWeather(paramsJson string) string {
        var params GetCurrentWeatherParams
        err := json.Unmarshal([]byte(paramsJson), &params)
        if err != nil {
            return "参数错误"
        }
        return fmt.Sprintf("当前%s天气为晴,温度为28度", params.Date)
    }
    
    func GetCurrentTime(paramsJson string) string {
        return "当前时间是:" + time.Now().Format("2006-01-02 15:04:05")
    }

    enter image description here

作者:admin  创建时间:2025-05-10 22:32
最后编辑:admin  更新时间:2025-05-22 10:29
上一篇:
下一篇: