AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)

项目目标

  1. 满足自己筛选基金经理;
  2. 写一个跟AI相关的项目;
  3. 学习与锻炼Python编程;
  4. 学习与使用开源框架 Qwen-agent。  

项目背景

       早在2020年底就尝试过爬取网上的基金数据用来解决自己的问题了,从网上学到一些筛选基金经理的理论,想着去实践,但是专业网站、APP都不能满足。
        因此,就想着自己来写一个工具,从数据采集到最后数据分析结果呈现,尽可能地让整个过程自动化。可是,一直未去实践,仅是有想法而已,画画流程图而已。
        2020年,随着 GPT-3火爆全球,AI正式进入科技领域的热门。而我是在近一年多才慢慢主动去关注这个领域,去学习一些应用层技术。之后,了解到有chan2SQL,觉得挺有意思的。很快就和自己以前构想的基金经理筛选项目结合到一起了。         

        好了,大概就这样,接下来就是实现的过程了。整个过程,基本上靠LLM chat工具来写代码,而我主要负责出想法,调试代码。  


想法 + 使用AI工具实现过程

工具与技术框架介绍

工具:

  • Windows 笔记本
  • PyCharm 2024.3.4 (Community Edition)  

技术框架:

  • Qwen-agen 0.0.24;
  • Python 3.12  

Python解释器里关键包:

  • mysql 0.0.3
  • mysql-connector-python    8.4.0     
  • mysqlclient    2.2.7    

为了方便阅读,此系列文章,每一篇开头都保持上述【项目目标】和【项目背景】,以及【想法 + 使用AI工具实现过程】中的“工具与技术框架介绍”。


基于初版代码优化过程

本次主要优化内容

  1. 使用 Flask 搭建 Web 服务;(好玩内容)
  2. 在 system prompt中静态注入表结构;
  3. 修改主程序的工作机制(重点内容);
  4. 提取生成的SQL。

初始代码如下

工具类(mysql_tool.py)

from qwen_agent.tools.base import BaseTool, register_tool
import mysql.connector
import json
 
@register_tool('mysql_query')
class MySQLQueryTool(BaseTool):
    description = '执行针对 MySQL 数据库的 SQL 查询,返回结果集。'
    parameters = [{
        'name': 'query',
        'type': 'string',
        'description': '用户输入的自然语言问题转换为 SQL 查询语句',
        'required': True
    }]
 
    def __init__(self, tool_cfg: dict = None):
        super().__init__(tool_cfg)  # 确保调用父类初始化
        cfg = tool_cfg or {}
        self.conn = mysql.connector.connect(
            host=cfg.get('host', 'localhost'),
            user=cfg.get('user', 'root'),
            password=cfg.get('password', ''),
            database=cfg.get('database', 'your_database')
        )
 
    def call(self, params: str, **kwargs) -> str:
        query = json.loads(params)['query']
        cursor = self.conn.cursor()
        try:
            cursor.execute(query)
            result = cursor.fetchall()
            return json.dumps({'result': result}, ensure_ascii=False)
        except Exception as e:
            return json.dumps({'error': str(e)}, ensure_ascii=False)
        finally:
            cursor.close()

主程序(main.py)

import os
from qwen_agent.agents import Assistant
from qwen_agent.llm import get_chat_model
 
# 设置环境变量(Windows 下推荐方式)
# set DASHSCOPE_API_KEY=your_api_key
 
# LLM 配置(使用 DashScope 服务)
llm_cfg = {
    'model': 'qwen-max',
    'model_server': 'dashscope',
    'tool_cfg': {
        'mysql_query': {
            'host': 'localhost',
            'user': 'root',
            'password': 'your_password',
            'database': 'your_database'
        }
    }
}
 
# 初始化 MySQL 工具
tools = ['mysql_query']
 
# 系统提示词
system_prompt = '''
你是一个数据库助手,能够将用户的自然语言问题转换为 SQL 查询语句,并调用 MySQL 工具执行。请遵循以下规则:
1. 仅输出 SQL 查询语句或工具调用结果。
2. 如果用户问题与数据库无关,请直接回答。
3. 确保生成的 SQL 语法正确,并符合 MySQL 规范。
'''
 
