
“文件系统加 bash 就够了”至少已经是过去六个月构建 agent 的事实标准。如果从 Manus 主流出圈算起,时间还更长。可问题是,文件系统和 bash 在真实垂直场景里到底给你带来什么,具体案例仍然少得可怜。所以我会讲讲我们如何为 Shortcut 打通一个用例:数据补全。

你有一张表,行是实体(公司、候选人、销售线索等等),还有一组你希望填上的列:一家公司的员工数和融资阶段,一个候选人的学历和代表作品,一个投资人的近期交易。数据补全就是去网页和外部来源里找这些事实,记录每条事实来自哪里,再把它们写回表里。
人们愿意为这件事付很多钱。销售团队会用数据补全销售线索,判断该联系谁、开场该说什么;招聘人员会补全候选人名单;投资人会补全交易 pipeline 并做尽调,等等。Clay 本质上就是电子表格驱动的数据补全,它在 2025 年 8 月以 31 亿美元估值融资 1 亿美元。
一句话版:Shortcut 解决数据补全,不是靠设计一个专门的数据补全工具,而是把它已经有的工具 filesystem-pilling 了。
吃下文件系统这颗药的基础知识
先讲基础。Filesystem-pilling 指的是给 agent 一个真正的文件系统和 shell,并让它通过磁盘上的文件来推进工作,而不是走其他方式。数据落到文件里,agent 用 bash 读写它们,流经 transcript 的主要是指向这些数据的指针,而不是数据本身。直接好处是:接收输入和生成输出时都省 token。举三个例子:
Bash
Bash 强大,正是因为 agent 什么都能跑;但这也意味着你没法控制输出。同一个命令可能打印 5 行,也可能打印 5000 行。所以不要试图提前把它框死,让它跑起来,再接住溢出的部分。输出太大塞不进 context 时,不要把它贴回 transcript;让它流到磁盘,模型只拿到截断后的开头和一个路径。pi 的 bash tool 是一个干净的参考:它把输出限制在 50 KB / 2000 行,溢出时追加完整路径。
$ grep -c ERROR app.log # 240k matching lines
... (first ~50k chars) ...
Full output: /tmp/pi-bash-a3f9.log
MCP
大多数 MCP 工具输出都是怪物。2 MB 深度嵌套 JSON。所以别让它进 context。把它持久化到磁盘,再用 JSON parser 返回一个路径和数据形状,而不是返回具体值。有了形状和几条样例记录,模型就可以直接去取真正想要的值:用 jq 抽它需要的两个字段,过滤出匹配的 12 行,再统计剩下的,而不是读完 4182 个联系人只为了用其中几条。
result saved to /mcp/contacts-8f21c0.json (2.1 MB).
JSON shape:
{ "contacts": [ { "id": string, "name": string, "email": string, "company": string } ] (4182 items) }
Subagents
假设你有一个非常适合委派给 subagent、并行度很高的任务,比如研究 200 家公司,找出它们最新一轮融资、员工数和主要产品。天真的做法是把一整段自然语言 brief 塞进每一次 subagent 调用里,于是在父 agent 的输出中,把几乎相同的一段话重新膨胀 200 次。修法只需要给 subagent tool 做一个小改动:让它的 query 可以接收一个 .txt 文件,而不只是内联字符串。一旦 query 是文件,agent 就可以用 bash 以编程方式生成全部 200 份,而不是手写出来。
jq -c '.[]' companies.json | while read -r c; do
name=$(jq -r '.name' <<<"$c"); domain=$(jq -r '.domain' <<<"$c")
cat > "briefs/${domain}.txt" <<EOF
Research ${name} (${domain}). Find: latest funding round and amount,
employee count, primary product, and any acquisitions in the last 2 years.
Cite a source URL for each fact.
EOF
done
这三个例子里跑的是同一个动作:把大块内容留在模型外面,让文件来承载它。这样更快、更便宜;那些没有花在大块内容上的 token,就能留给推理。context 越瘦,agent 越聪明。
案例研究:数据补全
上面这些基础做法对任何 agent 都有用。但 filesystem-pilling 放到一个定制的垂直场景里,比如数据补全,仍然划算吗?
显而易见的解法
从机械流程看,数据补全永远是同一个循环:从一张表开始,通过外部研究填上缺失列,附上来源,规范化答案,导出。
显而易见的解法会照着表格网格来:每个单元格里放一个 agent。Paradigm 做的是 AI-native spreadsheet,每个单元格里都有一个 agent,负责搜索网页并填入自己的值;Clay 按行补全;Quadratic、Freckle 等公司也各自做了同一思路的变体。执行模型就是 UI 本身:
for each row: 4,000 rows
for each missing column: × 3 columns
async spawn an agent for this cell = 12,000 agents
演示起来很好看——单元格一个接一个亮起来——而且产品抽象很干净。但它也只解决一种形状的问题。拿一张真实表格来看:

这张表里的每一列都会破坏单元格模型,而且破坏方式各不相同:
- 标签不是扁平的。Comp → Base 和 Comp → Equity 是一个逻辑概念,被嵌套表头拆成了两部分。一个只按单个 (row, column) 标签取值的工具,没地方表达这种层级,于是两个单元格会被当成互不相关的问题来研究。
- 标签不等于查询。“Notable projects of B. Okoro” 是一个很糟的搜索 query;“open-source projects and conference talks by B. Okoro, the candidate who led a payments rewrite” 才是好 query——但组合出这个 query 需要同一行里的其他单元格,而单元格 agent 永远看不到它们。
- 独立回答不会自动标准化。往任意一列向下看,highest degree 这一个事实会出现四种格式。每个单元格 agent 都是孤立作答,所以没有任何东西看过整列,也就没人把它统一起来。
这些不是几个彼此独立的 bug,而是同一个根因:cell-agent 工具把 agent 的智能限制在“行”ד列”×精确标签格式里。研究问题本来不是单元格形状的,但工具强迫它变成单元格形状。
修法:把一个普通网页搜索 filesystem-pill 掉
修法不是做一个更好的数据补全工具,而是不做这个工具。我们转而把一个普通网页搜索 filesystem-pill 掉。
Shortcut 的 web_search 只做一件朴素的事:接收一个 .txt 文件,里面每行一个 query,再接收一个用于整个批次的输出 schema;它并发运行这些 query,然后每个结果写一行 JSONL。它完全不知道什么是电子表格、行或单元格。所以 agent 用代码来组合这项任务:
rows = json.load(open("input_rows.json"))
with open("queries.txt", "w") as f:
for r in rows:
f.write(f"Highest degree, institution, total years of professional experience, "
f"and notable projects of {r['name']}, {r['title']} at {r['company']} "
f"({r['linkedin']})\n")
这会产出一个普通文件,每行都是一个自包含 query:
# queries.txt (4,000 lines)
Highest degree, institution, total years of professional experience, and notable projects of A. Chen, Staff Engineer at Google (linkedin.com/in/anqichen)
Highest degree, institution, total years of professional experience, and notable projects of B. Okoro, Principal Engineer at Stripe (linkedin.com/in/bokoro)
...
然后 agent 把它交给工具:
web_search({
inputPath: "/workspace/enrichment/queries.txt", // 4,000 lines
outputSchema: { type: "object", properties: {
highest_degree: { type: "string" },
institution: { type: "string" },
years_experience: { type: "number" },
notable_projects: { type: "array", items: { type: "string" } }
}, required: ["highest_degree", "years_experience"] }
})
这几行代码直接修掉了三个失败里的两个。三列被折叠成对每个人的一次查询,所以每一行都变成了真正的 query:姓名、职位和公司被编织在一起,而不是一个裸露的单元格标签。而且一个 schema 应用到整个批次,一次性钉住每一行的答案形状。这相当于查询次数省了 3 倍。
{"line":1,"status":"found","answer":{"highest_degree":"PhD","institution":"Stanford University","years_experience":11,"notable_projects":["Spanner","F1"]},"sources":[{"url":"..."}]}
{"line":2,"status":"found","answer":{"highest_degree":"MS","institution":"MIT","years_experience":7,"notable_projects":["..."]},"sources":[{"url":"..."}]}
{"line":3,"status":"not_found","answer":null,"sources":[]}
第三个失败,也就是标准化,交给下一步处理。结果文件回到发起搜索的同一个 agent loop;这时它带着整列视野来读结果:把 PhD 和 Doctorate 合并成一种形式,把一个跑偏的 "8 years" 强制转成 8,对返回太薄的项重新发起查询,然后不断迭代,直到这一列从结构上就是一致的。这一步是单元格模型在结构上不可能拥有的,因为没有任何 cell agent 能看到自己的单元格之外。
而且因为每一步都留下了文件,整个工作流获得了两个属性:可重跑,可审计。可重跑:后续阶段需要修正时,可以重新处理保存下来的 JSONL,而不是再跑一遍昂贵的网页研究。可审计:每个中间产物——组合出的 query、原始结果、片段,甚至被拒绝掉的来源——都是用户可以阅读、也可以下载的真实文件。
教训
面对电子表格补全这样的垂直场景,本能反应是做一个电子表格专用工具:每个单元格一个 agent,一个 enrichment engine,把网格烘进产品里。它演示效果好,也覆盖常见情况。
不要做那个垂直工具。把垂直问题转成通用问题,让 agent 自己组合 pipeline。边缘情况就不再是特例。同一组处理过数据补全的 primitives,也能处理下一个垂直场景,因为其中没有任何东西是关于数据补全本身的。
找到那个你正准备构建定制工具的位置,然后问:它的通用版本是什么?比你想象中更常见的情况是,你能为一个垂直场景构建的最好东西,恰恰是那个完全不针对它的东西。