'必须先读'的工作流约束在哪里
工作流约束在 prompt 而非 schema
一、原理:跨工具的依赖关系怎么表达
3.5 章我们看了 Edit 的 9 层 fallback。但 Edit 工具还有一个前置条件——LLM 必须先用 Read 读过这个文件才能用 Edit 改它。
如果 LLM 跳过 Read 直接 Edit——opencode 会拒绝:
"You must use your Read tool at least once in the conversation before editing."
这是个有意思的约束。从纯技术角度看,Edit 不"需要"Read 才能工作——文件系统能让你随便改文件。但 opencode 强制要求先 Read,这是产品判断。
为什么这么设计?要理解这个,我们要先理解 AI Agent 设计里的一个根本问题——跨工具的依赖关系怎么表达。
很多 tool 之间有"应该先做什么"的关系:
- Edit 文件之前应该先 Read 文件
- Edit 多处的时候应该先用 multiedit 而不是多次 edit
- Bash 跑命令之前应该 cd 到正确目录
- Task 派生 subagent 之前应该确认主 agent 自己做不了
- Webfetch 之前应该考虑能不能用 websearch 找到更精准的源
这些"应该"是工作流约束。它们不能用 schema 表达(schema 只描述参数)、不能用代码强制(代码不知道 LLM 的意图)——它们只能用 prompt 表达。
但 prompt 表达约束有几种方式:
方式 A:在 system prompt 里写
"When editing files, always read them first."
放在全局 system prompt。所有工具都看到。
方式 B:在工具 description 里写
把约束写在 edit.txt 里——"You must use Read first"。LLM 看到 edit 的描述就看到约束。
方式 C:在 tool 实现里强制
Edit 实现里检查"是否在 conversation 历史里 Read 过这个文件"。没读过就报错。
每种方式有优缺点。
A(system prompt):好处是统一表达、所有工具同步。缺点是 system prompt 容易过长、约束多了 LLM 会忽略中间的(lost in the middle)。
B(description):好处是约束跟工具绑定、容易维护。缺点是 LLM 在调 Read 时可能看不到 Edit 的约束(除非 LLM 提前知道接下来要 Edit)。
C(实现强制):好处是 100% 执行、不依赖 LLM 自觉。缺点是 LLM 不知道"为什么报错"——只能从 error message 学。
opencode 怎么选?B + C 组合。约束写在 edit.txt 的 description 里告诉 LLM "应该这样";同时在 edit 的实现里强制执行——没 Read 过就报错。
这种组合让约束事前预防 + 事后强制——LLM 看 description 时被引导、忘了的话被实现层挡住。
二、案例:edit.txt 里的工作流约束
打开 packages/opencode/src/tool/edit.txt,开头有这么一段:
Performs exact string replacements in files.
Usage:
- You must use your Read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
注意几个关键约束:
约束 1:必须先 Read
"You must use your Read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file."
两层表达:
- "you must use Read first" — 给 LLM 的明确指引
- "This tool will error" — 警告 LLM 不遵守会失败
这种"指引 + 警告"组合让 LLM 知道为什么要先 Read(不是 nice-to-have,是必须)。
约束 2:保留行号前缀后的精确缩进
"ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix"
这是个细致的约束。Read 返回的格式是 1: import sys—— 1: 是行号前缀,后面才是真实文件内容。如果 LLM 把行号前缀也算进 old_string,匹配会失败。
description 明确说"AFTER the line number prefix"——避免这个常见错误。
约束 3:优先 edit 已有文件而不是创建新文件
"ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required."
为什么?因为 LLM 经常有个本能——"我要解决问题,建个新文件来放方案"。但用户的需求往往是"在现有文件里改"——LLM 自己加新文件让代码库变乱。
这条约束让 LLM 默认偏向"修改已有",除非用户明确要求"创建新"。
约束 4:emoji 禁令
"Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked."
LLM 有个倾向——在代码里加 emoji(注释里、字符串里)。代码库里出现 emoji 是个奇怪体验——专业代码很少这样。
明确禁了——除非用户主动要。
约束 5:unique match 或 replace_all
"The edit will FAIL if
old_stringis not unique in the file. Either provide a larger string with more surrounding context to make it unique or usereplace_allto change every instance ofold_string."
这是 3.5 章讲过的——匹配多个时报错。但 description 在这里提前告诉 LLM "解决方案有两条路"——加 context 或者用 replace_all。
让 LLM 看到这个约束的时候就知道怎么应对,不需要等失败后才学。
把这 5 个约束合起来,edit.txt 的开头是个"工作流约束清单"——告诉 LLM "用这个工具要遵守这些规则"。
特别值得注意的是 Read-before-Edit 的实现层强制。
packages/opencode/src/tool/edit.ts 里有个检查:
const hasReadFile = ctx.session.history.some(msg =>
msg.parts.some(part =>
part.type === "tool-read" &&
part.input.file_path === params.file_path
)
)
if (!hasReadFile) {
throw new Error("You must use your Read tool at least once in the conversation before editing.")
}
简化版。实际代码可能更细——检查 Read 时间、检查文件是否被 git 修改过等。但核心逻辑是——遍历当前 session 的消息历史,看有没有 Read 过这个文件路径。
没读过——抛错。错误信息就是 description 里那句话——保持一致,让 LLM 一眼看到错误信息就知道是什么问题。
三、设计启示:工作流约束在 prompt 而非 schema
这一章的核心论点:跨工具的"先做什么再做什么"约束不能用 schema 表达——必须在 prompt 和实现层双重保障。
设计 AI Agent 的工作流约束时,下面几条原则有用:
1. 识别真正的工作流约束
不是所有"惯例"都需要做成约束。Edit 必须先 Read 是真约束(不读容易出错),但 Webfetch 之前必须 Websearch 不是真约束(取决于场景)。
把"必须遵守"的和"建议这样"的分开。前者用 description + 实现强制,后者只用 description。
2. description 里明确"为什么"
不只说"必须 Read first",要说"否则会 error"——LLM 知道后果才会重视。
可以更进一步——"必须 Read first 这样 Edit 才知道你的 old_string 准确"——让 LLM 理解约束的产品逻辑。
3. 实现层强制要给清晰错误
throw new Error("You must use Read first") 比 throw new Error("Operation failed") 强一百倍。LLM 看错误直接知道下一步——先 Read 再 Edit。
错误信息跟 description 用同样的措辞——让 LLM 把它们关联起来。
4. 约束不能太多
一个工具最多 5-10 条约束。再多 LLM 看不过来、记不住。如果你发现某个工具有 20 条约束——考虑是不是该拆成多个工具。
opencode 的 edit 有 5-6 条约束(加上行号前缀那一条算 6)——上限。
5. 跨 session 的约束要小心
"必须先 Read 才能 Edit"在 single session 里成立——history 里检查得到。如果跨 session 怎么办?比如用户开新 session 想 Edit 之前 session 里 Read 过的文件——重新 Read 还是允许?
opencode 选择:每个 session 独立检查,跨 session 不传递。这样新 session 必须重新 Read。
这是个 trade-off——增加用户友好(不要假设跨 session 状态)vs 增加成本(重复 Read)。
6. 约束的更新要谨慎
约束加进去后 LLM 会"学到"——之后每次 Edit 都自动先 Read。如果你后来想去掉这个约束,LLM 还会按旧习惯做——浪费 token。
加约束容易,去约束难。慎重决定。
7. 跨工具约束最常见的是"前置条件"
Edit before Read、commit before push、build before deploy——这种 "B 必须在 A 之后" 的模式很常见。统一用 description + 实现强制处理。
应用到你的 AI 产品:
- 如果你有
send_emailtool,前置要求"先 generate_email_draft"——避免直接发未审的内容 - 如果你有
delete_recordtool,前置要求"先 confirm_with_user"——避免误删 - 如果你有
deploytool,前置要求"先 run_tests"——避免上线坏代码
把这些约束在 description 里明确写出 + 实现层强制——你的 AI 产品就避免了大量"用户不期望"的行为。
最后一个观察。"必须先 Read" 这种约束很多 AI 产品没做——它们的 Edit 工具直接接 LLM 的输入。结果是常见的 bug——LLM 改了一个没看过的文件、改完发现不对、又来回拆腾。
opencode 把这个约束做进 description + 实现的双重保障——一个看似小细节,让"AI 改代码"这件事的成功率提升一档。这是产品工程细节决定体验的典型例子——demo 里看不到,但生产环境每天救你一次。
下一章 3.7 我们看 bash 工具——AI 跑 shell 命令时面对的"跨平台 + 安全"双重挑战。这是 opencode 工程化复杂度最高的工具之一。