# 初始化 Agent
bot = Assistant(
    llm=llm_cfg,
    system_message=system_prompt,
    function_list=tools
)
 
# 处理用户输入
messages = []
while True:
    query = input('请输入您的问题(输入 "exit" 退出): ')
    if query.lower() == 'exit':
        break
    messages.append({'role': 'user', 'content': query})
    response = []
    for r in bot.run(messages=messages):
        response.append(r)
    # print('SQL 执行结果:', response[-1].get('content', ''))
    print('full response ', respo

优化1:使用 Flask 搭建 Web 服务

【优化描述】要在一个网页实现问答对话

(使用的 LLM chat是:问小白,DeepSeek V3 联网模式:日常搜索)

prompt:

将最近的一次示例代码优化成能通过web网页对话的方式

主要是新写了一个app.py,一个 css 和 html文件,具体得到的代码如下:

Flask 主程序(app.py

from flask import Flask, render_template, request, jsonify
import os
import mysql_tool
from qwen_agent.agents import Assistant
from qwen_agent.llm import get_chat_model

app = Flask(__name__)
app.secret_key = 'your_secret_key'

# 加载环境变量
os.environ['DASHSCOPE_API_KEY'] = os.getenv('DASHSCOPE_API_KEY', 'your_api_key_here')

# 初始化模型和助手
llm_cfg = {
    'model': 'qwen-max',
    'model_server': 'dashscope',
    'tool_cfg': {
        'mysql_query': {}
    }
}

bot = Assistant(
    llm=llm_cfg,
    system_message='你是一个数据库助手,能将自然语言转换为 SQL 并执行。',
    function_list=['mysql_query']
)

messages = []

@app.route('/', methods=['GET', 'POST'])
def index():
    global messages
    if request.method == 'POST':
        user_input = request.form.get('query', '')
        if user_input.lower() == 'exit':
            return render_template('index.html', history=messages)

        messages.append({'role': 'user', 'content': user_input})

        # 执行 Qwen-Agent 处理
        full_response = ''
        for r in bot.run(messages=messages):
            if isinstance(r, dict):
                content = r.get('content', '')
            else:
                content = str(r)
            full_response += content

        messages.append({'role': 'assistant', 'content': full_response})

    return render_template('index.html', history=messages)

if __name__ == '__main__':
    app.run(debug=True)

前端页面(templates/index.html) -- 注意文件路径,建议按照路径,新增一个文件夹 templates




    
    Chat2SQL Web
    


    

Chat2SQL Web

{% for msg in history %}
{{ msg.role | title }}:
{{ msg.content }}
{% endfor %}

样式文件(static/style.css)  -- 注意文件路径,建议按照路径,新增一个文件夹 static

body {
    font-family: Arial, sans-serif;
    margin: 20px;
    background-color: #f7f7f7;
}

form {
    margin-bottom: 20px;
}

input[type="text"] {
    padding: 10px;
    width: 60%;
    font-size: 16px;
    margin-right: 10px;
}

button {
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
}

.chat-box {
    background-color: #fff;
    padding: 15px;
    border-radius: 8px;
    max-height: 500px;
    overflow-y: auto;
}

.user {
    color: blue;
}

.assistant {
    color: green;
}

pre {
    white-space: pre-wrap;
    word-wrap: break-word;
}

运行 app.py,使用 PyCharm,需注意配置一下,具体如下图:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第1张图片

至此,优化1已完成,运行成功访问页面:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第2张图片

优化2:在 system prompt中静态注入表结构

【优化描述】这个优化是分析到大模型给出的回复,总是不稳定,就想着把表结构给到它,是不是会让它更懂按需转成SQL呢?有这个想法,也是看到别人有这么做过。

(使用的 LLM chat是:问小白,DeepSeek V3 联网模式:日常搜索)

prompt:

上述最新版的代码,大语言模型是否会获取到数据库对应的表结构来推断怎么给用户的问题来写SQL解决问题呢

根据回复的方案(如下截图),结合需求,先选择了最简便的方式——“静态注入表结构”

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第3张图片

优化后的 app.py 完整代码如下:

from flask import Flask, render_template, request
import mysql_tool
from qwen_agent.agents import Assistant

app = Flask(__name__)

# 初始化模型和助手
llm_cfg = {
    'model': 'qwen-max',
    'model_server': 'dashscope',
    'tool_cfg': {
        'mysql_query': {}
    }
}

# 获取表结构信息(需提前定义)
def get_table_schema():
    return """
    表结构如下:
    - 表名: fund_list
      字段(字段注释 | 字段类型): fund_id (基金ID | varchar), fund_name (基金名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), manager_ids (当前管理人ID | varchar), manager_names (当前管理人名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: fund_manager
      字段(字段注释 | 字段类型): manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), work_duration (从业时长 | varchar), work_years (从业年份 | decimal), 
        manager_link (基金经理详情链接 | varchar), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: manager_fund_rel
      字段(字段注释 | 字段类型): id (主键 | int), manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), 
        company_id (基金公司编码 | varchar), company_name (基金公司名称 | varchar), fund_id (基金ID | varchar), fund_name (基金名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), fund_managing_time (基金管理时间 | varchar), 
        fund_managing_duration (管理时长 | varchar), fund_managing_years (管理年份 | decimal), term_return_rate (任期回报率(保留4位小数存储) | decimal),
        year_return_rate (年化收益率(保留4位小数存储) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    """

# 修改 system_prompt
system_prompt = f'''
你是一个数据库助手,能将自然语言转换为 SQL 并执行。请遵循以下规则:
1. 始终参考以下表结构:
{get_table_schema()}
2. 生成的 SQL 必须严格符合表结构和字段名。
3. 如果问题涉及未提及的表或字段,请直接告知用户。
'''

bot = Assistant(
    llm=llm_cfg,
    system_message=system_prompt,
    function_list=['mysql_query']
)

messages = []

@app.route('/', methods=['GET', 'POST'])
def index():
    global messages
    if request.method == 'POST':
        user_input = request.form.get('query', '')
        if user_input.lower() == 'exit':
            return render_template('index.html', history=messages)

        messages.append({'role': 'user', 'content': user_input})

        # 执行 Qwen-Agent 处理
        full_response = []
        for r in bot.run(messages=messages):
            if isinstance(r, dict):
                content = r.get('content', '')
            else:
                content = str(r)
            full_response.append(content)

        print(full_response)
        messages.append({'role': 'assistant', 'content': full_response})

    return render_template('index.html', history=messages)

if __name__ == '__main__':
    app.run(debug=True)

变动的代码,主要就是新增了:def get_table_schema():,然后修改了变量:system_prompt

优化3:修改主程序的工作机制

        按上述优化1、2完成之后,多次运行,得到的回复中是存在需要的SQL语句了,但是回复的内容实在太长了,也不知为何。

于是继续向它——(使用的 LLM chat是:问小白,DeepSeek V3 联网模式:日常搜索)提问

prompt:

使用了静态注入表结构之后,执行 Qwen-Agent 处理的回复还是比较长,怎么能拿到大语言模型写出来的SQL到数据库中执行的结果呢

根据回复的内容,意识到 app.py 代码执行逻辑是这样的:

用户输入自然语言问题
               ↓
Qwen-Agent 根据 system_prompt 和历史消息生成 SQL
               ↓
调用 mysql_query 工具执行 SQL 查询
               ↓
返回查询结果
               ↓
AI 将结果解释成自然语言回复给用户

这个过程中大语言模型执行了 Function_Calling。 为啥知道是执行了 Function_Calling,是因为之前有一些相关积累:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第4张图片

于是,就想确定是否要调整代码的逻辑,也就是是否还要继续使用 Function_Calling。为此,使用的 LLM chat是:通义,Qwen3

prompt:

    系统工作机制如下:

    用户输入自然语言问题
           ↓
    Qwen-Agent 根据 system_prompt 和历史消息生成 SQL
           ↓
    调用 mysql_query 工具执行 SQL 查询
           ↓
    返回查询结果
           ↓
    AI 将结果解释成自然语言回复给用户

    将上述工作机制中“调用 mysql_query 工具执行 SQL 查询”,这一步改为使用代码,而不是使用模型驱动工具调用,两者的优缺点是什么

结合回答的方案描述,关键内容如下截图:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第5张图片

为此,决定采用如上述截图所述的混合模式:混合模式(AI 生成 SQL + 代码执行),它的工作机制如下:

        用户输入自然语言问题
               ↓
        Qwen-Agent 根据 system_prompt 和历史消息生成 SQL
               ↓
        代码层接收 SQL 并安全执行查询(如使用参数化、连接池等)
               ↓
        数据库返回查询结果
               ↓
        代码层将结果传回给 AI 模型
               ↓
        AI 将结果解释成自然语言回复给用户

继续使用:通义,Qwen3

prompt:(将整个 app.py 的代码都粘贴进去了)

    from flask import Flask, render_template, request
    import mysql_tool
    from qwen_agent.agents import Assistant

    app = Flask(__name__)

    # 初始化模型和助手
    llm_cfg = {
        'model': 'qwen-max',
        'model_server': 'dashscope',
        'tool_cfg': {
            'mysql_query': {}
        }
    }

    '''
        qwen1.5-14b-chat: 这个模型不可以,老写不出正确的SQL,连表结构都给到 prompt 了,还是如此;
        
    '''

    # 获取表结构信息(需提前定义)
    def get_table_schema():
        return """
        表结构如下:
        - 表名: fund_list
          字段(字段注释 | 字段类型): fund_id (基金ID | varchar), fund_name (基金名称 | varchar), company_id (基金公司编码 | varchar), 
            company_name (基金公司名称 | varchar), manager_ids (当前管理人ID | varchar), manager_names (当前管理人名称 | varchar), 
            fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
        - 表名: fund_manager
          字段(字段注释 | 字段类型): manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), company_id (基金公司编码 | varchar), 
            company_name (基金公司名称 | varchar), work_duration (从业时长 | varchar), work_years (从业年份 | decimal), 
            manager_link (基金经理详情链接 | varchar), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
        - 表名: manager_fund_rel
          字段(字段注释 | 字段类型): id (主键 | int), manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), 
            company_id (基金公司编码 | varchar), company_name (基金公司名称 | varchar), fund_id (基金ID | varchar), fund_name (基金名称 | varchar), 
            fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), fund_managing_time (基金管理时间 | varchar), 
            fund_managing_duration (管理时长 | varchar), fund_managing_years (管理年份 | decimal), term_return_rate (任期回报率(保留4位小数存储) | decimal),
            year_return_rate (年化收益率(保留4位小数存储) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
        """

    # 修改 system_prompt
    system_prompt = f'''
    你是一个数据库助手,能将自然语言转换为 SQL 并执行。请遵循以下规则:
    1. 始终参考以下表结构:
    {get_table_schema()}
    2. 生成的 SQL 必须严格符合表结构和字段名。
    3. 如果问题涉及未提及的表或字段,请直接告知用户。
    '''

    bot = Assistant(
        llm=llm_cfg,
        system_message=system_prompt,
        function_list=['mysql_query']
    )

    messages = []

    @app.route('/', methods=['GET', 'POST'])
    def index():
        global messages
        if request.method == 'POST':
            user_input = request.form.get('query', '')
            if user_input.lower() == 'exit':
                return render_template('index.html', history=messages)

            messages.append({'role': 'user', 'content': user_input})

            # 执行 Qwen-Agent 处理
            full_response = []
            for r in bot.run(messages=messages):
                if isinstance(r, dict):
                    content = r.get('content', '')
                else:
                    content = str(r)
                full_response.append(content)

            print(full_response)
            messages.append({'role': 'assistant', 'content': full_response})

        return render_template('index.html', history=messages)

    if __name__ == '__main__':
        app.run(debug=True)

我需将上述代码的工作机制从“tool_call 执行 SQL” 修改为“混合模式(AI 生成 SQL + 代码执行)”

因为只告诉了大模型 app.py 的代码,其他没告诉,然后它给了一版新的 html,也懒得对比,于是就新建了一个文件夹来保存新生成的代码。新代码如下:

新的 app.py

from flask import Flask, render_template, request
import mysql.connector
from mysql.connector import Error
import os
from dotenv import load_dotenv
from qwen_agent.agents import Assistant

app = Flask(__name__)

# 加载环境变量(用于数据库配置)
load_dotenv()

# 数据库连接配置(从 .env 文件读取)
def get_db_connection():
    try:
        connection = mysql.connector.connect(
            host=os.getenv('DB_HOST'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD'),
            database=os.getenv('DB_NAME')
        )
        return connection
    except Error as e:
        print(f"数据库连接失败: {e}")
        return None

# 获取表结构信息(供 LLM 使用)
def get_table_schema():
    return """
    表结构如下:
    - 表名: fund_list
      字段(字段注释 | 字段类型): fund_id (基金ID | varchar), fund_name (基金名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), manager_ids (当前管理人ID | varchar), manager_names (当前管理人名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: fund_manager
      字段(字段注释 | 字段类型): manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), work_duration (从业时长 | varchar), work_years (从业年份 | decimal), 
        manager_link (基金经理详情链接 | varchar), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: manager_fund_rel
      字段(字段注释 | 字段类型): id (主键 | int), manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), 
        company_id (基金公司编码 | varchar), company_name (基金公司名称 | varchar), fund_id (基金ID | varchar), fund_name (基金名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), fund_managing_time (基金管理时间 | varchar), 
        fund_managing_duration (管理时长 | varchar), fund_managing_years (管理年份 | decimal), term_return_rate (任期回报率(保留4位小数存储) | decimal),
        year_return_rate (年化收益率(保留4位小数存储) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    """

# 构建 system_prompt,强调只生成 SQL 不执行
system_prompt = f'''
你是一个数据库助手,能将自然语言转换为 SQL。请遵循以下规则:
1. 始终参考以下表结构:
{get_table_schema()}
2. 生成的 SQL 必须严格符合表结构和字段名。
3. 如果问题涉及未提及的表或字段,请直接告知用户。
4. ❗你只需输出 SQL 语句,不要执行它,也不要解释。
'''

# 初始化 Qwen 助手(不注册任何工具)
llm_cfg = {
    'model': 'qwen-max',
    'model_server': 'dashscope'
}

bot = Assistant(
    llm=llm_cfg,
    system_message=system_prompt
)

messages = []

@app.route('/', methods=['GET', 'POST'])
def index():
    global messages
    generated_sql = ''
    execution_result = None

    if request.method == 'POST':
        user_input = request.form.get('query', '')
        execute_sql = request.form.get('execute_sql')  # 是否点击了执行按钮

        if user_input.lower() in ['exit', 'quit']:
            return render_template('index.html', history=messages, sql="")

        if not execute_sql:
            # 用户输入自然语言,让 AI 生成 SQL
            messages.append({'role': 'user', 'content': user_input})

            full_response = []
            for r in bot.run(messages=messages):
                content = r.get('content', '') if isinstance(r, dict) else str(r)
                full_response.append(content)

            generated_sql = ''.join(full_response).strip()
            messages.append({'role': 'assistant', 'content': generated_sql})
        else:
            # 用户点击“执行 SQL”,使用后端手动执行
            sql_to_run = request.form.get('sql_to_execute', '').strip()
            conn = get_db_connection()
            if conn and conn.is_connected():
                cursor = conn.cursor(dictionary=True)
                try:
                    cursor.execute(sql_to_run)
                    execution_result = cursor.fetchall()
                except Error as e:
                    execution_result = f"执行错误:{str(e)}"
                finally:
                    cursor.close()
                    conn.close()
            else:
                execution_result = "数据库连接失败"

    return render_template(
        'index.html',
        history=messages,
        execution_result=execution_result
    )

if __name__ == '__main__':
    app.run(debug=True)

模板文件 templates/index.html 

-- 注意文件路径,建议按照路径,新增一个文件夹 templates需要在新建的文件夹下




    
    SQL 生成助手


    

欢迎使用 Qwen SQL 生成助手(混合模式)

{% for msg in history %}
{{ msg.role }}:
{{ msg.content }}
{% endfor %}

{% if execution_result is not none %}

执行结果:

{% if execution_result is string %}

{{ execution_result }}

{% else %} {% for row in execution_result[0] %} {% endfor %} {% for row in execution_result %} {% for key, value in row.items() %} {% endfor %} {% endfor %}
{{ loop.key }}
{{ value }}
{% endif %} {% endif %}

按之前的方式配置运行参数,运行新的 app.py,运行是成功的,说明生成的代码是可运行的。至此,主程序的工作机制优化完成。

优化4:提取生成的SQL

【优化描述】根据优化3,得到的代码,执行之后,生成的回复中是生成了 SQL的,只不过输出的是一长串分析,不是简单的结果。

使用的是:通义,Qwen3

prompt:

prompt 大语言模型通过用户输入的自然语言生成SQL的回复,只要SQL语句即可,其他都不要输出,怎么实现

根据回复的内容:

1. 在 Prompt 中明确要求只输出 SQL,-- 代码本就实现

2. 确保 AI 助手不调用工具, -- 代码本就实现(上述优化3)

3. 处理 AI 返回的内容 ,-- 代码本就实现

4. 进一步清洗输出内容(关键步骤)

import re

def extract_sql(text):
    # 匹配以 SELECT/INSERT/UPDATE/DELETE 开头的 SQL
    match = re.search(r'(SELECT\s+.*?;|INSERT\s+.*?;|UPDATE\s+.*?;|DELETE\s+.*?;)', text, re.DOTALL | re.IGNORECASE)
    if match:
        return match.group(0).strip()
    return ''

按照上述步骤4,修改了 app.py的代码,其实就是将上述代码赋值粘贴到 app.py 中,只要在调用它之前的任何位置均可,然后修改返回前端时的参数,先调用“extract_sql”提取出SQL,再返回。具体代码,先不粘贴了,后面会粘贴上来的。

运行代码后,结果还是得到预期的,还是和原来一样。

继续问大模型:

prompt:(这次我直接将其中一次回复的内容全部粘贴进去)

按照上述1、2、3、4步骤修改代码,结果不是直接输出 SQL,输出的是:
[{'role': 'assistant', 'content': 'SELECT', 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 1}}}}][{'role': 'assistant', 'content': 'SELECT manager', 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 2}}}}][{'role': 'assistant', 'content': 'SELECT manager_id', 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 3}}}}][{'role': 'assistant', 'content': 'SELECT manager_id, manager_name FROM', 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 7}}}}][{'role': 'assistant', 'content': 'SELECT manager_id, manager_name FROM fund_manager WHERE manager', 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 11}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 15}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 19}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 23}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_f", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 27}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_fund_rel WHERE manager", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 31}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_fund_rel WHERE manager_name LIKE '邱", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 35}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_fund_rel WHERE manager_name LIKE '邱%';", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'null', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 37}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_fund_rel WHERE manager_name LIKE '邱%';", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'stop', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 37}}}}][{'role': 'assistant', 'content': "SELECT manager_id, manager_name FROM fund_manager WHERE manager_name LIKE '邱%' UNION SELECT DISTINCT manager_id, manager_name FROM manager_fund_rel WHERE manager_name LIKE '邱%';", 'reasoning_content': '', 'extra': {'model_service_info': {'status_code': , 'request_id': '3bde0131-56d7-9563-95f7-57a1700c33a5', 'code': '', 'message': '', 'output': {'text': None, 'choices': [{'finish_reason': 'stop', 'message': {}}], 'finish_reason': None}, 'usage': {'input_tokens': 545, 'output_tokens': 37}}}}]

