ARTICLE · 980731
AI 能读不能写?两个工具让它真正“动手”改代码
AI 能读不能写?两个工具让它真正“动手”改代码
# 从零做编程助手 · 007 | 文件操作:让 AI 真正“动手”改代码
先看一个尴尬场景
前面我们实现了工具调用框架,但只有 read_file(读)和 list_directory(列目录)。这让 AI 只能“看”,不能“写”:
你: 帮我创建一个 hello.py,打印 Hello WorldAI: 好的,我来帮你创建这个文件。但是我目前只有 read_file 工具,无法写入文件。你可以手动创建这个文件,内容如下:print(”Hello World”)(AI 表示,只有眼睛,没有手,有什么活还是得你自己干)
这篇补上写的能力,加入两个最核心的文件操作工具:
write_file | ||
edit_file |
为什么需要 edit_file ?不能直接都让 write_file 干了吗?
修改文件有两种思路:
方式 A:全量覆盖(write_file)
1. read_file(”main.py”) → 读入完整内容2. 在脑子里修改3. write_file(”main.py”, 修改后的完整内容) → 覆盖原文件
方式 B:精确替换(edit_file)
1. read_file(”main.py”) → 找到要改的那几行2. edit_file(”main.py”, old_str=”x = 1”, new_str=”x = 2”)
方式 A 简单直接,但有两个问题:在文件很大时,每次传输完整内容很浪费 Token;LLM 可能会以为"幻觉"的关系,不小心改到不想改的地方。
方式 B 是目前主流 Harness 工具采用的方式,也是本文的重点:old_str → new_str 精确替换。
edit_file(file_path="main.py",old_str="x = 1", # 要找的旧文本(必须精确匹配)new_str="x = 2", # 要替换成的新文本)
这个设计有三个好处:
工具定义
write_file(写入文件)
{"type": "function","function": {"name": "write_file","description": "创建新文件或覆盖已有文件。如果文件已存在,其内容将被完全替换。","parameters": {"type": "object","properties": {"file_path": {"type": "string", "description": "要写入的文件路径"},"content": {"type": "string", "description": "要写入的文件完整内容"}},"required": ["file_path", "content"]}}}
edit_file(编辑文件)
{"type": "function","function": {"name": "edit_file","description": "精确替换文件中的指定内容。找到 old_str 并用 new_str 替换。old_str 必须在文件中精确匹配且唯一。","parameters": {"type": "object","properties": {"file_path": {"type": "string", "description": "要编辑的文件路径"},"old_str": {"type": "string", "description": "要被替换的原始文本,必须精确匹配(包括空格、缩进、换行)"},"new_str": {"type": "string", "description": "替换后的新文本"}},"required": ["file_path", "old_str", "new_str"]}}}
工具实现
write_file
def _write_file(file_path, content):"""创建或覆盖文件"""try:os.makedirs(os.path.dirname(file_path) or ".", exist_ok=True)with open(file_path, "w", encoding="utf-8") as f:f.write(content)return f"文件 '{file_path}' 写入成功({len(content)} 字符)"except Exception as e:return f"写入文件失败: {e}"
edit_file
def _edit_file(file_path, old_str, new_str):"""精确替换文件中的内容"""try:with open(file_path, "r", encoding="utf-8") as f:file_content = f.read()except FileNotFoundError:return f"错误:文件 '{file_path}' 不存在,请先使用 read_file 确认"# 查找 old_str 在文件中的出现次数count = file_content.count(old_str)if count == 0:return f"错误:在文件 '{file_path}' 中未找到指定的 old_str。请使用 read_file 重新读取文件确认内容。"if count > 1:return f"错误:old_str 在文件中出现了 {count} 次,不够唯一。请提供更多上下文使 old_str 唯一。"new_content = file_content.replace(old_str, new_str, 1)try:with open(file_path, "w", encoding="utf-8") as f:f.write(new_content)return f"文件 '{file_path}' 编辑成功:将 {len(old_str)} 字符替换为 {len(new_str)} 字符"except Exception as e:return f"编辑文件失败: {e}"
为什么匹配失败时要报错,不能兼容或容错?
这是一个刻意的设计,逼着 LLM 先读文件:
正确流程:read_file("main.py") ← 先读edit_file("main.py", old_str="...精确内容...", new_str="...")错误流程:edit_file("main.py", old_str="...猜测的内容...", new_str="...")错误:未找到 old_str! ← 被拒绝LLM 被迫回头 read_file,再给出正确的 old_str
这个“严厉”的设计,会让 AI 的行为更可控、更安全。
完整代码变化
与第 6 步相比,变化集中在 3 处:
TOOLS 列表新增 write_file和edit_file两个定义;execute_tool() 新增两个分支; 系统提示词更新使用指引:
SYSTEM_PROMPT = """...使用工具时:1. 修改文件前,必须先 read_file 读取文件内容2. 创建新文件用 write_file,修改已有文件用 edit_file3. edit_file 的 old_str 必须从文件中精确复制(包括缩进和空格)4. 如果 edit_file 报错”未找到 old_str”,重新 read_file 后再试..."""
运行演示
你: 创建一个 hello.py,内容打印 Hello World 并包含一个 main 函数AI: 好的,我来创建这个文件。执行工具: write_file({'file_path': 'hello.py', 'content': '...'})文件 'hello.py' 写入成功你: 给 hello.py 加上类型注解和文档AI: 让我先读取文件内容。执行工具: read_file({'file_path': 'hello.py'})def main():print(”Hello World”)if __name__ == ”__main__”:main()我看到文件内容了,现在添加类型注解和文档。执行工具: edit_file({'file_path': 'hello.py','old_str': 'def main():\n print(”Hello World”)','new_str': 'def main() -> None:\n ”””程序入口,打印问候语”””\n print(”Hello World”)'})文件 'hello.py' 编辑成功:42 字符替换为 75 字符已完成!给 main 函数加上了返回类型注解 -> None 和文档字符串。
小结
关键:edit_file 的 old_str → new_str 模式是 AI 编程助手的核心设计之一。它通过“严格匹配”逼 LLM 先读后改、精确操作,有效防止幻觉修改。
下一篇
现在 AI 能读、能写、能改文件了,但还缺一个能执行命令的关键能力。下一篇加入 bash 工具,让 AI 能运行 pip install、python test.py、git status,真正成为全能的编程助手。
如果这篇文章对你有帮助:
点个「在看」,让我知道这个系列有人需要 转发给一个好奇“AI 怎么改代码”的朋友 💡 思考题:如果 LLM 在 edit_file 时故意给出一个模糊的 old_str(比如只写一个字母 "x"),会发生什么?看看代码里的唯一性检查怎么处理。