MCP 查数实战:让 LLM 安全地查数据库(只读账号、SQL 校验、参数漂移)

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 月,对账照样通过。格式、范围、意图理解、参数一致性各管一段,对账逻辑只管最后一段。

在意准确性的场景,查询前把条件摘要展示给用户确认,确认后应用保存这份条件,再去对工具参数。这比把所有希望压在一句提示词上更容易检查。

八、踩坑

  1. 提示词里的「严格使用这个日期」不会变成代码判断,对账要写代码
  2. 对账的意图让模型两边都填,检查失去意义,意图必须来自应用状态
  3. 共享可变的 currentMonth 并发时串请求,每次请求建自己的实例
  4. 包装完留了直通入口,检查等于白加,接线要验证
  5. 查出来为空和条件写错是两回事,别混

总结

环节 一句话
参数漂移 意图和工具参数是两份独立产物,中间隔着一次模型调用
三道检查 格式、范围、意图一致性,缺第三道照样查错月
对账逻辑 意图来自应用状态,不让模型自证
数据库限制 只读账号、白名单、行数上限
验证 主动构造错误参数试边界,别等事故

结合系列,Function Calling 管工具调用,MCP 管协议,这篇把查数的限制补在数据库前面。安全篇的只读账号和可观测性篇的失败日志是它的两道后盾。觉得有用点个关注。

来源:日期漂移案例与对账思路参考自 CSDN 实战文章《Spring AI + MCP 查数实战》(changhaike,blog.csdn.net/changhaike/article/details/167384338),本文写通用工程版,未搬 Spring AI 特定实现。

相关推荐
weixin_468466854 小时前
从“看懂”到“做到”:视觉-语言-动作模型如何让机器人真正执行指令
人工智能·ai·机器人·大语言模型·前沿科技
captain3764 小时前
MyBatis(入门5)其他查询操作
java·数据库·spring boot·mybatis
只睡四小时5 小时前
MCP 弃用 SSE?Node 分帧漏 81.7% 事件
node.js·webassembly·实时通信·mcp·streamable http
小此方5 小时前
Re:Mysql数据库基础篇(八):详细讲解复合查询、多表查询、自连接、多行、多列子查询与合并查询
数据库·mysql
xszivvvv5 小时前
Mariadb数据库frm文件异常,如何恢复数据
数据库·mariadb
Eloudy5 小时前
sgl-kernel 项目的演进:不同时间点的角色与去向
llm·sglang
smile_life_6 小时前
MySQL 设置 防止账户被暴力破解
数据库·mysql
ShineWinsu6 小时前
对于Qt:QRadioButton常见属性以及QButtonGroup的解析
数据库·c++·qt·microsoft·gui·qradiobutton·qbuttongroup
测试盐6 小时前
oracle数据库卡顿排查
数据库·oracle
滕州市燕猫虎计算机科技工作室个体工商户6 小时前
MySQL、MVCC、多版本并发控制
数据库·mysql·mvcc·数据库事务