2.DeepSeek驱动的数据分析Agent实战.md 29 KB

DeepSeek 驱动的数据分析 Agent 实战

用大模型让数据"开口说话"——从架构设计到代码落地的完整指南


一、为什么要做数据分析 Agent

1.1 传统数据分析的痛点

做过数据分析的同学都知道,日常工作中最折磨人的不是算法本身,而是从"业务问题"到"拿到结果"这条路上的各种摩擦

痛点 典型场景 真实代价
SQL 门槛高 运营同学想知道"上个月华东区 GMV 多少",得找数据分析师写 SQL 排队等半天,分析师还得反复确认口径
代码成本大 画个趋势图要写一堆 matplotlib 样板代码 80% 时间在调样式,20% 在思考业务
沟通损耗重 "我要看环比" -> 分析师理解成"同比" -> 返工 需求理解偏差是最大的隐性成本
重复劳动多 每周固定出几张报表,逻辑一模一样 机械重复,毫无技术含量

核心矛盾很清晰:业务人员懂业务但不会写代码,技术人员会写代码但不一定懂业务。中间这条鸿沟,正是大模型 Agent 可以填平的地方。

1.2 大模型能解决什么

大语言模型(LLM)在数据分析场景中,核心能力可以拆成三层:

  • 自然语言理解:把"帮我看看上个月各区域销售情况"解析成结构化意图——时间范围(上月)、分组维度(区域)、指标(销售额)。这一层纯靠 LLM 就能搞定。
  • 代码生成:根据解析出的意图,自动生成 SQL 查询或 Python 分析代码。这也是 LLM 的强项,尤其是 DeepSeek-Coder 系列在代码生成上的表现相当能打。
  • 推理与规划:面对复杂分析需求(比如"找出销量下滑的原因"),需要 Agent 自主拆解任务、规划执行步骤。这一层需要 LLM + 工具调用 + 业务上下文共同协作。

一句话总结:LLM 负责"理解"和"生成",Agent 框架负责"调度"和"执行"


二、系统架构总览

2.1 整体架构

整个系统由两个核心 Agent 协同工作,各司其职:

▲ 系统架构:用户提问 → 编排器路由 → NL2SQL Agent 查数据 / 可视化 Agent 画图表

2.2 两个 Agent 的分工

维度 NL2SQL 查询 Agent 数据可视化 Agent
职责 把自然语言翻译成 SQL,查询数据库 把查询结果翻译成 Python 代码,生成图表
核心工具 LangChain SQLDatabaseToolkit 自定义 Python 代码执行工具
输入 用户的自然语言问题 查询返回的结构化数据
输出 查询结果(表格数据) 统计分析 + 可视化图表
典型问题 "技术部有多少人?" "画一个各部门平均薪资的柱状图"

2.3 技术栈一览

组件 技术选型 选型理由
大模型 DeepSeek-Chat / DeepSeek-Coder 国产头部,代码能力强,性价比高
Agent 框架 LangChain 1.3.1 生态成熟,create_agent API 简洁可靠
数据库 MySQL 8.0+ 通用性强,适合演示
数据处理 Pandas + NumPy Python 数据分析标准库
可视化 Matplotlib + Seaborn 灵活可控,适合 Agent 生成代码
沙箱执行 自定义 exec 沙箱 轻量级,可控性强

三、技术选型与环境准备

3.1 依赖安装

# 核心框架(锁定 langchain 1.3.1,API 稳定可靠)
pip install langchain==1.3.1 langchain-openai langchain-community

# 数据库驱动(MySQL)
pip install mysql-connector-python pymysql

# 数据处理与可视化
pip install pandas numpy matplotlib seaborn

3.2 初始化大模型客户端

import os
from langchain_openai import ChatOpenAI

# 通过环境变量读取 API Key,避免硬编码泄露风险
llm = ChatOpenAI(
    model="deepseek-v4-flash",                          # DeepSeek 的对话模型
    api_key=os.getenv("DEEPSEEK_API_KEY"),          # 从环境变量加载
    base_url="https://api.deepseek.com",            # DeepSeek API 地址
    temperature=0,                                   # 设为 0 保证 SQL 生成的确定性
)

