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 特定实现。

相关推荐
我不会插花弄玉3 小时前
7.函数【由浅入深-MySQL】
android·数据库·mysql
周小董3 小时前
[1369]Windows上安装PostgreSQL
数据库·windows·postgresql
VIP_CQCRE5 小时前
给团队搭一个 AI 工作台:Open WebUI 接入 Ace Data Cloud 实战
docker·ai·rag·open webui·ace data cloud
Smoothcloud润云5 小时前
PyTorch推理服务动态批处理实战:兼顾吞吐与延迟
人工智能·pytorch·python·深度学习·ai·gpu算力
网络毒刘5 小时前
国庆后开工第一周:用一张 Cursor 能力地图安排学习路径(Rules→MCP→工作流)
cursor·工作流·rules·mcp·学习路径
一只积极向上的小咸鱼6 小时前
第一性原理思考:从拆解问题到用实验检验答案
数据库
这个DBA有点耶6 小时前
时序数据库InfluxDB高基数问题深度拆解:内存索引膨胀机制与OOM完整链路
数据库·架构·dba
加油yx6 小时前
MySQL数据类型
数据库·mysql
桃西西呀6 小时前
LangGraph4j之一:客服请求分诊既要循环又要分流,我用 LangGraph4j 把控制流抽成了数据
人工智能·llm·ai编程