如何让大模型操作 MySQL 数据库?

大模型本身不具备直接读写数据库的能力,它只会"生成文本"。

要让大模型操作 MySQL 数据库,需要借助大模型的 Tool Calling(工具调用) 能力,配合 Agent 循环 来实现。

Agent 循环(Agent Loop) 就是让大模型"反复思考、反复调用工具、直到得出最终答案"的循环机制。它的本质是:大模型不会自己执行工具,它只能"提出调用的请求",所以需要我们的代码代替它执行,再把结果喂回去,循环往复。

核心思路:Tool Calling + Agent 循环

整体的流程如下,为了更好地理解 Tool 调用,还加入了天气查询的例子:

项目结构总览

txt 复制代码
tool-mysql-test/
├── package.json           # 依赖:langchain / mysql2 / express / vue3 / element-plus
├── .env                   # DASHSCOPE_API_KEY(阿里云百炼)+ MySQL 连接配置
├── db.js                  # MySQL 连接池(基于 mysql2/promise)
├── init-db.js             # 初始化 mysql_test 数据库和 users 表
├── server.js              # Express 后端 + Agent 循环(核心)
├── tools/
│   ├── users.tool.js      # MySQL 增删改查 Tool(5 个)
│   └── weather.tool.js    # 天气查询 Tool(1 个)
└── src/                   # 前端(Vue 3 + Element Plus 聊天界面)

数据模型是 users 用户表:

sql 复制代码
CREATE TABLE IF NOT EXISTS users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(50) NOT NULL COMMENT '姓名',
  email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
  age INT DEFAULT 0 COMMENT '年龄',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

第一步:把 MySQL 操作封装成 Tool

什么是 Tool?

Tool(工具)= 名称 + 功能描述 + 入参 Schema + 真正的执行函数 四件套。大模型通过"名称 + 描述"决定何时调用,通过"Schema"生成合法参数,而"执行函数"由我们的代码实现。

LangChain 提供了 tool() 工厂函数,一行代码就能定义工具。下面是 users.tool.js 中的真实案例:

js 复制代码
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { pool } from "../db.js";

/** 1. 查:分页查询所有用户 */
const listUsersTool = tool(
  async ({ page = 1, pageSize = 10 }) => {
    const offset = (page - 1) * pageSize;
    const [rows] = await pool.query(
      "SELECT * FROM users ORDER BY id DESC LIMIT ? OFFSET ?",
      [pageSize, offset]
    );
    const [[{ total }]] = await pool.query(
      "SELECT COUNT(*) AS total FROM users"
    );
    return JSON.stringify({ list: rows, total, page, pageSize });
  },
  {
    name: "list_users",
    description:
      "分页查询所有用户,返回用户列表和总数。可用于查看数据库里有哪些用户。",
    schema: z.object({
      page: z
        .number()
        .int()
        .min(1)
        .optional()
        .describe("页码,从 1 开始,默认 1"),
      pageSize: z
        .number()
        .int()
        .min(1)
        .max(100)
        .optional()
        .describe("每页条数,默认 10"),
    }),
  }
);
  • 第一个参数:真正的执行函数(异步函数),在这里执行 SQL。
  • 第二个参数 :
    • name:工具唯一标识,大模型靠它引用工具。
    • description:工具的功能说明,这是大模型决定"该不该用"的关键依据,描述越清晰,调用越准确
    • schema:用 zod 声明入参结构,大模型会照着它生成合法的 JSON 参数。

增删改查工具集

users.tool.js 一共封装了 5 个工具,覆盖完整的 CRUD:

工具名 作用 入参
list_users 分页查询所有用户 pagepageSize(可选)
get_user 按 id 查询单个用户 id
create_user 新增用户 nameemailage(可选)
update_user 按 id 修改用户 idnameemailage(可选)
delete_user 按 id 删除用户 id

以"新增用户"为例:

js 复制代码
const createUserTool = tool(
  async ({ name, email, age = 0 }) => {
    const [result] = await pool.query(
      "INSERT INTO users (name, email, age) VALUES (?, ?, ?)",
      [name, email, age]
    );
    return JSON.stringify({
      message: "新增成功",
      id: result.insertId,
      name,
      email,
      age,
    });
  },
  {
    name: "create_user",
    description:
      "新增一个用户,需要提供 name(姓名) 和 email(邮箱),age(年龄) 可选。邮箱重复时会报错。",
    schema: z.object({
      name: z.string().describe("用户姓名"),
      email: z.string().email().describe("用户邮箱,必须唯一"),
      age: z.number().int().min(0).optional().describe("用户年龄,默认 0"),
    }),
  }
);

核心要点:返回结果统一用 JSON.stringify 序列化成字符串 。因为 Tool 的返回值最终要作为一条 tool 角色的消息塞回给大模型,字符串格式最通用、最不易出错。

最后统一导出,供后端汇总:

js 复制代码
export const userTools = [
  listUsersTool,
  getUserTool,
  createUserTool,
  updateUserTool,
  deleteUserTool,
];

非数据库能力也能封成 Tool

工具非常灵活,只要是函数就可以封装成 Tool 绑定到大模型。

weather.tool.js 演示了如何把"外部能力"(如天气查询 API)也封装成 Tool,让大模型在同一个会话里既能操作数据库、又能查天气:

js 复制代码
const getWeatherTool = tool(
  async ({ city }) => {
    await new Promise((r) => setTimeout(r, 200)); // 模拟网络延迟
    const data = WEATHER_MOCK[city];
    if (!data) {
      return JSON.stringify({ message: `暂不支持查询 ${city} 的天气` });
    }
    return JSON.stringify({ city, ...data });
  },
  {
    name: "get_weather",
    description: "查询指定城市的当前天气情况,返回天气、温度、风力等信息。",
    schema: z.object({
      city: z.string().describe("城市名称,例如:北京、上海、广州"),
    }),
  }
);

第二步:把 Tool 绑定给大模型

server.js 里先汇总所有工具,再通过 bindTools 把工具绑定给大模型:

js 复制代码
import { ChatOpenAI } from "@langchain/openai";

/* 汇总所有工具,并用 name 建索引,方便 Agent 循环里按名字调用 */
const tools = [...userTools, ...weatherTools];
const toolsMap = Object.fromEntries(tools.map((t) => [t.name, t]));

/* 创建 qwen-plus(通过阿里云百炼兼容模式) */
const llm = new ChatOpenAI({
  model: "qwen-plus",
  apiKey: process.env.DASHSCOPE_API_KEY,
  configuration: {
    baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  },
  temperature: 0,
});

/* 把 Tool 绑定给 LLM ------ 关键一步 */
const llmWithTools = llm.bindTools(tools);
  • bindTools(tools):把工具列表注入请求,告诉大模型"你现在可以使用这些工具"。
  • temperature: 0:工具调用属于确定性任务,温度设为 0 能减少"编造参数"的概率,让大模型更听话、更稳定。
  • 这里通过阿里云百炼的 OpenAI 兼容模式接入 qwen-plus,所以直接用 ChatOpenAI 即可,不需要额外的 SDK。

第三步:实现 Agent 循环

Agent 循环是整套机制的引擎,逻辑非常简单,但非常关键:

js 复制代码
async function runAgent(userMessage) {
  const messages = [
    { role: "system", content: SYSTEM_PROMPT },
    { role: "user", content: userMessage },
  ];

  const steps = []; // 记录工具调用过程,方便前端展示
  let response = await llmWithTools.invoke(messages);

  // 只要 LLM 决定调用工具,就循环执行
  while (response.tool_calls?.length > 0) {
    messages.push(response); // 把 LLM 的回复(含 tool_calls)追加进上下文

    for (const toolCall of response.tool_calls) {
      const name = toolCall.name;
      const args = toolCall.args;
      console.log(`调用工具: ${name}(${JSON.stringify(args)})`);

      let toolResult;
      try {
        toolResult = await toolsMap[name].invoke(args); // 真正执行工具
      } catch (err) {
        toolResult = JSON.stringify({ error: err.message || String(err) });
      }

      steps.push({ name, args, result: toolResult });

      // 把工具结果作为 tool 角色消息返回给 LLM(用 tool_call_id 关联)
      messages.push({
        role: "tool",
        content: toolResult,
        tool_call_id: toolCall.id,
      });
    }

    response = await llmWithTools.invoke(messages); // 带着工具结果再问一次
  }

  return { content: response.content, steps };
}