看到了回复的内容:“AI 助手在生成 SQL 时,是以“流式片段”的方式返回结果的(逐字、逐词生成),导致 bot.run() 的每次迭代返回的是一个中间片段,而不是完整的 SQL 语句。”

结合代码:

full_response = []
for r in bot.run(messages=messages):
    content = r.get('content', '') if isinstance(r, dict) else str(r)
    full_response.append(content)

只要认真看一下代码就知道了,为何有一个for循环,为何定义的变量是一个列表?

        那时,有一种说不出的感觉,是太过于依赖 LLM Chat 了,都没好好地分析一下代码输出的处理,以至于到现在才发现问题所在。幸运的是,这个问题没有前面的一系列过程,只不过它让本就正确的回复藏在长长的字符串中。

解决办法:

# 获取 AI 最终生成的 SQL
final_content = ''
for r in bot.run(messages=messages):
    if isinstance(r, dict):
	    content = r.get('content', '')
    else:
	    content = str(r)
	final_content = content.strip()  # 保留最后一次的内容

messages.append({'role': 'assistant', 'content': final_content})
messages.append({'role': 'assistant_sql', 'content': extract_sql(final_content)})

最终的 app.py 代码如下:

from flask import Flask, render_template, request
import mysql.connector
from mysql.connector import Error
import os
from dotenv import load_dotenv
from qwen_agent.agents import Assistant

