LangChain-for-EDA-使用-Python-构建-CSV-精确性检查代理

LangChain for EDA:使用 Python 构建 CSV 精确性检查代理

原文:towardsdatascience.com/langchain-for-eda-build-a-csv-sanity-check-agent-in-python/

聊天机器人回答问题,代理执行动作。

这正是我们今天文章中将要尝试的内容。

在本文中,我们将使用 LangChain 和 Python 构建我们自己的 CSV 精确性检查代理。有了这个代理,我们将自动化典型的探索性数据分析(EDA)任务,如显示列、检测缺失值(NaNs)和检索描述性统计。

代理逐步决定调用哪个工具以及何时回答关于我们数据的问题。这与传统意义上的应用程序有很大不同,在传统意义上,开发者定义了过程是如何工作的(例如,通过 if-else 循环)。这也远远超出了简单的提示,因为我们正在构建一个行动系统(尽管是简单的),而不仅仅是说话。

如果你:

  • …使用 Pandas 并希望自动化 EDA。

  • …对 LLM 感兴趣,但迄今为止对 LangChain 的经验很少。

  • …想通过一个简单的例子了解代理是如何真正工作的(从设置到迷你评估)。

*目录

我们要构建的以及原因

动手实验:使用 LangChain 的 CSV 精确性检查代理

迷你评估

最终思考 – 陷阱、技巧和下一步

你可以在哪里继续学习?*

我们要构建的以及原因

代理是我们分配任务的系统。然后系统自己决定使用哪些工具来解决这些任务。

这需要三个组件:

代理 = LLM + 工具 + 控制逻辑

让我们更详细地看看这三个组件:

  • LLM 提供智能:它理解问题,规划步骤,并决定做什么。

  • 这些工具是代理可以调用的小的 Python 函数(例如,get_schema()get_nulls()):它们从数据中提供特定信息,例如列名或统计数据。

  • 控制逻辑(策略)确保 LLM 不会立即响应,而是首先决定是否应该使用工具。它逐步思考:首先分析问题,然后选择适当的工具,然后解释结果,并在必要时选择下一步,最后返回响应。

与经典提示中的手动描述所有数据不同,我们将责任转移给代理:系统应该自行行动,但只能使用提供的工具。

让我们看看一个简单的例子:

用户询问:“CSV 中的平均年龄是多少?”

在这一点上,代理调用我们定义的工具,df.describe()。输出是一个结构清晰的价值(例如,“mean”:29.7)。在这里我们还可以看到,这可以减少或最小化幻觉,因为系统知道应该应用什么,不能返回像“可能在 20 到 40 之间”这样的答案。

LangChain 作为一个框架

我们使用LangChain 框架作为代理。这允许我们将 LLMs 与工具连接起来,并构建具有定义行为的系统。系统可以执行操作,而不仅仅是提供答案或生成文本。详细的解释会使这篇文章太长。但在之前的文章中,你可以找到 LangChain 的解释以及与 Langflow 的比较:LangChain vs Langflow:使用代码或拖放构建简单的 LLM 应用程序

代理为我们做了什么

当我们收到新的 CSV 文件时,我们通常会首先问自己以下问题(探索性数据分析的开始):

  • 有哪些列?

  • 数据缺失在哪里?

  • 描述性统计看起来像什么?

这正是我们希望代理自动执行的事情。

我们为代理定义的工具

为了使代理工作,它需要明确定义的工具。最好尽可能将它们定义为小、具体和可控。这样,我们就可以避免错误、幻觉或输出不清晰,因为它们会使输出确定。它们还使代理可重复和可测试,因为相同的输入应该产生一致的结果。

在我们的例子中,我们定义了三个工具:

  • schema:返回列名和数据类型。

  • nulls:显示包含缺失值的列(包括数字)。

  • describe:为数值列提供描述性统计。

之后,我们将添加一个小型评估,以确保我们的代理能够正确工作。

