
如果你的编程Agent总是在大型代码库里改错地方,如果让它read一个文件却带来了19个无关函数污染context,如果你怀疑"1 file 1 concept"这类约定究竟有没有实际效果——那么,这里有一份来自star数23k的实战框架的测量结果。
“文件会不会太多了?”
这是关于filefunc最常被问到的问题。把186个文件拆成626个,管理上会不会失控?
答案在Hono里。Hono是一个可以运行在Cloudflare Workers、Deno、Bun、Node.js上的超轻量Web框架,star数23k+,npm周下载量100万+,是经过生产环境验证的实战代码。我们用filefunc对这套代码进行了重构,4419个测试,全部通过。(可在 park-jun-woo/hono fork中亲自验证)
数字
| 指标 | 原版 | 重构后 |
|---|---|---|
| 源文件数 | 186 | 626 |
| 总行数 | 24,653 | 30,244 |
| filefunc违规数 | 397 | 0 |
| vitest通过 | 4419 | 4419 |
| vitest失败 | 4 | 4(原有缺陷) |
| vitest跳过 | 33 | 33 |
文件数增加了3.4倍,行数增加了23%,违规数从397降到了0,没有一个测试被破坏——准确说是与原版完全相同的4个测试失败(原版本身就有的缺陷)。23%的行数增加来自注解(//ff:func、//ff:what)和re-export hub。逻辑一行都没有改变,是纯粹的结构重构。
关键不在于文件数,而在于"读取长度"
“零违规、626个文件"其实只是代理指标。filefunc真正的目标不是把文件拆得越细越好——而是让Agent在read某个概念时,只读到那个概念,且不会过长。所以真正需要证明的数字不是违规数,而是每个文件的读取长度。我们测量了。
| 每文件行数 | 原版 | 重构后 |
|---|---|---|
| 中位数 | 60.0 | 17.5(−71%) |
| p90 | 305 | 119(−61%) |
| 最大值 | 2,778 | 1,051(−62%) |
| ≤20行文件占比 | 26% | 54% |
Agent打开一个概念,以前平均要消化60行,现在只需消化18行。最坏情况(p90)也从305行降到了120行。函数本身的长度没有变(中位数11→12行)——这很自然,因为我们不是在重写函数,而是在重新排列它们。减少的是"为了读某一个概念,不得不连带读到的周边代码”。
为什么这很重要?长context不是免费的。LLM会系统性地遗漏埋在长输入中间的信息(Liu et al., Lost in the Middle, TACL 2023, arXiv:2307.03172)。在编程任务中,随着context变长,性能会急剧下滑——在某个基准测试中,Claude 3.5 Sonnet的准确率从29%崩溃到了3%(Rando et al., LongCodeBench, 2025, arXiv:2505.07897)。而按概念切割、只给精确所需内容,比整文件喂入能更好地提升代码补全质量(Yusuf et al., 2025, arXiv:2510.06606)。缩短读取长度不是审美偏好,而是对准确率的主动防御。
types.ts 问题
不谈抽象,看具体。Hono原版的src/types.ts里有超过20个接口和类型。
AI Agent想找HonoRequest这一个类型,却不得不read整个文件?那19个无关类型就跟着进来了,context被污染。
重构后,每个类型都是独立文件。只需要HonoRequest,就只read hono_request.ts这一个文件。原来的types.ts作为re-export hub保留,现有的import路径照常可用。
# 原版
import { HonoRequest } from './types' // 20+个类型一起进来
# 重构后
import { HonoRequest } from './types' // 同样的路径,同样的行为
// 内部实际上是 types.ts → hono_request.ts re-export
从外部看,什么都没变。从AI Agent的角度看,一切都变了。
嵌套深度从6降到2
Hono的路由算法很复杂。trie-router的Node.search嵌套深度为6。
for → if → if → for → if → if // 深度6
depth 6是烂代码吗?不是。trie搜索本来就嵌套深。但AI Agent要理解这个函数,就必须一次性把6层嵌套装进脑子里。人也一样。filefunc将内部逻辑提取为私有方法和模块级箭头函数,depth 6→2,每个片段只含一种控制流,整体算法不变。
# 原版:单块式 search
Node.search() // 深度6,100行+
# 重构后:分解为片段
Node.search() // 深度2,只负责组合
→ matchParam() // 深度1,参数匹配
→ matchWildcard() // 深度1,通配符处理
→ mergeHandlers() // 深度1,合并处理器
TypeScript的F1规则,以及诚实的尾部
filefunc的核心规则F1是"一个文件一个函数"。在Go里很直观,但在TypeScript里拆文件会破坏模块系统——把类方法移到外部文件,this绑定就消失了。因此filefunc的TypeScript解析器(ts_ast.js)只计数function声明,不计数const箭头函数。原则是"一个文件一个概念",而不是"语法上只有一个function"。
这里需要诚实。这种做法对简单情况(类型、单一helper)分离得很干净,但并没有把所有东西都分离清楚。重新测量重构后的结果:
- 626个文件中,90%(566个)函数数≤1个——满足"1文件1概念"。(原版是70%。)
- 但60个文件(9.6%)里仍然有2个以上函数共存,而且恰好这些文件都很长——这60个文件的行数中位数为151行。例如
src/utils/url.ts里14个函数共用319行。
也就是说,const箭头函数的做法通过了计数器,但只部分实现了目标。如果一个文件里保留了多个箭头函数,Agent打开这个文件时,读到的仍然是多个概念。指标成为目标的那一刻,指标就开始失效(Goodhart)。filefunc也不例外——剩余read-length风险的大部分都集中在这10%的尾部。不把"零违规"这个数字捧上神坛,还要测量哪里还没做到——这才是验证。
“那到底好在哪儿?”
文件变成626个,人可能会不舒服,打开目录一堆文件扑面而来。但AI Agent不会打开目录,它用grep。
rg '//ff:func' --glob '*.ts' -l | head -20 # 提取候选文件
rg '//ff:what.*router' --glob '*.ts' # 只查路由相关函数
186个文件里平均每个有3-4个函数,grep找到文件之后还要read,无关函数跟着进来。626个文件里每个只有1个概念,grep找到的文件=所需概念,中间过程消失了。在Agent的代码导航中,“定位相关位置(localization)“是下游问题解决的瓶颈(Chen et al., LocAgent, 2025, arXiv:2503.09089),而filefunc通过让概念与文件边界对齐,使这个定位过程变得确定性。
函数粒度总是正确答案吗
反面证据也要诚实面对。一项对照实验报告称,在RAG代码自动补全中,“函数级分块比其他策略低3.6~5.6pp,且不是帕累托最优”(Wu et al., 2026, arXiv:2605.04763)。函数级不是万能的。
不过,这是不同层次的讨论。那个实验是检索器把代码切块塞进提示词的自动补全场景。filefunc处理的不是检索器生成chunk,而是Agent直接选择文件进行read的运作单元。分块策略(retrieval chunk)和运作单元(agent打开的文件)是不同层次。即便如此,明确说明这个区别依然重要——“拆得越细越好"不是filefunc的主张。主张是"Agent读取的单元与概念一致,读取长度就会缩短”,上面的数字证明了这一点。
约束即自由
用filefunc重构Hono,验证了一件事。
结构性约束不会限制代码,它解放了导航。
文件增多是成本。但当每个文件只承载一个概念时,Agent就能精确read所需内容,不被无关context污染——读取长度从60行降至18行的测量就是证据。对人来说也一样,函数名即文件名,目录就是目录。
397个违规归零,4419个测试与原版一致通过。而且这个结果任何人都可以在README的重构报告里自己复现。这就是"1 file 1 concept"不只是理论,而是实战的证明。包括那剩余的9.6%的尾部在内。
相关文章
- Agent可运营的代码库 — “一个文件20个函数,Agent性能下降30~85%",本文上位命题
- filefunc:一个文件一个概念 — 约定本身的定义,本文是其大规模实证篇
- 构建Agent可运营的系统 — 让Agent能够打开封闭遗留代码的宏观叙事
- Ratchet Pattern — 让4419个测试"在机器叫停之前持续完成"的确定性门控
延伸阅读(外部)
- Effective context engineering for AI agents — Anthropic. 将"context rot"与有限的注意力预算作为核心问题点出的第一手资料——filefunc减少无效context的理由与之同根。
- Strategies and Tactics for working with Coding Agents — Brian Kihoon Lee. 主张亲手设计基于grep的导航与信息架构——与让Agent"只读目标"的文件结构约定直接呼应。
- The Vibes Don’t Scale — Paul Stack. 解析vibe coding在规模化后因架构漂移而崩溃的机制——filefunc所要解决的"大型代码库让Agent失效"这一问题意识的来源。
- Agentic Engineering Patterns — Simon Willison. Context Quarantine/Pruning等context管理模式——将filefunc的agent-operable主张扩展到实践模式语言中。
- Agent Harness Engineering — Addy Osmani. Agent性能由模型周边基础设施决定——将代码结构约定重新语境化为harness的一个轴心。
参考文献
- Liu et al. “Lost in the Middle: How Language Models Use Long Contexts” (TACL 2023, arXiv:2307.03172)
- Rando et al. “LongCodeBench: Evaluating Coding LLMs at 1M Context Windows” (2025, arXiv:2505.07897)
- Yusuf et al. “Beyond More Context: How Granularity and Order Drive Code Completion Quality” (2025, arXiv:2510.06606)
- Chen et al. “LocAgent: Graph-Guided LLM Agents for Code Localization” (2025, arXiv:2503.09089)
- Wu et al. “How Does Chunking Affect Retrieval-Augmented Code Completion? A Controlled Empirical Study” (2026, arXiv:2605.04763)
- 重构结果验证: park-jun-woo/hono (README的filefunc Refactoring Report) · 约定: filefunc
- 题图:AI 生成(Google Gemini)