ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent 接数据库的正确姿势:连接池、Text2SQL 校验与生产避坑指南

Agent 接数据库的正确姿势:连接池、Text2SQL 校验与生产避坑指南 我见过太多团队在 Agent 接数据库这一步栽跟头。最常见的做法是把数据库连接串写进 system prompt让大模型自己生成 SQL 直接执行结果周五晚上被运维电话叫醒“你的 Agent 把线上订单表扫了一遍”“连接数被打满了”“它删了一条不该删的数据”。Agent 这个圈子不缺炫酷的 demo缺的是把数据库当成正规基础设施去接入的态度。这篇文章想聊的就是接入的正确姿势Agent 和数据库之间应该有几层、连接池怎么配、Text2SQL 到底该不该让模型直接碰线上库、记忆存储用什么数据结构、生产环境有哪些雷区。适合正在做 Agent 开发、或者想把大模型接进现有业务系统的工程师。我尽量少讲概念多给可落地的方案代码和参数都是我自己在项目里跑过、验证过的东西。1. Agent 和数据库之间到底该隔几层1.1 先分清三类刚需记忆、业务读写、知识检索在讨论“接入姿势”之前得先把 Agent 为什么需要数据库这件事拆清楚。我观察下来Agent 对数据库的需求基本是三类很多人把它们混为一谈导致选型和架构一团糟。第一类是记忆存储。Agent 要记住用户的历史偏好、会话上下文、长期画像这些数据需要持久化。短期会话可以放缓存但跨天的长期记忆必须落库。第二类是业务数据读写。比如用户问“我的订单到哪了”“上个月花了多少钱”Agent 需要去查真实的业务表。如果要执行“帮我改一下收货地址”“把这笔订单标记为已支付”就涉及写操作。第三类是知识检索。Agent 需要基于私有文档回答问题时通常要把文档切片 embedding 后存进向量库做相似度检索。这就是 RAG 的存储底座。这三类场景对数据库的形态、权限、操作方式的要求完全不同。记忆可以用 KV 或关系库业务读写必须走严格受控的接口知识检索需要向量能力。所以“接入姿势”不是一个统一方案而是分场景设计。1.2 那些“一把梭”的接法是怎么把系统搞挂的我复盘过不少事故问题几乎都出在“Agent 直连数据库”这种一把梭的做法上。这里罗列几种典型错误接法你对照一下自己有没有踩过。第一种把数据库连接串直接写进 prompt。比如在 system prompt 里写“你是一个数据库助手连接串是 mysql://root:xxxhost:3306/shop根据用户问题执行 SQL 并返回结果”。先不说凭据泄露的问题——你把这个 prompt 发给任何用户对方都能看到 root 密码。更严重的是模型生成的 SQL 完全没有校验一个语义偏差就可能把表删了。第二种每个请求新建一个连接。Agent 并发稍高一点数据库连接数瞬间被打满。MySQL 默认 max_connections 通常是 15120 个并发 Agent 任务每个开 10 个连接直接把你数据库拖垮。这种问题在压测之前根本发现不了因为平时流量低。第三种让模型直接写 SQL 操作生产库。模型对业务语义的理解是有概率的不是 100% 准确。它可能把“近三个月”理解成“最近三个月的数据全部删除”也可能在拼接用户输入时造成 SQL 注入。只要有一次代价就是整张表的数据。第四种把所有表结构一股脑塞进 prompt。表一多token 直接爆掉模型反而不知道该看哪张表。而且表结构是变化的prompt 里的 schema 过期之后模型会一本正经地用旧字段名生成 SQL报错都不知道为什么。这些问题的根源都一样Agent 是概率系统数据库是确定性系统两者之间必须加一层“受控的确定性接口”。正确姿势不是让 Agent 直接操作数据库而是让 Agent 操作一组预定义的工具函数数据库访问全部收敛在这组函数里。2. 选型的底层逻辑不是先选数据库是先选数据形态2.1 业务数据用关系型但别让 Agent 碰裸表业务数据优先选 MySQL 或 PostgreSQL这一点没什么争议。事务、约束、审计日志这些能力关系型数据库最成熟。但我要强调一个容易被忽略的点Agent 不应该直接访问业务裸表而应该通过视图或接口访问。什么意思呢比如用户表里有手机号、身份证号、会员等级、内部备注字段。Agent 查询时根本不需要看到所有字段。你可以建一个v_user_basic视图只暴露user_id, user_name, user_level, created_at这几个字段然后让 Agent 的工具函数只查这个视图。这样既减少 token 消耗又避免敏感字段被模型“顺手”输出。还有一个细节业务表的主键命名、字段注释一定要规范。模型理解表结构主要靠字段名和注释你字段叫a1、b2注释是空的模型大概率会乱猜。反过来说字段注释写得清晰Text2SQL 的准确率能肉眼可见地提升。2.2 RAG 和记忆召回向量库的定位不要搞错Agent 做知识库问答时向量数据库几乎是必选项。Milvus、pgvector、Qdrant、Chroma 都行看你的部署环境。但很多人把向量库定位搞错了——以为向量库是“搜索全部答案的地方”。实际的正确定位是“召回候选”。向量检索的作用是把最相似的 Top-K 片段捞出来真正的答案生成在大模型侧。所以向量库只负责存 embedding 和做相似度计算质量高低取决于分块策略和 embedding 模型而不是向量库本身。我实测过一个经验混合检索比纯向量检索稳。具体做法是向量检索和 BM25 关键词检索各出一批结果再用 RRFReciprocal Rank Fusion合并排序。尤其在专业术语多的场景纯向量检索经常把“连接池”匹配成“连接数”BM25 反而能精确命中。如果你做的是客服、医疗、法律这类垂直领域强烈建议上混合检索。2.3 Redis 这类缓存库只做短期状态别当真相源Agent 的状态管理里Redis 很常用。会话上下文、临时缓存、限流计数、分布式锁Redis 都能扛。但有一条红线我反复跟团队强调Redis 里的数据可以丢可以过期但绝不能成为唯一的数据源。我见过一个项目把用户订阅信息直接存在 Redis 里结果一次 Redis 重启用户数据丢了Agent 对着空数据给出了一堆离谱建议。正确做法是Redis 只保存可重建的短期状态比如当前会话的临时上下文、防重放的请求 token所有需要持久化的数据必须同步写入关系库。短期记忆和长期记忆分开这是 Agent 记忆设计的基本盘。3. 工具函数封装Agent 的手和眼怎么和数据库对接3.1 核心原则LLM 选工具、填参数数据库操作交给函数我在多个 Agent 项目里实践下来最稳的接入模式是“函数即边界”。不要把 SQL 的生成权交给模型而是预先定义好一组工具函数让模型在函数列表里做选择、填参数然后由函数内部执行受控的数据库操作。from dbutils.pooled_db import PooledDB import pymysql, json from flask import jsonify # 连接池在模块加载时创建全局复用 pool PooledDB( creatorpymysql, maxconnections20, mincached5, maxcached15, blockingTrue, setsession[SET time_zone00:00], ping1, host127.0.0.1, useragent_read, passwordyour_password, databaseshop, charsetutf8mb4, ) def query_orders(user_id: str, limit: int 10) - dict: 查询指定用户的最近订单列表。user_id 必须为纯数字字符串。 if not user_id.isdigit(): return {error: user_id 参数不合法} limit max(1, min(int(limit), 50)) # 强制 limit 上限 conn pool.connection() try: with conn.cursor() as cur: sql SELECT order_id, amount, status, created_at FROM orders WHERE user_id %s ORDER BY created_at DESC LIMIT %s cur.execute(sql, (user_id, limit)) rows cur.fetchall() return json.loads(json.dumps(rows, defaultstr)) finally: conn.close()这段代码里有四个必须养成的习惯参数化查询防止注入参数白名单校验user_id.isdigit()写入操作必须走独立函数而不是放一个通用“执行任意 SQL”的工具连接用完一定归还conn.close()实际是还给连接池。为什么要这么设计因为 LLM 是概率系统它可能把参数填错。工具函数的参数校验就是兜底。比如用户输入“帮我查一下订单号 abc 的单子”模型可能把abc直接填进user_id没有白名单校验的话数据库就会执行一个带字符串参数的查询。加一层校验既规避错误也让模型收到明确的报错信息反馈到下一步对话里。3.2 连接池配置与超时处理参数按这个基准调连接池是 Agent 接数据库的命门。我建议直接用 HikariCPJava 生态或 DBUtils 的 PooledDBPython 生态不要每次请求 new 连接。连接池的核心参数就五个最大连接数、最小空闲数、连接超时、空闲超时、最大生命周期。不同数据库驱动叫法不太一样但语义一致。以 HikariCP 为例maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000我个人的配置基准是maximum-pool-size约等于 Agent 单实例并发任务数乘 2再加 5 的余量。比如你跑 10 个并发 Agent 任务池子配 25 左右比较合理。配太大反而危险因为数据库服务端max_connections是全局限额其他业务会被你挤爆。ping1对应 Python 的 PooledDB表示每次从池里取连接时先探测一下连接是否还活着。这个必须开否则 MySQL 的wait_timeout默认 8 小时把空闲连接断开后你池里的连接全是“僵尸连接”第一次查询会直接抛异常。Java 的 HikariCP 有内置的 connection test query可以设成SELECT 1。另一个容易漏的是连接的最大生命周期一定要小于数据库服务端的wait_timeout。max-lifetime配 30 分钟比较安全MySQL 那边wait_timeout不要调太大保持默认或者设成 1 小时两边配合连接就不会断在中间。3.3 权限和路由最小账号 只读副本权限设计这件事我见过太多团队在项目上线前才想起。正确做法是从第一天就给 Agent 建独立数据库账号不要把 root 或业务主账号给 Agent 用。基础原则是“最小化”。查询型 Agent 只给SELECT权限CREATE USER agent_read% IDENTIFIED BY strong_password; GRANT SELECT ON shop.* TO agent_read%;如果 Agent 需要写入订单、更新用户资料单独建一个agent_write账号只授予具体业务表比如orders、user_address的INSERT、UPDATE权限永远不给DELETE和TRUNCATE。真正的删除操作应该走业务接口的软删除逻辑而不是数据库层面的物理删除。生产环境再进一步Agent 的查询尽量走只读副本。把主库的 binlog 同步到从库Agent 连接从库这样 Agent 的慢查询、扫描操作再夸张也不会拖垮主库写入。这是我在线上扛住过压力的经验——Agent 的查询模式很难预估给它一个独立从库是最便宜的隔离方案。4. Text2SQL 中间层让 LLM 写 SQL但别让它直接执行4.1 为什么需要中间层模型幻觉、SQL 注入、连接失控Text2SQL 是 Agent 接入数据库最诱人的能力也是风险最高的一条路。用户说一句“查一下上个月销售额 TOP10 的商品”模型直接生成 SQL 并执行体验确实好。但直接执行的前提是“完全信任模型”这在生产环境是危险的。三个风险我必须展开讲。第一个是模型幻觉导致的语义偏差比如模型对表结构理解错把created_at当成updated_at或者 JOIN 条件写错结果查出来的数据完全是错的而且用户很难察觉。第二个是 SQL 注入虽然 LLM 本身不会故意攻击你但它会把用户输入直接拼进 SQL用户说“删除订单号 123DROP TABLE orders”时模型可能原样生成。第三个是连接失控模型生成的 SQL 可能没有 LIMIT或者做全表扫描一次查询就能把数据库 IO 打满。所以我的方案是让模型生成 SQL但执行前加一个确定性校验层。模型负责“写”校验器负责“审”数据库负责“只读执行”。这样既保留自然语言查询的体验又不会让概率问题直接冲击数据库。4.2 Schema 注入与字段注释让模型“知道”而不是“乱猜”Text2SQL 的第一步是让模型了解数据库结构但这里的做法有讲究。不能把整个库的所有表结构都丢进 prompttoken 不够模型也会迷失。正确做法是通过语义相关性预筛选只注入相关的几张表。我通常会维护一个“表-字段-注释”的元数据表先从information_schema拉取再人工补充业务含义描述。查询时先用用户问题做关键词匹配选出候选表再把这些表的字段名、字段类型、注释、样例值传入 prompt。SELECT column_name, column_type, column_comment FROM information_schema.columns WHERE table_schema shop AND table_name orders ORDER BY ordinal_position;注意字段注释这一项绝大多数团队都没写。如果你在一个没有注释的库存表上做 Text2SQL模型基本是在盲猜。我给过一个经验字段注释至少要包含“业务含义 取值范围 样例”比如status VARCHAR(16) COMMENT 订单状态: pending/paid/shipped/completed/cancelled。模型看到这样的注释生成 SQL 的准确率会高很多。4.3 可落地的校验器黑名单、白名单、行数上限生成 SQL 之后进入校验器。校验器必须跑在模型生成的 SQL 和数据库之间拦截高风险操作。我用过最有效的三层校验都不复杂但组合起来很稳。第一层是黑名单正则。凡是出现DROP、TRUNCATE、DELETE、UPDATE、ALTER、多语句分号、注释符--的直接拒绝。对查询型 Agent 来说这些操作本来就不该出现。import re FORBIDDEN [ r\bdrop\b, r\btruncate\b, r\bdelete\b, r\bupdate\b, r\binsert\b, r\balter\b, r\bcreate\b, r\b--, r;.*;, ] def validate_sql(sql: str) - tuple[bool, str]: sql_lower sql.lower() for pattern in FORBIDDEN: if re.search(pattern, sql_lower): return False, fSQL 包含被禁止的关键字: {pattern} if sql_lower.count(;) 1: return False, SQL 不允许包含多条语句 if ; in sql.rstrip().rstrip(;).strip(): return False, SQL 语句后不能附带额外命令 return True, 第二层是只检查查询语句结构。用正则或简单解析器确认 SQL 以SELECT开头并且包含LIMIT。如果没有 LIMIT就给大模型 Fallback 逻辑自动补一个LIMIT 100防止全表扫描。第三层是 EXPLAIN 预检。在真正执行前先EXPLAIN一下 SQL检查访问类型。如果type是ALL全表扫描并且表数据量很大就直接拦截告诉模型“这张表全表扫描代价过高请加上 WHERE 条件”。这一步能避免很多慢查询事故。4.4 实测效果与优化手段题库和 few-shot 带来的提升我最早搭 Text2SQL 中间层时单表简单查询的准确率大概在 80% 左右多表 JOIN 直接掉到 50% 以下。后来靠两个手段拉回了 10 多个百分点。第一个是建“语义对齐题库”。收集高频用户问题和对应的正确 SQL作为 few-shot 示例写进 prompt。比如用户常问“上周订单量多少”就在题库里放一条示例“上周订单量多少 - SELECT COUNT(*) FROM orders WHERE created_at DATE_SUB(NOW(), INTERVAL 7 DAY)”。模型看到和你真实业务相关的示例生成质量会明显提升。第二个是修正字段名映射。模型经常把业务口语翻译错比如“退款”对应refund_amount模型可能用成charge_back。做法是在元数据表里加一个“别名”字段把“退款、退钱、退回”统一映射到refund_amount。这个改动很小但对准确率的提升立竿见影。校验器拦截率也要看数据。在加了黑名单和 EXPLAIN 之后拦截率大约在 8%-12%其中大部分是漏写 LIMIT 和 JOIN 条件错误。跑一段时间你会发现大部分错误不是模型不会写 SQL而是“不知道你的表和字段到底是什么意思”。所以投入精力维护 schema 元数据比换更强的模型更划算。5. Agent 记忆和数据库不是把对话记录全塞进去5.1 短期、长期记忆的分段存储设计很多 Agent 项目做完之后才发现记忆才是最棘手的问题。无脑把整段对话历史写进数据库既不经济也不利于召回。我的做法是把记忆分成两段短期记忆放 Redis 或内存长期记忆放关系库。短期记忆就是当前会话的上下文包含最近几轮对话、临时状态变量。它的特点是写频繁、读频繁、过期快。放在 Redis 里设置 30 分钟到 1 小时的 TTL会话结束就自动清掉。注意不要把短期记忆当长期记忆的缓冲——它不需要落库。长期记忆是用户画像、历史偏好、关键事件比如“用户不喜欢辣”“用户上次反映过配送太慢”。这些信息的提取不能靠把对话原文存下来而是要靠 Agent 在每轮对话的结尾做一个“总结摘要”动作抽取可结构化的字段写入数据库。长期记忆才是需要关系库或向量库兜底的部分。5.2 记忆召回向量检索 关键字段直查长期记忆存进去之后怎么在关键时刻召回决定了记忆有没有用。我的方案是双通道召回。结构化记忆走关键字段直查。比如用户 ID、偏好标签、历史订单数这些信息是明确的直接用WHERE user_id ?就能查到不需要也不应该走向量检索。很多团队把这事做复杂了——所有记忆都 embedding 进向量库反而丢失了精确性。非结构化记忆走向量检索。用户说过的话、总结出的抽象偏好这些是自然语言适合切片 embedding 后存进向量库。召回时拿当前轮次的用户输入向量去检索取 Top 3-5 条相关信息拼进 prompt。建议关系库和向量库配合关系库存记忆的元数据用户 ID、记忆类型、创建时间、来源会话 ID向量库存语义向量并关联到元数据 id。这样既能精确筛选又能语义召回。5.3 记忆写入的抽稀与遗忘策略记忆不是越多越好。我见过一个 Agent 把用户说过的每句话都写入长期记忆结果每次查询都返回几百条无关内容prompt 直接被淹没。所以写入要做抽稀只在检测到“关键信息”时写入。具体来说可以用一个简单的规则引擎命中用户偏好关键词喜欢/不喜欢/希望/建议时把这句话作为记忆候选然后经过一个 LLM 摘要步骤压缩成一句话存入。没有新信息量的寒暄、重复确认一律不写。遗忘策略同样重要。数据库表里加两个字段importance_score重要度1-5和last_access_at最近访问时间。定期清理任务按规则处理重要度低于 2 且超过 90 天未访问的记忆标记为归档超过 180 天的物理删除。这样才能保证记忆库长期保持高质量而不是变成垃圾堆。6. 生产环境容易爆的五个雷区6.1 连接泄漏和连接池耗尽连接池耗尽是我在 Agent 项目里遇到频率最高的问题。症状是 Agent 响应突然变慢日志里报connection timeout紧接着数据库的Threads_connected计数飙高。原因基本都是业务代码里查完数据没归还连接或者事务里开了连接但异常分支没走 close。排查方式很简单连上数据库执行SHOW PROCESSLIST;看有没有大量Sleep状态的连接再查SHOW STATUS LIKE Threads_connected;对比连接池上限。如果 Threads_connected 长期接近连接池上限就说明有泄漏。预防方法一个是靠代码规范所有数据库操作放在 try/finally 里确保归还连接就像前面代码示例那样。另一个是给连接池打开泄漏检测HikariCP 有leak-detection-thresholdPooledDB 可以在外层封装记录连接借用时间超过阈值打印告警。6.2 慢 SQL 拖垮 Agent 响应时间Agent 的自然语言交互对响应时间是敏感的用户问一句5 秒才回体验就很差了。而数据库慢查询常常把整个链路拖成 30 秒以上。除了前面说的 EXPLAIN 预检生产环境一定要给 Agent 的数据库操作设置独立的超时。Python 的 pymysql 可以在连接参数里指定read_timeout和write_timeout我一般设 10 秒Java 侧可以用 HikariCP 的connection-timeout配合 MyBatis 的defaultStatementTimeout。宁可这个查询失败让 Agent 说“数据查询超时”也不能让它卡住整个线程。慢查询日志必须每天看。MySQL 的slow_query_log打开long_query_time调成 0.5 秒Agent 产生的慢查询会比业务系统多得多。你会惊讶地发现很多慢查询都是模型生成 SQL 时漏了 WHERE 条件导致的全表扫描。把这些慢查询收进反馈集定期优化题库准确率会持续提升。6.3 循环写入和重复调用造成脏数据Agent 的任务循环里工具调用失败后会自动重试。如果工具函数写入数据库时不带幂等控制重试就会产生重复数据。我踩过一次Agent 给用户创建工单第一次调用超时但实际写入成功Agent 重试同一个工单被写了两遍。解决方案是在业务表里加request_id字段每次工具调用生成一个 UUID 作为请求唯一 ID。写入时先查request_id是否已存在存在就直接返回已有结果不存在才执行插入。这条幂等逻辑的成本很低但能根治重试脏数据。6.4 时区与序列化问题这个雷区特别隐蔽。Agent 的底层模型对时间表达是混乱的它可能把“下午 3 点”理解成15:00也可能理解成03:00 PM。而数据库的DATETIME类型又不带时区信息如果 Python 的 datetime 对象直接序列化很容易出现“时间差 8 小时”的诡异 bug。我的建议是所有 Agent 相关的数据库连接统一使用 UTC 时区应用层做转换后再展示给用户。连接池初始化时执行SET time_zone 00:00代码里所有时间字段用datetime对象操作序列化时统一格式如2024-01-15T10:30:00Z。再配合一个时区转换组件把最终展示时间转成用户本地时区。这套方案一旦定下来就不要改否则线上必乱。6.5 Agent 权限失控与审计缺失前面讲了账号最小化这里再补一点Agent 的数据库操作日志必须完整留存。因为 Agent 的行为是概率性的出了问题需要能回溯“它在哪个时候、用哪个工具、执行了什么 SQL”。实现不复杂在工具函数入口和出口各打一条日志记录agent_id, user_id, tool_name, sql, params, duration_ms, status_code。日志写到独立的审计表定期归档。一旦线上出现数据异常这个审计表就是你排查的第一现场。别嫌麻烦等出了问题没有日志那才是真的叫天天不应。7. 踩坑实录三个印象深刻的案例7.1 一次误传表名的参数校验拦截这事发生在测试环境但让我意识到参数校验必须从第一天就做好。当时我在写一个订单查询工具模型在调用query_orders时把参数user_id填成了表名orders。如果函数没有参数校验SQL 会变成WHERE user_id orders然后返回空结果Agent 就会一脸正经地告诉用户“您没有订单”。我在工具函数里加了参数类型校验和长度限制发现这个错误后直接返回错误信息给模型模型又重新生成了正确的调用。整个过程没有引发事故但它让我确信工具函数只要有一点含糊LLM 就会随机犯错。参数校验不是后端开发的专利Agent 工具函数尤其需要因为走过来的参数都经过了一道概率模型。7.2 连接池参数翻车50 个并发任务把数据库打爆这个案例最疼。当时我自信地给 Agent 配了maximum-pool-size50原因是“并发任务 50 个一个任务一个连接总够了吧”。结果压测时数据库直接拒绝连接MySQL 错误日志里全是Too many connections。复盘时发现数据库服务端max_connections只有 200而除了 Agent 连接池的 50 个连接业务系统、监控、备份都占了一部分。Agent 连接池一上来就把余量吃光了。更糟的是连接池里的连接没有被正确复用——每次任务结束时连接归还逻辑有 bug导致连接泄漏。后来我把连接池降到 20修好归还逻辑同时给数据库账号加了个单账号连接数上限才稳下来。血泪教训是连接池上限不是越大越好它必须小于数据库服务端限额并且给其他业务留出余量。你还要把 Agent 连接池的监控接到告警系统Threads_connected超过阈值就报警。7.3 Text2SQL 把“删掉”理解成了 DELETE用户对 Agent 说“帮我把上个月的报表数据删掉重算”模型生成的 SQL 是DELETE FROM monthly_report WHERE month 2025-01。这条 SQL 被我的黑名单校验器拦住了。这其实是我故意设计的正面案例如果中间层没加校验这行数据就没了。这次经历让我把“危险操作”处理机制固化了。默认情况下Agent 生成的写操作 SQL 一律不直接执行而是转化为“操作确认”流程Agent 向用户确认“您要删除 2025-01 的月报数据共 128 行是否继续”用户确认后走业务服务接口的软删除逻辑在数据表里标记is_deleted1而不是物理删除。这样既能完成用户指令又保留了数据恢复的余地。回看这三个案例我最大的体会是Agent 接入数据库本质上不是技术问题而是“如何在一个概率系统和一个确定性系统之间建立可控边界”的工程问题。工具函数是边界参数校验是边界连接池参数是边界Text2SQL 校验器是边界权限账号也是边界。每加一道边界线上事故的概率就低一个数量级。这个过程不用追求一次做到完美但每一道边界都值得在项目初期就埋下去。
返回列表