← 返回上一页

第7章: 「数析」智能数据分析台

基于 LangChain Agent + 通义千问实现自然语言数据分析与可视化平台

📋 项目概述

本章带你完成 阶段 3:「数析」智能数据分析台 项目: 基于 LangChain Agent + 通义千问 构建一个具备Agent 工具链设计、自然语言转数据分析、工具治理、人机协同审核的智能数据分析平台。

✅ 完成本项目你将掌握以下7大核心能力

  • 1、Agent 架构与工具链设计:基于 LangChain Agent 框架设计数据分析助手架构,掌握 ReAct(推理+行动)核心循环;定义数据分析工具集,实现 AI 自主决策调用工具。
  • 2、数据分析能力集成:使用 Pandas 进行数据清洗、统计分析、可视化;掌握让 AI 生成 Python 代码执行分析任务的安全沙箱机制。
  • 3、自然语言转数据分析:实现用户用自然语言描述分析需求,AI 自动拆解为数据提取 → 分析计算 → 可视化呈现 → 结论生成的完整工作流。
  • 4、工具治理与安全边界:建立工具注册中心、权限控制、审计日志、限流熔断的治理体系;解决生产环境中的工具安全风险。
  • 5、人机协同审核机制:设置复杂分析任务的人工确认机制,自动同步当前分析上下文至人工审核;避免重复描述,提升协同效率。
  • 6、数据复盘与质量优化:自动采集分析日志,分析高频错误类型、用户满意度低的分析场景;输出优化建议,实现持续迭代。
  • 7、可视化与报告生成:基于 Matplotlib/Plotly 生成多样化图表(折线图/柱状图/饼图/热力图);支持一键导出数据分析报告(PDF/HTML)。

🛠️ 技术栈

  • 前端框架:Streamlit
  • 大模型:通义千问 (Tongyi Qwen)
  • 数据处理:Pandas
  • AI 框架:LangChain

🧩 7 大核心能力详解(内嵌 SVG + 实战案例 + 最小代码)

本章的难点不在“能跑一个 Pandas 脚本”,而在于把自然语言 → 可控的分析流程做成一个可交付的产品: 能工具化、可审计、可回放、可复盘。

统一业务场景:运营数据分析(CSV)
典型提问:“近 30 天 GMV 趋势如何?”“哪个渠道转化率最低?”“是否存在异常高退款的商品?” 目标是:模型不仅给结论,还要给分析步骤 + 关键中间结果 + 图表,并且每一步可追踪。
核心1)Agent 架构与工具链:让 AI "会做事"而不是"只会说"
目标:讲清 Agent 的 ReAct 循环(Thought→Action→Observation→Answer),把“读取数据/统计/画图/导出报告”封装成可控工具。
为什么重要:数据分析是连续决策:先看字段→再做分组→发现异常→再验证。 Agent 的价值是把流程变成可追踪的工具调用链,而不是一次性瞎答。
ReAct (Reasoning + Acting) 完整循环 用户问题 Thought: 分析 Action: 工具调用 Observation: 结果 Thought: 判断 Final Answer 需要更多信息时循环 🔧 工具箱 (Tool Registry) • inspect_schema: 查看数据结构 • compute_metrics: 计算指标 • plot_trend: 绘制趋势图 • export_report: 导出报告 🧠 决策逻辑 • 信息足够?→ Final Answer • 需要更多信息?→ 继续工具调用 • 工具选择基于上下文 • 每步都有推理记录 📊 执行轨迹 (Trace) • 完整的推理链记录 • 工具调用参数与结果 • 可复现、可审计 • 错误处理与回滚
可复现实例:问"哪个渠道转化率最低?给出近 30 天趋势图"。Agent 先 inspect_schema → compute_metrics(cvr) → plot_trend → answer。
📝 最小可运行代码:Agent 工具注册与执行
🔄 Agent 工具执行流程(含类名/方法名)
1. 用户提问
executor.invoke()
"哪个渠道转化率最低?"
2. Agent 推理
ReAct Agent
create_react_agent()
3. 工具执行
@tool 装饰器
inspect_schema()
4. 数据分析
Pandas 处理
compute_metrics()
5. 可视化
Matplotlib
plot_trend()
6. 最终答案
LLM 输出
"渠道C转化率最低"
🔗 关键组件关系:
AgentExecutor → 执行引擎
create_react_agent() → 创建 ReAct 智能体
@tool → 工具装饰器
Pandas → 数据处理
Matplotlib → 图表生成
python
# ============================
# 目的:用“工具化”的方式把 Pandas 能力做成可控产品,而不是让 LLM 直接写/执行任意脚本
#
# 你将看到:
# - 用 @tool 把“看表结构 / 统计指标 / 画图”等能力封装成受控工具
# - ReAct Agent 通过 Thought/Action/Observation 循环选择调用这些工具
# - verbose=True 时可以观察每一步工具调用轨迹
#
# 注意:本段是“最小可运行示例”,为了教学清晰做了简化:
# - 数据用内存 DataFrame 模拟
# - 绘图输出保存到本地文件
# ============================

import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
from langchain.tools import tool
from langchain_classic.agents import AgentExecutor, create_react_agent
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate

# 设置中文字体支持
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

# 示例数据:模拟 30 天、3 个渠道的 GMV 和订单数
# 后续工具都会基于这个 df 做分析
df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=30),
    'channel': ['A', 'B', 'C'] * 10,
    'gmv': [1000, 1200, 800, 1500, 900, 1100] * 5,
    'order_count': [50, 60, 40, 75, 45, 55] * 5
})

@tool
def inspect_schema(df_name: str) -> str:
    """检查 DataFrame 的列名和数据类型
    
    Args:
        df_name: 数据框名称(当前只支持示例数据)
    """
    # 返回 schema 信息(列名 / 维度 / dtype),帮助模型做后续决策
    return f"columns: {list(df.columns)}, shape: {df.shape}, dtypes: {df.dtypes.to_dict()}"

@tool
def compute_metrics(input_json: str) -> str:
    """按指定字段分组计算指标
    
    Args:
        input_json: JSON字符串,格式为 {"by": "字段名", "metric": "字段名", "top_n": 数字}
    """
    import json
    
    try:
        params = json.loads(input_json) if isinstance(input_json, str) else input_json
        by = params.get('by')
        metric = params.get('metric')
        top_n = params.get('top_n', 5)
    except (json.JSONDecodeError, AttributeError) as e:
        return f"参数解析错误: {e}"
    
    # 基础校验:避免模型给空字段
    if not by or not metric:
        return "错误:必须提供 by 和 metric 参数"
    
    # 字段校验:避免越权访问不存在字段
    if by not in df.columns or metric not in df.columns:
        return f"错误:字段 {by}{metric} 不存在"
    
    result = df.groupby(by)[metric].sum().sort_values(ascending=False)
    return f"Top {top_n} {metric} by {by}:\n{result.head(top_n).to_string()}"

@tool
def compute_conversion_rate(input_json: str) -> str:
    """计算转化率
    
    Args:
        input_json: JSON字符串,格式为 {"by": "分组字段名"}
    """
    import json
    
    try:
        params = json.loads(input_json) if isinstance(input_json, str) else input_json
        by = params.get('by')
    except (json.JSONDecodeError, AttributeError) as e:
        return f"参数解析错误: {e}"
    
    if not by:
        return "错误:必须提供 by 参数"
    
    if by not in df.columns:
        return f"错误:字段 {by} 不存在"
    
    # 计算转化率 = 订单数 / GMV (简化示例)
    result = df.groupby(by).apply(lambda x: (x['order_count'].sum() / x['gmv'].sum()) * 100).round(2)
    return f"各{by}转化率(%):\n{result.to_string()}"

@tool
def plot_trend(input_json: str) -> str:
    """绘制趋势图并保存
    
    Args:
        input_json: JSON字符串,格式为 {"metric": "字段名", "by": "分组字段名(可选)"}
    """
    import json
    
    try:
        params = json.loads(input_json) if isinstance(input_json, str) else input_json
        metric = params.get('metric')
        by = params.get('by')
    except (json.JSONDecodeError, AttributeError) as e:
        return f"参数解析错误: {e}"
    
    if not metric:
        return "错误:必须提供 metric 参数"
    
    if metric not in df.columns:
        return f"错误:字段 {metric} 不存在"
    
    try:
        plt.figure(figsize=(10, 6))
        if by:
            if by not in df.columns:
                return f"错误:分组字段 {by} 不存在"
            for group in df[by].unique():
                data = df[df[by] == group]
                plt.plot(data['date'], data[metric], label=f'{by}={group}')
        else:
            plt.plot(df['date'], df[metric])
        
        plt.title(f'{metric} 趋势图')
        plt.xlabel('日期')
        plt.ylabel(metric)
        plt.legend()
        plt.grid(True)
        
        # 保存图片,忽略字体警告
        import warnings
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", UserWarning)  # 忽略字体警告
            plt.savefig('trend_plot.png', bbox_inches='tight')
        plt.close()
        return "趋势图已保存为 trend_plot.png"
    except Exception as e:
        return f"绘图错误: {e}"

# 初始化 LLM 和 Agent
llm = ChatTongyi(model="qwen-turbo", temperature=0)

# ReAct 提示词模板
prompt = ChatPromptTemplate.from_template("""你是一个数据分析助手。使用以下工具来回答用户的问题。

可用工具:
{tools}

工具名称:
{tool_names}

使用以下格式:
Question: 用户的问题
Thought: 我需要思考如何回答这个问题
Action: 选择一个工具
Action Input: 工具的输入参数(必须是JSON格式)
Observation: 工具的输出结果
... (可以重复 Thought/Action/Action Input/Observation 多次)
Thought: 我现在知道最终答案了
Final Answer: 基于观察结果的最终答案(不要在Final Answer后再执行任何Action)

重要提示:
- inspect_schema 工具:查看数据结构,参数:{{"df_name": "数据框名称"}}
- compute_metrics 工具:计算分组指标,参数:{{"by": "字段名", "metric": "字段名", "top_n": 数字}}
- compute_conversion_rate 工具:计算转化率,参数:{{"by": "分组字段名"}}
- plot_trend 工具:绘制趋势图,参数:{{"metric": "字段名", "by": "分组字段名(可选)"}}
- 一旦给出Final Answer,就不能再执行任何Action
- 确保在Final Answer中完成所有需要的分析

开始!

Question: {input}
Thought: {agent_scratchpad}""")

# 创建 Agent
tools = [inspect_schema, compute_metrics, compute_conversion_rate, plot_trend]
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent, 
    tools=tools, 
    verbose=True, 
    max_iterations=5,
    handle_parsing_errors=True,  # 处理解析错误,让 Agent 重试
    return_intermediate_steps=True  # 返回中间轨迹(intermediate_steps)用于打印
)

# 执行示例
if __name__ == "__main__":
    # 测试多个工具调用
    questions = [
        "哪个渠道的GMV最高?请画出各渠道GMV的趋势图",
        "请分析各渠道的订单数量情况,并计算订单转化率",
        "查看数据结构,然后给出整体数据分析报告"
    ]
    
    for i, question in enumerate(questions, 1):
        print(f"\n{'='*60}")
        print(f"问题 {i}: {question}")
        print('='*60)
        
        result = executor.invoke({"input": question})
        output = result.get("output", None) if isinstance(result, dict) else None
        print(f"\n最终回答: {output}")
        
        # 显示工具调用统计
        steps = result.get("intermediate_steps", None) if isinstance(result, dict) else None
        if steps:
            print(f"\n工具调用轨迹:")
            for step_num, (action, observation) in enumerate(steps, 1):
                tool_name = getattr(action, "tool", None)
                tool_input = getattr(action, "tool_input", None)
                print(f"  步骤 {step_num}: 调用工具 '{tool_name}'")
                print(f"    输入参数: {tool_input}")
                obs_str = str(observation)
                print(f"    输出结果: {obs_str[:100]}..." if len(obs_str) > 100 else f"    输出结果: {obs_str}")
                print()
        else:
            print("\n工具调用轨迹: 未返回 intermediate_steps(请确认 return_intermediate_steps=True,或通过 verbose 日志查看轨迹)")
        
        print(f"{'='*60}\n")
✅ 验证方法
运行 executor,verbose 输出应包含至少 3 步工具调用轨迹(inspect→compute→plot),每步有 Action/Observation 记录,同一 query 多次运行结果一致。
💬 面试加分
"我们用 ReAct 循环而非单次调用,Agent 自主决定先看 schema 再选指标再画图。和 Code Interpreter 不同的是,我们只允许调用预定义工具,不执行任意代码,每步记录在 trace 中,保证可复现、可审计。"
安全2)数据分析能力集成:Pandas 能力“产品化”而不是“写脚本”
目标:把清洗、聚合、TopN、异常检测等能力封装成函数/工具,让模型选择调用,不直接执行任意 Python。
为什么重要:LLM 直接写代码不可控(越权/死循环/读写本地文件)。工具化 + 参数白名单 + 运行时隔离才能上线。
关键澄清:“只做工具化/白名单”并不等于安全。因为工具本身依然是可执行代码:如果工具实现里出现 while True、N^2 聚合、递归爆栈、巨量 DataFrame join、或误把全量数据 dump 到日志,仍然会导致 CPU 跑满/内存爆掉/磁盘写满/服务阻塞。也就是说,工具化解决的是“模型不能随意生成新代码”的问题,但没有解决工具运行时的 DoS 风险
上线标准(控制面)要补齐:
1)超时与中断:每个工具必须有 timeout(如 3-5s),超时要可强制中断,防死循环/慢查询。
2)隔离执行:重要工具建议下沉到独立进程/容器(甚至 sandbox),避免把主服务拖死;并限制网络/文件系统能力。
3)最小权限:工具只能访问业务允许的数据源/目录,不能随意读写本地文件、访问内网。
4)资源配额:限制内存、CPU、结果集大小、日志大小、top_n 上限,防止“合法参数”也把机器打爆。
5)可观测与审计:记录工具名、入参、耗时、行数、异常堆栈摘要(脱敏),才能追查与复盘。
6)幂等与熔断:工具要尽量无副作用;连续失败/超时要熔断降级,避免雪崩。
场景:“按渠道统计 GMV,并找出 Top3 渠道”,工具链应输出可复用表格数据。
text
// ============================
// 目的:给 LLM 一个“工具调用协议”的例子(结构化动作),让它输出可被系统解析的参数
//
// 含义:
// - tool:要调用的工具名(注册中心里必须存在)
// - args:该工具的入参(必须是白名单字段 + 合法范围)
//
// 注意:这里展示的是“结构化指令”,不是让模型写 Pandas 脚本
// ============================
{"tool":"groupby_sum","args":{"by":"channel","metric":"gmv","top_n":3}}
工具化 vs 任意代码执行:安全边界对比 ❌ 危险:LLM 直接写代码 exec(llm_output) # 可能包含: import os; os.remove("/") while True: pass # 死循环 open("/etc/passwd").read() ✓ 安全:工具化 + 运行时隔离 tool_registry["groupby_sum"]( by="channel", # 白名单字段 metric="gmv", # 预定义指标 timeout=5s # 超时控制
📝 生产级实现:create_agent + 中间件 + 审批 + 监控
🔄 生产级 Agent 调用链路
用户请求
production_agent_invoke()
"按渠道统计GMV"
限流检查
rate_limiter.is_allowed()
10次/60秒
高危检测
is_high_risk_operation()
关键词检测
Agent 执行
data_analysis_agent.invoke()
create_agent()
工具调用
safe_groupby()
@tool 装饰
重试逻辑
for attempt in range(3)
指数退避
参数验证
ALLOWED_COLUMNS
白名单检查
数据聚合
df.groupby().sum()
Pandas 处理
监控记录
logger.info()
执行时间+状态
结果返回
JSON 格式
status+result
🔗 关键调用链路:
production_agent_invoke() → 入口函数,统一处理
rate_limiter.is_allowed() → 限流保护
is_high_risk_operation() → 高危检测
create_agent() → 创建智能体
@tool safe_groupby() → 工具执行
logger.info() → 监控日志
python
# ============================
# 目的:生产级“数据分析 Agent”骨架(可上线思路示例)
#
# 这一段强调“产品化”必备能力:
# - 用户级限流(防滥用)
# - 高危操作检测 + Human-in-the-Loop
# - 审计日志(trace_id、risk_level、耗时、状态)
# - 工具白名单 + 参数校验 + top_n/导出行数上限
# - 可观测性:日志、耗时、成功率
#
# 注意:这是教学示例,重点是架构与治理点,部分细节需按你实际框架版本做适配。
# ============================

import os
import json
import time
import logging
import uuid
import hashlib
from typing import Dict, Any, List, Optional
from datetime import datetime
from dataclasses import dataclass
from enum import Enum

from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware.model_call_limit import ModelCallLimitMiddleware
from langchain.agents.middleware.tool_call_limit import ToolCallLimitMiddleware
from langchain.agents.middleware.model_retry import ModelRetryMiddleware
from langchain.agents.middleware.tool_retry import ToolRetryMiddleware
from langchain_community.chat_models import ChatTongyi
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import HumanMessage, AIMessage

# === 数据处理库 ===
import pandas as pd
import numpy as np

# === 生产环境推荐组合 ===
# 1️⃣ 安全:system_prompt + middleware(限流)+ interrupt_before(高危审批)
# 2️⃣ 可靠:middleware(重试)+ name(监控标识)  
# 3️⃣ 可观测:name + debug=False + 外部日志系统 + 审计追踪

# 配置日志系统(生产建议:输出到集中式日志平台;这里用文件+控制台演示)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('/tmp/data_analysis_agent.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger("data_analysis_agent")

# 审计日志记录器
audit_logger = logging.getLogger("audit")

# === 数据模型 ===
@dataclass
class AuditLog:
    """审计日志数据模型"""
    trace_id: str
    user_id: str
    operation: str
    status: str
    timestamp: datetime
    tools_used: List[str]
    risk_level: str
    execution_time: float
    error_message: Optional[str] = None

class RiskLevel(Enum):
    """风险级别枚举"""
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class OperationStatus(Enum):
    """操作状态枚举"""
    PENDING = "pending"
    APPROVED = "approved"
    REJECTED = "rejected"
    CANCELLED = "cancelled"
    COMPLETED = "completed"
    FAILED = "failed"

# === 高级限流器(只保留用户维度) ===
# 说明:限流的第一层防线,按 user_id 统计调用频次,保护系统不被刷爆
class AdvancedRateLimiter:
    """高级限流器:当前示例中只做“用户级别”限流,便于和业务账号体系对齐"""
    def __init__(self):
        # 仅按 user_id 做滑动时间窗口统计
        self.user_limits: dict[str, list[float]] = {}
    
    def is_allowed(self, user_id: str) -> tuple[bool, str]:
        """检查是否允许请求,返回 (是否允许, 限制原因)

        这里的策略是:同一 user_id 在 60 秒窗口内最多 10 次调用。
        """
        now = time.time()
        key = f"user_{user_id}"
        window_seconds = 60
        max_requests = 10

        if key not in self.user_limits:
            self.user_limits[key] = []
        
        # 清理窗口外的旧请求
        self.user_limits[key] = [
            ts for ts in self.user_limits[key]
            if now - ts < window_seconds
        ]

        if len(self.user_limits[key]) >= max_requests:
            return False, f"用户限流:{user_id}{window_seconds} 秒内最多 {max_requests} 次调用"

        # 记录本次调用时间戳
        self.user_limits[key].append(now)
        return True, "允许"

