Read 的 2000 行默认
基础工具的'教新手'使命
一、原理:高频工具的"教新手"问题
任何 Agent 产品里,read 工具都是被调用最频繁的工具之一。LLM 处理代码任务时几乎每个 turn 都会读文件——读源码、读配置、读 README、读 git log。
但高频不代表简单。read 看起来是个"路径进、内容出"的简单工具,实际上它隐藏了一堆产品判断:
- 文件大怎么办?全读吗?读多少行?
- 行号要不要加?为什么?
- 二进制文件呢?图片呢?PDF 呢?
- 路径写错了怎么报错?
- 编码怎么处理?UTF-8、UTF-16、GBK?
- 行结尾是 LF 还是 CRLF?转吗?
- 文件刚被改、磁盘还没 flush 怎么办?
每个判断都影响 LLM 的下一步决策。读错文件 LLM 会基于错信息做错事——产品体验崩。
而且 read 是 LLM "刚学会"用的第一个工具。新手 LLM(指刚开始这个 session、还不熟悉这个 Agent 的工具集)调 read 的失败率比调其他工具高——因为它对 read 的"行为约束"还没建立直觉。
所以 read 的 description 承担一个特殊使命——"教新手"。它不只描述功能,它教 LLM 如何用 read 才能成功。
这个使命让 read 的 description 比其他工具长得多。我们看 opencode 怎么处理。
二、案例:read.txt 的 12 条注意事项
打开 packages/opencode/src/tool/read.txt,全文大概是这样(节选):
Reads a file from the filesystem. You can access any file directly by using this tool.
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
Usage:
- The file_path parameter must be an absolute path, not a relative path
- By default, it reads up to 2000 lines starting from the beginning of the file
- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
- Any lines longer than 2000 characters will be truncated
- Results are returned using cat -n format, with line numbers starting at 1
- This tool allows Claude to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.
- This tool can read Jupyter notebooks (.ipynb files) and returns all cells (code, markdown, raw) with their outputs.
- For Jupyter notebooks (.ipynb files), use the NotebookRead instead
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
Notes:
- ALWAYS prefer reading a file in its entirety unless you are dealing with very long files (>10,000 lines)
- When dealing with images, use Read to view them. The tool will process and analyze them.
- For very large files, you can use offset and limit to read sections of the file at a time. However, avoid reading tiny repeated slices (30 line chunks). When in doubt, read more not less.
- This will read up to 2000 lines from the beginning of the file by default. If the file is longer, the read tool will fail with a helpful error.
- For binary files this tool will not work.
我们逐条拆解里面的产品判断。
判断 1:绝对路径而非相对路径
"The file_path parameter must be an absolute path, not a relative path"
为什么?因为 LLM 经常搞错"当前工作目录"。它以为 cwd 是 project root,实际上可能是别的。强制绝对路径消除歧义——LLM 必须自己拼出完整路径,agent 不做"猜测当前目录"的事。
这是个用户体验偏好的妥协——人写脚本喜欢相对路径,但 LLM 容易出错。所以 opencode 选了"对 LLM 友好"——强制绝对路径。
判断 2:2000 行默认
"By default, it reads up to 2000 lines starting from the beginning of the file" "ALWAYS prefer reading a file in its entirety unless you are dealing with very long files (>10,000 lines)"
为什么 2000 行?这是个权衡:
- 太少(比如 100 行)——LLM 经常拿不到完整 context,要多次 read 拼起来
- 太多(比如 50,000 行)——超大文件直接吃光 context,浪费 token
- 2000 行是大多数文件都能全读完的范围(项目里 90% 的源文件 < 2000 行)
这个数字是 opencode 团队大量实验调出来的。如果你做类似产品,你的 2000 可能是 1000 或者 5000——取决于你的用户场景。
判断 3:行号 (cat -n 格式)
"Results are returned using cat -n format, with line numbers starting at 1"
为什么要加行号?因为 LLM 后面要 edit 这个文件——edit 需要"精确指定行"。如果 read 输出没有行号,LLM 要自己数行——容易数错。
带行号的格式是:
1: import sys
2:
3: def main():
这种 <行号>: <内容> 格式让 LLM 直接复用——它要改第 3 行就引用 3: def main():。
注意一个细节:行号用右对齐空格填充(" 1:"、" 10:"、" 100:"、"1000:")。这让所有行的内容部分对齐——人读起来好看、LLM 处理时也容易识别。
判断 4:超长行截断
"Any lines longer than 2000 characters will be truncated"
为什么?因为某些文件有超长行——比如 minified JS 一行几十万字符。如果不截断,一行就能吃光 context window。
但截断有个副作用——LLM 看到的内容跟实际文件不一样。所以 description 明确说明"会截断"——让 LLM 知道这个事实,可以决定是不是用 grep 找具体内容而不是 read。
判断 5:图片处理
"This tool allows Claude to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually..."
read 不止读文本,也读图片。但图片走的是 multimodal 通道——LLM "看到"图片本身,不是 base64 字符串。
这意味着 LLM 可以看图回答问题——"这个截图里的错误信息是什么"、"这个 UI 设计有什么问题"。但前提是 LLM 支持 multimodal(Claude、GPT-4V 支持,老的 GPT-3.5 不行)。
description 明确说支持图片——让 LLM 知道可以用这个 tool 看图。
判断 6:PDF 处理
"This tool can read PDF files (.pdf). PDFs are processed page by page..."
PDF 是常见的文档格式。但读 PDF 跟读文本不一样——要提取文本、可能要 OCR、可能要识别表格和图表。
opencode 把这部分逻辑内置在 read 里——LLM 不需要单独的 read_pdf 工具,它就用 read。这是个产品判断:简化 tool 集——避免"read、read_pdf、read_image、read_notebook"这种重复工具。
判断 7:Jupyter notebook
"This tool can read Jupyter notebooks (.ipynb files) and returns all cells..."
类似 PDF——notebook 是结构化文档,read 内部识别后给 LLM 返回结构化结果(每个 cell 的代码、输出、markdown)。
不需要单独的 notebook 工具。
判断 8:并行化建议
"It is always better to speculatively read multiple files as a batch that are potentially useful"
read 是高频但便宜的操作。LLM 应该批量并行 read 而不是一次读一个再决定下一个。
这条建议让 LLM 在调 read 时倾向于"先批量探索"而不是"逐个分析"——前者快得多。
判断 9:避免小片段重复读
"Avoid reading tiny repeated slices (30 line chunks). When in doubt, read more not less."
LLM 有时候会犯一个错——为了"省 token"反复读小片段。结果是来回 5 次 read 反而比一次 read 全文更费。
description 明确说"宁多读"——校准 LLM 的本能。
判断 10:空文件警告
"If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents"
空文件容易让 LLM 困惑——"我读了为什么没内容"。opencode 主动给个 warning 而不是返回空字符串——让 LLM 知道"是空,不是 read 失败"。
判断 11:二进制文件
"For binary files this tool will not work"
明确告诉 LLM 这个边界。LLM 看到二进制文件就知道不要尝试 read。
判断 12:错误处理
"It is okay to read a file that does not exist; an error will be returned"
读不存在的文件不是 catastrophic error——LLM 收到错误后可以决定是路径错了还是文件还没创建。
这跟其他工具的错误处理是同一种风格——错误是"可恢复的信号",不是"产品崩溃"。
这 12 条判断叠加,让 read 的 description 长得"违和"——比 grep 或 glob 长 3-5 倍。但每一条都对应一个常见 LLM 误用——它们是经验积累的产物。
三、设计启示:高频工具值得花最多 prompt 工程
这一章的核心论点:高频工具的 description 应该是产品里最长、最精心、最反复调整的——因为每条判断会被复用几百万次。
如果你做 AI 产品有高频工具,下面几条原则有用:
1. 用调用频次决定 description 长度
如果一个工具一天被调 10 万次,每条 description 改动会影响 10 万次决策。值得花周级时间调。
如果一个工具一周被调 100 次,description 简短即可。
2. 从 LLM 真实误用中提取规则
不要凭空想象规则——观察 LLM 怎么用错的。每个常见错误对应一条 description 里的明确指引。
opencode 的 12 条规则都是从真实失败案例提取的——每条对应一个曾经踩过的坑。
3. 教 LLM 选择默认行为
"by default, it reads 2000 lines"——明确告诉 LLM 默认值。LLM 不需要每次都纠结 limit 该填多少——默认行为就够用。
只有特殊场景才需要 LLM 偏离默认("如果是很长文件,可以用 offset 翻页")。
4. 把"边界情况"前置说明
空文件、二进制文件、超长行——这些边界 LLM 会遇到。提前在 description 里说清楚——避免 LLM 拿到奇怪结果后困惑。
5. 用"对 LLM 友好"代替"对人友好"
行号格式、绝对路径、明确错误——这些都是为 LLM 优化的。可能跟"人写脚本的习惯"冲突,但 LLM 才是 description 的主要读者。
6. 给反例和正例都举例子
"应该这样:批量并行 read 多个文件" "不应该这样:30 行 30 行小片段反复 read"
并列对比让 LLM 直接选对路径。
7. 持续迭代
每次 LLM 用错——记录、分析、加 description 里的规则。description 是个活的文档,不是写完就放着。
最后一个观察。read 的 description 长度——表面看起来"啰嗦"——是个反直觉的优化。直觉上 prompt 越短越好(省 token、避免 LLM 分心)。但实际上 read 的高调用频次和高误用率让"详细 description"成为最佳 ROI:
- 一次写好 12 条规则,每次调用都受益
- 减少 LLM 误用,减少 retry 浪费
- 12 条规则只占几百 tokens,跟节省的浪费比微不足道
理解这个 trade-off,你就知道 prompt engineering 不是"越短越好"——是按 ROI 优化。高频工具值得"啰嗦",低频工具应该"精简"。这是经验丰富的 AI 产品工程师跟新手的关键差别。
下一章 3.6 我们看 read 跟 edit 之间一个特别的关系——"必须先读才能改"的工作流约束。这是把"流程约束"显式编码到 prompt 的经典案例。