手搓Agent实战:Text-to-SQL数据库查询能力全解析
2026/9/13 5:44:27 网站建设 项目流程

最近在做“手搓 Agent”系列,前几关把工具调用和 ReAct 循环跑通之后,我一度觉得 Agent 差不多也就这样了——能查天气、能算算术,但本质上还是个会喘气的 API 封装。直到我把 Text-to-SQL 接进去,让 Agent 可以直接问数据库要答案,这个想法才被彻底推翻。

这一关是第 2.3 关的中篇,主题很聚焦:给 Agent 搭一个 Text-to-SQL 数据库查询能力。所谓 Text-to-SQL,简单说就是用户用自然语言提问,Agent 负责把问题翻译成 SQL、去数据库执行、再把查询结果整理成人话返回。听起来好像就是“让大模型写 SQL”,但真正落地时你会发现,里面藏着 schema 上下文、安全执行、结果回填、错误恢复一堆细节。这篇会把关键点一个个讲透,并给出一份可以直接抄的 Python 实现。适合已经会基础 Agent 开发、正打算让 Agent 干点实际活儿的读者,尤其是做选课系统、教务管理这类数据查询场景的朋友。

1. Text-to-SQL 到底难在哪:不是“转 SQL”,而是补全认知缺口

很多人第一次接触 Text-to-SQL,第一反应是:这不就是让大模型写个 SQL 吗?大模型写 SQL 不是挺厉害的吗?确实厉害,但厉害是有限制的——你直接丢一句“帮我查一下计算机专业学生的选课情况”给模型,它大概率会给你一个看起来很像样、实际上跑不通的 SQL,因为在它眼里“选课情况”对应哪张表、“计算机专业”对应哪个字段,这些信息它根本不知道。

1.1 模型缺的不是语法,而是数据库的“地图”

写 SQL 这件事本身,对今天的 LLM 来说确实不难。难的是它不知道你的数据库长什么样。你有个表叫enrollments,里面有个字段叫student_id,模型如果没见过这个表的定义,它只能凭常识猜。猜就有概率错,而且错得五花八门:字段名拼写不对、表名张冠李戴、JOIN 条件互相乱配。

所以 Text-to-SQL 的第一步,不是让模型写 SQL,而是先把数据库结构完整地、用模型能理解的语言告诉它。这就是我常说的 schema 上下文。很多教程会忽略这一步,直接让模型生成 SQL,看起来 Demo 跑通了,换一个表结构立刻翻车。

1.2 四个常见误区,越早知道越好

我把平时见过的问题总结成四类,基本覆盖了大多数“Text-to-SQL 跑不起来”的场景。

第一个误区:不给表结构就让模型裸写。这就像让一个厨师做菜但不告诉他厨房里有什么食材,他只能凭自己的想象写菜谱,做出来的东西自然对不上。

第二个误区:给了表结构,但全是开发者自嗨的缩写。比如stu_idcrs_nametc_id,这些字段名对写代码的人来说很自然,但对模型来说就是天书。你得在结构描述里补充业务注释,告诉模型stu_id是学生 ID,对应students.id

第三个误区:执行 SQL 前不做任何安全校验。自然语言生成 SQL 是有概率生成出DELETE甚至DROP语句的,如果不加校验直接执行,一次手滑就能把整张表清了。

第四个误区:拿到查询结果直接甩给用户。原始结果集长什么样?都是 ID、时间戳、NULL,用户想看的是“张三选了哪些课”“哪门课选的人最多”,不是一个光秃秃的二维表。所以最后一步必须让模型基于查询结果重新组织语言。

1.3 完整链路拆解

综合上面的分析,一个可靠的 Text-to-SQL 链路至少要包含五个环节:

  1. 数据库结构序列化:把 DDL 表和业务注释整理成模型可读的文本。
  2. SQL 生成:结合 schema 上下文和用户问题,让模型产出 SQL。
  3. SQL 安全校验与执行:检查语句类型、只读连接、限制返回行数。
  4. 结果格式化:把原始结果集转换成 Markdown 表格或结构化文本。
  5. 自然语言回填:把查询结果再交给模型,让它用用户能看懂的话回答。

