MCP 查数实战:让 LLM 安全地查数据库(只读账号、SQL 校验、参数漂移)
LLM 系列又一篇。Function Calling 篇讲了工具调用,MCP 篇讲了协议,安全篇讲了密钥。这篇补上模型查数据库的实战。参数会漂移,账号要限权,限制要放在数据库前面。
一、日期校验通过了,查出来的却是别的月
一篇公开的实战文章记录过这个案例,Java 技术栈,Spring AI 接 MCP,本地教学库。用户要查 2026 年 8 月每天的支付金额。应用校验了意图里的日期,格式合法,范围是完整自然月,都通过。工具最后执行的查询是 9 月 1 日到 10 月 1 日。
9 月也是完整自然月,开始日期合法,结束日期合法,范围合法,检查全过,查的月份换掉了。教学库里 9 月 1 日刚好有一笔 500 元的已支付订单,查回来 500 元。这个数确实来自数据库,用户要的是 8 月,交上来的是 9 月的账。
这个坑是作者主动构造错误参数试出来的,没等模型偶尔出错。验证比等事故便宜,批量篇讲过同一件事,先 20 条过三关再放大。
二、参数为什么会漂移
把调用链摊开。模型整理查询意图,应用校验意图里的日期,把日期写进提示词,模型再生成工具调用参数,MCP 工具执行查询。
意图和工具参数中间隔着一次模型调用。意图解析出来的日期存在应用状态里,模型后来生成的参数是一份全新的 JSON,两份之间没有对账。意图是 8 月 1 日到 9 月 1 日,参数可以变成 9 月 1 日到 10 月 1 日,没有代码拦它。
提示词里写「请严格使用这个日期」有帮助,这句话不会自动变成一段判断。对账要写代码。
三、三种检查分开管
格式检查管「能不能解析成日期」,范围检查管「是不是完整自然月」,两道都过了,9 月 1 日到 10 月 1 日照样合法。第三道管「这个范围是不是用户这次要查的」,缺了它,用户要 8 月、查回来 9 月的事故照样发生。
四、对账逻辑怎么写
在真正调用查询工具之前加一道检查,参数里的范围和本次意图对不上就拒绝。关键在用来对账的那份意图必须来自应用保存的状态。模型两边都填 9 月,比较当然通过,检查就白做了。
参考文章是 Java 和 Spring AI 的,这里写通用版。
python
def checked_tool(delegate, expected_range: tuple):
"""包装原工具,对账的意图来自应用状态,不来自模型输出"""
def call(args: dict) -> str:
actual = (args["start_date"], args["end_date"])
if actual != expected_range:
raise ValueError("查询范围与本次意图不一致,未执行查询")
return delegate(args)
return call
几个细节。每次请求建自己的包装实例,月份存进闭包,别放共享可变状态。两个用户并发,一个查 8 月一个查 9 月,共享字段会串。只包装查数工具,找表、看字段的其他工具照常走。包装完确认接线生效,原来的直通入口留着,检查等于白加。
五、数据库侧的限制
参数校验管得住模型,管不住代码 bug,数据库前面再放三道。
账号用只读的,专门给 AI 应用建一个用户,只授 SELECT,不给 UPDATE 和 DELETE。表加白名单,工具校验表名在允许列表里。查询加行数上限和超时,超出的直接拒绝,防止一次全表扫描把库拖垮。
python
ALLOWED_TABLES = {"demo_orders", "demo_payments"}
def validate_query(table: str, where: str) -> None:
if table not in ALLOWED_TABLES:
raise ValueError(f"表 {table} 不在允许列表")
for bad in (";", "--", "drop", "delete", "update", "insert"):
if bad in where.lower():
raise ValueError("条件里带非查询关键字")
六、怎么验证真的拦住了
别问模型一次,看到正常答案就宣布修好。直接构造参数试边界。
| 本次意图 | 工具参数 | 结果 |
|---|---|---|
| 8 月 | 8 月 1 日到 9 月 1 日 | 返回 8 月数据 |
| 8 月 | 9 月 1 日到 10 月 1 日 | 拒绝,原工具未执行 |
| 8 月 | 8 月 15 日到 8 月 31 日 | 拒绝,不是完整自然月 |
| 6 月 | 6 月 1 日到 7 月 1 日 | 正常执行,返回空 |
最后一行也是正常情况,查对月份但库里没记录,别把「查出来为空」和「条件写错」混为一谈。
七、边界要说清
这道检查守住的是应用保存的意图和实际发出的参数一致。意图解析本身就错呢,用户说查 8 月,模型理解成 9 月,应用保存的就是 9 月,对账照样通过。格式、范围、意图理解、参数一致性各管一段,对账逻辑只管最后一段。
在意准确性的场景,查询前把条件摘要展示给用户确认,确认后应用保存这份条件,再去对工具参数。这比把所有希望压在一句提示词上更容易检查。
八、踩坑
- 提示词里的「严格使用这个日期」不会变成代码判断,对账要写代码
- 对账的意图让模型两边都填,检查失去意义,意图必须来自应用状态
- 共享可变的 currentMonth 并发时串请求,每次请求建自己的实例
- 包装完留了直通入口,检查等于白加,接线要验证
- 查出来为空和条件写错是两回事,别混
总结
| 环节 | 一句话 |
|---|---|
| 参数漂移 | 意图和工具参数是两份独立产物,中间隔着一次模型调用 |
| 三道检查 | 格式、范围、意图一致性,缺第三道照样查错月 |
| 对账逻辑 | 意图来自应用状态,不让模型自证 |
| 数据库限制 | 只读账号、白名单、行数上限 |
| 验证 | 主动构造错误参数试边界,别等事故 |
结合系列,Function Calling 管工具调用,MCP 管协议,这篇把查数的限制补在数据库前面。安全篇的只读账号和可观测性篇的失败日志是它的两道后盾。觉得有用点个关注。
来源:日期漂移案例与对账思路参考自 CSDN 实战文章《Spring AI + MCP 查数实战》(changhaike,blog.csdn.net/changhaike/article/details/167384338),本文写通用工程版,未搬 Spring AI 特定实现。