rate_limiter = AdvancedRateLimiter()

# === 高级安全检测器 ===
class SecurityAnalyzer:
    """高级安全分析器"""
    def __init__(self):
        self.high_risk_keywords = [
            "删除", "drop", "truncate", "清空", "覆盖", "全量", "所有数据",
            "export", "导出", "下载", "敏感", "权限", "管理员", "root"
        ]
        self.sensitive_fields = {"salary", "phone", "email", "id_card", "bank_account"}
        self.destructive_tools = {"delete_data", "truncate_table", "drop_table"}
    
    def analyze_risk(self, input_text: str, tools_used: List[str], fields_accessed: List[str]) -> RiskLevel:
        """分析操作风险级别"""
        risk_score = 0
        
        # 关键词检测
        if any(keyword in input_text.lower() for keyword in self.high_risk_keywords):
            risk_score += 3
        
        # 敏感字段检测
        if any(field in self.sensitive_fields for field in fields_accessed):
            risk_score += 2
        
        # 破坏性工具检测
        if any(tool in self.destructive_tools for tool in tools_used):
            risk_score += 3
        
        # 大量数据导出检测
        if "export" in input_text.lower() or "导出" in input_text:
            risk_score += 2
        
        # 数据量检测
        if any(word in input_text.lower() for word in ["10000", "一万", "all", "全部"]):
            risk_score += 1
        
        if risk_score >= 5:
            return RiskLevel.HIGH
        elif risk_score >= 3:
            return RiskLevel.MEDIUM
        else:
            return RiskLevel.LOW
    
    def is_high_risk_operation(self, input_text: str) -> bool:
        """检测是否为高危操作"""
        risk = self.analyze_risk(input_text, [], [])
        return risk == RiskLevel.HIGH

security_analyzer = SecurityAnalyzer()

# === 审计追踪系统 ===
class AuditTrail:
    """审计追踪系统"""
    def __init__(self):
        self.logs = []
    
    def create_log(self, user_id: str, operation: str, risk_level: RiskLevel) -> AuditLog:
        """创建审计日志"""
        trace_id = str(uuid.uuid4())[:8]
        return AuditLog(
            trace_id=trace_id,
            user_id=user_id,
            operation=operation,
            status=OperationStatus.PENDING.value,
            timestamp=datetime.now(),
            tools_used=[],
            risk_level=risk_level.value,
            execution_time=0.0
        )
    
    def log_operation(self, audit_log: AuditLog):
        """记录操作日志"""
        log_data = {
            "trace_id": audit_log.trace_id,
            "user_id": audit_log.user_id,
            "operation": audit_log.operation,
            "status": audit_log.status,
            "timestamp": audit_log.timestamp.isoformat(),
            "tools_used": audit_log.tools_used,
            "risk_level": audit_log.risk_level,
            "execution_time": audit_log.execution_time,
            "error_message": audit_log.error_message
        }
        
        # 记录到审计日志
        audit_logger.info(json.dumps(log_data, ensure_ascii=False))
        
        # 记录到内存(实际生产环境应该用数据库)
        self.logs.append(log_data)

audit_trail = AuditTrail()

# === 性能监控器 ===
class PerformanceMonitor:
    """性能监控器"""
    def __init__(self):
        self.metrics = {}
    
    def record_execution(self, tool_name: str, execution_time: float, success: bool):
        """记录执行指标"""
        if tool_name not in self.metrics:
            self.metrics[tool_name] = {
                "total_calls": 0,
                "success_calls": 0,
                "total_time": 0.0,
                "avg_time": 0.0,
                "success_rate": 0.0
            }
        
        metrics = self.metrics[tool_name]
        metrics["total_calls"] += 1
        metrics["total_time"] += execution_time
        metrics["avg_time"] = metrics["total_time"] / metrics["total_calls"]
        
        if success:
            metrics["success_calls"] += 1
        
        metrics["success_rate"] = metrics["success_calls"] / metrics["total_calls"]
        
        # 记录到日志
        logger.info(f"Performance: {tool_name} - avg_time: {metrics['avg_time']:.3f}s, success_rate: {metrics['success_rate']:.2%}")

performance_monitor = PerformanceMonitor()


# === 示例数据 ===
import pandas as pd
import numpy as np

# 生成更丰富的示例数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=100)
channels = ['A', 'B', 'C', 'D', 'E']
regions = ['北京', '上海', '广州', '深圳', '杭州']

df = pd.DataFrame({
    'date': np.random.choice(dates, 1000),
    'channel': np.random.choice(channels, 1000),
    'region': np.random.choice(regions, 1000),
    'gmv': np.random.normal(1000, 300, 1000).astype(int),
    'order_count': np.random.poisson(50, 1000),
    'user_count': np.random.poisson(100, 1000),
    'product_category': np.random.choice(['电子产品', '服装', '食品', '家居'], 1000)
})

# 确保数据合理性
df['gmv'] = df['gmv'].clip(lower=100)
df['order_count'] = df['order_count'].clip(lower=1)
df['user_count'] = df['user_count'].clip(lower=1)

# === 数据分析工具(生产级封装) ===
# 说明:工具化的核心是“白名单 + 上限”,确保模型只能在安全边界内做分析
ALLOWED_COLUMNS = {"channel", "gmv", "order_count", "date", "region", "user_count", "product_category"}
ALLOWED_AGGREGATIONS = {"sum", "mean", "count", "max", "min", "std"}
MAX_TOP_N = 100
MAX_EXPORT_ROWS = 10000

@tool
def safe_groupby(input_json: str) -> str:
    """安全的分组聚合(生产级实现)
    
    Args:
        input_json: JSON格式,{"by": ["字段名"], "metric": "字段名", "agg": "聚合方式", "top_n": 数字}
    """
    # 记录耗时,用于性能追踪
    start_time = time.time()
    tool_name = "safe_groupby"
    
    try:
        # 解析参数(来自 LLM 的结构化 JSON)
        params = json.loads(input_json)
        group_by = params.get("by", [])
        metric = params.get("metric")
        agg_func = params.get("agg", "sum")
        top_n = min(params.get("top_n", 5), MAX_TOP_N)
        
        # 参数验证(字段白名单 + 聚合方式白名单 + top_n 上限)
        if not isinstance(group_by, list):
            group_by = [group_by]
        
        if not all(col in ALLOWED_COLUMNS for col in group_by + [metric]):
            return f"错误:只允许字段 {ALLOWED_COLUMNS}"
        
        if agg_func not in ALLOWED_AGGREGATIONS:
            return f"错误:只允许聚合方式 {ALLOWED_AGGREGATIONS}"
        
        # 执行聚合(只允许预定义操作,避免复杂/危险表达式)
        if agg_func == "sum":
            result = df.groupby(group_by)[metric].sum().nlargest(top_n)
        elif agg_func == "mean":
            result = df.groupby(group_by)[metric].mean().nlargest(top_n)
        elif agg_func == "count":
            result = df.groupby(group_by)[metric].count().nlargest(top_n)
        elif agg_func == "max":
            result = df.groupby(group_by)[metric].max().nlargest(top_n)
        elif agg_func == "min":
            result = df.groupby(group_by)[metric].min().nlargest(top_n)
        elif agg_func == "std":
            result = df.groupby(group_by)[metric].std().nlargest(top_n)
        
        result_str = result.to_string()
        
        # 记录性能指标
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, True)
        
        # 监控记录
        logger.info(json.dumps({
            "timestamp": datetime.now().isoformat(),
            "tool_name": tool_name,
            "execution_time": round(execution_time, 3),
            "status": "success",
            "params": input_json[:100] + "..." if len(input_json) > 100 else input_json,
            "result_rows": len(result)
        }))
        
        return result_str
        
    except Exception as e:
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, False)
        
        logger.error(json.dumps({
            "timestamp": datetime.now().isoformat(),
            "tool_name": tool_name,
            "execution_time": round(execution_time, 3),
            "status": "error",
            "error": str(e),
            "params": input_json[:100] + "..." if len(input_json) > 100 else input_json
        }))
        
        return f"执行错误:{str(e)}"

@tool
def safe_filter(input_json: str) -> str:
    """安全的数据过滤
    
    Args:
        input_json: JSON格式,{"conditions": [{"field": "字段名", "operator": "操作符", "value": "值"}], "limit": 数字}
    """
    start_time = time.time()
    tool_name = "safe_filter"
    
    try:
        params = json.loads(input_json)
        conditions = params.get("conditions", [])
        limit = min(params.get("limit", 100), MAX_EXPORT_ROWS)
        
        # 参数验证
        for condition in conditions:
            field = condition.get("field")
            if field not in ALLOWED_COLUMNS:
                return f"错误:不允许字段 {field}"
        
        # 构建过滤条件
        filtered_df = df.copy()
        for condition in conditions:
            field = condition["field"]
            operator = condition["operator"]
            value = condition["value"]
            
            if operator == "eq":
                filtered_df = filtered_df[filtered_df[field] == value]
            elif operator == "gt":
                filtered_df = filtered_df[filtered_df[field] > value]
            elif operator == "lt":
                filtered_df = filtered_df[filtered_df[field] < value]
            elif operator == "in":
                if isinstance(value, list):
                    filtered_df = filtered_df[filtered_df[field].isin(value)]
                else:
                    filtered_df = filtered_df[filtered_df[field] == value]
        
        result = filtered_df.head(limit).to_string()
        
        # 记录性能指标
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, True)
        
        return result
        
    except Exception as e:
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, False)
        return f"过滤错误:{str(e)}"

@tool
def detect_anomalies(input_json: str) -> str:
    """异常检测
    
    Args:
        input_json: JSON格式,{"metric": "指标名", "method": "检测方法", "threshold": 阈值}
    """
    start_time = time.time()
    tool_name = "detect_anomalies"
    
    try:
        params = json.loads(input_json)
        metric = params.get("metric")
        method = params.get("method", "zscore")
        threshold = params.get("threshold", 2.0)
        
        if metric not in ALLOWED_COLUMNS:
            return f"错误:不允许字段 {metric}"
        
        # 简单的异常检测
        if method == "zscore":
            z_scores = np.abs((df[metric] - df[metric].mean()) / df[metric].std())
            anomalies = df[z_scores > threshold]
        
        result = f"检测到 {len(anomalies)} 个异常点\n"
        result += f"异常值范围:{anomalies[metric].min():.2f} - {anomalies[metric].max():.2f}\n"
        result += "异常样本:\n" + anomalies.head(5).to_string()
        
        # 记录性能指标
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, True)
        
        return result
        
    except Exception as e:
        execution_time = time.time() - start_time
        performance_monitor.record_execution(tool_name, execution_time, False)
        return f"异常检测错误:{str(e)}"

# === 创建生产级 Agent ===
# 说明:create_agent 负责把 model + tools + system_prompt 组装为一个可调用的 Agent
api_key = os.getenv("DASHSCOPE_API_KEY")
llm = ChatTongyi(model_name="qwen-turbo", dashscope_api_key=api_key, temperature=0)

# 系统提示词(安全边界):把“不能做什么/只能做什么”写清楚,比事后兜底更重要
SYSTEM_PROMPT = """你是一个智能数据分析助手。请遵循以下规则:

🔒 安全约束:
1. 只能使用提供的工具,不能执行任意代码
2. 敏感操作(删除、导出全量数据等)需要人工审批
3. 严格遵守数据隐私和权限边界

📊 分析规范:
1. 先理解业务问题,再选择合适的分析工具
2. 确保分析结果可解释、可复现
3. 重要结论要提供数据支撑

⚡ 性能要求:
1. 优先使用预聚合数据,避免全表扫描
2. 合理设置 top_n,防止返回过大数据集
3. 复杂分析要分步骤执行

🛠️ 可用工具:
- safe_groupby: 分组聚合分析
- safe_filter: 数据过滤查询
- detect_anomalies: 异常检测分析"""

# 创建 Agent(带监控标识)
# name:用于监控标识(例如打到 tracing/metrics 系统)
data_analysis_agent = create_agent(
    model=llm,
    tools=[safe_groupby, safe_filter, detect_anomalies],
    system_prompt=SYSTEM_PROMPT,
    name="data_analysis_agent",  # 监控标识
    debug=False,  # 生产环境关闭 debug
    # 中间件:示例展示按“单次运行”维度控制调用次数
    # 注意:ModelCallLimitMiddleware / ToolCallLimitMiddleware 需根据你实际使用的 LangChain 版本
    # 引入对应实现或自定义实现,这里只作为 API 用法示意。
    middleware=(
        # 限流保护:防止单次运行内滥用模型 / 工具
        ModelCallLimitMiddleware(run_limit=10),    # 每次运行最多 10 次模型调用
        ToolCallLimitMiddleware(run_limit=5),      # 每次运行最多 5 次工具调用
    ),
)

# === 高级生产级调用包装器 ===
class ProductionAgentWrapper:
    """生产级 Agent 包装器:完整的安全、监控、审计功能"""
    
    def __init__(self, agent, user_id: str = "default_user"):
        self.agent = agent
        self.user_id = user_id
    
    def invoke(self, user_input: str, require_approval: bool = True) -> Dict[str, Any]:
        """生产级 Agent 调用"""
        trace_id = str(uuid.uuid4())[:8]
        start_time = time.time()
        
        # 创建审计日志
        risk_level = security_analyzer.analyze_risk(user_input, [], [])
        audit_log = audit_trail.create_log(self.user_id, user_input, risk_level)
        
        try:
            # 1. 用户级限流检查(第一道保护)
            allowed, reason = rate_limiter.is_allowed(self.user_id)
            if not allowed:
                audit_log.status = OperationStatus.REJECTED.value
                audit_log.error_message = reason
                audit_trail.log_operation(audit_log)
                
                return {
                    "status": "rate_limited",
                    "message": reason,
                    "trace_id": trace_id,
                    "agent_name": "data_analysis_agent"
                }
            
            # 2. 高危操作检测(需要人工确认时先返回 pending)
            if require_approval and risk_level == RiskLevel.HIGH:
                audit_log.status = OperationStatus.PENDING.value
                audit_trail.log_operation(audit_log)
                
                return {
                    "status": "pending_approval",
                    "message": "检测到高危操作,需要人工审批",
                    "requires_approval": True,
                    "operation": user_input,
                    "trace_id": trace_id,
                    "risk_level": risk_level.value,
                    "agent_name": "data_analysis_agent"
                }
            
            # 3. 执行分析:将用户输入封装为 messages 交给 Agent
            config = RunnableConfig(
                callbacks=[],
                tags=["production", "data_analysis"],
                metadata={"trace_id": trace_id, "user_id": self.user_id}
            )
            
            result = self.agent.invoke(
                {"messages": [HumanMessage(content=user_input)]},
                config=config
            )
            
            # 4. 记录成功日志
            execution_time = time.time() - start_time
            audit_log.status = OperationStatus.COMPLETED.value
            audit_log.execution_time = execution_time
            audit_log.tools_used = ["data_analysis_agent"]
            audit_trail.log_operation(audit_log)
            
            # 5. 记录性能指标
            logger.info(json.dumps({
                "timestamp": datetime.now().isoformat(),
                "trace_id": trace_id,
                "user_id": self.user_id,
                "agent_name": "data_analysis_agent",
                "execution_time": round(execution_time, 3),
                "status": "success",
                "query": user_input[:100] + "..." if len(user_input) > 100 else user_input,
                "risk_level": risk_level.value
            }))
            
            return {
                "status": "success",
                "result": result["messages"][-1].content,
                "trace_id": trace_id,
                "agent_name": "data_analysis_agent",
                "execution_time": round(execution_time, 3),
                "risk_level": risk_level.value
            }
            
        except Exception as e:
            execution_time = time.time() - start_time
            
            # 记录错误日志
            audit_log.status = OperationStatus.FAILED.value
            audit_log.execution_time = execution_time
            audit_log.error_message = str(e)
            audit_trail.log_operation(audit_log)
            
            logger.error(json.dumps({
                "timestamp": datetime.now().isoformat(),
                "trace_id": trace_id,
                "user_id": self.user_id,
                "agent_name": "data_analysis_agent",
                "execution_time": round(execution_time, 3),
                "status": "error",
                "error": str(e),
                "query": user_input
            }))
            
            return {
                "status": "error",
                "error": str(e),
                "trace_id": trace_id,
                "agent_name": "data_analysis_agent"
            }
    
    def get_performance_metrics(self) -> Dict[str, Any]:
        """获取性能指标"""
        return {
            "performance_metrics": performance_monitor.metrics,
            "recent_logs": audit_trail.logs[-10:],  # 最近10条日志
            "system_status": "healthy"
        }

# 创建生产级 Agent 实例
production_agent = ProductionAgentWrapper(data_analysis_agent, "user_001")

# === 使用示例 ===
if __name__ == "__main__":
    print("=== 生产级数据分析系统演示 ===\n")
    
    # 示例1:正常分析请求
    print("1. 正常分析请求:")
    response1 = production_agent.invoke(
        '{"by": ["channel"], "metric": "gmv", "agg": "sum", "top_n": 3}'
    )
    print(f"状态: {response1['status']}")
    print(f"结果: {response1.get('result', '无结果')[:100]}...")
    print(f"执行时间: {response1.get('execution_time', 0)}秒")
    print(f"风险级别: {response1.get('risk_level', 'unknown')}")
    print()
    
    # 示例2:数据过滤请求
    print("2. 数据过滤请求:")
    response2 = production_agent.invoke(
        '{"conditions": [{"field": "channel", "operator": "in", "value": ["A", "B"]}], "limit": 5}'
    )
    print(f"状态: {response2['status']}")
    print(f"结果预览: {response2.get('result', '无结果')[:100]}...")
    print()
    
    # 示例3:异常检测请求
    print("3. 异常检测请求:")
    response3 = production_agent.invoke(
        '{"metric": "gmv", "method": "zscore", "threshold": 2.0}'
    )
    print(f"状态: {response3['status']}")
    print(f"结果预览: {response3.get('result', '无结果')[:100]}...")
    print()
    
    # 示例4:高危操作请求
    print("4. 高危操作请求:")
    response4 = production_agent.invoke("删除所有渠道数据")
    print(f"状态: {response4['status']}")
    print(f"消息: {response4.get('message', '无消息')}")
    print(f"需要审批: {response4.get('requires_approval', False)}")
    print()
    
    # 示例5:获取性能指标
    print("5. 性能指标:")
    metrics = production_agent.get_performance_metrics()
    print(f"工具调用统计: {len(metrics['performance_metrics'])} 个工具")
    for tool_name, tool_metrics in metrics['performance_metrics'].items():
        print(f"  {tool_name}: {tool_metrics['total_calls']}次调用, "
              f"成功率 {tool_metrics['success_rate']:.1%}, "
              f"平均耗时 {tool_metrics['avg_time']:.3f}秒")
    print()
    
    print("=== 演示完成 ===")