app = Flask(__name__)

# 加载环境变量(用于数据库配置)
load_dotenv()

# 数据库连接配置(从 .env 文件读取)
def get_db_connection():
    try:
        connection = mysql.connector.connect(
            host=os.getenv('DB_HOST'),
            user=os.getenv('DB_USER'),
            password=os.getenv('DB_PASSWORD'),
            database=os.getenv('DB_NAME')
        )
        return connection
    except Error as e:
        print(f"数据库连接失败: {e}")
        return None

# 获取表结构信息(供 LLM 使用)
def get_table_schema():
    return """
    表结构如下:
    - 表名: fund_list
      字段(字段注释 | 字段类型): fund_id (基金ID | varchar), fund_name (基金名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), manager_ids (当前管理人ID | varchar), manager_names (当前管理人名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: fund_manager
      字段(字段注释 | 字段类型): manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), company_id (基金公司编码 | varchar), 
        company_name (基金公司名称 | varchar), work_duration (从业时长 | varchar), work_years (从业年份 | decimal), 
        manager_link (基金经理详情链接 | varchar), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    - 表名: manager_fund_rel
      字段(字段注释 | 字段类型): id (主键 | int), manager_id (基金经理ID | varchar), manager_name (基金经理名称 | varchar), 
        company_id (基金公司编码 | varchar), company_name (基金公司名称 | varchar), fund_id (基金ID | varchar), fund_name (基金名称 | varchar), 
        fund_type (基金类型 | varchar), fund_size (基金规模(元) | decimal), fund_managing_time (基金管理时间 | varchar), 
        fund_managing_duration (管理时长 | varchar), fund_managing_years (管理年份 | decimal), term_return_rate (任期回报率(保留4位小数存储) | decimal),
        year_return_rate (年化收益率(保留4位小数存储) | decimal), create_user (创建人 | varchar), create_time (创建时间 | datetime), update_time (更新时间 | datetime)
    """

