基于LLM的智能表格数据处理系统:Pneuma-Seeker的设计与实现
1. 项目概述:当LLM遇见表格数据,一场数据处理的革命
如果你也像我一样,每天需要和大量的Excel、CSV表格打交道,那你一定对数据清洗和准备这个“脏活累活”深有体会。从识别缺失值、异常值,到统一格式、拆分合并列,再到理解每一列数据的业务含义,整个过程繁琐、重复,且极度依赖经验。一个不小心,就可能因为对数据理解不透彻,导致后续分析结果南辕北辙。Pneuma-Seeker这个项目,正是为了解决这个痛点而生。它不是一个简单的脚本工具,而是一个基于大语言模型(LLM)的交互式表格数据发现与准备系统。简单来说,它试图让机器像一位经验丰富的数据分析师一样,通过自然语言与你对话,帮你“看懂”数据,并“动手”完成数据准备工作。
“Pneuma”在古希腊语中有“气息”、“精神”之意,而“Seeker”是探索者。这个名字本身就暗示了这个系统的核心:赋予数据以“灵性”,让系统能够主动探索和理解数据的内在逻辑。它的核心价值在于,将LLM强大的语义理解和推理能力,与结构化表格数据的处理需求相结合,从而极大地降低数据预处理的门槛,提升数据科学家和分析师的工作效率与准确性。无论你是刚入门的数据新手,还是希望从重复劳动中解放出来的资深专家,Pneuma-Seeker都提供了一个全新的、更智能的交互范式。
2. 核心设计思路:从“命令式”到“对话式”的范式转移
传统的表格数据处理工具,无论是Pandas、Excel还是专业的ETL平台,都遵循着“命令式”的范式。用户需要明确知道每一步要做什么,用什么函数,参数如何设置。这要求用户既是业务专家,又是工具专家。Pneuma-Seeker的设计哲学则完全不同,它追求的是“对话式”或“声明式”的交互。你只需要告诉系统你的目标或困惑,系统就能理解你的意图,并生成相应的处理步骤或直接给出结果。
2.1 系统核心架构拆解
为了实现这一目标,Pneuma-Seeker的架构通常围绕以下几个核心模块构建:
自然语言理解与任务规划模块:这是系统的大脑。它接收用户用自然语言提出的请求,例如“帮我找出所有销售额为负的记录”或“将‘订单日期’列拆分成‘年’、‘月’、‘日’三列”。LLM在此处的角色是理解用户意图,并将其分解为一系列可执行的数据操作原子任务(Atomic Tasks)。这背后需要LLM具备一定的领域知识,理解常见的数据操作术语(如filter, groupby, merge, pivot等)和数据质量问题(如missing, outlier, inconsistency)。
表格数据感知与上下文管理模块:LLM本身并不“看见”表格。因此,系统需要将表格数据以一种LLM能够理解的方式“喂”给它。常见的做法包括:
- 元数据摘要:提取表格的列名、数据类型、样本值、统计摘要(如均值、中位数、唯一值数量)等,形成一段结构化的文本描述。
- 智能采样与向量化:对于大型表格,全量送入LLM成本过高。系统会智能采样部分行,或对单元格内容进行向量化嵌入,帮助LLM感知数据分布和内容模式。
- 对话历史管理:记录当前会话中用户已执行的操作和数据状态变化,确保LLM在后续对话中拥有正确的上下文,理解“当前数据已经是什么样子了”。
代码生成与安全执行模块:这是系统的双手。LLM在理解了任务和数据后,需要生成可执行的代码(通常是Python/Pandas代码)来完成具体操作。这个模块至关重要,它必须确保生成的代码是安全、正确且高效的。
- 安全沙箱:生成的代码必须在一个隔离的沙箱环境中执行,防止任意代码执行对用户系统造成危害。
- 代码验证与回退:执行代码前,可以进行简单的语法和逻辑检查。如果执行出错,系统应能捕获异常,并尝试让LLM分析错误原因并重新生成代码,或提供清晰的错误信息给用户。
- 操作可解释性:系统不仅执行代码,还应能解释“我为什么要这么做”,例如“检测到‘年龄’列有大于150的异常值,根据常识将其标记为异常”。
交互式可视化与反馈模块:系统处理后的结果需要直观地展示给用户。这包括更新后的表格预览、数据质量报告(如缺失值分布图、异常值散点图)、以及操作日志。用户可以根据结果给出反馈,如“这个转换不对,我的日期格式是‘DD/MM/YYYY’”,系统据此进行迭代优化。
2.2 为什么选择LLM而非传统规则引擎?
你可能会问,用一套精心设计的规则引擎,配合图形化界面(GUI)拖拽,不也能实现类似功能吗?确实可以,但LLM带来了两个根本性的优势:
处理模糊与未知问题的能力:规则引擎擅长处理定义明确、模式固定的任务。但数据世界充满意外,比如千奇百怪的日期格式、业务特有的缩写、隐藏在文本列中的结构化信息。LLM凭借其在大规模语料上学到的泛化能力,能够更好地理解和处理这些“非标”情况。例如,用户说“把地址里的省份信息提取出来”,即使地址格式不统一,LLM也有很大概率能正确识别并生成提取逻辑。
极低的交互与学习成本:学习一个功能强大的GUI或一门新的查询语言(如SQL)需要时间成本。而自然语言是人最本能的交互方式。Pneuma-Seeker让用户可以用最直接的方式表达需求,无需记忆特定语法或点击层层菜单,实现了“所想即所得”的交互体验。
注意:LLM并非万能。它在处理精确数值计算、保证100%确定性输出方面仍有不足。因此,一个成熟的Pneuma-Seeker系统,应是“LLM引导 + 确定性代码执行 + 用户监督”的结合体,LLM负责理解、规划和生成,而确定性的编程库(如Pandas)负责可靠地执行。
3. 关键技术细节与实操要点
理解了宏观架构,我们深入到几个关键的技术实现细节,这些是决定系统是否好用的核心。
3.1 如何让LLM“读懂”表格?——提示词工程的艺术
这是整个系统最核心的环节。直接给LLM扔一个CSV文件是行不通的。我们需要精心设计提示词(Prompt),将表格信息、用户指令、历史上下文和期望的输出格式整合在一起。
一个基础但有效的提示词结构可能如下:
你是一个专业的数据分析助手。请根据以下表格信息和用户请求,生成安全、正确的Pandas代码来解决数据问题。 【表格元数据】 - 文件名:sales_data.csv - 列信息: 1. `OrderID` (字符串类型): 订单编号,示例值:'ORD001' 2. `OrderDate` (对象类型,可能是日期): 订单日期,示例值:'2023-01-15', '15/01/2023' 3. `Product` (字符串类型): 产品名称,示例值:'Laptop', 'Mouse' 4. `Quantity` (整数类型): 数量,示例值:2, 5, -1 5. `UnitPrice` (浮点数类型): 单价,示例值:999.99, 25.50 6. `TotalSales` (浮点数类型): 总销售额,由Quantity*UnitPrice计算得来,示例值:1999.98, 127.50 - 数据预览(前3行): OrderID,OrderDate,Product,Quantity,UnitPrice,TotalSales ORD001,2023-01-15,Laptop,2,999.99,1999.98 ORD002,15/01/2023,Mouse,5,25.50,127.50 ORD003,2023-01-16,Keyboard,-1,45.00,-45.00 - 当前数据状态:这是原始加载的数据,尚未进行任何处理。 【历史操作】 无。 【用户当前请求】 找出所有数量为负数或总销售额为负数的异常订单记录。 【你的任务】 1. 分析用户请求。 2. 生成一段Pandas代码,实现该请求。代码应加载名为`df`的DataFrame(它已包含上述数据)。 3. 代码应包含必要的注释,并最终将筛选出的异常数据保存到一个名为`anomaly_df`的新DataFrame中。 4. 只输出代码,不要输出任何解释。 【输出格式】 ```python # 你的代码 here**实操心得**: * **结构化描述优于纯文本**:将列信息、数据类型、示例值分开列出,比一段话描述更利于LLM解析。 * **提供样本数据**:3-5行样本数据能让LLM直观感受数据格式,对于识别日期、数字格式等问题至关重要。 * **明确数据状态**:告诉LLM当前数据是原始状态还是经过了一些处理,避免它基于错误假设生成代码。 * **严格约束输出格式**:要求“只输出代码”,可以极大减少LLM“废话”和解析成本。对于复杂任务,也可以要求输出“代码+简短说明”两部分。 ### 3.2 代码生成与执行的可靠性保障 生成的代码能否安全、正确地运行,是用户体验的生死线。 1. **沙箱环境**:绝对不要在用户的主进程或拥有高权限的环境中执行动态生成的代码。必须使用如`Docker`容器、`Pyodide`(Web环境)、或严格的`restrictedpython`等沙箱技术。例如,在Docker容器中运行代码,限制其网络、文件系统访问权限。 2. **防御性提示与预处理**:在提示词中明确要求LLM生成安全的代码。禁止使用`os.system`, `subprocess`, `__import__`等危险函数。可以在执行前,对生成的代码字符串进行简单的静态扫描,过滤掉黑名单中的危险关键词。 3. **错误处理与重试机制**:代码执行出错是常态。系统需要捕获异常(如`KeyError`, `ValueError`, `SyntaxError`),并将错误信息连同上下文,再次发送给LLM,请求其分析错误并修正代码。可以设置一个最大重试次数(如3次),避免陷入死循环。 ```python # 伪代码示例 max_retries = 3 for attempt in range(max_retries): code = llm_generate_code(prompt) try: # 在沙箱中执行code result = sandbox.execute(code, df) return result except Exception as e: if attempt < max_retries - 1: # 将错误信息e补充到prompt中,让LLM重试 prompt += f"\n上次生成的代码执行出错:{str(e)}。请修正代码。" else: raise Exception(f“代码生成失败,最终错误:{str(e)}”) ``` 4. **操作的可逆性与审计**:所有通过LLM生成并执行的操作,都应该被记录到一个“操作历史”或“数据谱系”中。记录内容包括:原始请求、生成的代码、执行结果摘要(如影响了多少行)。这为用户提供了“撤销”某一步操作的可能,也便于审计和复现整个数据准备流程。 ### 3.3 复杂任务的处理:从单轮对话到智能体工作流 简单的“查找异常值”、“重命名列”可以单轮对话解决。但面对复杂请求,如“分析各产品季度销售趋势,并找出增长最快的三个产品”,这需要多个步骤:数据透视、排序、筛选、可视化。这时,系统需要具备**任务规划和分解**的能力。 这通常通过引入**智能体(Agent)**框架来实现。系统内的LLM扮演一个“规划者”角色: 1. **规划**:将复杂目标分解为子任务序列。[子任务1:确保‘OrderDate’是日期格式并提取季度],[子任务2:按产品和季度分组计算销售总额],[子任务3:计算季度环比增长率],[子任务4:筛选并排序]。 2. **执行**:为每个子任务生成并执行代码。 3. **观察**:检查每个子任务的结果,判断是否成功,数据状态是否如预期。 4. **循环**:根据观察结果,决定继续下一个子任务,还是重新规划。 这个过程类似于AutoGPT等智能体的“思考-行动-观察”循环。对于Pneuma-Seeker,关键在于如何为数据操作这个特定领域设计有效的子任务动作空间和状态评估机制。 ## 4. 系统搭建实操与核心环节实现 假设我们要从零开始构建一个最小可行产品(MVP)级别的Pneuma-Seeker。技术栈可以选择`Python`作为后端,`Streamlit`或`Gradio`构建简单前端,LLM服务可以使用OpenAI API或本地部署的`Qwen`、`Llama`等开源模型。 ### 4.1 环境准备与依赖安装 首先,创建一个干净的Python环境并安装核心库。 ```bash # 创建并激活虚拟环境 python -m venv pneuma_env source pneuma_env/bin/activate # Linux/Mac # pneuma_env\Scripts\activate # Windows # 安装核心依赖 pip install pandas numpy openai streamlit # 基础数据处理、LLM接口、Web界面 pip install python-dotenv # 管理API密钥 # 如果使用本地模型,可能需要安装 transformers, torch, vllm等 # pip install transformers torch4.2 核心模块代码实现
我们实现一个最简化的单轮对话核心引擎。
# core_engine.py import pandas as pd import openai from dotenv import load_dotenv import os import ast import re load_dotenv() class PneumaSeekerCore: def __init__(self, llm_api_key=None, model="gpt-4"): self.llm_api_key = llm_api_key or os.getenv("OPENAI_API_KEY") self.client = openai.OpenAI(api_key=self.llm_api_key) self.model = model self.df = None # 当前处理的DataFrame self.conversation_history = [] # 记录对话历史 def load_data(self, file_path): """加载表格数据""" if file_path.endswith('.csv'): self.df = pd.read_csv(file_path) elif file_path.endswith(('.xls', '.xlsx')): self.df = pd.read_excel(file_path) else: raise ValueError("Unsupported file format") return f"数据加载成功,共 {self.df.shape[0]} 行,{self.df.shape[1]} 列。" def _generate_table_context(self): """生成描述表格上下文的提示词部分""" if self.df is None: return "【表格信息】未加载数据。" context = "【表格元数据】\n" context += f"- 行数:{self.df.shape[0]}, 列数:{self.df.shape[1]}\n" context += "- 列信息:\n" for col in self.df.columns: dtype = str(self.df[col].dtype) sample = self.df[col].dropna().head(2).tolist() sample_str = str(sample[:2]) if len(sample) > 0 else "[]" context += f" `{col}` ({dtype}类型): 示例值 {sample_str}\n" context += f"- 数据预览(前3行):\n{self.df.head(3).to_string(index=False)}\n" return context def _generate_prompt(self, user_query): """组装完整的提示词""" system_role = """你是一个专业的数据分析助手,精通Python的Pandas库。请根据用户请求和表格信息,生成安全、正确、简洁的Pandas代码。代码必须针对名为`df`的DataFrame进行操作。禁止使用任何可能危害系统的函数(如os, subprocess, eval, exec)。只输出代码块,不要输出任何解释性文字。""" prompt = f"{system_role}\n\n" prompt += self._generate_table_context() prompt += f"\n【用户请求】\n{user_query}\n\n" prompt += "【输出格式】\n```python\n# 你的代码\n```" return prompt def _extract_code(self, llm_response): """从LLM回复中提取代码块""" # 使用正则表达式匹配 ```python ... ``` 格式的代码块 pattern = r'```python\n(.*?)\n```' matches = re.findall(pattern, llm_response, re.DOTALL) if matches: return matches[0].strip() # 如果没有代码块标记,尝试将整个回复视为代码(风险较高,仅作后备) return llm_response.strip() def _execute_code_safely(self, code, df): """在受限环境中执行生成的代码""" # 这是一个简化的示例。生产环境必须使用真正的沙箱! # 1. 安全检查:禁止危险模块和函数 blacklist = ['__import__', 'eval', 'exec', 'open', 'os.', 'subprocess.', 'system', 'shutil.'] for item in blacklist: if item in code: raise SecurityError(f"检测到禁止使用的关键字: {item}") # 2. 准备安全的全局和局部命名空间 safe_globals = { 'pd': pd, 'np': np, 'df': df.copy() # 传入副本,防止原数据被意外修改 } safe_locals = {} # 3. 执行代码 try: exec(code, safe_globals, safe_locals) except Exception as e: raise ExecutionError(f"代码执行错误: {type(e).__name__}: {e}") # 4. 尝试从命名空间中获取结果DataFrame # 约定:操作结果应保存在变量 `result_df` 中,或者最后一句是DataFrame表达式 result_df = safe_locals.get('result_df', None) if result_df is None and 'df' in safe_locals: # 如果代码直接修改了传入的df result_df = safe_locals['df'] elif result_df is None: # 尝试获取最后一个赋值语句的结果 # 注意:这是一个非常简陋的启发式方法,生产环境需要更稳健的方案 pass return result_df if result_df is not None else df def process_query(self, user_query): """处理用户查询的主流程""" # 1. 生成提示词 prompt = self._generate_prompt(user_query) # 2. 调用LLM response = self.client.chat.completions.create( model=self.model, messages=[{"role": "user", "content": prompt}], temperature=0.1, # 低温度,保证代码生成的确定性 max_tokens=1000 ) llm_output = response.choices[0].message.content # 3. 提取代码 code_to_execute = self._extract_code(llm_output) if not code_to_execute: return None, "LLM未生成有效的代码。", llm_output # 4. 执行代码 try: result_df = self._execute_code_safely(code_to_execute, self.df) # 更新当前数据框 self.df = result_df.copy() # 记录对话 self.conversation_history.append({ "query": user_query, "code": code_to_execute, "result_shape": result_df.shape }) return result_df, "执行成功!", code_to_execute except SecurityError as se: return None, f"安全拦截:{str(se)}", code_to_execute except ExecutionError as ee: return None, f"执行错误:{str(ee)}", code_to_execute except Exception as e: return None, f"未知错误:{str(e)}", code_to_execute # 定义自定义异常 class SecurityError(Exception): pass class ExecutionError(Exception): pass4.3 构建交互式前端界面
使用Streamlit可以快速构建一个Web界面。
# app.py import streamlit as st from core_engine import PneumaSeekerCore import pandas as pd st.set_page_config(page_title="Pneuma-Seeker MVP", layout="wide") st.title("🧠 Pneuma-Seeker: 智能表格数据助手") # 初始化会话状态 if 'core' not in st.session_state: st.session_state.core = PneumaSeekerCore() if 'df' not in st.session_state: st.session_state.df = None if 'history' not in st.session_state: st.session_state.history = [] # 侧边栏:数据上传 with st.sidebar: st.header("1. 上传数据") uploaded_file = st.file_uploader("选择CSV或Excel文件", type=['csv', 'xlsx', 'xls']) if uploaded_file is not None: try: # 根据文件类型读取 if uploaded_file.name.endswith('.csv'): st.session_state.df = pd.read_csv(uploaded_file) else: st.session_state.df = pd.read_excel(uploaded_file) st.session_state.core.df = st.session_state.df st.success(f"数据加载成功!形状: {st.session_state.df.shape}") # 显示预览 with st.expander("数据预览"): st.dataframe(st.session_state.df.head(10)) except Exception as e: st.error(f"文件读取失败: {e}") # 主界面 if st.session_state.df is not None: col1, col2 = st.columns([2, 1]) with col1: st.header("当前数据") st.dataframe(st.session_state.df, use_container_width=True) with col2: st.header("数据概览") st.metric("总行数", st.session_state.df.shape[0]) st.metric("总列数", st.session_state.df.shape[1]) # 显示列信息 with st.expander("列详情"): for col in st.session_state.df.columns: dtype = st.session_state.df[col].dtype non_null = st.session_state.df[col].count() st.text(f"{col}: {dtype} (非空值: {non_null})") st.divider() st.header("💬 与数据对话") # 输入查询 user_query = st.text_area( "输入你的数据请求(例如:'找出所有缺失值','计算每类产品的平均价格'):", height=100, key="query_input" ) if st.button("执行", type="primary") and user_query: with st.spinner("AI正在思考并生成代码..."): result_df, message, generated_code = st.session_state.core.process_query(user_query) # 显示结果 if result_df is not None: st.success(message) st.session_state.df = result_df st.session_state.core.df = result_df # 显示新数据 st.subheader("处理后的数据") st.dataframe(result_df, use_container_width=True) else: st.error(message) # 显示生成的代码 with st.expander("查看生成的代码"): st.code(generated_code, language='python') # 记录历史 st.session_state.history.append({ "query": user_query, "code": generated_code, "message": message }) # 显示操作历史 if st.session_state.history: st.divider() st.header("📜 操作历史") for i, entry in enumerate(reversed(st.session_state.history[-5:]), 1): # 显示最近5条 with st.expander(f"请求 {i}: {entry['query'][:50]}..."): st.text(f"状态: {entry['message']}") st.code(entry['code'], language='python') else: st.info("👈 请在左侧上传一个数据文件以开始。")这个MVP实现了核心流程:上传数据、用自然语言查询、生成并执行代码、展示结果和代码。虽然简陋,但它清晰地展示了Pneuma-Seeker的核心交互模式。
5. 常见问题、挑战与优化方向实录
在实际开发和测试这样一个系统时,你会遇到一系列预料之中和预料之外的挑战。以下是我在构建类似系统时踩过的坑和思考的解决方案。
5.1 LLM相关挑战
生成代码的稳定性与准确性:
- 问题:LLM可能会“幻觉”出一些不存在的Pandas方法或参数,或者生成的代码逻辑正确但语法有细微错误。
- 对策:
- Few-shot Prompting:在提示词中提供几个高质量的例子(例如,一个处理缺失值的例子,一个重命名的例子),让LLM模仿。
- 后处理与验证:生成代码后,可以用
ast模块进行语法解析,确保没有语法错误。对于简单的逻辑错误,可以尝试让LLM解释其生成的代码逻辑,或对小型测试数据集运行以快速验证。 - 使用更专业的模型:通用LLM在代码生成上可能不如专门在代码上微调过的模型(如CodeLlama、DeepSeek-Coder)。如果任务以代码生成为主,优先考虑这类模型。
上下文长度限制与大型表格:
- 问题:表格有上百列、数十万行,远超LLM的上下文窗口。
- 对策:
- 智能摘要:不发送所有数据。发送列名、数据类型、统计摘要(均值、方差、唯一值数)、以及可能的关键列(如主键、目标变量)的少量样本。对于行数据,可以发送随机采样或分层采样的几百行。
- 分块处理与记忆:对于涉及全表扫描的操作(如“找出所有重复行”),让LLM生成可以处理大数据集的代码(利用Pandas的向量化操作),而不是让LLM自己“看”完所有数据。系统需要维护一个“数据状态”的记忆,让LLM知道当前数据已经经过了哪些变换。
成本与延迟:
- 问题:频繁调用商用LLM API成本高,且网络请求带来延迟。
- 对策:
- 本地模型部署:对于企业内部应用,部署如Qwen、Llama等开源模型是更可控、成本更低的选择。虽然能力可能稍弱,但通过精心设计的提示词和任务分解,可以满足大部分场景。
- 缓存与优化:对常见的、重复性的查询(如“显示前10行”、“查看数据类型”)可以缓存LLM的回复或直接由系统规则处理,无需调用LLM。
- 异步处理:对于耗时的复杂任务,可以改为异步处理,先返回任务ID,处理完成后通知用户。
5.2 系统与工程挑战
安全性:
- 问题:动态执行代码是最大的安全风险。
- 对策:如前所述,沙箱是必须的。Docker容器是最佳实践之一,可以严格限制资源(CPU、内存)、网络和文件系统访问。此外,需要建立代码审查和黑白名单机制。
错误处理与用户体验:
- 问题:代码执行失败时,给用户一个Python的
Traceback堆栈信息是极不友好的。 - 对策:系统需要将底层的技术错误“翻译”成用户能理解的业务语言。例如,
KeyError可以提示“您提到的列名‘XX’在表中不存在,当前存在的列有:...”。建立一套常见的错误类型到友好提示的映射。
- 问题:代码执行失败时,给用户一个Python的
复杂查询与多轮对话:
- 问题:用户的需求往往是递进的。“先找出异常值” -> “然后对这些异常值进行分组统计” -> “最后把结果导出”。系统需要记住之前的操作和当前数据状态。
- 对策:维护一个会话状态。这个状态不仅包括当前的
DataFrame,还包括一个精简的“操作历史”描述,在每次生成提示词时都附加上去,让LLM知道“我们现在在哪儿”。例如:“【历史操作】已过滤出‘销售额’大于10000的记录,并对‘产品类别’进行了分组。”
5.3 进阶优化方向
当基础功能跑通后,可以考虑以下方向让系统变得更强大、更智能:
主动数据发现与建议:不等待用户提问,系统加载数据后主动分析,生成一份“数据健康报告”。例如:“发现‘客户年龄’列有5%的缺失值;‘订单金额’列有3个负值,可能是录入错误;‘注册日期’和‘最后登录日期’格式不统一。”并给出修复建议。这需要集成一些自动化的数据剖析库。
可视化生成:将自然语言请求扩展到可视化。“为销售额随时间变化画一个折线图”。LLM需要生成使用
Matplotlib或Plotly的代码。这要求提示词中包含图表类型的知识。与领域知识结合:为特定行业(如金融、电商、生物信息)定制化。在提示词中注入领域术语和常见分析模式,让系统更“懂行”。例如,在电商场景,它能理解“GMV”、“复购率”、“SKU”等概念。
可复用的“技能”库:将用户成功执行过的复杂操作(如一个特定的数据清洗流程)保存为“技能”或“模板”。其他用户遇到类似任务时,可以直接调用或稍作修改,避免重复劳动,也降低了LLM出错的概率。
构建Pneuma-Seeker这样的系统,是一个持续迭代和打磨的过程。它不仅仅是LLM技术的一个应用,更是对传统人机交互方式的一次深刻反思。从我的实践经验来看,最大的收获不是做出了一个多酷的工具,而是在这个过程中,被迫更深入地思考了“我们到底想从数据中获得什么”以及“如何让机器更好地理解人的意图”这两个根本问题。这条路还很长,但每一次让机器成功理解并完成一个模糊的数据请求时,那种感觉,就像为冰冷的数据注入了一丝“气息”。
