大模型本身不具备直接读写数据库的能力,它只会"生成文本"。
要让大模型操作 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 |
分页查询所有用户 | page、pageSize(可选) |
get_user |
按 id 查询单个用户 | id |
create_user |
新增用户 | name、email、age(可选) |
update_user |
按 id 修改用户 | id、name、email、age(可选) |
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",大模型可能:
- 先调用
list_users找到张三的id; - 再用拿到的
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.js 用 mysql2/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,核心就四步:
- 封装 Tool :用
tool()+zod把 SQL 操作变成"名称 + 描述 + Schema + 执行函数"; - 绑定模型 :用
llm.bindTools(tools)把工具注入大模型; - 实现 Agent 循环 :
while (response.tool_calls)反复"问模型 → 执行工具 → 回传结果",直到模型给出最终回答; - 设计提示词:声明能力、禁止编造、补充隐式规则。
最核心的思想是:大模型永远只负责"决策",代码永远负责"执行"。 数据库的安全(参数化、最小权限)、参数的合法性(zod)、循环的正确性(tool_call_id 关联)都在代码这一侧兜底,大模型只是那个聪明的"调度员"。