从零实现一个基于 Ollama + Go + MySQL 的 Text-to-SQL 智能体(M1 实战)

前言

最近在搭建一个私有化的 AI Agent,目标是让大模型能安全地查询企业内部数据库。作为一个运维出身、正在学习 Go 和云原生的人,我不想只当"调包侠",而是想亲手打通从"自然语言 → 工具调用 → 数据库查询 → 自然语言回答"的全链路。

今天这篇文章记录了 M1 里程碑 的完整实现过程:在一台 Linux 虚拟机上,用 Go 语言调用 Ollama(Qwen2.5 7B 模型),结合 MySQL,实现一个能够回答"销售部平均工资是多少?"的 Text-to-SQL 智能体

整个过程不依赖 K8s、不涉及复杂的 MCP 协议,只聚焦于最核心的 Tool Use(函数调用) 机制。读完你将明白:

  • 大模型如何通过 tool_calls 输出结构化指令

  • Go 程序如何解析并执行这些指令

  • 如何将查询结果再次交给模型总结成人话

环境准备

硬件/软件环境

  • 一台 Linux 虚拟机(Ubuntu 22.04 / CentOS 9)

  • Docker 及 Docker Compose(可选)

  • Go 1.21+

  • 足够的内存(至少 8GB,用于运行 7B 模型)

安装 Docker 并运行 Ollama

bash

复制代码
# 安装 Docker
curl -fsSL https://get.docker.com | bash
sudo systemctl enable --now docker

# 运行 Ollama 容器,暴露 API 端口 11434
docker run -d --name ollama -p 11434:11434 ollama/ollama

# 拉取 qwen2.5:7b 模型(约 4GB,耐心等待)
docker exec -it ollama ollama pull qwen2.5:7b

验证模型是否可用:

bash

复制代码
curl http://localhost:11434/api/tags

启动测试 MySQL

bash

复制代码
docker run --name mysql-test -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -d mysql:8.0

# 创建数据库和表
docker exec -i mysql-test mysql -uroot -p123456 <<EOF
CREATE DATABASE company;
USE company;
CREATE TABLE employees (
    id INT,
    name VARCHAR(50),
    dept VARCHAR(50),
    salary INT
);
INSERT INTO employees VALUES (1, '张三', 'sales', 70000);
INSERT INTO employees VALUES (2, '李四', 'sales', 80000);
INSERT INTO employees VALUES (3, '王五', 'engineering', 100000);
EOF

注意:部门名我用小写 sales,与大模型生成 SQL 的习惯一致,避免后续大小写问题。

核心代码实现

创建一个目录 ~/mcp-demo,新建 main.go。下面分块解释。

1. 定义数据结构(与 Ollama API 通信)

go

复制代码
type Message struct {
    Role      string     `json:"role"`
    Content   string     `json:"content,omitempty"`
    ToolCalls []ToolCall `json:"tool_calls,omitempty"`
}

type ToolCall struct {
    Function struct {
        Name      string          `json:"name"`
        Arguments json.RawMessage `json:"arguments"` // 注意:这里是 RawMessage
    } `json:"function"`
}

type Tool struct {
    Type     string   `json:"type"`
    Function Function `json:"function"`
}

type Function struct {
    Name        string                 `json:"name"`
    Description string                 `json:"description"`
    Parameters  map[string]interface{} `json:"parameters"`
}

关键点:Argumentsjson.RawMessage,因为模型返回的是一个 JSON 对象(例如 {"sql": "SELECT ..."}),而不是字符串。

2. 调用 Ollama 的函数

go

复制代码
func callOllama(messages []Message, tools []Tool) (OllamaResponse, error) {
    reqBody := OllamaRequest{
        Model:    "qwen2.5:7b",
        Messages: messages,
        Tools:    tools,
        Stream:   false,
    }
    jsonData, _ := json.Marshal(reqBody)
    resp, err := http.Post("http://localhost:11434/api/chat", "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return OllamaResponse{}, err
    }
    defer resp.Body.Close()
    bodyBytes, _ := io.ReadAll(resp.Body)
    var result OllamaResponse
    json.Unmarshal(bodyBytes, &result)
    return result, nil
}

3. 执行 SQL 查询

go

复制代码
func executeSQL(sqlStr string) string {
    db, _ := sql.Open("mysql", "root:123456@tcp(localhost:3306)/company")
    defer db.Close()
    rows, err := db.Query(sqlStr)
    if err != nil {
        return fmt.Sprintf("Query error: %v", err)
    }
    defer rows.Close()
    // 将结果转换为 JSON 字符串
    // ... (完整代码见文末)
    return string(jsonBytes)
}

4. 主流程:两轮调用

go