✅ 验证方法
1. 在工具中故意写死循环,验证超时控制生效;2. 传入非法字段名,验证白名单拦截;3. 传入超大 top_n,验证参数范围限制。
💬 面试加分
"我没有让 LLM 直接执行任意 Python,而是将 Pandas 能力封装为预定义工具(groupby_sum、detect_anomaly 等),每个工具有参数白名单。模型只能选工具+传参,不能写任意代码,从架构上杜绝注入攻击。"
规划3)自然语言转工作流:先结构化计划,再执行
目标:把一句话拆成计划:范围(时间/过滤)→ 指标 → 分组 → 图表 → 输出物。
为什么重要:不先计划,系统容易“口径不一致”(时间范围、分母不同),结论不可复现。
自然语言 → 结构化计划 → 工具执行 "近30天各渠道GMV趋势" 📋 结构化计划 range: last_30d metrics: [gmv] group_by: channel chart: line 🔧 按计划执行工具链 filter → groupby → plot 💡 为什么先计划再执行? 不先计划 → 口径不一致 → 同一问题两次回答不同 → 不可复现 → 无法上线
📝 最小可运行代码:真实大模型调用 + 先计划再执行效果对比
🔍 两种模式的差别(你要对比的核心点)
1)计划模式:先让模型输出 JSON 计划(intent/range/steps/deliver) → 系统做 校验(白名单/步数/类型) → 再按步骤调用工具。
2)直接模式:不产出计划,也不做工具白名单校验,通常就是 一次大模型调用直出结论(更快,但口径/可复现/可审计更弱)。
python
# ============================
# 目的:展示“先计划再执行”的最小闭环(Plan-and-Execute 思路)
#
# 你将看到:
# - 第一步让模型把自然语言问题“翻译”为结构化计划(JSON)
# - 第二步由系统按计划逐步调用工具(或函数)执行
# - 计划受 schema 约束,保证口径一致、可复现、可审计
#
# 注意:这段代码用通义千问(ChatTongyi)演示“真实 API 调用”,并保持与本章其它模块一致。
# 安全建议:
# - API Key 必须从环境变量读取,禁止写死在代码里
# - 计划必须做白名单约束(允许工具、允许字段、最大步数)
# ============================

import json
import time
import uuid
import os
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from langchain_community.chat_models import ChatTongyi
from langchain_core.messages import SystemMessage, HumanMessage

# 初始化大模型(与本章其它代码保持一致)
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
    raise ValueError("请先在环境变量中设置 DASHSCOPE_API_KEY")
llm = ChatTongyi(model_name="qwen-turbo", dashscope_api_key=api_key, temperature=0)

# 模拟业务数据
sample_data = {
    "sales_data": [
        {"date": "2024-01-01", "channel": "A", "gmv": 1200, "orders": 60},
        {"date": "2024-01-02", "channel": "B", "gmv": 800, "orders": 40},
        {"date": "2024-01-03", "channel": "C", "gmv": 1500, "orders": 75},
        # ... 更多数据
    ]
}

# 计划 Schema:约束模型输出,避免“随口编计划”
PLAN_SCHEMA = {
    "required": ["intent", "range", "steps", "deliver"],
    "step_required": ["tool", "args"],
    "allowed_tools": {"safe_filter", "safe_groupby", "detect_anomalies", "plot"},
    "allowed_ranges": {"last_7d", "last_30d", "this_month", "custom"},
    "max_steps": 6,
}

# 增强的计划生成提示词:让模型理解业务上下文,并输出“可执行计划”
PLAN_PROMPT = """你是专业的数据分析规划师。请将用户问题转换为结构化的分析计划JSON。

业务背景:
- 数据源:电商销售数据,包含字段:date(日期), channel(渠道), gmv(销售额), orders(订单数)
- 支持的分析:趋势分析、渠道对比、异常检测、转化率计算
- 输出要求:可执行的工具链步骤

请严格按照以下JSON格式输出,不要包含任何其他文字:
{
  "intent": "分析意图描述",
  "range": "时间范围(last_7d/last_30d/this_month/custom)",
  "steps": [
    {"tool": "工具名", "args": {"参数名": "参数值"}},
    ...
  ],
  "deliver": "交付物描述"
}

可用工具:
- safe_filter: 数据过滤 {"conditions": [{"field": "字段", "op": ">", "value": "数值"}], "limit": 数量}
- safe_groupby: 分组统计 {"by": ["字段"], "metric": "指标", "agg": "sum/avg/count", "top_n": 数量}
- detect_anomalies: 异常检测 {"metric": "指标", "method": "std/iqr"}
- plot: 图表生成 {"chart": "line/bar/pie", "x": "X轴字段", "y": "Y轴字段", "series": "分组字段"}

示例:
用户问题:"近30天各渠道GMV趋势分析"
输出:
{
  "intent": "分析近30天各渠道GMV趋势变化",
  "range": "last_30d",
  "steps": [
    {"tool": "safe_filter", "args": {"conditions": [{"field": "date", "op": ">=", "value": "2024-01-01"}], "limit": 1000}},
    {"tool": "safe_groupby", "args": {"by": ["channel", "date"], "metric": "gmv", "agg": "sum", "top_n": 50}},
    {"tool": "plot", "args": {"chart": "line", "x": "date", "y": "gmv", "series": "channel"}}
  ],
  "deliver": "渠道GMV趋势图+数据洞察"
}

现在请处理用户问题:"""

@dataclass
class TraceEvent:
    name: str
    ok: bool
    ms: int
    meta: Dict[str, Any]

class Trace:
    def __init__(self, query: str):
        self.trace_id = f"t-{uuid.uuid4().hex[:6]}"
        self.query = query
        self.start = time.time()
        self.events: List[TraceEvent] = []

    def log(self, name: str, ok: bool, meta: Optional[Dict[str, Any]] = None):
        meta = meta or {}
        self.events.append(
            TraceEvent(
                name=name,
                ok=ok,
                ms=round((time.time() - self.start) * 1000),
                meta=meta,
            )
        )

    def summary(self) -> Dict[str, Any]:
        return {
            "trace_id": self.trace_id,
            "query": self.query,
            "events": [e.__dict__ for e in self.events],
        }

def call_llm_for_planning(query: str) -> Dict[str, Any]:
    """使用真实大模型生成分析计划"""
    try:
        resp = llm.invoke(
            [
                SystemMessage(content="你是专业的数据分析规划师,只输出JSON格式的分析计划。"),
                HumanMessage(content=PLAN_PROMPT + query),
            ]
        )
        content = (resp.content or "").strip()
        
        # 清理可能的markdown标记
        if content.startswith("```json"):
            content = content[7:]
        if content.endswith("```"):
            content = content[:-3]
        
        return json.loads(content)
    except Exception as e:
        print(f"大模型调用失败: {e}")
        # 降级到示例计划
        return {
            "intent": "分析渠道GMV趋势",
            "range": "last_30d",
            "steps": [
                {"tool": "safe_filter", "args": {"conditions": [], "limit": 1000}},
                {"tool": "safe_groupby", "args": {"by": ["channel"], "metric": "gmv", "agg": "sum", "top_n": 5}},
                {"tool": "plot", "args": {"chart": "line", "x": "date", "y": "gmv", "series": "channel"}}
            ],
            "deliver": "图表+结论"
        }

def parse_query_to_plan(query: str, use_llm: bool = True) -> Dict[str, Any]:
    """自然语言 → 结构化分析计划"""
    if use_llm:
        return call_llm_for_planning(query)
    else:
        # 直接执行模式(无计划)
        return {"direct_execution": True, "query": query}

def validate_plan(plan: Dict[str, Any]) -> Tuple[bool, str]:
    """计划校验:必填字段、工具白名单、步数上限、字段类型"""
    if "direct_execution" in plan:
        return True, "直接执行模式"
    
    missing = [k for k in PLAN_SCHEMA["required"] if k not in plan]
    if missing:
        return False, f"计划缺少字段: {missing}"

    if plan.get("range") not in PLAN_SCHEMA["allowed_ranges"]:
        return False, f"range 不合法: {plan.get('range')}"

    steps = plan.get("steps")
    if not isinstance(steps, list) or len(steps) == 0:
        return False, "steps 必须为非空数组"
    if len(steps) > PLAN_SCHEMA["max_steps"]:
        return False, f"steps 过多: {len(steps)} > {PLAN_SCHEMA['max_steps']}"

    for i, s in enumerate(steps):
        if not isinstance(s, dict):
            return False, f"step[{i}] 必须为对象"
        for k in PLAN_SCHEMA["step_required"]:
            if k not in s:
                return False, f"step[{i}] 缺少字段: {k}"
        if s["tool"] not in PLAN_SCHEMA["allowed_tools"]:
            return False, f"step[{i}] tool 不允许: {s['tool']}"
        if not isinstance(s["args"], dict):
            return False, f"step[{i}] args 必须为对象"

    return True, "ok"

def assess_risk(plan: Dict[str, Any]) -> str:
    """风险评估"""
    if "direct_execution" in plan:
        return "high"  # 直接执行风险较高
    
    HIGH_RISK = {"export", "delete", "drop", "truncate", "salary", "id_card"}
    text = json.dumps(plan, ensure_ascii=False).lower()
    return "high" if any(k in text for k in HIGH_RISK) else "low"

def compile_plan(plan: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
    """计划 → 可执行步骤"""
    if "direct_execution" in plan:
        return [("direct_execute", {"query": plan["query"]})]
    return [(s["tool"], s["args"]) for s in plan["steps"]]

def execute_plan(steps: List[Tuple[str, Dict[str, Any]]], tools: Dict[str, Any], trace: Trace) -> Dict[str, Any]:
    """按步骤执行:每步记录 trace"""
    ctx: Dict[str, Any] = {}
    for tool_name, args in steps:
        t0 = time.time()
        try:
            fn = tools[tool_name]
            out = fn(args, ctx)
            ctx[tool_name] = out
            trace.log(tool_name, True, {"ms_step": round((time.time() - t0) * 1000), "result_count": len(str(out))})
        except Exception as e:
            trace.log(tool_name, False, {"error": str(e)})
            raise
    return ctx

# 工具实现
def safe_filter_tool(args: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]:
    """安全的数据过滤工具"""
    print(f"🔍 执行数据过滤: {args}")
    time.sleep(0.1)  # 模拟处理时间
    return {"filtered_rows": 856, "data_preview": "date,channel,gmv,orders"}

def safe_groupby_tool(args: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]:
    """安全的分组统计工具"""
    print(f"📊 执行分组统计: {args}")
    time.sleep(0.2)  # 模拟计算时间
    return {
        "groupby_result": [
            {"channel": "A", "gmv": 15600, "orders": 780},
            {"channel": "B", "gmv": 12300, "orders": 615},
            {"channel": "C", "gmv": 18900, "orders": 945}
        ]
    }

def detect_anomalies_tool(args: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]:
    """异常检测工具"""
    print(f"🚨 执行异常检测: {args}")
    time.sleep(0.15)  # 模拟检测时间
    return {"anomalies": 2, "anomaly_points": ["2024-01-15", "2024-01-23"]}

def plot_tool(args: Dict[str, Any], ctx: Dict[str, Any]) -> str:
    """图表生成工具"""
    print(f"📈 生成图表: {args}")
    time.sleep(0.3)  # 模拟绘图时间
    return "chart_saved_to=/output/channel_gmv_trend.png"

def call_llm_direct_answer(query: str) -> str:
    """直接执行模式:一次大模型调用直出答案(更贴近真实世界的“无计划”形态)"""
    resp = llm.invoke(
        [
            SystemMessage(content=(
                "你是数据分析助手。现在是‘直接回答’模式:"
                "不要输出JSON计划,不要输出工具步骤,只输出最终结论与关键依据。"
                "如果缺少数据,请明确说明假设与不确定性。"
            )),
            HumanMessage(content=query),
        ]
    )
    return (resp.content or "").strip()

def direct_execute_tool(args: Dict[str, Any], ctx: Dict[str, Any]) -> Dict[str, Any]:
    """直接执行工具(无计划模式)"""
    query = args["query"]
    print(f"⚡ 直接执行(一次 LLM 调用直出): {query}")
    answer = call_llm_direct_answer(query)
    return {"answer": answer}

def demonstrate_planning_vs_direct():
    """演示:先计划再执行 vs 直接执行的效果对比"""
    
    test_queries = [
        "近30天各渠道GMV趋势分析,找出异常点",
        "对比不同渠道的订单转化率,给出优化建议",
        "分析上周销售数据,识别表现异常的产品"
    ]
    
    tools = {
        "safe_filter": safe_filter_tool,
        "safe_groupby": safe_groupby_tool,
        "detect_anomalies": detect_anomalies_tool,
        "plot": plot_tool,
        "direct_execute": direct_execute_tool,
    }
    
    print("=" * 80)
    print("🎯 先结构化计划,再执行 - vs - 直接执行模式对比")
    print("=" * 80)
    
    for i, query in enumerate(test_queries, 1):
        print(f"\n📋 测试问题 {i}: {query}")
        print("-" * 60)
        
        # 模式1:先计划再执行
        print("\n🔧 模式1: 先结构化计划,再执行")
        trace1 = Trace(query)
        
        try:
            plan = parse_query_to_plan(query, use_llm=True)
            print(f"📝 生成的计划:")
            print(json.dumps(plan, ensure_ascii=False, indent=2))
            
            ok, reason = validate_plan(plan)
            trace1.log("validate_plan", ok, {"reason": reason})
            print(f"✅ 计划验证: {'通过' if ok else '失败'} - {reason}")
            
            if ok:
                risk = assess_risk(plan)
                trace1.log("assess_risk", True, {"risk": risk})
                print(f"⚠️ 风险评估: {risk}")
                
                steps = compile_plan(plan)
                print(f"🎯 执行步骤: {len(steps)} 步")
                
                ctx = execute_plan(steps, tools, trace1)
                print(f"🎉 执行完成,结果: {len(ctx)} 个工具输出")
            else:
                print("❌ 计划验证失败,跳过执行")
                
        except Exception as e:
            print(f"❌ 计划模式执行失败: {e}")
        
        print(f"\n📊 计划模式执行轨迹:")
        for event in trace1.events:
            status = "✅" if event.ok else "❌"
            print(f"  {status} {event.name} ({event.ms}ms) - {event.meta}")
        
        # 模式2:直接执行
        print("\n⚡ 模式2: 直接执行(无计划)")
        trace2 = Trace(query)
        
        try:
            plan2 = parse_query_to_plan(query, use_llm=False)
            steps2 = compile_plan(plan2)
            ctx2 = execute_plan(steps2, tools, trace2)
            print(f"🎉 直接执行完成")
            if isinstance(ctx2, dict) and "direct_execute" in ctx2 and isinstance(ctx2["direct_execute"], dict):
                ans = ctx2["direct_execute"].get("answer", "")
                if ans:
                    print("\n🧾 直接模式答案预览:")
                    print(ans[:400] + ("..." if len(ans) > 400 else ""))
            
        except Exception as e:
            print(f"❌ 直接执行失败: {e}")
        
        print(f"\n📊 直接执行轨迹:")
        for event in trace2.events:
            status = "✅" if event.ok else "❌"
            print(f"  {status} {event.name} ({event.ms}ms) - {event.meta}")
        
        # 对比总结
        print(f"\n🔄 效果对比:")
        plan_steps = len([e for e in trace1.events if e.name in ["safe_filter", "safe_groupby", "detect_anomalies", "plot"]])
        direct_steps = len([e for e in trace2.events if e.name == "direct_execute"])
        
        plan_time = sum(e.ms for e in trace1.events)
        direct_time = sum(e.ms for e in trace2.events)
        
        print(f"  📈 计划模式: {plan_steps} 个工具调用, 耗时 {plan_time}ms")
        print(f"  ⚡ 直接模式: {direct_steps} 个工具调用, 耗时 {direct_time}ms")
        print(f"  🎯 计划模式优势: 可追溯、可复现、可控制、可审计")
        print(f"  ⚡ 直接模式优势: 响应快、简单直接")
        print(f"  📝 建议: 生产环境推荐计划模式,开发调试可用直接模式")

if __name__ == "__main__":
    # 运行演示
    demonstrate_planning_vs_direct()
    
    print("\n" + "=" * 80)
    print("🎯 核心结论:先结构化计划,再执行的价值")
    print("=" * 80)
    print("✅ 可复现性: 同一问题多次执行,步骤和结果一致")
    print("✅ 可控制性: 每个步骤都有明确的输入输出,便于调试")
    print("✅ 可审计性: 完整的执行轨迹,满足合规要求")
    print("✅ 可扩展性: 新增工具只需扩展计划模板")
    print("✅ 可解释性: 用户能理解AI的\"思考过程\"")
    print("⚠️ 权衡: 增加了执行时间和复杂度")
    print("🚀 生产建议: 关键业务必须使用计划模式,简单查询可考虑直接模式")
✅ 验证方法
运行上面的代码,观察两种模式的输出差异:计划模式会输出完整的JSON计划和执行轨迹,直接模式只给出快速结果。
🎬 实际运行效果展示
当用户提问:"近30天各渠道GMV趋势分析,找出异常点"
🔧 计划模式输出:
📝 生成的计划:
{
  "intent": "分析近30天各渠道GMV趋势变化并识别异常点",
  "range": "last_30d",
  "steps": [
    {"tool": "safe_filter", "args": {"conditions": [{"field": "date", "op": ">=", "value": "2024-01-01"}], "limit": 1000}},
    {"tool": "safe_groupby", "args": {"by": ["channel", "date"], "metric": "gmv", "agg": "sum", "top_n": 50}},
    {"tool": "detect_anomalies", "args": {"metric": "gmv", "method": "std"}},
    {"tool": "plot", "args": {"chart": "line", "x": "date", "y": "gmv", "series": "channel"}}
  ],
  "deliver": "渠道GMV趋势图+异常点分析报告"
}

✅ 计划验证: 通过 - ok
⚠️ 风险评估: low
🎯 执行步骤: 4 步
🔍 执行数据过滤: {'conditions': [{'field': 'date', 'op': '>=', 'value': '2024-01-01'}], 'limit': 1000}
📊 执行分组统计: {'by': ['channel', 'date'], 'metric': 'gmv', 'agg': 'sum', 'top_n': 50}
🚨 执行异常检测: {'metric': 'gmv', 'method': 'std'}
📈 生成图表: {'chart': 'line', 'x': 'date', 'y': 'gmv', 'series': 'channel'}
🎉 执行完成,结果: 4 个工具输出
📊 计划模式执行轨迹:
  ✅ validate_plan (15ms) - {'reason': 'ok'}
  ✅ assess_risk (2ms) - {'risk': 'low'}
  ✅ safe_filter (102ms) - {'ms_step': 100, 'result_count': 45}
  ✅ safe_groupby (205ms) - {'ms_step': 200, 'result_count': 156}
  ✅ detect_anomalies (158ms) - {'ms_step': 150, 'result_count': 67}
  ✅ plot (308ms) - {'ms_step': 300, 'result_count': 89}
⚡ 直接模式输出:
⚡ 直接执行分析: 近30天各渠道GMV趋势分析,找出异常点
🎉 直接执行完成
📊 直接执行轨迹:
  ✅ direct_execute (503ms) - {'ms_step': 500, 'result_count': 78}
🔄 对比分析:
📈 计划模式: 4 个工具调用, 耗时 788ms
⚡ 直接模式: 1 个工具调用, 耗时 503ms

🎯 计划模式优势:
  • 可追溯: 每个步骤都有详细记录
  • 可复现: 同样问题总是生成相同步骤
  • 可控制: 可以调整或跳过特定步骤
  • 可审计: 满足企业合规要求

⚡ 直接模式优势:
  • 响应快: 少了计划生成时间
  • 简单直接: 适合简单查询
  • 资源少: 不需要大模型调用

📝 生产建议: 关键业务必须使用计划模式,简单查询可考虑直接模式
💬 面试加分
"我们不直接执行查询,而是先将自然语言拆解为结构化计划(范围/指标/分组/图表/交付物),确认口径后再执行。这避免了'同一问题两次结果不同'的可复现性问题。实际项目中,计划模式的可追溯性帮我们快速定位了多个数据口径不一致的问题。"
治理4)工具治理与安全边界:权限 / 审计 / 限流
目标:每次工具调用都有 trace_id、参数、结果摘要、耗时、是否成功;敏感工具需要权限。
场景:"导出全量明细"属于高风险工具,普通用户不可用。
🔥 为什么工具治理至关重要?
企业级风险:没有治理的 AI 系统等于"无证驾驶"——数据泄露、权限滥用、系统崩溃、合规违规,每一个都可能造成百万级损失
技术复杂性:需要同时考虑静态权限(角色矩阵)、动态权限(数据范围)、审计合规(GDPR/等保)、系统稳定性(限流熔断)、可追溯性(全链路 trace)。
业务影响:治理缺失会导致数据安全事件服务不可用合规罚款品牌声誉受损,甚至业务停摆
🎯 治理复杂度分析
静态权限:角色矩阵(5×12)+ 工具分级(4级)+ 数据权限(行级+列级)
动态权限:时间窗口(工作时间/紧急)+ 地域限制(办公IP)+ 风险评分(实时计算)
系统保护:限流(用户/工具/全局)+ 熔断(错误率>30%)+ 降级(只读模式)
审计合规:全链路 trace + 数据脱敏 + 合规报表(日/周/月)+ 告警(实时/批量)
text
// ============================
// 目的:审计日志(audit log)示例 —— “拒绝也要留痕”
//
// 字段解释:
// - trace_id:一次请求/一次任务的全链路追踪 ID
// - user:触发操作的用户(或角色)
// - tool:尝试调用的工具
// - args:工具入参(必要时需要脱敏/截断)
// - status:allow / deny / pending 等
// - reason:拒绝原因(权限不足、超限、风险过高...)
// ============================
{"trace_id":"t-xxx","user":"ops","tool":"export_report","args":{"format":"pdf"},"status":"deny","reason":"permission"}
企业级工具治理全景图 📦 工具注册中心 名称 / 参数 Schema / 权限级别 风险等级 / 审计要求 / 限流策略 🔐 多维度权限鉴权 角色权限 + 数据权限 + 时间窗口 IP白名单 + 动态风险评估 ⚡ 执行控制层 限流熔断 + 重试机制 + 超时控制 资源隔离 + 降级策略 📊 全链路审计监控 trace_id / 用户 / 工具 / 参数 / 结果 / 耗时 / 错误码 实时告警 / 合规报表 / 风险分析 / 性能监控 ⚠️ 高风险工具: 12个 👥 用户角色: 5级 📈 日调用量: 100万+ 🔒 合规要求: GDPR+等保
工具治理三层架构:注册 → 鉴权 → 审计 📦 工具注册中心 名称 / 参数 Schema / 权限级别 🔐 权限鉴权层 user_role ≥ tool_level ? 📝 审计日志 + 限流熔断 trace_id / 参数 / 耗时 / 结果 / QPS ✓ user=admin tool=export_report → allow (200ms) ✗ user=viewer tool=export_report → deny (permission) 权限矩阵 viewer: [inspect, compute] admin: [inspect, compute, export, clean]
📝 最小可运行代码:工具注册 + 权限校验
python
# ============================
# 目的:最小“权限 / 审计 / 限流”可运行示例(复制即可跑)
#
# 你将得到:
# - 工具注册(工具名/权限级别/实现函数)
# - 权限校验:deny 也必须写审计
# - 用户维度限流:同一用户 1 秒内最多 N 次
# - 统一的受保护调用入口:guarded_call(...)
# ============================

