
前阵子我参与了一场面向实习生的模拟面试有一道题反复被问到业务方说“帮我拉一下最近30天每个品类的下单用户数”你打算怎么让机器自动完成这件事大多数候选人能把“自然语言转SQL”也就是NL2SQL这个名词说出来但再往下问就卡住了——表结构怎么喂给模型多表怎么JOINSQL报错了怎么办这篇博文就围绕这个模拟面试场景讲清楚我从零构建一个NL2SQL智能体Agent的全过程它要解决什么问题、系统怎么拆、每个模块怎么实现、指标怎么评测、坑在哪里。适合准备Agent方向面试的实习生也适合要落地智能数据分析产品的工程师参考。1. 项目定位与架构设计NL2SQL在Agent面试中怎么答才有含金量1.1 先想清楚NL2SQL到底解决什么问题NL2SQL全称Text-to-SQL核心目标很单纯输入一句自然语言查询输出一条能正确执行的SQL并且跑出来的结果就是用户要的数。看似简单实际拆开全是细节。用户的问法五花八门“近30天”到底含不含今天“下单用户数”是去重用户还是订单数“品类”是按一级类目还是叶子类目统计这些在自然语言里都可能是模糊的而SQL里每一个含糊都会变成数据错误。传统做法是业务提数、数据开发写SQL、来回确认口径。NL2SQL要替代的就是这个环节让非技术用户自助取数。做这个项目时我会先跟听的人强调它的难点从来不是“让大模型学会SQL语法”而是“让系统理解数据库结构、业务口径和用户意图三者的关系”。这句话等于在面试时把自己的思考层级拉高了——你不是在调接口你是在做语义到结构的映射本质上就是在自然语言和结构化数据之间搭一座智能桥梁。1.2 Agent在NL2SQL里的角色不是生成器是编排者单拿大模型直接生成SQL在简单表结构上效果还行但一旦涉及十几张表、几十个字段模型就开始“幻觉列名”。这时候Agent的价值就体现出来了它可以像人一样分步干活。我的方案是把任务拆成Plan→Act→Observe循环。模型先看用户问题再决定调哪些工具查一下表结构、查一下字段注释、看看有没有相似样例根据这些信息生成SQL。生成后不直接返回而是交给数据库执行器去跑如果报错Agent看到错误信息再修正。这才是Agent相对普通提示词工程的本质区别生成结果可以被外部环境验证模型能根据验证结果自我纠错。面试时想突出亮点就要把这条链路讲清楚。如果只说“我用Agent封装了一下”等于没说。要说清楚状态怎么流转、什么条件下进重试、什么条件下放弃这才是考察Agent能力的关键。1.3 整体架构六个关键环节我最后沉淀出的架构是这样的问题解析判断是不是取数请求提取时间、维度、指标等要素。Schema检索从元数据里召回可能相关的表和字段控制上下文长度。查询规划让模型先输出执行计划再写SQL而不是一步到位。SQL生成输出带有JSON结构的SQL草稿。执行校验拿到真实数据库执行捕获异常并与预期结果核对。反馈修正把异常信息拼回Prompt再次生成限制重试次数。这里有一个很反直觉的经验我试用过“直接生成完整SQL”的端到端方案也试用过“先生成执行计划再生成SQL”后者在复杂查询上的准确率明显更高。因为SQL里的JOIN条件、分组粒度、过滤顺序本质上是规划的落地先把规划写清楚模型不容易在细节上走偏。为便于理解我给几个方案做了对比方案是否需要数据库反馈多表能力出错时自愈能力实现成本大模型直接生成SQL否弱无低提示词固定样例否中无低Agent执行反馈是强有中高这个表在面试时很有用它能说明你为什么选更复杂的架构而不是单纯“堆模型”。2. 核心模块拆解从Schema理解到SQL生成的五个关键环节2.1 数据与评测集没有好题库调优就是盲人摸象任何NL2SQL项目第一步都不是写代码而是造数据。我用了一套完全虚构的电商业务库用户表、订单表、订单明细表、商品表、品类表外键关系模拟真实业务。表之间怎么关联要先梳理清楚例如订单表通过user_id关联用户表通过product_id关联商品表商品表通过category_id关联品类表。然后我按难度分三档写问题集基础档单表过滤排序例如“查一下昨天成交订单的总金额”。中等档两表JOIN聚合例如“每个城市的用户最近7天下单次数”。困难档多表关联时间窗口去重计数例如“近30天每个一级品类下有过支付行为的用户数”。每道题配标准SQL和执行结果。注意一个问题评测集要留30%以上不参与迭代专门用来做回归测试否则很容易出现过拟合模型记住题目而不是学会理解自然语言。这一点我在实际项目里吃过亏——之前图方便把训练和评测混在一起指标虚高一上新题就崩。2.2 Schema理解与列映射解决“模型瞎编列名”问题大模型不知道你的库长什么样。如果你在提示词里只写一句“数据库是电商库”它大概率会编出user_name、product_name这种看似合理的字段但实际上库里可能是uname或者pname。解决思路是把Schema变成上下文的一部分而且是结构化的。我用了两招。第一招用检索召回候选表和字段而不是把所有建表语句都塞进Prompt。实现方式很直接把表名、字段名、字段注释用embedding向量化拿用户问题去相似度检索取Top K。第二招给模型提供字段级别的“别名映射”例如“用户”对应uname和u_id“订单金额”对应order_amt而非amount。这个映射表第一版手动维护后来发现可以直接让模型根据DDL和历史问题自动生成候选别名。这一步的效果立竿见影。加上别名映射之前模型生成的SQL大概有30%会引用不存在的列名加上之后这个比例降到5%以下。面试时能把这个过程讲清楚比背诵大模型API参数要加分得多。2.3 SQL生成提示词结构与Few-shot样例选择提示词不是越长越好但关键信息不能少。我最终采用的模板包含四块角色定义、任务说明、Schema信息、输出约束。下面是一个我实际在用的提示词骨架做了脱敏你可以直接拿去改你是一位资深数据分析工程师请根据给定的数据库结构将用户的自然语言查询转换成SQL。数据库表结构如下...此处填充检索引擎返回的建表语句和字段注释 要求 1. 只输出JSON格式为{plan: 你打算先做什么再做什么, sql: 可执行的SQL}。 2. SQL必须是MySQL语法。 3. 涉及时间过滤时统一用order_time字段。 4. 如果用户问题缺少关键信息在plan字段里说明缺什么sql置空。为什么让模型先输出plan因为我在实测中发现让模型输出一条SQL很快但它可能跳过了“判断是否需要JOIN”这一步。先写plan相当于强制推理虽然多花几十毫秒换来的是复杂查询准确率提升明显。Few-shot样例的选择也要动态化。固定几条样例的问题是用户问题涉及“门店维度”时你给的样例全是“用户维度”参考价值很低。所以我改成检索式Few-shot先把历史题目embedding存起来新问题进来时取相似度最高的两到三条作为示例效果比固定样例稳定得多。2.4 执行反馈闭环Agent自纠错的落地方式模型生成的SQL第一次就能完美执行的占比其实不高尤其在多表JOIN和复杂过滤场景。真正让系统可用的是执行反馈闭环。具体做法是这样Agent拿到SQL草稿后直接连数据库执行。如果引擎抛异常捕获到错误信息例如“Unknown column user.name”就把这段错误拼进第二次提示词告诉模型“你上一条SQL报错了错误信息是…请修正”。注意要限制重试次数我通常设两轮。超过两轮还在报同样的错就放弃自动修复返回“需要人工介入”。这个设计让系统有了类似人写SQL时的自查能力。有一次模型生成的SQL在语法上完全正确但查询结果为空。Agent的反馈环节发现返回0行后会主动提示“结果为空可能是过滤条件过严”然后检查是不是把“近30天”理解成“最近一天”了。这种能发现问题而不是傻傻返回空结果的表现才叫Agent否则只是一个SQL生成封装。3. 实操复盘以“近30天各品类下单用户数”为例跑通全流程3.1 问题拆解一句话里藏着哪些SQL要素我模拟面试时最爱用的题目是“近30天每个品类的下单用户数”。这句话看似简单实际覆盖了NL2SQL最常见的四个考点。第一时间窗口“近30天”要用order_time和当前时间做比较边界处理不当会差一天第二层级关系“品类”是多级概念统计口径到底取哪个层级必须先确认第三去重计数“用户数”意味着要对user_id做DISTINCT否则一单多件会把人数虚高第四状态过滤“下单”需要加上订单状态字段比如只要statuspaid不然把未支付订单也算进去就错了。这个拆解过程不是事后才做而是在Agent的plan环节就要让模型先列出来。模型先输出“我要先过滤时间、再过滤支付状态、然后JOIN品类表、最后按品类分组统计去重用户数”后面生成的SQL准确率高很多。3.2 一次完整调用链演示我把流程写成了Python伪代码方便你理解Agent每一步做了什么def nl2sql_agent(user_query, db_conn, schema_store, retriever): # Step 1: schema retrieval candidates retriever.retrieve(user_query, schema_store, top_k3) # Step 2: generate draft SQL prompt build_prompt(user_query, candidates, fewshot_examplesretrieve_fewshots(user_query)) draft llm_call(prompt) # returns {plan: ..., sql: ...} # Step 3: execute and observe for attempt in range(MAX_RETRY): try: result execute_sql(db_conn, draft[sql]) return result, draft[sql] except SQLException as e: fix_prompt build_fix_prompt(user_query, candidates, draft[sql], str(e)) draft llm_call(fix_prompt) return None, manual_intervention这套伪代码可以拆成三层说明。schema_store存的是DDL、字段注释和别名映射retriever用embedding做语义召回execute_sql拿到真实连接执行从而拿到真正反馈。实际部署时我会在execute_sql里加一个超时控制和一个返回行数上限防止误写全表查询把内存打爆。3.3 三个关键SQL细节实战演示第一JOIN和子查询的选择。这道题的“品类”来自分类表而订单表里只有product_id所以要么JOIN商品表再JOIN品类表要么用子查询。实际测下来JOIN性能更好也更利于后续扩展度量字段所以我让Agent默认优先JOIN。第二COUNT(DISTINCT user_id)和COUNT()天壤之别。订单表一行可能对应一个商品用户买三件商品会产生三条明细记录。用COUNT()得到的是商品明细数不是人数。这一步错误非常隐蔽因为SQL不会报错但数字是错的。第三时间边界。不同数据库的日期函数不一样MySQL用DATE_SUB或者DATEDIFF判断ClickHouse则用toDate。实测中最稳的写法是order_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND order_time CURDATE()也就是含今天的过去29天到30天前的窗口。这个细节能体现工程师有没有真跑过SQL而不只是读文档。4. 评测指标与调优手段执行准确率之外还该看什么4.1 评测指标不能只看执行通过率做任何系统都要能量化效果。NL2SQL最常用的指标是执行准确率Execution Accuracy也就是模型生成的SQL跑出来的结果和标准SQL结果是否一致完全一致算对。但它有个毛病可能SQL写法完全不同结果却一致。所以我会同时看逻辑准确率Logical Accuracy比对两棵语法树的语义结构是否等价。实际项目里这两个指标一起看。某次调优时执行准确率从70%涨到85%我一度以为效果很好了后来抽查发现模型学会了一种取巧写法——它不JOIN而是全表扫描再内存过滤结果碰巧对了。这种SQL在数据量大时必炸。加逻辑准确率约束后这类问题就被拦住了。此外还要维护一组分类指标单表查询准确率、多表JOIN准确率、聚合查询准确率。因为整体指标看不出薄弱点而分类指标能直接指导下一步调优方向。4.2 调优三板斧提示词迭代、知识增强、样例选择第一板斧是提示词迭代。从“直接输出SQL”到“先输出计划再输出SQL”我在测试集上测了大概8个百分点的提升。第二板斧是知识增强把业务口径写进上下文。例如“用户数去重user_id数量GMV支付成功的订单金额合计”这些知识模型不会自己推导出来必须显式注入。第三板斧就是前面提到的检索式Few-shot动态选样例比固定样例更稳。这三者的投入产出比从高到低大概是知识增强大于提示词迭代提示词迭代大于堆更多样例。原因是业务口径的歧义是NL2SQL错误的最大来源而大部分初学者会忽略这一点。4.3 多表与方言的专项优化多表场景最大的问题是JOIN关系错误。模型知道要连三张表但不知道该怎么连。我在调优阶段给Schema检索加了一项“外键路径提示”例如告诉模型category_id是连接product和category的键效果立竿见影。方言兼容也很现实。同一个“昨天”在三种数据库里写法完全不一样。我的做法是在Prompt里注入dialect配置再在评测集里分方言跑回归。甚至可以让Agent先确认数据库类型再选择语法模板避免生成一条用户不得不手改的SQL。5. 高频Bug与排查技巧实测中踩过的典型坑5.1 八个典型坑速查现象根因排查与解法SQL里出现不存在的列名模型对Schema记忆靠猜加别名映射缩小候选字段范围多表JOIN漏条件模型不知道表间关联注入外键路径提示GROUP BY粒度不对对“品类”层级理解错在plan环节强化口径确认WHERE和HAVING用错过滤发生在聚合前后分不清提示词里显式说明两者差异时间算错一天边界值处理不一致统一时间窗口模板COUNT没去重忽视“用户数/去重数”在知识增强中加语义规则召回表不全Embedding检索漏掉关键表提高Top K并用LLM二次筛选修正死循环重试逻辑无终止条件设置最大重试次数和错误去重5.2 排查工具与日志追踪这个系统的排错过程高度依赖日志。我建议每一轮都记录trace_id、用户问题、召回的表、生成的SQL、数据库异常、第几次重试、最终是否成功。出现问题时按trace_id串起来看很快能定位到是Schema检索阶段出错还是SQL生成阶段出错还是执行校验阶段出错。我在本地搭过最小版本。首先把日志写到结构化文件不用print调试。其次对每个阶段单独抽指标比如schema召回覆盖率即“标准SQL涉及的表是否都在召回结果里”这个指标能提前发现很多问题。最后是定期回归把测试集和开发集分开每次改完提示词跑一遍全部测试避免改一处坏一处。5.3 避坑心得先窄后宽先稳后快如果让我给想做类似项目的人一条最实用的建议先做窄场景再做宽场景。不要一上来就支持自由问答先从“固定几个指标固定时间范围”做起把链路跑通、评测做好再逐步放开。很多项目挂在“模型总是生成奇怪的SQL”上其实是没给模型划定边界。另一个心得是不要让Agent过度自由。有人喜欢让模型“自由发挥”决定调哪些工具结果模型动不动就查全部表。我的做法是把工具调用限制在固定集合里必须返回结构化结果。这样系统更可控也更接近生产环境的工程约束。我自己的体会是NL2SQL和Agent的组合是入门Agent开发的一个极佳训练场。聊天类Agent的对错难以验证但SQL有一个强校验器——数据库。它跑对了就是对了跑不对就是不对不存在含糊这种确定性对初学者非常友好。这个项目做到最后我最大的收获不是提示词技巧而是理解了外部反馈在Agent闭环中的分量。如果你也在准备Agent实习面试与其背一些概念不如真的把这个流程跑一遍调几个参数看准确率怎么变。下一个值得挑战的方向是怎么把业务口径库做得更完整让Agent在不同业务库之间迁移时有更强的泛化能力。动手跑起来比看十篇文章都管用。