别再让 AI「心算」CSV:为 DeepSeek Harness 增加精确的数据分析工具

0 阅读12分钟

之前我写过几个 DeepSeek Harness 相关项目:替换产品名称和 Logo 的品牌插件、给 Web profile 增加独立窗口的桌面插件,以及帮助普通用户安装和启动 DSH 的脚本。

这些项目主要解决「怎么打开、怎么配置、怎么看起来像自己的产品」。

这次我想换一个方向:让 DSH 帮人完成一件具体的日常工作——分析导出的 CSV。

销售流水、费用清单、工单记录、运营报表,很多后台都能导出 CSV。面对这些文件,我们经常想问的并不复杂:

  • 各地区有多少笔已支付订单,金额合计是多少?
  • 哪些费用超过了 500,能不能按金额从高到低列出来?
  • 每种状态有多少条工单?
  • 报表里的零,究竟是真的零,还是金额没有填写?

dsh-csv-analyst 就是围绕这些问题做的一个只读工具插件。用户用自然语言描述需求,模型负责选择条件和解释结果,插件负责读取数据和执行确定的计算。

DSH CSV Analyst 分析效果

截图来自真实 DSH Web 页面和真实模型调用,使用的是仓库内的合成示例数据,不是客户报表。页面中的简洁表格是模型根据已查询结果、按追问整理的版本。

一、为什么还要专门做一个 CSV 插件

DSH 本来就能读取文件、执行命令,也可以让模型写 Python 脚本分析数据。这个插件不是因为「DSH 不会处理 CSV」,也不是要替代 Excel 或 pandas。

我更关心的是:对于常见的筛选和统计,能不能把容易出错的环节固定下来,让使用者不用每次都依赖模型重新写一段计算代码?

例如,把文件内容交给模型后,它可能直接根据预览计算;让模型写脚本,它又需要决定怎么处理空值、用什么数值类型、要不要去掉负数、返回多少行。

这些选择不一定错,但如果没有明确说明,就很难判断结果代表什么。

所以这个插件把工作分成两部分:

用户用自然语言提问
        ↓
模型选择列、筛选条件和统计指标
        ↓
CSV 工具读取完整的限额内文件并执行计算
        ↓
返回数据、记录数、参与计算的数量和分页信息
        ↓
模型整理成适合阅读的说明或表格

模型仍然参与工作,但它不需要凭文字预览「心算」合计,也不需要为了这些基础统计生成一段可执行代码。

二、先看一个最容易被误读的例子

仓库提供了一份很小的 sales.csv:

region,amount,status
广州,0.10,paid
广州,0.20,paid
上海,10.00,pending
上海,,paid
广州,-0.05,paid

现在问 DSH:

帮我分析 sales.csv:先检查数据质量,再按地区统计已支付订单数和金额合计,指出各地区金额缺失的记录数。

只统计 status = paid,会得到下面的结果:

地区已付订单数有效金额条数金额缺失条数已知金额合计
广州3300.25
上海1010(没有可参与求和的金额)
合计4310.25

广州的合计是:

0.10 + 0.20 − 0.05 = 0.25

这里保留负数参与计算,不自动判断它是不是退款,也不擅自把它当成异常删除。

上海则有一条已支付记录,但金额为空。工具对空数值集合的求和返回 "0",同时告诉模型:实际参与金额计算的记录数是 0。

所以,这个结果不能解释为「上海真实收入就是零」。在实际报表里,可以把它标为「金额未知,待补充」,而不是直接当成完整的业务金额。

我希望这个插件解决的,不只是「算出一个数」,还有「让人知道这个数是怎么算出来的」。

三、两个工具,分别负责发现数据和执行查询

插件没有额外做一套聊天界面,而是向 DSH 注册两个模型可调用的工具。

1. csv_inspect:先看清文件

csv_inspect 返回:

  • 原始列名;
  • 全文件的数据记录数;
  • 每列的空值、合法十进制数和其他值数量;
  • 有上限的样本预览;
  • 文件字节数和所读取内容的 SHA-256。

列名不被自动改写,数据也不被自动转成 JavaScript 数字。

例如,订单编号 00123 仍然保留为字符串,不会因为看起来像数字就丢掉前导零。是否按数值进行比较,由后续查询条件明确指定。