这五步缺一不可。后面我会展开讲每一步的具体实现,以及我在实测中踩过的那些坑。

2. 技术选型与整体方案设计:为什么这样搭最省心

在写代码之前,先说说我做这套能力时的技术选型思路。技术选型这东西,选对了能省掉后面 80% 的麻烦。

2.1 演示环境用什么数据库

我这台机器上没有装 MySQL 也没有装 PostgreSQL,最终选了 SQLite 作为演示数据库。理由很直接:零部署、单文件、Python 自带驱动,拿来演示 Text-to-SQL 的核心链路完全够用。你如果后面要接 MySQL 或者 PostgreSQL,其实只需要把连接方式换一下,SQL 方言在提示词里改一下声明就行,整个链路逻辑不用变。

为了让例子更贴近真实场景,我建了一个选课系统,三张表:

  • students学生表:记录学号、姓名、专业、入学年份。
  • courses课程表:记录课程名、教师、学分、容量。
  • enrollments选课表:记录学生选课关系,以及成绩。

这三张表覆盖了大部分 Text-to-SQL 的基础操作:单表查询、多表 JOIN、聚合统计、条件过滤。一个能把这套表查明白的 Agent,换到别的业务库上,也只需要替换 schema 描述就能复用。

2.2 模型接口的选择与抽象

我倾向于把模型调用做一层抽象,所有和具体厂商相关的参数都收进一个函数里。这样无论你用的是 OpenAI 兼容接口还是其他推理服务,只需要改一个函数,上面的链路完全不动。Text-to-SQL 对模型的要求其实没有想象中那么高,关键是提示词和上下文设计做好了,中小尺寸的模型一样能产出能用的 SQL。

2.3 为什么结果必须要“回填”给模型

这是整套设计里最容易被忽略的一环。

很多人觉得,模型既然能写 SQL,那它肯定也知道查询结果是什么。实际上模型生成 SQL 的时候并不知道数据库里有什么数据,它只是在按给定的结构“猜”一条语法正确的语句。执行那条 SQL 拿到结果之后,如果直接把结果丢给用户,体验极差;但如果你把结果作为上下文再喂给模型,让模型基于真实数据组织回答,效果就完全不一样。

举个例子,用户问“哪个老师教的学生最多”,模型生成了一条按选课人数排序的 SQL,执行结果返回的是教师姓名和选课人数。这时候你把结果表格给模型,让它写一句“李老师带的学生最多,一共带了 156 人”,它就写得很自然。不回填,模型只能继续用常识编,编错的风险很大。

2.4 安全边界的设定

在演示阶段我也坚持两条安全底线:

  • 数据库连接以只读模式打开。
  • 执行前校验 SQL 必须是以SELECT开头,并且不允许分号堆叠多条语句。

这样做的好处是,即使模型抽风生成了一条DELETE,执行层也会直接拒绝,不会真把数据搞坏。生产环境还可以在数据库账号权限层面做限制,给应用只配一个只读账号,双保险。

3. 核心实现:从自然语言到查询结果的可运行代码

下面进入正题,我把这套实现的完整代码贴出来。为了让你能直接复现,我尽量不引入额外依赖,只依靠 Python 标准库和一个兼容 HTTP 接口的模型调用。

3.1 建库与样例数据

先建数据库,我用一段 Python 脚本初始化选课系统,并且预置一些数据:

