不只是"自然语言转 SQL":一个生产级数据库节点的技术亮点拆解

简介: 让 AI 直接写 SQL 查库,难点不在生成,而在可控。本文拆解硅基边界 Silicon-AI 数据库节点的技术亮点:两级渐进式 Schema 裁剪压缩上下文;增删改三级权限闸门用代码硬校验拦截,不依赖模型自觉;INSERT 必填字段执行前拦截;写操作自动回查真实数据根治幻觉;SQL 封装为工具支持多步编排与级联删除;并覆盖 SQL 消毒、PostgreSQL JSONB 类型转换、连接池参数化、慢 SQL 监听、SSE 会话治理与 AI/手写双模式。每条亮点均配示意代码。

本文是「成都硅基边界」零代码构建平台旗下 Silicon-AI 智能体引擎实战复盘系列第六篇。前五篇聊了智能体、RAG、LangChain 教学、MCP 插件与工作流引擎,这一篇聚焦数据库:让 AI 直接对业务库做增删改查,到底有哪些坑,我们又是怎么一层层拦住的。

文中每个技术亮点都配了示意代码——均根据设计改写、非项目真实源码,只用于演示思路与 API 用法。

一、背景:RAG 解决不了"结构化查询"

企业最有价值的数据,大多躺在关系型数据库里:订单、客户、库存、账单。RAG 擅长从文档里找答案,但对"上个月华东区销售额 TOP5 的产品"这类问题无能为力——答案不是某段文字,而是需要聚合计算的结果。

所以"让 AI 直接写 SQL 查库"(NL2SQL)几乎是所有智能体平台的必备能力。但它也是风险最高的一环:

  • 上下文爆炸:几十张表的完整 schema 塞进提示词,token 直接爆表,还会引入噪声;
  • 危险的写操作:模型一句 DELETE FROM orders 就能删库,提示词里写"请不要删除数据"根本拦不住;
  • 幻觉式回答:SQL 到底执行成功没有、影响了哪些行,模型可能凭空编造;
  • 跨表关联无能:真实业务往往是多步操作(先查 A 表拿 ID,再写入 B 表)。

Silicon-AI 的数据库节点围绕这四个问题做了一整套设计,下面逐个拆解技术亮点。

二、亮点一:渐进式 Schema 裁剪,先筛表再取字段

最直觉的做法是把整个库的 schema 塞给模型,代价是上下文爆炸。我们改成两级渐进式检索:

flowchart TD
    A[用户问题] --> B[第一级:筛选相关表<br/>按问题语义从库中挑出候选表]
    B --> C[第二级:取表字段详情<br/>只展开被选中的表结构]
    C --> D[组装精简 Schema]
    D --> E[LLM 生成 SQL]
    E --> F{是否写操作}
    F -->|否| G[执行并返回结果]
    F -->|是| H[权限闸门校验]
    H -->|通过| I[执行 + 写后回查]
    H -->|拒绝| J[返回明确拒绝原因]
    I --> K[组织答案]

对应的示意实现:

# 示意代码(非项目源码):两级 schema 裁剪,把全量 schema 变成按需加载
def build_prompt_schema(question: str, db) -> str:
    # 第一级:只给「表名 + 表描述」,让模型挑出相关表
    candidates = db.list_tables()                   # [(name, comment), ...]
    picked = llm_pick_tables(question, candidates)  # 模型返回相关表名列表

    # 第二级:只展开被选中表的字段(列名 / 类型 / 是否必填 / 业务含义)
    blocks = []
    for table in picked:
        cols = db.get_table_fields(table)
        lines = [f"- {c.name} {c.type}"
                 f"{'(必填)' if c.required else ''}:{c.comment}"
                 for c in cols]
        blocks.append(f"表 {table}:\n" + "\n".join(lines))
    return "\n\n".join(blocks)

schema 从"全量"变成按需加载,上下文成本和噪声同步下降——这是效果与成本双赢的第一步。

三、亮点二:权限闸门——不依赖"模型自觉"