为什么这是一个代理而不是一个应用程序?

我们不是在构建一个具有固定序列的经典程序(例如,使用 if-else),而是模型根据问题自行规划,选择适当的工具,并在必要时组合步骤以得出答案:

这张图片显示了传统应用程序和这个代理之间的区别。

作者提供的可视化。

实践示例:使用 LangChain 的 CSV-Sanity-Check 代理

1) 设置

先决条件:必须安装 Python 3.10 或更高版本。AI 工具世界的许多包都需要≥ 3.10。你可以在下面的代码和 repo 链接中找到。

新手的提示:

你可以通过在 cmd.exe 中输入“python –version”来检查这一点

使用下面的代码,我们首先创建一个新的项目,创建一个独立的 Python 环境并激活它。我们这样做是为了使包和版本可重复,并且不与其他项目合并。

新手的提示:

我在 Windows 上工作。我们通过 Windows + R > cmd 打开终端,并粘贴以下代码

mkdir csv-agent

cd csv-agent
python -m venv .venv
.venv\Scripts\activate

然后我们安装必要的包:

pip install "langchain>=0.2,<0.3" "langchain-openai>=0.1.7" "langchain-community>=0.2" pandas seaborn

使用此命令,我们将 LangChain 锁定在 0.2 版本,并安装 OpenAI 连接和社区包。我们还安装 pandas 用于 EDA 函数和 seaborn 用于加载泰坦尼克号样本数据集。

图像显示了创建环境和安装包的过程。

作者拍摄的截图。

新手提示

如果您不想使用 OpenAI,您可以在本地使用 Ollama(例如,使用 Llama 或 Mistral)进行工作。此选项在代码的后面部分可用

2) 在 prepare_data.py 中准备数据集

接下来,我们创建一个名为 prepare_data.py 的 Python 文件。我使用 Visual Studio Code 进行此操作,但您也可以使用其他 IDE。在此文件中,我们加载泰坦尼克号数据集,因为它公开可用。

# prepare_data.py
import seaborn as sns
df = sns.load_dataset("titanic")
df.to_csv("titanic.csv", index=False)
print("Saved titanic.csv")

使用 seaborn.load_dataset(“titanic”),我们将公共数据集(891 行 + 第一行包含列名)直接加载到内存中,并保存为 titanic.csv。该数据集仅包含数值、布尔和分类列,非常适合 EDA 代理。

新手提示

  • sns.load_dataset() 需要互联网访问(数据来自 seaborn 仓库)。

  • 将文件保存在项目文件夹(csv-agent)中,以便 main.py 可以找到它

在终端中,我们使用以下命令执行 Python 文件,以便 titanic.csv 文件位于项目中:

python prepare_data.py

然后,我们在终端中看到 csv 已保存,并在文件夹中看到 titanic.csv 文件:

图像显示了 csv 保存后终端中的结果。

作者拍摄的截图。

图像显示了项目的文件夹结构。

作者拍摄的截图。

侧记 - 泰坦尼克号数据集

分析基于泰坦尼克号数据集 (OpenML ID 40945),该数据集在 OpenML 上标记为公开。

当我们打开文件时,我们看到以下 14 个列和 891 行数据。泰坦尼克号数据集是探索性数据分析(EDA)的经典示例。它包含有关泰坦尼克号 891 名乘客的信息,常用于研究特征(例如,性别、年龄、票舱等级)与生存之间的关系。

图像显示了 Excel 中的泰坦尼克号数据集。

作者拍摄的截图。