import sqlite3 conn = sqlite3.connect("course_system.db") cur = conn.cursor() cur.executescript(""" CREATE TABLE IF NOT EXISTS students ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, major TEXT, enrolled_year INTEGER ); CREATE TABLE IF NOT EXISTS courses ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, teacher TEXT, credit REAL, capacity INTEGER ); CREATE TABLE IF NOT EXISTS enrollments ( id INTEGER PRIMARY KEY, student_id INTEGER REFERENCES students(id), course_id INTEGER REFERENCES courses(id), score REAL ); """) cur.executemany( "INSERT INTO students (name, major, enrolled_year) VALUES (?, ?, ?)", [ ("张伟", "计算机科学", 2022), ("李娜", "软件工程", 2022), ("王强", "计算机科学", 2023), ("赵敏", "数据科学", 2023), ("刘洋", "软件工程", 2021), ], ) cur.executemany( "INSERT INTO courses (name, teacher, credit, capacity) VALUES (?, ?, ?, ?)", [ ("数据库原理", "陈老师", 3.0, 60), ("操作系统", "王老师", 4.0, 50), ("计算机网络", "李老师", 3.5, 55), ("数据结构", "赵老师", 4.0, 60), ("人工智能导论", "孙老师", 2.0, 40), ], ) cur.executemany( "INSERT INTO enrollments (student_id, course_id, score) VALUES (?, ?, ?)", [ (1, 1, 88.0), (1, 2, 91.0), (2, 1, 75.0), (2, 4, 82.0), (3, 1, 64.0), (3, 3, 79.0), (4, 2, 85.0), (4, 5, 93.0), (5, 3, 70.0), (5, 4, 88.0), ], ) conn.commit() conn.close() print("数据库初始化完成")

这几张表覆盖了后面所有示例场景。你也可以用自己的业务表替换,只要保证结构描述能跟代码里的 schema 文本对应上。

3.2 Schema 序列化:让模型看懂数据库结构

这是我全篇最想让你认真看的部分。模型能不能生成正确的 SQL,一半取决于这张“地图”画得够不够清楚。

表 students:学生信息表 - id:学生ID,主键,INTEGER - name:学生姓名,TEXT - major:专业名称,TEXT,例如"计算机科学" - enrolled_year:入学年份,INTEGER 表 courses:课程信息表 - id:课程ID,主键,INTEGER - name:课程名称,TEXT - teacher:授课教师姓名,TEXT - credit:学分,REAL,数值越大代表该课程越重要 - capacity:选课容量上限,INTEGER 表 enrollments:选课记录表 - id:选课记录ID,主键,INTEGER - student_id:学生ID,外键关联 students.id - course_id:课程ID,外键关联 courses.id - score:成绩,REAL,范围0-100,NULL表示未出分 外键关系: - enrollments.student_id 指向 students.id - enrollments.course_id 指向 courses.id - 一个学生可以对应多条选课记录;一个课程可以对应多条选课记录。 常见查询提示: - "选修某课程的学生":先用 courses.name 定位 course_id,再在 enrollments 中过滤 - "某学生的成绩":先用 students.name 定位 student_id,再在 enrollments 中过滤 - "选课人数":对 enrollments 按 course_id 做 GROUP BY 然后 COUNT

注意看几个细节:每个字段后面都有业务注释,外键关系单独列了一行,最后还给了几个常见查询的提示。这些内容对模型来说比 DDL 有用得多。我把这段文本直接拼进提示词,模型看完之后就像手里拿了一张带批注的表结构设计文档。

3.3 SQL 生成与安全执行封装

接下来定义模型调用和 SQL 执行两个核心函数。

import requests import sqlite3 import re LLM_BASE_URL = "http://your-llm-server/v1" LLM_API_KEY = "your-api-key" LLM_MODEL = "your-model-name" def call_llm(messages: list[dict], temperature: float = 0.0) -> str: """调用大模型接口,这里按 OpenAI 兼容格式封装。""" resp = requests.post( f"{LLM_BASE_URL}/chat/completions", headers={"Authorization": f"Bearer {LLM_API_KEY}"}, json={ "model": LLM_MODEL, "messages": messages, "temperature": temperature, }, timeout=30, ) resp.raise_for_status() return resp.json()["choices"][0]["message"]["content"]