这是安全上最关键的设计。平台给每个数据库配了三个独立开关:新增权限(add)、修改权限(edit)、删除权限(delete)。SQL 执行前先解析语句类型,命中对应开关:

# 示意代码(非项目源码):权限闸门——安全策略必须是确定性代码
def detect_op(sql: str) -> str:
    """按语句前缀判定操作类型。"""
    head = sql.strip().upper()
    for op in ("INSERT", "UPDATE", "DELETE", "SELECT"):
        if head.startswith(op):
            return op
    return "UNKNOWN"

def run_sql(sql: str, fields=None) -> str:
    op = detect_op(sql)
    if op == "INSERT" and not config.add_authority:
        return "当前数据库未开启新增权限,操作已拒绝"
    if op == "UPDATE" and not config.edit_authority:
        return "当前数据库未开启修改权限,操作已拒绝"
    if op == "DELETE" and not config.delete_authority:
        return "当前数据库未开启删除权限,操作已拒绝"
    return execute(sql)      # 通过闸门才真正执行

核心思想:安全策略必须是确定性代码,不能是提示词里的"请小心"。提示词是软约束,模型有概率不遵守;代码是硬校验,百分百拦得住。我们采取"软硬双保险"——提示词里告知规则减少误生成,代码里再做一次拦截兜底。

四、亮点三:必填字段硬校验,拦截"半截 INSERT"

写库时另一类常见事故是"插入了不完整的记录":schema 里 required=true 的列没给值,模型自己也没意识到。我们的做法是在执行前做必填字段硬校验:

# 示意代码(非项目源码):解析 INSERT 目标表与列名,比对必填列
import re

def parse_insert_target(sql: str):
    """从 INSERT 语句解析目标表名与显式列名列表。"""
    m = re.match(r"\s*INSERT\s+INTO\s+(?:[\w]+\.)?[`\"\[]?(\w+)[`\"\]]?"
                 r"(?:\s*\(([^)]*)\))?", sql, re.IGNORECASE)
    if not m:
        return None, []
    table = m.group(1)
    cols = [c.strip() for c in m.group(2).split(",")] if m.group(2) else []
    return table, cols

def check_required(sql: str, schema: dict):
    """required=true 的列必须显式出现,不依赖模型自觉。"""
    table, cols = parse_insert_target(sql)
    if not table or table not in schema:
        return
    missing = [c.name for c in schema[table].columns
               if c.required and c.name not in cols]
    if missing:
        raise ValueError(f"缺少必填字段:{', '.join(missing)}")

而且提示词里也会明确告知模型:"若用户未提供必填字段值,不要生成 SQL,直接说明缺失哪些字段——即使生成也会被系统拦截"。告诉模型有拦截机制,它就不太会硬着头皮乱生成,这种"预告规则"比单纯禁止更有效。

五、亮点四:写操作回查——让模型看见"实际写了什么"

这是我认为最巧妙的一环。模型生成 UPDATE 或 INSERT 后,如果只返回"执行成功",它很可能会凭想象向用户描述结果("已为您新增了订单"——但订单号是数据库生成的,它其实不知道)。

我们的处理是:写操作执行后自动回查一次——

# 示意代码(非项目源码):写后回查,让模型基于真实数据回答
def execute_write(sql: str, session, schema):
    op = detect_op(sql)

    if op == "INSERT":
        new_ids = session.execute(sql).inserted_primary_keys   # 拿到新行主键
        rows = session.execute(
            f"SELECT * FROM {parse_insert_target(sql)[0]} WHERE id IN :ids",
            {
   "ids": new_ids})
        return {
   "affected": len(new_ids), "rows": rows.fetchall()}

    if op == "UPDATE":
        table = re.match(r"\s*UPDATE\s+(?:[\w]+\.)?[`\"\[]?(\w+)", sql,
                         re.IGNORECASE).group(1)
        where = extract_where_clause(sql)          # 抽取原 WHERE 条件
        affected = session.execute(sql).rowcount
        rows = session.execute(f"SELECT * FROM {table} WHERE {where}")
        return {
   "affected": affected, "rows": rows.fetchall()}
  • INSERT 执行完,按新行 ID 回查刚插入的那条记录;
  • UPDATE 执行完,按原 WHERE 条件回查更新后的行。

