资讯详情 MAC-SQL实战:多智能体协作破解复杂Text-to-SQL查询难题
📅 2026/10/21 14:23:45
Text-to-SQL 这活儿干过的都知道难度从来不在 SQL 语法本身而在于你没法用一个提示词让模型在一个几十张表、上百个字段的数据库里精准定位那张对的表、那几列同时把三层嵌套子查询一次写对。我在做一个内部数据分析助手时被反复毒打过单表查询还行一旦涉及多表 Join 加聚合加时间范围过滤生成结果就开始飘要么表选错要么条件漏掉要么直接报语法错。后来接触到 MAC-SQL 多智能体协作框架思路一下子通了——不再逼一个 Agent 全包全揽而是把读题、选表、拆题、写 SQL、自查拆给不同角色去做每个角色只专注一件事。这篇文章就围绕 MAC-SQL 的设计思路、流水线拆解、工程落地和踩坑记录展开想给业务搭一套可用的 Text-to-SQL 服务的同学可以直接拿来当参考。1. 单一Agent的瓶颈为什么复杂库表查询总在最后一步崩掉1.1 业务库表的十选一难题全量注入与注意力稀释先看一个最普遍的场景。公司的业务库通常有几十张表光订单中心可能就拆了订单主表、订单明细、支付流水、退款记录、物流信息……你要让大模型直接生成 SQL第一步就会面临 schema 膨胀问题。大多数人的第一版做法是把所有表的建表语句拼成一大段作为上下文让大模型自己挑。实际效果却很惨prompt 动不动几千 token模型在处理时注意力被大量无关表名和字段名稀释。比如让模型查华东区每个部门的销售冠军是谁它可能在度量的过程中被商品表、库存表里的字段干扰最终 Join 了错误的表或者把 dept_id 写成 product_id。这不是模型笨是人给的信息结构就有问题——你让一个人在一堆无关材料里找答案他也会被干扰。MAC-SQL 解决这个问题的核心思路是在生成 SQL 之前先做一个专门的选表环节让一个独立 Agent 只干一件事——从全部 schema 里挑出与问题相关的表和字段再给下游。这样写 SQL 的 Agent 看到的上下文是精简过后的小 schema注意力集中错误率自然下降。1.2 复杂问题需要多步推理一步到位必然出错第二个瓶颈是推理链断裂。一个真正的业务问题比如2023 年华东区每个部门中销售额最高的员工是谁拆开看至少有四步通过区域映射表找到华东区对应的员工按员工聚合 2023 年的订单金额关联员工所属部门用窗口函数或嵌套子查询求每个部门的最高值。如果只给一个大模型一个生成 SQL的 prompt它是靠一口气完成这四步的。模型内部确实具备推理能力但一次性输出结果的稳定性很差——任何一步的中间结果偏了最终 SQL 就错。更麻烦的是一旦 SQL 报错或者执行结果不对你没有办法定位是哪一步出了问题。错误是黑盒的排查成本极高。MAC-SQL 的分工方式很直接引入分解器角色先不写 SQL而是把用户问题拆成若干可以独立验证的子问题形成执行计划。每个子问题单独写成 SQL 片段最后再拼装。这就像做菜先列菜单而不是边炒菜边想下一步放什么。1.3 没有反馈闭环的生成是盲写第三个痛点也是很多初版方案彻底翻车的地方模型生成 SQL 之后系统直接返回结果给用户没有任何自查机制。你可能遇到过这种场景——SQL 语法完全正确执行也不报错但查出来的数据明显不对。比如问题里写着2023 年SQL 里写的是 EXTRACT(YEAR FROM order_date) 2023看起来没错但如果库里的 order_date 存的是下单时间而非支付时间业务上要的口径是支付时间结果就完全不是一个意思。这种逻辑正确但口径错误的问题单靠模型自己是发现不了的。MAC-SQL 的做法是加一个精炼器在拿到 SQL 初稿后结合执行反馈比如是否报错、返回行数是否异常回来重新审视 SQL必要时让前置的 Selector 或 Decomposer 重新调整。整个链路不是一条直线走到黑而是带反馈回环的协作流程。这三板斧——专注选表、显式拆解、执行反馈自校正就是 MAC-SQL 多智能体协作框架的出发点。理解了这个动机再看它的架构就很顺了。2. 三个智能体的分工与协作Selector、Decomposer、Refiner 的角色边界2.1 三种角色的职责定义MAC-SQL 的核心由三个智能体组成很多人喜欢把它们类比成项目经理 技术主管 资深开发的组合我实际体验下来还挺贴切。Selector选择器负责 schema 剪枝。输入是数据库全量 schema 和用户问题输出是候选表、候选字段以及筛选理由。它相当于一个懂得读题的同学先把资料范围缩小。Decomposer分解器负责把复杂问题拆成可执行的子问题列表。输入是用户问题、Selector 筛出的相关 schema输出是若干步骤每个步骤都对应一个可独立计算的子查询目标。它不写具体 SQL只写做什么。Refiner精炼器负责生成 SQL 初稿、执行校错、必要时重写。输入是子问题列表和相关 schema输出是最终 SQL。同时它还要负责在收到执行反馈后修正 SQL。下面这个表可以直观看出三者差异角色输入输出核心技能失败代价Selector全量 schema 用户问题相关表/字段/理由信息过滤与相关性判断表选错后面全错Decomposer用户问题 筛选后 schema子问题步骤列表任务拆解与依赖规划步骤不可执行生成跑偏Refiner子问题列表 筛选后 schema可执行 SQL语法生成与执行纠错SQL 报错或口径错误2.2 协作数据流线性主干 条件回退三者协作的主流模式是一条线性主干用户问题先交给 Selector输出相关 schema 后交给 DecomposerDecomposer 输出步骤后由 Refiner 逐步骤生成 SQL 片段并拼装。这是核心骨架跑通之后再看回环部分。回环指的是Refiner 执行 SQL 如果报错或结果异常它可以带着错误信息回到两个地方——一是自己调整 SQL二是如果发现是 schema 支持不够比如遗漏了某个关键表那就把信息返回给 Selector让 Selector 补充表后再重新走一遍分解和生成。这种条件回退在真实工程里非常重要因为业务问题千奇百怪第一次选表重来都是不可能全对的。我见过一些团队做多智能体时把流程设计得特别复杂互相之间反复传递消息结果 token 消耗巨大且响应慢。MAC-SQL 的价值在于它守住了主干的简洁性正常情况下就三级依次执行异常时再触发回退而不是让三个 Agent 全程同步对话。2.3 为什么是选择-分解-精炼这个顺序而不是生成-检查-再生成有人可能会问Selector 确实需要前置但 Decomposer 和 Refiner 为什么不能合并成一个 Agent它俩都属于理解问题并产出结果的阶段。我的实测体会是如果合并模型会偷懒直接跳过拆解步骤去写 SQL。分开之后Decomposer 的输出被强制物化为中间结构等于给推理过程加了一个检查点。一旦最终 SQL 出错你可以回看 Decomposer 的步骤定义判断到底是拆解方向错了还是 SQL 实现错了错误定位成本大大降低。这个设计还有一个隐性好处子问题列表天然就是执行计划Refiner 可以一次只写一个步骤对应的 SQL 片段写完后拼装。每一步的上下文负担小正确率比一次性生成整条 SQL 高得多。这里我强烈建议不要轻易把这三级压缩成两级除非你只处理非常简单的单表查询。3. 一次完整查询的拆解过程从自然语言到 SQL 的流水线走读3.1 一个贯穿案例多表聚合的完整链路为了看得清楚我用一个内部模拟业务库作为案例。库里有五张核心表employees员工表id, name, department_id, hire_datedepartments部门表id, dept_nameorders订单表id, employee_id, order_date, amount, statusregions区域表id, region_nameemployee_region员工区域关联表employee_id, region_id用户提出的问题是2023 年华东区每个部门中销售额最高的员工是谁这是典型的多表 Join 聚合 分组 TopN 问题对单一 Agent 来说已经属于高难度。我们用 MAC-SQL 的完整链路走一遍。3.2 Selector 的筛选输出长什么样Selector 拿到全量 schema 和问题后会分析出与问题相关的表。我的框架里会要求它以 JSON 格式输出方便下游解析{ related_tables: [employees, departments, orders, regions, employee_region], related_columns: { employees: [id, name, department_id], departments: [id, dept_name], orders: [employee_id, order_date, amount, status], regions: [id, region_name], employee_region: [employee_id, region_id] }, reason: 问题需要区分2023年订单、华东区员工、每个部门、销售额最高因此需要关联员工、部门、订单、区域以及员工区域关联五张表。 }注意一个细节Selector 不仅选表还选列。这一步很关键因为如果只选表下游 Refiner 依然要面对每个表的全部字段上下文压缩效果就大打折扣。列级筛选越小下游模型的注意力越集中。比如 orders 表可能有十几列这里只筛出四个关键列意图非常干净。3.3 Decomposer 如何把问题拆成可执行计划拿到筛选后的 schemaDecomposer 开始拆解。它不写 SQL只输出结构化子任务{ steps: [ {step_id: 1, goal: 找到华东区对应的员工ID集合, depends_on: []}, {step_id: 2, goal: 按员工聚合2023年订单金额得到每个员工的年度销售额, depends_on: [1]}, {step_id: 3, goal: 关联员工所属部门得到每个员工的部门归属和销售额, depends_on: [2]}, {step_id: 4, goal: 按部门分组找出每个部门销售额最高的员工, depends_on: [3]} ], join_hint: 员工表经employee_region与区域表关联订单表经employee_id与员工表关联部门表经department_id与员工表关联 }这个步骤列表的价值在于每个步骤都是可以独立验证的中间结果。比如步骤 1我可以单独执行一条 SQL 看看返回的员工 ID 是不是都在华东区内如果不对就不会往后走。对比一次生成整条 SQL 的做法这种分步可验证的特性极大降低了排错难度。3.4 Refiner 的生成链路单步 SQL → 执行反馈 → 修正Refiner 按步骤逐个生成 SQL 片段然后拼装。我们看它最终生成的主查询会长什么样WITH regional_employees AS ( SELECT e.id AS employee_id, e.name, e.department_id FROM employees e JOIN employee_region er ON e.id er.employee_id JOIN regions r ON er.region_id r.id WHERE r.region_name 华东区 ), employee_sales AS ( SELECT o.employee_id, SUM(CASE WHEN o.status ! cancelled THEN o.amount ELSE 0 END) AS total_sales FROM orders o JOIN regional_employees re ON o.employee_id re.employee_id WHERE o.order_date 2023-01-01 AND o.order_date 2024-01-01 GROUP BY o.employee_id ), ranked_sales AS ( SELECT es.employee_id, d.dept_name, es.total_sales, ROW_NUMBER() OVER (PARTITION BY d.dept_name ORDER BY es.total_sales DESC) AS rn FROM employee_sales es JOIN employees e ON es.employee_id e.id JOIN departments d ON e.department_id d.id ) SELECT employee_id, dept_name, total_sales FROM ranked_sales WHERE rn 1;生成之后Refiner 会执行这条 SQL。如果执行成功但返回空集它会检查是不是区域名称写错了、订单日期过滤是否有问题如果报错它会把数据库返回的错误信息一起丢回给模型提示模型重写。这套逻辑完成后才算完成一次完整服务。4. 让协作真正跑起来的关键设计Prompt 构造、上下文预算与自校正机制4.1 每个 Agent 的 Prompt 模板怎么搭很多人在复现多智能体框架时觉得难点在于智能体通信实际上最影响效果的往往是 prompt 模板设计。MAC-SQL 的三个 Agent 各有一套独立的 prompt核心是让每个角色明白自己的职责边界不要越界。Selector 的 prompt 我建议强调三点只做信息过滤、不做任何 SQL 生成、输出必须是 JSON。还要给出如果问题模糊保守优先的规则——不确定的表宁可多选也别漏选因为多选只浪费一点 token漏选则会导致整个 SQL 错误。Decomposer 的 prompt 要强调步骤必须可执行、可验证。很多模型会把步骤拆成分析订单情况这种抽象描述这是不合格的。合格步骤应该是按员工聚合 2023 年订单金额。我在实测中发现给 Decomposer 一两个好的 few-shot 示例比反复强调要有效得多。模型模仿能力很强示例里的风格直接被复刻。Refiner 的 prompt 最关键要求它每一步只生成一个 SQL 片段最后再组装。如果让它一口气生成完整 SQL效果会退回单 Agent 的老路。另外要明确提示它在收到错误信息后的处理策略先判断报错类型语法错误、字段不存在、逻辑异常再决定重写还是回退。4.2 上下文与 token 预算分配不要让后段 Agent 被历史淹没一个容易被忽视的问题是整条链路跑下来如果每一步都把用户原始问题、之前所有 Agent 的输出全部塞给下一个 Agent越到后面上下文越长模型越容易被无关信息干扰。MAC-SQL 的工程化实现里必须做上下文裁剪。我给一个可复用的预算分配策略。假设单次请求总 token 预算为 8k环节Token 预算主要承载内容Selector 输入3k全量 schema 用户问题Selector 输出0.5k筛选后的表/列 JSONDecomposer 输入1k筛选后 schema 用户问题Decomposer 输出1k子问题步骤列表Refiner 输入2k步骤列表 相关列定义Refiner 输出0.5kSQL 片段与最终 SQL这个分配方案的核心思想越到下游输入越精简让最需要精准输出的 Refiner 用最少的上下文。实际落地时你可以根据模型窗口大小等比缩放。4.3 自校正反馈还能怎么加码MAC-SQL 里的 Refiner 自校正不自于一句话而是一套反馈信号。除了最基本的SQL 执行报错信息回传之外我强烈建议加两个反馈源第一个是结果行数校验。先让 Refiner 额外生成一条 COUNT(*) 辅助查询与最终 SQL 一起执行。比如案例里的每个部门 Top1正常的返回行数应该等于部门数。如果返回 200 行明显异常说明分组条件写错了。这种数量级的异常判断模型看一眼就能反应过来。第二个是断言校验。对于用户问题中明确出现的数值条件比如销售额大于 500 万中的 500 万可以让 Refiner 在生成 SQL 前先输出一个断言SQL 中是否包含该阈值的比较条件。如果断言失败直接判定 SQL 错误触发重写。这个方法对数字条件多的金融、销售类查询特别有效。4.4 模型选型与成本权衡三个 Agent 不一定用同一个模型。Selector 和 Decomposer 的任务偏理解 归纳相对简单可以用参数量较小的模型或者同一模型的不同低配版本Refiner 需要较强的编码能力和自我纠错能力应该用能力最强的模型。我测试过一个组合Selector 和 Decomposer 用中等能力的模型Refiner 用能力更强的模型整体效果不降成本却下降了约三成。当然这种搭配依赖具体的模型能力和业务复杂度建议你根据自己的接入方式来评估没有绝对最优解。5. 实测对比与踩坑记录哪些失败模式最值得警惕5.1 同一批数据上的效果对比为了验证多智能体协作的实际增益我在模拟业务库上做了一轮小规模评测。测试集包含 50 条查询分为三类简单单表查询、多表 Join 查询、复杂聚合嵌套查询。对比了三种方案方案简单单表多表 Join复杂聚合嵌套单 Agent 直接生成72%41%22%单 Agent 执行反馈自纠正76%52%31%MAC-SQL 三智能体线性协作81%63%47%MAC-SQL 条件回退闭环84%71%55%这个数据说明两个问题。第一多智能体的增益主要集中在复杂查询上简单查询反而因为链路变长而收益有限甚至有轻微延迟成本。第二只做线性协作还不够接入回退闭环后多表 Join 和复杂聚合的提升幅度最明显。如果业务场景大多是单表查询你确实不需要上完整的 MAC-SQL这是很务实的结论。5.2 失败模式一Selector 漏选关键表最常见的失败原因是 Selector 漏选表。比如用户问每个产品品类的库存周转天数Selector 可能选了产品表、库存表却漏了采购明细表导致下游无法计算周转天数。排查时发现 Decomposer 写出的步骤已经包含了计算采购量这一步但因为核心表没选进来Refiner 只能硬编一个不存在的关联最终 SQL 执行报错。我后来在 Selector 的 prompt 里强制加了一条规则对于包含每个各个按 XX 分组这类表达的问题如果 schema 中存在对应的维度表必须一并选入。加了这条规则后漏选率下降了近一半。这说明 Selector 的设计不能只靠大模型悟性还要把业务中的常见模式沉淀成显式规则。5.3 失败模式二Decomposer 分解出不可执行的步骤第二个高频问题在 Decomposer 输出的步骤过于抽象。例如它写出分析 2023 年华东区的销售情况这种步骤没法直接对应到 SQL 片段。原因在于模型的训练数据里这类步骤通常会出现在文本分析场景而不是 SQL 生成场景。解决办法是给 Decomposer 配置一个步骤质量检查器或者至少在 prompt 里明确每次输出的步骤必须包含具体表名和字段名。我在实际操作中会让步骤描述强制采用动词 对象 条件的句式比如按员工聚合 2023 年的订单金额而不是分析销售情况。这个句式要求写进 prompt 之后Decomposer 的步骤可执行性明显提升。5.4 失败模式三Refiner 的过度修正第三种情况很有意思不是模型不够聪明而是它太聪明了。Refiner 在拿到执行反馈后有时会因为一次报错而大规模重写 SQL把本来正确的部分也改坏了。比如 SQL 本身只是少了左括号模型收到报错后却把整个 JOIN 结构都换了结果语法对了业务逻辑错了。我处理这个问题的办法是给 Refiner 的重写设置边界每次修正时先定位错误发生在哪个子步骤只允许修改对应的 SQL 片段不允许重写其他步骤的产物。同时在生成时限定重写版本号如果连续两次修正后仍然报错就触发回退到 Decomposer重新拆解而不是让 Refiner 无限自旋。加了这两条约束之后过度修正的比例大幅度下降。5.5 调参心得temperature、few-shot 与重试上限最后汇报几个可操作的经验值。Temperature 建议统一设在 0 到 0.2 之间尤其 Decomposer 和 Refiner低温能显著减少随机性输出。Refiner 的 few-shot 示例控制在 2 到 3 个多了会挤占上下文少了风格不稳定。整个链路的重试上限我建议设为 3 次到达上限后把中间产物和原始问题返回给用户侧由人工介入而不是无限烧 token 在模型自旋上。6. 把 MAC-SQL 搬进业务系统工程化落地与二次开发建议6.1 业务 SQL 接口的配套约束框架跑通之后真正落地到业务系统还有不少工程问题。首先是数据库账号的权限边界这一点无论如何强调都不过分。给 Text-to-SQL 服务用的账号必须是最小权限最好数据库级别只开放 SELECT 权限并且通过只读账号连接。其次是查询防护在 Refiner 生成 SQL 后接入一个规则引擎兜底。规则包含禁止 SELECT * 导出全表、必须带 LIMIT 限制返回行数、禁止执行多语句拼接防止注入、对涉及大表全扫的查询做超时熔断。这些规则不属于 MAC-SQL 的核心但少了它们再好的框架上线也让人睡不着觉。6.2 从基准集迁移到自有库的适配点如果你是在标准 Text-to-SQL 基准集上验证过 MAC-SQL再迁移到业务自有库有三个适配点不能跳过。第一schema 表示方式要调整基准集的 schema 通常是单库多表且结构清晰业务库可能有视图、物化视图、分区表这些都要在 schema 描述里体现第二业务字段经常有特殊含义比如 status 字段的不同取值代表什么需要在 schema 描述中附带字段注释Selector 才会正确判断相关性第三用户问题的表达习惯差异很大业务人员会讲上个月卖得最好的货而基准集都是规范表述这需要在 Decomposer 环节增加一个问题改写前置步骤把口语改写为规范查询意图。6.3 多智能体编排的工程实现思路代码实现上不一定要上重量级的多智能体框架。我用最简单的 Python 状态机就能把核心跑起来。伪代码逻辑如下def run_mac_sql(question, full_schema, context): # 第一阶段选表 schema_json selector.run(question, full_schema) # 第二阶段拆解 steps decomposer.run(question, schema_json) # 第三阶段精炼支持回退 for attempt in range(MAX_RETRY): sql refiner.generate(steps, schema_json, context) result, error execute_sql(sql) if error is None and validate(result, steps): return sql, result context.append_error(error or result_anomaly) # 重试耗尽回退到 Decomposer 重新拆解 steps decomposer.run_with_feedback(question, schema_json, context) ...这段代码里有两个小细节值得注意一是 context 对象保存的是历史错误摘要不是原始错误日志避免上下文无限膨胀二是 validate 函数做简单的行数断言校验帮助 Refiner 判断执行成功但结果可能错误的情况。6.4 扩展方向记忆模块、工具调用与人工介入MAC-SQL 的原始框架是基线的实际业务中还可以继续扩展。比较推荐的方向有三个。第一是跨请求记忆。同一个用户在会话里有多个连续问题时直接把每一轮都跑完整链路会让 Decomposer 把同一个库表拆解好几遍。可以把 Selector 的筛选结果和 Decomposer 的步骤列表缓存到会话级后续问题如果涉及同一组表直接复用中间产物。第二是工具调用。在 Decomposer 拆解后可以挂一个工具查询模块自动去取字段注释或者数据库字典里的业务口径说明补充给 Refiner。这个做法的本质是让 Refiner 不只看 schema 名字还看业务定义对口径错误类问题很有效。第三是人机协同。当 Self-Refine 连续两次失败时把中间产物筛选表、拆解步骤、错误列表打包发送到一个人工审核队列。有经验的 DBA 往往一眼就能看出 Schema 选错还是步骤拆错他修正后可以再重新喂给 Refiner。我的实际体会是MAC-SQL 这类多智能体协作框架最值得学习的地方不是某个具体实现细节而是它把一个模型全包的思维转化成了流水线 专业化分工 反馈闭环的工程思维。你不需要完全抄它的实现只要抓住选表前置、拆解显式化、执行反馈这三个核心思想再结合自己业务的表结构特点做适配大概率能做出比单一提示词方案稳定几个档次的 Text-to-SQL 服务。特别是当你被复杂多表查询折磨过之后你会发现让每个智能体只做一件小事并把每一步的可验证性做扎实比任何魔法提示词都可靠得多。