我上周刚把问数项目的第一个版本跑通,从零搭了一套基于LCODER的AI Agent智能体架构。问数项目说白了就是让业务人员用自然语言直接查数据库,比如"上个月华东区销售额前10的品类是什么",系统自动完成意图理解、SQL生成、查询执行、结果解读一整条链路。这篇文章是LCODER之AI Agent开发实战系列的第一篇,重点拆解项目架构设计——为什么这么分层、核心模块怎么划分、Agent的运行逻辑是什么、技术选型背后的考量。适合正在做AI Agent开发、或者准备把大模型落地到数据查询场景的工程师参考,这篇文章会给你一份可以直接抄作业的架构蓝图。
1. 项目背景与核心需求:问数这件事,到底难在哪
1.1 为什么需要"问数项目"
传统的数据查询路径是:业务提需求→数仓写SQL→出报表→业务看图。这个链路的问题在于慢,一个简单的取数需求排队下来至少半天,碰上口径来回确认,拖两天也很正常。问数项目想做的事情,就是把这个链路压缩成一句自然语言提问,让AI Agent自动完成从语义理解到结果返回的过程。
但把所有环节串起来,远比想象中复杂。最初我尝试过用纯Prompt工程来做,让大模型直接输出SQL,再执行返回结果,效果很不稳定。问题主要出在几个地方:大模型不了解库表结构导致字段名编造、长长的WHERE条件容易漏条件、多轮对话的上下文容易丢失、执行结果没法自动做可视化。这也是我决定切换到Agent架构的核心原因——问数不是一个单一的文本生成任务,而是一个需要感知、规划、行动、反思的完整智能体任务。
1.2 目标场景与核心能力边界
在动手设计架构之前,先明确问数项目的场景边界,否则很容易做成一个什么都想干但什么都干不好的系统。我圈定的核心场景是:
- 面向内部业务人员的经营分析查询,不面向C端用户
- 支持单表查询和简单多表关联,复杂ETL逻辑不在Agent能力范围内
- 查询结果返回后,Agent需要给出一段自然语言的业务解读
- 支持多轮对话,用户可以逐层追问
能力边界也画得很清楚:Agent不做数据加工和清洗,不在SQL里做复杂窗口函数,不做自助建模。边界画清楚之后,架构设计才不会跑偏,也方便后续做能力迭代。
整体设计思路是"能用Agent解决的就用Agent,不能让Agent做的一律挡住"。比如SQL生成交给Agent,但SQL的安全性校验必须走规则引擎;表结构识别交给Agent,但字段语义映射必须走元数据缓存。
2. 整体架构设计:LCODER问数项目智能体的四层结构
2.1 分层架构与核心模块划分
问数项目的整体架构分为四层:接入层、Agent核心层、工具层、数据与基础服务层。每一层解决一类问题,层与层之间只通过标准接口通信。
接入层负责对接外部使用方,包括Web端对话界面、企业IM机器人和OpenAPI三种方式。这里有一个设计决策:接入层只做协议转换和会话管理,不承载任何业务逻辑。所有请求统一封装成标准Message格式,进入Agent核心层的消息队列。
Agent核心层是整个架构的中枢,也是LCODER这个Agent框架重点解决的问题。它包含六个核心模块:
- 意图识别模块:判断用户提问属于查数、追问、闲聊还是报表生成
- 任务规划模块:将用户目标拆解为可执行的子任务序列
- 工具调度模块:根据子任务类型选择合适的工具并传递参数
- 上下文管理模块:维护多轮对话的记忆,支持指代消解
- Schema感知模块:向大模型提供准确的库表结构信息
- 结果解析模块:将查询结果转化为自然语言解读和可视化方案
工具层是Agent可以调用的能力集合,包括SQL执行工具、元数据查询工具、图表生成工具、数据字典工具等。每个工具都遵循统一的输入输出协议。
数据与基础服务层包括MySQL、Redis、向量数据库、对象存储和模型网关。模型网关在这里是个关键设计,统一封装了与不同大模型的交互方式。
2.2 技术选型与选型理由
技术选型是架构设计里最容易被低估的环节。很多团队一上来就选了最热门的框架,结果项目做到一半发现某层不合适,推翻重来。
我在LCODER问数项目里的核心选型是:Python 3.11 + FastAPI作为服务框架,LangGraph负责Agent的编排,MySQL和向量数据库搭配存储元数据,Redis做缓存和会话管理,模型网关兼容主流的闭源和开源大模型。
先解释为什么用LangGraph而不是LangChain。LangChain的问题在于抽象层级太高,当Agent的流程比较复杂时,流程控制能力会很弱。LangGraph提供了图结构的编排方式,可以把意图识别、SQL生成、SQL校验、结果解读这些节点定义成一张有向图,每个节点是一个可编程的步骤,节点之间通过状态对象传递数据,这对问数这类多步骤强交互的场景非常合适。
数据库选MySQL而不是PG,主要是历史原因,数仓已经跑在MySQL上。向量数据库选的是开源的Milvus,存表结构信息、字段描述和查询历史的Embedding。用向量检索而不是直接查元数据表,是因为用户在提问时的表述经常和表名/字段名不一致,向量检索能做语义匹配。比如用户问"销量",实际字段名是"sale_cnt",光靠字符串匹配是匹配不上的。
2.3 为什么不用一个巨型Prompt解决问题
这里分享一个我踩过坑之后的反思。在项目初期,我试过把系统提示词写得极其详细,把数据库所有表结构、字段注释、业务口径全部塞进一个Prompt里,期望大模型"看完就能查"。实际效果很差。
原因主要有三个。第一是上下文窗口有限,表结构一多,几千个字段灌进去,Token成本高且有效信息被稀释。第二是大模型的注意力机制决定了它对Prompt中间位置的敏感度低于开头和结尾,埋在一大堆表结构中最关键的表反而不容易被注意到。第三是字段口径常有冲突,比如"销售额"在订单表是去掉退款后的净额,在报表表是含退款的总金额,同一个词在不同表里语义完全不同,一个巨型Prompt没法处理这种细粒度差异。
所以架构上必须拆开:表结构不靠人工写死在Prompt里,而是按需通过Schema感知模块检索出来,动态注入Prompt。这就是Agent架构相比纯Prompt工程的核心优势——具备感知能力,能够动态决定要"看"什么信息。
3. Agent核心运行逻辑:智能体是怎么"想"的
3.1 从用户提问到Agent决策的完整链路
要理解Agent的运行方式,首先得跳出"大模型问答"的思维框架。Agent不是简单地"输入问题→输出答案",而是"感知状态→规划动作→调用工具→更新状态→继续决策"的循环。
以"上个月华东区销售额前10的品类"这个问题为例,Agent的处理链路是这样的:
- 接入层收到用户消息,带上会话ID进入Agent核心层
- 意图识别节点判断这是一次数据查询请求,类型为"查数"
- Schema感知节点从元数据库检索出与"销售额""华东区""品类"相关的表和字段
- SQL生成节点基于检索到的Schema信息和用户问题生成SQL候选
- SQL校验节点对生成的SQL做安全性检查,包括是否只读、是否有LIMIT、是否有明显语法错误
- SQL执行工具执行查询,返回结果集
- 结果解析节点根据结果集生成自然语言解读,同时判断是否需要生成图表
- 最终回复返回给用户,同时把本轮对话写入上下文管理器
这个过程看起来是线性的,但在LangGraph里是图结构,节点之间有分支和跳转。比如第5步SQL校验如果发现生成的SQL有问题,会直接回到第4步让大模型重新生成,而不是直接报错给用户。
3.2 LLM在问数链路中的三个关键角色
同一个大模型,在问数项目里实际上承担了三种不同的角色,这也是Agent架构与传统Chatbot最不一样的地方。
第一个角色是意图理解和任务规划器。这个环节大模型需要判断用户的意图类型,并决定需要调用哪些工具。这里我用的是Function Calling机制,让大模型从预设的工具列表中选择,而不是让它自由发挥。比如用户说"帮我看看这个数据",意图识别模块需要结合上下文判断"这个数据"指的是上轮查询的结果集,还是需要重新发起一次查询。
第二个角色是SQL生成器。这是问数项目里最核心也是最难的部分。这里的大模型实际上在做"受约束的生成"——约束来自两个方面,一是Schema感知模块提供的表结构信息,二是SQL规范里的业务口径定义。我测试过不下十种Prompt模板,最终发现效果最好的是"先解释再生成"模式:要求大模型先说明自己理解了这个表是干什么的、用户想问的是什么,然后再生成SQL。这个中间推理步骤对提升准确率帮助很大。
第三个角色是结果解读器。查询返回的是结构化数据,用户要的是通俗易懂的结论。这个环节大模型需要把数据转化为文字描述,比如"销售额排名第一的是休闲食品,达到2300万,环比增长12%"。这个角色对上下文的要求很高,因为它需要引用上轮生成的SQL和结果集来做分析。
3.3 需要几个Agent?单Agent还是多Agent
架构设计时我面临一个选择:是一个Agent从头干到尾,还是拆成多个各司其职的子Agent。这个取舍在LangGraph里尤其重要,因为图编排可以很灵活地组合节点。
我的最终方案是"单Agent核心 + 多专家节点"。也就是从用户视角看是一个Agent在对话,但从架构视角看,内部有多个专家节点承担不同职责。核心Agent负责任务调度和上下文维护,专家节点专注于特定任务。
为什么不用多Agent独立部署呢?我实测下来,多Agent通信的开销和复杂度远超收益。每个Agent都有独立的上下文和记忆,当A Agent的输出需要传给B Agent时,序列化/反序列化是有信息损失的。更头疼的是问题定位,多Agent场景下你很难追踪到底是哪个Agent给出了错误结果。
单Agent核心加多专家节点的模式,既保留了任务分工的优势,又避免了多Agent通信的复杂度。专家的上下文由核心Agent统一管理,相当于是一个大脑指挥多双手,而不是多个大脑各自决策。
4. 关键链路设计:从自然语言到查询结果的落地细节
4.1 完整问数流程的状态管理与数据流
在LangGraph里,状态对象是整个图流转的"中枢神经"。每个节点执行完毕后,都要把结果写入状态对象,下一个节点从状态里读取需要的数据。这条数据链路设计得好不好,直接影响系统的稳定性和可调试性。
我这里定义了一个基础状态对象,包含以下核心字段:
- session_id: 会话标识,用于区分不同用户和对话
- user_query: 原始用户问题
- clarified_query: 经过指代消解后的问题(把"上个月""这个品类"这种指代替换为具体内容)
- intent: 意图识别结果
- retrieved_schema: Schema感知模块检索到的表结构信息
- generated_sql: 生成的SQL
- sql_validated: SQL校验结果
- query_result: SQL执行返回的结果集
- final_response: 最终拼装好的回复内容
- 上下文存储:包含历史问题的压缩摘要和最近几轮的完整记录
每个节点只负责读取自己需要的字段,在结束时更新自己的输出字段。这样做的好处是支持链路追踪——任何一轮对话都能从日志里完整复盘状态对象的变化过程,定位问题非常高效。
4.2 Schema感知:让大模型"读懂"数据库的关键机制
Schema感知是问数项目里最容易被忽视但影响最大的模块。很多团队做问数项目效果差,追根溯源就是Schema感知没做好,大模型拿到的表结构信息要么不够、要么不准确。
我这里的Schema感知模块采用"离线索引 + 在线检索"的设计。离线阶段,把数据库里每张表的表名、字段名、字段注释、字段类型、枚举值、表之间的主外键关系全部抽取出来,做一个清洗和向量化,写入向量数据库。同时,业务口径文档也被向量化保存,比如"销售额=订单金额-退款金额"这条口径定义,会被关联到订单表和退款表的相关字段上。
在线检索阶段,当用户发起查询时,Schema感知模块把用户问题做向量化,在向量数据库里做相似度检索,筛出最相关的表结构信息,限制在15个字段以内。检索条件同时包含表名/字段名的文本匹配和向量语义匹配,双路召回再融合排序。
这套机制最重要的价值,是把"大模型需要知道的信息"从全量缩小到"与本次问题相关的信息",既是效果保障也是成本控制手段。实测下来,加上Schema感知之后,SQL生成准确率从52%提升到了81%。
4.3 SQL生成与校验的双保险机制
SQL生成是问数项目的核心,但再强的模型也会犯错,所以安全兜底和准确性校验绝对不能省。我这里的做法是生成和校验双保险。
生成阶段,我要求大模型输出SQL的同时输出一段自解释。为什么一定要自解释?因为这段文字可以强制大模型"想清楚再写"。我实测过,加入自解释之后,SQL的语法错误率下降了30%以上。同时生成阶段会给定几个约束:只能SELECT,不能多语句,必须有LIMIT子句,表名和字段名必须来自提供的Schema列表。
校验阶段做了三层规则校验加一层语义校验。规则校验包括:SQL语句是否以SELECT开头、是否包含INSERT/UPDATE/DELETE/DROP/ALTER等危险关键字、LIMIT子句是否存在且值是否合理、表名字段名是否在Schema白名单内。语义校验交给LLM再做一次审查:把最终生成的SQL和用户问题、Schema信息一起发给模型,让模型判断"这个SQL和用户的问题是否一致、有没有明显的逻辑矛盾"。
这里给一个参考的校验结果处理方式:规则校验不通过的直接打回重新生成,重新生成两次仍不通过的返回给用户"抱歉暂不支持该查询";语义校验不通过的会提示大模型修正。这些规则都写入配置文件,方便后续调整,不写死在代码里。
4.4 多轮对话的上下文管理与指代消解
问数项目的多轮对话比普通客服机器人复杂得多,因为每一轮都可能产生新的SQL和结果集,上下文里同时存在"语义信息"和"数据结构信息"。
我的上下文管理分两层。短期记忆存最近5轮的完整对话记录、每轮生成的SQL和执行结果;长期记忆存用户在整个会话中的核心关注点、常用查询维度、经常用到的业务口径,做摘要后存入Redis。
指代消解是另一件很麻烦的事。用户说"上个月"到底是哪个月?"这个品类"指的是前一轮结果里的哪个品类?我的做法是在每次处理新问题前,先让大模型结合历史上下文做一次"问题改写",把指代词替换为具体内容。改写后的问题才进入Schema感知和SQL生成流程。实测这个前置步骤对多轮追问的准确率提升非常明显,从63%提高到了比首轮查询只低5个百分点的水平。
5. 工程化落地:代码结构、环境准备与开发顺序
5.1 项目工程结构是怎么组织的
架构如果只停留在文档里,那就不叫落地。下面是这个问数项目第一版的核心代码结构,实际项目的结构会多一些模块,但骨架就是这个。每个目录的职责在代码里通过命名和注释做了明确区分,避免后续越写越乱。
lcoder_agent/ ├── api/ # 接入层:HTTP接口、WS接口、IM机器人适配 │ ├── routes/ │ ├── schemas/ # 请求/响应模型 │ └── middleware/ ├── agent/ # Agent核心层 │ ├── graph/ # LangGraph状态图定义 │ ├── nodes/ # 每个图节点的具体实现 │ │ ├── intent_node.py │ │ ├── schema_node.py │ │ ├── sql_gen_node.py │ │ ├── sql_check_node.py │ │ ├── exec_node.py │ │ └── response_node.py │ ├── memory/ # 上下文管理、指代消解 │ └── state.py # 状态对象定义 ├── tools/ # 工具层 │ ├── sql_executor.py │ ├── schema_retriever.py │ ├── chart_generator.py │ └── registry.py # 工具注册中心 ├── meta/ # Schema离线索引构建 │ ├── extractor.py │ ├── vectorizer.py │ └── indexer.py ├── llm/ # 模型网关封装 │ ├── base.py │ ├── factory.py │ └── providers/ ├── config/ # 配置文件 └── tests/ # 单元测试和回归测试5.2 开发顺序建议:先跑通一条最简链路
一开始别想着把完整架构都实现出来,那样会陷入无尽的调试。我给出的建议是先跑通一条最简链路:用户输入→意图识别→SQL生成→SQL执行→结果返回。这五个节点串起来能跑以后,再逐一把Schema感知、SQL校验、上下文管理、结果解读这些模块加进去。
开发顺序可以按依赖关系分四步走。第一步是初始化LangGraph状态图,定义好状态对象,把最简单的几个节点挂上去,用假数据跑通整个图。第二步接入Schema感知模块,建好离线索引管道,把向量数据库打通,这样才能在SQL生成时动态注入Schema信息。第三步做SQL校验和重试机制,这是保障安全的关键,必须尽早补上。第四步加上下文管理和多轮对话增强。
每一步都有明确的验证标准。第一步的标准是"任意一段写死的SQL能返回结果",第二步的标准是"同一句自然语言查询,5次生成SQL的准确率能达到80%以上",第三步的标准是"危险SQL能被拦截且正常SQL不受影响",第四步的标准是"连续三轮追问能正确指代历史结果"。
5.3 环境准备与依赖清单
环境依赖的坑比想象中多,这里梳理一份第一版实际用到的依赖清单,省得大家在装环境时踩重复的坑。
Python环境建议用3.11,太旧的版本对LangGraph和Pydantic的兼容性都不太好。核心依赖包括langgraph、langchain-core、fastapi、uvicorn、pydantic、sqlalchemy、pymysql、redis、openai、milvus-python。模型方面,第一版建议直接用主流的闭源API,开发效率最高,等流程跑通了再考虑替换成私有化部署的开源模型。
数据库账号建议单独创建一个只读账号给Agent用,这和后面要讲的安全机制是配套的。向量数据库用Milvus的话注意先在本地起一个Standalone模式的实例,等联调环境再切集群模式。
5.4 配置管理的设计要点
问数项目的配置项并不少,而且同类配置的形态差异很大。我拆成了三类配置来管理,避免把所有东西堆在一个文件里。
第一类是模型配置,包括API地址、密钥、模型名称、温度参数、max_tokens等。第二类是工具配置,包括数据库连接信息、Redis连接、向量数据库连接、SQL执行超时时间、LIMIT默认值。第三类是Agent行为配置,包括意图类型列表、SQL校验规则、重试次数、上下文轮数等。
模型配置和工具配置用YAML文件管理,Agent行为配置因为经常调整,放到配置中心里,支持热更新。有一个实用的经验:SQL生成相关的Prompt不要写在代码文件里,单独维护一个prompt目录,用版本管理。每次Prompt改动都留档,方便回溯是哪次Prompt改动影响了效果。
提示:SQL执行超时时间建议设为10秒,超过直接终止并提示用户"查询超时"。数据库层面还要再设一个statement级别的超时,双保险,防止慢SQL拖垮连接池。
6. 常见问题与排查实录:这些坑我替你们踩过了
6.1 问题排查速查表
开发过程中遇到的高频问题,整理成速查表,遇到类似问题可以直接对照排查。
| 现象 | 大概率原因 | 排查思路 |
|---|---|---|
| SQL生成总用错字段 | Schema感知注入的表结构不完整 | 查看检索到的字段列表,确认是否匹配用户问题 |
| 生成的SQL语法正确但执行超时 | 缺少LIMIT约束或表连接条件缺失 | 检查SQL校验规则,加LIMIT强制约束 |
| 多轮对话答非所问 | 指代消解失败 | 查看clarified_query,确认指代词是否被正确替换 |
| 查询结果正确但解读错误 | 结果解读Prompt缺少数据上下文 | 检查最终生成解读时是否传入了完整结果集 |
| 同一问题不同结果 | 大模型温度参数偏高 | 调低temperature,必要时改为0.1 |
| Agent流程卡住不执行下一步 | LangGraph状态对象字段更新异常 | 打开状态追踪日志,检查各节点输出 |
6.2 关于SQL安全的设计底线
问数项目直接操作数据库,安全设计必须放在第一位。我第一版就确定了几条原则,后续不管是加功能还是改架构都不会突破这层底线。
数据库账号用只读账号,从根源上杜绝写操作。SQL校验在应用层做一层规则拦截,危险关键字直接打回。但规则拦截不是全部,数据库账号层面的只读是最底层的保证,这两者不能互相替代。
所有查询强制加LIMIT,默认100行,防止业务人员一条SQL拉千万行数据。查询打标,用户ID、会话ID、生成的SQL、执行耗时全部写入日志,方便做审计回溯。
6.3 现阶段的心得与后续架构演进方向
问数项目第一版跑通之后,我对AI Agent开发最大的感受是:架构的价值不在"用上最新框架",而在把复杂问题拆解成可维护的模块,每个模块负责一小块,出了问题能快速定位修补。
LandGraph这套图编排的方式,在问数这个场景下真的比传统Agent框架顺手得多。之前用纯LangChain的时候,每个节点的输入输出全靠模型自己控制,排错经常要靠猜。换成LangGraph之后,每个节点是明确的Python函数,状态流转清清楚楚,调试的时候直接打印状态对象就能看到全部信息。
接下来的演进方向,我打算在几个方面继续深入:一是Schema感知维度继续扩展,增加数据血缘信息,让Agent能理解"这个字段是从哪张表汇总出来的";二是结果可视化能力增强,不只是生成图表,而是能根据数据特征自动选择最合适的图表类型;三是引入反馈闭环,把用户的点赞点踩数据收集起来,作为Prompt优化和Schema权重调整的依据。
最后分享一个小技巧:问数项目的Prompt测试一定要做成自动化。我维护了一个覆盖50多个典型问题的测试集,每次改Prompt都跑一遍回归,用"SQL生成是否准确、结果是否有重大偏差"做自动判定。别信"感觉变好了"这种主观判断,AI项目的效果评估必须量化。
这个系列后续会继续更新,下一篇文章重点讲Schema感知模块的实现细节,包括离线索引管道怎么搭建、向量检索的排序策略怎么调优,到时候再聊。