print("✅ DeepSeek 模型初始化完成")

**为什么 temperature=0?** 生成 SQL 和分析代码属于确定性任务,不需要随机性。温度设为 0 可以让模型输出更稳定、可复现。


四、数据层搭建:建表与灌数据

4.1 表结构设计

我们用一个电商订单系统作为演示场景,涉及三张核心表:

4.2 建表与数据初始化

import os
import mysql.connector

# ============================================================
# 第一步:建立数据库连接
# ============================================================
# 使用环境变量管理敏感信息,这是生产级代码的基本规范
conn = mysql.connector.connect(
    host=os.getenv("DB_HOST", "127.0.0.1"),
    port=int(os.getenv("DB_PORT", 3306)),
    user=os.getenv("DB_USER", "root"),
    password=os.getenv("DB_PASSWORD", ""),
    database=os.getenv("DB_NAME", "analytics_demo"),
)
cursor = conn.cursor()

# ============================================================
# 第二步:创建表结构
# ============================================================

# 员工表:存储公司内部人员信息
# 用于分析:部门人数分布、薪资结构、入职趋势等
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
    id          INT PRIMARY KEY AUTO_INCREMENT,  -- 自增主键
    name        VARCHAR(50)  NOT NULL,           -- 姓名
    department  VARCHAR(50)  NOT NULL,           -- 所属部门
    salary      DECIMAL(10,2) NOT NULL,          -- 月薪(精确到分)
    hire_date   DATE                             -- 入职日期
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")

# 产品表:存储在售商品信息
# 用于分析:品类销售、价格分布、库存预警等
cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
    id           INT PRIMARY KEY AUTO_INCREMENT,  -- 自增主键
    product_name VARCHAR(100) NOT NULL,           -- 商品名称
    category     VARCHAR(50)  NOT NULL,           -- 商品分类
    price        DECIMAL(10,2) NOT NULL,          -- 单价
    stock        INT DEFAULT 0                    -- 当前库存量
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")