循环终止条件:当大模型不再返回 tool_calls 时,说明它已经拿到所有工具结果、开始生成最终回答

上下文里的三种角色

整个循环往 messages 里不断追加消息,角色分工如下:

角色 作用
system 告诉大模型"你是谁、有什么工具、怎么用"
user 用户的原话
assistant 大模型的回复;当它决定调工具时,回复里带 tool_calls
tool 工具执行结果,必须通过 tool_call_id 和某次 tool_calls 一一对应

tool_call_id 是协议的关键 :大模型发出一个 tool_calls(带唯一 id),我们执行完工具后,必须把结果挂在同一个 id 上返回,大模型才能正确关联"哪个请求对应哪个结果"。

支持多步工具调用

循环天然支持"链式调用"。比如用户问"把张三的年龄改成 30",大模型可能:

  1. 先调用 list_users 找到张三的 id;
  2. 再用拿到的 id 调用 update_user

每一步的工具结果都会进入下一轮对话,让大模型"越战越勇"。这正是系统提示词里第 4 条规则的作用:

text 复制代码
如果用户没有明确说明 id,但需要修改/删除时,先调用 list_users 或 get_user 找到对应的用户。

系统提示词的设计

系统提示词决定了 Agent 的"行为边界",server.js 中这样设计:

js 复制代码
const SYSTEM_PROMPT = `你是一个智能助手,拥有以下能力:
- 通过 MySQL 工具对 users 用户表进行增删改查(list_users / get_user / create_user / update_user / delete_user)
- 通过 get_weather 工具查询城市天气

使用规则:
1. 用户需要查数据、改数据、新增或删除用户时,调用对应的 MySQL 工具,不要凭空编造数据。
2. 用户查询天气时,调用 get_weather 工具。
3. 工具执行完成后,用自然语言把结果清晰地回复给用户。
4. 如果用户没有明确说明 id,但需要修改/删除时,先调用 list_users 或 get_user 找到对应的用户。
5. 回答尽量简洁、口语化。`;

这里有几条实用的设计经验:

  • 声明能力清单:让大模型知道"我有什么可用",避免它胡编乱造。
  • 禁止编造数据:明确要求查询类问题必须走工具,而不是凭记忆回复,这是防止"幻觉"的关键。
  • 补充隐式规则:例如"没给 id 就先查一下",这类规则能显著提升多步工具调用的成功率。

前端如何展示工具调用过程

server.js 在返回最终回答的同时,把 steps(每一步的工具名、参数、结果)一起返回给前端:

js 复制代码
app.post("/api/chat", async (req, res) => {
  const { message } = req.body;
  const result = await runAgent(message.trim());
  res.json({ code: 0, data: result }); // { content: "最终回答", steps: [...] }
});

前端 App.vue 拿到 steps 后,在回答气泡下方用折叠面板展示"工具调用过程",让用户能看到 Agent 的推理过程,而不是一个黑盒:

html 复制代码
<el-collapse-item :title="`工具调用过程(${m.steps.length} 次)`" name="1">
  <div v-for="(s, j) in m.steps" :key="j" class="tool-step">
    <div class="head">
      <span class="tname">{{ s.name }}</span>
      <span class="args">({{ JSON.stringify(s.args) }})</span>
    </div>
    <div class="result">{{ pretty(s.result) }}</div>
  </div>
</el-collapse-item>

展示工具过程对调试和用户体验都很有价值:出问题时能定位是哪一步参数不对,也让用户明白结果是怎么来的。

关键技术点总结

参数校验(zod Schema)