import json
import time
import uuid
from collections import deque
from typing import Any, Callable, Dict, Deque


# 角色等级映射:把“角色字符串”转成可比较的数字。
# 生产环境通常来自:账号系统 / IAM / OAuth claims。
ROLE_LEVELS = {"viewer": 1, "editor": 2, "admin": 3}


def audit_log(
    *,
    trace_id: str,
    user: str,
    role: str,
    tool: str,
    args: Dict[str, Any],
    status: str,
    reason: str = "",
    ms: int = 0,
):
    # 审计日志:建议在生产写入数据库 / ES / 日志平台。
    # 这里为了“复制即可跑”,直接 print JSON(一行一条,便于 grep)。
    record = {
        "trace_id": trace_id,
        "user": user,
        "role": role,
        "tool": tool,
        "args": args,
        "status": status,
        "reason": reason,
        "ms": ms,
        "ts": round(time.time(), 3),
    }
    print(json.dumps(record, ensure_ascii=False))


class UserRateLimiter:
    # 用户维度限流(滑动窗口):
    # - window_seconds:窗口大小(秒)
    # - max_calls:窗口内允许的最大调用数
    # 说明:这是“系统保护”的第一道门(防刷、防爆)。
    def __init__(self, max_calls: int, window_seconds: float):
        self.max_calls = max_calls
        self.window_seconds = window_seconds
        self.calls: Dict[str, Deque[float]] = {}

    def allow(self, user: str) -> bool:
        # deque 里存该 user 的历史调用时间戳;每次调用先淘汰窗口外的数据。
        now = time.time()
        q = self.calls.setdefault(user, deque())
        while q and now - q[0] > self.window_seconds:
            q.popleft()
        if len(q) >= self.max_calls:
            return False
        q.append(now)
        return True


def inspect_schema(_: Dict[str, Any]) -> Dict[str, Any]:
    # 示例“低风险工具”:查看 schema。
    # 要点:工具函数签名统一为 fn(args) -> json-serializable 输出。
    return {"columns": ["date", "channel", "gmv", "orders"], "rows": 1000}


def export_report(args: Dict[str, Any]) -> Dict[str, Any]:
    # 示例“高风险工具”:导出报告。
    # 在企业里 export 往往涉及:大范围数据 + 敏感字段 + 外发链路。
    fmt = args.get("format", "csv")
    return {"export": "ok", "format": fmt, "url": "https://example.com/report"}


# 工具注册中心(最小版):
# - level:调用该工具所需的最低角色等级
# - fn:工具实现
# 生产环境可扩展:参数 schema、超时、计费、owner、风险等级、脱敏策略等。
TOOL_REGISTRY: Dict[str, Dict[str, Any]] = {
    "inspect_schema": {"level": "viewer", "fn": inspect_schema},
    "export_report": {"level": "admin", "fn": export_report},
}


def check_permission(role: str, tool_name: str) -> bool:
    # 权限校验:
    # - tool 不存在:直接拒绝
    # - role_level >= tool_required_level:允许
    tool = TOOL_REGISTRY.get(tool_name)
    if not tool:
        return False
    return ROLE_LEVELS.get(role, 0) >= ROLE_LEVELS.get(tool["level"], 999)


# 限流器:这里设置为 1 秒内最多 3 次,仅用于演示。
# 实战里你会按:用户等级/接口/工具/全局分别限流,且通常配合 Redis。
rate_limiter = UserRateLimiter(max_calls=3, window_seconds=1.0)


def guarded_call(user: str, role: str, tool: str, args: Dict[str, Any]) -> Dict[str, Any]:
    # 统一的“受保护工具调用入口”(治理核心):
    # 1) 生成 trace_id(全链路追踪)
    # 2) 限流:超限直接 deny + 审计
    # 3) 鉴权:无权限直接 deny + 审计
    # 4) 执行工具:allow + 审计(含耗时)
    trace_id = f"t-{uuid.uuid4().hex[:6]}"
    t0 = time.time()

    if not rate_limiter.allow(user):
        audit_log(trace_id=trace_id, user=user, role=role, tool=tool, args=args, status="deny", reason="rate_limit")
        return {"ok": False, "error": "rate_limited", "trace_id": trace_id}

    if not check_permission(role, tool):
        audit_log(trace_id=trace_id, user=user, role=role, tool=tool, args=args, status="deny", reason="permission")
        return {"ok": False, "error": "permission_denied", "trace_id": trace_id}

    fn: Callable[[Dict[str, Any]], Any] = TOOL_REGISTRY[tool]["fn"]
    out = fn(args)
    audit_log(
        trace_id=trace_id,
        user=user,
        role=role,
        tool=tool,
        args=args,
        status="allow",
        ms=round((time.time() - t0) * 1000),
    )
    return {"ok": True, "data": out, "trace_id": trace_id}


if __name__ == "__main__":
    # 演示输出:
    # - 你会看到每次调用都打印一条审计 JSON(allow/deny 都有)
    # - 同一用户短时间连续调用会触发 rate_limit
    print("\n--- viewer 调用 inspect_schema(允许)---")
    print(guarded_call("u1", "viewer", "inspect_schema", {}))

    print("\n--- viewer 调用 export_report(拒绝:权限不足,deny 也审计)---")
    print(guarded_call("u1", "viewer", "export_report", {"format": "pdf"}))

    print("\n--- admin 调用 export_report(允许)---")
    print(guarded_call("admin_1", "admin", "export_report", {"format": "pdf"}))

    print("\n--- 同一用户快速连续调用(触发限流)---")
    for _ in range(5):
        print(guarded_call("u2", "viewer", "inspect_schema", {}))
✅ 验证方法
低权限用户(viewer)触发 export_report 必须拒绝并写入审计日志(含 trace_id、reason);同一工具 1 秒内调用 >10 次应触发限流。
💬 面试加分
"我设计了工具注册中心 + 三层治理:注册时声明参数 Schema 和权限级别,运行时鉴权+限流,事后审计日志可追溯。每次调用都有 trace_id,deny 也留记录。"
协同5)人机协同审核:高风险步骤"确认再执行"
目标:当涉及敏感字段/大范围导出/破坏性操作时,系统暂停并请求确认。全自动在企业场景中不可接受——高风险操作一旦出错,损失不可逆
Human-in-the-Loop 决策流 Agent 生成计划 风险? low ✓ 自动执行 high ⏸ 暂停,等待确认 确认 → 执行 拒绝 → 不执行+审计
📝 真实可执行代码(可直接复制运行):LangChain Agent 版本 — 使用 HumanInTheLoopMiddleware 实现人工确认
python
import os
import logging

from langchain.agents import create_agent
from langchain.agents.middleware.human_in_the_loop import HumanInTheLoopMiddleware
from langchain_core.tools import tool
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import Command

# 配置日志
logging.basicConfig(level=logging.INFO)

# ============================
# 定义高危工具:数据导出
# ============================
@tool
def export_salary_report(department: str, format: str = "csv") -> str:
    """导出部门薪资报告(高危工具)。
    
    Args:
        department: 部门名称,如 "IT"、"Sales"
        format: 导出格式,支持 "csv" 或 "excel"
    
    Returns:
        导出结果描述
    """
    # 模拟导出操作(真实场景会访问数据库并生成文件)
    print(f"[EXPORT] 正在导出 {department} 部门的薪资报告,格式:{format}")
    return f"已成功导出 {department} 部门薪资报告(格式:{format}),包含敏感字段:姓名、工号、薪资、手机号"


@tool
def query_employee_info(employee_id: str) -> str:
    """查询员工基本信息(普通工具,不涉及敏感数据)。
    
    Args:
        employee_id: 员工工号
    
    Returns:
        员工基本信息
    """
    print(f"[QUERY] 查询员工信息:{employee_id}")
    return f"员工 {employee_id}:张三,IT部门,入职时间 2020-01-15"


# ============================
# 初始化 LLM 和 Agent
# ============================
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
    raise ValueError("请先在环境变量中设置 DASHSCOPE_API_KEY")

llm = ChatTongyi(model_name="qwen-turbo", dashscope_api_key=api_key, temperature=0)

# 配置人类介入中间件:只对 export_salary_report 工具触发审批
hitl = HumanInTheLoopMiddleware(
    interrupt_on={
        "export_salary_report": True,  # 导出薪资报告需要人工确认
    }
)

# 创建 Agent
agent = create_agent(
    llm,
    tools=[export_salary_report, query_employee_info],
    middleware=(hitl,),  # 启用人类介入中间件
    system_prompt=(
        "你是一个数据分析助手。"
        "当用户需要导出薪资报告时,必须调用 export_salary_report 工具。"
        "当用户需要查询员工信息时,调用 query_employee_info 工具。"
    ),
    debug=True,
    checkpointer=MemorySaver(),  # 必须配置 checkpointer 才能支持中断后继续
)

# ============================
# 执行流程
# ============================
run_config = {
    "configurable": {"thread_id": "salary_export_demo"},
    "recursion_limit": 100,
}

# 用户请求:导出 IT 部门薪资报告(会触发人工确认)
state = agent.invoke(
    {"messages": [("human", "帮我导出 IT 部门的薪资报告,格式用 CSV")]},
    config=run_config
)

# 检查是否触发了人工审批
if "__interrupt__" in state:
    req = state["__interrupt__"][0].value
    action_req = req["action_requests"][0]
    
    print("\n" + "="*60)
    print("🚨 检测到高风险操作,需要人工确认")
    print("="*60)
    print(f"工具名称: {action_req['name']}")
    print(f"工具参数: {action_req['args']}")
    print(f"工具描述: {action_req.get('description', 'N/A')}")
    print("="*60)
    
    # 人工决策
    decision = input("\n请选择操作 (approve/reject/edit): ").strip().lower()
    
    if decision == "approve":
        print("\n✅ 已批准,正在执行工具调用...")
        cmd = Command(resume={"decisions": [{"type": "approve"}]})
    elif decision == "reject":
        print("\n❌ 已拒绝,工具不会执行")
        cmd = Command(resume={"decisions": [{"type": "reject", "message": "人工拒绝执行"}]})
    elif decision == "edit":
        # 示例:修改导出格式为 excel
        print("\n✏️ 已编辑,修改导出格式为 excel")
        edited_action = {
            "name": action_req["name"],
            "args": {"department": action_req["args"]["department"], "format": "excel"},
        }
        cmd = Command(resume={"decisions": [{"type": "edit", "edited_action": edited_action}]})
    else:
        print("\n⚠️ 无效决策,默认拒绝")
        cmd = Command(resume={"decisions": [{"type": "reject", "message": "未给出有效决策"}]})
    
    # 继续执行
    state = agent.invoke(cmd, config=run_config)

# 输出最终结果
print("\n" + "="*60)
print("📊 最终结果")
print("="*60)
print(state["messages"][-1].content)
print("="*60)

# ============================
# 验证要点
# ============================
print("\n✅ 验证要点:")
print("1. 调用 export_salary_report 时会触发人工确认")
print("2. 选择 approve 后工具会执行,可以看到 [EXPORT] 日志")
print("3. 选择 reject 后工具不会执行,不会看到 [EXPORT] 日志")
print("4. 选择 edit 后可以修改工具参数(如导出格式)")
print("5. 调用 query_employee_info 不会触发确认,直接执行")
print("\n💡 扩展建议:")
print("- 可以在 interrupt_on 中添加更多高危工具(如 delete_data、update_schema)")
print("- 可以结合审计日志记录所有确认/拒绝操作")
print("- 可以配置不同角色的审批权限(如普通用户只能查询,管理员才能导出)")
🔍 代码关键点解析
1. HumanInTheLoopMiddleware:配置 interrupt_on={"export_salary_report": True},只对指定工具触发审批
2. MemorySaver:必须配置 checkpointer 才能支持中断后继续执行(保存会话状态)
3. __interrupt__ 检测:通过 if "__interrupt__" in state 判断是否触发了人工审批
4. Command(resume=...):使用 Command 对象传递人工决策(approve/reject/edit)
5. thread_id:配置唯一的 thread_id 确保同一会话可以中断后继续
💬 面试加分
"我设计了 Human-in-the-Loop 机制:Agent 生成计划后自动评估风险(敏感字段/大量导出/破坏性工具),高风险暂停等确认。拒绝后工具链不调用且写入审计日志。在 LangChain 中使用 HumanInTheLoopMiddleware + MemorySaver 实现,支持 approve/reject/edit 三种决策,确保高危操作可控。"
可观测6)复盘与质量优化:错误可定位、慢点可解释
目标:把每次分析的 query、计划、工具调用、关键中间结果、耗时写入 trace。AI 应用最大的痛点是"黑盒"——出了错不知道错在哪一步,性能慢不知道慢在哪个环节
全链路追踪时间线 查询 0ms 计划 150ms 检查 270ms 计算 610ms 绘图 810ms 完成 ✓ 980ms 追踪ID: t-001 查询 → 计划(150ms) → 检查结构(120ms)✓ → 计算指标(340ms)✓ → 绘制趋势(200ms)✓ → 总计: 980ms 成功:true
🔍 如何做到错误可定位?
错误上下文捕获:每个工具调用都记录输入参数输出结果错误堆栈执行环境(用户/时间/IP),形成完整错误链路。
错误分类标记:区分数据错误(字段不存在/类型不匹配)、逻辑错误(参数组合不合理)、系统错误(超时/内存不足),便于快速定位问题类型。
错误回放机制:保存完整的执行上下文,可在本地环境1:1 复现线上错误,支持调试和修复验证。
⚡ 如何做到性能可解释?
细粒度耗时分解:每个步骤记录开始时间结束时间耗时占比,识别性能瓶颈(如 LLM 调用占 80%)。
资源使用监控:记录内存占用CPU 使用网络 IO,解释为什么某些步骤特别慢(如大文件处理)。
性能基线对比:建立历史性能基线,异常时自动告警(如:某步骤耗时超过平均值 3 倍)。
📝 真实可执行代码(可直接复制运行):LangChain Agent 版本 — 使用 Middleware 实现日志和性能监控
python
import os
import time
import logging
from typing import Dict, List

from langchain.agents import create_agent
from langchain.agents.middleware.types import AgentMiddleware, ToolCallRequest
from langchain_core.tools import tool
from langchain_community.chat_models import ChatTongyi
from langgraph.checkpoint.memory import MemorySaver

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# ============================
# 自定义 Middleware:日志记录
# ============================
class ToolLoggingMiddleware(AgentMiddleware):
    """记录每次工具调用的详细信息(工具名、参数、结果)"""
    
    def __init__(self, logger_name: str = "mw.tools", include_args: bool = True) -> None:
        super().__init__()
        self.logger = logging.getLogger(logger_name)
        self.include_args = include_args
        self.call_history: List[Dict] = []  # 保存所有调用历史
    
    def wrap_tool_call(self, request: ToolCallRequest, handler):
        tool_call = request.tool_call or {}
        name = tool_call.get("name")
        args = tool_call.get("args")
        
        # 记录工具调用开始
        prefix = "[ToolLoggingMiddleware]"
        if self.include_args:
            print(f"{prefix} 🔧 tool_call name={name} args={args}")
            self.logger.info("tool_call name=%s args=%s", name, args)
        else:
            print(f"{prefix} 🔧 tool_call name={name}")
            self.logger.info("tool_call name=%s", name)
        
        # 执行工具
        start_time = time.time()
        try:
            result = handler(request)
            success = True
            error_msg = None
        except Exception as e:
            result = None
            success = False
            error_msg = str(e)
            print(f"{prefix} ❌ tool_error name={name} error={error_msg}")
            self.logger.error("tool_error name=%s error=%s", name, error_msg)
            raise  # 重新抛出异常
        
        duration_ms = int((time.time() - start_time) * 1000)
        
        # 记录工具调用结果
        print(f"{prefix} ✅ tool_result name={name} type={type(result).__name__} duration={duration_ms}ms")
        self.logger.info("tool_result name=%s type=%s duration=%sms", name, type(result).__name__, duration_ms)
        
        # 保存到历史记录
        self.call_history.append({
            "tool_name": name,
            "args": args if self.include_args else None,
            "success": success,
            "error_msg": error_msg,
            "duration_ms": duration_ms,
            "timestamp": time.time()
        })
        
        return result
    
    def get_call_history(self) -> List[Dict]:
        """获取所有工具调用历史"""
        return self.call_history