复制代码
func main() {
    // 定义工具
    tools := []Tool{
        {
            Type: "function",
            Function: Function{
                Name:        "query_sql",
                Description: "Execute SELECT SQL. Table employees has columns: id, name, dept, salary.",
                Parameters: map[string]interface{}{
                    "type": "object",
                    "properties": map[string]interface{}{
                        "sql": map[string]string{"type": "string"},
                    },
                    "required": []string{"sql"},
                },
            },
        },
    }

    userQuestion := "销售部的平均工资是多少?"
    messages := []Message{{Role: "user", Content: userQuestion}}

    // 第一轮:带 tools 调用,期望模型返回 tool_calls
    resp, _ := callOllama(messages, tools)
    tc := resp.Message.ToolCalls[0]

    var args struct{ SQL string `json:"sql"` }
    json.Unmarshal(tc.Function.Arguments, &args)
    fmt.Println("执行 SQL:", args.SQL)
    resultJSON := executeSQL(args.SQL)

    // 第二轮:将工具结果发送给模型,不再提供 tools
    secondMessages := []Message{
        {Role: "user", Content: userQuestion},
        resp.Message, // assistant 的 tool_calls 消息
        {Role: "tool", Content: resultJSON},
    }
    finalResp, _ := callOllama(secondMessages, nil)
    fmt.Println("最终回答:", finalResp.Message.Content)
}

运行与调试

第一次运行遇到的问题

  • 报错 Query was empty :因为模型返回的 arguments 是对象,但代码中定义为 string,导致解析后 sql 字段为空。

  • 修复 :将 Arguments 类型改为 json.RawMessage

  • 第二次报错 Unknown column 'dept' :因为数据库列名是 department,而模型生成的是 dept

  • 修复 :统一列名为 dept,并将部门值改为小写。

最终成功输出

text

复制代码
📤 请求 Ollama: {...}
📡 Ollama 原始响应: {"message":{"tool_calls":[{"function":{"name":"query_sql","arguments":{"sql":"SELECT AVG(salary) FROM employees WHERE dept = 'sales'"}}}]}}
🔧 执行 SQL: SELECT AVG(salary) FROM employees WHERE dept = 'sales'
📊 查询结果: [{"avg_salary":"75000.0000"}]
🤖 最终回答: 销售部的平均工资是75,000元。

关键收获

  1. Tool Use 本质:大模型并不真正"执行"任何操作,它只是输出一个结构化的 JSON,你的程序负责解析并执行。

  2. 两轮调用模式 :第一轮带工具定义,模型返回 tool_calls;第二轮将执行结果作为 tool 角色消息发回,模型总结输出。

  3. 调试技巧 :打印 Ollama 原始响应,观察 arguments 的实际格式,有助于快速定位反序列化问题。

  4. 数据一致性:表结构、列名、数据值的大小写都会影响模型生成的 SQL,需要在工具描述中明确提示,或通过数据库设计保持一致。

下一步计划

本文实现的是最简版本(一个工具、一轮工具调用)。接下来将:

  • 将 SQL 执行器拆分为独立进程,通过 JSON-RPC over stdio 通信(向 MCP 协议靠拢)。

  • 实现 ReAct 循环:让模型在遇到错误时能够自我修正,重新生成工具调用。

  • 容器化并使用 Kubernetes 部署整个系统。


写在最后:从零开始理解 AI Agent 的内部机制并不难,关键是分步拆解、亲手调试。希望这篇文章能帮助更多像你一样的开发者跨过"只会调 API"的门槛,真正掌握 AI 工程化的底层原理。如果你在实现过程中遇到任何问题,欢迎留言交流。

相关推荐
xurime2 小时前
Excelize 开源十周年,发布 2.11.0 版本
golang·开源·github·excel·导出·导入·excelize·基础库
万亿少女的梦1683 小时前
基于Python的高考志愿填报辅助系统设计与实现
java·spring boot·python·mysql·vue
万亿少女的梦1683 小时前
基于SpringBoot与Vue的历史博物馆展品展示与游客管理系统设计
spring boot·mysql·vue·系统设计·博物馆管理系统
衍生星球4 小时前
SpringBoot3 + Vue3 + 微信小程序如何学习,以及学哪些技术,组件
sql·微信小程序·uni-app·vue·springboot
灯澜忆梦5 小时前
MySQL 完整学习路线
学习·mysql
阿里云云原生6 小时前
不改代码也能监控 AI Agent?揭秘 OBI 如何在内核层精准解析 GenAI 语义流量
云原生·agent
ClouGence6 小时前
保障公共安全,一线治安机构实时数据应用与实践
数据库·mysql·postgresql
Minxinbb6 小时前
TDSQL for MySQL 服务器重启操作
数据库·mysql·dba
PawSQL6 小时前
PawSQL 技术月报 | 2026年6月
数据库·sql·tdsql·pawsql·sql审核
飞翔沫沫情7 小时前
K8s Alloy 采集 Pod 控制台日志
云原生·容器·kubernetes