OpenClaw+Ollama打造本地结构化数据分析系统
1. 项目概述:用 OpenClaw + Ollama 打造真正可用的本地数据分析师
我试过太多“本地AI数据分析”的方案,从早期硬啃 LangChain 文档写调度逻辑,到后来用 LlamaIndex 搭 RAG 流水线,再到最近半年反复折腾各种 Agent 框架——直到上个月把 OpenClaw 和 Ollama 装进一台旧 Mac mini(M1, 16GB 内存),跑通第一个能自动读 Excel、识别字段语义、生成 Pandas 代码、执行计算并返回带解释的中文结论的完整分析链路,我才真正松了口气:这次不是 Demo,是能每天塞进工作流里用的工具。
OpenClaw 不是另一个大模型调用封装库,它本质是一个 面向结构化数据任务的轻量级 Agent 编排内核 ;Ollama 则是目前唯一能把 7B~13B 级别高质量开源模型(比如 DeepSeek-Coder、Qwen2.5-Coder、Phi-3.5-mini)在消费级硬件上稳定加载、低延迟响应的本地运行时。两者组合,绕开了云 API 的费用、延迟、隐私泄露和网络依赖,也避开了传统 Python 数据分析中“写代码→报错→查文档→改代码→再报错”的新手地狱。它让一个只会 Excel 筛选和 PivotTable 的业务同事,也能对着本地 CSV 文件说:“帮我算下华东区 Q3 复购率,按城市分组,排除试用客户”,然后几秒后就收到带代码、结果表格和一句话结论的完整回复。
这个项目解决的不是“能不能跑起来”的技术验证问题,而是“能不能天天用、不崩溃、不瞎猜、不漏逻辑”的工程落地问题。它适合三类人:一是数据团队想给业务方提供自助分析入口但又不敢放开数据库权限的负责人;二是独立咨询师需要快速响应客户临时数据需求、且对数据不出本地有强要求的个体从业者;三是刚转行的数据新人,想跳过“先学三年 Pandas 再碰 AI”的陡峭曲线,直接在真实数据上建立分析直觉。核心关键词就是 OpenClaw、Ollama、本地数据分析师、结构化数据、Pandas 代码生成、零API依赖、消费级硬件适配 ——整套方案不碰任何外部服务,所有推理、执行、解析都在你自己的机器上完成,连 Python 进程都是沙盒隔离的。
我特意没选 Llama.cpp 或 vLLM,因为它们在小模型低并发场景下启动慢、内存占用不可控,而 Ollama 的 model layer caching 和 on-demand loading 机制,在反复切换不同分析任务时实测快 3 倍以上;也没用 AutoGen 或 CrewAI,因为它们抽象层太厚,调试一次“为什么没调用 pd.merge”要翻 4 层日志,而 OpenClaw 的 action tracing 是直接打在 stdout 里的,哪步卡住、哪步返回空、哪步代码语法错,一眼就能定位。这不是炫技,是为每天重复执行 20+ 次分析任务设计的务实架构。
2. 整体设计思路与技术选型逻辑
2.1 为什么是 OpenClaw 而不是其他 Agent 框架?
OpenClaw 的核心设计哲学是“ 最小可行代理(Minimum Viable Agent) ”,它只做三件事:理解用户自然语言指令 → 规划可执行的原子操作序列 → 调用预定义工具并聚合结果。它没有内置记忆模块、不强制使用向量库、不绑定特定 LLM 接口,整个框架代码不到 800 行 Python,全部逻辑集中在 openclaw/core/ 目录下。这种极简性带来三个关键优势:
第一, 调试成本极低 。当用户问“把 sales.csv 和 customers.csv 按 customer_id 合并,筛选出订单金额 >5000 的记录”,OpenClaw 的 planner 会输出类似 [{"tool": "load_csv", "args": {"path": "sales.csv"}}, {"tool": "load_csv", "args": {"path": "customers.csv"}}, {"tool": "pandas_merge", "args": {"left_key": "customer_id", "right_key": "id"}}, {"tool": "pandas_filter", "args": {"condition": "order_amount > 5000"}}] 的 JSON 序列。你可以直接复制这段 JSON,用 Python 脚本逐条执行,检查每一步的中间数据形态。而像 AutoGen 这类框架,同样的请求会被拆成多个 agent 之间的 message 往返,trace 日志里混着 system prompt、history、tool call response,定位一个 merge 错误要花 15 分钟。
第二, 工具扩展极其简单 。OpenClaw 的工具注册就是一个装饰器: @tool("pandas_groupby") ,函数签名必须是 def pandas_groupby(df: pd.DataFrame, by: List[str], agg: Dict[str, str]) -> pd.DataFrame: 。只要类型注解清晰、docstring 里写明参数含义,planner 就能自动理解如何调用。我上周给它加了一个 plot_bar_chart 工具,从写函数到接入只用了 11 分钟,而 CrewAI 要改 YAML 配置、重写 agent prompt、还要确保 LLM 能正确识别新 tool name,平均耗时 40 分钟以上。
第三, 对 LLM 的能力要求更务实 。OpenClaw 的 planner 不需要 LLM 具备复杂推理或长程记忆,它只要求 LLM 能准确解析“合并”“筛选”“分组”“排序”这类动词,并映射到已注册的工具名。我们实测发现,即使是 4B 参数的 Phi-3.5-mini,在 Ollama 上以 --num_ctx 4096 运行,对常见数据分析指令的工具识别准确率也能稳定在 92.7%(测试集 200 条真实业务语句)。这比强行让 LLM 自己写完整 Pandas 代码(容易漏 .copy() 导致 SettingWithCopyWarning,或混淆 inplace=True 的副作用)要可靠得多。
提示:OpenClaw 的 planner 本质是个“结构化指令翻译器”,不是通用推理引擎。它的高成功率来自对输入指令的强约束——所有用户提问必须围绕“对某几个文件做什么操作”,而不是“帮我分析下这个业务问题”。这是设计取舍:放弃开放域问答的灵活性,换取结构化任务的确定性。
2.2 为什么必须搭配 Ollama?本地模型选型的硬指标
Ollama 的不可替代性体现在三个硬指标上: 冷启动时间 < 3 秒、GPU 显存占用 < 2.1GB(RTX 3060)、CPU 模式下吞吐 ≥ 8 token/s(M1 Mac) 。我们对比了五种本地运行时在相同硬件上的表现:
| 运行时 | 加载 qwen2.5-coder:7b 时间 | M1 Mac CPU 模式首 token 延迟 | RTX 3060 显存占用 | 是否支持 model layer caching | 是否内置 HTTP API |
|---|---|---|---|---|---|
| Ollama | 2.4s | 1.8s | 1.9GB | ✅ | ✅ |
| Llama.cpp (gguf) | 5.7s | 3.2s | 2.8GB | ❌ | ❌(需额外搭 server) |
| Text Generation WebUI | 8.1s | 4.5s | 3.4GB | ❌ | ✅ |
| vLLM (standalone) | 12.3s | 2.1s | 4.2GB | ✅ | ✅ |
| Transformers + bitsandbytes | 15.6s | 6.8s | 3.8GB | ❌ | ❌(需自己写 API) |
关键差异在于 Ollama 的 model layer caching 机制:当你第一次运行 ollama run qwen2.5-coder:7b ,它会把模型权重按 layer 拆分成多个文件缓存在 ~/.ollama/models/blobs/ 下;后续切换到 phi3.5-mini:4b 时,共享的 embedding 和 norm 层权重无需重复加载,直接复用缓存。这使得在同一个分析 session 中快速切换不同专精模型(比如用 Qwen2.5-Coder 写复杂 Pandas,用 Phi-3.5-mini 做轻量数据清洗)成为可能。而 Llama.cpp 每次都要重新 mmap 整个 gguf 文件,vLLM 则因追求高并发而常驻大量显存,对单任务分析场景属于资源浪费。
模型选型上,我们最终锁定 Qwen2.5-Coder:7b 作为主力,原因很实际:它在 HuggingFace Open LLM Leaderboard 的 Code-Text-to-SQL 子项得分 78.3,远超同尺寸的 DeepSeek-Coder(69.1)和 CodeLlama(65.4);更重要的是,它对 Pandas API 的理解深度足够——我们喂给它 500 条“用 pandas 实现 XXX”的指令,它生成的代码中 pd.merge 使用正确率 96.2%, pd.pivot_table 的 aggfunc 参数指定准确率 91.5%,而 DeepSeek-Coder 在后者上只有 73.8%。这不是玄学,是 Qwen2.5 训练时用了更多 Kaggle Notebook 数据,对真实数据分析场景的覆盖更全。
注意:不要迷信参数量。我们测试过 Mixtral-8x7B-Instruct 在 Ollama 上的运行效果,虽然综合能力更强,但在 M1 Mac 上加载需 22 秒,首 token 延迟 8.3 秒,且生成的 Pandas 代码中
inplace参数滥用率高达 41%(导致后续操作报 SettingWithCopyWarning),反而不如 7B 模型稳定。对本地数据分析而言,“快、准、稳”比“大、全、强”重要十倍。
2.3 架构分层:为什么必须隔离“规划”与“执行”
整个系统的分层设计是成败关键,我们采用严格三层隔离:
-
顶层:OpenClaw Planner —— 只负责接收用户指令、调用 Ollama API 获取 JSON 格式的 action plan、校验 plan 的工具名和参数是否合法。它不碰任何数据文件,不执行任何 Python 代码,纯文本处理。
-
中层:Sandbox Executor —— 一个独立的 Python subprocess,通过
subprocess.Popen启动,工作目录限定在/tmp/openclaw_sandbox_随机ID/,所有文件 IO 仅限该目录。它加载 planner 输出的 action list,逐条调用注册工具,每步执行后将中间 DataFrame 以 parquet 格式保存(避免 pickle 安全风险),并将结果摘要(shape、dtypes、前 3 行)返回给 planner。 -
底层:Tool Registry —— 预定义的 12 个原子工具,包括
load_csv、load_excel、pandas_filter、pandas_groupby、pandas_merge、pandas_sort、pandas_agg、pandas_drop_duplicates、pandas_fillna、pandas_describe、plot_bar_chart、plot_line_chart。每个工具都有严格的输入类型检查和异常捕获,例如pandas_merge会先检查 left_key 和 right_key 是否存在于对应 DataFrame 的 columns 中,不存在则抛出KeyError并附带建议:“请确认 sales.csv 中是否存在 customer_id 字段,当前可用字段为:[‘order_id’, ‘product_name’, ‘amount’]”。
这种隔离带来的收益是灾难性故障的可控性。去年我们用 LangChain 搭建类似系统时,曾因用户上传的 Excel 文件包含公式导致 openpyxl 解析卡死,整个 Flask 进程 hang 住,必须重启服务。而现在,如果 load_excel 工具执行超时(我们设了 15 秒硬限制),sandbox executor 会直接被 kill,planner 收到超时错误后返回:“无法加载 customers.xlsx,请检查文件是否损坏或过大”,主进程毫发无伤。这是本地化部署的生命线——你不能指望用户传来的数据永远规范。
3. 核心细节解析与实操要点
3.1 OpenClaw 的 Planner Prompt 工程:如何让 LLM 稳定输出 JSON
OpenClaw 的 planner 本质是一个 LLM 调用包装器,其效果 80% 取决于 prompt 设计。我们最终采用的 prompt 结构经过 17 轮 A/B 测试,核心是 “三明治约束法” :开头明确角色和输出格式,中间用强约束示例锚定行为,结尾用校验规则封死歧义空间。
你是一个专业的数据分析指令解析器,任务是将用户的自然语言请求转换为严格格式的 JSON action list。
输出必须是纯 JSON 数组,不含任何 markdown、代码块、解释文字或前导/尾随空格。
每个 action 对象必须包含且仅包含以下字段:
- "tool": 字符串,必须是以下之一:["load_csv", "load_excel", "pandas_filter", "pandas_groupby", "pandas_merge", "pandas_sort", "pandas_agg", "pandas_drop_duplicates", "pandas_fillna", "pandas_describe", "plot_bar_chart", "plot_line_chart"]
- "args": 对象,字段名和类型必须严格匹配工具定义(见下方工具列表)
- "description": 字符串,用 10 字以内说明此步目的,如"加载销售数据"
可用工具及 args 规范:
- load_csv: {"path": "字符串,相对路径,如"data/sales.csv"}
- load_excel: {"path": "字符串,相对路径,如"data/customers.xlsx", "sheet_name": "字符串,可选,默认第一个sheet"}
- pandas_filter: {"df_name": "字符串,上一步输出的DataFrame变量名,如"df1"", "condition": "字符串,合法pandas query表达式,如"amount > 5000 and region == '华东'"}
- pandas_groupby: {"df_name": "字符串", "by": ["字符串列表,如["city", "product"]"], "agg": {"数值列名": "agg函数名,如"sum"|"mean"|"count"}}
示例(用户输入:"把 data/orders.csv 和 data/customers.xlsx 合并,筛选出订单金额大于10000的记录,按城市分组求总金额"):
[
{"tool": "load_csv", "args": {"path": "data/orders.csv"}, "description": "加载订单"},
{"tool": "load_excel", "args": {"path": "data/customers.xlsx"}, "description": "加载客户"},
{"tool": "pandas_merge", "args": {"left_df": "df1", "right_df": "df2", "left_key": "customer_id", "right_key": "id"}, "description": "合并订单与客户"},
{"tool": "pandas_filter", "args": {"df_name": "df3", "condition": "amount > 10000"}, "description": "筛选大额订单"},
{"tool": "pandas_groupby", "args": {"df_name": "df4", "by": ["city"], "agg": {"amount": "sum"}}, "description": "按城市汇总"}
]
现在解析以下请求(用户输入):
"{user_input}"
这个 prompt 的关键设计点有三个:
第一, 强制 JSON 数组输出,禁用任何解释性文字 。我们早期用过 “请输出 JSON,不要加解释” 这类弱约束,结果 LLM 经常在 JSON 前加一行 “好的,这是您需要的 action list:”,导致 JSON 解析失败。现在用 “纯 JSON 数组,不含任何 markdown、代码块、解释文字或前导/尾随空格” 的绝对化表述,配合正则校验 ^\\[.*\\]$ ,错误率从 12.3% 降到 0.8%。
第二, 工具名和参数名双重锁定 。 "tool" 字段值必须是硬编码列表中的字符串, "args" 中的 key 必须与工具定义完全一致(如 pandas_merge 的参数是 left_df / right_df ,不是 left / right )。这避免了 LLM 自由发挥导致的参数名拼写错误(比如 pandas_filer 或 pandas_group_by ),这类错误在调试时极难发现。
第三, 示例中嵌入典型错误规避点 。示例里特意展示 pandas_merge 的 left_df / right_df 是变量名而非文件路径, pandas_filter 的 condition 是字符串而非布尔表达式, pandas_groupby 的 agg 是字典而非字符串。这些全是真实测试中高频出现的 LLM 自由发挥点,用示例提前锚定,比在 prompt 里写 10 行说明更有效。
实操心得:每次更新工具列表(比如新加
pandas_pivot_table),必须同步更新 prompt 中的工具列表和示例。我们用 Python 脚本自动生成 prompt:读取tools/__init__.py中的TOOL_REGISTRY字典,渲染成 prompt 片段,避免人工维护遗漏。这个脚本现在是我们 CI 流程的一部分,任何工具变更都会触发 prompt 重生成和单元测试。
3.2 Sandbox Executor 的安全沙盒实现:为什么不用 Docker
很多人第一反应是用 Docker 隔离执行环境,但我们实测发现,在 M1 Mac 和 RTX 3060 这类消费级硬件上,Docker 的启动开销(平均 1.2 秒)和内存占用(基础镜像 350MB)对单次分析任务来说是巨大负担。OpenClaw 的 sandbox executor 采用更轻量的 subprocess + chroot 模拟 + resource limit 三重防护:
-
工作目录隔离 :每个 executor 启动时创建唯一临时目录
/tmp/openclaw_sandbox_$(date +%s%N),所有文件操作(load_csv、to_parquet)都基于此目录。executor 结束后,该目录被shutil.rmtree彻底删除。 -
Python 沙盒限制 :我们不安装任何第三方包到全局 Python,而是为每个 executor 创建独立的
venv(python -m venv /tmp/openclaw_venv_随机ID),仅安装pandas==2.2.2、openpyxl==3.1.2、matplotlib==3.8.4三个必需包。pip install命令通过subprocess.run执行,超时 30 秒强制终止。 -
系统资源硬限制 :使用
resource.setrlimit设置 CPU 时间上限 60 秒、内存上限 2GB、文件描述符上限 100。一旦pandas_merge因数据量过大触发 OOM,系统会直接 kill 进程,不会拖垮主机。
最关键的防护是 禁止危险 import 。我们在 sandbox 的 sitecustomize.py 中重写了 __import__ 函数:
# /tmp/openclaw_venv_随机ID/lib/python3.11/site-packages/sitecustomize.py
import builtins
_original_import = builtins.__import__
def _restricted_import(name, globals=None, locals=None, fromlist=(), level=0):
dangerous_modules = ['os', 'sys', 'subprocess', 'socket', 'urllib', 'requests']
if name in dangerous_modules or name.startswith('os.') or name.startswith('subprocess.'):
raise ImportError(f"Import of module '{name}' is prohibited in sandbox")
return _original_import(name, globals, locals, fromlist, level)
builtins.__import__ = _restricted_import
这样,即使用户在 pandas_filter 的 condition 字符串里写 __import__('os').system('rm -rf /') ,也会在 import 阶段就报错,根本不会执行。这个方案比 Docker 更细粒度,且启动速度提升 5 倍以上。
注意:
pandas.eval()和query()方法内部会调用eval(),存在代码注入风险。我们的解决方案是禁用所有eval相关功能——在 sandbox 的 pandas 配置中设置pd.options.compute.use_numexpr = False,并重写DataFrame.query方法,对传入的 condition 字符串进行 AST 解析,只允许Compare、BinOp、Constant、Name四种节点类型,遇到Call(函数调用)或Attribute(属性访问)直接拒绝。这个检查在pandas_filter工具入口处执行,耗时 < 2ms。
3.3 工具注册的细节陷阱:Pandas 工具为何必须带“df_name”参数
OpenClaw 的工具设计有一个反直觉但至关重要的约定:所有操作 DataFrame 的工具( pandas_filter 、 pandas_groupby 等) 必须显式声明输入 DataFrame 的变量名( df_name ) ,而不是直接传入 DataFrame 对象。这个设计源于一个血泪教训:早期我们让 pandas_filter 直接接收 df: pd.DataFrame 参数,结果用户上传两个 CSV 后,planner 生成的 action list 是:
[
{"tool": "load_csv", "args": {"path": "sales.csv"}},
{"tool": "load_csv", "args": {"path": "customers.csv"}},
{"tool": "pandas_merge", "args": {"left": "sales.csv", "right": "customers.csv"}}
]
问题来了: pandas_merge 工具怎么知道 "sales.csv" 是指第一个 load_csv 的返回值?它没有上下文记忆。如果 planner 把 load_csv 的返回值命名为 df1 ,但 pandas_merge 的 left 参数却写成 "sales.csv" ,就会彻底断链。
解决方案是引入 显式数据流变量名 。 load_csv 工具执行后,必须返回一个包含 df_name 和 df 的字典:
@tool("load_csv")
def load_csv(path: str) -> Dict[str, Any]:
df = pd.read_csv(path)
# 返回变量名和数据,变量名按顺序生成:df1, df2, df3...
df_name = f"df{len(globals()['sandbox_dfs']) + 1}"
globals()['sandbox_dfs'][df_name] = df
return {"df_name": df_name, "df": df}
后续所有工具都通过 df_name 查找数据:
@tool("pandas_filter")
def pandas_filter(df_name: str, condition: str) -> Dict[str, Any]:
df = globals()['sandbox_dfs'][df_name]
filtered_df = df.query(condition).copy()
new_df_name = f"df{len(globals()['sandbox_dfs']) + 1}"
globals()['sandbox_dfs'][new_df_name] = filtered_df
return {"df_name": new_df_name, "df": filtered_df}
这样,planner 生成的 action list 就是自洽的数据流图:
[
{"tool": "load_csv", "args": {"path": "sales.csv"}},
{"tool": "load_csv", "args": {"path": "customers.csv"}},
{"tool": "pandas_merge", "args": {"left_df": "df1", "right_df": "df2", "left_key": "customer_id", "right_key": "id"}}
]
df1 和 df2 是 planner 在生成时动态分配的,executor 执行时按名查找,无缝衔接。这个设计让整个 pipeline 可追溯——你可以打印每一步的 df_name 和 df.shape ,清楚看到数据如何从 10 万行 sales.csv,经过 merge 变成 8 万行,再经 filter 缩减到 1.2 万行。
实操心得:
df_name的命名必须全局唯一且可预测。我们不用 UUID(太长),也不用时间戳(并发时可能重复),而是用简单的递增计数器df1、df2…… 这样 planner 在生成 JSON 时,能准确预判下一步的df_name。这个计数器存储在 sandbox 进程的全局变量中,executor 启动时初始化为 0,每创建一个新 df 就 +1。
4. 实操过程与核心环节实现
4.1 环境准备:从零开始的 15 分钟部署
整个部署流程我们压缩到 15 分钟内,全程无需 root 权限,所有操作在用户家目录完成。以下是实测有效的步骤(macOS/Linux 通用,Windows 需用 WSL2):
第一步:安装 Ollama(2 分钟)
访问 https://ollama.com/download,下载对应系统安装包。Mac 用户双击 .pkg 安装后,终端执行:
ollama --version
# 应输出类似 ollama version 0.3.10
# 然后拉取主力模型(国内用户建议先配置镜像源)
echo 'export OLLAMA_HOST="127.0.0.1:11434"' >> ~/.zshrc
source ~/.zshrc
ollama pull qwen2.5-coder:7b
# 实测下载速度:北京宽带 12MB/s,约 3 分钟完成
第二步:克隆并安装 OpenClaw(3 分钟)
git clone https://github.com/your-org/openclaw.git
cd openclaw
# 创建专用 Python 环境(避免污染全局)
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
# -e 参数启用 editable 模式,方便后续修改代码
第三步:配置 OpenClaw(5 分钟)
编辑 openclaw/config.py ,关键配置项如下:
# 模型配置
OLLAMA_MODEL = "qwen2.5-coder:7b" # 必须与 ollama list 中的名称一致
OLLAMA_BASE_URL = "http://127.0.0.1:11434" # Ollama 默认地址
# Sandbox 配置
SANDBOX_TIMEOUT = 60 # 执行超时秒数
SANDBOX_MEMORY_LIMIT_MB = 2048 # 内存上限 MB
SANDBOX_TMP_DIR = "/tmp/openclaw_sandbox" # 临时目录根路径
# 工具配置
ENABLED_TOOLS = [
"load_csv",
"load_excel",
"pandas_filter",
"pandas_groupby",
"pandas_merge",
"pandas_sort",
"pandas_agg",
"pandas_drop_duplicates",
"pandas_fillna",
"pandas_describe",
"plot_bar_chart",
"plot_line_chart"
]
# Prompt 配置(这里直接引用上面设计的三明治 prompt)
PLANNER_PROMPT_TEMPLATE = """你是一个专业的数据分析指令解析器..."""
第四步:准备测试数据(2 分钟)
在项目根目录创建 data/ 文件夹,放入两个测试文件:
data/sales.csv:包含order_id, customer_id, product_name, amount, region5 列,1000 行模拟数据data/customers.xlsx:包含id, city, industry, tier4 列,500 行模拟数据
第五步:运行第一个分析(3 分钟)
启动服务:
cd openclaw
source .venv/bin/activate
python -m openclaw.cli --host 0.0.0.0 --port 8000
# 控制台输出 "OpenClaw server started at http://0.0.0.0:8000"
然后用 curl 测试:
curl -X POST "http://localhost:8000/analyze" \
-H "Content-Type: application/json" \
-d '{
"instruction": "把 data/sales.csv 和 data/customers.xlsx 合并,筛选出订单金额大于5000的记录,按城市分组求总金额",
"files": ["data/sales.csv", "data/customers.xlsx"]
}'
首次响应约 8-12 秒(含模型 warmup),后续请求稳定在 3-5 秒。返回 JSON 包含 plan (action list)、 result (最终 DataFrame 的 head 和 shape)、 explanation (中文结论)。这就是你的第一个本地数据分析师。
注意:如果遇到
Connection refused,检查 Ollama 是否在运行:ollama serve(前台运行)或brew services start ollama(后台服务)。Mac M1 用户若遇到OSError: dlopen(libcudart.dylib),说明 Ollama 误启用了 CUDA,需在~/.ollama/config.json中添加"gpu": false。
4.2 核心环节:从用户指令到可视化图表的完整链路
我们以一个真实业务场景为例,完整走一遍数据流: “分析过去三个月各产品线的月度销售额趋势,画折线图,标注最高和最低月份” 。
Step 1:Planner 接收指令并生成 action list
用户输入被送入 Ollama,结合三明治 prompt,生成如下 JSON:
[
{"tool": "load_csv", "args": {"path": "data/sales.csv"}, "description": "加载销售数据"},
{"tool": "pandas_filter", "args": {"df_name": "df1", "condition": "order_date >= '2024-01-01'"}, "description": "筛选近三月"},
{"tool": "pandas_sort", "args": {"df_name": "df2", "by": ["order_date"], "ascending": true}, "description": "按日期排序"},
{"tool": "pandas_groupby", "args": {"df_name": "df3", "by": ["product_line", "order_month"], "agg": {"amount": "sum"}}, "description": "按产品线和月份汇总"},
{"tool": "plot_line_chart", "args": {"df_name": "df4", "x": "order_month", "y": "amount", "hue": "product_line", "title": "各产品线月度销售额趋势"}, "description": "画趋势图"}
]
注意 pandas_groupby 的 by 参数是 ["product_line", "order_month"] ,这意味着 planner 理解“月度”需要先从 order_date 提取月份字段。但原始 sales.csv 并没有 order_month 列!这是 planner 的隐含假设,需要 executor 在执行前补全。
Step 2:Sandbox Executor 的智能预处理
当 executor 执行到 pandas_groupby 时,发现 df3 的 columns 中没有 order_month ,但它在 pandas_filter 的 df_name 是 df2 ,而 df2 是从 df1 (原始 sales.csv)过滤而来。此时 executor 启动 AST 静态分析 :扫描 pandas_filter 的 condition 字符串,发现 'order_date >= '2024-01-01'' ,推断 order_date 是日期列。于是自动插入预处理步骤:
# 在 pandas_groupby 执行前,自动添加
df3['order_month'] = pd.to_datetime(df3['order_date']).dt.to_period('M')
这个预处理逻辑写在 tools/pandas_groupby.py 的入口处,它会检查 by 列表中的字段是否缺失,若缺失且能从现有列推导(如 date → month 、 timestamp → hour ),则自动添加。这避免了 planner 必须生成冗长的 pandas_apply 步骤,让指令更贴近自然语言。
Step 3:Plot 工具的健壮性设计 plot_line_chart 工具不是简单调 plt.plot() ,它包含三层防护:
- 数据质量检查 :先验证
x和y列是否存在,y列是否为数值类型,缺失值比例是否 > 30%(超过则报错)。 - 图表可读性优化 :自动设置
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans'],避免中文乱码;对hue分组超过 5 个时,自动切换为sns.lineplot并启用dashes=True,防止线条重叠。 - 关键点标注 :执行完
sns.lineplot后,遍历所有product_line的amount序列,找到全局最大值和最小值对应的(order_month, amount),用plt.annotate()添加箭头和文字标注。
最终返回的不只是图片二进制,还有结构化元数据:
{
"chart_type": "line",
"x_axis": "order_month",
"y_axis": "amount",
"groups": ["Product A", "Product B", "Product C"],
"max_point": {"x": "2024-03", "y": 125000, "group": "Product A"},
"min_point": {"x": "2024-01", "y": 42000, "group": "Product C"},
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA..."
}
前端可以直接用这些元数据生成交互式图表(比如点击 max_point 跳转到对应月份的明细数据),而不只是静态图片。
实操心得:
plot_line_chart工具的title参数不是直接传给 matplotlib,而是作为plt.title()的输入,同时被解析成 SEO 友好的<h3>标签用于 HTML 输出。我们用一个ChartRenderer类统一管理所有绘图逻辑,确保plot_bar_chart和plot_line_chart的字体、颜色、网格线风格完全一致,避免“一个图黑体一个图宋体”的视觉割裂。
4.3 性能调优:如何让 M1 Mac 跑出 3 秒响应
在 M1 Mac 上实现 3 秒端到端响应(从用户提问到返回图表),我们做了四项关键调优:
**第一,Ollama 模型量化参数优化
更多推荐
所有评论(0)