
问题
AI 代码 agent(如 Claude Code)在探索代码时,用 grep 定位文件,用 read 打开文件。read 的基本单元是文件。
但如果一个文件里有 20 个函数,会发生什么?
为了找 CrossError 这一个 type 而执行 read
→ 连带读入 19 个无关函数
→ 上下文污染
斯坦福大学 2024 年发布的"Lost in the Middle"研究指出,当相关信息被淹没在上下文中间时,LLM 性能下降 30% 以上。亚马逊 2025 年的"Context Length Alone Hurts LLM Performance"研究表明,即使是空白的无关 token,也会导致性能下降 13.9%~85%。
研究已经证明「上下文越短越好」。然而,缺少一种能够从结构上切分代码、只读取必要内容的工具。
filefunc 填补了这一空白。它是专为 Go 应用开发设计的代码结构规范与 CLI 工具,适用场景包括:后端服务、CLI 工具、代码生成器、SSOT 验证器。
核心原则
一个文件,一个概念。文件名即概念名。
无论是 func、type、interface 还是 const 的集合,规则都相同。所有细则均由这一原则派生。
# 不使用 filefunc
read utils.go → 20 个 func,19 个无关。上下文污染。
# 使用 filefunc
read check_one_file_one_func.go → 1 个 func。恰好是需要的内容。
比起挑出需要的 5~10 个,更重要的是不打开那多余的 290 个。
第一公民是 AI agent
filefunc 的代码结构是为 AI agent 而设计的,而非为人类。
AI agent 不用 ls,而是用 grep 探索。无论项目有 500 还是 1000 个文件,一条 rg '//ff:func feature=validate' 就能搞定。文件越多,每个文件越小,单次 read 带入的噪声越少,反而更有利。
「文件是不是会变得太多?」这个问题会被提出。对人类来说确实如此。但人类的不便可以在视图层(如 VSCode 扩展)解决。filefunc 的结构不向人类习惯妥协。
探索路径的改变
现有方式
用户请求
→ 不知道有什么,先 ls、find
→ 打开文件,了解结构
→ 再次 grep 找相关文件
→ 打开后发现 20 个 func,大多数无关
→ 探索成本 > 实际工作时间
filefunc 方式
用户请求 + 提供 codebook
→ 看 codebook,立刻构建 grep 查询
→ read 20~30 个文件(每个文件一个概念,全部有效上下文)
→ 执行任务
read 了 30 个文件,但全部都是有效上下文,那 30 个不是问题。read 了 1 个文件,却带入了相当于 30 个文件的内容,那才是问题。
Codebook
Codebook 在 filefunc 设计中占据最重要的位置。Codebook 的优先级高于 annotation 规则。只有 codebook 设计得好,grep 查询才会精准;grep 精准,read 列表才会干净。
# codebook.yaml
required:
feature: [validate, annotate, chain, parse, codebook, report, cli]
type: [command, rule, parser, walker, model, formatter, loader, util]
optional:
pattern: [error-collection, file-visitor, rule-registry]
level: [error, warning, info]
required 中的键在所有 //ff:func 和 //ff:type annotation 中必须存在,以确保 grep 的可靠性——required 键不允许留空。optional 键仅在相关时使用。
Codebook 是 AI agent 的项目地图。没有 codebook,就等于在不知道词汇的情况下开始探索。有了 codebook,就能直接抛出 feature=validate、type=rule 这样的精确查询,无需漫无目的地探索。
在 annotation 中使用 codebook 以外的值属于 ERROR。通过 codebook 规范化词汇,缺失的 feature、重复的 type、模糊的分类都会在列表中暴露出来。只有能看到漏洞,才能有效管理。Codebook 本身也是被验证的对象——required 中至少需要一个键,不允许重复值,只允许小写字母和连字符。
元数据 Annotation
在每个文件顶部添加 annotation,使得不需要 read 整个 body,只需读取开头几行即可了解元信息。
//ff:func feature=validate type=rule control=sequence
//ff:what F1: validates one func per file
//ff:why Primary citizen is AI agent. 1 file 1 concept prevents context pollution.
//ff:checked llm=gpt-oss:20b hash=a3f8c1d2
func CheckOneFileOneFunc(gf *model.GoFile) []model.Violation {
| Annotation | 内容 | 是否必须 |
|---|---|---|
//ff:func | func 文件的 feature、type、control 等元信息 | func 文件必须 |
//ff:type | type 文件的 feature、type 等元信息 | type 文件必须 |
//ff:what | 单行说明——这个函数/类型做什么 | 必须 |
//ff:why | 为什么这样实现——决策依据 | 可选 |
//ff:checked | LLM 验证签名(由 llmc 自动生成) | 自动 |
格式为 //ff:key key1=value1 key2=value2。可直接用 grep/ripgrep 搜索,结构化的 key-value 也便于工具解析。与 Go 的 //go:generate、//go:embed 惯例采用相同模式。
control — 1 func 1 control
control= 在所有 func 文件中为必填项。值为以下三者之一:
| control | 含义 | depth 限制 |
|---|---|---|
sequence | 顺序执行 | 2 |
selection | 分支(switch) | 2 |
iteration | 循环(loop) | dimension + 1 |
基于 Böhm-Jacopini 定理(1966)。所有程序都是 sequence、selection、iteration 三种控制结构的组合。filefunc 将这一规则强制到函数级别——一个函数只能有一种控制流。
control=iteration 的函数还必须填写 dimension=,用于说明所遍历数据的维度。dimension=1 表示扁平列表(depth ≤ 2),dimension ≥ 2 则需要命名类型(struct/interface)的嵌套。
filefunc 也会验证 control 与实际代码的一致性。若 control=selection 但代码中没有 switch,或 control=sequence 但存在 switch 或 loop,均为 ERROR。
LLM 探索管道
Annotation 起到搜索索引的作用,无需向量嵌入等重型基础设施即可运行。
1. 结构性缩减(无需 LLM,使用 grep)
基于 codebook 构建 grep 查询
→ 筛选出 20~30 个候选文件
2. 元信息判断(无需 LLM 或使用超小型 LLM)
只 read 每个文件顶部的 annotation
→ 通过 name/input/output/what 缩减至 5~10 个
3. 精细处理(大型 LLM,最小上下文)
仅对 5~10 个文件执行完整 read
→ 修改/生成代码
随着阶段推进,上下文不断收窄。当大型 LLM 介入时,留下的只有真正必要的文件。
CLI
validate — 代码结构规则验证
filefunc validate # 当前目录
filefunc validate /path/to/project # 显式指定项目根目录
filefunc validate --format json
项目根目录需要存在 go.mod 和 codebook.yaml。只读操作。存在违规时退出码为 1。
chain — 调用关系追踪
filefunc chain func RunAll # 一度关系(默认)
filefunc chain func RunAll --chon 2 # 二度关系(含一起被调用的函数)
filefunc chain func RunAll --chon 3 # 三度关系(最大)
filefunc chain func RunAll --child-depth 3 # 仅下游调用
filefunc chain func RunAll --parent-depth 3 # 仅上游调用者
filefunc chain feature validate # 整个 feature
实时 AST 分析。--chon 表示关系距离。一度是直接调用/被调用,二度进一步包含一起被调用的函数。
传统 go callgraph 会静态分析所有调用,产生数千个节点。chain 仅在同一 feature 内追踪。Codebook 的 feature 就是缩放级别。
llmc — LLM 验证
filefunc llmc # 当前目录
filefunc llmc --model qwen3:8b
filefunc llmc --threshold 0.9
使用本地 LLM(ollama)验证 //ff:what 是否与 func body 一致。评分范围 0.0~1.0,默认阈值 0.8。通过验证后自动写入 //ff:checked llm=模型名 hash=哈希值。若 body 发生变更,hash 将不一致,需要重新验证。
这是用小型 LLM 解决 annotation drift 核心问题的方案——//ff:what 是自然语言,无法进行机械化验证。由于 1 file 1 func 保证了文件与函数的一一对应,这一方法才得以实现。
规则
文件结构规则
| 规则 | 违规时 |
|---|---|
| 一个文件只有一个 func(文件名 = 函数名) | ERROR |
| 一个文件只有一个 type(文件名 = 类型名) | ERROR |
| 方法:1 file 1 method | ERROR |
| init() 不可单独存在(需与 var 或 func 同在) | ERROR |
_test.go 允许多个 func | 例外 |
| 语义上属于同一组的 const 允许放在同一文件 | 例外 |
代码质量规则
| 规则 | 违规时 |
|---|---|
| 嵌套深度:sequence=2,selection=2,iteration=dimension+1 | ERROR |
| func 最大 1000 行 | ERROR |
| func 建议:sequence/iteration 100 行,selection 300 行 | WARNING |
嵌套深度因 control 类型而异。sequence 和 selection 最大深度为 2。iteration 的最大深度为 dimension + 1——dimension=1(扁平列表)时深度为 2,dimension=2(嵌套结构)时深度为 3。结合 Go 的 early return 模式,绝大多数代码都能在此限制内实现。
selection(switch)的 case 容易变长,因此建议行数上限放宽至 300。
.ffignore
在项目根目录放置 .ffignore,可在所有 filefunc 命令中排除对应路径。语法与 .gitignore 相同。
vendor/
*.pb.go
*_gen.go
internal/legacy/
用于排除无法强制 filefunc 规则的代码,如生成代码(protobuf、代码生成输出)或外部 vendor 代码。
与 whyso 的联动
由于 func = file,函数级别的变更历史与文件级别完全对应。
whyso history check_ssac_openapi.go # CheckSSaCOpenAPI 函数的变更历史
如果一个文件中有多个函数,就必须翻 diff 才能知道是哪个函数被修改了。使用 filefunc,文件变更即函数变更,追踪成本为零。
隐性耦合检测
whyso coupling check_ssac_openapi.go
在同一请求中一起被修改的函数:
check_response_fields.go 8次
check_err_status.go 5次
types.go 4次
没有显式关系,却频繁出现在 coupling 统计中,这是隐藏依赖的信号。可能是用不同角度实现同一业务规则的函数、没有 interface 却在隐式对齐 format 的代码,或者总是同时出现的 bug。
为什么只针对 Go
如果不是 Go,filefunc 的结构化会很困难。gofmt 强制代码格式,early return 是惯例,没有异常机制,package 即目录。若要扩展到其他语言,需要相当于 gofmt 级别的结构强制策略,这超出了 filefunc 的范围。
适用场景也很明确:后端服务、CLI 工具、代码生成器、SSOT 验证器。算法库、底层系统编程、性能关键热路径不在适用范围之内。
总结
LLM 时代的代码结构,应当以 AI 的探索效率为导向,而非人类的浏览习惯。filefunc 是这一转变的第一步。
一个文件,一个概念。用 codebook 规范化词汇,用 annotation 附加元信息,用一次 grep 找到精确的文件。单次 read 不会带入无关代码。上下文污染的防控,由文件结构本身来解决。