预览默认显示 5 行,但质量计数针对整个限额内文件,不是只检查这 5 行。需要减少预览数据时,也可以设置 sample_rows: 0。

2. csv_query:筛选、分组、统计和分页

csv_query 支持选列、同时满足的筛选条件、分组、聚合、多字段排序和分页。

下面是模型可以提交的一次查询,不是要求普通用户手写的操作步骤:

{
  "path": "sales.csv",
  "filters": [
    { "column": "status", "op": "eq", "value": "paid" }
  ],
  "group_by": ["region"],
  "metrics": [
    { "op": "count", "as": "orders" },
    { "op": "sum", "column": "amount", "as": "revenue" }
  ]
}

聚合支持 count、sum、avg、min、max。文本匹配区分大小写;数值比较需要明确使用十进制模式,不猜测货币单位或千位分隔符。

真实分组查询的工具详情

这张截图展开了真实查询的返回数据:原始文件有 5 条记录,匹配到 4 条,分组结果有 2 行。contributingRows 则记录每个指标实际使用了多少条数据。

四、我最在意的三个统计细节

1. 金额求和不用二进制浮点数

JavaScript 里一个很熟悉的例子是:

0.1 + 0.2

结果不是字面上的 0.3,而是 0.30000000000000004。

这并不意味着所有脚本都会把金额算错,而是金额计算需要明确选择数值类型。

插件使用 decimal.js 做十进制计算,数值结果以字符串返回,例如 "0.25",避免再次经过 JSON 浮点数转换而丢失精度。

在配置的数字位数限制内,求和保持精确。均值是另一回事:有些平均数无法用有限小数精确表示,因此按配置进行 half-up 四舍五入,默认保留最多 12 位小数。

2. 不把空金额偷偷转换成零

对于数值聚合:

  • 空白金额不参与计算;
  • 非空但不能按约定解析的金额直接报错;
  • 空集合的求和是 "0";
  • 没有有效数值时,均值和极值是 null;
  • 每个指标都有实际参与记录数。

例如,¥100、1,000、1e3 不会被插件擅自解释成某个金额。应该先确认格式并转换数据,而不是边计算边猜测。

实际页面演示里,模型也曾漏掉数值筛选所需的 mode: "decimal"。工具明确拒绝了这次请求,模型补上条件后再继续查询,而不是悄悄按另一种比较方式执行。

模型可能写错参数,工具不能因此返回一份看起来正常、实际口径不同的结果。

3. 分页不改变统计范围

「只返回前十行」和「只统计前十行」是两件不同的事。

这个插件先处理整个限额内文件,完成筛选和聚合,再对输出结果排序、分页。返回值分别说明:

字段含义
totalRows原始数据记录总数
matchedRows满足条件的记录数
resultRows分页前的全部输出行数
returnedRows本页返回的行数
truncated本页是否遗漏了其他输出行

改变 limit 不会改变统计合计。

文件超出配置上限时,插件直接拒绝计算,不读取一部分之后把局部结果当成全量结果返回。

五、核心实现:复用 DSH 的工具和文件系统

和之前的插件一样,这个项目没有修改 DeepSeek Harness 核心代码。

它是一个标准 bundle:

- insert:
    - id: csv-analyst
      name: dsh-csv-analyst

插件入口声明所需的宿主能力:

export const name = 'csv-analyst'
export const inject = ['tools', 'fs', 'workingDirectory']

ctx.tools 负责工具注册和正常的执行策略,ctx.fs 负责文件读取,ctx.workingDirectory 提供会话当前的工作目录。插件不另建文件系统,也不替换模型或 agent loop。

读取文件时,字节上限直接传给文件系统服务:

const bytes = await ctx.fs.readBytes(target, signal, config.maxBytes)

这样不是「先把大文件读进内存,再判断超限」,而是在读取操作本身就限制完整文件的大小。

CSV 解析使用 Papa Parse,不是重新写一个引号和换行解析器。工具返回值有明确的 schema,模型可见的内容由同一个标准结果生成,DSH 的会话日志负责保存工具调用和结果。

另外,工具注册会随插件卸载清理;卸载过程中也会取消并等待正在进行的操作结束,避免留下仍在读取文件的后台任务。

六、安装之后,用自然语言提问即可

