从零实现一个基于 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"`
}
关键点:Arguments 用 json.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元。
关键收获
-
Tool Use 本质:大模型并不真正“执行”任何操作,它只是输出一个结构化的 JSON,你的程序负责解析并执行。
-
两轮调用模式:第一轮带工具定义,模型返回
tool_calls;第二轮将执行结果作为tool角色消息发回,模型总结输出。 -
调试技巧:打印 Ollama 原始响应,观察
arguments的实际格式,有助于快速定位反序列化问题。 -
数据一致性:表结构、列名、数据值的大小写都会影响模型生成的 SQL,需要在工具描述中明确提示,或通过数据库设计保持一致。
下一步计划
本文实现的是最简版本(一个工具、一轮工具调用)。接下来将:
-
将 SQL 执行器拆分为独立进程,通过 JSON-RPC over stdio 通信(向 MCP 协议靠拢)。
-
实现 ReAct 循环:让模型在遇到错误时能够自我修正,重新生成工具调用。
-
容器化并使用 Kubernetes 部署整个系统。
写在最后:从零开始理解 AI Agent 的内部机制并不难,关键是分步拆解、亲手调试。希望这篇文章能帮助更多像你一样的开发者跨过“只会调 API”的门槛,真正掌握 AI 工程化的底层原理。如果你在实现过程中遇到任何问题,欢迎留言交流。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)