这里是 14 个列及其简要说明:

  • 生存: 生存(1)或未生存(0)。

  • pclass: 票舱等级(1 = 一等舱,2 = 二等舱,3 = 三等舱)。

  • 性别: 乘客的性别。

  • 年龄: 乘客的年龄(以年为单位,可能缺失)。

  • sibsp: 船上兄弟姐妹/配偶的数量。

  • parch: 船上父母/孩子的数量。

  • 票价: 乘客支付的票价。

  • 登船港口: 登船港口(C = 雷恩,Q = 皇后镇,S = 南安普顿)。

  • 舱位等级: 作为文本的票舱等级(一等舱,二等舱,三等舱)。对应于 pclass。

  • who: 分类“男性”、“女性”、“儿童”。

  • adult_male: 布尔字段:乘客是否为成年男性(True/False)?

  • deck: 船舱甲板(通常缺失)。

  • embark_town: 登船港口城市(如瑟堡、昆士敦、南安普顿)。

  • alone: 布尔字段:乘客是否独自旅行(True/False)?

*高级读者可选

如果您想以后跟踪和评估您的代理运行,可以使用 LangSmith

2) 在 main.py 中定义工具

接下来,我们定义各种工具。为此,我们创建一个新的 Python 文件,命名为 main.py,并将其保存在 csv-agent 文件夹中。我们向其中添加以下代码:

# main.py
import os, json
import pandas as pd

# --- 0) Loading CSV ---
DF_PATH = "titanic.csv"
df = pd.read_csv(DF_PATH)

# --- 1) Defining tools as small, concise commands ---
# IMPORTANT: Tools return strings (in this case, JSON strings) so that the LLM sees clearly structured responses.

from langchain_core.tools import tool

@tool
def tool_schema(dummy: str) -> str:
    """Returns column names and data types as JSON."""
    schema = {col: str(dtype) for col, dtype in df.dtypes.items()}
    return json.dumps(schema)

@tool
def tool_nulls(dummy: str) -> str:
    """Returns columns with the number of missing values as JSON (only columns with >0 missing values)."""
    nulls = df.isna().sum()
    result = {col: int(n) for col, n in nulls.items() if n > 0}
    return json.dumps(result)

@tool
def tool_describe(input_str: str) -> str:
    """
    Returns describe() statistics.
    Optional: input_str can contain a comma-separated list of columns, e.g. "age, fare".
    """
    cols = None
    if input_str and input_str.strip():
        cols = [c.strip() for c in input_str.split(",") if c.strip() in df.columns]
    stats = df[cols].describe() if cols else df.describe()
    # describe() has a MultiIndex. Flatten it for the LLM to keep it readable:
    return stats.to_csv(index=True)

在导入必要的包后,我们将 titanic.csv 一次性加载到 df 中,并定义了三个小型、定义明确的工具。让我们逐一详细看看这些工具:

  • tool_schema 返回列名和数据类型作为 JSON。这让我们对正在处理的内容有一个概述,通常是任何数据分析的第一步。即使工具不需要输入(如模式),它也必须接受一个参数,因为代理总是传递一个字符串。我们简单地忽略它。

  • tool_nulls 统计每列的缺失值并只返回具有缺失值的列。

  • tool_describe 调用 df.describe()。需要注意的是,此工具仅适用于数值列。另一方面,字符串或布尔值将被忽略。这是在检查或 EDA 中的重要步骤。这使我们能够快速查看不同列的平均值、最小值、最大值等。对于大型 CSV 文件,describe() 可能需要很长时间。在这种情况下,您可以将 df.sample(n=10000) 作为采样逻辑进行集成,例如。

这些工具是通过它们访问数据的受控接口。它们是确定性的,因此是可重复的。工具应该是清晰和有限的:换句话说,它们应该只有一个功能或任务。


我们为什么需要工具呢?

一个 LLM 可以生成文本,但不能直接“看到”数据。为了使 LLM 能够有意义地与 CSV 文件一起工作,我们需要提供接口。这正是工具的作用:

工具是代理允许调用的小型 Python 函数。我们不是让所有东西都免费,而是只允许非常具体、可重复的操作。


代码究竟做了什么?

