【AI 编程】拒绝上下文过载:如何让 Claude Code “渐进式阅读”
时间过得真快,距离上次发话题已经过去几个月,成年人的时间真是不经用。马上过年了,想罢年前一定要发点东西出来的。预祝大家新年快乐。
老实说,我在使用 AI coding 时,最抓狂的不是它写不出代码,而是它 太喜欢 “一口闷” 了。
场景通常是这样的:
我让 Claude Code 查一个 Bug,它二话不说读取了一个 5000 行的 server.log 或者把整个 utils.py 塞进上下文。结果就是:
- Token 燃烧:我的钱包在滴血。
- 上下文污染:关键信息被淹没在几千行无关代码里,它的智商瞬间掉线,开始胡言乱语。
- 响应变慢:处理大量 Token 需要时间。
这其实就是典型的 上下文过载(Context Overload。模型就像一个贪婪的读者,如果你不限制它,它会试图把整个图书馆搬回家,而不是只借那一本它需要的书。
最近我在研究 Anthropic 提倡的 渐进式披露(Progressive Disclosure),并折腾出了一套强制性的文件读取策略。今天分享给大家,亲测能让 Claude Code 的脑子清醒不少。
什么是 “渐进式披露”?
别被这个学术名词吓到。用人话说是:不要给 AI 看全图,除非它问你要。
这就好比你作为一个人类程序员接手新项目,你不会上来就把 10 万行代码从头读到尾。你会先看目录结构(ls),再搜关键字(grep),最后只打开相关的那几十行代码(read)。
Anthropic 的文档里一直强调这一点:让模型先通过搜索定位,再通过切片读取。
但在实际的 CLI 工具中,Claude 有时候很懒,或者说 “太勤快”,默认行为往往是直接 Read 全文。所以,我们需要给它装一个 “防呆开关”。
这个 Hook 是怎么工作的?
我写了一个 Python 脚本作为 PreToolUse 的 Hook(工具调用前拦截器),配合 CLAUDE.md 的提示词,搞了一套 软硬兼施 的组合拳。
核心逻辑
这个方案由两部分组成:
- “软” 规则(Prompt):在系统提示词里告诉它,读文件必须加
offset(起始行)和limit(行数限制)。 - “硬” 拦截(Hook 脚本):这是关键。当 Claude Code 试图调用
Read工具时,脚本会检查目标文件的大小。
- 如果文件超过 1000 行 或 ,且 Claude Code 没有 指定
offset/limit: - 拦截操作!返回 Exit Code 2。
- 杀手锏:在 stderr 里返回一段精心设计的报错信息。这段报错不仅告诉它 “你错了”,还告诉它 “你应该怎么做”(比如:推荐你先用 Grep 搜一下,然后只读第 X 行附近的 50 行)。
为什么它非常 Work?
这利用了 LLM 的一个特性:它们非常听 “报错信息” 的话。
当 Tool Use 失败并返回一个明确的 “推荐路径” 时,Claude 会立刻在这个报错的 Context 下进行自我修正。
- Claude: “我要读
app.log。” (未指定范围) - Hook: (拦截) “不行,文件太大了。你必须指定读取范围。建议先用
Grep搜一下关键词。” - Claude: (收到报错) “噢,抱歉。那我们就先用
Grep搜一下 ‘Error’ 关键字吧。”
看,这就强行把它拽回了 “渐进式披露” 的最佳实践路径上。
如何食用
你需要两个东西:一个是配置在项目根目录的规则文件,一个是实际执行拦截的 Python 脚本。
1. 提示词 (CLAUDE.md)
把这段加到你的项目提示词文件中。这相当于 “先礼后兵”,先告诉它规则。
中文版本
### 文件读取策略
** 强制规则 **:每次调用 Read 工具时 ** 必须 ** 指定 `offset` 和 `limit` 参数,禁止使用默认值。
#### 参数要求
| 参数 | 要求 | 说明 |
| ------ | -------------- | ----------------------------- |
| `offset` | ** 必须指定 ** | 起始行号(从 0 开始) |
| `limit` | ** 必须指定 ** | 读取行数,单次不超过 500 行 |
#### 读取流程 1. ** 侦察 **:先用 Grep 了解文件结构,或定位目标关键词行号。
2. ** 精准打击 **:使用 offset + limit 精确读取目标区域。
3. ** 扩展 **:如果需要更多上下文,再调整 offset 继续读取。
** 目标 **:保持上下文精准、最小化。如果不遵守,工具调用将被 Hook 拦截。English Version
### File Reading Strategy **MANDATORY RULE**: Every `Read` tool call **MUST** verify `offset` and `limit` parameters. Default full-file reads are prohibited for non-trivial files.
#### Parameter Requirements
| Param | Requirement | Description |
| -------- | -------------- | ----------------------------- |
| `offset` | **REQUIRED** | Start line number (0-indexed) |
| `limit` | **REQUIRED** | Max lines to read (Max 500) |
#### Workflow 1. **Recon**: Use `Grep` first to understand structure or locate keywords.
2. **Surgical Read**: Use `offset` + `limit` to read only the relevant section.
3. **Expand**: Adjust `offset` to read more context only if strictly necessary.
**Goal**: Keep context precise and minimal. Violations will be blocked by the PreToolUse hook.
2. The Hook (Python 脚本)
保存为 read_limit_hook.py,并在你的 Claude CLI 配置 hook(如果你不会可以直接把文件给 claude code 让它代劳)。
(这个脚本稍微有点长,但逻辑很简单:检查文件大小 → 检查参数 → 决定是放行、自动修正还是报错拦截)
#!/usr/bin/env python3 """
PreToolUse hook for Read tool - Enforce offset/limit and block large file reads.
""" import json
import sys
import os
from datetime import datetime
# --- 配置区域 ---
MAX_FILE_LINES = 1000 # 超过这个行数必须切片读
MAX_FILE_BYTES = 50 * 1024
MAX_SINGLE_READ_LINES = 500 # 一次最多读 500 行
MAX_SINGLE_READ_BYTES = 20 * 1024 # 跳过不需要检查的二进制文件
SKIP_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.pdf', '.exe', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'}
# 日志文件(可选,帮你分析它浪费了多少次尝试)
LOG_FILE = os.path.expandvars ("$USERPROFILE/.claude/hooks/read-stats.log")
def get_file_stats (file_path):
try:
if not os.path.exists (file_path): return None, None
size = os.path.getsize (file_path)
with open (file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = sum (1 for _ in f)
return lines, size
except:
return None, None def format_bytes (size):
if size >= 1024 * 1024: return f"{size / (1024 * 1024):.1f} MB" if size >= 1024: return f"{size / 1024:.1f} KB" return f"{size} B" def main ():
try:
input_data = json.load (sys.stdin)
except:
sys.exit (0) # 甚至不是 JSON,不管了
tool_name = input_data.get ("tool_name", "")
tool_input = input_data.get ("tool_input", {})
# 只管 Read 工具 if tool_name != "Read":
sys.exit (0)
file_path = tool_input.get ("file_path", "")
offset = tool_input.get ("offset")
limit = tool_input.get ("limit")
# 1. 扩展名检查
ext = os.path.splitext (file_path)[1].lower ()
if ext in SKIP_EXTENSIONS: sys.exit (0)
lines, size = get_file_stats (file_path)
if lines is None: sys.exit (0) # 读不到文件,让 Claude 自己处理错误 # 2. 检查是否是大文件
is_large_file = lines > MAX_FILE_LINES or size > MAX_FILE_BYTES
if is_large_file:
# 如果是大文件,且没有指定 offset 或 limit -> 拦截! if offset is None or limit is None:
reason = f"{lines} lines / {format_bytes (size)}"
error_msg = (
f"BLOCKED: File is too large ({reason}) for a full read.\n" f"You MUST use 'offset' and 'limit' to read specific sections.\n\n" f"Strategy:\n" f"1. Use`Grep`to find the line number of your function/variable.\n" f"2. Then`Read`with offset=LINE_NUM, limit=50.\n" f"DO NOT try to read the whole file again."
)
print (error_msg, file=sys.stderr)
sys.exit (2) # 2 通常表示操作被拒绝 # 3. 检查单次读取是否贪得无厌 if limit is not None and limit > MAX_SINGLE_READ_LINES:
print (f"BLOCKED: Limit {limit} is too high. Max allowed is {MAX_SINGLE_READ_LINES}.", file=sys.stderr)
sys.exit (2)
# 4. 贴心的小功能:如果有 offset 没 limit,自动帮它补上 limit,防止它犯傻 if offset is not None and limit is None:
output = {
"hookSpecificOutput": {
"permissionDecision": "allow",
"updatedInput": { "limit": MAX_SINGLE_READ_LINES }
}
}
print (json.dumps (output))
sys.exit (0)
sys.exit (0)
if __name__ == "__main__":
main ()
效果
装上这一套之后,你会发现 Claude 的行为模式变了:
- 以前:读取
main.c(3000 行) → 思考 → 修改。 - 现在:尝试读取
main.c→ 被拦截 → 思考 “哦,我应该先搜一下” → Grepmain函数 → 读取main函数周围 50 行 → 思考 → 修改。
虽然多了一步交互,但 上下文极其干净,Token 消耗量能降低 80% 以上,而且修改的准确率反而提高了。
试一下吧,让你的 Claude Code 甚至其他 Agent 学会渐进式的读取。