回查结果再喂回模型,它组织出的回答就是基于真实数据的:"订单已创建,订单号 SO20260827,金额 1,280 元"。让模型只描述它真正看到的东西,是消灭写操作幻觉最有效的手段。

六、亮点五:多步 SQL 编排,处理真实业务的"连环操作"

真实业务很少是一句 SQL 搞定的。我们把 SQL 执行封装成工具,让模型在循环里多次调用它,从而支持多步编排:

# 示意代码(非项目源码):SQL 作为工具,模型循环调用完成多步操作
executed_steps = []

@tool
def sql_exec(sql: str, fields: list = None) -> str:
    """执行一条 SQL。SELECT 返回结果集;写操作返回影响行数与回查数据。"""
    run_sql_permission_gate(sql)          # 亮点二:权限闸门
    check_required(sql, schema_map)       # 亮点三:必填校验
    result = execute_write(sql, session, schema_map)   # 亮点四:写后回查
    executed_steps.append({
   "sql": sql, "affected": result["affected"]})
    return format_result(result)

@tool
def get_table_fields(table: str) -> str:
    """查询指定表的字段结构(用于中途补充上下文)。"""
    return db.describe(table)

# 模型在循环里多次调用:先查 A → 再查 B → 最后插入 C
agent = create_agent(
    model=llm,
    tools=[sql_exec, get_table_fields, search_tables],
    system_prompt="复杂需求请拆成多步,每步用工具执行,后续步骤依赖前一步的真实结果。",
)
  • 关联新增:先 SELECT 客户表拿到客户 ID → 再 SELECT 产品表拿到产品 ID → 最后用两个 ID INSERT 订单表;
  • 级联删除:删除主表记录前,先清理关联表中的依赖数据。

每一步执行都记进 executed_steps(执行轨迹),哪一步成功、哪一步失败、影响了多少行,一目了然——既是给模型看的上下文,也是给用户和运维看的审计日志。

七、亮点六:工程细节里的真功夫

# 示意代码(非项目源码):SQL 消毒——模型输出常裹着 markdown 与分号
def sanitize_sql(raw: str) -> str:
    s = raw.strip()
    s = re.sub(r"^```(?:sql)?|```$", "", s, flags=re.MULTILINE).strip()  # 去代码块
    s = re.sub(r"--.*$", "", s, flags=re.MULTILINE)                      # 去行注释
    s = s.rstrip(";").strip()                                            # 去尾部分号
    return s
  1. SQL 消毒:去掉 Markdown 代码块、尾部分号、行注释,避免"明明是对的却执行报错";
  2. 语句类型路由:SELECT 与写操作走不同返回策略——写操作只在真正影响数据时才返回结果,避免"执行了 0 行"却回答"已成功";
  3. PostgreSQL JSONB 的坑:对 jsonb 字段更新时,jsonb_set(data, ...) 直接写会报类型错误,必须显式转换:
-- 正确:先 ::jsonb 再 jsonb_set,最后 ::text 转回
UPDATE t SET data = jsonb_set(data::jsonb, '{key}', '"newValue"')::text WHERE id = 1;

-- 错误:缺少类型转换,PostgreSQL 会报函数不匹配
UPDATE t SET data = jsonb_set(data, '{key}', '"newValue"') WHERE id = 1;
  1. 输出字段描述:要求模型在调用工具时说明每个输出字段的业务含义,让最终结果"可解释",而不是一堆裸列名:
# 示意代码(非项目源码):工具参数要求声明输出字段的业务含义
class SqlExecToolArgs(BaseModel):
    sql: str = Field(description="要执行的 SQL 语句")
    fields: List[FieldDesc] = Field(
        default=[],
        description="SELECT 输出字段描述列表;INSERT/UPDATE/DELETE 可为空")

