直接开场说重点。我之前写过LCODER系列的AI Agent开发第一篇把问数项目的整体架构和Agent能力边界划清楚了这篇接着往里走把基础设施层的每一块地基填实。问数项目说到底就是让业务人员用大白话查数但“大白话”这三个字背后链路比你想象的长得多。从自然语言到SQL从SQL到数据结果从结果到可读回答每一步都依赖下面这层基础设施撑住。基础设施搭建这个词听起来不性感但往往是项目后面返工率最高的地方。我见过太多团队一上来就冲LLM和Agent编排先跑通一条demo再说结果模型一换、数据源一加、权限一变整个链路重新翻修。这篇就把我踩过的坑、验证过的方案、以及最终稳定跑起来的一套底座设计全部摊开讲项目还没开工的可以先当checklist用已经在做Agent项目的对照着查漏补缺。1. 问数Agent到底是什么先对齐认知再动手1.1 “问数”的本质是把口语翻译成可执行的查询链路问数项目里很多人把重点放反了以为核心是“让Agent学会写SQL”。实际上问数的核心是让自然语言和企业数据之间建立一条语义对齐的通道。业务人员说的“上个月华东区的退货率怎么样”这句话能不能被准确映射到某个事实表、某些维度字段、某种聚合逻辑这才是成败关键。我在设计中把问数Agent定位成三层结构。最上层是交互层负责理解用户意图、澄清歧义中间层是语义层负责把意图映射到指标、维度和表最底层是执行层负责生成SQL、调度执行、返回结果。基础设施搭建就是把中间层和最底层的“路”先修好。很多人会觉得语义层用大模型做就行RAG召回一下相关表结构丢给LLM生成SQL这部分确实能跑通demo。但真实场景里用户不会说“从orders表里按region字段等于华东、date字段在2024年1月1日到1月31日之间对returned_flag等于1的记录计数再除以总订单数”。用户只会说“退货率”你指望模型自己脑补出这一整套映射逻辑基本等于赌命。1.2 基础设施不等于装几个组件基础设施搭建这个阶段容易犯的错误是把“基础设施”理解成“把Mysql、Redis、向量库、LLM API都部署好就算完了”。这不叫基础设施这只是环境初始化。在我理解里问数项目的基础设施应该包含四层能力数据连接层统一管理各类数据源连接、元数据采集、数据字典维护让Agent有一个稳定、可理解的数据视图。语义映射层把业务术语、指标定义、维度取值统一沉淀下来形成一套机器可读的语义层配置这是Agent理解业务的关键知识库。模型与提示词管理统一管理大模型接入、prompt模板、Few-shot样本、模型切换策略避免prompt写在代码里散落各处。执行与安全层负责SQL生成后的校验、权限管控、查询超时、结果集大小限制等保护机制确保Agent不会把生产库拖垮。这四层就是这篇要搭建的主体。没有这四层Agent只会在玩具demo里打转一旦接到真实业务数据就直接失控。我在LCODER系列第一篇聊过“Agent不是模型而是系统”这件事基础设施搭建就是把这个系统骨架立起来。2. 基础设施选型为什么我放弃了一站式框架2.1 现成框架的问题不止是定制化难市面上做Agent基础设施的框架不少从LangChain、LlamaIndex到各类DataAI一体化的平台都有。说实话问数项目用现成框架起步确实快但一旦进入生产环境问题就会排队出现。第一个问题是语义层的缺失。通用Agent框架默认你喂给模型的上下文就是模型需要的上下文但问数场景中“字段注释写得稀烂”、“同一个指标在两张表里口径不一致”才是常态。你丢一堆表结构给LLM它生成SQL的时候根本不知道应该引用哪张表、哪个字段更别说默认过滤条件、权限控制这些隐含约束。第二个问题是生命周期管理。问数项目跑起来之后表结构会变、指标口径会调、权限策略会改。如果你把语义层散落在prompt里、把连接串写在代码里、把权限逻辑挂在应用层后期每一次变更都是一次小规模重构。基础设施搭建要解决的恰恰是这个“变”的问题。所以我最终选了轻量化自研路线。底层用LangGraph做Agent编排这个库对流程控制的支持比LangChain更灵活语义层完全自建数据层通过统一元数据服务管理。当时还有个备选方案是直接用开源的语义层项目但评估下来那些项目大多面向BI工具对Agent场景的适配度不够与其改别人的轮子不如自己造一个更贴合需求的窄轮子。2.2 核心组件清单与选型理由基础设施层我用到的核心组件如下每个组件选择的理由我都会备注清楚组件选型选型理由Agent编排框架LangGraph支持有向图编排能精细控制多轮追问、分支判断、异常恢复比链式调用更贴近真实问答场景大模型接入OpenAI兼容接口本地部署备用生产环境用云API效果最稳本地模型做降级和敏感数据处理向量存储Milvus元数据量大、需支持过滤查询和混合检索Milvus在工程成熟度上更靠谱关系型元数据库MySQL存语义配置、指标定义、权限策略等结构化数据够用且团队熟悉数据源连接层自研轻量连接器 JDBC/HTTP适配不同数据源类型差异化处理同时保证连接池和超时控制统一管理Embedding服务BGE-M3系列中文语义理解效果好、向量维度适中综合性价比高于OpenAI的Embedding任务队列Redis 自研异步任务SQL执行可能很慢不能让请求一直占着HTTP连接必须走异步化这套选型不是一步到位的。第一版我甚至试过用FastAPI直接接LLM把表结构塞进system prompt里就开干效果也能看但每张表字段一多上下文就爆。后来从第二版才开始引入元数据服务和语义层整个稳定性才上来。选基础设施组件永远是“先跑通再重构成熟”但同时要有预判意识别把路走死。2.3 关于向量库的执念与反例在建基础设施的时候我在向量库选择上纠结了很久。第一次搭建用的方案是把所有表结构、字段注释、历史问答对全部塞进向量库然后靠相似度检索召回——这个思路在demo里表现非常好演示的时候领导眼睛都亮了。但一接真实数据就露馅了。我们有个业务库有800多张表、几千个字段注释质量参差不齐很多字段名是拼音缩写。向量检索召回的top10看起来“语义相近”实际完全不是业务想要的“表A.字段B”。后来我调整了思路向量检索只用来召回“候选集”真正的精准匹配靠语义层的规则映射和指标字典。两者是个协作关系不是替代关系。这个经验直接影响基础设施搭建方案。所以我在设计上把“用于检索的知识”和“用于生成的语义配置”分开存储——向量库里存的是文档碎片和历史问答样本MySQL里存的是严谨的指标定义和表关系。两条腿走路比单靠向量库一条腿稳很多。3. 数据链路与元数据服务搭建3.1 元数据服务是整个问数Agent的“眼”如果把问数Agent拟人化元数据服务就是它的视力。业务人员问“这个月的销售额”Agent能不能找到正确的表、正确的字段、正确的聚合逻辑全靠元数据服务供数。我搭建元数据服务的时候分了三个层次数据源元数据自动采集表结构、字段类型、注释、主外键关系、行数估算。这是最原始的一层跟业务无关纯粹描述“数据库里有什么”。业务元数据在原始表结构之上增加业务语义描述。比如“orders表.status字段”要补充“status1表示已支付status2表示已退款status3表示已取消”这类业务含义。指标层元数据把常用指标抽象出来每个指标绑定计算公式和可用维度。比如“销售额SUM(amount) WHERE status1”并且允许按日期、区域、渠道维度拆分。这三层元数据一层比一层接近业务语言也正是从“数据库物理结构”到“Agent需要的逻辑模型”的必经之路。3.2 自动采集还是人工维护两条腿走路元数据采集如果全自动会被脏数据坑哭。生产库里的字段注释经常是空的或者写了“备注1”“备注2”这种废信息。但如果全人工维护几百张表能维护到崩溃。我的方案是自动采集人工校准结合。自动采集负责把表结构、索引、行数、外键这些物理信息抓全业务含义部分用规则引擎自动打标比如字段名包含“amount”“price”“fee”自动标记为金额类包含“time”“date”“day”自动标记为时间类。初步标记完之后业务人员只需在管理界面上做二次确认和补充这能节省大量人力。还有个细节容易被忽略元数据采集要支持定时增量更新。因为数据库表结构不是一成不变的业务迭代会加字段、改注释。我当时做了一个每日凌晨跑一次的同步任务把结构变更记录到变更日志里再推送给管理员确认。没有这个机制后面Agent生成SQL的准确率会随着表结构漂移越来越差。3.3 连接层该做和不该做的事连接层负责按需从各个数据源拉取真实数据执行SQL。这里最容易踩的坑是连接层要不要顺便做SQL校验、做过滤、做权限限制我的答案是不要在连接层做业务限制。连接层只做三件事——获取连接、执行SQL、返回结果集。至于SQL是否安全、是否越权、是否超时这些应该由上层Agent编排层来决策和拦截。原因很简单连接层做太多事会导致不同租户、不同场景的差异化逻辑互相纠缠后期没法维护。连接层真正需要做扎实的是连接池治理和超时控制。我们的生产环境同时接了MySQL、ClickHouse和几个HTTP数据API每种数据源的超时表现完全不一样。MySQL超过30秒的查询就该杀ClickHouse可以放长到60秒HTTP接口需要单独配置重试策略。还有一个很关键的点所有通过Agent生成并执行的SQL必须强制开启只读事务禁止任何写操作。这是安全底线哪怕内部系统也不例外。4. 语义层构建让Agent真正“看懂”业务4.1 语义层不是一个prompt是一套配置体系很多问数项目失败在把语义层等同于“写一个详细的system prompt”。Prompt确实能承载一部分语义信息比如告诉模型“用户说的销售额指SUM(amount)字段”但这种方式存在两个致命问题。第一prompt的长度有限你不可能把几百个指标定义全部塞进去。第二不同用户、不同场景对同一个业务词的语义可能是不同的。销售部说“成交额”和财务部说“成交额”可能是两个口径一个prompt怎么兼顾所以我把语义层做成了一套可配置、可路由、可动态加载的体系。底层是MySQL存的指标字典每个指标记录包含指标名称、口径描述、计算公式、有效期、关联表、默认维度、授权角色等字段。当用户提问时Agent先通过LLM做意图识别粗粗判断用户在问哪个主题域再去加载对应主题域下的指标配置合并成context再调用生成SQL。这样做的好处是每个指标配置短小精悍上下文占用很小查询时只加载相关指标既省token又减少干扰。代价是需要花人力梳理指标字典——但这件事一旦做完后续Agent准确率提升非常明显属于投入产出比极高的基础设施投资。4.2 维度与指标的抽象策略维度是另一个容易失控的地方。同一个“区域”在不同表里可能叫region、area_code、zone_name值也可能是“华东”“EAST”“01”这种五花八门的编码。如果语义层不做维度归一化LLM生成SQL的时候就会随机选一个字段结果自然对不齐。我的抽象策略是定义一套逻辑维度和物理字段的映射关系。逻辑维度由业务部门定义比如“区域”就是一个逻辑维度物理字段是各个表中与逻辑维度存在映射关系的具体字段。在Agent生成SQL时优先匹配当前表中有映射的物理字段匹配不到再寻求人工澄清。维度归一化建议配合字典表做。比如区域字段的编码对照关系维护一张映射表Agent生成SQL时会自动关联翻译把用户说的“华东”翻译成数据库里的区域编码。这个能力非常值得投入因为维度是问数里最高频的限定条件也是出错率最高的环节。4.3 指标冲突检测一个容易被低估的环节同时加载多个指标配置时可能发生冲突。举个例子用户问“华东区销售额和华北区销售额的差”系统需要识别出“销售额”指标被用了两次只是维度值不同。如果语义层不做去重合并Agent可能会把两个指标当成不同实体生成出的SQL靠外连接把数据关联起来结果就是笛卡尔积爆炸。我的解决方案是在指标配置里记录指标的“身份指纹”即指标名聚合方式金额单位默认业务时段维度。同一个指纹的指标不管出现几次都只生成一份SQL片段然后按不同维度值做条件拆分。这个小设计看起来不起眼但能避免大量生成SQL时的逻辑紊乱。指标冲突检测我建议做成单独的服务接口Agent在组织SQL前先做一次预检查。虽然会增加一次调用链路但换来的是SQL生成质量的稳定性这个延迟值得花。5. 知识库与动态Schema构建5.1 为什么需要动态Schema而不是静态表结构普通的数据应用连数据库通常直接查information_schema或者硬编码表结构。问数Agent做不到这点因为Agent面对的是一堆“看似能查但不知道上下文”的数据表。直接把所有表结构塞给LLM上下文窗口会爆炸只塞部分表结构又可能缺失关键关联信息。动态Schema解决的是“按需加载按场景组装Schema”的问题。具体做法是在解析完用户问题之后先做一次路由判断缩小到可能涉及的主题域和物理表范围然后只把这些表的Schema加载出来再结合字段注释、样例值生成“可用的Schema描述”。我当时为这个环节设计了一个多级路由先由LLM根据用户问题判断主题域比如“订单域”“用户域”“商品域”再通过向量检索在主题域内召回相关表和字段最后用规则过滤掉敏感字段和无效字段再交给生成SQL的环节。这套流程跑通后生成SQL的准确率提升了一大截。原因并不神秘——LLM在少数高度相关字段里选字段远比在几百张表的全量字段里瞎猜更靠谱。5.2 样例值驱动的Schema增强Schema增强是让生成SQL更准确的一个实用技巧。把字段注释丢给模型模型只能知道这是“金额”“日期”但不知道金额字段里存的是“元”还是“万元”日期字段是“下单时间”还是“支付时间”。我的做法是在Schema描述中注入样例值。比如Region字段样例值会显示“华东、华北、华南”OrderDate字段样例值显示“2024-03-15 14:22:10”。这些样例值不占用太多token但对消除模型误解有奇效。抽取样例值需要小心两个点一是抽取值要覆盖常见场景不能是极端的脏数据二是敏感字段不能随便抽样例。比如用户的手机号、身份证号这类字段要么脱敏后展示要么直接禁止作为样例。我们当时是在元数据服务里为每个字段配置了“是否可用于样例展示”的标记位建表时人工确认一遍从源头挡住敏感信息泄漏。5.3 上下文压缩让LLM只看到该看的另一个实践中很重要的问题是上下文浪费。每次查询都把表结构、指标定义、历史样本全部带上调用成本高不说信息过多还会干扰判断。后来我引入了一个上下文压缩层在真正调用生成SQL的LLM之前把元数据、语义配置、路由结果、历史相似问答全部整理成一页精炼的指令页。这个“指令页”包含五块内容当前用户在问什么重写后的标准问句。候选物理表的精简Schema字段名、类型、关键注释、样例值。相关指标定义的简述名称、口径、计算公式。统一过滤条件比如默认只查最近一年、默认排除测试数据。Few-shot示例在相似问题中选1-3条历史问答对。这个东西直接决定生成SQL的质量。一开始我嫌麻烦没做结果LLM每次生成SQL都要“猜”用户的隐藏约束后来花费大量时间在SQL纠错上。把上下文做精之后这个情况显著改善我强烈建议你在基础设施阶段就预留这个组件的位子。6. 权限模型与安全执行设计6.1 数据权限不能只靠“提示词约束”问数Agent最危险的地方在于一旦用户绕过UI直接调API他就可以让Agent执行任意SQL。如果底层不做强制校验仅靠提示词里写的“你是一个数据助手只能查询有权限的数据”在真实攻击者面前就是一层纸。我的权限模型设计分三层应用层权限、数据源层权限和数据粒度权限。应用层权限决定“谁能用问数功能”简单说就是登录用户角色判断。数据源层权限决定“这个用户连哪个数据源”我们通过数据源标签和用户组映射来控制。数据粒度权限是最关键的它控制“用户查询哪些行、哪些列”。比如普通销售只能看自己负责区域的订单数据不能看全公司数据某些敏感字段如成本价、毛利普通员工查询时会被直接脱敏或拒绝返回。这三层权限的校验第一层在Controller层做第二层在连接层做第三层在生成SQL阶段由Agent结合用户上下文自动注入行级过滤条件。6.2 强制“只读行级过滤”的落实方式强制只读在基础设施层面落实很简单就是给连接层使用的数据库账号设置只读权限这是数据库账号层面的事代码里不用写逻辑。但行级过滤要复杂很多因为不同用户的数据范围是动态的。我的方案是在Agent生成SQL之后、执行之前加一个“SQL改写/校验”环节。这个环节会根据当前用户的数据权限范围自动往SQL上追加WHERE条件。比如用户A的可见区域是“华东区”那么SQL会被自动加一条region IN (EAST, EAST_CN)的子句。这里有三个注意事项用户权限范围的数据必须从权限服务动态获取不能缓存太久权限变动要实时生效。注入的过滤条件命名要跟数据库实际字段对齐这个映射关系放在元数据服务里统一维护。如果SQL里已经包含同字段的过滤条件要自动合并而不是叠加否则可能产生逻辑错误甚至查不到数据。行级过滤如果用得不好会造成两种后果一种是不生效形同虚设另一种是过滤得过严把用户本来有权限看的数据也挡掉了。建议在测试阶段多准备几个不同权限角色的账号逐一验证。6.3 执行保护机制超时、限流、结果集截断数据查询是对生产库的直接访问必须做好保护。我们在基础设施建设阶段就预置了三道防线超时控制可配置的查询超时时间默认30秒超过自动取消。对于大查询我们做了异步化处理任务进入队列执行完成后通过WebSocket推送给前端。并发控制同一个用户同时最多只能跑3个查询防止用户用脚本疯狂刷接口。同时对每个数据源设置了全局并发上限避免Agent查询把生产库连接池打爆。结果集截断SQL返回的结果集行数和列数都做上限控制默认最大返回500行。超过上限时Agent会收到提示并尝试通过聚合方式缩减数据量。这三道防线不复杂但一定要在基础设施阶段就做进架构不能等上线后出事故再补。我见过不止一个团队把Agent直接挂在生产库上跑结果业务方点一下报表数据库CPU直接飙到100%。这种事故一旦发生领导层对AI项目的信任度就会大打折扣。7. 常见问题与排查技巧实录7.1 元数据采集到了但Agent仍然“看不懂”表现象明明元数据服务里表结构已经采集全了字段注释也有但是Agent生成SQL时还是选择了错误的表或字段。排查思路先看动态Schema是否真的把正确的表信息加载出来了。很多时候问题出在路由环节——用户问题里的表达方式跟元数据里的主题域标签不匹配导致该召回的没召回。可以在日志里把“路由结果”“召回表清单”完整打出来人工确认是路由问题还是生成问题。另外要重点检查Sample Value是否干净。脏样例值对模型的误导非常严重比如一个“status”字段样例值全是“1”“2”“3”模型根本不知道含义这时候需要在元数据注释里补充“1待支付2已支付”这类说明。7.2 SQL生成正确但执行结果与业务认知不符现象Agent生成的SQL逻辑上完全正确表、字段、聚合方式都对但跑出来的结果跟业务方人工统计的数字对不上。这类问题十有八九出在口径定义上。比如“销售额”在语义层被定义成了“订单金额总和”但业务方心里的销售额是不含退款、不含取消订单的。这时候光看SQL发现不了问题要看指标口径描述是否和业务预期一致。排查时需要让业务方提供一份“标杆报表”把Agent查出来的结果和标杆报表逐行对比。一般来说对不上的原因集中在三个地方是否过滤了无效状态、是否排除了测试数据、是否处理了跨天/跨月边界。把这几个维度逐一比对就能定位到口径偏差点。7.3 查询性能太慢页面转圈圈现象Agent已经生成了SQL执行也没报错但查询耗时动辄一两分钟体验很差。处理办法分两步。第一确认慢是否来自SQL本身——看看是否少了必要的时间范围限制、是否做了大表全扫、是否JOIN了过多表。很多情况下Agent生成的SQL没有带默认时间过滤我们通过校验阶段自动追加“最近365天”这类默认条件来兜底。第二如果SQL本身没问题但数据量实在大就要考虑走数仓或OLAP引擎不能指望业务MySQL扛得住。有一个小技巧在SQL执行前先做一个“预估扫描行数”的检查。MySQL下可以用EXPLAIN估算扫描行数超过阈值就直接提示用户收窄条件不要等查询跑了几十秒才反馈“超时”。这个体验细节非常加分。7.4 多轮对话中的上下文污染现象用户先问“华东区的销售额”然后接着问“那退货率呢”Agent需要理解“那”指的是华东区。但如果Agent把上一轮的所有字段、SQL、结果都塞进上下文很容易生成出跟“退货率”无关的奇怪SQL。解决思路是在多轮对话管理中引入“槽位记忆”机制只保留上一轮中的关键约束条件维度值、时间范围、过滤条件丢掉其它过程信息。我们把每一轮对话的结构化信息用户意图、SQL模板、约束条件、指标清单单独保存一份下一轮开始时只组装这些结构化数据作为上下文而不是把上一轮的整段对话文本直接灌给LLM。8. 实践经验与后续规划8.1 搭建过程中的几个关键心得第一个心得基础设施永远要先于Agent逻辑落地。我踩过最大的坑就是把Agent的业务逻辑生成SQL、追问澄清先写完了回过头再补元数据和语义层结果业务逻辑里到处是临时写死的表名和字段名补基础设施时要反推重构成本极高。正确顺序应该是先把元数据采集、语义配置、权限服务搭好Agent逻辑只是这些基础设施的“调用方”。第二个心得不要把打通demo当作完成基础设施。上一个demo只需跑通一条线但生产环境要经得起“随便什么业务问题都能问”的考验。基础设施阶段要把异常分支都盖住查不到数据怎么办、用户输入含敏感词怎么办、业务词存在歧义怎么办、数据源抖动怎么办。这些才是基础设施价值的体现。第三个心得多准备几个“验收型问题”。在基础设施搭建完成后我会准备20个覆盖常见业务场景的验收问题包括简单查询、维度对比、趋势分析、多表关联、含歧义问题等。每次改完基础设施都拿这批问题回归一遍能快速暴露各种隐藏问题。8.2 下一步Agent编排层和模型调优基础设施搭建完成之后下一步就是Agent编排层的开发。到时候会在LangGraph里把“意图识别—语义映射—SQL生成—SQL校验—执行反馈—答案生成”整条链路串起来同时加入多轮追问和主动澄清能力。模型调优方面我计划积累足量的真实问答对数据做Few-shot样本库和SFT微调数据准备。问数项目这类场景通用模型虽然能用但要做到稳定的准确率还是需要在业务数据上做针对性优化。Few-shot方式启动成本最低先跑起来收集bad case再迭代成微调数据。还有一块后续值得做的是主动数据洞察。当基础设施稳定之后可以训练Agent从历史查询日志里挖掘高频问题主动生成数据周报、异常提醒——那是把问数Agent从“被动问答”升级为“主动运营”的方向也是下一篇文章重点探索的内容。