import re

def extract_sql(text):
    # 匹配以 SELECT/INSERT/UPDATE/DELETE 开头的 SQL
    match = re.search(r'(SELECT\s+.*?;|INSERT\s+.*?;|UPDATE\s+.*?;|DELETE\s+.*?;)', text, re.DOTALL | re.IGNORECASE)
    if match:
        return match.group(0).strip()
    return ''

# 构建 system_prompt,强调只生成 SQL 不执行
system_prompt = f'''
你是一个数据库助手,能将自然语言转换为 SQL。请遵循以下规则:
1. 始终参考以下表结构:
{get_table_schema()}
2. 生成的 SQL 必须严格符合表结构和字段名。
3. 如果问题涉及未提及的表或字段,请直接告知用户。
4. 你只需输出 MySQL 格式的 SQL 语句,不要执行它,也不要解释。
'''

# 初始化 Qwen 助手(不注册任何工具)
llm_cfg = {
    'model': 'qwen-plus', #'qwen-max',
    'model_server': 'dashscope'
}

bot = Assistant(
    llm=llm_cfg,
    system_message=system_prompt
)

messages = []

@app.route('/', methods=['GET', 'POST'])
def index():
    global messages
    execution_result = None

    if request.method == 'POST':
        user_input = request.form.get('query', '')
        execute_sql = request.form.get('execute_sql')  # 是否点击了执行按钮

        if user_input.lower() in ['exit', 'quit']:
            return render_template('index.html', history=messages, sql="")

        if not execute_sql:
            # 用户输入自然语言,让 AI 生成 SQL
            messages.append({'role': 'user', 'content': user_input})

            '''
            以“流式片段”的方式返回结果的(逐字、逐词生成),导致 bot.run() 的每次迭代返回的是一个中间片段,而不是完整的 SQL 语句
            '''
            # full_response = []
            # for r in bot.run(messages=messages):
            #     content = r.get('content', '') if isinstance(r, dict) else str(r)
            #     full_response.append(content)
            # generated_sql = ''.join(full_response).strip()

            # 获取 AI 最终生成的 SQL
            final_content = ''
            for r in bot.run(messages=messages):
                if isinstance(r, dict):
                    content = r.get('content', '')
                else:
                    content = str(r)
                final_content = content.strip()  # 保留最后一次的内容

            messages.append({'role': 'assistant', 'content': final_content})
            messages.append({'role': 'assistant_sql', 'content': extract_sql(final_content)})
        else:
            # 用户点击“执行 SQL”,使用后端手动执行
            sql_to_run = request.form.get('sql_to_execute', '').strip()
            conn = get_db_connection()
            if conn and conn.is_connected():
                cursor = conn.cursor(dictionary=True)
                try:
                    cursor.execute(sql_to_run)
                    execution_result = cursor.fetchall()
                except Error as e:
                    execution_result = f"执行错误:{str(e)}"
                finally:
                    cursor.close()
                    conn.close()
            else:
                execution_result = "数据库连接失败"

    return render_template(
        'index.html',
        history=messages,
        execution_result=execution_result
    )