使用 @tool 装饰器,LangChain 会自动从函数签名和文档字符串中推断工具的名称、描述和参数模式。这意味着我们只需要编写函数本身。LangChain 会处理其余部分。

  • 模型传递与工具模式匹配的参数(通常是 JSON)。在本教程中,我们保持简单,接受单个字符串参数(例如,input_str: str 或我们忽略的虚拟字符串)。

  • 工具总是返回一个字符串(文本)。JSON 是结构化数据的理想选择,我们使用 return json.dumps(…) 定义。

该图像显示了代理如何使用工具进行多步骤推理。

由作者进行可视化。

这是一个多步骤的思维过程。LLM 逐次迭代地计划。它不会直接响应,而是逐步思考:它决定调用哪个工具,解释结果,并可能继续进行,直到它有足够的信息来响应。

4) 在 main.py 中注册 LangChain 的工具

我们将以下代码添加到相同的 main.py 文件中,以注册之前定义的工具:

# --- 2) Registering tools for LangChain ---

tools = [tool_schema, tool_nulls, tool_describe]

通过此代码,我们只需将装饰过的函数收集到一个列表中。每个函数都已经通过 @tool 装饰器转换成了 LangChain 工具。

5) 在 main.py 中配置 LLM

接下来,我们配置代理使用的 LLM。在这里,你可以使用 OpenAI 的变体或使用 Ollama 的开源工具。

我使用了 OpenAI,因此我们首先需要设置 API 密钥:

OpenAI 中,我们创建一个新的 API 密钥:

该图像显示了如何在 OpenAI 中创建 API 密钥。

由作者拍摄的截图。

我们然后直接复制它(它将不会在以后显示)并在终端中使用以下命令将其设置为环境变量。

setx OPENAI_API_KEY "your_key”

重要的是在之后重启 cmd 并重新激活 .venv。我们可以使用 echo 来检查 API 密钥是否已保存。

该图像显示了如何在终端中检查 API 密钥是否已保存。

由作者拍摄的截图。

现在我们将以下代码添加到 main.py 的末尾:

# --- 3) Configure LLM ---
# Option A: OpenAI (simple)
#   export OPENAI_API_KEY=...    # Windows: setx OPENAI_API_KEY "YOUR_KEY"
#   Use a lower temperature for more stable tool usage
USE_OPENAI = bool(os.getenv("OPENAI_API_KEY"))

if USE_OPENAI:
    from langchain_openai import ChatOpenAI
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
else:
    # Option B: Local with Ollama (make sure to pull the model first, e.g. 'ollama run llama3')
    from langchain_community.chat_models import ChatOllama
    llm = ChatOllama(model="llama3.1:8b", temperature=0.1)

如果可用 OpenAI_API_KEY,代码将使用 OpenAI,否则使用本地 Ollama。

我们将温度设置为 0.1。这确保了响应更加确定性,这对于后续测试很重要。

我们还使用 gpt-4o-mini 作为 LLM。这是一个由 OpenAI 提供的轻量级模型,专注于工具使用。

新手提示:

温度决定了大型语言模型(LLM)的创造性响应程度。如果我们输入 0.0,它将以确定性方式响应。这意味着当输入相同时,模型几乎总是返回相同的答案。这对于结构化任务,例如工具使用、代码或事实等,是有益的。如果我们指定 1.0,模型将以创造性和多样化的方式响应。这意味着模型变化更多,可以提出不同的表述或解决方案,这对于头脑风暴或文本创意等是有益的。

6) 使用策略在 main.py 中定义代理的行为

在这一步中,我们定义了代理应该如何表现。系统提示设置了策略。

# --- 4) Narrow Policy/Prompt (Agent Behavior) ---
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

SYSTEM_PROMPT = (
    "You are a data-focused assistant. "
    "If a question requires information from the CSV, first use an appropriate tool. "
    "Use only one tool call per step if possible. "
    "Answer concisely and in a structured way. "
    "If no tool fits, briefly explain why.\n\n"
    "Available tools:\n{tools}\n"
    "Use only these tools: {tool_names}."
)

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", SYSTEM_PROMPT),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ]
)