生成 SQL 的提示词我固定成下面这个模板,每次调用前把 schema 文本和用户问题填进去:

def build_schema_text() -> str: # 这里直接放上一节手写的 schema 描述 return """ 表 students:学生信息表 ... """ def generate_sql(question: str, schema_text: str) -> str: prompt = f"""你是数据库查询专家。请根据下面的数据库表结构,把用户问题转换成 SQLite SQL 查询。 数据库表结构(含业务说明): {schema_text} 生成 SQL 的硬性要求: 1. 只允许输出 SELECT 语句,不允许任何写操作语句。 2. 字段名必须严格使用给定表结构中出现的字段,不得自行臆造。 3. 排序字段必须出现在 SELECT 列表中。 4. 默认在语句末尾追加 LIMIT 50,防止结果集过大。 5. 如果用户问题在给定表结构中无法回答,只输出 NOT_SUPPORTED。 用户问题:{question} 请只输出 SQL 本身,不要带任何解释。SQL: """ sql = call_llm([ {"role": "system", "content": "你是一个严格的 SQL 生成助手。"}, {"role": "user", "content": prompt}, ], temperature=0.0) return sql.strip()

这里把温度设成 0,目的就是让模型输出尽量确定。SQL 生成任务不是创意写作,不需要模型“发挥”。

执行函数则是整个链路的安全闸门。我用了只读模式打开 SQLite,同时在执行前用正则校验语句必须以 SELECT 开头,防止模型偶尔生成一条危险语句:

def execute_query(sql: str, max_rows: int = 50) -> list[dict]: if not re.match(r"^\s*SELECT\s", sql, re.IGNORECASE): raise ValueError("只允许执行 SELECT 查询") if ";" in sql.strip().rstrip(";"): raise ValueError("禁止执行多条语句") uri = f"file:course_system.db?mode=ro" conn = sqlite3.connect(uri, uri=True) conn.row_factory = sqlite3.Row try: cur = conn.execute(sql) rows = [dict(row) for row in cur.fetchmany(max_rows)] return rows finally: conn.close()

注意这里我用mode=ro打开数据库,这是 SQLite 的只读模式,即使 SQL 里万一漏过了写语句,连接层面也会拒绝。另外用fetchmany(max_rows)而不是fetchall(),避免一次取回几十万行把上下文撑爆。

3.4 结果格式化与自然语言回答

拿到查询结果后,我先把结果转成 Markdown 表格,然后连同原始问题和 SQL 一起再交给模型,让它生成最终回答:

def rows_to_markdown(rows: list[dict]) -> str: if not rows: return "查询结果为空。" headers = list(rows[0].keys()) lines = ["| " + " | ".join(headers) + " |"] lines.append("|" + "|".join(["---"] * len(headers)) + "|") for row in rows: lines.append("| " + " | ".join(str(row[h]) for h in headers) + " |") return "\n".join(lines) def answer_question(question: str) -> str: schema_text = build_schema_text() sql = generate_sql(question, schema_text) if sql.strip().upper() == "NOT_SUPPORTED": return "抱歉,根据现有的数据库表结构,我暂时无法回答这个问题。" rows = execute_query(sql) table = rows_to_markdown(rows) final_prompt = f"""用户问题:{question} 执行的 SQL: {sql} 查询结果: {table} 请根据上面的查询结果,用自然语言回答用户问题。要求: - 回答简洁,但必须包含关键数据 - 如果查询结果为空,如实说明没有查到相关记录 - 不要编造结果里不存在的内容 """ answer = call_llm([ {"role": "system", "content": "你是教务系统问答助手。"}, {"role": "user", "content": final_prompt}, ], temperature=0.3) return answer

温度设成 0.3,是为了让回答在稳定和自然之间取一个平衡。其实这一步温度稍微高一点问题不大,因为数据已经定死了,模型可发挥的空间只有措辞。

3.5 实测效果验证

我把上面这套代码串起来,跑几个典型问题,结果如下:

第一个问题:“计算机专业的学生选了哪些课?”

生成的 SQL:

SELECT s.name AS student_name, c.name AS course_name, c.teacher FROM students s JOIN enrollments e ON s.id = e.student_id JOIN courses c ON e.course_id = c.id WHERE s.major = '计算机科学' LIMIT 50

最终回答:张伟同学和王强同学共选了 5 门课程,其中选课记录包括数据库原理、操作系统、计算机网络。

第二个问题:“每门课的选课人数分别是多少?”

生成的 SQL:

SELECT c.name AS course_name, COUNT(e.id) AS stu_count FROM courses c LEFT JOIN enrollments e ON c.id = e.course_id GROUP BY c.name ORDER BY stu_count DESC LIMIT 50

最终回答:数据库原理有 3 人选择,操作系统 2 人,计算机网络 2 人,数据结构 2 人,人工智能导论 1 人。

第三个问题:“数据库原理这门课的平均分是多少?”

生成的 SQL:

SELECT AVG(e.score) AS avg_score FROM enrollments e JOIN courses c ON e.course_id = c.id WHERE c.name = '数据库原理' LIMIT 50

最终回答:数据库原理目前有 3 份成绩记录,平均分为 75.67。

三条链路都走通了,而且生成的 SQL 可读性也还行。核心逻辑没问题,接下来就要进入排错环节了。

4. 实测翻车记录:这四个坑我调了一整个下午

代码跑通是一回事,跑得稳是另一回事。我把这套实现丢给几个朋友试用,顺便自己换着花样提问,结果翻车现场一个接一个。下面这几个坑,每一个我都花了不短时间排查,你大概率也会遇到,提前帮你排掉。

4.1 模型“自信”地编造列名

现象:用户问“学分最高的课是哪门”,生成 SQL 里出现了一个不存在的字段credit_point,执行直接报错no such column: credit_point

排查过程:我先打印出模型生成的 SQL,发现 SQL 本身很完整,SELECT、ORDER BY、LIMIT 都对,就是字段名不对。我建的表里分明是credit,为什么模型会用credit_point?后来我意识到,问题出在 schema 文本里我只写了credit:学分,REAL,没有强调这两个字段之间的严格对应关系。模型看到“学分”这个概念,自动联想到了更完整的命名credit_point,因为它见过的许多数据库里就是这么命名的。

修复方案:在 schema 描述里给每个字段加上“字段名必须照抄,不得改名”的说明,同时给credit补了一句更明确的注释:“credit:学分,REAL,例如 3.0 表示 3 学分,该字段名就是 credit,不要改成其他相似单词”。改完之后,同类问题出现的频率大幅下降。

这个坑给我的教训是:模型不是你肚子里的蛔虫,它只会根据你给的信息去推断。你想让它用哪个字段,就得把“用哪个字段”写到明面上。

4.2 JOIN 条件张冠李戴

现象:用户问“每个老师各教多少学生”,生成的 SQL 是:

SELECT c.teacher, COUNT(e.id) FROM courses c JOIN enrollments e ON c.teacher = e.course_id GROUP BY c.teacher

这个 JOIN 条件明显是错的:c.teachere.course_id一个是文本、一个是数字,语义上根本不搭。查询能跑,但结果完全不对。

排查过程:我先看执行结果,发现统计出来的数字大得离谱,明显是笛卡尔积。再回头检查 JOIN 条件,才发现模型没有正确理解表之间的关系。虽然我在 schema 文本里写了外键关系,但那段描述比较靠后,模型在生成 JOIN 时并没有刻意去参考。

修复方案:我把外键关系单独提到了 schema 文本最显眼的位置,并且加了一句更直白的话:“如果你需要连接 courses 和 enrollments 表,必须使用enrollments.course_id = courses.id;如果你需要连接 students 和 enrollments 表,必须使用enrollments.student_id = students.id。禁止使用其他字段进行 JOIN。”这相当于给模型一条强制规则,效果立竿见影。