八、亮点七:连接与会话治理(工程底座)

再聪明的查询也架不住连接池崩了。

# 示意代码(非项目源码):连接串编码 + 连接池参数化
from urllib.parse import quote_plus

# 密码含 @ 必须百分号编码,否则 make_url 按第一个 @ 切分导致主机解析错误
url = (f"postgresql+psycopg://{quote_plus(user)}:{quote_plus(password)}"
       f"@{host}/{dbname}")

engine = create_engine(
    url,
    pool_pre_ping=True,          # 自动剔除失效连接
    pool_size=pool_size,         # 多副本部署时按 PG max_connections 反推
    max_overflow=max_overflow,
    pool_recycle=pool_recycle,
    pool_timeout=pool_timeout,
    connect_args={
   "connect_timeout": 10,
                  "keepalives": 1, "keepalives_idle": 60,   # 防中间设备静默断连
                  "application_name": "silicon-ai"},         # pg_stat_activity 可溯源
)
# 示意代码(非项目源码):慢 SQL 监听,区分「SQL 慢」与「连接池等待」
@event.listens_for(engine, "before_cursor_execute")
def _start(conn, cursor, statement, params, context, executemany):
    context._t0 = time.monotonic()

@event.listens_for(engine, "after_cursor_execute")
def _log_slow(conn, cursor, statement, params, context, executemany):
    ms = (time.monotonic() - context._t0) * 1000
    if ms > 500:
        logger.warning("慢SQL %.0fms: %s", ms, " ".join(statement.split())[:200])
# 示意代码(非项目源码):ASGI 中间件 + ContextVar,会话覆盖流式全生命周期
session_context: ContextVar[Session] = ContextVar("session_context")

class SessionMiddleware:
    async def __call__(self, scope, receive, send):
        if scope["type"] in ("http", "websocket"):
            with Session(engine) as session:
                token = session_context.set(session)
                try:
                    await self.app(scope, receive, send)   # SSE 流式期间 session 依然可用
                finally:
                    session_context.reset(token)

def get_current_session() -> Session:
    return session_context.get()      # 任何代码无需层层传参即可拿到会话

四个要点:密码百分号编码(生产库密码含 @ 会毁掉连接串)、连接池参数化 + pre_ping + keepalive + application_name、慢 SQL 监听(有慢日志 = SQL 慢;没慢日志但请求慢 = 卡在连接池等待)、会话覆盖 SSE 全生命周期(纯 ASGI 中间件 + ContextVar,流式响应内部也能拿到会话)。

九、两种模式:AI 生成 SQL 与手写 SQL

# 示意代码(非项目源码):双模式共用同一条拦截管道
if db_config.create_sql == "ai":
    state = rag_database.query(question, llm)      # 模型生成 SQL 并执行
else:
    state = rag_database.exec_sql(db_config.sql)   # 直接执行用户手写 SQL

# 无论哪条路径,权限闸门、必填校验、写后回查全部一致生效

AI 模式适合业务人员(自然语言提问),自定义模式适合有确定逻辑的场景(不消耗生成 token、结果完全可控)。两种模式共用同一套权限校验与执行管道——上层怎么来不重要,底层拦截只有一条。

十、结语:让 AI 写 SQL 的三条铁律

回顾整个设计,可以浓缩成三句话:

  1. 最小上下文——渐进式 schema 裁剪,只给模型它此刻需要知道的表结构;
  2. 硬校验优先——权限、必填字段这类安全与完整性约束,必须是代码拦截,提示词只做辅助;
  3. 写后必回查——让模型基于真实数据回答,而不是基于想象。

NL2SQL 的难点从来不是"让模型写出 SQL",而是让它在可控、可审计、可解释的前提下写对 SQL。把这三点做到位,AI 查库才真正能进生产。

