Qwen开源模型实战:从零构建数据分析问答与报表生成智能体
最近被问得最多的一个场景就是拿Qwen这类开源模型去做数据分析智能体。说白了就是让模型看懂一张表听懂一句人话然后自动把SQL写出来、把图表画出来、把结论讲清楚。这个方向现在很热但网上大部分教程只讲“怎么调API”不讲“怎么把模型真正用起来”。我这篇就围绕Qwen Code Skills实战把从零搭建一个数据问答与报表生成智能体的完整思路、关键代码、踩坑记录一次性说清楚。这套东西适合谁如果你是数据分析师、后端开发、或者是刚接触大模型应用但想快速落地一个内部工具的人这篇内容可以直接照着做。不需要你懂模型训练也不需要你有GPU只要会Python基础、会写SQL就能搭出一个能实际用的智能体。1. 整体思路与方案选型1.1 为什么要选“代码路径”而不是“直答路径”一开始你可能觉得数据分析智能体不就让模型直接读表、直接给结论吗实际上这个思路在真实场景里很难走通。模型直接看一张几百列的大表然后凭“感觉”回答“哪个品类利润最高”它会一本正经地编数字而且编得特别像真的。这就是大模型最坑的地方它不是一个计算器它是一个语言模型擅长的是文本生成不是数值精确计算。所以我的核心思路是转换路径让模型不直接回答业务问题而是让它写代码然后让代码去执行、去计算、去取数。也就是所谓的“Code Skills”模式——把代码当成模型的手和脚模型负责理解问题和生成代码真正的数据操作全部交给解释器去跑。这个路线的优势有三个准确性有保障。SQL或Pandas代码是确定性执行的算出来是多少就是多少不会像模型那样“大概感觉”。可审计性。每一步逻辑都落在代码里业务方问你“这个数怎么来的”你直接把生成的SQL甩给他看比一张嘴解释半天有用得多。可扩展性。后面想接入新的数据源、新的报表格式只要扩展工具函数就行不需要重新调模型。1.2 技术架构和工具选型整个智能体的架构拆成三层模型层、工具层、数据层。模型层用Qwen原因很直接它的代码生成能力在开源模型里属于第一梯队对中文指令的理解也足够好而且可以本地部署或者通过API调用适配不同数据敏感度的场景。如果你数据不能出内网那就私有化部署如果允许走API那直接用在线版本更省事。工具层是核心我给它起了个名字叫“技能注册表”。这个表里会登记所有智能体可以调用的能力包括查数据库执行SQL读取Excel或CSV生成HTML报表导出Excel文件画图表关键点是让模型知道“有什么工具可用”并知道每个工具要传什么参数。这里面用的是Function Calling机制或者叫工具调用。模型看到用户问题时会先判断“我要不要调用工具、调用哪个工具、传什么参数”然后返回一个结构化的调用请求我们再在代码里真正执行它。数据层最简单先用SQLite或者本地的Excel文件就够了。真实环境里如果你要接MySQL、PostgreSQL只需要改一下数据库连接串和SQL方言别的逻辑基本不用动。这个架构看起来简单但有一个很多教程没讲透的地方工具调用的循环不能只做一次。模型生成SQL之后可能会报错数据库里表结构可能需要先探查报表里可能有动态参数。所以智能体的真正形态是一个循环模型出方案、执行工具、把执行结果反馈给模型、模型根据反馈做下一步决策。这个循环到什么时候结束要么是模型说“我已经完成”要么是达到了设定的最大轮数。1.3 为什么用“提示词工具”而不是微调模型这里想多说一点。很多人一上来就问“要不要微调模型”我的建议是不要急着微调。微调的适用场景是你要让模型学会一种全新的能力而这种能力很难用文字描述清楚。但数据分析问答这件事模型的底层能力已经完全够用了——它能理解SQL能写Pandas能看懂中文业务问题。你缺的不是模型能力而是把能力和数据连起来的那层胶水。这层胶水就是提示词和工具调用。提示词负责约束模型的行为边界比如“你必须先调用工具再回答”“不要编造数据”“如果SQL报错请根据错误信息修正”。工具调用负责把模型和真实数据环境打通。这两件事用微调来做成本高、周期长、维护难而且效果不一定比得上把工具层做扎实。我测试下来只要提示词写得够细、工具定义得够清楚Qwen在数据分析这个场景下的表现非常稳定。微调这件事等你的场景有大量固定格式需求、提示词已经压不住的时候再考虑也不迟。2. 核心细节解析与实操要点2.1 数据问答的关键链路自然语言转查询整个数据问答的链路看起来只有“问一个问题、得到一个答案”两步但拆开来看内部至少有五个环节意图识别用户是想查数还是想生成报表还是想解释一个指标的口径表结构探查模型需要先知道数据库里有哪些表、每张表有哪些字段才能写出正确的SQL。代码生成根据问题和表结构生成可执行的SQL或Pandas代码。执行与校验跑代码如果报错就自动修正如果结果为空就告诉模型重新组织查询条件。结果包装把查询结果整理成自然语言答案、图表或报表文件。这五个环节里最容易出问题的其实是第二个表结构探查。如果你直接把一个连接串丢给模型让它自己去猜表里有什么字段它大概率会按照“常见业务表”的刻板印象去写SQL。比如你的表里字段叫prd_nm模型可能猜成product_name一执行就报错。我的做法是加一个前置步骤在系统提示词里把表结构信息直接注入进去。你可以在初始化智能体时执行一次PRAGMA table_info(表名)或者查询information_schema把字段名、字段类型、注释全部拼成文本塞给模型。这样模型在生成SQL的时候是“看着真实表结构”在写而不是“凭想象”在写。这里有一个很多人忽视的细节字段注释非常关键。如果你的字段名是c1、c2这种无意义的命名那你需要有一张字段映射表告诉模型c1代表“订单日期”、c2代表“客户ID”。否则模型写出来的SQL再正确也不知道该查哪个字段。2.2 报表生成的三种常见形态数据问答搞定了接下来就是报表生成。我做过很多次之后发现报表这个东西不能只给一种输出格式因为不同场景的消费方式完全不同。第一种是Markdown表格适合直接在聊天窗口里看。比如用户问“按月份统计销售额”模型返回一张Markdown表格渲染出来直接可读。这种形式实现最简单但只能应付少量数据数据量大了以后表格会变得非常长反而不利于阅读。第二种是HTML报表适合发给领导和业务方。我通常让模型生成一个自包含的HTML文件内嵌CSS样式和图表库比如ECharts打开浏览器就能看。这种报表可以做得很漂亮有标题、有汇总卡片、有趋势图、有明细表观感上完全像一个正式的商业报表工具做出来的东西。第三种是Excel文件适合需要二次加工的场景。我会封装一个工具让模型拿到数据之后调用pandas.to_excel()把结果保存成一个.xlsx文件。这个工具的封装稍微有点讲究因为不能让模型直接决定文件路径否则它会乱写所以我一般会把文件路径作为参数传给它或者让工具函数自动生成带时间戳的文件名。还有一点特别重要报表不能只有“数据”还要有“解读”。我试过只让模型生成一个表格结果领导看着表格问“所以呢我们到底卖得好不好”所以后来我在提示词里固定了一句话任何报表必须附带“数据结论”小节至少写清楚“最高值是多少、最低值出现在什么时候、整体趋势是什么”。这样一来报表的价值一下子就不一样了。2.3 多轮对话与上下文管理数据问答是一个天然的多轮交互场景。用户第一轮问“这个月销售额是多少”第二轮大概率会接着问“那和上月比呢”第三轮可能还要追问“哪个区域贡献最大”。这些追问都依赖上下文如果你不做处理每一轮都当成独立问题用户体验会非常割裂。我处理上下文的方式是在每一轮对话里把当前用户问题和历史对话记录一起传给模型。同时在历史记录里保留“工具执行结果”的摘要而不只是保留纯文本对话。举个例子如果上一轮模型生成了SQL并执行拿到了结果那我在下一轮传给模型的历史里会把这条SQL和它的执行结果或结果摘要一起放进去。这样用户说“那和上月比呢”时模型是知道“这个月”指代的是哪张表、哪个月份的。但这里有一个度的问题上下文不能无限膨胀。如果对话轮数很多工具执行结果全部塞进去很快会把上下文窗口占满而且会引入大量噪声。我的做法是每一轮只保留工具执行的摘要截断到前200个字符左右不让完整数据进历史。当历史轮数超过8轮时丢弃最早的对话只保留最近几轮。如果用户明确开始一个新话题比如换个表问提供一个“清空上下文”的机制。多轮对话做得好不好直接决定这个智能体是“玩具”还是“工具”。很多人搭的时候觉得效果还行一测多轮就露馅问题基本都出在上下文管理上。3. 实操过程与核心环节实现3.1 环境准备与基础依赖做这一步的前提是你已经装好了Python环境版本建议3.10以上。我用到的依赖不算多核心就四个pip install qwen-agent-core pandas openpyxl jinja2解释一下每个库的用途qwen-agent-core是用来对接Qwen模型能力和工具调用编排的省去自己手写HTTP轮询的麻烦pandas负责数据处理和Excel输出openpyxl是pandas写Excel的后端引擎jinja2用来渲染HTML报表模板。如果你用的是在线API还需要装一下对应模型服务的SDK。这里我就不写具体产品名了你自己按所用的服务商文档装即可。如果是本地部署确认一下模型服务的接口地址和鉴权方式就行。3.2 定义技能注册表技能注册表是整个智能体的“操作手册”模型会从这个注册表里了解自己能调用什么工具。我实现的时候把每个技能定义成一个Python函数然后附加一段JSON Schema描述告诉模型这个工具的名称、功能、参数、返回值。拿“执行SQL查询”这个最核心的技能举例它的函数签名我这样写def run_sql_query(sql: str) - dict: 执行SQL查询并返回结果。 参数: sql: 完整的SQL查询语句必须是SELECT开头。 返回: 包含列名和行数据的字典。 conn sqlite3.connect(DATABASE_PATH) try: df pd.read_sql_query(sql, conn) return { columns: df.columns.tolist(), rows: df.head(50).values.tolist(), row_count: len(df) } except Exception as e: return {error: str(e)}对应的工具描述我写成这样让模型知道什么情况下该调用它{ name: run_sql_query, description: 在业务数据库上执行SQL查询可用于取数、统计、聚合。任何需要从数据库获取数据的问题都应该先调用此工具。, parameters: { type: object, properties: { sql: {type: string, description: 要执行的SELECT SQL语句} }, required: [sql] } }这里有个我从实践里总结的小技巧工具描述里一定要写清楚“什么时候用”和“什么时候不用”。明明有两个工具一个查数据库、一个读Excel如果描述写得含糊模型就容易选错。你把边界条件写明确模型的选择准确率会明显提升。3.3 搭建智能体主循环主循环是整个智能体的调度中枢。流程是先接收用户问题然后让模型决定是直接回答还是调用工具如果是调用工具就在本地执行执行结果回传给模型再让模型判断是否需要继续调用直到模型给出最终答案。我这里用一个简化版的循环来说明这个过程def run_agent(user_question, messages_history): messages [{role: system, content: SYSTEM_PROMPT}] messages_history messages.append({role: user, content: user_question}) for step in range(MAX_STEPS): # 设置最大迭代次数防止死循环 response client.chat(messagesmessages, toolsTOOL_SCHEMAS) if response.tool_calls: for call in response.tool_calls: tool_result execute_tool(call.function.name, call.function.arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(tool_result, ensure_asciiFalse) }) continue # 模型看到工具结果后继续判断 final_answer response.content messages_history.append({role: user, content: user_question}) messages_history.append({role: assistant, content: final_answer}) return final_answer return 处理超时请简化问题后重试。这个循环看起来简单但有几个细节值得抠MAX_STEPS一定要设上限经验值是4到6轮。否则模型可能在SQL报错、改SQL、又报错、又改SQL之间无限循环。工具执行结果返回给模型时不要返回完整大表只返回前50行和行数统计。如果数据量很大模型反而不知道该关注哪部分。如果工具返回了error字段一定要原样传给模型。模型具备根据SQL报错信息自我修正的能力——这是Qwen这类模型很擅长的地方。3.4 报表生成的实现细节报表生成我单独写了一个工具函数。它接收参数报表标题、数据摘要、图表配置、表格数据然后返回一个HTML文件路径。def generate_html_report(title: str, kpis: str, table_data: dict, chart_data: dict None) - str: template ENV.get_template(report_template.html.j2) html_content template.render( titletitle, kpiskpis, columnstable_data[columns], rowstable_data[rows], chart_datachart_data ) filename freport_{datetime.datetime.now().strftime(%Y%m%d_%H%M%S)}.html filepath os.path.join(REPORT_DIR, filename) with open(filepath, w, encodingutf-8) as f: f.write(html_content) return filepath在提示词里我会明确要求模型当用户提出“生成报表”“做个报告”“出个图表”这类需求时先调用run_sql_query拿数据再调用generate_html_report生成文件最后在回复里附上文件路径和核心结论。报表模板用的是Jinja2渲染。模板里预留了KPI卡片区、图表区、表格区的位置。为什么不用模型直接生成HTML因为模型生成的HTML通常是“写了能跑但很丑”而且容易引入不安全的脚本。固定模板的好处是样式统一、安全可控模型只需要往里填数据就行。3.5 提示词的精调经验提示词是整个智能体里最值得反复打磨的部分。我初始版本的系统提示词只写了三句话“你是一个数据分析助手请根据问题查询数据库并回答。”效果嘛能跑但经常出现不调工具就乱答、字段猜错、报表格式不对这些问题。后来我迭代到一个大约800字的版本稳定性才有了质的提升。核心要点是这些你是数据分析智能体你的工作流程必须严格遵循以下步骤 1. 理解用户问题判断是否需要查询数据。 2. 如果需要数据必须先调用run_sql_query工具不能直接回答。 3. 查看工具返回结果如果结果为空或报错说明原因并调整SQL。 4. 根据查询结果组织回答回答必须包含结论、关键数字、对比信息。 5. 如果用户要求报表调用generate_html_report工具生成文件。 严禁编造不存在的数字所有数据必须来自工具执行结果。一句话总结提示词的要点把流程写死、把边界说清、把禁忌列明。模型不是不懂而是容易“发挥”你的提示词就是为了限制它的发挥空间让它做一个规范的操作员。4. 常见问题与排查技巧实录4.1 SQL生成错误与自纠错机制第一类高频问题模型生成的SQL执行报错常见原因包括字段名猜错、表名前缀漏写、聚合函数用错、日期格式不匹配。针对这个问题我的排查思路是三步走。第一步看错误信息是什么类型。字段不存在、语法错误、函数不存在这三类问题的修正方向完全不同。第二步确认表结构信息是否给足了。如果你只给了字段名没给字段类型模型把字符串字段当数字去聚合自然会挂。第三步确认是否启用了重试机制。在智能体主循环里工具返回错误之后不要直接放弃而是把错误信息塞回模型让它根据报错修正SQL。这个自纠错能力我实测非常有用差不多可以解决八成以上的SQL错误。还有一个高频问题是模型生成的SQL写了一个LIMIT 10结果用户问的是“总共多少条”模型拿前10条去做统计数据对不上。解决方法是在工具描述里明确写“统计类问题应使用COUNT聚合不要使用LIMIT”并且在工具返回结果里加上row_count字段让模型看到总行数。4.2 上下文混淆与多轮追问失忆第二个高频问题多轮对话中模型把两个不同问题的上下文搞混了。典型场景是用户先问“A区域销售额”再问“那B区域的利润率呢”模型可能会沿用上一轮的过滤条件导致结果张冠李戴。排查时我会先看传给模型的历史消息里是否包含了完整的工具调用记录。如果没有模型自然不知道上一轮查的是什么表、用的什么条件。另一个排查点是上下文截断。如果你做历史截断时把用户最初的问题截掉了模型就可能丢失关键过滤条件。我的解决方案是在每一轮消息的末尾追加一个“当前状态确认”字段用一句话概括当前对话涉及的查询对象和条件。这个做法相当于给模型一个“短期记忆锚点”防止它在长对话里漂移。4.3 报表中文乱码与样式问题报表生成时遇到最多的问题就是中文乱码。这个问题主要出现在两个环节一是HTML文件没有设置meta charsetutf-8二是Excel导出时字体不支持中文字符。HTML的问题很好解决在模板头部固定加上字符集声明就行。Excel的问题更隐蔽因为pandas默认写Excel时不会自动处理中文宽字体单元格会显示成通用字体看起来不专业。我的处理方法是生成Excel后用openpyxl重新打开文件把中文字段所在列的字体统一设置为等线或微软雅黑并调整列宽。这些细节虽然不影响数据准确性但非常影响使用者的观感尤其是给业务方用的报表美观度就是专业度的一部分。4.4 常见问题速查表我把日常踩坑的问题整理成一个速查表方便你对着排查。问题现象可能原因解决方案模型不调工具直接编答案提示词未强调“必须先调工具”在系统提示词中加入硬性流程约束SQL报“字段不存在”表结构未注入或字段描述缺失启动时自动探查表结构拼接字段说明查询结果总是空表名或库名写错初始化时打印实际表名确认命名一致报表文件打不开文件路径含中文或空格统一使用生成的时间戳文件名避免特殊字符多轮回答与历史矛盾上下文截断过于激进保留最近8轮并附加当前状态摘要模型返回超长代码未执行工具循环次数不够MAX_STEPS调整为6超时信息回传模型速查表看起来简单但每一条背后都是我实际跑崩过之后总结出来的。建议你把这些规则直接固化到系统提示词或工具描述里而不是靠每次人工去调。5. 从实验到落地几点扩展建议5.1 从SQLite迁移到真实数据库如果你只是本地测试SQLite足够了。但要落地给业务方用至少迁移到MySQL或PostgreSQL。迁移的时候有两个改动点一是数据库连接方式把sqlite3.connect换成对应数据库的驱动二是SQL方言差异比如分页写法、日期函数的差异这个需要你在工具函数里做一个简单的SQL方言适配层。我的建议是直接把run_sql_query工具改成支持多数据源配置通过一个db_key参数来区分不同的数据库连接。这样以后接新数据源不用改模型逻辑只需要增加一条连接配置。5.2 权限与安全不能让人随意查库数据分析智能体看起来人畜无害但如果你把“执行SQL”的能力开放给所有人风险就很大。比如有人写一条DROP TABLE或者一条超大的JOIN查询拖垮数据库这都是真实可能发生的事。安全措施我做了三层。第一层工具函数只允许SELECT开头的SQL其他语句直接拒绝执行。第二层SQL执行统一走只读账号从数据库权限层面杜绝写操作。第三层查询结果默认截断50行防止一次性拉出百万行数据把内存打爆。这三层加完风险就基本可控了。5.3 效果评估与持续优化最后聊一个不那么技术但很重要的问题怎么评估这个智能体做得好不好。我自己的做法是准备一套固定测试集包含几十个典型业务问题覆盖查询、对比、归因、报表生成四类场景。每次调整提示词或工具逻辑之后跑一遍全集记录答对多少题、报错多少次、报表格式是否合格。这套回归测试看起来很笨但很有效。模型的输出有随机性你单测几个问题觉得效果不错不代表整体稳定。只有通过固定测试集做回归才能放心把版本提交给业务方使用。我个人的体会是数据分析智能体这个方向真正的难点从来不是“调用一个大模型”而是把数据链路、工具边界、业务流程三件事缝合好。Qwen的能力给你提供了一个很好的起点但你能不能让它在你的数据环境里稳定干活取决于你愿不愿意花时间打磨那些“脏活累活”。把工具定义清楚、把提示词写扎实、把错误处理做完善这套系统就会从一个“会聊天的玩具”变成真正能替代重复取数工作的生产力工具。