ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

问数智能体架构实战:四层设计+LCODER工程化落地

问数智能体架构实战:四层设计+LCODER工程化落地 “我这个需求不复杂就是查个数据为什么要等三天”——这是去年业务方跟我说的原话也是“问数项目智能体”这个项目真正立项的导火索。当时我们内部给这个实战项目起了个代号叫LCODER没有玄乎的寓意就是“以写代码的精度做AI Agent工程化”的缩写。这个系列第一篇我先把项目架构讲清楚一个问数智能体到底由哪些部分组成每一层为什么必须有以及从零搭它的时候哪些地方最容易翻车。如果你手里正好也要做类似的数据问答、报表自动生成、业务自助取数这类Agent这篇应该能帮你省掉至少两周的试错时间。做Agent和做传统接口不一样难的不是某一段代码而是整个系统的职责切分。一个问数智能体表面上只是“对话→出数”背后却要串起大模型、SQL能力、数据权限、图表渲染、多轮记忆这些完全不同的模块。架构没想清楚就动手后面每加一个功能都是在给系统打补丁。这篇我会先讲清楚为什么不能拿一段靠提示词堆出来的脚本去交付再完整拆解LCODER项目的四层架构与模块设计然后给出一套基于LangGraph的落地代码骨架最后把我在真实环境里踩过的问题做一次排查汇总。1. 问数项目启动前先想清楚Agent要解决什么问题1.1 所谓“问数”本质是给业务人员一台数据翻译机很多团队做问数项目上来就把它理解为“做一个聊天机器人”这个定位从第一句话就跑偏了。问数项目的核心需求从来不是“聊天”而是把业务人员脑子里的业务问题翻译成可执行、可验证、有权限边界的SQL查询再把查询结果翻译回人类能直接看懂的语言和图表。它是一台数据翻译机不是一个话痨。我当时和业务方确认需求时把他们的原始诉求“能查数就行”一点点拆开最后拆成了这几条必须满足的硬指标支持自然语言提问例如“上个月华东区销售额环比增长了多少”。查询结果必须能追溯业务人员有权知道这个数字来自哪张表、哪个口径。权限体系必须复用公司现有的数据权限不能让Agent成为绕过权限的后门。回答要快不能让业务等30秒结果还是个错的。至少要支持多轮对话比如先问“华东区呢”Agent要知道这是接着上一句话在问。这个清单列出来之后大家才意识到我们不是在做一个提示词封装工具而是在做一个由大模型驱动、但又被工程手段严格约束的数据服务系统。这也是我把“项目架构”作为LCODER实战第一讲的核心理由——后面的所有代码、节点编排、工具封装都是在为上面这五条硬指标服务的。架构没想清楚后面全是债。1.2 为什么“提示词API”糊出来的东西没法交付2024年前后市面上出现了大量“一行代码接入大模型”的教程很多人以为问数Agent就是“把用户问题丢给GPT让GPT直接给SQL再执行一下”我也试过这个路子结论是在演示环境能跑在生产环境必挂。原因很具体。第一大模型直接生成SQL的准确率看着还行但没人兜底。用户随口的一句“最近业绩怎么样”模型可能生成三种截然不同的SQL查出来的数完全对不上。传统软件工程里我们有强类型、有单元测试、有代码评审但在轻量提示词方案里这些东西全部缺席了。第二上下文窗口扛不住真实查询。一旦业务人员连续追问三五轮历史对话、表结构、字段注释全部堆进上下文模型的表现会肉眼可见地下降而且token成本涨得飞快。第三也是最致命的——这个方案没有显式的流程控制点。权限校验在哪里做非法SQL怎么拦截敏感数据怎么屏蔽查出来的结果怎么保证正确你没法在“一句话”里塞进这么多控制逻辑。所以我在LCODER项目里一开始就定了一个原则大模型负责“聪明的部分”工程系统负责“确定的部分”。聪明指的是意图理解、SQL生成、结果解读这些天然需要泛化能力的环节确定性指的是流程控制、权限校验、参数校验、执行超时、结果缓存这些一点都不能含糊的工程环节。要让这两类东西协同工作就必须有一个清晰的系统架构。1.3 架构设计前必须锁定的三个边界条件真正动手画架构图之前我逼着团队回答了三组问题这三组答案直接决定了后面项目的复杂度和交付周期。第一个边界是用户边界谁在用这个Agent他们问什么样的问题。我们最开始想着“所有人问所有业务指标”结果发现这个目标根本无法收敛因为每个BU的数据口径都不一样。最后我们把用户范围圈定为运营团队和数据分析师问题类型圈定为围绕核心指标报表的查询与解读。边界一收模型的prompt可以写得非常具体NL2SQL的schema也不需要全量导入性能和准确率立刻上一个台阶。第二个边界是数据边界Agent能访问哪些库、哪些表、哪些字段哪些不能碰。这块需要和数据组、安全组反复对表。我们最终把数据分成了三类一类是可直接开放给Agent查询的聚合表一类是需要动态鉴权的明细表还有一类是永远不允许进Agent的黑名单表。这个分类直接决定了后面架构里的权限拦截模块怎么设计。第三个边界是失败边界模型猜错了意图怎么办SQL查出来为空怎么办执行超时了返回什么用户对结果提出质疑时怎么回溯。很多团队把这三个问题留到上线后再说而我们选择在架构阶段就定义清楚——每一类失败都需要一个明确的结果状态和一条兜底路径。这三条边界定了整个架构的约束条件也就定了不是“做一个尽量聪明的AI”而是“在一个受限范围内做一个高确定性、高可控性的智能体系统”。2. 整体架构分层与LCODER模块设计2.1 四层架构接入层、编排层、能力层、基础设施层LCODER问数项目最终采用了经典的四层架构每层只做自己职责范围内的事层与层之间通过明确的接口协议沟通。这里我直接列出每一层的职责和关键组件后面再逐一展开重点模块。第一层是接入层负责所有外部入口。包括Web对话框、企业IM机器人我们主要接的是飞书、以及OpenAPI接口。这一层做的事情很纯粹接收用户输入完成基本的格式校验和身份认证然后把标准化后的消息对象传给下一层。它不关心任何大模型或者SQL的事情这样未来无论多一个钉钉机器人还是一个微信小程序都只是多接一个适配器而已。第二层是编排层这是整个Agent的大脑也是LCODER里代码量最大、测试最充分的一层。它负责运行Agent的主循环接收任务→拆解意图→调度工具→审查结果→返回答复。这一层我们用LangGraph基于状态图实现后面第四章会给出具体代码骨架。之所以选择状态图而不是自由函数调用是因为问数流程天然存在分支和人工介入点用户问题需要澄清、安全审查不通过需要走拒绝路径、SQL执行失败需要重试这些都必须以显式的节点和边来定义不能用隐式的函数调用来表达。第三层是能力层也可以叫工具层是把各种原子能力封装成Agent可调用的“技能”的地方。在LCODER里能力层主要包含五个模块NL2SQL模块、SQL执行器、语义模型加载器、指标口径解释器、图表生成模块。每个模块都以标准工具接口暴露给编排层也就是符合MCP协议的工具定义后面会细说。第四层是基础设施层包括向量数据库存表结构描述、指标口径、少量示例、元数据中心、Redis会话存储、MySQL业务库、日志与链路追踪系统等。这层不直接参与Agent逻辑但没有它Agent就是一台没有记忆、没有知识、出了事故无从追溯的裸机器。四层架构看起来朴素但它的好处在项目进入迭代期之后才真正显现出来任何一个能力模块升级比如把NL2SQL从直接生成换成带检索的生成都只需要变更能力层内部实现编排层和接入层完全不动。2.2 LCODER工程目录怎么划分才不失控架构图只是理念落到代码仓库时如果目录划分不合理一样会乱成一锅粥。LCODER项目的工程目录在折腾了两轮重构之后最终稳定为下面这个结构lcoder/ ├── apps/ │ ├── web/ # Web对话入口接入层 │ ├── feishu/ # 飞书机器人适配接入层 │ └── api/ # OpenAPI服务接入层 ├── core/ │ ├── agent/ # 编排层核心状态图、节点定义、路由 │ ├── tools/ # 能力层工具封装与MCP Server │ ├── memory/ # 记忆模块短期/长期记忆接口 │ └── security/ # 安全模块权限校验、敏感词、审计 ├── capabilities/ │ ├── nl2sql/ # 自然语言转SQL │ ├── executor/ # SQL执行器带超时和拦截 │ ├── semantics/ # 指标口径与语义模型 │ └── charting/ # 图表渲染服务 ├── infra/ │ ├── vector_store/ # 向量库封装 │ ├── redis_cache/ # Redis缓存与会话 │ └── observability/ # 日志、指标、链路追踪 ├── tests/ │ ├── unit/ │ ├── integration/ │ └── eval/ # Agent效果评测集 └── configs/ ├── prompts/ # 所有prompt模板独立存放 ├── tools/ # 工具注册配置 └── models/ # 模型路由配置这个目录的关键设计决策有两个第一所有prompt不散落在代码里统一放configs/prompts/因为prompt本质上是配置而非代码它会被频繁调整放进代码里只会不断触发无意义的发版流程第二能力层和编排层严格分离core/只依赖能力层暴露的工具接口绝不允许core/agent里出现一行直接操作数据库的代码。这个约束让调试变得极其舒服——编排出问题查core取数算错查capabilities两边都不需要互相翻代码。2.3 技术选型的理由为什么编排框架比硬编码状态机更省心聊到技术选型LCODER内部其实发生过一次激烈争论。当时有两条路线一条是直接用LangGraph这类现成的Agent编排框架另一条是自己硬编码一套基于if-else的状态流转逻辑。主张自研的同学理由也很充分“我们就一个问数场景流程那么固定写状态机也就两百行为什么要引一个重框架”我当时的判断是现在看是两百行但问数Agent的流程一定会在三个月内长出分支。事实也果然如此。最开始我们的流程图只有“理解→出SQL→执行→回答”四个节点但上线前就已经长出了“意图澄清→多轮追问→SQL修正→权限判定→图表生成→口径解释”差不多十个节点而且节点之间的边有条件分支、有循环、还有人机协同的暂停点。这种复杂度下硬编码状态机会变成一张没人敢碰的蜘蛛网。另外LangGraph这类框架带来一个硬编码难以复制的核心能力断点续跑和人工介入。在问数场景里很多时候Agent发现SQL的过滤条件不明确比如用户说“最近三个月”但指标口径里“最近三个月”有两种定义它应该停下来反问用户而不是自作主张。LangGraph对“人在回路”支持得很自然你可以在图的任意节点上插入审核步骤这对问数Agent这种需要严谨性的场景太重要了。如果你没用过LangGraph可以把它的核心模型理解为“一个有状态的图”节点就是函数边就是函数之间的连接关系状态是一个全局可读写的数据结构节点函数读入状态、处理、然后写出新的状态。Agent主循环就是在这个图里走了一圈又一圈直到走到结束节点。这也是我在第四章要带大家实操的内容。3. 核心Agent节点的定义与关键设计3.1 节点一用户意图识别与问题理解很多人做Agent习惯把“理解意图”和“执行任务”揉在同一个大模型调用里LCODER项目早期也这么干过效果很差。因为问数场景里用户说的话信息密度极高一句话里既有时间范围、又有业务维度、还有指标名称任何一个要素判断错后面生成的SQL都是错的。所以我们把“理解用户”拆成了独立的意图识别节点它只做一件事从用户输入中抽取出结构化的查询参数。这里的关键是定义清晰的状态结构。简单地说我们在状态里定义了一个QueryIntent对象包含这些字段metrics要查的指标如销售额、订单量、dimensions维度如区域、品类、time_range时间范围如近30天、上季度、filters附加过滤条件、aggregation聚合方式、need_chart是否需要图表等。意图识别节点的工作就是把一句大白话映射成这样一个结构化对象。确定性和容错性在这里是第一位的。比如用户说“上个月华东和华南的退货率对比”模型需要识别出这是两个维度的对比查询时间范围是自然月指标是退货率。如果识别结果中metrics为空节点会直接走“澄清分支”——在状态图上这个澄清分支是一条显式边Agent会回复“我理解您想查退货相关的数据需要补一下指标口径是退款金额占比还是退货订单占比”这就是架构带来的确定性。3.2 节点二NL2SQL与查询执行意图识别完成后系统进入最核心的NL2SQL节点。这个节点负责把QueryIntent转换成可执行的SQL。在LCODER里我们刻意没有让这个节点直接面对整个数据库的全部表结构而是先经过一个“语义模型”裁剪步骤。这个裁剪步骤是准确率提升的最大功臣之一。在任意一个中大型公司里直接把数百张表的全量schema塞给大模型生成准确SQL的概率一定会被无关表和字段干扰拖垮。我们提前把业务方最常用的指标、维度、表关系整理成了语义模型模型不需要理解所有表只需要从语义模型中找到指标对应的表和字段即可。这个做法和RAG的思路一致先缩小检索范围再让模型做生成而不是让模型在无限大的空间里瞎猜。SQL生成之后并不会直接被当成可信产物执行而是必须经过一个叫做“SQL校验器”的组件。这个组件不是大模型就是一套纯规则代码。它做三件事白名单表校验只允许查询语义模型中注册过的表、只读校验强制拦截INSERT/UPDATE/DELETE/DDL、敏感字段脱敏校验比如手机号、身份证号字段必须打码。校验通过后SQL进入执行器执行器统一设置超时我们生产环境设的是10秒、最大返回行数默认2000行、强制走只读副本等。我曾经在一篇文章里看到一句话“Agent的边界就是工具的能力边界。”这句话在SQL执行环节体现得淋漓尽致。你给Agent什么样的执行权限它就拥有什么样的能力你如果不在执行器层面加护栏大模型生成的SQL就是一把没有保险栓的枪。这也是为什么我坚持SQL执行器一定要用强规则代码实现而不是让大模型自己判断“这个SQL能不能执行”。3.3 节点三结果解读与图表生成SQL执行成功只是拿到了一个二维数据集。真正让业务人员觉得“好用”的体验来自最后一步结果解读与图表生成。这一步最容易被技术团队忽略因为它看上去只是“把结果交给大模型让它写一段自然语言总结”。但实际做过才知道这里至少有四个坑等着你。第一个坑是数据准确性大模型在解读结构化数据时会因为“聪明过头”而凭空算错数字。比如查询结果是1543210模型总结时可能写成“约154万”这勉强还能接受但它有时候会把两行数据自行相加得出一个不存在的第三个数据这个错误是致命的。我们的对策是把查询结果直接嵌入到prompt里并强制模型“只能基于给定数据进行文字描述禁止自行计算和推断新数字”同时用规则把关键数字抽出来在展示时以卡片形式渲染让用户在可视化卡片上看到的是原始查询结果而不是大模型复述的数字。第二个坑是图表类型的选择柱状图、折线图、饼图不是随便选的需要结合查询的维度和时间序列特征来决定。我们在图表节点里用规则判断有时间维度且有连续多期数据默认折线图有分类维度且数量小于5用柱状图占比分析则用饼图。模型在这里的角色只是补全标题和坐标轴标签而不是决定图形类型。第三个坑是多轮对话中的指代消解。用户上一句话刚问完“华东区销售额”接着问“那华南呢”这时候系统状态里必须保存上一轮的查询上下文。这个能力由记忆模块提供下一节展开。第四个坑是“无结果”场景的处理。当查询结果为空直接给用户返回“没有数据”是最差的体验。我们会先判断过滤条件是否可能过紧然后给出建议“当前时间范围内无数据是否尝试扩大到近90天”这本质上是一个“元认知”节点需要Agent对自己上一轮的查询行为做反思这个能力我们在第三个迭代才补上体验提升非常明显。3.4 安全审核节点问数Agent不能踩的红线AI Agent落地的最大障碍不是模型能力不足而是安全性不可控。问数Agent直接面对的是企业核心数据资产安全审核节点在我的架构里优先级甚至高于准确率。这听起来像是在说口号但我带队验证过后才敢这么说安全审核不能做成事后审查必须做成流程中的一个阻断节点。LCODER项目里安全审核节点做了三层设计。第一层是输入侧审核用户提问本身是否涉及敏感对象比如某些特定的人名、客户名单这个由关键词规则和模型分类器共同完成。第二层是SQL侧审核前面提到过包括表白名单、只读校验、脱敏校验、行数限制。第三层是输出侧审核查询结果的字段级脱敏、结果是否包含敏感聚合信息比如样本量过小导致可能反向推断出个体数据这是数据合规里很常见的坑。有人可能会问这么多审核会不会让Agent变得“不太智能”我的答案是安全和智能不冲突但需要有一个清晰的产品设计思路。对于正常的业务查询三秒内全部校验通过走完全流程对于触碰边界的查询直接走“拒绝并解释”路径明确告诉用户“这个查询涉及客户明细数据当前权限无法访问”。这种设计反而让用户更有安全感因为他们知道这个Agent不是一本道、什么都能查而是一套有规则的数据服务。在做架构规划时我强烈建议把安全审核节点的位置画进流程图里而不是把它当作一个通用的过滤器挂在入口。4. 实操实录用LangGraph把架构变成可运行系统4.1 先把状态图画出来再动手写代码进入实操环节很多初学者的第一反应是打开IDE写代码我会拦一下Agent开发是少有的必须“先画图后编码”的领域尤其是问数这种流程敏感的Agent状态图就是它的数据结构也是它的产品原型。我带着团队在白板上画了第一版问数Agent状态图节点包括receive_query接收并标准化输入、extract_intent意图识别、clarify意图澄清、build_sql生成SQL、validate_sqlSQL校验、execute_sql执行查询、generate_answer生成回答、generate_chart生成图表、security_review安全审核、end结束。节点画完之后再用箭头把这些节点连起来连箭头的过程就是思考路由逻辑的过程validate_sql失败后是回到build_sql重试还是直接结束重试次数上限是多少security_review不通过是进入拒绝回答节点还是进入人工审批这些路由逻辑必须在写代码前全部想清楚。得到白板图之后我们把它“翻译”成LangGraph的StateGraph。这个过程基本是机械翻译每个白板节点对应一个Python函数每条白板箭头对应add_edge或者add_conditional_edges白板上标注的条件对应条件路由的path_map。所以架构的价值在实操环节直接体现出来因为前面把边界和职责都分清楚了这里写代码的速度非常快核心图定义在半天内就完成了。4.2 核心编排代码状态定义是最容易被低估的LangGraph代码里最难的部分不是节点函数怎么写而是状态类怎么设计。状态就是整个Agent运行时共享的一张“流水单”所有节点都在上面读写。LCODER项目的第一版状态定义得很粗糙一个字典里放了一个messages列表就开干了结果后面每个节点都在往里面塞数据什么都往里存最后调试时根本分不清某个字段是哪个节点写的、该由谁消费。经过重构后的状态定义我建议用TypedDict把所有字段显式声明出来并且按“谁生产、谁消费”把字段分组管理。下面是核心代码骨架保留了可运行的关键逻辑from typing import TypedDict, List, Dict, Any, Optional from langgraph.graph import StateGraph, END import json class QueryIntent(TypedDict): metrics: List[str] # 指标列表如 [销售额] dimensions: List[str] # 维度列表如 [华东区, 华南区] time_range: Optional[str] # 时间范围如 近30天 filters: List[Dict[str, Any]] need_chart: bool # 是否需要图表 class AgentState(TypedDict): # 对话基本字段 user_input: str history: List[Dict[str, str]] # 意图识别结果 intent: Optional[QueryIntent] # SQL生成执行链路 sql: Optional[str] sql_valid: bool sql_error: Optional[str] query_result: Optional[Dict[str, Any]] # 安全审核标记 security_reviews: Dict[str, bool] # 最终输出 output_message: Optional[str] chart_config: Optional[Dict[str, Any]] end_reason: str def receive_query(state: AgentState) - AgentState: # 从接入层传入的message已经经过预处理这里只做状态初始化 state[user_input] state[user_input].strip() state[sql_valid] False state[security_reviews] {input: False, sql: False, output: False} state[end_reason] return state def extract_intent(state: AgentState) - AgentState: # 调用LLM从user_input history提取结构化QueryIntent # 关键让模型输出JSON然后做schema校验不合法就走澄清 # 伪代码省略实际LLM调用细节 intent_json llm_call( system_prompt你是问数意图解析器请从用户输入中提取查询参数只输出JSON。, user_inputstate[user_input], historystate[history], ) try: intent validate_intent_schema(json.loads(intent_json)) state[intent] intent except Exception: state[intent] None state[output_message] 请补充查询指标或时间范围例如上季度华东区销售额。 state[end_reason] clarify_needed return state def user_input_missing_intent(state: AgentState) - bool: # 条件路由函数意图提取失败则进入澄清提示否则继续 return state[intent] is None状态类定义完成之后定义节点函数和路由就顺理成章了。这里有一个经验要分享每个节点函数只负责自己职责内的字段更新原则上不要在一个节点里同时更新sql和query_result。这样在出问题时打开状态就一眼能看到链路停在哪一步。状态定义就是Agent世界的“数据库表结构”把表结构设计好后面的应用层代码再乱也不会乱到哪里去。下面是完整的状态图组装过程def build_agent_graph(): g StateGraph(AgentState) # 注册节点 g.add_node(receive_query, receive_query) g.add_node(extract_intent, extract_intent) g.add_node(build_sql, build_sql_from_intent) g.add_node(validate_sql, validate_sql) g.add_node(execute_sql, execute_sql) g.add_node(generate_answer, generate_answer) g.add_node(generate_chart, generate_chart) g.add_node(security_review, security_review) # 入口与条件路由 g.set_entry_point(receive_query) g.add_node(receive_query, receive_query) g.add_edge(receive_query, extract_intent) g.add_conditional_edges( extract_intent, user_input_missing_intent, {True: generate_answer, False: build_sql}, ) # SQL生成与校验失败则带error信息回到build_sql最多重试3次 g.add_edge(build_sql, validate_sql) g.add_conditional_edges( validate_sql, sql_validation_retry, {True: build_sql, False: execute_sql}, ) # 查询执行 - 安全审核 - 输出 g.add_edge(execute_sql, security_review) g.add_conditional_edges( security_review, security_review_passed, {True: generate_answer, False: generate_answer}, # 拒绝时也走generate_answer但内容不同 ) g.add_edge(security_review, generate_chart) g.add_edge(generate_chart, generate_answer) g.add_edge(generate_answer, END) return g.compile()这里需要说明两个容易被新手误解的设计。第一security_review不通过时为什么还连到generate_answer因为我们在generate_answer内部会判断安全审核结果如果不通过生成的是“拒绝访问并解释原因”的回复而不是数据的解读。这个设计是为了保证所有最终回答都走同一个输出出口统一做格式化和日志留存在安全审计时更容易追溯。第二SQL校验失败后的重试逻辑这里sql_validation_retry里面维护了一个重试计数器并且把上次报错信息拼接到下一次build_sql的prompt里。比如执行器提示“字段 order_amount 不存在”下一次build_sql看到这个提示有很大概率自己修正过来。这个做法把大模型的自我修正能力变成了流程的一部分而不是寄希望于它“突然变聪明”。4.3 MCP工具集成把数据库查询能力封装成技能问数Agent的能力层不只是NL2SQL和SQL执行器在LCODER的架构里所有能力都统一以“工具”的形式暴露给Agent而这个工具协议我们选择走MCP标准。MCP的全称是Model Context Protocol它做的事情本质上是为“大模型调用外部工具”定义了一套统一的接口规范你可以把它理解成大模型世界的USB接口工具方只要实现一套MCP Server任何支持MCP的Agent都可以直接调用不需要为每家Agent单独写适配。为什么选MCP而不是自己定义一个工具接口虽然我们现在只有问数Agent一个场景但未来极可能有更多Agent比如报表解释Agent、告警根因分析Agent。如果每个Agent都用自己的工具协议去对接数据能力层那数据组会被这种重复劳动逼疯。MCP标准的好处是我们把数据查询能力做成一套通用的MCP Server后任何一个Agent接入都只需改配置不需要改代码。下面是一个简化的MCP工具定义示例把查询业务指标的能力封装成标准工具。这里框架用的是FastMCPLangGraph客户端通过list_tools和call_tool与Server交互。简化一下直接用函数装饰器来定义from fastmcp import FastMCP import pandas as pd from lcoder.capabilities.executor import execute_query mcp FastMCP(lcoder-query-server) mcp.tool() def query_business_metric( metric_name: str, dimensions: list[str], start_date: str, end_date: str, aggregation: str sum, ) - dict: 查询核心业务指标。 Args: metric_name: 指标名来自指标字典如 sales_amount, order_count dimensions: 维度列表如 [region, product_category] start_date: 开始日期YYYY-MM-DD 格式 end_date: 结束日期YYYY-MM-DD 格式 aggregation: 聚合方式sum/avg/count sql build_sql_from_metric( metric_namemetric_name, dimensionsdimensions, start_datestart_date, end_dateend_date, aggregationaggregation, ) df execute_query(sql) return df.to_dict(orientrecords)这里有一个设计细节很关键MCP工具的参数定义里我不让大模型自由发挥table_name或者直接传一段SQL进去而是让它从“指标字典”中选择metric_name传结构化的维度与日期范围由工具内部去组装SQL。这等于把最脆弱的“大模型生成SQL”这一步收窄成了“大模型从预定义指标中选择并填参数”在工程上大大提高了可控性。当然LCODER同时也保留了让大模型从语义模型直接生成复杂SQL的路径但那套路径会经过更严格的校验和灰度放量不会一上来就对所有用户开放。这个分层控制策略是问数Agent能在生产环境安全放量的底气之一。4.4 记忆模块与上下文管理的落地位置问数Agent绕过不了一个坎多轮对话。用户第一句问“上个月销售额”第二句问“那订单量呢”第三句问“华东的呢”这三句话如果脱离上下文来看后面两句根本没办法回答。所以在LCODER架构里记忆模块不是一个可选项而是编排层的重要支撑组件。我们落地了两层记忆。第一层是短期会话记忆记录当前会话最近N轮我们实践下来取6到8轮效果最好超过这个数量之后Token成本增加但理解效果不再提升的问答摘要。这里的存储载体就是Rediskey是会话IDvalue是经过裁剪的对话记录。第二层是长期记忆记录用户/业务团队常用的指标偏好、常用的时间口径、常看的报表。长期记忆存在向量库里意图识别节点召回这些偏好作为上下文提示用户会明显感觉到“这个Agent越来越懂我们”。从架构位置上看记忆模块横跨编排层和基础设施层。编排层定义记忆的读写接口比如get_session_history、save_session_history基础设施层提供Redis、向量库等具体实现。这样做的好处是可以随时替换底层存储比如Redis集群想换成内存Grid只需要改动infra层的适配代码。记忆模块带来的一个实际副作用是prompt的组装变复杂了。我在开发中专门写了context_builder模块负责把历史记忆、长期偏好、语义模型相关的表结构描述统一拼装成大模型需要的完整上下文。拼装的顺序、截断策略都是有讲究的不是简单地把所有东西丢进去。这里我的经验是优先保留最近一轮完整对话和长期偏好中间细节可以压缩成摘要这样既控制token长度又不会丢失关键信息。5. 常见问题与排查技巧实录5.1 问题速查表架构建设期最常踩的七个坑我整理了一份问数Agent架构阶段的常见问题速查表这些问题不是从文档里抄的而是我们团队实际踩过的。每家公司的数据情况不同但下述问题在问数类项目中绕不开建议直接对照排查。序号现象根因解决方案1模型生成的SQL查出的数和报表组验证的数对不上指标口径没有语义化模型按自己的理解关联了表建指标字典每个指标明确到表、字段、聚合公式严格提示词约定2多轮对话中第二句开始就答非所问上下文截断策略太粗暴或者历史记录没有和当前问题拼接单独做context_builder按最近轮次摘要方式压缩3明明有这个字段模型却说找不到语义模型里的元数据信息太少字段注释不完整给关键字段补详细的业务注释把枚举值含义写清楚4权限绕过通过组合查询能反推明细只做了表和字段级权限没做结果集级脱敏增加输出侧审核小样本场景强制打码或者拒绝5SQL执行慢拖垮整个Agent响应执行器没有单独设置超时和资源限制执行器层强制超时10秒、限制返行数、只读副本6Agent在澄清分支打转用户被问烦了澄清逻辑没有兜底连续澄清超过两次还是没拿到有效信息设置最大澄清次数超过后直接转人工或者按默认参数查询7每次迭代改prompt都要重新发版prompt写在代码里和业务交付强耦合把prompt全部配置化用配置中心动态更新热加载生效5.2 我的排查思路与调优心得上面这七个坑实践中最容易让人焦头烂额的是第一个“对不上数”。因为它的bug不在代码层而在语义层命令行可没法调试。我用了一个“解释-标注-验证”三步法才把口径问题彻底梳理明白。第一步让Agent在输出SQL的同时强制输出“我用到了哪些表和计算逻辑”这一步叫解释。第二步把这些解释推回给数据组同学请他们来标注口径是否准确——这一步叫标注。第三步拿一套标注过的测试集把Agent生成的结果和标注结果做批量比对这一步叫验证。这套流程实际上搭建了一个面向NL2SQL的评测集现在已经成为我们每次模型升级前必须跑完的环节。如果你也在做问数项目我强烈建议从第一天就开始攒评测集哪怕只有100条真实问题也比靠感觉调模型强一百倍。第二个我特别想提醒的调优点是关于模型的选择。问数Agent内部不同节点对模型能力的诉求是完全不一样的。意图识别和结果解读这类节点用中等参数的模型就够了便宜、快效果还稳定但NL2SQL节点一定要用当前能力最强的主力模型因为SQL生成是最吃推理能力的环节。这种“分节点用多模型”的做法表面上增加了架构复杂度实际上省下的token成本和对体验的改善立竿见影。还有一个容易被忽视的优化点是“结果缓存”。相同或相似的查询问题在生产环境重复出现的概率并不低我们在Redis里对“意图hash语义模型版本”维度做了结果缓存缓存命中时直接跳过SQL生成和执行两个高成本节点。实测下来命中率大概在25%到30%之间整体P95时延直接降了40%。这个收益几乎是免费的但前提是缓存key的设计得能有效区分不同口径不然容易拿旧口径的结果糊弄人反而惹祸。6. 从单Agent到Multi-Agent的演进空间6.1 什么情况下“一个Agent干所有事”会失灵LCODER问数项目第一版是一个典型但功能完整的单Agent一个大脑、一套工具、一条主链路。这个架构在业务规模可控的初期是性价比最高的选择。但项目跑了半年后我们明显感受到单Agent的瓶颈所有逻辑都塞在图里状态字段越来越多节点函数越写越长做一个很小的产品改动都要重新评估整个链路的影响面。具体来说有两个信号。第一个信号是“提示词开始互相打架”。当NL2SQL节点不仅要处理常规查询还要处理同比环比、明细透视、多指标对比等各种复杂查询时你为了覆盖所有场景往prompt里不断加few-shot示例最后会发现示例之间冲突模型表现不升反降。第二个信号是“调试成本急剧上升”。链路中任何一个小分支出错都需要把整个状态图拉出来人肉定位这对团队的心智负担太大了。这时候就开始考虑向Multi-Agent架构演进。Multi-Agent不是把一个大Agent拆成一群小Agent的噱头而是当单一职责的提示词和单一图节点已经无法承载业务复杂度时通过角色分化来降低每个Agent内部复杂度、同时提升系统整体泛化能力的工程手段。这个演进必须以当前系统的可观测性为基础如果你连当前单Agent的每一步都追踪不清楚那拆成多Agent只会更乱。6.2 按“角色拆分”演进分析Agent、取数Agent、图表Agent在LCODER的实际演进路线里我们是按“角色”拆的而不是按“功能”拆的。当时设计了三个角色分析Agent、取数Agent、图表Agent。每个角色都是独立可运行的Agent节点由编排层统一调度。分析Agent干掉的是原来“意图识别结果解读”两个节点的工作量升级版它负责和用户对话理解业务问题判断问题应该用哪个数据域去回答然后调度其他Agent。取数Agent专门负责从语义模型生成SQL、校验并执行查询它内部可以维护独立的few-shot示例和错误修正策略不用再担心示例之间的冲突。图表Agent负责把结构化数据转换成用户可读的可视化表达它内部可以只关注图表类型选择、配色和标题生成不再掺和SQL和指标的事。这个拆分的本质是让每个Agent的prompt边界变得清晰从而让模型在各自领域内做到“专家级”表现。实际效果也确实明显NL2SQL的准确率在拆分后提升了一个台阶因为取数Agent的prompt可以做得非常专精不再被图表生成等无关信息干扰。还有一个更轻量的演进方案是用Spring AI Multi-Agent这类框架来编排它和LangGraph思路类似但在Java生态里集成度更好。我们后续会把部分交互型Agent迁到Spring AI Multi-Agent上因为公司主技术栈本来就是Java让后端团队直接维护会更顺手。这个选择说明一个问题Agent编排框架不是标准答案关键看团队的技术栈和场景复杂度。架构师要做的是理解不同框架的心智模型然后在合适的时机做切换而不是死守一个框架不放。6.3 个人实操体会架构是长出来的不是画出来的最后这部分不算总结就说几句我在项目里最真实的体会。第一句别迷信“顶配架构”。我们见过很多团队一上来就规划十个Agent、全套RAG、知识图谱、实时学习结果半年了连一个能稳定回答简单问题的Agent都没交付。问数项目的第一版功能少一点没关系架构留好扩展位就行。LCODER的第一版连图表生成都是规则拼接的但现在回忆起来正因为第一版足够简单我们才能快速上线让业务方尽早把真实问题丢过来这些真实问题才是第二轮架构演进的唯一依据。第二句Agent项目的难点往往不在模型而在模型之外的工程系统。谁能把权限、超时、缓存、评测、可观测性做得靠谱谁才能真正把AI Agent用到生产环境。第三句永远给“用户不按常理提问”留一条退路。我们的Agent上线第一天就有用户问“最近行情怎么样”这完全不在指标字典里。后来我们加了一条通配路径识别不到任何指标时不硬答而是反问“您是想查询业绩、库存还是供应链数据”这个看起来不起眼的分支反而是用户满意度最高的一条路径。我会把LCODER系列的后续内容陆续整理出来下一篇应该会重点细化NL2SQL的语义模型设计和评测集搭建。如果你也在搭自己的Agent或者对问数类项目有别的踩坑经历欢迎回来一起讨论。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进