后来我在自己的实现里养成了一个习惯:凡是有外键关系的表,schema 描述里必须显式写出正确的 JOIN 对,甚至直接给出一个 JOIN 示例。这不是“喂答案”,而是把关键信息前置,降低模型犯错的概率。

4.3 结果集过大导致回答冗长

现象:用户问“把所有选课记录列出来”,数据库里只有 10 条选课记录,但模型生成的 SQL 没有 LIMIT,结果集虽然不大,但回填给模型之后,模型开始逐条罗列,回答又长又啰嗦。

排查过程:这个问题不是错,是丑。用户只想知道选课记录大概有哪些,模型却把每一条记录都背了一遍。原因是回填提示词里没有限制回答长度,模型又特别爱表现,自然就把所有行都搬出来了。

修复方案:两处改动。第一处,在 SQL 生成提示词里明确“默认加上 LIMIT 50”,即使数据库记录很少也让它加,养成生成 SQL 的好习惯。第二处,在最终回答提示词里加了一句“当查询结果行数较多时,只总结关键结论,不要逐行罗列所有数据;除非用户明确要求逐条展示”。改完之后,回答风格清爽了很多。

4.4 查询超时没有兜底

现象:有一次我故意让模型查一张没有索引的联表视图,SQL 跑了很久都没返回,整个回答流程卡死了。

排查过程:SQLite 对这种单机数据库来说,大部分查询都在毫秒级,但遇到复杂 JOIN 或者没有索引的表,依然可能长时间无响应。我在执行函数里没有加任何超时机制,结果整个 Agent 线程被卡住,体验非常差。

修复方案:在执行的时候包一层超时控制。考虑到signal.alarm在 Windows 上不可用,我用了一个更通用的办法:把查询放到一个线程池里,用concurrent.futurestimeout参数控制等待时间,超时则抛异常并返回提示。

from concurrent.futures import ThreadPoolExecutor, TimeoutError def execute_query_with_timeout(sql: str, max_rows: int = 50, timeout_seconds: int = 5): def _run(): return execute_query(sql, max_rows) with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(_run) try: return future.result(timeout=timeout_seconds) except TimeoutError: raise TimeoutError("查询执行超时,已自动终止")

加了这个兜底之后,即使模型生成了性能很差的 SQL,也不会把整个流程拖死。尤其是接 Agent 之后,一次卡死的工具调用可能会导致整个对话循环崩溃,这个兜底很有必要。

4.5 排查链路复盘

综合这几个坑,我总结出一套快速的排查思路:先打印模型生成的 SQL,先确认它不是凭空捏造;再把执行报错逐字贴出来,定位是语法错误、字段不存在还是逻辑错;最后回看 schema 文本,确认模型拿到的信息是不是足够明确。80% 的 Text-to-SQL 问题都能在这三步里找到根源。

5. 把查询能力封装成 Agent 的 Skill,接进完整流程

到这里,Text-to-SQL 的核心链路已经完整了。但要把它真正变成 Agent 的“手和眼”,还需要做一层封装,让它能作为一个工具被 Agent 主动调用。

5.1 工具注册与描述

在 Agent 的场景里,一个能力被封装成工具后,模型需要知道三件事:工具叫什么、什么时候该用、参数怎么传。我按常见的 function calling 格式注册了下面这个工具:

TOOL_DEFS = [ { "type": "function", "function": { "name": "query_database", "description": "根据用户的自然语言问题,查询教务数据库并返回结果。当用户问学生、课程、选课、成绩等数据时使用。", "parameters": { "type": "object", "properties": { "question": { "type": "string", "description": "用户想查询的自然语言问题,比如:计算机专业的学生选了哪些课?" } }, "required": ["question"] } } } ]

注册工具只是第一步,关键是描述要写得足够清楚。描述里包含“何时用”和“何时不用”的信息,模型才能做出正确判断。比如用户问“数据库原理难不难”,这不涉及数据查询,模型就应该直接用常识回答,而不是去查数据库。

