3、问数智能体
真正把用户问题变成查询结果的,是项目中的问数智能体。
这个智能体基于 LangGraph 实现,不是单轮“Prompt + LLM”的简单模式,而是一个可分步骤执行、可观测、可纠错的图结构流程。

如果顺着这张流程图往下看,整个问数智能体本质是一条“先理解问题、再召回信息、最后生成并执行 SQL”的工作流。
为了先抓住主线,也可以把这一整段流程先压缩成 3 个阶段:
- 理解问题 先从用户问题里提取适合检索的关键词,并保留原始问题作为语义兜底。
- 准备 SQL 所需上下文 先粗召回相关字段、指标和值,再把它们整理、补齐、过滤成更适合生成 SQL 的上下文。
- 生成并执行 SQL 在上下文齐备后生成 SQL,再做校验、必要时纠错,最后真正执行。
带着这个“三阶段”视角再往下看,就会更容易理解为什么中间会有“召回、合并、过滤”这几步,它们其实都是在为生成 SQL 做准备。
3.1 第一步:抽取关键词

流程图最上方的第一个节点是“抽取关键词”。
这一步的作用,是先把用户问题中真正对查询有帮助的核心词抽出来,尽量去掉一些无关紧要的表达。比如用户说:
- 帮我统计一下华北地区的销售总额
系统不一定要原封不动拿整句话去做后续检索,而是可以优先提取出更关键的词,例如:华北地区、销售总额。这样做的目的,是让后面的检索更聚焦、更干净。
在实现上,这一步会借助分词和关键词提取工具来完成。当前项目这里使用了 jieba 提供的关键词提取能力。jieba 是一个常用的 Python 中文分词与关键词提取库,它底层使用的是 TF-IDF 思路。
TF-IDF 可以做一个直观理解:它不是简单统计“哪个词出现得多”,而是评估“哪个词在当前这句话里更重要、更有代表性”。一个词如果在当前问题里出现得比较关键,但又不是那种在各种句子里到处都出现的普通词,那么它更有可能被识别为关键词。反过来,像“的”“了”“一下”这类在很多句子里都会反复出现的词,通常就不适合当作关键词。
所以在这个项目里,TF-IDF 的作用可以简单理解为:从用户问题中尽量提炼出真正对数据库查询有帮助的核心词,把那些无关紧要、干扰检索的词先过滤掉,从而让后面的字段召回、指标召回和字段取值检索更准确。
当然,关键词抽取不是百分之百完美的,理论上存在漏掉重要信息的可能。所以实际流程里,也会把原始问题本身一并保留下来,作为一种补充保险。这样做可以避免只依赖关键词而导致信息丢失。
这里需要特别理解一点:提取关键词的意义,在于先把问题中最适合检索的核心词提炼出来,提高后续召回的效率和准确性;而保留原始问题,则是为了避免关键词抽取过程中遗漏语义细节,作为完整语义的兜底信息。也就是说,关键词和原始问题并不是二选一的关系,而是“一个负责聚焦检索,一个负责补充完整语义”。
3.2 第二步:并行召回三类信息
从“抽取关键词”往下,流程图同时分出了三条支路:
- 召回字段信息
- 召回指标信息
- 召回字段取值
这三步之间没有严格先后依赖关系,所以它们可以并行执行。并行的好处是,一方面节省时间,另一方面也符合实际查询逻辑,因为一个问题往往同时涉及字段、指标和字段值。
这里可以把这一步看作一次粗召回。也就是说,这一步的目标不是立刻得到最终答案,而是先尽量把“可能相关的材料”多找一些出来,宁可先宽一点,也不要一开始就漏掉真正相关的信息。
- 召回字段信息

这一步主要去向量索引中检索与问题最相关的字段。比如问题里有“地区”“销售总额”这类表达,系统就要尽量把 region_name、order_amount 等可能相关的字段找出来。
为了提高召回效果,系统并不只依赖最初抽出来的关键词,还会借助大模型进一步做“关键词扩展”。也就是说,系统会让模型从“我要查字段信息”这个目标出发,补充一些更适合用于字段检索的词,例如把“销售总额”扩展成“销售金额”之类的表达。这样做的本质,是为了提高字段召回率。
- 召回指标信息

