1. 项目概述:问数项目到底要解决什么问题
当我把“问数项目”四个字发给团队的时候,很多人的第一反应是:这不就是把原来的报表查询加个ChatGPT入口吗?真实做起来才知道,这个认知差得不是一点半点。传统BI看数是“人找数”——你打开报表,筛选维度,点击导出,整个流程是面向固定页面和固定维度的;而“问数”是“数找人”——用户用一句自然语言描述诉求,系统自己去理解指标、定位表、生成查询、返回答案。这个转变表面上是交互方式变了,骨子里是整个架构模式从“查询系统”变成了“智能体应用”。
这里要结合LCODER的背景说一句。LCODER本身是一个面向AI辅助编程和Agent开发的实践社区,我们在做的这个问数项目智能体,是拿LCODER体系里沉淀的编排思想、多Agent协作模板来落地的一个业务示例。目的很直接:为公司内部的运营、产品、财务同学提供一个“用大白话查数据”的入口,减少他们等排期、提工单、看文档的时间损耗。
从业务价值上看,这个项目最值得做的事情不是对线一个聊天机器人,而是把企业内部的数据使用门槛打下来。过去一个非技术同学想取数,要先搞清楚数据库表结构、指标口径、SQL语法,这个过程通常要两三天;Agent化之后,这些复杂度全部转移到工程侧,业务同学只需要描述“我想看最近30天华东区的新增用户数,按天维度”,剩下全部交给Agent处理。
当然,说理想很容易,落地才是关键。问数项目看起来是个“自然语言转SQL”的经典题目,真正难的是两件事:第一,Agent要理解企业内部复杂的指标口径和表关系,不是拿通用大模型一套就完事;第二,Agent必须能被工程体系管理、监控、容错,不能是一个在黑盒里自己乱跑的东西。这两点直接决定了我们在架构选型和实现路径上的所有取舍。
2. 整体架构设计:不是“聊天机器人”,而是“有手有脑的智能体”
定位明确了,我们再来看架构。问数项目智能体的架构设计,核心要回答一个问题:Agent应该以什么形态存在于企业内部?我把它拆成了四层:交互层、编排层、工具层、数据服务层。每一层单独看都不复杂,但把它们串成一个完整链路,需要想清楚数据怎么流动、控制权怎么交接、异常怎么兜底。
2.1 第一层:交互层与意图澄清
交互层最容易理解,就是用户直接接触的那一层。在问数项目里,入口形态可以是一个IM机器人(飞书、钉钉、企微),也可以是一个Web对话页面。交互层的核心不只是聊天窗口,而是把用户的模糊诉求“翻译”成结构化的查询意图。这里有个容易被忽视的细节:交互层必须保留“澄清机制”。用户说“看一下销售情况”的时候,Agent不应该直接去猜表,而应该回问一句“您指的是哪个业务线?时间范围是?”。这个澄清不是靠工程写死的流程,而是通过提示词约束模型在意图置信度不足时主动发问。我们在LCODER的模板里把这类交互封装成“意图确认节点”,效果比让模型自由发挥稳定得多。
以“看一下销售情况”这句话为例,不同角色对“销售情况”的理解完全不同。运营想看的是订单量和转化率,财务想看的是回款和毛利,销售负责人想看的是区域排名和环比增速。如果Agent不问清楚就直接去查数,大概率给出一个看似正确但没人真正想看的答案。我们的做法是:当意图识别节点对指标和维度的置信度低于0.6时,强制进入澄清子流程,用选择题或填空式的追问引导用户补齐信息,而不是放任模型继续向下游执行。
2.2 第二层:编排层与图结构状态机
编排层是整个Agent的“大脑”,也是架构设计的重点。传统的“输入-处理-输出”串行流程根本跑不动问数这种复杂场景,因为你没法预估用户会提出什么问题、需要几步才能完成。我们采用的是基于图结构的编排方式,把整个任务拆成“意图识别→查询计划生成→工具调用→结果校验→回复生成”这样一个有向图,中间允许有分支、循环和回退。这套思路最成熟的开源载体是LangGraph,它把Agent的每一次状态变换都变成图里的一个节点,节点之间通过条件边流转,这样既保留了ReAct模式的灵活性,又能通过图结构对流程做显式的控制。
为什么要选图编排而不是纯粹的“让大模型自由发挥”?说白了,可观测性和可控性。自由发挥的Agent在demo里惊艳,在生产环境里就是定时炸弹。图编排允许我们在任意节点记录输入输出、注入人类审批、设置循环上限,这些东西在架构评审的时候是必须拿得出手的。另外,图结构的另一个好处是支持“子图嵌套”——我们可以把“指标查询”做成一个独立子图,然后在更上层的主图里引用它,这样多Agent协作、流程复用、单点测试都变得可行。
2.3 第三层:工具层与MCP接入
工具层是Agent的“手脚”。问数项目里最核心的工具是两类:一类是元数据查询工具(获取表结构、字段注释、指标口径),另一类是数据查询工具(执行SQL返回结果)。此外还有指标推荐工具、图表生成工具、权限校验工具等等。这里强调一个架构设计上的原则:工具接口的粒度宁可细一点,不要粗。经验是,单个工具只做一件事,参数尽量少,工具描述写清楚“什么时候用、怎么用”,这样模型才容易正确选择。把一堆功能揉进一个大而全的“执行SQL”工具里,看似简化了接口,实际上是在考验模型的工具选择能力,踩坑概率极高。
工具层的协议选型我们最终采用了MCP标准。MCP把工具以标准化server的形式对外暴露,任何支持MCP的客户端都可以直接调用。在这个项目中,我们把元数据查询工具和数据查询服务封装成两个独立的MCP server,编排层通过标准协议发现和调用。这个设计的长期价值在于:将来如果想把同样的工具能力开放给其他Agent或者第三方系统,不需要重新开发一套接口,直接用MCP client去连就行。
2.4 第四层:数据服务层与语义层隔离
数据服务层负责接住工具层发过来的查询请求,把SQL执行、数据权限校验、数据脱敏这些能力全部收敛在这一层。这么设计的好处是:Agent编排层可以完全不用关心底层数据存在哪、用什么查询引擎,只需要知道这个数据服务接不接。在实际落地时,我们接的是一套统一指标服务,SQL不是直接打到业务库里,而是打到指标语义层,由语义层负责把指标名翻译成物理SQL。这层抽象是问数项目能够“多业务线复用”的关键。如果你直接让Agent生成SQL去打业务库,第一轮可能还行,等表结构一改、业务字段一调整,Agent生成的SQL基本全废,维护成本会立刻失控。
这里要特别说明数据安全的下沉策略。最开始我们试图在提示词里约束模型“只查询用户有权限的数据”,实际效果非常差。后来把权限校验全部下沉到数据服务层:SQL执行前由服务端统一注入数据权限过滤条件,不依赖模型自觉。这个改动是问数项目安全性的关键转折点——权限是硬规则,必须由代码保证,而不是靠提示词约束。
3. 技术选型与工具链解析
架构层面有了蓝图,技术选型就变成了最现实的博弈。市面上Agent编排框架多得吓人,选型必须结合团队实际情况。这里把我们在问数项目中经过对比验证的核心选型思路逐一讲透,包括LangGraph、Spring AI Multi-Agent和MCP协议,以及它们各自在项目里的生态位。
3.1 LangGraph:把Agent流程变成可控的图
LangGraph在Agent项目里这么受欢迎,核心原因是它把“图状态机”和“大模型调用”优雅地结合在一起。它允许你定义状态对象,每个节点返回状态增量,边可以是固定的也可以是条件的。翻译成人话就是:你可以非常精确地控制Agent每一步的行为,哪些步骤必须串行、哪些可以并行、出错之后怎么回退。
在问数项目里,LangGraph带来的直接收益是“流程可测试性”。你可以不看模型实际生成了什么,只通过图结构的转移路径来判断运行是否正常。比如用户问“本月华东销售”,正确路径应该是“意图识别→指标推荐→SQL生成→执行→回答”,如果日志显示走错了分支,问题定位会非常快。另一个实际收益是断点恢复机制。LangGraph原生支持checkpointer,可以把每一步的中间状态持久化到数据库。这个能力在做长耗时查询时特别好用——Agent调完SQL执行工具发现要跑30秒,没必要让用户干等,先把状态存下来,异步恢复继续生成回复即可。
3.2 Spring AI Multi-Agent:Java团队的务实切入点
如果团队是Java技术栈,不打算引入Python服务,Spring AI的Multi-Agent模块值得认真考虑。它提供了一套基于Java的Agent抽象,支持ChatClient、Tool Calling、多Agent协作等功能。好处是能和现有的Spring Boot微服务体系无缝整合,Maven引一下依赖就能用。
不过有一说一,Spring AI在Agent编排的成熟度上还比不上LangGraph。它的定位更像是一个“快速起步工具箱”,适合中小规模应用、编排逻辑相对简单的场景。如果你需要细粒度的条件路由、复杂的状态回溯、长时间运行的任务状态持久化,Java生态里目前的方案还是偏早期。我们团队最终的选择是LangGraph为核心编排层,Spring Boot作为外围接口服务,这算是取两者之长的折中方案。
这个组合的具体形态是这样的:用户的HTTP请求先到Spring Boot网关,网关做认证鉴权、限流、会话管理,然后把请求转发给Python侧的LangGraph编排服务。编排服务跑完Agent流程,把结果返回给Spring Boot,再回给前端。Python负责“智能”,Java负责“接入”,各干各擅长的事。如果将来有大并发需求,Python侧做了个无状态化设计,可以通过消息队列横向扩容。
3.3 MCP协议:工具接入的“统一语言”
MCP(Model Context Protocol)是AI Agent开发绕不开的一个词。它的核心价值是标准化了“大模型如何调用外部工具”这个接口协议。在没有MCP之前,每个Agent框架都要自己定义一套工具调用标准,换个框架工具层基本要重写;有了MCP之后,工具以标准化的server形式对外暴露,任何支持MCP的客户端都可以直接调用。
在问数项目的架构里,我们把数据查询工具、元数据工具封装成了标准MCP server,编排层通过MCP协议去发现和调用工具。这里要给大家一个实用建议:不要一开始就追求把所有工具都变成MCP标准,第一版直接把核心的数据查询工具做成MCP,其他工具先用本地函数封装,等跑通了再逐步迁移。过度设计在Agent项目里是常态,但架构演进是渐进式的,一上来就铺太大的摊子,维护成本会把自己压垮。
4. 实操环节:搭建问数Agent的最小可用架构
这一节走一遍实际的搭建过程,目标不是完整的生产系统,而是一个能跑通的“最小骨架”——意图识别、查询计划、工具调用、结果回复这四件事都能动起来。整个实现基于LangGraph和Python,代码结构尽量精简,方便后续扩展。
4.1 定义Agent的状态与数据流
用LangGraph开发,第一步永远是定义State。问数项目的State建议至少包含这些字段:
- messages:多轮对话的消息列表,大模型上下文的基础
- intent:当前识别到的用户意图
- query_plan:拆解后的查询计划,包括时间范围、维度、指标列表
- tool_calls:本轮需要调用的工具记录
- intermediate_results:工具返回的中间结果
- final_answer:最终返回给用户的内容
State的设计决定了Agent能力的边界,这一步不要着急写代码。建议先把业务流程里所有可能出现的“中间产物”列出来,再去看哪些需要进State,哪些可以只在节点内部消化。State里塞太多临时变量会让整个图变得极难调试。用TypedDict定义即可,加上total=False允许字段可选,这样在节点执行顺序上有更大的灵活性。
from typing import TypedDict, Optional class QueryPlan(TypedDict): metrics: list[str] dimensions: list[str] time_range: str filters: dict class AgentState(TypedDict, total=False): messages: list[dict] intent: str query_plan: QueryPlan tool_calls: list[dict] intermediate_results: list[dict] final_answer: str4.2 实现意图识别与查询计划生成节点
意图识别用大模型做绝不复杂:准备好系统提示词,告诉模型用户的问题属于哪几种意图(查数据、看趋势、做对比、问口径、闲聊),让它输出JSON格式的结果。代码层面就是一个函数,调用一次LLM,解析返回值。这里有一个实用的技巧:把大模型的输出格式约束为JSON Schema,并要求模型严格按Schema输出,解析代码几乎不需要做异常处理。
INTENT_PROMPT = """你是一个问数系统的意图识别模块。请判断用户诉求属于哪一种意图: - query_data: 需要查询具体数据 - compare: 需要对比两个或多个维度的数据 - trend: 需要查看趋势变化 - ask_metric: 想了解指标口径或定义 - chitchat: 与查数无关的闲聊 输出JSON: {"intent": "意图类型", "confidence": 0-1之间的分数, "reason": "判断理由"}""" def recognize_intent(state: AgentState) -> AgentState: response = llm.chat([ {"role": "system", "content": INTENT_PROMPT}, *state["messages"] ]) result = json.loads(response.content) state["intent"] = result["intent"] return state查询计划生成节点稍微复杂一点。它需要把“本月华东销售”拆成“时间范围=本月,区域=华东,指标=销售额”,同时要映射到对应的表和字段。这块依赖的是一套指标字典,通过少量示例和描述传给模型做few-shot,让模型学会“指标名→表字段”的映射规律。第一次跑的时候准确率可能只有六成,别慌,这是正常现象,后面通过工具调用返回的元数据反馈去迭代提示词。
4.3 工具注册与ReAct循环搭建
LangGraph里工具调用最常见的方式是ToolNode。把数据查询工具注册成ToolNode节点,然后在大模型节点和ToolNode之间连一条条件边:模型判断需要调用工具就走ToolNode,调用结束回到模型节点继续生成。这个循环就是ReAct模式的落地形态——Reason(推理)和Act(行动)交替进行。
from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode, tools_condition graph = StateGraph(AgentState) graph.add_node("intent", recognize_intent) graph.add_node("plan", generate_query_plan) graph.add_node("llm", call_model) graph.add_node("tools", ToolNode([query_data_tool, get_metadata_tool])) graph.set_entry_point("intent") graph.add_edge("intent", "plan") graph.add_edge("plan", "llm") graph.add_conditional_edges("llm", tools_condition, {"tools": "tools", END: END}) graph.add_edge("tools", "llm")这里注意tools_condition是LangGraph预置的条件函数,它会检查模型最后一条消息是否包含tool_calls字段,有就走工具节点,没有就结束。实际项目中还需要在llm节点后面加一个max_iterations检查器,防止Agent无限循环。我给这个项目设置的默认上限是5轮工具调用,超过强制结束并提示用户拆解问题。
4.4 处理工具返回结果的大小问题
工具调用有一个必须提前踩的坑:工具的返回结果,尤其是SQL查询结果,可能会非常大。如果你把上万行结果直接塞回给模型当作上下文,一次对话的Token就可能烧掉几十万。我们的做法是分两层处理:结果大于一定行数时先做聚合摘要,或者只返回前N行加进度提示;需要完整数据时走下载链路,不经过模型上下文。
方案一:在数据查询工具内部做行数限制,默认只返回前50行,同时附带total_rows字段告诉模型总行数。
方案二:如果是超大结果集,数据查询工具直接把结果写入临时存储(对象存储或临时表),返回给模型的是一个下载链接而不是数据本身。这样既节省Token,又能保证数据的完整性。
这两个方案配合使用效果最好。日常的“本月销售额是多少”这类问题走方案一就够了;用户需要“导出一份所有订单明细”的时候走方案二。实现的细节是,数据查询工具接收一个参数output_type:detail返回全部数据,summary返回聚合摘要,根据参数决定处理逻辑。
4.5 接入大模型与提示词工程
模型本身的选择,问数场景推荐优先考虑带较强SQL能力和工具调用能力的模型,当前市面上的主流大模型基本都达标。真正拉开差距的是提示词怎么写。在写问数Agent提示词时的一个核心原则:给模型“边界感”。系统提示词里明确告诉模型哪些能做、哪些不能做、不确定时怎么处理,以及必须遵守的输出格式。比万能的提示词更管用的是给模型几个“标准样例”,让它照着样例的风格来回答。
SYSTEM_PROMPT = """你是一个企业数据分析助手,负责把用户的自然语言问题转换为数据查询,并以通俗的语言回答。 你可以使用的工具: - query_data(sql): 执行数据查询,返回查询结果 - get_metadata(table): 获取表结构和字段说明 规则: 1. 必须基于工具返回的真实数据回答,不允许编造数字。 2. 如果用户问题含糊不清,必须先澄清,不要猜测。 3. 查询结果只返回前50行或聚合摘要,数据较多时提示用户是否有更细的筛选条件。 4. 用户问到你不知道的指标时,先调用get_metadata查看表结构,再决定怎么查询。 5. 回答风格:先用一句话概括结论,再列出关键数据,最后给出必要的解读。 示例: 用户问:上月华东区的销售额是多少? 你的回答:上月(2025年3月)华东区销售额为 1,235 万元,环比增长 8.2%。 """注意提示词中明确要求模型“必须基于工具返回的真实数据回答”,这一条能很大程度扼杀模型的幻觉冲动。我见过不少问数项目翻车,查的根本原因都是模型在数据缺失时自己“脑补”了一个数字,用户拿去开会汇报,后果非常严重。
5. 实践踩坑记:五大高频问题与排查方法
单看每一步都不算难,难的是系统串起来跑之后各种隐藏问题就冒出来了。这一节记录问数项目里最典型的高频问题和排查经验,每一类问题都对应一套可复用的解决方法。
5.1 大模型幻觉导致SQL与业务口径不一致
最经典的翻车现场:用户问“本月销售额”,模型生成的SQL是按订单金额汇总,但业务口径其实是按回款金额。这类问题的根因是模型没有正确理解指标口径。解决思路是在工具层增加“指标解释查询”——模型在执行SQL之前,先调用元数据工具获取相关指标的详细定义,如果定义模糊就向用户确认。另外在指标字典里加上了别名和反例,显著降低了口径误判的概率。
这个问题的排查通常很隐蔽,因为光看SQL很“对”,语法正确、表名正确、字段存在,但算出来的数和业务口径对不上。我们在日志里加了semantic_anchor字段,记录模型使用的指标ID,和业务侧维护的指标口径库做交叉验证,一旦发现指标ID匹配异常就发出告警,由专人介入确认。
5.2 Agent死循环或调用链过长
一旦工具调用链超过三步,模型就开始“绕圈”,在同一个工具上来回调用,最终耗尽预算。这里不要指望模型自己收敛,架构层面必须给硬约束。我们在LangGraph里设置了最大迭代次数,超过就强制结束并回复“这个问题太复杂,请尝试拆分成多个问题”,同时加上了“如果同一工具连续调用超过2次,触发人类审批”。这些硬性护栏在demo中可能显得多余,但生产环境是救命的。
排查这类问题时,重点看两个指标:平均Agent循环深度、单次任务Token消耗分布。如果发现某类用户问题的循环深度普遍偏高,说明查询计划生成节点对任务的拆解不合理,需要优化提示词。如果是偶发的“卡死”问题,多半是工具的返回格式让模型理解困难,需要在工具描述里补充更详细的格式说明。
5.3 工具调用链过长导致Token耗尽
这是问数项目上线初期最让人头疼的问题。用户问一个稍微复杂点的问题,Agent可能要调4-5次工具,每次工具返回可能带上千行样本数据,一轮跑下来Token消耗是正常值的10倍以上。优化手段有三板斧:第一,工具返回结果精简再精简,能返回聚合值就不返回明细;第二,把多轮工具调用合并成一次批量调用,比如同时查销售额和订单量这种可以并行执行的动作,放进同一个工具请求里处理;第三,引入缓存,对相同或高度相似的查询结果做短期缓存(TTL设置为5-10分钟),减少重复计算。
5.4 权限控制如何嵌入Agent
问数项目会对不同角色开放不同的数据范围。一开始我们做了最粗暴的方案:在提示词里告诉模型“这个用户只能看华东数据”。结果模型根本不买账,有时候会把条件忘掉。后来把权限校验下沉到工具层:SQL执行前由数据服务统一注入数据权限过滤条件,而不是依赖模型记住。这样即使模型生成了全量SQL,最后执行出来的也只会是用户有权看到的数据。这个改动是权限安全的关键转折点。
具体实现是让用户认证模块在通过认证后,在请求上下文中注入一个permissions对象,包含用户角色和可见数据域。数据查询工具执行前调用权限过滤组件,自动把SQL改写为带权限条件的版本:原始SQL是select count(*) from orders where date >= '2025-01-01',改写后变成select count(*) from orders where date >= '2025-01-01' and region in ('华东', '华南')。整个过程Agent无感知,也不用担心模型“忘记”加权限条件。
5.5 元数据过期导致Agent“瞎猜”
问数项目另一个坑是元数据不同步。业务侧改了字段名、删了表,Agent拿到的还是旧元数据,生成的SQL自然是错的。应对办法是给元数据服务加缓存失效机制:业务方在更新表结构后调用接口主动刷新元数据缓存;同时每天早上定时同步一次。另外,当数据查询工具因为字段不存在报错时,不要只是把异常信息丢给模型,而是自动触发一次元数据刷新,再让模型根据最新元数据重新生成SQL。
6. 下一步规划与实践总结
问数项目智能体的第一期遮“项目架构”,核心要传达的始终是那个观点:做Agent项目,难度不在“智能”,而在“工程”。这期文章的篇幅主要集中在架构设计的逻辑和技术选型的权衡上,因为架构没定清楚,后面一切开发都是在沙滩上盖楼。在问数项目里,我们靠LangGraph把流程变成了可控的图,靠MCP把工具变成了可复用的标准服务,靠数据服务层的语义隔离把权限和数据安全牢牢攥在工程侧,这几件事做到了,Agent本身“聪明不聪明”反而成了次要问题。
后续这个系列我计划继续往下拆,按“项目架构→指标语义层→多Agent协作→生产可观测性”的顺序推进。第二篇重点写指标语义层怎么设计,包括指标字典的结构、口径识别的方法、以及Agent如何利用语义层提高SQL生成的正确率;第三篇写多Agent协作,如何把查询、分析、图表生成拆成独立Agent,以及它们怎么通过编排层协同工作;第四篇写生产环境的可观测性与评估体系,比如怎么给Agent的每一次查询打分、用哪些指标评价一个Agent系统的健康度。
最后说一个我自己测试时候的体验:问数Agent这类项目,demo里跑通一个漂亮案例一点都不难,难的是把它放在真实业务流里跑一个月、跑三个月,还能保持稳定和可信。这就要求我们在架构阶段多留一些“监控眼”和“逃生通道”——让我再选一次的话,我会把可观测性设计提前到第一天就纳入架构,而不是等出了问题再去补。这个教训,希望看这篇文章的朋友们能直接带走。