已安装 DSH 的用户,可以把插件加入 Web profile:

dsh plugin --profile web add dsh-csv-analyst
dsh web

如果是在 DeepSeek Harness 源码仓库里运行,使用当前仓库的入口:

pnpm dsh plugin --profile web add dsh-csv-analyst
pnpm dsh web

npm 发布包已经包含构建产物,普通安装不需要再手动构建插件。

把 UTF-8 CSV 放到会话工作目录,然后直接描述需求,例如:

检查 sales.csv 的数据质量,按地区统计已支付订单数和金额合计。请同时列出金额缺失的条数,不要把空金额解释成真实收入为零。

在 expenses.csv 中找出金额大于 500 的记录,返回商户、分类和金额,按金额降序排列。

统计 support.tsv 中各状态的工单数量,这个文件的分隔符是制表符。

插件基于 DSH 0.2.2-rc.2、Node 25.6.1 开发和测试。版本只是开发基线的说明:插件不通过 engines、限定 peer 版本或运行时版本判断拦截其他版本。但取消限制,不等于已经验证未来所有 API 变化都兼容。

需要卸载时:

dsh plugin --profile web remove dsh-csv-analyst

插件不创建专属用户数据文件。卸载不会删除 DSH 历史会话中已经记录的工具结果。

七、哪些限制需要提前说清楚

当前版本是只读 CSV 分析工具,不是 Excel 编辑器、数据库或图表生成器。

主要限制包括:

  • 只接受 UTF-8 文本,支持 UTF-8 BOM;.xlsx、GBK、UTF-16 等数据需要先转换;
  • 默认使用逗号,不自动猜分隔符;制表符、分号、竖线可以明确指定;
  • 表头必须非空且唯一,缺列、多列和解析器检测到的引用错误会报错;
  • 默认最多读取 8 MiB、10 万条数据记录,返回页面最多 200 行;
  • 每次调用重新读取文件,跨页查询应核对 SHA-256,不能把多次调用当成一个固定快照。

这些限额可以通过 profile 的 cordis.patch.yml 配置:

- id: csv-analyst
  config:
    maxBytes: 8388608
    maxRows: 100000
    maxReturnedRows: 200
    defaultPreviewRows: 5
    defaultPageRows: 100
    averageDecimalPlaces: 12

还有一个更重要的提醒:只读不等于离线,也不等于保密。

插件自身不写文件、不请求网络、不启动子进程,也不执行模型生成的代码。但它返回的单元格和统计结果会进入正常的模型消息,并可能保留在 DSH 会话日志里;文件访问权限仍由宿主配置决定,其他已启用工具也有各自的能力。

所以,处理真实客户、财务或个人数据之前,仍然要确认模型提供方、权限和会话保存策略。表格里的文字同样是不可信输入,不能因为来自 CSV 就自动把它当成指令。

八、发布前做了哪些验证

目前发布的 0.1.1 包含 103 个测试用例,源码的语句、分支、函数和行覆盖率都达到 100%。

除了基础解析和查询,还覆盖了大整数与小数求和、空值、排序、超限拒绝、取消、会话目录、权限拒绝、卸载和重新挂载。金额求和另用独立生成的整数分值做属性测试,而不只是比较几组手写答案。

GitHub CI 在 Linux、macOS、Windows 上执行检查和依赖审计;另外使用真实 DSH 组合安装打包后的插件,并通过已记录会话完成无密钥重放。真实模型调用和页面截图也使用了实际工具结果,没有注入模拟回复。

发布时依赖审计未发现已知漏洞。但测试覆盖率和审计报告都不是「永远没有缺陷」的证明,也不能代替对模型结论的核对。

结语

这次我没有再增加一个打开 DSH 的入口,也没有给它换一套界面,而是把一个常见的数据处理场景做成了可安装的工具。

dsh-csv-analyst 的价值不在于模型能写出更长的分析,而在于它能拿到一份更明确的统计结果:计算范围是什么,金额是否有效,多少条记录参与了计算,返回的是全部结果还是其中一页。

用户仍然可以自然语言提问,模型仍然可以整理说明;只是金额求和、空值处理和分页口径,不再交给一段临时生成的文字或代码来决定。

项目地址:

github.com/ningbonb/ds…

npm 包:

www.npmjs.com/package/dsh…