这一步和字段召回类似,也是走向量检索,只不过目标换成了 metric_info 中的业务指标。比如用户问“销售总额”,系统不仅要想到具体字段 order_amount,也要有机会召回 GMV 这样的指标定义。
这里同样会做关键词扩展。因为用户问题里的说法,未必和指标名一模一样。通过扩展关键词,系统能更容易把“问题的说法”和“元数据里的指标定义”对上。
- 召回字段取值

这一步主要走全文检索,目标是找到问题中提到的那些真实字段取值。比如:华北地区;黄金会员;手机数码。
这些信息不是字段名,而是字段里存的值,所以要去全文索引里查。
从理想方案来看,字段取值检索其实做成“全文检索 + 向量检索”的混合检索会更好,因为有些场景可能存在别称、拼音、不同书写方式等问题。但在当前项目中,为了先把主流程做清楚,这一步做了简化,主要采用的是全文检索。后续如果需要继续优化,再往这一步叠加混合检索能力会比较自然。
3.3 第三步:合并召回信息

三路粗召回完成后,系统拿到的是三份相对分散的结果:
- 一批字段信息
- 一批指标信息
- 一批字段取值
但这些信息此时还不能直接丢给大模型。因为它们是散的、碎的、不成体系的,而大模型在生成 SQL 时更需要的是结构化、成组的信息。
所以流程图的下一步是“合并召回信息”。
这一步的核心目标,是把零散的召回结果整理成两份更适合后续使用的上下文:
- 表信息及其字段信息
- 指标信息及其相关字段说明
这里要特别理解几个关键动作:
- 召回到字段之后,可以根据字段里的
table_id把字段重新按表归组,整理成“某张表有哪些可用字段”的结构。 - 召回到指标之后,要把指标相关字段也考虑进来,因为一个指标往往依赖一个或多个字段。
- 召回到字段取值之后,要把这些值重新挂回到它所属字段上,让字段的
examples或候选取值信息更完整。
这一步可以看作:前面的召回是在“捞零件”,而这一步是在“拼装上下文”。如果说 3.2 是先把相关材料尽量找全,那么 3.3 做的就是把这些材料按 SQL 生成真正需要的形态组织起来。
这里还需要特别理解一种很重要的结果形态:当这些信息被整理好之后,表信息通常会被组织成类似 yaml/table_infos.yaml 这样的结构。也就是说,系统并不是把“零散字段列表”直接交给大模型,而是把它们先整理成“按表分组、表下挂字段”的层次化信息。
这种结构的好处是非常直观的:
- 大模型能一眼看出有哪些表
- 每张表是什么角色,是事实表还是维度表
- 每张表下面有哪些字段
- 每个字段的类型、角色、示例值、描述和别名分别是什么
结合当前这个示例,还可以进一步理解“合并召回信息”为什么不是简单拼接:
- 同一张表下的字段会被重新归组,而不是继续散落在一个列表里。
- 字段的
examples、description、alias等信息也会一起保留下来,方便后续生成 SQL 时同时理解“字段叫什么、字段里可能有什么值、用户还可能怎么称呼它”。 fact_order这种事实表和dim_region这种维度表会同时出现,因为大模型后面不仅要知道统计哪个指标,还要知道从哪个维度去分析。- 像
customer_id、date_id、product_id、region_id这类外键字段也会被一起保留,因为它们在后续多表关联时经常是必需的。
除了表信息,合并之后的指标信息也会被整理成一个单独的层次化结果文件,通常类似 yaml/metric_infos.yaml 这样的结构。
结合这个示例,可以更容易理解“合并召回信息”阶段的指标整理逻辑:
- 系统不会只保留一个孤立的指标名,而是会把指标的
description、relevant_columns、alias一并整理出来。 description的作用,是告诉大模型这个指标在业务上到底是什么意思,避免模型只看到缩写却不知道含义。relevant_columns会把指标和具体字段关联起来,例如GMV对应fact_order.order_amount,这样后续生成 SQL 时更容易知道应该聚合哪个字段。alias会保留用户常见说法,例如“成交总额”“订单总额”“平均单价”,这样系统在理解自然语言问题时就不会只依赖指标原名。
也就是说,合并召回信息这一步的真正目标,不只是“把结果拼起来”,而是把表信息和指标信息都整理成一种适合大模型阅读和理解的层次化上下文。这里展示 yaml/table_infos.yaml 和 yaml/metric_infos.yaml,本质是为了说明这一点。
3.4 第四步:过滤指标信息与过滤表格信息
即使经过合并,当前拿到的信息也不一定就是最终最合适的结果。因为前一步的目标是“先组织起来”,而不是“已经足够精确”。通常这时还会存在两类问题:
- 有些字段、表、指标虽然被召回了,但和当前问题关系并不强
- 有些字段是为了补全主外键关系被带进来的,但本身并不是问题关注的重点
因此,流程图接下来又分成两步筛选:
- 过滤指标信息
- 过滤表格信息