目录
相关文章
|
2月前
|
机器学习/深度学习 数据采集 人工智能
企业知识库搭建实战:RAG 从文档导入到检索调优全流程拆解
本文详解企业知识库搭建实战:以RAG为核心,覆盖文档导入、智能解析分段、语义/增强检索调优全流程。结合硅基边界平台案例,直击解析策略、分段长度、相似度阈值等关键参数调优要点,助技术/产品/运营团队两周内快速验证AI问答效果,让私域资料真正变成“会回答的AI”。
304 1
|
2月前
|
人工智能 中间件 定位技术
LangChain 入门教学:一张地图搞懂模型、链、RAG、图与智能体
本文是一份面向初学者的LangChain系统性入门指南,以“认知地图”为主线,清晰梳理LangChain 1.0、LangGraph、RAG、智能体(`create_agent`)等核心模块的定位与协作关系,摒弃过时API,聚焦2026年官方推荐架构,助开发者快速建立正确心智模型。(239字)
559 0
|
Java 关系型数据库 中间件
分库分表(3)——ShardingJDBC实践
分库分表(3)——ShardingJDBC实践
1760 0
分库分表(3)——ShardingJDBC实践
|
2月前
|
存储 监控 API
基于 RAG + LangChain 搭建企业级私有知识库问答系统(2026 实战版)
本文是作者基于多个企业RAG知识库落地经验的实战总结,提供完整可运行代码与十年避坑指南。涵盖文档解析、混合检索、向量存储、DeepSeek接入、结果重排、拒答机制及效果评估,助你构建本地可运行、生产可扩展的企业级私有知识库系统。(239字)
588 1
|
9月前
|
存储 人工智能 分布式计算
阿里云 OpenLake:AI 时代的全模态、多引擎、一体化解决方案深度解析
阿里云徐晟详解OpenLake:构建全模态、多引擎、一体化智能数据体系,融合大数据与AI,支持湖仓一体、Agentic Data及AI搜索,助力企业降本增效、加速AI落地。(239字)
1145 2
阿里云 OpenLake:AI 时代的全模态、多引擎、一体化解决方案深度解析
|
7月前
|
人工智能 自然语言处理 数据可视化
JeecgBoot低代码 AI工作流知识库节点:构建企业私域RAG问答的核心引擎
JeecgBoot低代码平台的知识库节点是构建企业私域RAG问答系统的核心组件,通过灵活的多知识库查询、可调节的TOP K和Score阈值参数以及结构化输出变量,让开发者无需编写检索代码即可实现基于企业知识的精准AI问答。
342 2
|
7月前
|
存储 人工智能 监控
多智能体系统的三种编排模式:Supervisor、Pipeline 与 Swarm
2026年,多智能体系统成主流:单智能体易陷上下文污染、角色混乱与故障扩散;而Supervisor、Pipeline、Swarm三类编排模式,配合结构化通信、按能力拆分、置信度验证与全链路Tracing,可构建更可靠、可控、可扩展的AI协作系统。
1238 2
多智能体系统的三种编排模式:Supervisor、Pipeline 与 Swarm
|
存储 安全 测试技术
Python面试题精选及解析
本文详解Python面试中的六大道经典问题,涵盖列表与元组区别、深浅拷贝、`__new__`与`__init__`、GIL影响、协程原理及可变与不可变类型,助你提升逻辑思维与问题解决能力,全面备战Python技术面试。
880 1
|
11月前
|
SQL 人工智能 BI
AI 在数据库操作中的各类应用场景、方案与实践指南
本文系统梳理AI在数据库操作中的8大核心场景,涵盖智能查询生成、性能优化、数据质量监控与自动化报表等,结合SQL实例与最佳实践,展现AI如何赋能数据库开发,提升效率与洞察力。
1230 1
AI 在数据库操作中的各类应用场景、方案与实践指南
|
11月前
|
Kubernetes API 开发工具
深入浅出K8S技术原理,搞懂K8S?这一篇就够了!
本文以“K8S帝国”为喻,系统解析Kubernetes核心技术原理。从声明式API、架构设计到网络、存储、安全、运维生态,深入浅出揭示其自动化编排本质,展现K8S如何成为云时代分布式操作系统的基石。(239字)
4540 8

热门文章

最新文章