KDnuggets

Build an AI Data Analyst That Thinks Like a Senior Analyst

8.5内容质量

TL;DR · AI 摘要

构建六阶段AI数据分析师工具,通过验证数据量和生成SQL查询提升分析可靠性。

核心要点

  • 使用六阶段流程确保AI分析结果的可信度,包括数据验证和SQL生成
  • 工具兼容Anthropic和OpenAI API,支持自定义密钥集成
  • 小数据集(如29行记录)能暴露快速回答的缺陷

结构提纲

按章节快速跳转。

  1. 对比chatbot与高级分析师的决策流程差异,提出六阶段验证方法论

  2. 使用online_orders.csv数据集展示小样本数据的验证挑战

  3. 依次包含业务理解、假设生成、SQL规划、验证、摘要和建议六个环节

  4. 支持Anthropic和OpenAI API的可扩展架构设计

  5. 强制检查数据量阈值确保结论可靠性

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AI数据分析师工具
    • 六阶段流程
      • 业务理解
      • SQL验证
      • 数据量检查
    • 技术实现
      • Python工具包
      • LLM API集成
    • 数据案例
      • online_orders.csv

金句 / Highlights

值得收藏与分享的关键句。

#AI#数据科学#Python#机器学习#SQL
打开原文

构建一个像高级分析师一样思考的AI数据分析师 - KDnuggets

publ: 2026年9月9日

  • 博客热门文章
  • 主题 AI 职业建议 计算机视觉 数据工程 数据科学 语言模型 机器学习 MLOps NLP 编程 Python SQL
  • 数据集
  • 活动
  • 资源 快速参考指南 推荐 技术简报
  • 广告

加入新闻通讯

#header end

/ad_wrapper

构建一个像高级分析师一样思考的AI数据分析师

一个在得出答案前会验证数据的六阶段流程。

作者:

Nate Rosidi

,KDnuggets市场趋势与SQL内容专家,2026年9月9日发布于

人工智能

<div class="addthis_native_toolbox"></div>

向聊天机器人询问"我们应该多运行哪种促销活动",它会立即给出答案。它选择一个数字,自信地陈述,然后停止。它选择看起来数字最好的促销活动,并自信地表达选择。但它可能从未检查这个数字背后的数据量。基于10笔订单看起来很好的促销活动,远不如在1000笔订单中表现良好的促销活动有说服力。

高级分析师故意放慢速度。他们会重新表述问题,形成假设,编写查询,然后在向高管汇报前检查结果是否有足够的数据支撑。

我们可以将这种严谨性编码到程序中。

在本教程中,我们将构建一个小型Python工具包,通过六个阶段处理问题,而不是一个提示:业务理解、假设生成、SQL规划、验证、高管摘要和建议。

该工具包兼容Anthropic或OpenAI API,因此您需要自行提供密钥。指向任何表格,它都会运行相同的六个阶段。

以下所有代码按顺序执行,从加载CSV到最终建议,因此您可以在自己的数据上使用笔记本进行跟踪。

数据

在本文中,我们将使用名为online_orders.csv的数据表。您可以在StrataScratch面试问题中查看此数据集。它包含29行订单级数据:售出的产品、应用的促销活动、每单位成本、客户、日期和售出单位数。

product_id

promotion_id

cost_in_dollars

customer_id

date_sold

units_sold

1

2

2022-04-01

4

3

6

2022-05-24

10

2022-05-01

9

…

5

8

15

首先,我们使用Pandas加载它:

code
import pandas as pd
from IPython.display import display
orders = pd.read_csv("online_orders.csv")
print(f"Loaded {len(orders):,} rows and {len(orders.columns)} columns.")
display(orders.head())

#### 输出

code
Loaded 29 rows and 6 columns.

跨越3个月、4个促销活动和11个产品的29个订单。这个数据集足够小,以至于每个分组中的每个组都非常重要,这正是快速答案容易出错的那种数据集。

#### 检查模式

在接触任何大型语言模型(LLM)之前,我们先查看表中实际包含的内容:

code
schema_preview = pd.DataFrame({
    "column": orders.columns,
    "dtype": orders.dtypes.astype(str).values,
    "missing_values": orders.isna().sum().values,
})
display(schema_preview)

column

dtype

missing_values

int64

0

object

没有缺失值,且date_sold存储为文本而不是真正的日期。

确定性合理性检查

在调用任何LLM之前,普通的SQL已经能告诉我们一些信息。我们使用DuckDB注册数据框,这使我们能够在不设置数据库服务器的情况下对其运行真正的SQL。

code
import duckdb
con = duckdb.connect()
con.register("online_orders", orders)
preview = con.execute("""
    SELECT
        promotion_id,
        COUNT(*) AS n_orders,
        SUM(units_sold) AS total_units,
        SUM(cost_in_dollars * units_sold) AS total_revenue,
        ROUND(AVG(units_sold), 2) AS avg_units_per_order
    FROM online_orders
    GROUP BY promotion_id
    ORDER BY avg_units_per_order DESC
""").df()
display(preview)

订单数量

总销售单位数

总营收

每单平均销售单位数

8.0

64.0

8.00

12

77.0

407.0

6.42

55.0

199.0

5.50

31.0

185.0

5.17

按每单平均销售单位数排序,促销活动4以8.00位居榜首。

它后面紧跟着恰好1个订单。如果有人问"哪个促销活动的平均表现最好",并立即根据单个订单的强度推荐促销活动4,这就是整个流水线其余部分要避免的陷阱。

LLM包装器

流水线不应该关心你给它的是Anthropic客户端还是OpenAI客户端。一个轻量级包装器会显式指定提供者并调用对应的方法。对于Anthropic,回复可能包含多个内容块,因此它会扫描内容寻找第一个类型为文本的块,而不是假设它在最前面。

code
class LLMClient:
    def __init__(self, client, model, provider):
        self.client = client
        self.model = model
        self.provider = provider

    def complete(self, prompt):
        if self.provider == "anthropic":
            response = self.client.messages.create(
                model=self.model,
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            for block in response.content:
                if block.type == "text":
                    return block.text

            raise ValueError("No text block found in Claude's response.")

        if self.provider == "openai":
            response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
            )
            return response.choices[0].message.content

        raise ValueError(f"Unsupported provider: {self.provider}")

这为流水线其余部分提供了一个统一的complete()方法。特定提供者的响应格式被隐藏在包装器内部,因此后续阶段不需要单独处理Anthropic和OpenAI的代码路径。如果提供者不被支持,或者Claude没有返回可用的文本块,包装器会明确失败,而不是静默传递无效响应。

后续每个阶段都会要求模型返回JSON,因此我们需要一个额外的辅助工具从文本回复中提取JSON。一些回复会用三重反引号代码围栏包裹,因此辅助工具会先剥离这些标记,然后回退到扫描文本寻找第一个有效的JSON对象或数组。

code
import json
import re
def parse_json(text):
    text = text.strip()

    if text.startswith("```"):
        text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
        text = re.sub(r"\s*```$", "", text)

    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass
    candidates = []
    object_match = re.search(r"\{.*\}", text, re.DOTALL)
    array_match = re.search(r"\[.*\]", text, re.DOTALL)
    if object_match:
        candidates.append(object_match)
    if array_match:
        candidates.append(array_match)
    candidates.sort(key=lambda match: match.start())
    for match in candidates:
        try:
            return json.loads(match.group(0))
        except json.JSONDecodeError:
            continue
    raise ValueError(f"No valid JSON found in model output:\n{text}")

解析器从最简单的情况开始:如果整个回复本身就是有效的 JSON,会立即返回。如果失败,它会寻找嵌入在周围文本中的对象或数组,并按出现顺序尝试候选内容。这种方式使处理流程对常见的模型格式错误更加宽容,但仍然会在没有可用 JSON 时抛出错误。

第一阶段:业务理解

第一阶段会用表实际能回答的表述方式重述问题,明确数据粒度,并列出任何分析开始前的限制条件。

