简介:本资源是一个基于LangChain框架构建的多智能体数据检索与可视化系统实现方案,面向Python中级开发者、AI工程实践者及数据分析方向学习者,解决多源异构数据协同检索、自然语言交互式查询与动态可视化呈现的一体化技术落地问题。压缩包共14个文件,含4个核心Python脚本(如main.py主流程、graph.py图谱构建、display.py可视化渲染)、8张系统界面与架构示意图(PNG),以及requirements.txt依赖清单和README.md使用说明,整体大小5.68MB,结构清晰、开箱即用。已有58人下载学习,可直接运行并快速理解多智能体分工协作机制、LangChain链式调用设计、Streamlit动态交互图表集成等关键技术点;配套图像涵盖系统架构图、检索流程图、可视化效果截图及模块关系图,便于对照代码理解整体设计逻辑与实现细节。 很多做数据的朋友问过我同一个问题:数据报表工具那么多,为什么还要自己搭一套基于Langchain多智能体的检索与可视化系统?我的回答通常是——因为市面上的工具解决不了“我问一句,它给我一张图”这件事,尤其是当业务方的问题横跨多张表、多种口径,甚至问题本身都带着歧义时。这套系统的核心,是把“理解问题、写SQL、查数据、画图表”拆成多个Agent协作完成,而不是让一个Agent包打天下。这篇文章会完整复盘我如何用Langchain搭建多智能体数据检索与可视化系统,包括架构设计、核心代码思路、部署细节,以及我在实际使用中踩过的坑。适合正在做数据问答、指标平台、报表自动化的开发者参考,也适合想搞懂多智能体到底怎么落地的朋友。
1. 为什么“查数+出图”这件事,单Agent搞不定
1.1 单Agent方案的三个典型失败场景
我最早做的版本非常朴素:一个Agent,把数据库Schema、图表规范、业务口径全部塞进同一个Prompt,让它一次完成“理解问题-生成SQL-查询-返回图表”。表面上看很省事,实际跑起来却频繁翻车,而且翻车的模式很有规律。
第一个典型问题是角色冲突。同一个模型在同一段上下文里既要扮演“严谨的数据工程师”去推断SQL逻辑,又要扮演“懂审美的可视化设计师”去选择图表类型。这两个角色对语言的敏感度完全不同。要求它严格时,它连“退货率”这种业务指标都不敢推断;要求它灵活时,它在SQL里开始自作主张地加减字段。最后的结果是,SQL生成质量被图表的开放要求拖累,两边都做不好。
第二个问题是上下文膨胀。系统Prompt里塞了十几张表的建表语句、五六个业务口径、三四条图表规范,每次请求的Token消耗非常惊人。更麻烦的是,对话一旦多轮,历史消息和系统提示词混在一起,模型很容易遗忘早期的约束条件,回答开始前言不搭后语。
第三个问题是最致命的——没有兜底机制。单Agent生成的SQL如果错了,没有任何环节能拦截,直接送进数据库执行。运气好时返回一个报错,运气不好时返回一个看似正常、实际口径完全错误的数,而业务方往往发现不了。
1.2 一次实测对比:单Agent vs 多Agent
我拿同一批测试问题做过对比,一共50条覆盖“趋势查询、对比查询、占比查询、明细查询”四类场景。单Agent方案的SQL生成正确率在58%左右,而且一旦涉及多表关联,正确率直线下降到40%以下。后来拆成多Agent,由意图路由、SQL生成、审查、可视化四个角色分工协作,同样的50条问题,SQL首轮正确率提升到76%,加入审查反馈重写后,最终正确率稳定在88%左右。
这个提升并不神秘。多智能体的本质不是用了更聪明的模型,而是把复杂任务拆成了多个可以分别校验的简单环节。每个Agent的Prompt更短、目标更单一,模型不需要在同一个上下文里切换角色,输出自然更稳定。而且审查Agent会把“挑错”这个动作显式建出来,让错误在进入数据库之前就被拦截一轮。
2. 系统架构:四个Agent如何分工协作
2.1 项目目录与角色划分
我的工程结构比较清晰,项目名叫langchain-mas-visual,目录如下:
langchain-mas-visual/ ├── app.py # Flask入口 ├── agents/ │ ├── router_agent.py # 意图路由Agent │ ├── query_agent.py # SQL生成与执行Agent │ ├── review_agent.py # 审查Agent │ └── visual_agent.py # 可视化Agent ├── core/ │ ├── llm_factory.py # LLM实例管理 │ ├── schema_loader.py # 元数据加载与裁剪 │ ├── memory.py # 会话记忆 │ └── data_pipeline.py # 查询结果统一封装 ├── templates/ │ └── index.html # 前端页面 └── static/ ├── echarts.min.js └── app.js四个Agent的职责非常明确。Router Agent负责判断用户问题类型,是数据查询还是普通闲聊,顺便提取时间范围、地区等关键维度;Query Agent负责根据Router传过来的结构化意图,结合Schema生成SQL并执行查询;Review Agent是质检员,用独立的视角检查SQL或查询结果是否有问题,发现问题就附上修改建议丢回给Query Agent;Visual Agent负责分析查询结果的数据特征,生成图表配置。
2.2 协作流程:不止是链式调用
很多文章讲到多智能体就喜欢画一张“A做完给B,B做完给C”的串行图。我的实践是,这套系统虽然整体是串行主线,但中间存在一个非常重要的反馈回路,也就是Query Agent和Review Agent之间的“正反博弈”。
以用户问“华东区上个月退货率走势”为例,整个过程是这样的:
- Router Agent先判断这是一个数据查询请求,并输出结构化参数:区域=华东、时间=上月、指标=退货率、意图=趋势。
- Query Agent拿到参数后,从Schema里只加载相关表,生成一条SQL,例如
SELECT month, return_rate FROM order_summary WHERE region='华东' AND month >= ...。 - SQL不会立刻执行,而是先交给Review Agent。Review Agent会从业务口径审查:退货率的口径是什么?分子分母有没有算反?时间条件是否合理?如果发现SQL里退货率字段是比率值,但查询结果需要的是分子分母两个字段时,Review Agent会给出意见,例如“退货率需要单独计算,请改为同时查询退货订单数和总订单数”。
- Query Agent根据意见重写SQL,再次交给Review。除非双方达成一致,否则这个回合不会结束。
- SQL通过后才会执行查询,结果交给Visual Agent判断用折线图还是柱状图,生成ECharts配置,由前端渲染。
这条协作链路的核心在于“生成-审查-修正-执行-呈现”的闭环,而不是单向的传递。实测中,这个博弈机制能把SQL正确率从76%拉高到88%,效果显著。
2.3 状态传递:统一数据结构的价值
多智能体系统里最容易被忽略的问题是状态传递。我见过不少实现,两个Agent之间靠字符串拼接传递信息,比如把Prompt后面追加一段“这是上一个Agent生成的内容:xxx”,这种方式既浪费Token又容易丢字段。
我的做法是定义一个统一的中间数据结构QueryResult,所有Agent都往里填内容:
@dataclass class QueryResult: question: str # 原始用户问题 intent: str # 路由结果 sql: str # 最终执行的SQL columns: list # 列名列表 data: list # 查询结果 visual_type: str # 推荐图表类型 error: str = "" # 错误信息Router只负责填intent和question,Query Agent只负责填sql、columns、data,Visual Agent只读columns和data来生成图表。这样做的好处是,任何一个环节出问题都可以通过打印这个对象快速定位;每个Agent的输入输出都有稳定的协议,后面的Agent不会收到一堆格式混乱的中间产物。
3. 数据检索链路:从中文问句到可执行SQL
3.1 Schema加载:不是把建表语句全塞给模型
数据检索系统的命脉是Schema设计。直接把所有表的CREATE TABLE语句丢给大模型是最偷懒也最蠢的做法。一来Token消耗大,二来无关表会干扰模型判断。比如用户明明问的是订单,结果Schema里有用户表、商品表、库存表,模型就可能猜错关联关系。
我采用的方案是两层元数据过滤。第一层是表清单,只包含表名、表注释、行量级;第二层是字段详情,只在确定了候选表之后才加载。Router Agent先根据表清单判断可能涉及哪几张表,Query Agent再加载这些表的详细字段。
字段详情也不是简单罗列,而是一个结构化的元数据字典:
table_schema = { "table_name": "order_detail", "table_comment": "订单明细表", "columns": [ {"name": "order_id", "type": "string", "comment": "订单号"}, {"name": "region", "type": "string", "comment": "大区,枚举值:华东/华南/华北/西部"}, {"name": "return_flag", "type": "int", "comment": "是否退货,1为退货,0为正常"}, {"name": "order_amount", "type": "decimal", "comment": "订单金额,单位元"}, {"name": "order_time", "type": "datetime", "comment": "下单时间"} ] }“region的注释里写明枚举值”这个细节非常重要。如果没有枚举值说明,模型可能把“华东区”推断成模糊匹配,或者写错条件。枚举值注释能显著降低条件过滤的出错率。
3.2 Query Agent的Prompt设计
Query Agent的System Prompt我反复调了很多版,最终固定为四个部分:角色定义、Schema片段、输出格式、执行约束。
角色定义很简单但有效:“你是一名资深数据分析师,只负责根据用户问题生成SQL,不要回答其他问题。”这个“只负责”三个字能大幅减少模型跑偏的概率。
输出格式我强制要求JSON结构化,包含sql、explanation、potential_issue三个字段。explanation是给Review Agent看的执行逻辑说明,potential_issue是Query Agent自己觉得可能有歧义或风险的点。这个设计最初是为了方便调试,后来发现它还起到了“自我反思”的作用——模型在写SQL时就会下意识检查自己哪里可能出错。
3.3 Review Agent的博弈机制
Review Agent是整个系统的质量闸门。它的System Prompt刻意写得“刁钻”:“你是一名严谨的审计员,请挑出SQL中的错误、口径歧义、性能隐患和潜在风险。如果发现任何问题,返回修改建议;只有确认无误才返回通过。”
实际运行时,Query Agent生成的SQL和explanation会一起传给Review Agent。Review Agent会重点检查三个方向:
- 业务口径:退货率有没有该除的地方没除,百分比字段有没有当绝对值用;时间范围是否和问题匹配,比如“上月”是否被正确翻译成月初到月末。
- 语法与性能:多表关联是否缺少条件,是否可能出现笛卡尔积;有没有该加
LIMIT却没加的查询。 - 隐藏假设:用户没提但模型默认添加的过滤条件是否合理。
如果Review Agent不通过,它会返回一段结构化意见,包括问题类型、问题说明、修改建议。Query Agent会带着这段意见重新修改SQL,然后再次送审。我设置最大博弈轮数为2,超过两轮则采用Review Agent的意见作为兜底,避免无限循环导致延迟过高。
3.4 执行安全:不敢省掉的三道防线
数据检索系统最怕的不是模型答错,而是模型生成的SQL在数据库里“闯祸”。我在执行层做了三道防线。
第一道是数据库账号权限。系统使用的数据库账号只有SELECT权限,没有INSERT、UPDATE、DELETE、DDL。这是底层防线,也是最可靠的一层。
第二道是SQL静态检查。在SQL发送给数据库前,用规则过滤拦截可疑语句,包括注释符、分号拼接、INTO OUTFILE、SLEEP()等危险模式。这一步不是用来替代权限管控的,而是为了尽早拦截、早点告警。
第三道是查询兜底。所有查询自动加上LIMIT 2000,防止一次取出几十万行数据把内存打爆;同时设置超时时间,执行超过10秒就取消任务并提示用户“查询量过大,请缩小时间范围或增加筛选条件”。
3.5 查询结果的缓存策略
数据分析场景中,很多用户会反复查询相同的问题,比如“本月每天的订单量”这类固定报表。我在系统里增加了基于LRU的缓存层,缓存Key是“规范化后的SQL+参数”。短时间内的重复查询直接命中缓存,不再调用LLM,既省了Token又降低了延迟。
4. 可视化呈现:如何让数据自动“选对图”
4.1 图表类型推荐:不是玄学,是规则
Visual Agent是我认为整套系统里最容易被低估的部分。很多人以为可视化就是套一个模板,实际不然。图表选型错了,数据再准确也白搭。比如展示时间序列趋势用饼图,读者根本看不出走势。
我的Visual Agent不做复杂的“审美推理”,而是基于数据特征和查询意图的规则化判断:
- 查询意图是“趋势”:且结果包含时间字段,用折线图。
- 查询意图是“对比”:结果里有分类字段和一个数值字段,用柱状图。
- 查询意图是“占比”:结果里有分类字段且数值总量接近100%,用饼图。
- 查询意图是“明细”或“排行”:结果行数小于50,直接用表格,行数超过50再考虑用柱状图TopN。
这些规则被写进Visual Agent的System Prompt,模型只需要根据意图和结果数据的列类型做选择,几乎不会出错。
4.2 ECharts配置:让模型生成配置的坑
一开始我尝试让LLM直接输出完整的ECharts option对象,事实证明这是灾难。ECharts的配置项非常灵活,同一个含义可以用多种写法实现,模型经常输出冗余字段或错误属性,前端渲染出来是一张错乱的图。
后来我改成“模板注入+局部生成”策略。前端页面写死ECharts option的骨架,只把xAxis.data、series.data、title.text、series.type这几个字段留空,让Visual Agent以“填写卡片”的方式补充这些内容。模型不需要理解ECharts全部配置体系,只需学会“把数据填进这4个字段里”,出错率大幅下降。
4.3 前端页面与交互
前端我用一个极简的Jinja2模板,页面布局是左侧对话区、右侧图表区。用户输入问题后,前端通过fetch向后端发起请求,后端返回JSON,包含SQL、数据、推荐图表类型、ECharts配置。前端拿到配置后直接调用echarts.setOption渲染。
这里有一个值得注意的交互细节:后端接口如果等到全部Agent跑完才返回,用户体验会很差,因为一次完整查询可能需要10秒以上。我最终采用了流式响应的思路,后端在“路由完成”“SQL生成完成”“审查通过”“查询成功”“图表生成完成”这几个节点分别向前端推送进度信息。前端在图表区显示阶段状态,用户能实时看到系统“正在做什么”,等待的焦虑感会大幅降低。Flask自带的流式响应(Response(generate(), mimetype='text/event-stream'))足以支撑这个需求。
5. 工程化落地:Flask承接Langchain多Agent的部署实践
5.1 技术选型:为什么是Flask
这套系统本质上是一个内部数据问答平台,用户量不大,并发要求不高,但迭代速度快。选Flask而不是FastAPI的原因很简单:团队熟悉、生态成熟、模板渲染方便。FastAPI的异步性能优势在这个场景下发挥不出来,反而会增加团队的学习成本。
如果你要做的系统面向公网、并发较高,建议直接上FastAPI。但如果是企业内部的取数工具,Flask完全够用,不需要为了“潮流”而引入额外复杂度。
5.2 会话记忆:让追问不再失忆
多Agent系统的记忆管理比单Agent更复杂。因为每个Agent都可能需要访问历史会话,但记忆又不能全部塞给所有Agent,否则Token消耗无法接受。
我采用的方案是分层记忆。Router Agent持有完整的对话摘要,用来理解用户追问中的指代信息,比如“把上个月也加进来”;Query Agent只持有最近一轮的QueryResult,因为SQL生成只需要当前问题相关的上下文,历史SQL对它来说反而是干扰;Visual Agent不持有对话记忆,只根据当前查询结果出图。
对话摘要用LLM自动生成,每次对话结束后把本轮的关键信息压缩成一两句话,追加到长期记忆里。这样既保留了指代消解所需的信息,又不会让上下文无限膨胀。
5.3 并发控制与LLM限流
接LLM的API时遇到一个很现实的问题:并发超限。团队只是几个人内测,但多Agent串行调用会让每个请求产生三四次LLM调用,同时来几个请求就触发了限流。
我的解决方案是一个简单的内存限流器,用信号量控制同时进行的LLM调用数量,超过阈值则排队等待。对于内部系统来说,这比引入消息队列和任务队列简单得多。
还有一个容易忽略的点:延迟。一次完整的多Agent流程需要多次LLM调用,每个Agent的模型参数不同。我做了差异化配置:Router Agent用轻量模型,Query Agent和Review Agent用更强的模型,Visual Agent用中等模型。这样既不憋屈“看路”的环节,也不浪费钱在简单任务上。
5.4 配置管理与密钥保护
LLM的API Key、数据库连接串绝对不能硬编码在代码里。我在项目根目录放一个.env文件,用环境变量的方式注入。.env不进Git仓库,团队内部分发时只给.env.example模板。
数据库连接串还有一个额外的建议:不要给超级管理员账号,单独创建一个只读账号,既是为了安全,也是为了让开发环境与生产环境隔离。
6. 实测中的坑、教训与下一步取舍
6.1 高频踩坑记录
这里整理几个我在实测中遇到的高频问题,每一个都真实影响了最终的系统稳定性。
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| SQL里时间条件总写错 | Schema中的时间字段是datetime,模型默认按date处理 | 在字段注释中显式标注“类型为datetime,需处理时分秒” |
| 多表关联出现重复数据 | 模型搞不清楚一对多关系,导致JOIN结果膨胀 | 在表清单中补充表间关系说明 |
| Review Agent误报过多 | 审查Prompt太严格,总把正确SQL当错误打回 | 调整Review Agent的判定标准,只有确定性错误才打回 |
| 图表数据轴顺序乱 | 查询结果没有排序,导致折线图乱序 | 在Query Agent Prompt中要求按时间字段排序 |
| 用户追问“刚才那个”失忆 | 记忆只保存在单次会话中,刷新页面即丢失 | 增加基于会话ID的Redis持久化 |
6.2 关于Langchain和LangGraph的取舍
很多人在多智能体项目里会纠结要不要上LangGraph。我的经验是,如果你的协作流程相对固定,比如“路由-生成-审查-执行-可视化”这个链路基本不变,用Langchain的Agent + 自定义工作流已经足够。LangGraph更适合流程不固定、需要动态编排、分支很多、循环很深的场景。
当前项目里我用的其实是Langchain表达式语言(LCEL)来组合流程,配合手动实现的上面的博弈循环。如果未来要支持更复杂的多轮修复、并行查询多个数据源、按条件动态选择处理路径,我再考虑把底层换到LangGraph。
6.3 成本与Token优化
多Agent系统最大的隐性成本是Token消耗。每轮查询平均要消耗8000到15000个Token,其中很大一部分被Query Agent和Review Agent的博弈消耗掉。我用三个手段控制成本。
第一是缓存,重复查询直接命中,不调用LLM。第二是Schema裁剪,只加载和当前问题相关的表字段,大幅缩短Prompt长度。第三是模型分级,简单的SQL查询用中等模型生成,复杂查询才动用更强的模型。经过这三项优化,单次查询的Token成本降低了约40%。
6.4 后续扩展的可能方向
这套系统目前的形态还比较基础。如果继续演进,我会优先考虑三个方向。一是接入更多数据源类型,目前只支持MySQL,后续可以对接ClickHouse、PostgreSQL甚至API数据源。二是指标口径管理平台化,把常用指标的口径定义从Prompt中抽离出来,放到配置中心统一管理。三是引入更细粒度的权限控制,不同用户只能查询自己有权限的数据范围。
做这个系统的过程中,我最深的体会是:多智能体的价值不在于“看起来很酷”,而在于它能真正拆分复杂度,让每个环节可验证、可干预、可优化。数据检索这件事,恰恰是复杂度极高、错误代价极大、又非常适合分角色协作的典型场景。最后再分享一条经验:不要一开始就追求全自动,先用多Agent把流程跑通,再把每个角色的能力逐步增强,这样系统的稳定性和可维护性都会好很多。
本文还有配套的精品资源,点击获取