_tool_desc = "\n".join(f"- {t.name}: {t.description}" for t in tools)
_tool_names = ", ".join(t.name for t in tools)
prompt = prompt.partial(tools=_tool_desc, tool_names=_tool_names) 

首先,我们导入 ChatPromptTemplate 以结构化我们的代理提示。代码最重要的部分是系统提示:它定义了策略,即代理的“游戏规则”。在其中,我们定义代理每步只能使用一个工具,它应该是简洁的,并且它只能使用我们定义的工具。

在系统提示的最后两行中,我们确保 {tools} 列出了所有可用的工具及其描述,并且通过 {tool_names},我们确保智能体只能使用这些名称,而不能发明幻想工具。

此外,我们使用 MessagesPlaceholder(“agent_scratchpad”)。这是智能体存储中间步骤的地方:智能体存储它调用了哪些工具以及它接收了哪些结果。这允许它继续自己的推理链,直到得出最终答案。

7) 在 main.py 中创建工具调用智能体

在最后一步,我们定义智能体:

# --- 5) Create & Run Tool-Calling Agent ---
from langchain.agents import create_tool_calling_agent, AgentExecutor

agent = create_tool_calling_agent(llm=llm, tools=tools, prompt=prompt)
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=False,   # optional: True for debug logs
    max_iterations=3,
)

if __name__ == "__main__":
    user_query = "Which columns have missing values? List 'Column: Count'."
    result = agent_executor.invoke({"input": user_query})
    print("\n=== AGENT ANSWER ===")
    print(result["output"])

使用 create_tool_calling_agent,我们将我们的 LLM、工具和提示连接起来,形成一个工具调用智能体。

为了确保过程顺利运行,我们使用 AgentExecutor。它负责所谓的智能体循环:智能体首先计划需要做什么,然后调用一个工具,接收结果并决定是否需要另一个工具或是否可以提供最终答案。这个循环重复进行,直到结果准备好。

使用 verbose=True,我们可以在终端中查看中间步骤,这对于调试非常有帮助。例如,我们可以看到何时调用了哪个工具或返回了什么数据。如果一切运行顺利,我们也可以将其设置为 =False 以保持输出更清晰。

使用 max_iterations=3,我们限制智能体可能执行的推理-工具-响应周期数量。这有助于防止无限循环或过多的工具调用。在我们的例子中,智能体在回答之前可能会合理地调用模式 → nulls → 描述。

代码的最后部分,智能体使用示例输入“哪些列有缺失值?”进行执行。结果在终端中打印出来。

新手提示:

if name == “main”: 是一个标准的 Python 模式:如果我们直接在终端中通过 python main.py 执行文件,这个块中的代码将被启动。然而,如果我们只导入文件(例如,在 mini_eval.py 文件中稍后),这个块将被跳过。这允许我们将文件用作独立的脚本或在其他项目中作为模块重用。

8) 运行脚本:在终端中运行文件 main.py。

现在,我们在终端中输入 python main.py 来启动智能体。然后我们在终端中看到最终答案:

该图像显示了智能体在终端中显示的结果(缺失值数量)

作者拍摄的截图。

小型评估

最后,我们想要检查我们的智能体,这通过一个小型评估来完成。这确保了当我们在代码中稍后更改某些内容时,智能体能够正确行为并且不会引入任何“回归”。

main.py 的末尾,我们添加以下代码:

def ask_agent(query: str) -> str:
    return agent_executor.invoke({"input": query})["output"]

使用 ask_agent,我们将智能体调用封装在一个简单地返回字符串的函数中。这允许我们稍后从其他文件中调用智能体。

下一个块确保当直接调用main.py时执行测试运行。另一方面,如果我们把main导入到另一个文件中,只提供函数。

现在我们创建mini_eval.py文件并插入以下代码:

# mini_eval.py

from main import ask_agent