code
class SeniorAnalyst:
    MIN_SUPPORT = 3  # 仅在组别订单数达到此阈值后才信任结果

    def __init__(self, llm, table_name, dataframe):
        self.llm = llm
        self.table_name = table_name
        self.con = duckdb.connect()
        self.con.register(table_name, dataframe)
        self.schema = self.con.execute(f"DESCRIBE {table_name}").df()

    def understand_business_context(self, question):
        row_count = self.con.execute(
            f"SELECT COUNT(*) FROM {self.table_name}"
        ).fetchone()[0]

        columns = self.schema[
            ["column_name", "column_type"]
        ].to_dict("records")
        prompt = f"""你是一位资深数据分析师。利益相关者提出问题:"{question}"
表:{self.table_name}
列:{columns}
行数:{row_count}

用这张表实际能回答的表述方式重述利益相关者的问题。

同时说明表的粒度(一行代表什么),并列出你已经看到的任何限制条件:样本量、时间范围、缺失维度、缺失上下文等。

仅返回 JSON: {{"restated_question": "...", "grain": "...",
"limitations": ["...", "..."]}}"""
        context = parse_json(self.llm.complete(prompt))
        self.context = context
        return context

我们在处理"我们应该多运行哪种促销活动"这个问题时使用了claude-sonnet-5模型。以下是返回的结果。

它在运行任何查询之前就指出了样本量过小的问题——这与上面普通的SQL分组聚合已经显示的陷阱相同。这个提示只是一个线索,而非验证。处理流程仍然需要在代码中强制执行该限制,这就是下面验证阶段要做的事情。

第二阶段:假设生成

第二阶段会使用表中已有的列,提出具体且可验证的假设。

code
def generate_hypotheses(self, n=2):
    columns = list(self.schema["column_name"])
    prompt = f"""Business context: {self.context}
Propose {n} specific, testable hypotheses that would help answer the
restated question, using only columns in: {columns}.
Each hypothesis should be something we can test using SQL.
Return JSON only: [{{"hypothesis": "...", "why": "..."}}, ...]""
    hypotheses = parse_json(self.llm.complete(prompt))
    self.hypotheses = hypotheses
    return hypotheses

该流程会验证第一个假设。请注意这不是简单的平均值计算:它关注的是销量领先者是否以实际差距超越第二名,这与上面"最高平均值"查询中将一个等级的促销置于首位的表述已有明显区别。

阶段3:SQL规划

第三阶段将最佳假设转化为实际查询。我们要求同时返回行数和任何分组指标,因为验证阶段接下来会检查分组规模。

code
def plan_sql(self, hypothesis):
    columns = list(self.schema["column_name"])
    prompt = f"""Table: {self.table_name}