这两步本质上都是一次精筛。系统会借助大模型,结合用户原始问题,对召回结果再做一次语义判断,把无关项、冗余项剔除掉,只保留和当前查询意图最相关的表、字段和指标。
这一步非常重要,因为前面的召回更像是“宁可多召回一些,也不要漏太多”,而这里则是把“多出来的部分”再清洗一遍。你可以把这一段连起来记成:先粗召回,再结构化整理,最后精筛保留。
过滤完成之后,指标信息也会被整理成一个更精简的结果文件。例如,当前项目中的 yaml/filtered_metric_infos.yaml,就是“过滤指标信息”这一步的示例结果。
结合这个示例,可以更容易理解“过滤指标信息”这一步主要做了什么:
- 它不会把前面召回到的所有指标定义全部保留下来,而是只保留和当前问题真正相关的指标。
- 如果当前问题是在统计“销售总额”,那么最后保留下来的很可能就是
GMV这样的核心指标。 description会告诉大模型这个指标的业务含义,避免模型只认识名字、不理解定义。relevant_columns会把这个指标和具体字段关联起来,例如这里的fact_order.order_amount,这样后续生成 SQL 时就更容易定位到应该聚合哪个字段。alias则保留了“成交总额”“订单总额”这类不同说法,方便系统把用户问题里的表达和真实指标定义对齐。
另外还有一个工程细节值得注意:有些主键、外键字段在向量召回时并不容易自然被召回出来,但在真正写 SQL 时,多表关联又经常离不开它们。所以在实现里,系统还会通过元数据库把必要的主外键补回来,再在过滤阶段统一处理。也就是说,过滤阶段不是单纯做“删减”,它还会顺手保证真正生成 SQL 所需的关键结构没有丢。

过滤完成之后,表格信息通常会被整理成一个更精简的结果文件。例如,当前项目中的 yaml/filtered_table_infos.yaml,就是“过滤表格信息”这一步的示例结果。
结合这个示例,可以更容易理解“过滤表格信息”这一步主要做了什么:
- 它不会把前面召回到的所有表和所有字段原封不动保留下来,而是只保留当前问题真正需要的那一部分。
- 如果当前问题和“地区销售额”相关,那么最终可能只留下
fact_order和dim_region这两张核心表。 order_amount和region_name这种和问题直接相关的字段会被保留下来。region_id虽然不是用户问题里直接提到的字段,但因为它承担了主外键关联作用,所以也会一并保留。
也就是说,过滤表格信息这一步的目标,不只是“删掉无关内容”,而是把真正生成 SQL 必需的表结构上下文保留下来,并整理成更适合后续使用的 YAML 结构。到这一步为止,系统才算真正得到了“既相关、又完整、又能拿去生成 SQL”的上下文材料。
3.5 第五步:增加额外上下文

过滤之后,系统已经拿到了比较干净的表信息、字段信息和指标信息,但在真正生成 SQL 之前,流程图里还有一步“增加额外上下文”。这一步做的事情是:在“业务上下文”已经齐备之后,再把“环境上下文”补齐。
这一步补充的,主要是一些不直接属于表结构,但会影响 SQL 正确性的环境信息,例如:
- 当前时间
- 当前数据库类型
- 当前数据库版本
这一步看起来不起眼,其实很实用。例如,用户如果问“统计去年的销售总额”或者“统计上个月的销售额”,那模型必须知道“当前时间”是什么,才有可能正确推导出时间范围。
再比如,不同数据库或数据仓库引擎的 SQL 方言并不完全一致。即使整体都是 SQL,它们在函数、语法细节、版本支持上也可能存在差异。所以如果能够提前把数据库类型和版本告诉大模型,它生成的 SQL 往往会更贴近实际执行环境。
3.6 第六步:生成 SQL