# ============================
# 自定义 Middleware:性能监控
# ============================
class ToolTimingMiddleware(AgentMiddleware):
    """记录每次工具调用的耗时,识别性能瓶颈"""
    
    def __init__(self, logger_name: str = "mw.timing", threshold_ms: int = 100) -> None:
        super().__init__()
        self.logger = logging.getLogger(logger_name)
        self.threshold_ms = threshold_ms
        self.timing_stats: Dict[str, List[int]] = {}  # 保存每个工具的耗时统计
    
    def wrap_tool_call(self, request: ToolCallRequest, handler):
        tool_call = request.tool_call or {}
        name = tool_call.get("name")
        
        # 记录开始时间
        start = time.perf_counter()
        result = handler(request)
        cost_ms = int((time.perf_counter() - start) * 1000)
        
        # 保存耗时统计
        if name not in self.timing_stats:
            self.timing_stats[name] = []
        self.timing_stats[name].append(cost_ms)
        
        # 只记录超过阈值的调用
        prefix = "[ToolTimingMiddleware]"
        if cost_ms >= self.threshold_ms:
            print(f"{prefix} ⚡ tool_timing name={name} cost_ms={cost_ms} (超过阈值 {self.threshold_ms}ms)")
            self.logger.warning("tool_timing name=%s cost_ms=%s threshold=%s", name, cost_ms, self.threshold_ms)
        else:
            print(f"{prefix} ⚡ tool_timing name={name} cost_ms={cost_ms}")
            self.logger.info("tool_timing name=%s cost_ms=%s", name, cost_ms)
        
        return result
    
    def get_performance_summary(self) -> Dict[str, Dict]:
        """获取性能统计摘要"""
        summary = {}
        for tool_name, timings in self.timing_stats.items():
            if timings:
                summary[tool_name] = {
                    "total_calls": len(timings),
                    "avg_ms": sum(timings) / len(timings),
                    "min_ms": min(timings),
                    "max_ms": max(timings),
                    "total_ms": sum(timings)
                }
        return summary


# ============================
# 定义数据分析工具
# ============================
@tool
def check_schema(table_name: str) -> str:
    """检查数据表结构(模拟工具)"""
    time.sleep(0.1)  # 模拟耗时
    return f"表 {table_name} 包含字段:id, name, department, salary, hire_date"


@tool
def calculate_metrics(metric_name: str, group_by: str = None) -> str:
    """计算指标(模拟工具)"""
    time.sleep(0.2)  # 模拟耗时
    if group_by:
        return f"已计算 {metric_name},按 {group_by} 分组:IT部门平均值=15000, Sales部门平均值=12000"
    return f"已计算 {metric_name}:总体平均值=13500"


@tool
def generate_chart(chart_type: str, data_source: str) -> str:
    """生成图表(模拟工具)"""
    time.sleep(0.15)  # 模拟耗时
    return f"已生成 {chart_type} 图表,数据来源:{data_source},保存路径:/tmp/chart_{chart_type}.png"


# ============================
# 初始化 LLM 和 Agent
# ============================
api_key = os.getenv("DASHSCOPE_API_KEY")
if not api_key:
    raise ValueError("请先在环境变量中设置 DASHSCOPE_API_KEY")

llm = ChatTongyi(model_name="qwen-turbo", dashscope_api_key=api_key, temperature=0)

# 创建 middleware 实例
tool_logger = ToolLoggingMiddleware(logger_name="mw.tools", include_args=True)
tool_timer = ToolTimingMiddleware(logger_name="mw.timing", threshold_ms=100)

# 创建 Agent(middleware 链:日志 + 性能监控)
agent = create_agent(
    llm,
    tools=[check_schema, calculate_metrics, generate_chart],
    middleware=(tool_logger, tool_timer),  # 组合多个 middleware
    system_prompt=(
        "你是一个数据分析助手。"
        "当用户需要分析数据时,先检查表结构,然后计算指标,最后生成图表。"
    ),
    debug=True,
    checkpointer=MemorySaver(),
)

# ============================
# 执行分析任务
# ============================
run_config = {
    "configurable": {"thread_id": "data_analysis_demo"},
    "recursion_limit": 100,
}

print("="*60)
print("开始执行数据分析任务")
print("="*60)

state = agent.invoke(
    {"messages": [("human", "分析员工薪资数据:先检查 employees 表结构,然后计算各部门平均薪资,最后生成柱状图")]},
    config=run_config
)

print("\n" + "="*60)
print("📊 最终结果")
print("="*60)
print(state["messages"][-1].content)

# ============================
# 输出可观测性统计
# ============================
print("\n" + "="*60)
print("📈 工具调用历史(来自 ToolLoggingMiddleware)")
print("="*60)
import json
print(json.dumps(tool_logger.get_call_history(), ensure_ascii=False, indent=2))

print("\n" + "="*60)
print("⚡ 性能统计摘要(来自 ToolTimingMiddleware)")
print("="*60)
perf_summary = tool_timer.get_performance_summary()
print(json.dumps(perf_summary, ensure_ascii=False, indent=2))

# 识别性能瓶颈
if perf_summary:
    bottleneck = max(perf_summary.items(), key=lambda x: x[1]["avg_ms"])
    print(f"\n🔍 性能瓶颈:{bottleneck[0]} (平均耗时 {bottleneck[1]['avg_ms']:.1f}ms)")

# ============================
# 验证要点
# ============================
print("\n" + "="*60)
print("✅ 验证要点")
print("="*60)
print("1. ToolLoggingMiddleware 记录了所有工具调用的详细信息(名称、参数、结果、耗时)")
print("2. ToolTimingMiddleware 记录了每次调用的耗时,并识别超过阈值的慢调用")
print("3. 可以通过 get_call_history() 获取完整调用历史,用于错误回放")
print("4. 可以通过 get_performance_summary() 获取性能统计,识别瓶颈")
print("5. Middleware 是可组合的,可以同时启用多个(日志 + 性能 + 人工审批)")
print("\n💡 扩展建议:")
print("- 可以将 call_history 保存到数据库(如 Elasticsearch)用于全链路追踪")
print("- 可以添加错误分类逻辑(数据错误/逻辑错误/系统错误)")
print("- 可以集成 APM 平台(如 Datadog、Prometheus)进行实时监控")
print("- 可以添加告警机制(如耗时超过 P95 基线时发送通知)")
🔍 Middleware 实现关键点
1. wrap_tool_call 方法:拦截每次工具调用,在执行前后插入自定义逻辑(日志、计时、错误处理)
2. 状态保存:使用实例变量(call_history、timing_stats)保存所有调用记录,支持事后分析
3. 异常处理:捕获工具执行异常,记录错误信息后重新抛出,确保不影响 Agent 流程
4. 性能统计:使用 time.perf_counter() 精确计时,支持毫秒级性能分析
5. 可组合性:多个 middleware 可以链式组合,按顺序执行(日志 → 性能 → 人工审批)
💬 面试加分
"为解决 AI 黑盒问题,我设计了全链路 Trace:每次分析生成唯一 trace_id,记录 query→plan→每步工具调用的参数和耗时。线上出问题直接按 trace_id 回放,快速定位是 Prompt 问题还是数据问题。在 LangChain 中使用自定义 Middleware 实现,支持日志记录、性能监控、错误分类,可与 APM 平台集成。"
交付7)可视化与报告:图表是"证据",报告是"交付物"
目标:同一份输出包含:表格 + 图表 + 结论 + 风险提示,并可导出 HTML/PDF。一个查询可能需要同时返回多种形态的结果。
统一 JSON Schema → 多格式自适应渲染 📨 模型返回 JSON { "answer": "...", "table": {...}, "bar": {...} } 💬 文字结论 📊 数据表格 📈 可视化图表 🔄 自适应渲染逻辑 if "answer" in resp → st.write() if "table" in resp → st.dataframe() if "bar" in resp → st.bar_chart() if "line" in resp → st.line_chart()
📝 最小可运行代码:多格式响应渲染
python
# ============================
# 目的:前端渲染层“按 JSON Schema 自适应渲染”
#
# 设计点:
# - Agent 输出统一 JSON(answer/table/bar/line/scatter...)
# - UI 层不关心“是哪个工具做的”,只按字段渲染
# - 这样可以做到:新增一个图表类型,只要新增一个字段即可
# ============================

def render_response(response_dict: dict):
    """根据 JSON Schema 字段自适应选择渲染组件"""
    if "answer" in response_dict:
        st.success("**回答:**")
        st.write(response_dict["answer"])
    if "table" in response_dict:
        df = pd.DataFrame(
            response_dict["table"]["data"],
            columns=response_dict["table"]["columns"]
        )
        st.dataframe(df, use_container_width=True)
    for chart_type in ["bar", "line", "scatter"]:
        if chart_type in response_dict:
            create_chart(response_dict[chart_type], chart_type)
✅ 验证方法
"近 30 天趋势"必须输出折线图;"各渠道对比"必须输出条形图;导出 HTML 后重新打开仍能看到图与结论。
💬 面试加分
"我设计了统一 JSON Schema 响应协议(answer/table/bar/line/scatter),前端按字段类型自动选渲染组件,一套代码兼容多种数据结构。"
✅ 最低成本验收清单(你能马上验证)
  • 复杂问题先输出结构化计划(范围/指标/分组/图表/交付物),再执行。
  • 每次分析有 trace_id,可定位到具体步骤与耗时(慢点/错点都能找)。
  • 高风险操作必须走“确认再执行”,拒绝后不得调用工具。

🏗️ 项目架构

项目按 7 大核心能力 将业务逻辑拆解为独立类,main.py 通过门面类统一调用:

📁 文件结构

text
项目五:「数析」智能数据分析台/
├── main.py                 # Streamlit 界面(依赖 DataframeAgentFacade)
├── utils.py                # 7 大核心能力(按类拆解)
│   ├── ToolMeta            # 工具元数据(名称/权限/超时)
│   ├── ToolRegistry        # ① 工具注册中心(注册/鉴权/调用计数)
│   ├── AnalysisPlan        # 结构化分析计划(数据类)
│   ├── PlanEngine          # ② 自然语言 → 结构化计划(LLM 第一阶段)
│   ├── RiskGuard           # ③ 风险评估(高危工具/敏感字段/大导出)
│   ├── AuditEvent          # 审计事件(数据类)
│   ├── AuditLogger         # ④ 人机协同审核 + 审计日志
│   ├── DataAnalyzer        # ⑤ ReAct 执行层(逐步调用工具 + LLM 第二阶段)
│   ├── QualityOptimizer    # ⑥ 复盘分析 + 优化建议
│   ├── ReportExporter      # ⑦ HTML 报告生成
│   └── DataframeAgentFacade# 门面类:统一编排以上 7 大模块
├── requirements.txt        # 项目依赖
├── house_price.csv         # 示例数据:房价数据(545 条)
└── personal_data.csv       # 示例数据:个人消费数据(22 条)

🔗 完整调用链路

text
用户界面 (Streamlit / main.py)
    ↓ facade.analyze(df, query, human_confirmed)
 DataframeAgentFacade
    ├─① PlanEngine.parse()        ← 自然语言 → AnalysisPlan(LLM 第一阶段)
    ├─② RiskGuard.assess()        ← 评估风险等级(high / low)
    ├─③ AuditLogger.record()      ← 记录 cancelled(高风险未确认则阻断)
    ├─④ DataAnalyzer.execute()    ← 逐步调用 ToolRegistry 中的工具
    │       └─ LLM 第二阶段       ← 生成最终 JSON 结果
    ├─⑤ AuditLogger.record()      ← 记录 executed / failed
    └─⑥ ReportExporter(可选)    ← 生成 HTML 报告(含审计日志)
    ↓
 render_result()                  ← 自动分发 answer/table/bar/line/scatter
    ↓
QualityOptimizer.suggest()        ← 按需输出优化建议(基于审计日志)

⚙️ 环境准备

1. 安装依赖

text
# 安装依赖:建议在虚拟环境中执行,避免污染系统 Python
python -m pip install -U streamlit>=1.32.0 pandas==2.2.3 \
  langchain-classic==1.0.0 "langchain-core>=1,<2" "langchain-community>=0.3,<0.4" \
  "langchain-text-splitters>=1,<2" "langsmith>=0.1.17,<1" dashscope python-dotenv>=1.0.0

或使用 requirements.txt:

text
# requirements.txt(建议直接保存为 requirements.txt 使用)
#
# 说明:
# - 这里把关键依赖“锁版本区间”,避免因为上游升级导致示例无法运行
# - 如果你使用不同版本的 LangChain/LangGraph,请以你本地可跑版本为准
# - dashscope 负责调用通义千问 API

streamlit>=1.32.0
pandas==2.2.3

# LangChain 依赖(版本锁定,避免"在我电脑上能跑")
langchain-classic==1.0.0
langchain-core>=1,<2
langchain-community>=0.3,<0.4
langchain-text-splitters>=1,<2
langsmith>=0.1.17,<1

# 通义千问(DashScope)SDK
dashscope

# 可选:从 .env 加载环境变量
python-dotenv>=1.0.0

2. 获取通义千问 API Key

访问 阿里云 DashScope 控制台 获取 API Key

🧠 核心模块一:utils.py(数据分析 Agent)

按 7 大核心能力拆解为独立类,各司其职:ToolRegistry(工具注册)、PlanEngine(计划引擎)、RiskGuard(风险守卫)、AuditLogger(审计日志)、DataAnalyzer(数据分析)、QualityOptimizer(质量优化)、ReportExporter(报告导出),最终由 DataframeAgentFacade 统一编排。

python
"""utils.py — 「数析」智能数据分析台核心模块

本文件的目标:把“LLM + Pandas”的一次性脚本,升级为可讲解、可扩展、可上线的组件化系统。

核心思想:
- **LLM 负责理解自然语言**:把用户问题解析为一个结构化执行计划(AnalysisPlan)。
- **代码负责真实执行**:根据计划调用 Pandas 工具链,得到确定的表格/图表数据。
- **统一输出协议**:最终返回一个 JSON dict,交给界面层(main.py)渲染。

你可以把它理解为一个极简版的“Agent 系统”:
- Planning(PlanEngine):自然语言 -> steps(工具调用序列)
- Guard(RiskGuard):识别高风险/敏感字段,决定是否拦截
- Execute(DataAnalyzer):按 steps 执行工具链(DataFrame pipeline)
- Audit(AuditLogger):记录每次请求的 trace_id、状态、用过哪些工具

按 7 大核心能力拆解:
  1. ToolRegistry      — 工具注册中心(有哪些工具、怎么调用、调用计数)
  2. PlanEngine        — LLM 规划器(自然语言 -> 结构化计划 AnalysisPlan)
  3. RiskGuard         — 风险识别(高危工具/敏感字段/大导出)
  4. AuditLogger       — 审计留痕(人机协同 + 复盘)
  5. DataAnalyzer      — 执行器(按计划真实跑 Pandas 计算并产出结构化结果)
  6. QualityOptimizer  — 质量优化建议(基于审计日志的简单复盘)
  7. ReportExporter    — 报告导出(HTML 报告字符串)

统一输出协议(供 main.py 渲染):
- 纯文本:{"answer": "..."}
- 表格:  {"table": {"columns": [...], "data": [[...], ...]}}
- 条形图:{"bar":   {"columns": [x_col, y_col], "data": [[x,y], ...]}}
- 折线图:{"line":  {"x": "x列名", "y": "y列名", "columns": [...], "data": [[x,y], ...]}}
- 散点图:{"scatter":{...}}
并附带元信息:
- "_trace_id":一次请求的追踪 ID
- "_executed_tools":本次执行过的工具列表
"""

# ============================
# 说明:如何把 Pandas 能力“产品化”?
#
# 关键不是写脚本,而是把能力拆成可控组件:
# - ToolRegistry:工具注册/权限/超时/调用计数(治理入口)
# - PlanEngine:自然语言 → 结构化计划(保证口径一致)
# - RiskGuard + AuditLogger:高风险阻断 + 审计留痕(可上线)
# - DataAnalyzer:按计划执行工具链,最终产出统一 JSON(可渲染/可导出)
# - QualityOptimizer:基于审计日志做复盘与优化建议(持续迭代)
# - ReportExporter:把结果做成可交付物(HTML 报告)
#
# 使用方式(界面层 main.py 会这么用):
#   facade = DataframeAgentFacade(dashscope_api_key=...)
#   resp = facade.analyze(df, query, human_confirmed=False)
# ============================

import json
import logging
import re
import time
import uuid
from dataclasses import dataclass, asdict, field
from typing import Any, Callable, Dict, List, Optional, Tuple

import numpy as np
import pandas as pd

from langchain_community.chat_models import ChatTongyi

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s — %(message)s",
)
logger = logging.getLogger(__name__)


# ─────────────────────────────────────────────────────────────
# 1. ToolRegistry — Agent 架构与工具链设计
# ─────────────────────────────────────────────────────────────
@dataclass
class ToolMeta:
    """工具元数据。

    设计目的:让“工具”从散落的函数,变成可治理的资源。

    字段解释:
    - name: 工具名(会暴露给 LLM 规划器使用)
    - description: 给人读的描述(也会进入 LLM prompt,帮助模型选工具)
    - allowed_roles: 哪些角色能用(用于风控/权限,当前 demo 默认 analyst)
    - timeout_sec: 预留的超时控制(demo 未做强制超时)
    - fn: 真实执行函数(签名约定:fn(df, **args) -> DataFrame | dict | str)
    """
    name: str
    description: str
    allowed_roles: List[str]  # e.g. ["analyst", "admin"]
    timeout_sec: float = 5.0
    fn: Optional[Callable] = None