Columns: {columns}
Hypothesis to test: {hypothesis['hypothesis']}
Write one DuckDB SQL query that tests this hypothesis.
Use only the available columns, do not invent columns, and if the query
groups rows, include a COUNT(*) column named n_orders so the result can
be checked for sample size before anyone trusts it.
Return JSON only: {{"sql": "...", "purpose": "..."}}"""
    plan = parse_json(self.llm.complete(prompt))
    return plan
code
生成的SQL:
    WITH promo_sums AS (
        SELECT promotion_id, SUM(units_sold) AS total_units, COUNT(*) AS n_orders
        FROM online_orders
        GROUP BY promotion_id
    ),
    ranked AS (
        SELECT promotion_id, total_units, n_orders,
               RANK() OVER (ORDER BY total_units DESC) AS rnk
        FROM promo_sums
    )
    SELECT
        r1.promotion_id AS top_promotion_id,
        r1.total_units AS top_total_units,
        r1.n_orders AS top_n_orders,
        r2.promotion_id AS second_promotion_id,
        r2.total_units AS second_total_units,
        r2.n_orders AS second_n_orders,
        (r1.total_units - r2.total_units) * 1.0 / r2.total_units AS pct_difference
    FROM ranked r1
    JOIN ranked r2 ON r2.rnk = 2
    WHERE r1.rnk = 1

   'purpose': 'Identify the promotion_id with the highest total units
     sold and compare it to the second-highest to test whether it exceeds
     it by at least 20%, including order counts to assess statistical
     support.'

与简单的group by不同,模型采用了带窗口函数的通用表表达式(CTE),按总销量对促销活动进行排名,并将前两名放在同一行进行比较。

阶段4:验证

第四阶段运行查询并检查n_orders是否达到最低支持阈值。这是唯一一个纯代码阶段而非模型调用,因为该检查必须强制执行而非仅作建议。

code
def validate(self, sql_plan):
    result = self.con.execute(sql_plan["sql"]).df()
    if "n_orders" in result.columns:
        result["low_confidence"] = result["n_orders"] < self.MIN_SUPPORT
    else:
        result["low_confidence"] = False
    return result

top_promotion_id

top_total_units

top_n_orders

second_promotion_id

second_total_units

second_n_orders

pct_difference

low_confidence

0.4

False

此查询仅生成一行数据,且未被标记。促销活动1以总销量领先,有12个订单支持,促销活动2以10个订单排名第二,两者均超过了我们设定的最低3个订单门槛。检查仍然在此处执行——只是没有需要捕获的内容,因为此假设比较的是两个有充分数据支持的群体,而非依赖促销活动4的单一订单。

阶段5:高管摘要

第五阶段生成摘要,明确要求将任何被标记的行排除在主要结论之外。

code
def summarize(self, hypothesis, validated_result):
        flagged = validated_result[validated_result["low_confidence"]]
        prompt = f"""假设:{hypothesis['hypothesis']}
    查询结果:
    {validated_result.to_string(index=False)}
    被标记为低置信度的行支持的订单数量少于{self.MIN_SUPPORT}个
    不应作为结论依据。
    低置信度行:{flagged.to_dict('records')}
    根据结果撰写简洁的3到4句高管摘要
    结论仅基于展示的数据,明确避免
    将低置信度行作为主要结论,并且不要
    虚构数据不支持的解释。"""
        return self.llm.complete(prompt)

阶段6:建议

第六阶段提出行动建议,同样明确要求遵循相同规则:任何建议都不能基于低置信度数据或摘要未支持的事实。

code
def recommend(self, summary):
        prompt = f"""高管摘要:{summary}
    根据摘要支持的内容提出2到3个具体业务建议
    建议必须基于证据,不得依赖低置信度数据
    或虚构事实,若证据薄弱,应建议进一步分析
    而不是假装结论已明确。"""
        return self.llm.complete(prompt)

整合流程

运行方法将六个阶段串联起来。一次调用接收一个问题并返回所有中间结果:上下文、假设、SQL计划、验证后的表格、摘要和建议。

code
def run(self, question):
        context = self.understand_business_context(question)
        hypotheses = self.generate_hypotheses()
        top_hypothesis = hypotheses[0]
        plan = self.plan_sql(top_hypothesis)
        validated = self.validate(plan)
        summary = self.summarize(top_hypothesis, validated)
        recommendation = self.recommend(summary)
        return {
            "context": context,
            "hypotheses": hypotheses,
            "sql_plan": plan,
            "validated_result": validated,
            "summary": summary,
            "recommendation": recommendation,
        }

调用方式

无论使用哪个服务提供商,调用方式都保持一致。服务提供商需要显式指定而非从客户端对象猜测,如果忘记粘贴真实密钥,流程将拒绝执行。

code
PROVIDER = "anthropic"
    API_KEY = "YOUR_API_KEY_HERE"
    ANTHROPIC_MODEL = "claude-sonnet-5"
    OPENAI_MODEL = "gpt-4o"
    if API_KEY == "YOUR_API_KEY_HERE":
        raise ValueError(
            "在运行LLM部分之前,请将真实的API密钥粘贴到API_KEY中。"
        )
    if PROVIDER.lower() == "anthropic":
        from anthropic import Anthropic
        client = Anthropic(api_key=API_KEY)
        llm = LLMClient(client=client, model=ANTHROPIC_MODEL, provider="anthropic")
    elif PROVIDER.lower() == "openai":
        from openai import OpenAI
        client = OpenAI(api_key=API_KEY)
        llm = LLMClient(client=client, model=OPENAI_MODEL, provider="openai")
    else:
        raise ValueError("PROVIDER必须是'openai'或'anthropic'之一。")
    analyst = SeniorAnalyst(llm, "online_orders", orders)
    result = analyst.run("我们应该增加哪个促销活动?")
    print(result["summary"])
    print(result["recommendation"])

将PROVIDER设置为openai,插入一个OpenAI密钥,相同的六个阶段会无变化地针对gpt-4o运行。LLMClient是唯一知道它正在与哪个API通信的部分。

结论

这里六个阶段中的每一个单独来看都不复杂。重述问题、编写SQL、总结表格,这些都是单个提示已经能合理完成的事情。价值来自于查询和总结之间的验证阶段——在任何内容被称作答案之前,先检查n_orders。

在这个数据集上,即使在LLM被调用之前,这个检查就已经发现了问题:上面的普通SQL groupby按订单平均单位数对促销活动进行排序时,促销活动4仅基于一个订单就排名第一。这次模型选择测试的假设比较了两个更有依据的组——12个订单对10个订单——因此validate()没有需要标记的问题。无论模型提供什么比较,这个流程都会执行相同的n_orders检查,因此未来的表格或未来测试平均值而非总数的运行,都会被相同的代码行捕获。

这个流程在一个类上有6个方法,当指向下一个表格时,相同的6个方法会再次运行。

Nate Rosidi是一名数据科学家,目前从事产品战略工作。他还是一个兼职教授,教授分析课程,同时也是StrataScratch的创始人,这是一个帮助数据科学家通过顶级公司真实面试题准备面试的平台。Nate撰写关于职业市场最新趋势的文章,提供面试建议,分享数据科学项目,并涵盖所有与SQL相关的内容。

更多相关内容

  • 像软件工程师一样测试SQL:单元测试、CI/CD和数据…
  • 10个鲜为人知的Python库,它们会让你感觉自己像数据巫师
  • 7个看似玩笑的Python内置函数(直到你使用它们)
  • 新手指南:Gemini CLI的安装、设置和专业使用
  • 像专业人士一样调试和追踪LLM
  • 如何像专业人士一样使用深度研究

<hr class="grey-line"><br> <div><h3>我们推荐的5门顶级免费课程</h3><br> </div>

Mailchimp for WordPress v4.14.0 - https://wordpress.org/plugins/mailchimp-for-wp/

/ Mailchimp for WordPress插件

你可以从这里开始编辑。

如果评论已关闭。

<= 上一篇

#content end

<script type="text/javascript">kda_sid_write(kda_sid_n);</script>

最新文章

/

  • 打造一个思维如资深分析师的AI数据分析师 7种在有限硬件上高效训练LLM的方法 从RAG到自主智能AI:构建下一代智能企业系统 ArrowJS真的是自主智能时代的用户界面吗?我的发现 5种免费访问编码模型的方法 Switchyard:NVIDIA的开源路由库

热门文章

  • 5门免费课程助你从LLM入门者进阶为实践者
  • 2026年可使用的5个免费LLM API提供商
  • ArrowJS真的是自主智能时代的用户界面吗?我的发现
  • 自主智能AI在企业自动化中的5个实际应用案例
  • 5种免费访问编码模型的方法
  • 从RAG到自主智能AI:构建下一代智能企业系统
  • 2026年可本地运行的7大编码模型
  • 我让ChatGPT分析3个数据集,它每次都犯同样的错误
  • 停止使用if-else链:在Python中改用注册表模式
  • 从AI编码代理获取更好结果的10条规则

#content_wrapper end

© 2026

Guiding Tech Media

|

关于

联系我们

广告合作

隐私政策

服务条款

2026年9月9日由Nate Rosidi发布

blank

不,谢谢!

/.main_wrapper

<script defer type="text/javascript" src="https://s7.addthis.com/js/300/addthis_widget.js#pubid=gpsaddthis"></script>

noptimize

/noptimize