到这一步,系统终于具备了生成 SQL 所需的主要上下文:
- 用户原始问题
- 经过筛选后的表信息、字段信息和相关指标说明
- 可能需要的字段取值,以及当前时间、数据库环境等补充上下文
这时再把这些信息一起交给大模型,才算真正具备了较高质量生成 SQL 的基础。
也可以说,前面那么多步骤,最终都是在为这一刻服务:让模型在“知道该查什么、知道能用哪些表字段、知道值该怎么写、知道环境是什么”的前提下生成 SQL。
3.7 第七步:校验 SQL
SQL 生成出来之后,流程图并没有立刻执行,而是先进入“校验 SQL”。这一步的目的,是在真正执行之前先做一次预检查,尽早发现语法问题或明显错误。
在当前项目里,这一步主要通过 EXPLAIN 来完成。它不会真正执行 SQL 查询本身,而是先让数据库分析这条 SQL。如果 SQL 语法没有问题,通常就能顺利通过;如果 SQL 本身存在错误,数据库就会直接报错。
所以这一步可以看作:先不急着查数,先看看这条 SQL 至少能不能被数据库接受。
3.8 第八步:校正 SQL
如果“校验 SQL”通过,流程图就会直接往下执行。如果校验失败,流程图右侧会走到“校正 SQL”。
这一步会把前面生成 SQL 时用到的上下文,再加上数据库返回的报错信息,一起重新交给大模型,让它基于错误信息去修正这条 SQL。
从更完整的工程设计看,校正后的 SQL 理论上还可以再回到校验步骤做一次循环校验,这样会更稳。但当前项目在这里做了简化:校正之后直接进入执行阶段,没有继续做多轮循环,以避免流程过于复杂。
3.9 第九步:执行 SQL
当 SQL 通过校验,或者经过校正后准备执行时,流程图的最后一步就是“执行 SQL”。这一步会真正把 SQL 发给数据库执行,得到结果后再返回给用户。到这里,整条问数链路才算完整闭环。
3.10 本节小结
如果用更凝练的话来概括这张图,可以这样理解:
- 先理解问题
- 再并行召回字段、指标和值
- 把召回结果整合并筛干净
- 补齐环境上下文
- 生成 SQL
- 校验、必要时纠正
- 最后执行并返回结果
如果用更偏工程化的表述,这个智能体的核心工作其实可以浓缩成一句话:根据用户问题,召回相关字段、字段取值和指标信息,再让大模型基于这些信息生成 SQL。
这样的设计有几个明显好处:
- 每一步职责清晰,便于理解与维护
- 过程可观察,方便前端实时展示智能体执行状态
- 出错时可以在局部节点修正,因此整体上比单次大模型生成更稳定,也更适合真实工程场景
你可以把它理解成:我们不是把“大模型”直接当成最终答案,而是把它放进一条经过设计的工作流里,让它在可控的上下文和规则下完成问数任务。
本章小结:
- 「电商问数」的核心不是“直接把问题交给大模型”,而是“先构建元数据知识库,再让智能体围绕问题调用这些知识”。
- 元数据知识库由元数据库、向量索引、全文索引三部分组成,分别负责结构化存储、语义召回和字段取值匹配。
- 问数智能体不会一次性生成 SQL,而是经过关键词抽取、多路召回、合并、过滤、补充上下文、生成、校验、纠错、执行等步骤。
换个更整体的角度来看,「电商问数」始终围绕两大模块展开:一部分负责把表、字段、指标和值整理成可检索的元数据知识库,另一部分负责围绕用户问题调用这些知识,最终生成并执行 SQL。到这里,你已经对「电商问数」项目的业务背景、数仓基础和整体架构有了一个完整的认识。接下来的章节,我们就会正式进入开发环境、基础服务和代码实现部分,一步一步把这套系统搭建出来。