class ToolRegistry:
    """工具注册中心:统一管理工具元数据、权限与调用入口。

    你可以把它理解为一个“工具商店”:
    - 注册:有哪些工具(ToolMeta)
    - 查询:给定 tool_name 找到工具实现
    - 计数:记录每个工具被调用了多少次(便于监控/优化)
    """

    def __init__(self):
        self._registry: Dict[str, ToolMeta] = {}
        self._call_counts: Dict[str, int] = {}
        self._register_builtin_tools()

    def _register_builtin_tools(self):
        """注册内置工具。

        关键点:
        - 这些工具名会被 PlanEngine 暴露给 LLM,成为“规划”阶段可选 action。
        - 每个工具都绑定一个可执行的 fn(真实跑 Pandas 逻辑)。
        - 工具的参数 schema 由 PlanEngine 的 prompt 约束,避免“模型乱填参数”。
        """
        builtins = [
            ToolMeta("df_shape", "返回数据集行数/列数", ["analyst", "admin"], fn=self._tool_df_shape),
            ToolMeta("head", "返回前 N 行数据", ["analyst", "admin"], fn=self._tool_head),
            ToolMeta("col_mean", "计算某一列的平均值", ["analyst", "admin"], fn=self._tool_col_mean),
            ToolMeta("numeric_means", "计算所有数值列的平均值", ["analyst", "admin"], fn=self._tool_numeric_means),
            ToolMeta("value_counts", "统计某列各类别数量", ["analyst", "admin"], fn=self._tool_value_counts),
            ToolMeta("safe_filter", "按条件过滤数据行", ["analyst", "admin"], fn=self._tool_safe_filter),
            ToolMeta("group_agg", "分组聚合统计", ["analyst", "admin"], fn=self._tool_group_agg),
            ToolMeta("top_n", "按列排序取 TopN 行", ["analyst", "admin"], fn=self._tool_top_n),
            ToolMeta("anomaly_detect", "异常值检测(z-score)", ["analyst", "admin"], fn=self._tool_anomaly_detect),
            ToolMeta("plot_bar", "生成条形图数据", ["analyst", "admin"], fn=self._tool_plot_bar),
            ToolMeta("plot_line", "生成折线图数据", ["analyst", "admin"], fn=self._tool_plot_line),
            ToolMeta("plot_scatter", "生成散点图数据", ["analyst", "admin"], fn=self._tool_plot_scatter),
            ToolMeta("export_report", "导出 HTML/PDF 报告", ["admin"]),
            ToolMeta("delete_rows", "删除数据行(高危)", ["admin"]),
            ToolMeta("update_schema", "修改字段结构(高危)", ["admin"]),
        ]
        for t in builtins:
            self._registry[t.name] = t

    # ----------------------------
    # 内置工具实现(真实执行)
    # 约定:工具返回类型可以是:DataFrame / dict(table|bar|line|scatter) / str
    # ----------------------------
    @staticmethod
    def _tool_df_shape(df: pd.DataFrame, **_: Any) -> Dict[str, Any]:
        """返回行列数(回答类输出)。"""
        return {"answer": f"数据集共有 {df.shape[0]} 行,{df.shape[1]} 列。"}

    @staticmethod
    def _tool_head(df: pd.DataFrame, n: int = 10, **_: Any) -> pd.DataFrame:
        """返回前 N 行(DataFrame 输出,便于后续继续 pipeline)。"""
        n = int(n)
        return df.head(n)

    @staticmethod
    def _tool_col_mean(df: pd.DataFrame, column: str, **_: Any) -> Dict[str, Any]:
        """计算某一列的均值(回答类输出)。

        说明:
        - 会做 to_numeric(coerce) 以兼容字符串数字。
        - 若全为空/全非数值,则返回可解释的文本。
        """
        if column not in df.columns:
            return {"answer": f"列 {column} 不存在。"}
        s = pd.to_numeric(df[column], errors="coerce")
        v = float(s.mean()) if s.notna().any() else None
        return {"answer": f"{column} 列的平均值是 {v:.4f}" if v is not None else f"{column} 列无法计算平均值(非数值或全为空)。"}

    @staticmethod
    def _tool_numeric_means(df: pd.DataFrame, **_: Any) -> Dict[str, Any]:
        """计算所有数值列均值(表格输出)。"""
        num_df = df.select_dtypes(include=["number"])  # type: ignore[arg-type]
        means = num_df.mean(numeric_only=True).to_dict()
        out = pd.DataFrame({"column": list(means.keys()), "mean": list(means.values())})
        return {"table": {"columns": ["column", "mean"], "data": out.values.tolist()}}

    @staticmethod
    def _tool_value_counts(df: pd.DataFrame, column: str, top: int = 50, **_: Any) -> pd.DataFrame:
        """类别分布统计(返回 DataFrame)。

        为什么返回 DataFrame:
        - 这样可以和 plot_bar 组合:value_counts -> plot_bar
        - 如果直接返回 table dict,会“断开” pipeline,不利于组合。
        """
        if column not in df.columns:
            raise ValueError(f"列 {column} 不存在。")
        vc = df[column].astype(str).value_counts(dropna=False).head(int(top))
        out = pd.DataFrame({column: vc.index.tolist(), "count": vc.values.tolist()})
        # 返回 DataFrame,便于后续 plot_bar 继续消费
        return out

    @staticmethod
    def _tool_safe_filter(df: pd.DataFrame, expr: str, **_: Any) -> pd.DataFrame:
        """安全过滤(pandas.query)。

        风险说明:
        - query 表达式是字符串,理论上可能被滥用。
        - demo 简化处理:只允许 pandas.query 语法,异常抛给上层。
        - 生产化可加入:表达式白名单/黑名单、最大行数限制等。
        """
        # 只允许 pandas.query 表达式;异常直接抛出给上层。
        return df.query(expr)

    @staticmethod
    def _tool_group_agg(df: pd.DataFrame, by: str, agg: Dict[str, str], **_: Any) -> pd.DataFrame:
        """分组聚合。

        参数:
        - by: 分组列
        - agg: 形如 {"price": "mean", "area": "max"}
        """
        if by not in df.columns:
            raise ValueError(f"group by 列不存在: {by}")
        for col in agg.keys():
            if col not in df.columns:
                raise ValueError(f"聚合列不存在: {col}")
        res = df.groupby(by).agg(agg).reset_index()
        return res

    @staticmethod
    def _tool_top_n(df: pd.DataFrame, n: int = 5, by: Optional[str] = None, ascending: bool = False, **_: Any) -> pd.DataFrame:
        """按某列排序取 TopN。

        教学要点:
        - LLM 常把“最高/最大/TopN”映射到这个工具
        - by 会尽量 to_numeric 做数值排序(price 这类列)
        """
        n = int(n)
        if by and by in df.columns:
            key = pd.to_numeric(df[by], errors="coerce")
            tmp = df.copy()
            tmp["__sort_key__"] = key
            tmp = tmp.sort_values("__sort_key__", ascending=bool(ascending), na_position="last")
            tmp = tmp.drop(columns=["__sort_key__"])
            return tmp.head(n)
        return df.head(n)

    @staticmethod
    def _tool_anomaly_detect(df: pd.DataFrame, column: str, z_threshold: float = 3.0, top: int = 20, **_: Any) -> Dict[str, Any]:
        """异常值检测:z-score。

        输出:table(异常行)
        """
        if column not in df.columns:
            return {"answer": f"列 {column} 不存在。"}
        s = pd.to_numeric(df[column], errors="coerce")
        mu = s.mean()
        sigma = s.std(ddof=0)
        if sigma == 0 or np.isnan(sigma):
            return {"answer": f"列 {column} 无法进行异常检测(方差为 0 或无有效数值)。"}
        z = (s - mu) / sigma
        mask = z.abs() >= float(z_threshold)
        out = df.loc[mask].copy().head(int(top))
        return {"table": {"columns": [str(c) for c in out.columns], "data": out.astype(object).where(pd.notnull(out), None).values.tolist()}}

    @staticmethod
    def _tool_plot_bar(df: pd.DataFrame, x: str, y: str = "count", **_: Any) -> Dict[str, Any]:
        """生成条形图数据(bar)。

        约定:
        - 输入 df 通常来自 value_counts 或 group_agg
        - 输出结构为 {"bar": {"columns": [...], "data": [...]}}
        """
        # 约定:传入的 df 通常是 value_counts 的结果(两列:x + count)或 group_agg 的结果
        if x not in df.columns:
            raise ValueError(f"x 列不存在: {x}")
        if y not in df.columns:
            # 允许把第二列当 y
            y = df.columns[1] if len(df.columns) > 1 else y
        out = df[[x, y]].copy()
        return {"bar": {"columns": [str(x), str(y)], "data": out.values.tolist()}}

    @staticmethod
    def _tool_plot_line(df: pd.DataFrame, x: str, y: str, **_: Any) -> Dict[str, Any]:
        """生成折线图数据(line)。

        特殊语义:
        - x="index":使用行号作为 x 轴(当数据集没有时间列时,展示“趋势”常用)
        """
        # 支持 x="index":用行号作为趋势横轴
        if x == "index":
            out = df.copy()
            out["index"] = list(range(len(out)))
        else:
            out = df
        if x not in out.columns or y not in out.columns:
            raise ValueError(f"折线图列不存在: x={x}, y={y}")
        out2 = out[[x, y]].copy()
        return {"line": {"x": str(x), "y": str(y), "columns": [str(x), str(y)], "data": out2.values.tolist()}}

    @staticmethod
    def _tool_plot_scatter(df: pd.DataFrame, x: str, y: str, **_: Any) -> Dict[str, Any]:
        """生成散点图数据(scatter)。

        特殊语义:
        - x="index":使用行号作为 x 轴
        """
        if x == "index":
            out = df.copy()
            out["index"] = list(range(len(out)))
        else:
            out = df
        if x not in out.columns or y not in out.columns:
            raise ValueError(f"散点图列不存在: x={x}, y={y}")
        out2 = out[[x, y]].copy()
        return {"scatter": {"x": str(x), "y": str(y), "columns": [str(x), str(y)], "data": out2.values.tolist()}}

    def register(self, tool: ToolMeta):
        self._registry[tool.name] = tool
        logger.info(f"[ToolRegistry] 注册工具: {tool.name}")

    def get(self, name: str) -> Optional[ToolMeta]:
        return self._registry.get(name)

    def list_tools(self, role: str = "analyst") -> List[str]:
        return [n for n, t in self._registry.items() if role in t.allowed_roles]

    def record_call(self, name: str):
        self._call_counts[name] = self._call_counts.get(name, 0) + 1

    def call_stats(self) -> Dict[str, int]:
        return dict(self._call_counts)


# ─────────────────────────────────────────────────────────────
# 2. PlanEngine — 自然语言转结构化计划
# ─────────────────────────────────────────────────────────────
@dataclass
class AnalysisPlan:
    trace_id: str
    intent: str
    steps: List[Dict[str, Any]]
    fields: List[str]
    output_type: str  # answer / table / bar / line / scatter
    export_rows: int = 0


class PlanEngine:
    """将自然语言查询转换为结构化 AnalysisPlan,再交由 DataAnalyzer 执行。

    这是“Planning”阶段:
    - 输入:df 元信息(列名/类型/样本) + 用户自然语言 query
    - 输出:AnalysisPlan(intent + steps + output_type)

    关键:
    - prompt 里必须给清楚“可用工具列表 + 参数 schema”,否则 LLM 很容易输出不可执行的 steps。
    - 本 demo 要求 LLM **只输出 JSON**,然后用 _safe_parse 做容错解析。
    """

    def __init__(self, model: ChatTongyi, registry: ToolRegistry):
        self.model = model
        self.registry = registry

    def parse(self, df_meta: Dict[str, Any], query: str) -> AnalysisPlan:
        """调用 LLM 生成结构化计划。

        返回的计划必须满足:
        - steps: [{"tool": "工具名", "args": {...}}]
        - output_type: answer/table/bar/line/scatter

        注意:
        - 这里是 100% 强制走 LLM(按你的要求已移除规则兜底)。
        - 课堂演示时如果 API Key 无效,会在这里直接抛错。
        """
        allowed = self.registry.list_tools(role="analyst")
        prompt = f"""你是数据分析规划师。请将用户问题拆解为结构化 JSON 执行计划,并且严格使用下方工具与参数约定。

DataFrame 信息:
- 形状: {df_meta['shape']}
- 列名: {df_meta['columns']}
- 字段类型: {df_meta['dtypes']}
- 样本(15行): {df_meta['sample']}

可用工具: {allowed}

工具参数约定(非常重要,必须严格遵守;只输出 JSON,不要解释):
1) df_shape
   - args: {{}}
   - 用途:回答“多少行多少列”
2) head
   - args: {{"n": 10}}
   - 用途:显示前 N 行
3) col_mean
   - args: {{"column": "price"}}
   - 用途:某列平均值
4) numeric_means
   - args: {{}}
   - 用途:所有数值列的平均值(输出 table)
5) value_counts
   - args: {{"column": "furnishingstatus", "top": 50}}
   - 用途:类别分布统计(输出 table,列为 column + count)
6) safe_filter
   - args: {{"expr": "price > 5000000 and area >= 6000"}}
   - 用途:按条件过滤(pandas query 语法)
7) group_agg
   - args: {{"by": "furnishingstatus", "agg": {{"price": "mean"}}}}
   - 用途:分组聚合(agg 的 value 只能是: mean|sum|min|max|count)
8) top_n
   - args: {{"n": 5, "by": "price", "ascending": false}}
   - 用途:按列排序取 TopN(最高/最大 ascending=false;最低/最小 ascending=true)
9) plot_bar
   - args: {{"x": "furnishingstatus", "y": "count"}}
   - 用途:将当前 DataFrame 转为 bar 图数据
10) plot_line
   - args: {{"x": "index", "y": "price"}} 或 {{"x": "date", "y": "price"}}
   - 用途:折线图(必须确保 x/y 在当前 DataFrame 中存在)
11) plot_scatter
   - args: {{"x": "area", "y": "price"}}
   - 用途:散点图

用户问题: {query}

输出格式(仅返回 JSON,不要其他文字):
{{
  "intent": "一句话概括用户意图",
  "steps": [
    {{"tool": "工具名", "args": {{"参数名": "参数值"}}}}
  ],
  "fields": ["涉及字段列表"],
  "output_type": "answer|table|bar|line|scatter",
  "export_rows": 0
}}"""
        raw = self.model.invoke(prompt).content
        data = self._safe_parse(raw)
        return AnalysisPlan(
            trace_id="t-" + uuid.uuid4().hex[:8],
            intent=data.get("intent", query),
            steps=data.get("steps", []),
            fields=data.get("fields", []),
            output_type=data.get("output_type", "answer"),
            export_rows=data.get("export_rows", 0),
        )

    @staticmethod
    def _safe_parse(raw: str) -> Dict[str, Any]:
        """尽可能把模型输出解析成 dict。

        兼容几类常见异常输出:
        - 正常 JSON:{"a":1}
        - JSON 外围带杂字符:xxx {...} yyy
        - 多个 JSON 拼接:{"answer":"..."}{"table":{...}}
        """
        if not isinstance(raw, str):
            return {}

        s = raw.strip()
        if not s:
            return {}

        # 1) 直接尝试解析
        try:
            obj = json.loads(s)
            return obj if isinstance(obj, dict) else {}
        except Exception:
            pass

        # 2) 尝试从文本中抽取第一个 {...}
        m = re.search(r"\{.*\}", s, re.DOTALL)
        if m:
            blob = m.group()
            try:
                obj = json.loads(blob)
                return obj if isinstance(obj, dict) else {}
            except Exception:
                pass

        # 3) 解析“多个 JSON 对象拼接”的情况:{...}{...}
        #    用 JSONDecoder.raw_decode 逐段解析并 merge
        dec = json.JSONDecoder()
        i = 0
        merged: Dict[str, Any] = {}
        while i < len(s):
            # 找到下一个 '{'
            j = s.find('{', i)
            if j == -1:
                break
            try:
                obj, end = dec.raw_decode(s, j)
                if isinstance(obj, dict):
                    merged.update(obj)
                i = end
            except Exception:
                i = j + 1
        return merged


# ─────────────────────────────────────────────────────────────
# 3. RiskGuard — 工具治理与安全边界
# ─────────────────────────────────────────────────────────────
HIGH_RISK_TOOLS = {"export_report", "delete_rows", "update_schema"}
SENSITIVE_FIELDS = {"phone", "id_card", "salary", "password", "bank_account"}
LARGE_EXPORT_ROWS = 1000


class RiskGuard:
    """评估计划风险等级;阻断未授权的高危操作。"""

    def assess(self, plan: AnalysisPlan) -> Tuple[str, List[str]]:
        factors: List[str] = []
        tools_used = {s.get("tool", "") for s in plan.steps}
        fields_used = set(plan.fields)

        if tools_used & HIGH_RISK_TOOLS:
            factors.append("high_risk_tools=" + ",".join(sorted(tools_used & HIGH_RISK_TOOLS)))
        if fields_used & SENSITIVE_FIELDS:
            factors.append("sensitive_fields=" + ",".join(sorted(fields_used & SENSITIVE_FIELDS)))
        if plan.export_rows >= LARGE_EXPORT_ROWS:
            factors.append(f"large_export_rows={plan.export_rows}")

        level = "high" if factors else "low"
        return level, factors

    def validate_tools(self, plan: AnalysisPlan, registry: ToolRegistry) -> List[str]:
        """返回计划中未注册的工具名(空列表表示合法)。"""
        all_tools = set(registry._registry.keys())
        used = {s.get("tool", "") for s in plan.steps}
        return sorted(used - all_tools)


# ─────────────────────────────────────────────────────────────
# 4. AuditLogger — 人机协同审核 + 审计日志
# ─────────────────────────────────────────────────────────────
@dataclass
class AuditEvent:
    trace_id: str
    status: str  # pending / approved / cancelled / executed / failed
    risk_level: str
    risk_factors: List[str]
    intent: str
    executed_tools: List[str] = field(default_factory=list)
    result_keys: List[str] = field(default_factory=list)
    ts: float = field(default_factory=time.time)


class AuditLogger:
    """记录每次分析的审核状态与执行结果,支持复盘与质量回顾。"""

    def __init__(self):
        self._log: List[AuditEvent] = []

    def record(self, event: AuditEvent):
        self._log.append(event)
        logger.info(f"[Audit] {event.trace_id} status={event.status} risk={event.risk_level}")

    def all_events(self) -> List[Dict[str, Any]]:
        return [asdict(e) for e in self._log]

    def cancelled_count(self) -> int:
        return sum(1 for e in self._log if e.status == "cancelled")

    def executed_count(self) -> int:
        return sum(1 for e in self._log if e.status == "executed")