if __name__ == '__main__':
    app.run(debug=True)

运行代码成功,输入用户问题,结果如下截图:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第6张图片

        至此,优化4:提取生成的SQL已完成。不过,还是存在一点点瑕疵,就是换行的格式问题。这个应该可以 system prompt 那里调试一下,让它先不要用 MySQL格式化输出试一试。若不可以,再找其他办法了。

就先到这里了,代码这东西得慢慢来优化。

文章实在有点长了,能看到这里的朋友们,肯定是一个爱学习、爱写代码的人。

未完待续…

        上面也已提到,生成的SQL不能直接复制粘贴运行,还得处理一下换行输出带来的问题。还有,当前的代码版本也不支持多轮对话,实在不方便我们测试生成SQL的准确率等,以及在使用 LLM Chat 的过程中,也得到了不少优化的建议,如下截图所示:

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统(二)_第7张图片

慢慢来吧,步步为营,这才扎实。你们觉得呢?……


这里放一个第一篇文章的链接吧,方便大家阅读。

AI编程实战:Python + Qwen-agent 实现chat2SQL智能助手系统-CSDN博客

现在回过头来,第二篇完全可以脱离第一篇的,毕竟第一篇的结果代码都放在了第二篇,哈哈……

你可能感兴趣的:(AI编程,python,flask,pycharm,mysql,sql)