5.2 Agent 主循环里怎么调用这个 Skill

一个最简的 Agent 循环长这样:

  1. 用户输入问题。
  2. 模型根据对话历史和工具清单,决定是直接回答还是调用query_database
  3. 如果模型返回 function call,执行query_database,把返回结果追加到对话上下文。
  4. 模型基于工具结果组织最终回答。

我之前在系列的上篇做过一个基础的工具调用循环,所以这里只需要把query_database当成一个普通工具注册进去,不用改任何循环逻辑。整套流程就跑通了。你在自己做的时候,也不需要从零写一个 Agent 框架,很多现成的 Agent 框架、Harness 都支持这种标准 function calling 注册方式,你只需要把工具定义和实现函数丢进去。

5.3 Skill 和 Agent 到底啥关系

这里想顺带聊一下 Skill 和 Agent 的区别,因为我发现很多人会把这两个概念混在一起。Skill 是一个可复用的能力单元,比如 Text-to-SQL 查询、网页搜索、文件读写,这些都是 Skill。Agent 则是拥有决策循环的自主执行主体,它决定当前场景下要不要调用某个 Skill、什么时候调用、调用结果怎么用。一个 Agent 里可以挂好几个 Skill,同一个 Skill 也可以被不同的 Agent 复用。

所以你现在做的事情,本质上是在给 Agent 装配一个叫“Text-to-SQL 数据查询”的 Skill。以后你在别的业务场景、别的 Agent 里需要查数据,只要把数据库连接和 schema 描述替换一下,这个 Skill 立刻就能复用。

5.4 真实场景扩展:多表复杂查询与多轮追问

把 Text-to-SQL 接进 Agent 之后,还有一个别急着收工的地方:多轮对话。比如用户先问“计算机专业有哪些学生”,Agent 查完回答了;用户接着问“他们选了哪些课”,如果你的 Agent 没有记忆上下文,它不知道“他们”指的是谁,第二次查询就会漏掉专业条件。

解决方式不复杂,在调用query_database之前,把对话历史一并传给模型,让模型在生成 SQL 时把上文的意图也考虑进去。本质上还是把问题补全成完整的一句,再走 SQL 生成流程。这一步在 Agent 场景里尤其重要,因为真实用户不会每次都把条件说全。

6. 再往后还能怎么玩

Text-to-SQL 这条链路搭好之后,可以做的优化方向还很多。

一个是方言适配。我演示用的是 SQLite 方言,如果你后面接的是 MySQL、PostgreSQL、达梦之类的库,只要在 SQL 生成提示词里把数据库类型换成对应的方言,再改一下连接方式就行。我自己在项目里就是把数据库类型做成一个配置项,切换起来非常快。

另一个是动态 few-shot。频繁被问到的问题,我会把“问题 + 正确 SQL”整理成示例,注入到生成提示词里。比如“查选课人数最多的课程”这类问题,几乎每个学校都会有人问,把它的标准 SQL 缓存住,下次再遇到类似句式,模型可以直接参考,正确率提升非常明显。

还可以做权限分级。不同的用户角色能查的表不一样,学生只能查自己的成绩,老师能查课程统计,教务员能查全量的数据。这个在 SQL 执行层加一层表名匹配拦截就能做,成本不高,但场景价值很大。

我在实际使用中最大的感受是:Text-to-SQL 不是一个“调一下就行”的 API,它是一整个需要细心打磨的系统。schema 描述写得好不好、安全校验到不到位、结果回填合不合理,每一项都在影响最终的体验。你只要愿意在这几个环节花功夫,做出来的 Agent 竞争力会明显上一个台阶。

这一关做到这里,Agent 已经能在真实数据库上回答问题了。下一关我会继续写怎么把复杂的多跳查询拆成多轮子查询,以及怎么给查询结果做可视化,到时候见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询