# 订单表:记录每笔交易
# 用于分析:销售趋势、员工绩效、产品热度等
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
    id           INT PRIMARY KEY AUTO_INCREMENT,  -- 自增主键
    employee_id  INT NOT NULL,                    -- 下单员工(外键)
    product_id   INT NOT NULL,                    -- 购买商品(外键)
    quantity     INT NOT NULL,                    -- 购买数量
    order_date   DATE NOT NULL,                   -- 下单日期
    FOREIGN KEY (employee_id) REFERENCES employees(id),   -- 关联员工表
    FOREIGN KEY (product_id)  REFERENCES products(id)     -- 关联产品表
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
""")

# ============================================================
# 第三步:插入演示数据
# ============================================================

# 员工数据:5 个员工,分属 3 个部门
employees_data = [
    (1, "张三", "技术部",   20000.00, "2023-01-15"),
    (2, "李四", "销售部",   11000.00, "2023-02-20"),
    (3, "王五", "技术部",   16000.00, "2022-11-10"),
    (4, "赵六", "人力资源", 5000.00, "2023-03-01"),
    (5, "钱七", "销售部",   17000.00, "2022-12-05"),
]

# 产品数据:4 款商品,覆盖 2 个品类
products_data = [
    (1, "笔记本电脑", "电子产品", 6999.00, 500),
    (2, "机械键盘",   "电子产品", 399.00,  1000),
    (3, "办公椅",     "办公用品", 499.00,  300),
    (4, "显示器",     "电子产品", 1200.00, 400),
]

# 订单数据:5 笔订单,模拟真实购买行为
orders_data = [
    (1, 1, 1, 2, "2024-01-15"),  # 张三买了 2 台笔记本
    (2, 2, 2, 15, "2024-01-16"),  # 李四买了 15 个键盘
    (3, 3, 1, 10, "2024-01-17"),  # 王五买了 10 台笔记本
    (4, 5, 3, 6, "2024-01-18"),  # 钱七买了 6 把办公椅
    (5, 2, 4, 5, "2024-01-19"),  # 李四买了 5 台显示器
]

# executemany 批量插入,比逐条 insert 高效得多
cursor.executemany("INSERT IGNORE INTO employees VALUES (%s,%s,%s,%s,%s)", employees_data)
cursor.executemany("INSERT IGNORE INTO products  VALUES (%s,%s,%s,%s,%s)", products_data)
cursor.executemany("INSERT IGNORE INTO orders     VALUES (%s,%s,%s,%s,%s)", orders_data)

conn.commit()
conn.close()

print("✅ 数据库初始化完成")
print("   - employees 表:5 条员工记录")
print("   - products  表:4 条产品记录")
print("   - orders    表:5 条订单记录")

五、NL2SQL 自然语言查询 Agent

5.1 工作原理

NL2SQL Agent 的核心思路是:让 LLM 借助工具链自主完成"看表结构 → 写 SQL → 检查语法 → 执行查询 → 总结结果"的全流程

▲ NL2SQL Agent 工作流:侦察表结构 → 生成 SQL → 检查语法 → 执行查询 → 总结结果

关键点:Agent 不是一次性生成 SQL 就完事了,它会先侦察(看表结构)、再行动(写 SQL)、后验证(检查语法),这套工作流比人肉写 SQL 还严谨。

5.2 搭建 SQL Agent

import os
from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent  # langchain 1.3.1 的新 API

# ============================================================
# 第一步:连接数据库
# ============================================================
# SQLDatabase.from_uri 接受标准的数据库连接 URI
# LangChain 内部会用 SQLAlchemy 管理连接池
db_uri = (
    f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}"
    f"@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_NAME')}"
)
db = SQLDatabase.from_uri(db_uri)

# 验证连接:打印可用的表名
print(f"数据库连接成功")
print(f"   可用表:{db.get_usable_table_names()}")

# ============================================================
# 第二步:初始化大模型
# ============================================================
llm = ChatOpenAI(
    model="deepseek-v4-flash",
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url="https://api.deepseek.com",
    temperature=0,  # SQL 生成需要确定性输出
)

# ============================================================
# 第三步:创建 SQL 工具包
# ============================================================
# SQLDatabaseToolkit 会自动注册 4 个工具:
#   1. sql_db_list_tables    — 列出数据库中所有表
#   2. sql_db_schema         — 获取指定表的 DDL 结构
#   3. sql_db_query_checker  — 检查 SQL 语法是否正确
#   4. sql_db_query          — 执行 SQL 并返回结果
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()

print(f"\nSQL 工具包已加载({len(tools)} 个工具):")
for t in tools:
    print(f"   - {t.name}")

# ============================================================
# 第四步:定义 System Prompt(Agent 的"岗位说明书")
# ============================================================
SQL_AGENT_PROMPT = """你是一名专业的 SQL 数据分析师。

## 工作流程
1. 先用 sql_db_list_tables 查看数据库中有哪些表
2. 用 sql_db_schema 获取相关表的字段结构和类型
3. 生成 SQL 之前,用 sql_db_query_checker 检查语法
4. 确认无误后,用 sql_db_query 执行查询
5. 用中文总结查询结果,给出简洁的业务洞察

