{
  "metadata": {
    "id": "ch28",
    "title": "第28章：数据分析平台",
    "volume": "vol8",
    "volume_title": "实战案例集",
    "word_count": 26306,
    "difficulty": "advanced",
    "prerequisites": [
      "ch17"
    ],
    "key_concepts": [
      "需求分析与功能规划",
      "业务背景",
      "功能清单",
      "非功能需求",
      "架构设计",
      "项目结构",
      "核心类设计",
      "核心代码实现",
      "项目配置与 LLM 客户端",
      "数据库 Schema 加载器",
      "NL2SQL Agent（核心）",
      "SQL 安全执行器",
      "可视化推荐 Agent",
      "异常检测 Agent",
      "报告生成 Agent"
    ],
    "learning_objectives": [],
    "estimated_tokens": 15784,
    "source_file": "vol8/ch28_数据分析平台.md"
  },
  "overview": "",
  "sections": [
    {
      "id": "28.1",
      "title": "28.1 需求分析与功能规划",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.1.1",
          "title": "28.1.1 业务背景",
          "content": "数据驱动决策是企业的核心竞争力，但传统数据分析流程存在严重瓶颈：\n\n1. **需求排期长**：业务人员提出分析需求后，数据团队平均排期 3-7 天\n2. **沟通成本高**：SQL 不懂的 业务 和数据工程师之间反复确认口径\n3. **响应速度慢**：一个临时分析需求可能需要走完完整的数据申请流程\n4. **分析门槛高**：BI 工具（Tableau、Power BI）需要专业培训才能使用\n\n我们需要构建一个 AI 驱动的数据分析平台，让业务人员用自然语言就能完成数据查询、可视化和洞察发现：\n\n- **自然语言转 SQL**：用中文描述数据需求，自动生成并执行 SQL\n- **智能数据可视化**：根据查询结果自动推荐图表类型并生成可视化\n- **异常检测与告警**：自动发现数据中的异常模式，主动推送告警\n- **分析报告生成**：基于查询结果自动生成结构化分析报告"
        },
        {
          "id": "28.1.2",
          "title": "28.1.2 功能清单",
          "content": ""
        },
        {
          "id": "28.1.3",
          "title": "28.1.3 非功能需求",
          "content": "| 维度 | 指标 |\n|------|------|\n| SQL 生成准确率 | > 90%（标准查询） |\n| 查询响应时间 | P95 < 5 秒（百万级数据） |\n| 并发支持 | 100 QPS |\n| 支持数据库 | MySQL、PostgreSQL、ClickHouse |\n| 安全性 | 禁止 DELETE/DROP/TRUNCATE，只读模式 |\n\n---"
        }
      ]
    },
    {
      "id": "28.2",
      "title": "28.2 架构设计",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.2.1",
          "title": "28.2.1 项目结构",
          "content": ""
        },
        {
          "id": "28.2.2",
          "title": "28.2.2 核心类设计",
          "content": "系统由四个 Agent 组成，Pipeline 模式串联处理：\n\n- **NL2SQLAgent**：将自然语言转换为安全的 SQL 查询，支持多轮追问和语义纠正\n- **VisualizationAgent**：分析查询结果特征，推荐最佳图表类型\n- **AnomalyAgent**：对时间序列数据进行统计异常检测\n- **ReportAgent**：将分析结果汇总为结构化报告\n\n**设计决策**：Pipeline 模式而非并行，因为可视化、异常检测和报告生成都依赖 SQL 执行结果。但 NL2SQL 内部的 Schema 检索和意图识别可以并行。\n\n---"
        }
      ]
    },
    {
      "id": "28.3",
      "title": "28.3 核心代码实现",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.3.1",
          "title": "28.3.1 项目配置与 LLM 客户端",
          "content": ""
        },
        {
          "id": "28.3.2",
          "title": "28.3.2 数据库 Schema 加载器",
          "content": "NL2SQL 的核心前提是理解数据库结构："
        },
        {
          "id": "28.3.3",
          "title": "28.3.3 NL2SQL Agent（核心）",
          "content": ""
        },
        {
          "id": "28.3.4",
          "title": "28.3.4 SQL 安全执行器",
          "content": ""
        },
        {
          "id": "28.3.5",
          "title": "28.3.5 可视化推荐 Agent",
          "content": ""
        },
        {
          "id": "28.3.6",
          "title": "28.3.6 异常检测 Agent",
          "content": ""
        },
        {
          "id": "28.3.7",
          "title": "28.3.7 报告生成 Agent",
          "content": ""
        },
        {
          "id": "28.3.8",
          "title": "28.3.8 数据库服务与 FastAPI 入口",
          "content": "---"
        }
      ]
    },
    {
      "id": "28.4",
      "title": "28.4 演示与测试",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.4.1",
          "title": "28.4.1 演示数据库初始化",
          "content": ""
        },
        {
          "id": "28.4.2",
          "title": "28.4.2 测试用例",
          "content": "---"
        }
      ]
    },
    {
      "id": "28.5",
      "title": "28.5 部署",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.5.1",
          "title": "28.5.1 Docker 部署",
          "content": "---"
        }
      ]
    },
    {
      "id": "28.6",
      "title": "28.6 经验总结",
      "level": 2,
      "content": "",
      "subsections": [
        {
          "id": "28.6.1",
          "title": "28.6.1 踩坑记录",
          "content": "**坑1：NL2SQL 的 Schema 信息过载**\n\n早期我们将完整 DDL（包括索引、约束）全部传给 LLM，导致 Token 消耗大且准确率反而下降。解决方案是**只保留表名、列名、类型和注释**，去掉索引和外键约束。采样 3 行数据作为示例值，极大提升了生成准确率。\n\n**坑2：多表 JOIN 的歧义问题**\n\n\"销售额\"可能指 orders.amount 的 SUM，也可能涉及 products 的关联。\"各品类销售额\"需要 JOIN orders 和 products。我们通过 **Few-Shot 示例 + Schema 关系描述** 来消歧。在 Schema 中标注 `[FK]` 关系帮助 LLM 理解表间连接。\n\n**坑3：SQL 注入防护**\n\n用户输入的问题可能包含恶意 SQL 片段。我们通过**白名单验证**（只允许 SELECT）和**正则黑名单**（禁止 DDL/DML）双重防护。生产环境中建议使用只读数据库账号作为额外保障。\n\n**坑4：异常检测的基线选择**\n\n简单的 Z-Score 在数据有明显趋势时会误报（增长趋势中的正常高点被标记为异常）。我们增加了**趋势检测**步骤：如果整体趋势是增长的，则使用**滑动窗口 Z-Score**而非全局均值作为基线。"
        },
        {
          "id": "28.6.2",
          "title": "28.6.2 性能优化经验",
          "content": "1. **Schema 缓存**：数据库结构不常变化，启动时加载一次并缓存，避免每次查询都查 INFORMATION_SCHEMA\n2. **SQL 执行缓存**：相同的 SQL 查询结果缓存 5 分钟，相同问题不重复生成 SQL\n3. **低温度生成**：SQL 生成使用 temperature=0.1，确保输出的确定性\n4. **自动修正机制**：SQL 执行失败后自动修正，平均 1.2 次修正即可成功"
        },
        {
          "id": "28.6.3",
          "title": "28.6.3 关键设计模式总结",
          "content": "| 模式 | 应用场景 | 效果 |\n|------|---------|------|\n| Schema-as-Prompt | NL2SQL | 准确率提升 30% |\n| 自动修正循环 | SQL 执行容错 | 失败恢复率 85% |\n| 规则+特征混合 | 可视化推荐 | 推荐准确率 92% |\n| Pipeline 串联 | 完整分析流程 | 一次调用完成全流程 |"
        },
        {
          "id": "28.6.4",
          "title": "28.6.4 未来演进方向",
          "content": "1. **Text-to-Chart**：直接生成 ECharts/Plotly 代码，支持交互式图表\n2. **自然语言数据建模**：根据业务需求自动设计数据库表结构\n3. **预测分析**：集成时间序列预测（Prophet、ARIMA），自动生成预测报告\n4. **多数据源联邦查询**：同时查询 MySQL、ClickHouse、Elasticsearch\n\n---\n\n**本章小结**：AI 数据分析平台的核心是 **NL2SQL + 自动化 Pipeline**。关键在于 Schema 信息的高质量表示、SQL 安全执行机制和失败自修正能力。通过将自然语言查询、SQL 生成、安全执行、可视化推荐和异常检测串联为完整的 Pipeline，业务人员可以用一句话完成从\"提问题\"到\"拿结论\"的全流程，将数据分析的响应时间从天级缩短到秒级。"
        }
      ]
    }
  ],
  "code_blocks": [
    {
      "id": "code-1",
      "language": "text",
      "description": "- 分析报告生成：基于查询结果自动生成结构化分析报告",
      "code": "┌─────────────────────────────────────────────────────────┐\n│               AI 数据分析平台功能架构                     │\n├─────────────────────────────────────────────────────────┤\n│  ┌──────────────┐  ┌──────────────┐  ┌───────────────┐ │\n│  │  自然语言层   │  │  分析引擎层   │  │  可视化层     │ │\n│  │ • NL2SQL     │  │ • SQL优化器  │  │ • 图表推荐   │ │\n│  │ • 语义纠正   │  │ • 聚合计算   │  │ • 交互式图表 │ │\n│  │ • 多轮追问   │  │ • 趋势分析   │  │ • 仪表盘生成 │ │\n│  │ • 意图识别   │  │ • 异常检测   │  │ • 导出PDF    │ │\n│  └──────────────┘  └──────────────┘  └───────────────┘ │\n│  ┌───────────────────────────────────────────────────┐  │\n│  │                   数据接入层                        │  │\n│  │  • MySQL/PostgreSQL  • ClickHouse  • CSV/Excel    │  │\n│  └───────────────────────────────────────────────────┘  │\n└─────────────────────────────────────────────────────────┘",
      "section_ref": "28.1.2",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-2",
      "language": "text",
      "description": "",
      "code": "ai-data-platform/\n├── app/\n│   ├── main.py                    # FastAPI 入口\n│   ├── config.py                  # 配置管理\n│   ├── models/                    # 数据模型\n│   │   ├── query.py               # 查询模型\n│   │   ├── chart.py               # 图表模型\n│   │   └── report.py              # 报告模型\n│   ├── agents/                    # Agent 核心\n│   │   ├── nl2sql_agent.py        # 自然语言转 SQL\n│   │   ├── visualization_agent.py # 可视化推荐\n│   │   ├── anomaly_agent.py       # 异常检测\n│   │   └── report_agent.py        # 报告生成\n│   ├── services/                  # 业务服务\n│   │   ├── db_service.py          # 数据库连接管理\n│   │   ├── sql_executor.py        # SQL 安全执行\n│   │   └── cache_service.py       # 查询缓存\n│   └── utils/\n│       ├── llm_client.py          # LLM 客户端\n│       └── schema_loader.py       # 数据库 Schema 加载\n├── tests/\n├── demo_data/\n│   └── init_db.sql                # 演示数据库初始化\n└── requirements.txt",
      "section_ref": "28.2.1",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-3",
      "language": "python",
      "description": "",
      "code": "# app/config.py\n\"\"\"AI 数据分析平台配置管理\"\"\"\n\nfrom pydantic_settings import BaseSettings\nfrom enum import Enum\n\n\nclass DatabaseType(str, Enum):\n    MYSQL = \"mysql\"\n    POSTGRESQL = \"postgresql\"\n    CLICKHOUSE = \"clickhouse\"\n\n\nclass Settings(BaseSettings):\n    APP_NAME: str = \"AI 数据分析平台\"\n    APP_VERSION: str = \"1.0.0\"\n    DEBUG: bool = False\n\n    # LLM 配置\n    LLM_API_KEY: str = \"\"\n    LLM_BASE_URL: str = \"https://api.openai.com/v1\"\n    LLM_MODEL: str = \"gpt-4o\"\n    LLM_TEMPERATURE: float = 0.1\n    LLM_MAX_TOKENS: int = 4096\n\n    # 数据库配置\n    DB_TYPE: DatabaseType = DatabaseType.MYSQL\n    DB_HOST: str = \"localhost\"\n    DB_PORT: int = 3306\n    DB_USER: str = \"root\"\n    DB_PASSWORD: str = \"\"\n    DB_NAME: str = \"ecommerce\"\n    DB_READONLY: bool = True\n\n    # 查询限制\n    MAX_ROWS_RETURNED: int = 10000\n    QUERY_TIMEOUT: int = 30\n    MAX_SQL_LENGTH: int = 5000\n\n    # 缓存配置\n    CACHE_ENABLED: bool = True\n    CACHE_TTL: int = 300\n\n    class Config:\n        env_file = \".env\"\n        env_prefix = \"DP_\"\n\n\nsettings = Settings()",
      "section_ref": "28.3.1",
      "runnable": true,
      "dependencies": [
        "pydantic_settings"
      ]
    },
    {
      "id": "code-4",
      "language": "python",
      "description": "settings = Settings()",
      "code": "# app/utils/llm_client.py\n\"\"\"LLM 客户端封装\"\"\"\n\nimport json\nfrom typing import Optional, List, Dict\nfrom openai import OpenAI\nfrom app.config import settings\n\n\nclass LLMClient:\n    _instance: Optional['LLMClient'] = None\n\n    def __new__(cls) -> 'LLMClient':\n        if cls._instance is None:\n            cls._instance = super().__new__(cls)\n            cls._instance._client = OpenAI(\n                api_key=settings.LLM_API_KEY,\n                base_url=settings.LLM_BASE_URL,\n            )\n        return cls._instance\n\n    async def chat(\n        self,\n        messages: List[Dict[str, str]],\n        system_prompt: Optional[str] = None,\n        temperature: Optional[float] = None,\n        max_tokens: Optional[int] = None,\n        response_format: Optional[dict] = None,\n    ) -> str:\n        full_messages = []\n        if system_prompt:\n            full_messages.append({\"role\": \"system\", \"content\": system_prompt})\n        full_messages.extend(messages)\n        kwargs = {\n            \"model\": settings.LLM_MODEL,\n            \"messages\": full_messages,\n            \"temperature\": temperature or settings.LLM_TEMPERATURE,\n            \"max_tokens\": max_tokens or settings.LLM_MAX_TOKENS,\n        }\n        if response_format:\n            kwargs[\"response_format\"] = response_format\n        response = self._client.chat.completions.create(**kwargs)\n        return response.choices[0].message.content\n\n    async def chat_json(\n        self,\n        messages: List[Dict[str, str]],\n        system_prompt: Optional[str] = None,\n    ) -> dict:\n        content = await self.chat(\n            messages=messages, system_prompt=system_prompt,\n            temperature=0.1, response_format={\"type\": \"json_object\"},\n        )\n        return json.loads(content)\n\n\nllm_client = LLMClient()",
      "section_ref": "28.3.1",
      "runnable": true,
      "dependencies": [
        "openai",
        "app"
      ]
    },
    {
      "id": "code-5",
      "language": "python",
      "description": "NL2SQL 的核心前提是理解数据库结构：",
      "code": "# app/utils/schema_loader.py\n\"\"\"数据库 Schema 加载器\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Optional\nfrom app.config import settings\n\n\n@dataclass\nclass ColumnInfo:\n    name: str\n    data_type: str\n    comment: str = \"\"\n    is_primary: bool = False\n    is_foreign: bool = False\n    sample_values: List[str] = field(default_factory=list)\n\n\n@dataclass\nclass TableInfo:\n    name: str\n    comment: str = \"\"\n    columns: Dict[str, ColumnInfo] = field(default_factory=dict)\n    row_count: int = 0\n\n    def to_prompt(self) -> str:\n        \"\"\"生成给 LLM 的表描述\"\"\"\n        cols = []\n        for col in self.columns.values():\n            flags = []\n            if col.is_primary:\n                flags.append(\"PK\")\n            if col.is_foreign:\n                flags.append(\"FK\")\n            flag_str = f\" [{', '.join(flags)}]\" if flags else \"\"\n            sample = (f\" (例: {', '.join(col.sample_values[:3])})\"\n                      if col.sample_values else \"\")\n            cols.append(f\"  - {col.name}: {col.data_type}{flag_str}{sample}\")\n        header = f\"表 `{self.name}`\"\n        if self.comment:\n            header += f\" ({self.comment})\"\n        header += f\", 约{self.row_count}行:\\n\"\n        return header + \"\\n\".join(cols)\n\n\nclass SchemaLoader:\n    def __init__(self, db_service):\n        self._db = db_service\n        self._tables: Dict[str, TableInfo] = {}\n        self._ddl_cache: Optional[str] = None\n\n    async def load_schema(self) -> str:\n        if self._ddl_cache:\n            return self._ddl_cache\n\n        tables = await self._load_mysql()\n        self._tables = tables\n        self._ddl_cache = \"\\n\\n\".join(\n            t.to_prompt() for t in tables.values())\n        return self._ddl_cache\n\n    async def _load_mysql(self) -> Dict[str, TableInfo]:\n        tables = {}\n        table_names = await self._db.fetch_all(\n            \"SELECT TABLE_NAME, TABLE_COMMENT \"\n            \"FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s\",\n            (settings.DB_NAME,))\n        for name, comment in table_names:\n            cols_rows = await self._db.fetch_all(\n                \"SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT, \"\n                \"COLUMN_KEY FROM information_schema.COLUMNS \"\n                \"WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s \"\n                \"ORDER BY ORDINAL_POSITION\",\n                (settings.DB_NAME, name))\n            table = TableInfo(name=name, comment=comment or \"\")\n            for cname, dtype, ccomment, ckey in cols_rows:\n                table.columns[cname] = ColumnInfo(\n                    name=cname, data_type=dtype, comment=ccomment or \"\",\n                    is_primary=ckey == \"PRI\", is_foreign=ckey == \"MUL\")\n            # 采样数据\n            try:\n                samples = await self._db.fetch_all(\n                    f\"SELECT * FROM `{name}` LIMIT 3\")\n                for row in samples:\n                    for col_name, col in table.columns.items():\n                        if len(col.sample_values) < 3 and row.get(col_name):\n                            col.sample_values.append(\n                                str(row[col_name])[:50])\n            except Exception:\n                pass\n            # 行数\n            try:\n                count_row = await self._db.fetch_one(\n                    f\"SELECT COUNT(*) as cnt FROM `{name}`\")\n                table.row_count = count_row[\"cnt\"] if count_row else 0\n            except Exception:\n                pass\n            tables[name] = table\n        return tables",
      "section_ref": "28.3.2",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-6",
      "language": "python",
      "description": "",
      "code": "# app/agents/nl2sql_agent.py\n\"\"\"自然语言转 SQL Agent\"\"\"\n\nimport re\nfrom dataclasses import dataclass, field\nfrom typing import Optional, List, Dict\nfrom app.utils.llm_client import llm_client\nfrom app.utils.schema_loader import SchemaLoader\n\n\n@dataclass\nclass SQLResult:\n    sql: str\n    intent: str  # query/aggregate/trend/compare/rank\n    confidence: float\n    tables_used: List[str] = field(default_factory=list)\n    explanation: str = \"\"\n    follow_up_questions: List[str] = field(default_factory=list)\n\n\nDANGEROUS_KEYWORDS = re.compile(\n    r'\\b(DROP|DELETE|TRUNCATE|ALTER|CREATE|INSERT|UPDATE|GRANT|REVOKE)\\b',\n    re.IGNORECASE\n)\n\n\nclass NL2SQLAgent:\n    SYSTEM_PROMPT = \"\"\"你是一个专业的数据分析师，精通 SQL。将用户的自然语言问题转换为准确的 SQL 查询。\n\n数据库 Schema：\n{schema}\n\n规则：\n1. 只生成 SELECT 查询，禁止任何修改操作\n2. 使用表别名提高可读性\n3. 对聚合查询确保 GROUP BY 正确\n4. 时间范围使用标准日期函数\n5. 添加 LIMIT 限制结果数量（默认1000）\n6. 如果问题不明确，列出可能的歧义点\n\n返回 JSON：\n{{\n  \"sql\": \"生成的SQL语句\",\n  \"intent\": \"query|aggregate|trend|compare|rank\",\n  \"confidence\": 0.0-1.0,\n  \"explanation\": \"对SQL逻辑的简要说明\",\n  \"tables_used\": [\"使用的表名\"],\n  \"follow_up_questions\": [\"建议追问的问题\"]\n}}\"\"\"\n\n    def __init__(self, schema_loader: SchemaLoader):\n        self._schema_loader = schema_loader\n        self._schema_text = \"\"\n\n    async def _ensure_schema(self):\n        if not self._schema_text:\n            self._schema_text = await self._schema_loader.load_schema()\n\n    async def generate_sql(\n        self, question: str, context: Optional[Dict] = None,\n    ) -> SQLResult:\n        \"\"\"将自然语言转换为 SQL\"\"\"\n        await self._ensure_schema()\n        prompt = self.SYSTEM_PROMPT.format(schema=self._schema_text)\n        messages = self._build_messages(question, context)\n\n        try:\n            import json\n            result = await llm_client.chat_json(\n                messages=messages, system_prompt=prompt)\n            sql = self._sanitize_sql(result.get(\"sql\", \"\"))\n            return SQLResult(\n                sql=sql,\n                intent=result.get(\"intent\", \"query\"),\n                confidence=float(result.get(\"confidence\", 0.8)),\n                tables_used=result.get(\"tables_used\", []),\n                explanation=result.get(\"explanation\", \"\"),\n                follow_up_questions=result.get(\"follow_up_questions\", []),\n            )\n        except Exception as e:\n            return SQLResult(\n                sql=\"\", intent=\"query\", confidence=0.0,\n                explanation=f\"SQL 生成失败: {str(e)}\")\n\n    def _build_messages(\n        self, question: str, context: Optional[Dict] = None,\n    ) -> List[Dict]:\n        messages = []\n        if context and context.get(\"conversation\"):\n            messages.extend(context[\"conversation\"][-4:])\n        if context and context.get(\"last_sql\"):\n            messages.append({\n                \"role\": \"assistant\",\n                \"content\": f\"上一步 SQL: {context['last_sql']}\",\n            })\n        if context and context.get(\"time_range\"):\n            messages.append({\n                \"role\": \"system\",\n                \"content\": f\"时间范围: {context['time_range']}\",\n            })\n        messages.append({\"role\": \"user\", \"content\": question})\n        return messages\n\n    def _sanitize_sql(self, sql: str) -> str:\n        \"\"\"安全清理 SQL\"\"\"\n        sql = sql.strip()\n        if sql.startswith(\"```\"):\n            sql = re.sub(r'^```\\w*\\n?', '', sql)\n            sql = re.sub(r'\\n?```$', '', sql)\n        sql = sql.strip().rstrip(\";\")\n        if DANGEROUS_KEYWORDS.search(sql):\n            raise ValueError(\"检测到不允许的 SQL 操作（DDL/DML）\")\n        if not sql.upper().strip().startswith(\"SELECT\"):\n            raise ValueError(\"只允许 SELECT 查询\")\n        if \"LIMIT\" not in sql.upper():\n            sql += f\"\\n LIMIT 1000\"\n        return sql\n\n    async def refine_sql(\n        self, question: str, prev_sql: str, error_msg: str,\n    ) -> SQLResult:\n        \"\"\"SQL 执行失败后自动修正\"\"\"\n        await self._ensure_schema()\n        prompt = self.SYSTEM_PROMPT.format(schema=self._schema_text)\n        messages = [\n            {\"role\": \"user\", \"content\": question},\n            {\"role\": \"assistant\", \"content\": f\"SQL:\\n```sql\\n{prev_sql}\\n```\"},\n            {\"role\": \"user\", \"content\": f\"执行出错: {error_msg}\\n请修正。\"},\n        ]\n        try:\n            import json\n            result = await llm_client.chat_json(\n                messages=messages, system_prompt=prompt)\n            sql = self._sanitize_sql(result.get(\"sql\", \"\"))\n            return SQLResult(\n                sql=sql, confidence=float(result.get(\"confidence\", 0.6)),\n                explanation=f\"修正后: {result.get('explanation', '')}\",\n            )\n        except Exception:\n            return SQLResult(sql=\"\", confidence=0.0, explanation=\"修正失败\")",
      "section_ref": "28.3.3",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-7",
      "language": "python",
      "description": "",
      "code": "# app/services/sql_executor.py\n\"\"\"SQL 安全执行器\"\"\"\n\nimport re\nimport time\nimport logging\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional\nfrom app.config import settings\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass QueryResult:\n    columns: List[str]\n    rows: List[Dict[str, Any]]\n    row_count: int\n    execution_time: float\n    sql: str\n    truncated: bool = False\n    error: Optional[str] = None\n\n    def to_dict_list(self) -> List[Dict]:\n        return self.rows[:self.row_count]\n\n    def summary_stats(self) -> Dict[str, Any]:\n        \"\"\"对数值列计算统计摘要\"\"\"\n        stats = {}\n        for col in self.columns:\n            values = [row[col] for row in self.rows\n                      if row.get(col) is not None]\n            if not values:\n                continue\n            try:\n                nums = [float(v) for v in values]\n                stats[col] = {\n                    \"min\": round(min(nums), 2),\n                    \"max\": round(max(nums), 2),\n                    \"avg\": round(sum(nums) / len(nums), 2),\n                    \"count\": len(nums),\n                }\n            except (ValueError, TypeError):\n                unique = set(str(v) for v in values)\n                stats[col] = {\n                    \"unique_count\": len(unique),\n                    \"top_values\": list(unique)[:5],\n                }\n        return stats\n\n\nclass SQLExecutor:\n    def __init__(self, db_service):\n        self._db = db_service\n\n    async def execute(self, sql: str) -> QueryResult:\n        self._validate_sql(sql)\n        start = time.time()\n        try:\n            rows = await self._db.fetch_all(sql)\n            elapsed = time.time() - start\n            if not rows:\n                return QueryResult(\n                    columns=[], rows=[], row_count=0,\n                    execution_time=elapsed, sql=sql)\n            columns = list(rows[0].keys())\n            truncated = len(rows) >= 10000\n            return QueryResult(\n                columns=columns, rows=rows,\n                row_count=len(rows),\n                execution_time=round(elapsed, 3),\n                sql=sql, truncated=truncated,\n            )\n        except Exception as e:\n            elapsed = time.time() - start\n            logger.error(f\"SQL 执行失败: {e}\")\n            return QueryResult(\n                columns=[], rows=[], row_count=0,\n                execution_time=round(elapsed, 3),\n                sql=sql, error=str(e),\n            )\n\n    def _validate_sql(self, sql: str):\n        sql_upper = sql.upper().strip()\n        if not sql_upper.startswith(\"SELECT\"):\n            raise ValueError(\"只允许执行 SELECT 查询\")\n        if re.search(r';\\s*(DROP|DELETE|INSERT|UPDATE|ALTER)',\n                      sql, re.IGNORECASE):\n            raise ValueError(\"检测到多语句危险操作\")\n        if len(sql) > 5000:\n            raise ValueError(\"SQL 过长\")",
      "section_ref": "28.3.4",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-8",
      "language": "python",
      "description": "",
      "code": "# app/agents/visualization_agent.py\n\"\"\"智能可视化推荐 Agent\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional\nfrom app.services.sql_executor import QueryResult\n\n\n@dataclass\nclass ChartConfig:\n    chart_type: str  # bar/line/pie/scatter/table/metric\n    title: str\n    x_axis: str = \"\"\n    y_axis: str = \"\"\n    x_label: str = \"\"\n    y_label: str = \"\"\n    series: List[str] = field(default_factory=list)\n\n\n@dataclass\nclass VisualizationResult:\n    charts: List[ChartConfig]\n    recommendation_reason: str\n\n\nclass VisualizationAgent:\n    \"\"\"根据查询结果特征推荐最佳可视化方案\"\"\"\n\n    CHART_RULES = {\n        \"bar\": {\"triggers\": [\"类别\", \"排名\", \"TOP\", \"对比\", \"分类\"]},\n        \"line\": {\"triggers\": [\"趋势\", \"时间\", \"变化\", \"走势\", \"增长\"]},\n        \"pie\": {\"triggers\": [\"占比\", \"比例\", \"构成\", \"分布\", \"份额\"]},\n        \"scatter\": {\"triggers\": [\"相关性\", \"散点\", \"关系\"]},\n        \"metric\": {\"triggers\": [\"总计\", \"总量\", \"平均值\", \"总额\"]},\n    }\n\n    def recommend(\n        self, result: QueryResult, intent: str, question: str,\n    ) -> VisualizationResult:\n        charts = []\n        analysis = self._analyze_data(result)\n        rule_chart = self._match_rules(question, intent, analysis)\n        if rule_chart:\n            charts.append(rule_chart)\n        feature_chart = self._infer_from_features(result, analysis)\n        if feature_chart:\n            charts.append(feature_chart)\n        if not charts:\n            charts.append(ChartConfig(chart_type=\"table\", title=\"查询结果\"))\n        return VisualizationResult(charts=charts, recommendation_reason=\"自动推荐\")\n\n    def _analyze_data(self, result: QueryResult) -> Dict[str, Any]:\n        if not result.columns or not result.rows:\n            return {\"is_empty\": True, \"row_count\": 0}\n        numeric_cols, category_cols, date_cols = [], [], []\n        for col in result.columns:\n            values = [row.get(col) for row in result.rows[:50]\n                      if row.get(col) is not None]\n            if not values:\n                continue\n            is_numeric = True\n            for v in values[:10]:\n                try:\n                    float(v)\n                except (ValueError, TypeError):\n                    is_numeric = False\n                    break\n            if is_numeric:\n                numeric_cols.append(col)\n            elif self._is_date_col(values):\n                date_cols.append(col)\n            else:\n                category_cols.append(col)\n        return {\n            \"is_empty\": False, \"row_count\": result.row_count,\n            \"numeric_cols\": numeric_cols, \"category_cols\": category_cols,\n            \"date_cols\": date_cols, \"has_single_row\": result.row_count == 1,\n        }\n\n    def _is_date_col(self, values: List) -> bool:\n        import re\n        patterns = [r'\\d{4}-\\d{2}-\\d{2}', r'\\d{4}/\\d{2}/\\d{2}']\n        matched = sum(1 for v in values[:5]\n                      if any(re.search(p, str(v)) for p in patterns))\n        return matched >= 3\n\n    def _match_rules(self, question, intent, analysis):\n        q = question.lower()\n        for chart_type, rules in self.CHART_RULES.items():\n            if any(t in q for t in rules[\"triggers\"]):\n                return self._create_chart(chart_type, analysis, question)\n        return None\n\n    def _infer_from_features(self, result, analysis):\n        if analysis.get(\"has_single_row\") and analysis.get(\"numeric_cols\"):\n            return ChartConfig(\n                chart_type=\"metric\",\n                title=f\"{result.columns[0]} = {list(result.rows[0].values())[0]}\")\n        if analysis.get(\"date_cols\") and analysis.get(\"numeric_cols\"):\n            return ChartConfig(\n                chart_type=\"line\", title=\"数据趋势\",\n                x_axis=analysis[\"date_cols\"][0],\n                y_axis=analysis[\"numeric_cols\"][0])\n        if (len(analysis.get(\"category_cols\", [])) == 1\n                and analysis.get(\"numeric_cols\")):\n            return ChartConfig(\n                chart_type=\"bar\", title=\"分类对比\",\n                x_axis=analysis[\"category_cols\"][0],\n                y_axis=analysis[\"numeric_cols\"][0])\n        return None\n\n    def _create_chart(self, chart_type, analysis, question):\n        date_cols = analysis.get(\"date_cols\", [])\n        category_cols = analysis.get(\"category_cols\", [])\n        numeric_cols = analysis.get(\"numeric_cols\", [])\n        config = {\"chart_type\": chart_type, \"title\": question}\n        if chart_type == \"line\" and date_cols and numeric_cols:\n            config.update({\"x_axis\": date_cols[0], \"y_axis\": numeric_cols[0]})\n        elif chart_type in (\"bar\", \"pie\") and category_cols and numeric_cols:\n            config.update({\"x_axis\": category_cols[0], \"y_axis\": numeric_cols[0]})\n        elif chart_type == \"metric\" and numeric_cols:\n            config.update({\"series\": numeric_cols})\n        return ChartConfig(**config)",
      "section_ref": "28.3.5",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-9",
      "language": "python",
      "description": "",
      "code": "# app/agents/anomaly_agent.py\n\"\"\"数据异常检测 Agent\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional\nfrom statistics import mean, stdev\nfrom app.services.sql_executor import QueryResult\n\n\n@dataclass\nclass AnomalyPoint:\n    timestamp: str\n    value: float\n    expected_range: tuple\n    deviation_percent: float\n    severity: str  # info/warning/critical\n    description: str\n\n\n@dataclass\nclass AnomalyReport:\n    has_anomaly: bool\n    anomaly_points: List[AnomalyPoint]\n    overall_trend: str\n    summary: str\n    recommendations: List[str]\n\n\nclass AnomalyAgent:\n    \"\"\"基于统计方法的时序异常检测\"\"\"\n\n    def detect(\n        self, result: QueryResult, date_col: str, value_col: str,\n        sensitivity: float = 2.0,\n    ) -> AnomalyReport:\n        if not result.rows or date_col not in result.columns:\n            return AnomalyReport(\n                has_anomaly=False, anomaly_points=[],\n                overall_trend=\"unknown\", summary=\"数据不足\")\n\n        series = []\n        for row in result.rows:\n            try:\n                val = float(row.get(value_col, 0))\n                ts = str(row.get(date_col, \"\"))\n                series.append({\"timestamp\": ts, \"value\": val})\n            except (ValueError, TypeError):\n                continue\n\n        if len(series) < 3:\n            return AnomalyReport(\n                has_anomaly=False, anomaly_points=[],\n                overall_trend=\"unknown\", summary=\"数据点过少\")\n\n        values = [p[\"value\"] for p in series]\n        avg = mean(values)\n        std = stdev(values) if len(values) > 1 else 0\n\n        anomalies = []\n        for point in series:\n            z_score = abs(point[\"value\"] - avg) / std if std > 0 else 0\n            if z_score >= sensitivity:\n                lower = round(avg - sensitivity * std, 2)\n                upper = round(avg + sensitivity * std, 2)\n                deviation = (abs(point[\"value\"] - avg) / avg * 100\n                             if avg != 0 else 0)\n                anomalies.append(AnomalyPoint(\n                    timestamp=point[\"timestamp\"],\n                    value=point[\"value\"],\n                    expected_range=(lower, upper),\n                    deviation_percent=round(deviation, 1),\n                    severity=\"critical\" if z_score >= 3 else \"warning\",\n                    description=(f\"{point['timestamp']} 数值 \"\n                                 f\"{point['value']:.2f} 偏离均值 \"\n                                 f\"{deviation:.1f}%\"),\n                ))\n\n        trend = self._detect_trend(values)\n        summary = (f\"共分析 {len(series)} 个数据点；均值: {avg:.2f}, \"\n                   f\"标准差: {std:.2f}；趋势: {trend}；\"\n                   f\"异常点: {len(anomalies)}个\")\n        recs = []\n        if anomalies:\n            recs.append(f\"有 {len(anomalies)} 个异常点需要关注\")\n        if trend == \"decreasing\":\n            recs.append(\"整体呈下降趋势，建议分析原因\")\n\n        return AnomalyReport(\n            has_anomaly=len(anomalies) > 0,\n            anomaly_points=anomalies,\n            overall_trend=trend,\n            summary=summary,\n            recommendations=recs,\n        )\n\n    def _detect_trend(self, values: List[float]) -> str:\n        if len(values) < 2:\n            return \"stable\"\n        mid = len(values) // 2\n        first = mean(values[:mid])\n        second = mean(values[mid:])\n        if first == 0:\n            return \"stable\"\n        change = (second - first) / abs(first) * 100\n        if change > 10:\n            return \"increasing\"\n        elif change < -10:\n            return \"decreasing\"\n        return \"stable\"",
      "section_ref": "28.3.6",
      "runnable": true,
      "dependencies": [
        "statistics",
        "app"
      ]
    },
    {
      "id": "code-10",
      "language": "python",
      "description": "",
      "code": "# app/agents/report_agent.py\n\"\"\"分析报告生成 Agent\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional\nfrom datetime import datetime\nfrom app.utils.llm_client import llm_client\nfrom app.services.sql_executor import QueryResult\nfrom app.agents.anomaly_agent import AnomalyReport\n\n\n@dataclass\nclass ReportSection:\n    title: str\n    content: str\n    data_summary: Dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass\nclass AnalysisReport:\n    title: str\n    sections: List[ReportSection]\n    key_findings: List[str]\n    recommendations: List[str]\n    created_at: str = \"\"\n\n\nclass ReportAgent:\n    SYSTEM_PROMPT = \"\"\"你是一个资深数据分析师，擅长将数据查询结果转化为清晰的分析报告。\n要求：用数据说话、突出关键发现、给出可操作建议、使用 Markdown 格式。\"\"\"\n\n    async def generate(\n        self, question: str, result: QueryResult,\n        anomaly_report: Optional[AnomalyReport] = None,\n    ) -> AnalysisReport:\n        data_context = self._build_data_context(result)\n        messages = [\n            {\"role\": \"user\", \"content\": f\"用户问题: {question}\"},\n            {\"role\": \"assistant\", \"content\": f\"执行 SQL: {result.sql}\"},\n            {\"role\": \"user\", \"content\": f\"查询结果:\\n{data_context}\"},\n        ]\n        if anomaly_report and anomaly_report.has_anomaly:\n            anomaly_text = \"\\n\".join(\n                f\"- {a.description}\" for a in anomaly_report.anomaly_points)\n            messages.append({\n                \"role\": \"user\",\n                \"content\": (f\"异常检测:\\n{anomaly_text}\\n\"\n                            f\"趋势: {anomaly_report.overall_trend}\"),\n            })\n        messages.append({\"role\": \"user\", \"content\": \"请生成数据分析报告。\"})\n\n        try:\n            report_text = await llm_client.chat(\n                messages=messages, system_prompt=self.SYSTEM_PROMPT,\n                temperature=0.4, max_tokens=4096)\n            return AnalysisReport(\n                title=f\"分析报告: {question}\",\n                sections=[ReportSection(title=\"分析报告\", content=report_text)],\n                key_findings=[], recommendations=[],\n                created_at=datetime.now().isoformat(),\n            )\n        except Exception as e:\n            return AnalysisReport(\n                title=f\"分析报告: {question}\",\n                sections=[ReportSection(title=\"错误\",\n                                        content=f\"报告生成失败: {str(e)}\")],\n                key_findings=[], recommendations=[])\n\n    def _build_data_context(self, result: QueryResult) -> str:\n        lines = [f\"共 {result.row_count} 行, {len(result.columns)} 列\"]\n        lines.append(f\"列: {', '.join(result.columns)}\")\n        stats = result.summary_stats()\n        for col, s in stats.items():\n            if \"avg\" in s:\n                lines.append(f\"  {col}: 均值={s['avg']}, \"\n                             f\"最小={s['min']}, 最大={s['max']}\")\n        lines.append(\"数据预览:\")\n        for row in result.rows[:10]:\n            lines.append(\"  \" + str(dict(row)))\n        return \"\\n\".join(lines)",
      "section_ref": "28.3.7",
      "runnable": true,
      "dependencies": [
        "app"
      ]
    },
    {
      "id": "code-11",
      "language": "python",
      "description": "",
      "code": "# app/services/db_service.py\n\"\"\"数据库连接服务\"\"\"\n\nimport aiomysql\nfrom typing import List, Dict, Any, Optional\nfrom app.config import settings\n\n\nclass DatabaseService:\n    def __init__(self):\n        self._pool = None\n\n    async def connect(self):\n        self._pool = await aiomysql.create_pool(\n            host=settings.DB_HOST, port=settings.DB_PORT,\n            user=settings.DB_USER, password=settings.DB_PASSWORD,\n            db=settings.DB_NAME, maxsize=10,\n            autocommit=True, charset='utf8mb4')\n\n    async def close(self):\n        if self._pool:\n            self._pool.close()\n            await self._pool.wait_closed()\n\n    async def fetch_all(self, sql: str, args: tuple = ()) -> List[Dict]:\n        if not self._pool:\n            await self.connect()\n        async with self._pool.acquire() as conn:\n            async with conn.cursor(aiomysql.DictCursor) as cur:\n                await cur.execute(sql, args)\n                return await cur.fetchall()\n\n    async def fetch_one(self, sql: str, args: tuple = ()):\n        if not self._pool:\n            await self.connect()\n        async with self._pool.acquire() as conn:\n            async with conn.cursor(aiomysql.DictCursor) as cur:\n                await cur.execute(sql, args)\n                return await cur.fetchone()",
      "section_ref": "28.3.8",
      "runnable": true,
      "dependencies": [
        "aiomysql",
        "app"
      ]
    },
    {
      "id": "code-12",
      "language": "python",
      "description": "return await cur.fetchone()",
      "code": "# app/main.py\n\"\"\"AI 数据分析平台 - FastAPI 入口\"\"\"\n\nfrom contextlib import asynccontextmanager\nfrom fastapi import FastAPI, HTTPException\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom pydantic import BaseModel\nfrom typing import Optional\n\nfrom app.config import settings\nfrom app.services.db_service import DatabaseService\nfrom app.utils.schema_loader import SchemaLoader\nfrom app.agents.nl2sql_agent import NL2SQLAgent\nfrom app.agents.visualization_agent import VisualizationAgent\nfrom app.agents.anomaly_agent import AnomalyAgent\nfrom app.agents.report_agent import ReportAgent\nfrom app.services.sql_executor import SQLExecutor\n\ndb_service = DatabaseService()\nschema_loader = SchemaLoader(db_service)\nnl2sql_agent = NL2SQLAgent(schema_loader)\nviz_agent = VisualizationAgent()\nanomaly_agent = AnomalyAgent()\nreport_agent = ReportAgent()\nsql_executor = SQLExecutor(db_service)\nsessions: dict = {}\n\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n    await db_service.connect()\n    print(f\"🚀 {settings.APP_NAME} v{settings.APP_VERSION} 启动完成\")\n    yield\n    await db_service.close()\n\n\napp = FastAPI(\n    title=settings.APP_NAME, version=settings.APP_VERSION,\n    lifespan=lifespan,\n    description=\"用自然语言查询数据，AI 自动生成 SQL、图表和报告\")\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"],\n                   allow_credentials=True,\n                   allow_methods=[\"*\"], allow_headers=[\"*\"])\n\n\nclass QueryRequest(BaseModel):\n    question: str\n    session_id: Optional[str] = None\n    time_range: Optional[str] = None\n\n\nclass RefineRequest(BaseModel):\n    question: str\n    prev_sql: str\n    error_msg: str\n\n\n@app.get(\"/health\")\nasync def health():\n    return {\"status\": \"ok\", \"version\": settings.APP_VERSION}\n\n\n@app.get(\"/api/v1/schema\")\nasync def get_schema():\n    schema_text = await schema_loader.load_schema()\n    return {\"schema\": schema_text}\n\n\n@app.post(\"/api/v1/query\")\nasync def query_data(req: QueryRequest):\n    \"\"\"完整 Pipeline：NL2SQL → 执行 → 可视化 → 异常检测\"\"\"\n    context = sessions.get(req.session_id, {}) if req.session_id else {}\n    if req.time_range:\n        context[\"time_range\"] = req.time_range\n\n    # 1. NL2SQL\n    sql_result = await nl2sql_agent.generate_sql(req.question, context)\n    if not sql_result.sql:\n        raise HTTPException(400, detail=f\"无法生成 SQL: {sql_result.explanation}\")\n\n    # 2. 执行 SQL（失败自动修正）\n    exec_result = await sql_executor.execute(sql_result.sql)\n    if exec_result.error:\n        refined = await nl2sql_agent.refine_sql(\n            req.question, sql_result.sql, exec_result.error)\n        if refined.sql:\n            exec_result = await sql_executor.execute(refined.sql)\n            sql_result = refined\n        if exec_result.error:\n            raise HTTPException(400, detail=f\"SQL 执行失败: {exec_result.error}\")\n\n    # 3. 可视化推荐\n    viz_result = viz_agent.recommend(exec_result, sql_result.intent, req.question)\n\n    # 4. 异常检测（时间序列数据）\n    anomaly_report = None\n    date_cols = [c for c in exec_result.columns\n                 if any(k in c.lower() for k in [\"date\", \"time\", \"month\"])]\n    num_cols = [c for c in exec_result.columns\n                if exec_result.rows\n                and isinstance(exec_result.rows[0].get(c), (int, float))]\n    if date_cols and num_cols and exec_result.row_count >= 5:\n        anomaly_report = anomaly_agent.detect(\n            exec_result, date_cols[0], num_cols[0])\n\n    # 5. 保存会话\n    if req.session_id:\n        sessions[req.session_id] = {\n            \"last_sql\": sql_result.sql,\n            \"conversation\": [\n                {\"role\": \"user\", \"content\": req.question},\n                {\"role\": \"assistant\", \"content\": sql_result.explanation},\n            ],\n        }\n\n    return {\n        \"session_id\": req.session_id,\n        \"sql\": sql_result.sql,\n        \"explanation\": sql_result.explanation,\n        \"confidence\": sql_result.confidence,\n        \"data\": exec_result.to_dict_list(),\n        \"row_count\": exec_result.row_count,\n        \"execution_time\": exec_result.execution_time,\n        \"visualization\": [\n            {\"chart_type\": c.chart_type, \"title\": c.title,\n             \"x_axis\": c.x_axis, \"y_axis\": c.y_axis}\n            for c in viz_result.charts\n        ],\n        \"anomaly\": {\n            \"has_anomaly\": anomaly_report.has_anomaly,\n            \"summary\": anomaly_report.summary,\n        } if anomaly_report else None,\n        \"follow_up_questions\": sql_result.follow_up_questions,\n    }\n\n\n@app.post(\"/api/v1/report\")\nasync def generate_report(req: QueryRequest):\n    \"\"\"生成分析报告\"\"\"\n    context = sessions.get(req.session_id, {}) if req.session_id else {}\n    sql_result = await nl2sql_agent.generate_sql(req.question, context)\n    exec_result = await sql_executor.execute(sql_result.sql)\n    if exec_result.error:\n        raise HTTPException(400, detail=exec_result.error)\n    report = await report_agent.generate(req.question, exec_result)\n    return {\n        \"title\": report.title,\n        \"sections\": [{\"title\": s.title, \"content\": s.content}\n                     for s in report.sections],\n        \"key_findings\": report.key_findings,\n        \"recommendations\": report.recommendations,\n    }\n\n\nif __name__ == \"__main__\":\n    import uvicorn\n    uvicorn.run(\"app.main:app\", host=\"0.0.0.0\", port=8000,\n                reload=settings.DEBUG)",
      "section_ref": "28.3.8",
      "runnable": true,
      "dependencies": [
        "contextlib",
        "fastapi",
        "pydantic",
        "app"
      ]
    },
    {
      "id": "code-13",
      "language": "sql",
      "description": "",
      "code": "-- demo_data/init_db.sql\nCREATE TABLE IF NOT EXISTS users (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50) COMMENT '用户名',\n    city VARCHAR(50) COMMENT '城市',\n    register_date DATE COMMENT '注册日期',\n    vip_level INT DEFAULT 0 COMMENT 'VIP等级'\n) COMMENT '用户表';\n\nCREATE TABLE IF NOT EXISTS products (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    name VARCHAR(200) COMMENT '商品名称',\n    category VARCHAR(50) COMMENT '类目',\n    price DECIMAL(10,2) COMMENT '价格',\n    stock INT COMMENT '库存'\n) COMMENT '商品表';\n\nCREATE TABLE IF NOT EXISTS orders (\n    id INT PRIMARY KEY AUTO_INCREMENT,\n    user_id INT COMMENT '用户ID',\n    product_id INT COMMENT '商品ID',\n    amount DECIMAL(10,2) COMMENT '金额',\n    quantity INT COMMENT '数量',\n    status VARCHAR(20) COMMENT '状态',\n    order_date DATETIME COMMENT '下单时间'\n) COMMENT '订单表';\n\n-- 示例数据（省略完整 INSERT，结构同上）\nINSERT INTO users VALUES\n(1,'张三','北京','2024-01-15',3),\n(2,'李四','上海','2024-02-20',1),\n(3,'王五','广州','2024-03-10',2),\n(4,'赵六','深圳','2024-04-05',0);\nINSERT INTO products VALUES\n(1,'无线蓝牙耳机Pro','数码',299.00,500),\n(2,'智能手表S3','数码',1599.00,200),\n(3,'纯棉T恤','服饰',89.00,1000),\n(4,'运动跑鞋X1','运动',459.00,300);\nINSERT INTO orders VALUES\n(1,1,1,299.00,1,'completed','2024-10-01 10:30:00'),\n(2,2,3,178.00,2,'completed','2024-10-05 09:15:00'),\n(3,3,4,459.00,1,'pending','2024-10-07 16:45:00');",
      "section_ref": "28.4.1",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-14",
      "language": "python",
      "description": "",
      "code": "# tests/test_nl2sql.py\n\"\"\"NL2SQL 精度测试\"\"\"\n\nimport pytest\n\n\nclass MockSchemaLoader:\n    SCHEMA = \"\"\"\n表 `orders` (订单表):\n  - id: int [PK]\n  - user_id: int [FK]\n  - amount: decimal\n  - status: varchar\n  - order_date: datetime\n表 `users` (用户表):\n  - id: int [PK]\n  - username: varchar\n  - city: varchar\n  - vip_level: int\"\"\"\n\n    async def load_schema(self):\n        return self.SCHEMA\n\n\n@pytest.mark.asyncio\nasync def test_nl2sql_basic():\n    \"\"\"测试 NL2SQL 基本生成能力\"\"\"\n    from app.agents.nl2sql_agent import NL2SQLAgent\n    agent = NL2SQLAgent(MockSchemaLoader())\n    # 需要配置 LLM API_KEY 才能实际运行\n    # result = await agent.generate_sql(\"各城市的用户数量\")\n    # assert \"users\" in result.sql.lower()\n    # assert \"city\" in result.sql.lower()\n    pass\n\n\n@pytest.mark.asyncio\nasync def test_sql_sanitize():\n    \"\"\"测试 SQL 安全过滤\"\"\"\n    from app.agents.nl2sql_agent import NL2SQLAgent\n    agent = NL2SQLAgent(MockSchemaLoader())\n    # 测试危险 SQL 被拦截\n    try:\n        agent._sanitize_sql(\"DROP TABLE users\")\n        assert False, \"应该抛出异常\"\n    except ValueError as e:\n        assert \"不允许\" in str(e)",
      "section_ref": "28.4.2",
      "runnable": true,
      "dependencies": [
        "pytest"
      ]
    },
    {
      "id": "code-15",
      "language": "dockerfile",
      "description": "",
      "code": "FROM python:3.11-slim\nWORKDIR /app\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    gcc default-libmysqlclient-dev && rm -rf /var/lib/apt/lists/*\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY app/ ./app/\nEXPOSE 8000\nCMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]",
      "section_ref": "28.5.1",
      "runnable": false,
      "dependencies": []
    },
    {
      "id": "code-16",
      "language": "yaml",
      "description": "CMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]",
      "code": "# docker-compose.yml\nversion: '3.8'\nservices:\n  data-platform:\n    build: .\n    ports: [\"8000:8000\"]\n    environment:\n      - DP_LLM_API_KEY=${LLM_API_KEY}\n      - DP_DB_HOST=mysql\n      - DP_DB_USER=data_analyst\n      - DP_DB_PASSWORD=readonly_pass\n      - DP_DB_NAME=ecommerce\n    depends_on: [mysql]\n  mysql:\n    image: mysql:8\n    environment:\n      MYSQL_ROOT_PASSWORD: root123\n      MYSQL_DATABASE: ecommerce\n      MYSQL_USER: data_analyst\n      MYSQL_PASSWORD: readonly_pass\n    ports: [\"3306:3306\"]\n    volumes:\n      - ./demo_data/init_db.sql:/docker-entrypoint-initdb.d/init.sql",
      "section_ref": "28.5.1",
      "runnable": false,
      "dependencies": []
    }
  ],
  "tables": [
    {
      "headers": [
        "维度",
        "指标"
      ],
      "data": [
        [
          "SQL 生成准确率",
          "> 90%（标准查询）"
        ],
        [
          "查询响应时间",
          "P95 < 5 秒（百万级数据）"
        ],
        [
          "并发支持",
          "100 QPS"
        ],
        [
          "支持数据库",
          "MySQL、PostgreSQL、ClickHouse"
        ],
        [
          "安全性",
          "禁止 DELETE/DROP/TRUNCATE，只读模式"
        ]
      ]
    },
    {
      "headers": [
        "模式",
        "应用场景",
        "效果"
      ],
      "data": [
        [
          "Schema-as-Prompt",
          "NL2SQL",
          "准确率提升 30%"
        ],
        [
          "自动修正循环",
          "SQL 执行容错",
          "失败恢复率 85%"
        ],
        [
          "规则+特征混合",
          "可视化推荐",
          "推荐准确率 92%"
        ],
        [
          "Pipeline 串联",
          "完整分析流程",
          "一次调用完成全流程"
        ]
      ]
    }
  ],
  "key_takeaways": [],
  "common_pitfalls": [],
  "related_chapters": [
    "ch17"
  ]
}