AI 数据探索 Agent:多轮对话式的数据分析和一次性 SQL 的区别
AI 数据探索 Agent:多轮对话式的数据分析和一次性 SQL 的区别
大家好,我是朱大喜。你有没有这样的感觉:取数不难,难的是"知道自己该取什么数"。今天聊聊 AI Agent 怎么把"一次性 SQL"升级为"多轮对话式探索",让数据分析从"人适应工具"变成"工具理解人"。
一、"帮我查一下"和"帮我想一下"的区别
传统数据分析的典型流程是这样的:
老板:最近用户留存怎么样?查一下。 分析师:好的。(写 SQL) SELECT dt, retention_d7 FROM dws.user_retention_daily WHERE dt BETWEEN '20260701' AND '20260712' ORDER BY dt; 留存率 58%...62%...55%...咦? 老板:有什么异常吗? 分析师:(再写 SQL) SELECT channel, retention_d7 FROM dws.user_retention_daily WHERE dt = '20260712' GROUP BY channel; ANDROID 62%, IOS 48%!iOS 留存掉了! 老板:为什么? 分析师:(再写 SQL) SELECT * FROM dwd.app_crash_log WHERE dt = '20260712' AND platform = 'IOS' ...这个过程里,每一步的分析都依赖于上一步的结果。传统 SQL 是一次买卖,Agent 是一段对话。
为什么传统模式的上下文传递成本比看起来高得多?表面上每次只多写了一行 SQL,但真正消耗精力的是"数据解读的接力"。上一个 SQL 返回了一组数字,人需要消化这些数字("12% 的跌幅,算不算异常?"),然后把它翻译成下一个问题("可能是渠道问题还是产品问题?"),最后再翻译成 SQL。这个"数字→问题→SQL"的翻译循环,每一步都有信息损耗——人会选择性关注某些数据、忽略另一些,然后基于不完整的认知提出下一个问题。Agent 的价值就在于打破了这个人脑转译的瓶颈。
graph LR subgraph 传统模式: 线性SQL A[问题1] --> B[SQL1] C[问题2] --> D[SQL2] E[问题3] --> F[SQL3] B -.->|人工传递上下文| C D -.->|人工传递上下文| E end subgraph Agent模式: 对话式探索 G[用户: 留存怎么样?] --> H[Agent: 查总览] H --> I[Agent自主: 有没有异常?] I --> J[发现 iOS 留存掉了] J --> K[Agent自主: 为什么掉?] K --> L[下钻渠道/版本/地区] L --> M[Agent: 给你分析报告] end style F fill:#faa,stroke:#333 style M fill:#afa,stroke:#333二、Agent 的核心能力:不是会写 SQL,而是会"思考"
数据探索 Agent 和普通 Chat-to-SQL 工具的区别,在于它多了三个能力:
能力一:上下文记忆
Agent 记得你上一轮查了什么,不需要每次都重新描述业务背景:
用户: 查一下上周的 GMV Agent: 上周 GMV 为 3,280 万,环比下降 5.2%。 用户: 按渠道拆一下 Agent: (它知道"按渠道拆"指的是"按渠道拆上周 GMV",不是拆去年的) APP 端 2,150 万(持平),H5 端 680 万(↓18%),小程序端 450 万(↓8%) 用户: H5 为什么降这么多? Agent: (它知道你在追问 H5 下降的原因,自动下钻) 正在分析 H5 端流量来源和转化漏斗... H5 端自然搜索流量减少 32%,投放流量增加但转化率下降。 进一步排查:H5 页面加载时长从 2.1s 增加到 4.8s。 建议:检查 H5 CDN 节点状态。这个对话里,Agent 每一步都在利用前几步构建的上下文——它知道"拆"的是 GMV,"H5"是非 APP 渠道,"为什么降"需要看流量来源和转化漏斗。
为什么上下文记忆不只是"存对话历史"这么简单?如果你只是把前几轮的问题和回答原样塞进 Prompt,LLM 容易陷入"惯性跟随"——它看到你说"H5 为什么降",就会顺着 H5 这个关键词一直往下查,而忽略了可能更重要的横向对比(比如 APP 端虽然 GMV 持平,但转化率也在降,只是被流量增长掩盖了)。好的上下文记忆应该是结构化的分析状态:当前分析到了哪一层、排除了哪些假设、还剩下哪些方向没查、已发现的关键数字是多少。这比纯文本历史更有"分析感"。
能力二:自主探索路径
Agent 不是被动回答问题,而是主动追问自己没有把握的事情:
# Agent 的内部推理逻辑(简化版) class DataExplorationAgent: """ 数据探索 Agent 的核心逻辑 为什么需要"自主探索"而不是"被动等待指令": 大多数人问问题的时候并不清楚自己的分析路径, 他们需要的是一个"会追问"的分析师,而不是一个"哑巴"查询工具 """ def __init__(self, llm_client, sql_executor): self.llm = llm_client self.sql = sql_executor # 对话历史,每次对话都保留 self.conversation_history = [] # 当前分析任务的数据上下文 self.data_context = {} def explore(self, user_question: str) -> str: """ 开始一轮数据探索 不是简单地"翻译成 SQL 然后返回结果", 而是一个"理解 → 查询 → 判断是否足够 → 不够就继续查"的循环 """ # 第一步:理解用户意图 + 规划分析路径 plan = self.llm.plan_analysis(user_question, self.conversation_history) # 第二步:执行查询(可能多轮) for step in plan.steps: # 生成 SQL sql = self.llm.generate_sql( step.intent, self.data_context, # 当前已知的数据上下文 self.conversation_history # 完整对话历史 ) # 执行查询 result = self.sql.execute(sql) # 关键判断:当前结果是否足够回答问题? if self._need_deeper_analysis(result, step.intent): # 不够!自主生成下一步探索 next_step = self._generate_follow_up(result) plan.steps.append(next_step) # 更新数据上下文,让后续步骤知道已有的信息 self.data_context[step.name] = result # 第三步:综合所有探索结果,生成最终回答 return self.llm.synthesize_report(self.data_context, user_question) def _need_deeper_analysis(self, result, intent) -> bool: """ 判断是否需要进一步分析 几个典型触发条件: 1. 结果中有显著异常(如指标突然下跌超过阈值) 2. 结果只是表面现象,没有回答"为什么" 3. 用户原始问题暗示需要多维度分析 """ # 示例:如果发现某个渠道的指标异常偏离 if result.has_anomalous_segment(threshold=0.2): return True # 需要下钻 return False能力三:结果"翻译"能力
Agent 的最终输出不是数字和表格,而是一段有逻辑链条的分析报告:
def format_analysis_report(exploration_result: dict) -> str: """ 把数据探索结果翻译成人类语言 为什么需要翻译: 原始数据是"7月12日 iOS留存率 48%,上月同期 62%" 翻译后是"iOS 留存掉了 14 个百分点,初步判断和两天前的版本更新有关" 后者才是一线业务人员能用的信息 """ summary = f""" ## 数据探索报告 **核心发现**: {exploration_result['core_finding']} **变化趋势**: {exploration_result['trend_summary']} **异常归因**: """ for attr in exploration_result['attributions']: summary += f"- {attr['dimension']}: {attr['explanation']}\n" summary += f""" **建议下一步**: {exploration_result['recommendations']} **数据底稿**: (可以根据以下 SQL 复现本次分析) ```sql {exploration_result['source_sql']}"""
return summary
## 三、Agent 的架构怎么设计?三层结构 ```mermaid graph TB subgraph 用户交互层 A[自然语言问题] --> B[意图理解模块<br/>LLM] end subgraph 规划执行层 B --> C[分析路径规划器] C --> D{路径完备?} D -->|否| E[生成下一步查询] E --> F[SQL生成器] F --> G[执行引擎<br/>Spark/ClickHouse] G --> H[结果解析器] H --> D D -->|是| I[综合分析器] end subgraph 知识底座 J[表结构知识库<br/>字段含义/血缘] K[业务指标字典<br/>GMV=...,留存=...] L[历史分析案例<br/>Query → Report] end F -.-> J H -.-> K I -.-> L I --> M[最终报告] style C fill:#afa,stroke:#333 style I fill:#afa,stroke:#333这个架构的巧妙之处在于规划执行层是一个循环——结果解析器发现信息不够时,会自动回到路径规划器,生成下一轮查询。这个循环一直持续到综合分析器认为:"现在信息足够给一个靠谱的结论了"。
四、落地避坑指南
坑一:SQL 生成不靠谱怎么办?
让 Agent 自己写生产级 SQL 确实有风险。实践中我们加了三层校验:
def safe_execute_sql(generated_sql: str, schema_info: dict) -> dict: """ 安全执行 AI 生成的 SQL 为什么不能直接运行 AI 生成的 SQL: AI 有时候会编字段名、用不存在的表、写天量扫描的查询 """ # 校验一:字段名白名单 for field in extract_fields(generated_sql): if field not in schema_info['known_fields']: # 字段不在表里,尝试模糊匹配 suggestion = fuzzy_match(field, schema_info['known_fields']) return {"error": f"字段 {field} 不存在,你是想查 {suggestion} 吗?"} # 校验二:必须带分区条件 if 'WHERE' not in generated_sql.upper(): return {"error": "查询必须带 WHERE 条件限制分区,请加上 dt = '...'" } # 校验三:预估扫描量 # 为什么限制扫描量:防止一个查询扫全表几百 TB estimated_bytes = estimate_scan_bytes(generated_sql) if estimated_bytes > 10 * 1024**3: # 超过 10GB return {"error": f"预估扫描 {estimated_bytes/1024**3:.1f}GB,请缩小时间范围或加过滤条件"} # 通过校验,安全执行 return execute(generated_sql)坑二:Agent 说"我分析完了"但其实是糊弄
AI 有时候会在信息不足的情况下强行给结论。解决办法是设定"分析完成度"的最低标准:
def check_analysis_completeness(report: dict) -> bool: """ 检查分析是否真的完成了 为什么需要这个检查: LLM 有时会说"根据目前的信息,原因是A", 但其实它根本没有去看 A 相关的数据 """ checks = [ # 每个结论是否都有数据支撑? all(claim.get('evidence_sql') for claim in report['claims']), # 是否有异常但没有归因? not (report['has_anomaly'] and not report['attributions']), # 覆盖的维度是否足够? len(report['dimensions_analyzed']) >= report['min_dimensions'], ] return all(checks)坑三:用户期望 Management 过高
很多人以为 Agent 是"扔进去一个问题,自动出来完整报告"。实际上 Agent 最擅长的是辅助探索,不是替代分析师的判断。在团队落地时,我们采用**"Agent 初筛 + 分析师确认"**的模式:
Agent 产出的不是最终报告,而是: 1. 异常指标列表(带置信度) 2. 初步归因假设(需要分析师验证) 3. 进一步分析的 SQL 建议(分析师可以直接运行) 分析师要做的是: 1. 确认 Agent 的发现是否合理 2. 补充 Agent 遗漏的业务知识(比如"这次下降是因为大促结束") 3. 最终决策五、总结
🚨 踩坑提醒
Agent 在没有元数据时会"脑补"表结构:如果你的元数据字典里缺少某张表的字段说明,LLM 不会说"我不知道",而是会用它在训练数据里见过的类似表名来猜测字段。比如它看到
order_detail,就默认有order_status、user_id、amount这些字段,哪怕你的表根本不是这个结构。解决方法是把字段白名单校验设为必过的硬门槛——只要生成的 SQL 里包含任何不在已知字段列表里的名字,就拒绝执行并提示"未知字段"。分析循环必须有最大轮次上限:Agent 的
_need_deeper_analysis判断是基于数据特征的,但如果阈值设得太敏感,或者数据里有周期性波动被误判为异常,Agent 就会陷入"发现异常 → 下钻 → 又发现异常 → 继续下钻"的死循环。生产环境必须设置max_rounds(建议 5-8 轮)和单次查询的超时(30 秒),超过上限就强制输出"当前已知信息"而非继续深挖。多轮对话 token 累积会冲淡早期上下文:如果你用 LangChain 的
ConversationBufferMemory原样塞入所有历史,10 轮对话后 token 数轻松超过 4000,LLM 倾向于"只看最近几轮说话的内容",对最早的业务背景一问三不知。建议用摘要记忆(SummarizationMemory)或只保留结构化分析状态,而不是全量文本。
多轮对话式数据探索 Agent 的核心价值不是"替代写 SQL",而是把探索过程中的"衔接成本"降为零。传统模式下,每一次"接下来查什么"都需要人来做决策和编写 SQL;Agent 模式下,这些中间步骤全部自动化。
落地建议:不要一上来就追求"全自动",从异常检测 + 自动归因开始,让 Agent 替你做"发现异常 → 下钻归因"这件事。等你信任了 Agent 的 SQL 生成能力,再逐步放开"从问题到报告"的端到端流程。
技术栈上,LangChain 的 Agent 框架 + 公司的 SQL 查询引擎 + LLM 服务,是当前最成熟的组合。记住一点:Agent 的能力上限取决于你给它喂了多好的元数据(表结构、业务指标定义、历史分析案例)。
下篇我们来聊聊数据仓库的分层架构,ODS-DWD-DWS-ADS 每一层到底该做什么,不该做什么!