## 约束
- 只使用数据库中实际存在的表和字段,不要凭空编造
- 单次查询结果限制在 50 条以内
- 如果查询出错,分析错误原因后重新生成 SQL
- 回答要简洁专业,不要啰嗦
"""

# ============================================================
# 第五步:组装 Agent(langchain 1.3.1 一行搞定)
# ============================================================
# create_agent 返回一个可直接调用的 Runnable,无需再套 AgentExecutor
# 它内部自动处理工具调用、循环迭代、错误重试等逻辑
sql_agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=SQL_AGENT_PROMPT,
)

print("\nNL2SQL Agent 创建完成,可以开始提问了!")

5.3 查询示例

示例 1:简单计数查询

# 简单问题:Agent 只需一条 COUNT SQL 就能搞定
# create_agent 的 invoke 接口使用 messages 格式
response = sql_agent.invoke({
    "messages": [{"role": "user", "content": "公司一共有多少名员工?"}]
})

# 最终回答在最后一条消息里
final_msg = response["messages"][-1]
print(f"回答:{final_msg.content}")

Agent 内部执行过程(简化版):

调用 sql_db_list_tables -> 发现 employees, products, orders
调用 sql_db_schema("employees") -> 获取字段:id, name, department, salary, hire_date
生成 SQL: SELECT COUNT(*) FROM employees
返回结果: 5
总结: "公司共有 5 名员工"

示例 2:带条件的聚合查询

# 中等难度:需要 JOIN + WHERE + GROUP BY
response = sql_agent.invoke({
    "messages": [{"role": "user", "content": "列出技术部所有员工的姓名和薪资,按薪资从高到低排序"}]
})
final_msg = response["messages"][-1]
print(f"回答:{final_msg.content}")

示例 3:跨表关联查询

# 高难度:需要 JOIN 三张表 + 聚合计算
response = sql_agent.invoke({
    "messages": [{"role": "user", "content": "销售部的员工总共下了多少订单?订单总金额是多少?"}]
})
final_msg = response["messages"][-1]
print(f"回答:{final_msg.content}")

5.4 调试:查看 Agent 的思考过程

在实际开发中,你经常需要看 Agent 到底干了什么——生成了什么 SQL、中间结果是什么。create_agent 返回的 messages 列表包含完整的推理链:

# 遍历所有消息,还原 Agent 的完整推理链
response = sql_agent.invoke({
    "messages": [{"role": "user", "content": "每个部门各有多少人?"}]
})

for i, msg in enumerate(response["messages"]):
    msg_type = msg.__class__.__name__

    # AIMessage 中如果有 tool_calls,说明 Agent 调用了工具
    if hasattr(msg, "tool_calls") and msg.tool_calls:
        print(f"\n步骤 {i+1} [{msg_type}]:")
        for tc in msg.tool_calls:
            print(f"  调用工具:{tc['name']}")
            print(f"  参数:{tc.get('args', {})}")

    # ToolMessage 是工具返回的结果
    elif msg_type == "ToolMessage":
        print(f"  工具返回:{msg.content[:200]}")

    # AIMessage 的最终文本回答
    elif msg.content:
        print(f"\n最终回答:{msg.content}")

六、数据可视化 Agent 与代码沙箱

6.1 设计思路

NL2SQL Agent 解决了"查数据"的问题,但用户往往还想"看图表"。我们需要一个能自主编写 Python 代码、执行数据分析、生成可视化图表的 Agent。

核心挑战在于:如何让 Agent 安全地执行动态生成的代码?

▲ 可视化 Agent 工作流:Agent 生成代码 → 沙箱隔离执行 → 结果回传 → 解读后返回用户

6.2 加载数据到内存

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# ============================================================
# 配置 matplotlib 中文显示
# ============================================================
# Windows 用 SimHei(黑体),macOS 用 PingFang SC
# 如果还是乱码,试试安装 fonts-noto-cjk 并清除缓存
plt.rcParams["font.sans-serif"] = ["SimHei", "PingFang SC", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False  # 解决负号显示为方块的问题

# ============================================================
# 从数据库加载数据到 Pandas DataFrame
# ============================================================
# 用 SQLAlchemy engine 复用连接,避免重复创建连接池
from sqlalchemy import create_engine

engine = create_engine(db_uri)

employees_df = pd.read_sql("SELECT * FROM employees", engine)
products_df  = pd.read_sql("SELECT * FROM products", engine)
orders_df    = pd.read_sql("SELECT * FROM orders", engine)

print("✅ 数据加载完成")
print(f"   员工表:{len(employees_df)} 行 × {len(employees_df.columns)} 列")
print(f"   产品表:{len(products_df)} 行 × {len(products_df.columns)} 列")
print(f"   订单表:{len(orders_df)} 行 × {len(orders_df.columns)} 列")
print(f"\n📋 员工表示例:")
print(employees_df.head(3).to_string(index=False))

6.3 构建 Python 代码执行沙箱

这是整个可视化 Agent 的核心组件——一个受控的代码执行环境

import traceback
from io import StringIO
from contextlib import redirect_stdout
from langchain.tools import tool

# ============================================================
# 定义沙箱的"白名单"——只有这些库和数据可以被代码访问
# ============================================================
# 这是一种简单的安全策略:不在白名单里的东西,代码碰不到
SANDBOX_GLOBALS = {
    # 数据:Agent 可以分析这三张表
    "employees_df": employees_df,
    "products_df":  products_df,
    "orders_df":    orders_df,
    # 工具库:Agent 可以用这些库做分析和画图
    "pd":  pd,       # pandas — 数据处理
    "plt": plt,      # matplotlib — 基础绑图
    "sns": sns,      # seaborn — 统计可视化
    "np":  np,       # numpy — 数值计算
}


@tool
def execute_python_code(code: str) -> str:
    """
    执行 Python 代码进行数据分析和可视化。

    可用变量:
      - employees_df: 员工表 DataFrame(字段:id, name, department, salary, hire_date)
      - products_df:  产品表 DataFrame(字段:id, product_name, category, price, stock)
      - orders_df:    订单表 DataFrame(字段:id, employee_id, product_id, quantity, order_date)
      - pd:   pandas 库
      - plt:  matplotlib.pyplot
      - sns:  seaborn
      - np:   numpy

    使用示例:
      # 统计各部门平均薪资
      result = employees_df.groupby('department')['salary'].mean()
      print(result)

      # 画柱状图
      plt.figure(figsize=(10, 6))
      employees_df.groupby('department')['salary'].mean().plot(kind='bar')
      plt.title('各部门平均薪资')
      plt.show()
    """
    # 准备隔离的执行环境
    # globals_dict 提供白名单变量,locals_dict 收集执行过程中产生的新变量
    exec_globals = dict(SANDBOX_GLOBALS)
    exec_locals = {}

    # 用 StringIO 捕获 print() 的输出
    # 这样 Agent 生成的代码里所有的 print 语句都会被收集
    output_buffer = StringIO()

    try:
        # redirect_stdout 会把标准输出重定向到我们的 buffer
        with redirect_stdout(output_buffer):
            exec(code, exec_globals, exec_locals)

        result = output_buffer.getvalue()

        # 如果代码没有 print 任何东西,给个默认提示
        if not result.strip():
            result = "✅ 代码执行成功(无文本输出,可能已生成图表)"

        return f"执行成功:\n{result}"

    except Exception as e:
        # 出错时返回完整的错误堆栈,方便 Agent 自我修正
        error_detail = traceback.format_exc()
        return f"❌ 执行出错:{e}\n\n{error_detail}"


print("✅ Python 代码执行沙箱创建成功")

安全提示:上面的 exec 沙箱是最简实现,适合 Demo 和内部工具。生产环境建议用 Docker 容器沙箱WebAssembly 隔离,彻底杜绝恶意代码风险。详见第七章。

6.4 搭建可视化 Agent

from langchain.agents import create_agent

# ============================================================
# 定义可视化 Agent 的 System Prompt
# ============================================================
VISUALIZATION_PROMPT = """你是一名资深数据分析师,精通 Python、Pandas 和 Matplotlib 数据可视化。