# ─────────────────────────────────────────────────────────────
# 5. DataAnalyzer — 数据分析能力集成(ReAct 执行层)
# ─────────────────────────────────────────────────────────────
class DataAnalyzer:
    """依据 AnalysisPlan 逐步执行工具调用,返回最终 JSON 结果。"""

    def __init__(self, model: ChatTongyi, registry: ToolRegistry):
        self.model = model
        self.registry = registry

    def execute(self, plan: AnalysisPlan, df: pd.DataFrame, df_meta: Dict[str, Any]) -> Dict[str, Any]:
        """执行计划(Execute 阶段)。

        核心机制:DataFrame pipeline
        - work_df:当前工作 DataFrame(初始为原始 df)
        - 每一步调用一个工具:out = tool.fn(work_df, **args)
        - 如果 out 是 DataFrame:更新 work_df(用于下一步继续处理)
        - 如果 out 是 dict/str:视为“结构化结果”或“文本答案”,不更新 work_df

        最终结果的选择规则:
        - 如果最后一次工具输出是 dict 且包含 answer/table/bar/line/scatter,直接作为最终结果
        - 否则按 plan.output_type 做默认输出(通常是把 work_df 转 table)

        这样设计的好处:
        - LLM 只负责选工具/传参,不负责“编数据”
        - 工具链可组合(例如 value_counts -> plot_bar)
        - 输出结构稳定,渲染层简单
        """
        executed_tools: List[str] = []
        work_df: pd.DataFrame = df
        last_obj: Any = None

        def _df_to_table(d: pd.DataFrame, limit: int = 200) -> Dict[str, Any]:
            dd = d.head(limit)
            cols = [str(c) for c in dd.columns]
            data = dd.astype(object).where(pd.notnull(dd), None).values.tolist()
            return {"table": {"columns": cols, "data": data}}

        for step in plan.steps:
            tool_name = step.get("tool", "")
            args = step.get("args", {}) or {}
            meta = self.registry.get(tool_name)
            if meta is None or meta.fn is None:
                logger.warning(f"[DataAnalyzer] 跳过未注册/无实现工具: {tool_name}")
                continue

            self.registry.record_call(tool_name)
            executed_tools.append(tool_name)
            logger.info(f"[DataAnalyzer] 执行工具: {tool_name} args={args}")

            # 统一调用:工具默认以当前 work_df 为输入
            out = meta.fn(work_df, **args)
            last_obj = out

            # pipeline 规则:返回 DataFrame 则更新 work_df;返回 dict/str 则不更新
            if isinstance(out, pd.DataFrame):
                work_df = out

        # 组装最终结果(只要是 dict 且含结构化 key,就直接返回)
        result: Dict[str, Any]
        if isinstance(last_obj, dict) and any(k in last_obj for k in ("answer", "table", "bar", "line", "scatter")):
            result = dict(last_obj)
        elif isinstance(last_obj, str):
            result = {"answer": last_obj}
        else:
            # 根据 output_type 决定默认输出
            if plan.output_type in ("bar", "line", "scatter"):
                # 若 LLM 没输出 plot_*,兜底为 table(避免前端空白)
                result = _df_to_table(work_df)
            elif plan.output_type == "table":
                result = _df_to_table(work_df)
            else:
                result = {"answer": "已完成分析,但未生成可展示的结构化结果。"}

        result["_executed_tools"] = executed_tools
        result["_trace_id"] = plan.trace_id
        return result

    def _call_llm_for_result(self, plan: AnalysisPlan, df_meta: Dict[str, Any]) -> Dict[str, Any]:
        prompt = f"""你是数据分析执行助手。数据已加载为 Pandas DataFrame。

DataFrame 信息:
- 形状: {df_meta['shape']}
- 列名: {df_meta['columns']}
- 字段类型: {df_meta['dtypes']}
- 样本(15行): {df_meta['sample']}

分析意图: {plan.intent}
执行步骤: {json.dumps(plan.steps, ensure_ascii=False)}
期望输出类型: {plan.output_type}

输出格式(仅返回 JSON,不要其他文字):
- 文字回答: {{"answer": "答案文字"}}
- 表格:     {{"table": {{"columns": ["列1","列2"], "data": [[v1,v2]]}}}}
- 条形图:   {{"bar":   {{"columns": ["类别","值"], "data": [["A",10]]}}}}
- 折线图:   {{"line":  {{"x":"x轴真实列名","y":"y轴真实列名","columns":["x轴列名","y轴列名"],"data":[[x,y]]}}}}
- 散点图:   {{"scatter":{{"x":"x轴真实列名","y":"y轴真实列名","columns":["x轴列名","y轴列名"],"data":[[x,y]]}}}}

重要:columns 中必须填写数据集中真实存在的列名,不能用 "x"/"y" 等占位符。
只返回 JSON,不要代码,不要解释。"""
        raw = self.model.invoke(prompt).content
        # LLM 偶发会返回被转义的 JSON(例如 {\"table\":...} 或 "{...}"),这里做一次反转义/二次解析兜底
        raw_str = raw.strip() if isinstance(raw, str) else str(raw)
        result = PlanEngine._safe_parse(raw_str)
        if not result and raw_str.startswith('"') and raw_str.endswith('"'):
            try:
                inner = json.loads(raw_str)
                if isinstance(inner, str):
                    result = PlanEngine._safe_parse(inner)
            except Exception:
                pass
        if not result and raw_str.startswith("{\\\""):
            result = PlanEngine._safe_parse(raw_str.replace('\\"', '"'))
        result = result or {"answer": raw_str}
        # 若同时返回了 answer + 结构化数据,删除 answer 避免展示层渲染原始 JSON
        if "answer" in result and any(k in result for k in ("table", "bar", "line", "scatter")):
            result.pop("answer")
        return result


# ─────────────────────────────────────────────────────────────
# 6. QualityOptimizer — 数据复盘与质量优化
# ─────────────────────────────────────────────────────────────
class QualityOptimizer:
    """分析审计日志,输出高频错误类型和优化建议。"""

    def __init__(self, audit: AuditLogger):
        self.audit = audit

    def suggest(self) -> List[str]:
        events = self.audit.all_events()
        if not events:
            return ["暂无分析记录,无法给出建议。"]
        suggestions: List[str] = []
        failed = [e for e in events if e["status"] == "failed"]
        cancelled = [e for e in events if e["status"] == "cancelled"]
        if len(cancelled) > 0:
            suggestions.append(f"有 {len(cancelled)} 次因高风险被拦截,建议审查敏感字段权限或降低导出量。")
        if len(failed) > 0:
            suggestions.append(f"有 {len(failed)} 次执行失败,建议检查 Prompt 模板或数据字段名。")
        if not suggestions:
            suggestions.append("执行质量良好,无明显异常。")
        return suggestions


# ─────────────────────────────────────────────────────────────
# 7. ReportExporter — 可视化与报告生成
# ─────────────────────────────────────────────────────────────
class ReportExporter:
    """将分析结果转换为可下载的 HTML 报告字符串。"""

    def export_html(self, query: str, result: Dict[str, Any], audit_events: List[Dict]) -> str:
        rows = ""
        if "answer" in result:
            rows += f"回答:{result['answer']}"
        if "table" in result:
            t = result["table"]
            header = "".join(f"{c}" for c in t.get("columns", []))
            body = "".join(
                "" + "".join(f"{v}" for v in row) + ""
                for row in t.get("data", [])
            )
            rows += f"{header}{body}"
        trace_id = result.get("_trace_id", "N/A")
        tools = ", ".join(result.get("_executed_tools", []))
        return f"""
数析报告

  「数析」智能数据分析报告
  查询:{query}
  Trace ID:{trace_id} | 执行工具:{tools}
  {rows}
  
  审计日志
  {json.dumps(audit_events, ensure_ascii=False, indent=2)}
"""


# ─────────────────────────────────────────────────────────────
# 门面:DataframeAgentFacade — 统一编排 7 大模块
# ─────────────────────────────────────────────────────────────
class DataframeAgentFacade:
    """对外暴露单一入口 analyze(),内部编排全部 7 个模块。"""

    def __init__(self, dashscope_api_key: str):
        # 初始化大模型(生产环境建议加超时/重试/成本控制/熔断,这里略)
        self.model = ChatTongyi(
            model="qwen-turbo",
            dashscope_api_key=dashscope_api_key,
            temperature=0,
        )
        # 注册中心:统一管理工具、权限、超时、调用计数等
        self.registry = ToolRegistry()
        # 计划引擎:把自然语言 query 转成结构化 AnalysisPlan
        self.plan_eng = PlanEngine(self.model, self.registry)
        # 风险守卫:识别高危工具/敏感字段/大导出等
        self.risk = RiskGuard()
        # 审计记录:记录 cancelled/executed/failed
        self.audit = AuditLogger()
        # 执行器:按 plan.steps 调用工具,并最终让模型产出统一 JSON
        self.analyzer = DataAnalyzer(self.model, self.registry)
        # 优化器:基于审计日志输出可操作建议
        self.optimizer = QualityOptimizer(self.audit)
        # 导出器:把结果渲染成 HTML 报告
        self.exporter = ReportExporter()

    def analyze(
            self,
            df,
            query: str,
            human_confirmed: bool = False,
    ) -> Dict[str, Any]:
        import pandas as pd

        df_meta = {
            "shape": df.shape,
            "columns": list(df.columns),
            "dtypes": df.dtypes.astype(str).to_dict(),
            "sample": df.sample(n=min(15, len(df)), random_state=42).to_string(index=False),
        }

        # ① 生成结构化计划(第一阶段:Planning)
        # 说明:这里会调用 LLM(通义千问)来产生可执行的 steps。
        plan = self.plan_eng.parse(df_meta, query)
        logger.info(f"[Facade] Plan: intent={plan.intent} steps={len(plan.steps)}")

        # ② 风险评估:确定是否需要人工确认、是否包含未注册工具
        # - 风险等级 high:包含高危工具/敏感字段/大导出
        # - invalid_tools:LLM 可能输出了不存在的工具名(这里会记录 warning)
        risk_level, factors = self.risk.assess(plan)
        invalid_tools = self.risk.validate_tools(plan, self.registry)
        if invalid_tools:
            logger.warning(f"[Facade] 未注册工具: {invalid_tools}")

        # ③ 人机协同门控:高风险需要人工确认(Human-in-the-Loop)
        if risk_level == "high" and not human_confirmed:
            self.audit.record(AuditEvent(
                trace_id=plan.trace_id,
                status="cancelled",
                risk_level=risk_level,
                risk_factors=factors,
                intent=plan.intent,
            ))
            return {
                "blocked": True,
                "trace_id": plan.trace_id,
                "reason": "high_risk_not_confirmed",
                "risk_factors": factors,
            }

        # ④ 执行分析(第二阶段:Execute)
        # - 真正执行工具链(Pandas 计算)
        # - 写入审计日志(executed/failed)
        try:
            result = self.analyzer.execute(plan, df, df_meta)
            self.audit.record(AuditEvent(
                trace_id=plan.trace_id,
                status="executed",
                risk_level=risk_level,
                risk_factors=factors,
                intent=plan.intent,
                executed_tools=result.get("_executed_tools", []),
                result_keys=list(result.keys()),
            ))
        except Exception as e:
            self.audit.record(AuditEvent(
                trace_id=plan.trace_id,
                status="failed",
                risk_level=risk_level,
                risk_factors=factors,
                intent=plan.intent,
            ))
            raise ValueError(f"执行分析时出错: {e}") from e

        return result

    def export_report(self, query: str, result: Dict[str, Any]) -> str:
        return self.exporter.export_html(query, result, self.audit.all_events())

    def quality_suggestions(self) -> List[str]:
        return self.optimizer.suggest()


# ─────────────────────────────────────────────────────────────
# 向后兼容入口(main.py 仍可直接调用)
# ─────────────────────────────────────────────────────────────
def dataframe_agent(dashscope_api_key: str, df, query: str) -> Dict[str, Any]:
    facade = DataframeAgentFacade(dashscope_api_key)
    return facade.analyze(df, query, human_confirmed=True)

🔍 类职责总结

  • ToolRegistry:工具元数据、权限(allowed_roles)、调用计数统一管理,防止未注册工具被执行。
  • PlanEngine:LLM 两阶段调用——先生成结构化 AnalysisPlan,再由 DataAnalyzer 执行,保证口径一致可复现。
  • RiskGuard:基于高危工具集、敏感字段集、大导出量三维度评级,输出 high/low 及详细 factors。
  • AuditLogger:每次请求写入 AuditEvent(trace_id / status / risk / tools),支持复盘与合规审查。
  • DataAnalyzer:逐步执行 plan.steps,限流熔断(timeout 由 ToolMeta 配置),最终调用 LLM 生成 JSON 结果。
  • QualityOptimizer:扫描审计日志,统计 cancelled/failed 次数,输出可操作的优化建议。
  • ReportExporter:将结果 + 审计日志渲染为完整 HTML 报告,可直接供 Streamlit 下载。

💻 核心模块二:main.py(Streamlit 界面)

界面层直接依赖 DataframeAgentFacade,持久化 facade 实例于 st.session_state,支持人工确认高风险操作、查看审计日志、下载 HTML 报告、质量优化建议四大新功能。

python
"""main.py — 「数析」智能数据分析台 Streamlit 界面

对接 utils.py 的 DataframeAgentFacade,支持:
  - 人工确认高风险操作(Human-in-the-Loop)
  - 审计日志侧边栏查看
  - HTML 报告一键下载
  - 质量优化建议

设计要点:
  - 界面层只负责:数据上传、问题收集、结果渲染、交互控制
  - 业务逻辑全部委托给 DataframeAgentFacade(单一职责)
  - 使用 st.session_state 持久化 facade,保证审计日志与质量建议累积
  - 高风险操作默认阻断,必须用户勾选确认才能执行(合规要求)
"""

# ============================
# 目的:把 utils.py 的“产品化能力”用界面方式交付出来
#
# main.py 只负责三件事:
# 1) 读取/上传数据、收集用户自然语言问题
# 2) 调用 DataframeAgentFacade.analyze(...) 获取统一 JSON 结果
# 3) 根据 JSON 字段渲染:answer/table/bar/line/scatter,并展示审计日志
#
# 关键设计:
# - facade 对象放在 st.session_state,保证多轮分析时:审计日志/质量建议能累积
# - 高风险计划默认阻断,必须用户勾选 human_confirmed 才执行(Human-in-the-Loop)
# ============================

import json
import re

import pandas as pd
import streamlit as st

from utils import DataframeAgentFacade


# ─────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────
def render_chart(input_data: dict, chart_type: str):
    """通用图表渲染:bar / line / scatter。

    参数:
    - input_data: Agent 输出的标准化图表数据(columns/data/x/y/series)
    - chart_type: 图表类型('bar'|'line'|'scatter')

    处理逻辑:
    1) 将 input_data 转为 DataFrame
    2) 解析 x/y/series 列映射(处理 LLM 输出不一致问题)
    3) 根据图表类型调用 Streamlit 对应图表组件
    4) 异常时展示原始数据便于调试
    """
    # 约定:input_data 来自 utils.py 的工具输出,结构大致为:
    # - bar:     {"columns": [x_col, y_col], "data": [[x,y], ...]}
    # - line:    {"x": "真实x列名", "y": "真实y列名", "columns": [...], "data": [[x,y], ...]}
    # - scatter: 同 line
    #
    # 注意:LLM 可能会输出不一致的字段(例如 columns 里写成 ["x","y"] 占位符),
    # 所以这里做了列名解析与兜底渲染,保证页面不“白屏”。
    try:
        # input_data 是标准化的图表数据结构(由 Agent 输出),包含:
        # - columns: 列名
        # - data: 二维数组(行)或一维数组(值)
        # - x/y/series: 可选,指定 x/y 对应的列名
        columns  = input_data.get("columns", [])
        data     = input_data.get("data", [])
        x_col    = input_data.get("x")
        y_col    = input_data.get("y")
        series   = input_data.get("series")

        if not (isinstance(data, list) and data):
            raise ValueError("数据为空")

        if not isinstance(data[0], list):
            df_chart = pd.DataFrame({"值": data}, index=columns)
        else:
            df_chart = pd.DataFrame(data, columns=columns)

        # LLM 有时把 columns 写成 ["x","y"] 字面量,但 x/y 字段才是真实列名
        # 若 x_col/y_col 不在 df 列名中,尝试把对应位置的列重命名
        def resolve_col(col_hint, pos):
            """解析列名:优先使用 col_hint,否则使用位置 pos 的列名。"""
            if col_hint and col_hint in df_chart.columns:
                return col_hint
            if len(df_chart.columns) > pos:
                real = df_chart.columns[pos]
                if col_hint and col_hint != real:
                    df_chart.rename(columns={real: col_hint}, inplace=True)
                return col_hint if col_hint else real
            return None

        x_col = resolve_col(x_col, 0)
        y_col = resolve_col(y_col, 1)
        if series:
            series = resolve_col(series, 2)

        if chart_type == "bar":
            if x_col and y_col and x_col in df_chart.columns:
                st.bar_chart(df_chart.set_index(x_col)[y_col])
            else:
                st.bar_chart(df_chart)
        elif chart_type == "line":
            if x_col and y_col and x_col in df_chart.columns:
                df_sorted = df_chart.sort_values(x_col)
                st.line_chart(df_sorted, x=x_col, y=y_col)
            else:
                st.line_chart(df_chart)
        elif chart_type == "scatter":
            if x_col and y_col and x_col in df_chart.columns:
                st.scatter_chart(df_chart, x=x_col, y=y_col)
            else:
                st.scatter_chart(df_chart)
    except Exception as e:
        st.error(f"图表渲染失败: {e}")
        with st.expander("原始图表数据"):
            st.json(input_data)


def _try_unwrap_json_answer(result: dict) -> dict:
    """若 answer 字段是 JSON 字符串,尝试解析并合并回 result。

    背景:
    - 部分模型会把“最终 JSON”塞进 answer 字段(字符串形式),导致前端无法直接渲染 table/bar/line。
    - 这里做兼容:尽可能把 answer 里的 JSON 解析出来并 merge 回 result。

    处理策略:
    1) 直接解析 answer 字符串
    2) 若被包了一层字符串("{...}"),再解析一次
    3) 若被转义(\"{...}\"),先反转义再解析
    4) 兜底:用正则抽取 {...} 部分再解析
    """
    answer = result.get("answer", "")
    if not isinstance(answer, str):
        return result
    stripped = answer.strip()
    # 兼容 3 类常见输出:
    # 1) 直接 JSON:{"table":{...}}
    # 2) 被包了一层字符串:"{...}"(需要 loads 两次)
    # 3) JSON 被转义:\{"table":{...}\}(需要先反转义再 loads)
    candidates = []
    if stripped:
        candidates.append(stripped)
    if stripped.startswith('"') and stripped.endswith('"'):
        candidates.append(stripped)
    if stripped.startswith("{\\\""):
        candidates.append(stripped.replace('\\"', '"'))

    parsed = None
    for cand in candidates:
        try:
            obj = json.loads(cand)
            # 如果外层解析出来还是字符串,再解析一次
            if isinstance(obj, str) and obj.strip().startswith("{"):
                obj = json.loads(obj)
            if isinstance(obj, dict):
                parsed = obj
                break
        except Exception:
            continue
    if parsed is None:
        # 兜底:从 answer 中抽取 {...} 部分再尝试解析
        m = re.search(r"\{.*\}", stripped, re.DOTALL)
        if not m:
            return result
        blob = m.group()
        for cand in (blob, blob.replace('\\"', '"')):
            try:
                obj = json.loads(cand)
                if isinstance(obj, str) and obj.strip().startswith("{"):
                    obj = json.loads(obj)
                if isinstance(obj, dict):
                    parsed = obj
                    break
            except Exception:
                continue
        if parsed is None:
            return result
    if any(k in parsed for k in ("table", "bar", "line", "scatter", "answer")):
        merged = {k: v for k, v in result.items() if k != "answer"}
        merged.update(parsed)
        return merged
    return result


def render_result(result: dict):
    """统一渲染分析结果(answer / table / bar / line / scatter)。

    处理流程:
    1) 若结果被阻断,展示风险因子并返回
    2) 尝试从 answer 字段解析嵌套 JSON(兼容模型输出问题)
    3) 展示 trace_id 与执行工具信息
    4) 按优先级渲染:answer(仅当无结构化数据)→ table → 图表

    重要说明(教学讲解点):
    - 后端(utils.py)返回的是“结构化 JSON”,而不是直接返回 DataFrame。
    - UI 只做渲染,不做业务计算:这能保证职责清晰、方便测试与复用。
    - 若渲染异常,可以通过展开“原始图表数据”、或查看 Trace ID 对应的审计日志来定位。
    """
    if result.get("blocked"):
        st.error("🚫 高风险操作已被拦截,需人工确认后才能执行。")
        st.json({"risk_factors": result.get("risk_factors", [])})
        return

    # LLM 有时把整个 JSON 塞进 answer 字段,尝试拆解
    result = _try_unwrap_json_answer(result)

    trace_id = result.get("_trace_id", "N/A")
    tools    = result.get("_executed_tools", [])
    st.caption(f"🔍 Trace ID: `{trace_id}` | 执行工具: {', '.join(tools) or '无'}")

    has_visual = any(k in result for k in ("table", "bar", "line", "scatter"))

    # 仅当没有结构化数据时才展示文字回答,避免把 JSON 字符串当文字渲染
    if "answer" in result and not has_visual:
        st.success(result["answer"])

    if "table" in result:
        t = result["table"]
        st.dataframe(pd.DataFrame(t["data"], columns=t["columns"]), use_container_width=True)

    for chart_type in ("bar", "line", "scatter"):
        if chart_type in result:
            st.subheader({"bar": "📊 条形图", "line": "📈 折线图", "scatter": "🔵 散点图"}[chart_type])
            render_chart(result[chart_type], chart_type)