zod 声明参数后,LangChain 会自动做两层保障:

  • 模型侧:把 Schema 转成 JSON Schema 随请求发给大模型,让大模型生成合法参数;
  • 代码侧:执行工具前校验参数,非法参数会直接报错,不会进入 SQL。

SQL 注入防护(参数化查询)

所有 SQL 一律使用 ? 占位符 + 参数数组,绝不拼接字符串:

js 复制代码
// ✅ 安全:参数化查询,mysql2 会自动转义
await pool.query("DELETE FROM users WHERE id = ?", [id]);

// ❌ 危险:字符串拼接,存在 SQL 注入风险
// await pool.query(`DELETE FROM users WHERE id = ${id}`);

这是让大模型操作数据库时绝对不能省的一步 ------ 因为大模型生成的参数不可信。

数据库连接池

db.jsmysql2/promise 创建连接池,支持 await 写法、自动复用连接、限制并发:

js 复制代码
export const pool = mysql.createPool({
  host: dbConfig.host,
  port: dbConfig.port,
  user: dbConfig.user,
  password: dbConfig.password,
  database: dbConfig.database,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
});
  • waitForConnections 连接用完了,新请求是否排队等待(而不是直接报错)
  • connectionLimit 连接池最多同时建立多少个连接
  • queueLimit 排队的请求数量上限,为 0 不限制排队数量。连接占满时,所有请求都排队,直到有连接释放。适合并发量可控的小型应用。

工具结果必须序列化

工具返回的内容最终要作为 tool 角色消息塞给大模型,统一 JSON.stringify 成字符串,避免对象/类型问题导致大模型解析失败。

效果演示

bash 复制代码
# 1. 安装依赖
npm install

# 2. 配置 .env(填入 DASHSCOPE_API_KEY 和 MySQL 连接信息)

# 3. 初始化数据库
npm run init-db

# 4. 启动后端(端口 3000)
npm run dev

# 5. 另开终端启动前端(端口 5173)
npm run web:dev

浏览器打开 http://localhost:5173

完整源码可查看 github: github.com/Panda-plus5...

总结

让大模型操作 MySQL,核心就四步:

  1. 封装 Tool :用 tool() + zod 把 SQL 操作变成"名称 + 描述 + Schema + 执行函数";
  2. 绑定模型 :用 llm.bindTools(tools) 把工具注入大模型;
  3. 实现 Agent 循环 :while (response.tool_calls) 反复"问模型 → 执行工具 → 回传结果",直到模型给出最终回答;
  4. 设计提示词:声明能力、禁止编造、补充隐式规则。

最核心的思想是:大模型永远只负责"决策",代码永远负责"执行"。 数据库的安全(参数化、最小权限)、参数的合法性(zod)、循环的正确性(tool_call_id 关联)都在代码这一侧兜底,大模型只是那个聪明的"调度员"。

相关推荐
SamChan901 小时前
PDF 翻译服务的全链路可观测性设计:OpenTelemetry + Jaeger + Loki 实战方案
后端·python·pdf·机器翻译
小睿科技1 小时前
建筑AI睿兔大脑 | 工程造价AI化的技术路线:谁在真正解决算量痛点
人工智能
luckystar513~1 小时前
LLM那些事(二):LLM训练三阶段——从会接龙,到会听话,到会思考
人工智能
AI工具测评家1 小时前
论文AI率和重复率双降怎么做?拆解AIGC降重底层技术与实操方法
人工智能·aigc·降重·ai检测·查重·降ai
故七月1 小时前
基于最新Geo数据的测评行业服务商分析——万域智瞰深度洞察
大数据·人工智能
我的xiaodoujiao1 小时前
Django 基础知识详细图文教程 1-Django 介绍
后端·python·学习·测试工具·django
weixin_469163691 小时前
从提示词到驾驭工程:金融 AI 需求工程的三段式演进路径
大数据·人工智能·金融
安之眼Angleyes1 小时前
运动相机的“盲拍”困局与AX10的解法:AR实时预览+AI双引擎,让所见即所得
人工智能·数码相机·ar