tests = [
    ("Which columns have missing values?", ["age", "embarked", "deck", "embark_town"]),
    ("Show me the first 3 columns with their data types.", ["survived", "pclass", "sex"]),
    ("Give me a statistical summary of the 'age' column.", ["mean", "min", "max"]),
]

def passed(q, out, must_include):
    text = out.lower()
    return all(any(tok in text for tok in (m.lower(), str(m).lower())) for m in must_include)

if __name__ == "__main__":
    ok = 0
    for q, must in tests:
        out = ask_agent(q)
        result = passed(q, out, must)
        print(f"[{'OK' if result else 'FAIL'}] {q}\n{out}\n")
        ok += int(result)
    print(f"Passed {ok}/{len(tests)}") 

在代码中,我们定义了三个测试案例。每个测试案例包括一个针对代理的问题和一个必须在答案中出现的关键词列表。passed() 函数检查这些关键词是否包含在内。

预期测试结果

  • 测试 1:“哪些列有缺失值?”

    预期:输出提到 age、deck、embarked、embark_town。

  • 测试 2:“展示包含数据类型的头 3 列。” 预期:输出包含 survived、pclass、sex 等类型,如 int64 或 object。

  • 测试 3:“给出‘age’列的统计摘要。” 预期输出:输出包含 mean ≈ 29.7,min = 0.42,max = 80。

如果一切运行正确,脚本在最后会报告“Passed 3/3”。

我们在终端中得到这个输出。所以测试是有效的:

图像显示了小型评估的结果。

由作者拍摄的截图。


你可以在 GitHub 上的仓库中找到代码和 csv 文件。

在我的Substack 数据科学咖啡厅上,我分享来自数据科学、Python、AI、机器学习和科技领域的实用指南和精简更新——专为像你这样好奇的心灵而准备。

查看并订阅Medium或 Substack,如果你想保持最新动态。


最后的想法——陷阱、技巧和下一步

对于这个例子,LangChain 非常实用,因为它已经包含了并很好地展示了整个代理循环(规划、工具调用、控制)。然而,对于小型或结构清晰的任务,其他替代方案,如纯函数调用(例如,通过 OpenAI API)或经典的 EDA 框架如 Great Expectations 可能就足够了。话虽如此,LangChain 确实增加了一些开销。如果你只需要固定的 EDA 检查,一个简单的 Python 脚本会更简洁、更快。当你想要灵活扩展或编排多个工具和代理时,LangChain 特别有价值。

当与代理一起工作时,有一些事情你应该记住:

一个常见的陷阱是不清晰的工具描述:如果描述过于模糊,模型很容易选择错误的工具(误路由)。通过精确和具体的描述,我们可以大大减少这种情况。

另一个重要点是测试:即使是一个包含三个简单测试的小型评估也有助于在早期阶段检测回归(由于后续更改而未被注意到的错误)。

也值得从小处开始:在我们的例子中,我们只使用了三个明确定义的工具,但现在我们知道它们是可靠的。

关于这个代理,对于非常大的 CSV 文件,可能也需要采用采样(例如,df.sample(n=10000))来避免性能问题。请记住,如果每个问题都触发多个工具调用,LLM 代理也可能变得成本高昂。

在这篇文章中,我们构建了一个单独的代理来检查 CSV 文件。在实践中,多个代理通常会协同工作:例如,一个代理可以确保数据质量,而第二个代理则创建可视化。这种多代理系统是解决更复杂任务的下一步。

作为下一步,我们还可以将LangGraph纳入其中,以通过状态和编排扩展代理循环。这将使我们能够像在流程图中一样组装代理,包括中断、内存或更灵活的控制逻辑。

最后,在我们的例子中,我们手动定义了三个工具模式、nulls 和 describe。通过模型上下文协议 (MCP),我们可以以标准化的方式连接工具。例如,我们可以连接数据库、API 或 IDE。

你可以在哪里继续学习?

posted @ 2026-03-27 09:53  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报