## 可用数据
1. employees_df — 员工表(字段:id, name, department, salary, hire_date)
2. products_df  — 产品表(字段:id, product_name, category, price, stock)
3. orders_df    — 订单表(字段:id, employee_id, product_id, quantity, order_date)

## 工作流程
1. 理解用户的分析需求
2. 用 execute_python_code 工具编写并执行 Python 代码
3. 先做数据探索(head、describe、info),再做深入分析
4. 用中文解释分析结果,给出业务洞察

## 代码规范
- 绑图前设置中文字体:plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'DejaVu Sans']
- 设置 plt.rcParams['axes.unicode_minus'] = False
- 图表尺寸统一用 plt.figure(figsize=(10, 6))
- 必须添加标题、坐标轴标签,让图表自解释
- 用 print() 输出关键统计量,不要只画图不说话
- 图表标题用英文(避免渲染问题),但用中文向用户解释结果

## 注意事项
- 每次只执行一段完整的代码,不要拆成多段
- 先探索数据结构,再做分析——不要上来就画图
- 结果要有业务洞察,不只是"最大值是 XXX"
"""

# ============================================================
# 组装可视化 Agent(langchain 1.3.1 写法)
# ============================================================
viz_tools = [execute_python_code]

# 同样用 create_agent 一行搞定,和 SQL Agent 的创建方式完全一致
visualization_agent = create_agent(
    model=llm,
    tools=viz_tools,
    system_prompt=VISUALIZATION_PROMPT,
)

print("数据可视化 Agent 创建完成")

6.5 可视化示例

示例 1:基础统计分析

import warnings
warnings.filterwarnings("ignore")  # 抑制 matplotlib 的字体警告

response = visualization_agent.invoke({
    "messages": [{"role": "user", "content": "分析一下员工薪资的分布情况,包括均值、中位数、最大最小值等统计量"}]
})
final_msg = response["messages"][-1]
print(f"\n分析结果:\n{final_msg.content}")

Agent 内部生成的代码(大致):

# 数据探索
print("=== 薪资统计概览 ===")
print(employees_df['salary'].describe())

# 计算额外统计量
median_salary = employees_df['salary'].median()
print(f"\n中位数薪资:{median_salary:,.0f} 元")
print(f"薪资范围:{employees_df['salary'].min():,.0f} ~ {employees_df['salary'].max():,.0f} 元")
print(f"薪资标准差:{employees_df['salary'].std():,.0f} 元")

示例 2:分组对比图表

response = visualization_agent.invoke({
    "messages": [{"role": "user", "content": "画一个柱状图,展示各部门的平均薪资对比"}]
})
final_msg = response["messages"][-1]
print(f"\n分析结果:\n{final_msg.content}")

Agent 内部生成的代码(大致):

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 按部门计算平均薪资
dept_salary = employees_df.groupby('department')['salary'].mean().sort_values(ascending=False)

# 画柱状图
plt.figure(figsize=(10, 6))
bars = plt.bar(dept_salary.index, dept_salary.values, color=['#4C78A8', '#F58518', '#E45756'])
plt.title('Average Salary by Department', fontsize=16)
plt.xlabel('Department', fontsize=12)
plt.ylabel('Average Salary (CNY)', fontsize=12)

# 在柱子上方标注具体数值
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2., height,
             f'{height:,.0f}', ha='center', va='bottom', fontsize=11)

plt.tight_layout()
plt.show()

示例 3:跨表关联分析

response = visualization_agent.invoke({
    "messages": [{"role": "user", "content": "计算每个产品类别的库存总量,用水平条形图展示,并标注具体数值"}]
})
final_msg = response["messages"][-1]
print(f"\n分析结果:\n{final_msg.content}")

6.6 调试:查看 Agent 生成的代码

# 查看 Agent 的完整推理过程,重点是它生成了什么代码
response = visualization_agent.invoke({
    "messages": [{"role": "user", "content": "分析各产品类别的平均价格,画一个饼图"}]
})

for i, msg in enumerate(response["messages"]):
    msg_type = msg.__class__.__name__

    # AIMessage 中的 tool_calls 包含 Agent 生成的代码
    if msg_type == "AIMessage" and hasattr(msg, "tool_calls") and msg.tool_calls:
        print(f"\n{'='*60}")
        for tc in msg.tool_calls:
            if tc["name"] == "execute_python_code":
                print(f"Agent 生成的第 {i+1} 段代码:")
                print(f"{'='*60}")
                print(tc["args"].get("code", ""))

    # ToolMessage 是代码执行的结果
    elif msg_type == "ToolMessage":
        print(f"\n--- 执行结果 ---")
        print(msg.content[:500])

# 最终回答
final_msg = response["messages"][-1]
print(f"\n{'='*60}")
print(f"最终回答:\n{final_msg.content}")

七、系统整合与工程化建议

7.1 双 Agent 协作方案

在实际项目中,两个 Agent 需要协同工作。这里有两种主流方案:

方案 A:编排器模式(推荐)

编排器模式的核心思路是:用一个路由层判断用户意图,把请求分发到合适的 Agent:

用户提问 → 编排器判断意图
  ├─ 查询类 → NL2SQL Agent → 返回数据
  ├─ 分析类 → 可视化 Agent → 返回图表
  └─ 混合类 → 先查数据 → 再画图表 → 返回结果
# 编排器:根据用户意图路由到合适的 Agent
def run_data_analysis(user_query: str) -> str:
    """
    统一入口:自动判断用户意图,路由到对应的 Agent。
    """
    # 用 LLM 做意图分类(也可以用规则匹配,看场景复杂度)
    classification_prompt = f"""判断以下用户问题属于哪种类型:
    - "query":需要查询数据库获取数据
    - "visualize":需要画图或做统计分析
    - "both":需要先查数据,再画图分析

    用户问题:{user_query}

    只回复一个词:query / visualize / both"""

    intent = llm.invoke(classification_prompt).content.strip().lower()

    if intent == "query":
        # create_agent 返回的消息格式:取最后一条消息
        result = sql_agent.invoke({"messages": [{"role": "user", "content": user_query}]})
        return result["messages"][-1].content
    elif intent == "visualize":
        result = visualization_agent.invoke({"messages": [{"role": "user", "content": user_query}]})
        return result["messages"][-1].content
    else:  # both
        # 先查数据
        data_result = sql_agent.invoke({"messages": [{"role": "user", "content": user_query}]})
        data_content = data_result["messages"][-1].content
        # 把查询结果传给可视化 Agent
        viz_input = f"基于以下数据进行可视化分析:\n{data_content}\n\n原始问题:{user_query}"
        viz_result = visualization_agent.invoke({"messages": [{"role": "user", "content": viz_input}]})
        return viz_result["messages"][-1].content

方案 B:统一 Agent 模式(作业)

把 SQL 工具和 Python 代码执行工具都挂到同一个 Agent 上,让 LLM 自己决定调用哪些工具。优点是简单,缺点是工具太多时 LLM 容易"选择困难"。

7.2 安全最佳实践

风险 缓解措施
SQL 注入 使用参数化查询,Agent 生成的 SQL 只读权限
恶意代码执行 生产环境用 Docker/WASM 沙箱隔离
API Key 泄露 环境变量管理,禁止硬编码
数据泄露 按用户角色控制数据访问范围
大量数据导出 限制查询返回行数(LIMIT

7.3 生产环境部署清单

[ ] 数据库用户只授予 SELECT 权限(禁止 DROP/DELETE/UPDATE)
[ ] 代码沙箱替换为 Docker 容器(限制 CPU/内存/网络)
[ ] 接入日志系统,记录所有 Agent 调用和 SQL 执行
[ ] 添加速率限制,防止恶意刷接口
[ ] 实现会话管理,支持多轮对话上下文
[ ] 接入监控告警,异常查询及时通知
[ ] 前端加一层用户认证,不要裸奔
[ ] 考虑缓存热点查询结果,降低数据库压力

7.4 进阶方向

当你把这套系统跑通之后,可以继续探索:

  • 接入更多数据源:Excel、CSV、API 接口、MongoDB 等
  • 支持多轮对话:用 LangChain 的 Memory 模块维护上下文
  • 图表自动美化:用 LLM 选择最合适的图表类型和配色方案
  • 报告自动生成:定期跑批,自动生成周报/月报
  • RAG 增强:把数据字典、业务规则做成向量库,提升 Agent 的业务理解能力

写在最后:这套系统的核心价值不在于技术多复杂,而在于它打通了"人的语言"到"数据的语言"的最后一公里。让不懂 SQL 的人也能做数据分析,这才是 Agent 真正该干的事。