# ─────────────────────────────────────────────
# 页面配置
# ─────────────────────────────────────────────
# 配置说明:
# - page_title: 浏览器标签页标题
# - page_icon: 标签页图标(emoji)
# - layout: 'wide' 让内容占满屏幕宽度(适合表格/图表展示)
# ─────────────────────────────────────────────
st.set_page_config(
    page_title="「数析」智能数据分析台",
    page_icon="📊",
    layout="wide",
)
st.title("📊 「数析」智能数据分析台")
st.markdown("基于**通义千问 + 7 大核心能力架构**的智能数据分析平台。")

# ─────────────────────────────────────────────
# 侧边栏
# ─────────────────────────────────────────────
# 侧边栏功能:
# 1) API Key 配置(必填,用于调用通义千问)
# 2) 安全设置:人工确认高风险操作(Human-in-the-Loop)
# 3) 审计日志:展示最近 5 条记录(状态/trace_id/意图)
# 4) 质量建议:基于审计日志生成优化建议
# ─────────────────────────────────────────────
with st.sidebar:
    st.header("⚙️ 配置")
    api_key = st.text_input("通义千问 API 密钥", type="password",
                             help="阿里云 DashScope 控制台获取")
    st.markdown("[获取 API Key](https://dashscope.console.aliyun.com/apiKey)")

    # 人机协同:是否手动确认高风险操作
    st.divider()
    st.subheader("🛡️ 安全设置")
    human_confirmed = st.checkbox(
        "我已确认,允许执行高风险操作",
        value=False,
        help="涉及敏感字段、高危工具或大批量导出时,需勾选此项才能执行。",
    )

    # 审计日志
    st.divider()
    st.subheader("📋 审计日志")
    if "facade" in st.session_state:
        events = st.session_state["facade"].audit.all_events()
        st.caption(f"共 {len(events)} 条记录")
        for ev in events[-5:]:
            color = "🟢" if ev["status"] == "executed" else ("🔴" if ev["status"] == "cancelled" else "🟡")
            st.markdown(f"{color} `{ev['trace_id']}` {ev['status']}{ev['intent'][:20]}")
    else:
        st.caption("暂无记录")

    # 质量优化建议
    st.divider()
    st.subheader("💡 质量建议")
    if st.button("生成优化建议") and "facade" in st.session_state:
        for tip in st.session_state["facade"].quality_suggestions():
            st.info(tip)

# ─────────────────────────────────────────────
# 主区域:文件上传
# ─────────────────────────────────────────────
# 功能说明:
# - 支持 CSV 文件上传(类型限制)
# - 上传后展示:行数、列数、文件大小、数据预览
# - 数据存入 st.session_state.df,供后续分析使用
# ─────────────────────────────────────────────
st.header("📁 数据上传")
uploaded = st.file_uploader("上传 CSV 数据文件", type="csv")

if uploaded:
    try:
        st.session_state["df"] = pd.read_csv(uploaded)
        df = st.session_state["df"]
        c1, c2, c3 = st.columns(3)
        c1.metric("行数", df.shape[0])
        c2.metric("列数", df.shape[1])
        c3.metric("文件大小", f"{uploaded.size / 1024:.1f} KB")
        with st.expander("📊 原始数据预览", expanded=True):
            st.dataframe(df, height=280, use_container_width=True)
    except Exception as e:
        st.error(f"文件读取失败: {e}")

# ─────────────────────────────────────────────
# 主区域:智能查询
# ─────────────────────────────────────────────
# 查询流程:
# 1) 收集用户自然语言问题
# 2) 前置校验:API Key、数据文件、查询内容
# 3) 创建/更新 DataframeAgentFacade(存入 session_state)
# 4) 调用 facade.analyze(df, query, human_confirmed)
# 5) 渲染结果(阻断/成功/失败)
# 6) 若成功,提供 HTML 报告下载
# ─────────────────────────────────────────────
st.header("💬 智能查询")
query = st.text_area(
    "用自然语言描述分析需求:",
    placeholder="例如:近 30 天 GMV 趋势如何? / 哪个渠道转化率最低? / 用散点图展示价格与面积的关系",
    height=90,
)
btn = st.button("🚀 开始分析", type="primary")

if btn:
    if not api_key:
        st.warning("⚠️ 请先输入 API 密钥")
    elif "df" not in st.session_state:
        st.warning("⚠️ 请先上传数据文件")
    elif not query.strip():
        st.warning("⚠️ 请输入查询内容")
    else:
        # 每次点击都重新创建 facade,避免 session_state 缓存旧实例
        st.session_state["facade"]  = DataframeAgentFacade(api_key)
        st.session_state["_api_key"] = api_key

        facade: DataframeAgentFacade = st.session_state["facade"]  # type: ignore[assignment]

        with st.spinner("🤔 AI 正在分析,请稍候…"):
            try:
                result = facade.analyze(
                    st.session_state["df"],
                    query,
                    human_confirmed=human_confirmed,
                )
                st.header("📋 分析结果")
                render_result(result)

                # 报告下载
                if not result.get("blocked"):
                    html_report = facade.export_report(query, result)
                    st.download_button(
                        label="⬇️ 下载 HTML 报告",
                        data=html_report.encode("utf-8"),
                        file_name="analysis_report.html",
                        mime="text/html",
                    )

            except ValueError as e:
                st.error(f"❌ 分析失败:{e}")
                st.info("💡 建议:检查查询是否包含数据中存在的字段名")
            except Exception as e:
                st.error(f"❌ 系统错误:{e}")

🔍 main.py 关键设计点

  • Facade 持久化:DataframeAgentFacade 存入 st.session_state,审计日志跨多次查询累积,支持复盘。
  • Human-in-the-Loop:侧边栏复选框直接控制 human_confirmed,未勾选时高风险计划被阻断并显示风险因子。
  • 统一渲染:render_result() 函数按 JSON Schema 自动分发 answer / table / bar / line / scatter,一套代码兼容全类型输出。
  • 报告下载:调用 facade.export_report() 生成含审计日志的完整 HTML,一键下载。
  • 质量建议:侧边栏"生成优化建议"按钮调用 facade.quality_suggestions(),基于当前会话审计日志给出可操作建议。

🚀 运行项目

1. 启动应用

text
# 启动 Streamlit:本地会默认打开浏览器,端口通常是 8501
streamlit run main.py

成功启动后,浏览器会自动打开 http://localhost:8501

2. 下载示例数据

📥 示例数据文件

本项目提供了两个示例数据集,你可以下载后直接上传到应用中进行测试:

3. 使用流程

  1. 在侧边栏输入通义千问 API Key
  2. 下载并上传示例数据文件(house_price.csv 或 personal_data.csv)
  3. 在文本框中输入自然语言查询
  4. 点击"生成回答"按钮
  5. 查看 AI 返回的分析结果

4. 示例查询

基础查询

  • 数据集有多少行和多少列?
  • 显示前 10 行数据
  • price 列的平均值是多少?

统计分析

  • 计算所有数值列的平均值
  • 找出价格最高的 5 条记录
  • 统计 furnishingstatus 各个类别的数量

数据可视化

  • 用条形图展示 furnishingstatus 各类别的数量分布
  • 用折线图展示价格趋势
  • 用散点图展示价格和面积的关系
📊 原始数据示例(house_price.csv)
数据字段:price,area,bedrooms,bathrooms,stories,mainroad,guestroom,basement,hotwaterheating,airconditioning,parking,prefarea,furnishingstatus
示例用途:以下两个查询基于此数据集,展示平台的数据分析能力

💡 Prompt 工程详解

本项目的核心在于 Prompt 设计,它决定了 AI 能否准确理解需求并返回正确格式的结果。

🎯 Prompt 设计原则

  • 明确角色定位:"你是数据分析助手"
  • 提供上下文信息:DataFrame 的形状、列名、字段类型(dtypes)以及少量随机样本
  • 明确输出格式:只返回 JSON,不要其他文字
  • 分步骤引导:1. 分析问题 → 2. 生成代码 → 3. 返回 JSON
  • 给出示例:展示 answer / table / bar 等格式

🎨 扩展建议

  • 支持更多文件格式:Excel、JSON、Parquet
  • 添加更多图表类型:饼图、热力图、箱线图
  • 实现数据导出功能:下载分析结果为 CSV
  • 添加数据预处理:缺失值处理、异常值检测
  • 支持多数据集对比:上传多个文件进行对比分析
  • 切换到其他大模型:OpenAI GPT-4、文心一言等

🔥 项目难点与亮点深度剖析

面试中,面试官最关注的不是"你做了什么",而是"你解决了什么难题、为什么这样设计"。 以下是本项目中最值得深挖的 6 大亮点,每一个都能在面试中展开讲 3-5 分钟。

亮点 1 ReAct Agent 多步推理 — 让 AI "会做事"而不是"只会说"
难点:传统方式是用户问一句、AI 答一句,无法处理"先查字段→再分组统计→再画图→最后给结论"这样的连续决策任务。单次调用大模型无法完成多步骤数据分析。
方案:基于 LangChain Agent 实现 ReAct(Reasoning + Acting)循环:Thought → Action → Observation → Answer。每一步都有工具调用记录,可追踪、可复现。
面试话术:"我们没有让模型一次性回答,而是设计了基于 ReAct 的 Agent 循环:模型先生成分析计划,然后逐步调用预定义工具(schema 检查 → 指标计算 → 图表生成),每一步的输入输出都记录在 trace 中,保证结果可复现、可审计。"
亮点 2 Prompt 工程 — 结构化输出 + 多级容错解析
难点:大模型返回的结果格式不可控——有时返回纯文本、有时混入 Markdown 代码块、有时 JSON 格式不规范。直接 json.loads() 经常报错,导致前端无法渲染。
方案:设计了三级容错解析策略:① 直接 JSON 解析 → ② 正则提取 JSON 片段 → ③ 兜底返回纯文本。同时在 Prompt 中用"角色定位 + 上下文注入 + 输出格式约束 + Few-shot 示例"四重约束,将格式正确率从 ~60% 提升到 ~95%。
面试话术:"大模型返回格式不稳定是最常见的工程问题。我设计了三级容错解析机制,并在 Prompt 层用角色定位、上下文注入、格式约束、Few-shot 示例四重手段控制输出,最终将结构化输出成功率从 60% 提升到 95%。"
亮点 3 工具白名单 + 安全沙箱 — 防止 LLM 执行危险代码
难点:如果让大模型直接生成并执行任意 Python 代码,存在严重安全风险:执行 os.system()、读写本地文件、死循环耗尽资源、SQL 注入等。线上环境不可能这样做。
方案:将数据分析能力封装为预定义工具函数(groupby_sum、top_n、plot_trend 等),模型只能"选择调用哪个工具 + 传什么参数",不能执行任意代码。加上参数白名单校验 + 输入注入检测(拦截 import os 等恶意指令)。
面试话术:"我们没有让 LLM 直接执行代码,而是采用工具白名单机制:把所有分析能力封装成预定义工具,模型只能选择工具和传参。同时加了输入注入检测,比如用户在查询中夹带 import os 会被直接拦截,保证了生产环境的安全性。"
亮点 4 人机协同审核 — 高风险操作"确认再执行"
难点:全自动的 AI 分析虽然效率高,但涉及敏感数据导出、大范围删除、跨表关联等高风险操作时,一旦出错损失不可逆。纯自动化在企业场景中不可接受。
方案:引入风险评估机制:Agent 生成计划后,系统自动评估风险等级(low/medium/high)。高风险操作暂停执行,展示完整计划和影响范围,等待用户确认。拒绝确认则整个工具链不执行,且写入审计日志。
面试话术:"我设计了 Human-in-the-Loop 机制:Agent 生成分析计划后会自动评估风险等级,高风险操作(如全量导出、敏感字段访问)必须经过人工确认才能执行,拒绝后工具链不调用且写入审计日志,实现了自动化与安全性的平衡。"
亮点 5 全链路 Trace — 错误可定位、性能可分析、结果可复现
难点:AI 应用最大的痛点是"黑盒":出了错不知道错在哪一步,性能慢不知道慢在哪个环节,同样的问题两次回答不一样无法复现。这导致线上问题排查极其困难。
方案:每次分析生成唯一 trace_id,记录完整链路:query → plan → 每步工具调用(名称、参数、耗时、成功/失败)→ 最终输出。支持按 trace_id 回放,快速定位慢点和错点。
面试话术:"为了解决 AI 应用的黑盒问题,我设计了全链路 Trace 机制:每次分析生成唯一 trace_id,记录 query、计划、每步工具调用的参数和耗时。线上出问题可以直接按 trace_id 回放整个分析过程,快速定位是 Prompt 问题、模型问题还是数据问题。"
亮点 6 多格式自适应渲染 — 一个查询多种输出形态
难点:用户的一次查询可能需要同时返回文字结论 + 数据表格 + 可视化图表,而且数据格式多变(一维数组、二维矩阵、键值对),前端需要智能适配不同的渲染方式。
方案:设计了统一的 JSON Schema 响应协议(answer/table/bar/line/scatter),前端根据返回字段自动选择渲染组件。针对图表数据,实现了一维/二维数据格式的自动检测和转换,一套代码兼容多种数据结构。
面试话术:"我设计了统一的 JSON Schema 响应协议,模型返回的 JSON 中可以同时包含 answer、table、bar、line 等字段,前端根据字段类型自动选择渲染组件。图表数据还做了一维/二维格式自适应,一套代码兼容多种数据结构。"

⚡ 面试高频追问 & 参考回答

Q:为什么用 Agent 而不是直接调用大模型 API?
A:直接调用只能做单轮问答。数据分析是多步决策过程(先理解数据结构→确定分析方法→执行计算→生成图表→输出结论),Agent 的 ReAct 循环能自动拆解步骤、选择工具、迭代优化,适合这类连续决策场景。
Q:大模型返回格式不稳定怎么办?
A:三层策略。第一层 Prompt 约束:角色定位 + 格式示例 + "只返回 JSON"强指令。第二层解析容错:直接解析 → 正则提取 → 兜底文本。第三层监控告警:统计格式失败率,超过阈值触发 Prompt 优化。
Q:如何防止 Prompt 注入攻击?
A:三道防线。① 输入层:正则过滤 import / exec / eval / os.system 等危险关键词。② 工具层:白名单机制,模型只能调用预定义工具,不能执行任意代码。③ 输出层:对模型返回的内容做 JSON Schema 校验,拒绝不合规的输出。
Q:这个项目和 ChatGPT 的 Code Interpreter 有什么区别?
A:Code Interpreter 是通用沙箱执行任意代码,适合探索式分析。我们的方案是工具白名单 + 结构化计划 + 审计追踪,更适合企业场景:可控、可审计、可复现、有权限管理。两者定位不同,我们偏向生产级数据分析平台。

📄 简历撰写案例(可直接复制粘贴)

以下是根据本项目整理的简历项目经历,按照大厂简历标准格式编写,突出技术深度和业务价值。 可直接复制到简历中,根据个人情况微调即可。

可直接复制
「数析」智能数据分析平台
技术栈:Python / LangChain Agent / 通义千问 / Streamlit / Pandas / Matplotlib
项目描述:
基于 LangChain Agent + 通义千问大模型构建的企业级智能数据分析平台。用户上传 CSV 数据后,通过自然语言描述分析需求,系统自动完成数据提取、统计分析、可视化图表生成及分析报告导出,支持多步推理、工具治理和人机协同审核。
核心职责与成果:
  • 基于 LangChain Agent 框架设计 ReAct(Reasoning + Acting)推理循环,实现自然语言 → 结构化分析计划 → 多工具链式调用的自动化数据分析流程,支持 schema 检查、指标计算、图表生成等 6 类预定义工具
  • 设计三级容错 JSON 解析策略(直接解析 → 正则提取 → 兜底文本)+ Prompt 四重约束(角色定位 / 上下文注入 / 格式约束 / Few-shot 示例),将大模型结构化输出成功率从 ~60% 提升至 ~95%
  • 实现工具白名单 + 参数校验 + 输入注入检测机制,拦截 import / exec / os.system 等危险指令,杜绝 LLM 执行任意代码的安全风险
  • 引入 Human-in-the-Loop 审核机制,Agent 生成计划后自动评估风险等级,高风险操作(全量导出、敏感字段访问)需人工确认,拒绝后工具链不执行并写入审计日志
  • 设计全链路 Trace 机制(trace_id → query → plan → 工具调用链 → 耗时),支持按 trace_id 回放分析过程,线上问题定位效率提升 80%
  • 设计统一 JSON Schema 响应协议(answer / table / bar / line / scatter),前端根据字段类型自适应渲染,支持一维 / 二维数据格式自动转换
  • 使用 Streamlit 构建数据分析工作台,实现 CSV 上传 → 自然语言交互 → 图表展示 → 报告下载的完整闭环
✓ 已复制到剪贴板!

💡 简历撰写技巧

  • STAR 法则:每条经历包含 Situation(背景)→ Task(任务)→ Action(你做了什么)→ Result(量化成果)
  • 量化数据:尽量加数字,如"成功率从 60% 提升到 95%"、"问题定位效率提升 80%"、"支持 6 类预定义工具"
  • 技术深度:不要写"使用了 LangChain",要写"基于 ReAct 推理循环设计 Agent 架构"——体现你理解原理
  • 避免堆砌:不要罗列技术名词,要写"用这个技术解决了什么问题"
  • 差异化:重点突出安全机制、Trace 链路、人机协同这些"别人没有"的亮点
精简版 · 适合简历空间有限
「数析」智能数据分析平台
Python / LangChain Agent / 通义千问 / Streamlit / Pandas
  • 基于 LangChain Agent 实现 ReAct 多步推理,将自然语言查询自动拆解为结构化分析计划并链式调用 6 类预定义工具完成数据分析
  • 设计三级容错解析 + Prompt 四重约束,将大模型结构化输出成功率从 ~60% 提升至 ~95%
  • 实现工具白名单 + 注入检测 + Human-in-the-Loop 审核 + 全链路 Trace 的生产级安全与可观测性体系
  • 使用 Streamlit 构建数据分析工作台,实现 CSV 上传、自然语言交互、多格式图表渲染、报告导出的完整闭环
✓ 已复制到剪贴板!

📝 本章小结

🎓 你完成了「数析」智能数据分析台的7大核心能力

  • 1、Agent 架构设计:基于 LangChain Agent 掌握 ReAct 核心循环,定义数据分析工具集。
  • 2、数据分析能力集成:使用 Pandas 进行数据清洗、统计分析、可视化。
  • 3、自然语言转数据分析:实现用户用自然语言描述需求,AI 自动拆解完整工作流。
  • 4、工具治理与安全边界:建立工具注册中心、权限控制、审计日志、限流熔断。
  • 5、人机协同审核机制:设置复杂分析任务的人工确认机制,自动同步分析上下文。
  • 6、数据复盘与质量优化:自动采集分析日志,分析高频错误,输出优化建议。
  • 7、可视化与报告生成:基于 Matplotlib/Plotly 生成多样化图表,支持报告导出。

🚀 下一步学习

你已掌握「数析」智能数据分析台的开发能力。 接下来,进入阶段 4:「图识」多模态内容识别器, 学习如何将 AI 与图像识别结合,打造多模态内容提取系统。