首页 / 资讯中心 / 文章详情

DeepSeek-Agent-Harness-2026终极指南-第9章第40节-核心工具集开发-文件三件套readwriteedit:从读文件到改代码

DeepSeek-Agent-Harness-2026终极指南-第9章第40节-核心工具集开发-文件三件套readwriteedit:从读文件到改代码 ★ FEATURED ARTICLE
DeepSeek Agent Harness 2026终极指南 - 第9章第40节 文件三件套read/write/edit从读文件到改代码第8章Agent Loop从零实现全部完成——50行最小Loop、装饰器注册、工具执行器、流式Agent、单元测试Agent的核心引擎已经就位。从本节开始进入第9章核心工具集开发给Agent装上真正的双手。先做文件三件套read_file/write_file/edit_file让Agent能读代码、写代码、改代码。这是AI编程Agent最基础也最重要的能力。本文导航为什么文件工具是Agent的双手read_file带行号的智能读取write_file原子写入防丢失edit_file精准替换old_str→new_str完整实现file_tools.py实测Agent读改写全流程小结为什么文件工具是Agent的双手第7章的Agent能调天气、查时间但它不能碰文件系统。你让它帮我写一个Python脚本它只能说你可以这样写…不能真的创建文件。文件三件套让Agent从顾问变成工程师read_fileAgent能读代码、读配置、读日志write_fileAgent能创建新文件、写脚本、写配置edit_fileAgent能改代码、修bug、加功能有了这三个工具Agent就能真正帮你干活读你的项目代码理解结构写新代码实现功能改旧代码修bug或优化这是AI编程Agent的核心能力。Cursor、Claude Code、Copilot Workspace——所有AI编程工具底层都有这三个文件工具。read_file带行号的智能读取read_file的设计要考虑三个问题问题1大文件怎么办一个10万行的文件全部读出来会占满上下文窗口。解决方案支持offset和limit参数只读一部分。问题2怎么定位到具体行读出来的内容要带行号方便Agent引用。比如第42行有个bugAgent能精确定位。问题3二进制文件怎么办图片、PDF、编译后的文件不能当文本读。解决方案检测文件类型二进制文件返回错误提示。实现frompathlibimportPathfromdeep_pilot.tool_registryimporttooltooldefread_file(path:str,offset:int1,limit:int1000)-str: 读取文件内容带行号。 参数 - path: 文件路径相对或绝对 - offset: 起始行号从1开始默认1 - limit: 最多读取多少行默认1000 返回带行号的文件内容格式为行号: 内容 file_pathPath(path)# 检查文件是否存在ifnotfile_path.exists():returnf错误文件不存在 {path}# 检查是否是文件不是目录ifnotfile_path.is_file():returnf错误{path} 不是文件# 检查是否是文本文件简单判断尝试读取前1KBtry:withopen(file_path,r,encodingutf-8)asf:f.read(1024)exceptUnicodeDecodeError:returnf错误{path} 可能是二进制文件无法作为文本读取# 读取指定范围try:withopen(file_path,r,encodingutf-8)asf:linesf.readlines()total_lineslen(lines)# 参数校验ifoffset1:offset1ifoffsettotal_lines:returnf错误offset{offset}超出文件总行数{total_lines}# 截取范围end_linemin(offsetlimit-1,total_lines)selected_lineslines[offset-1:end_line]# 格式化输出行号: 内容result_lines[]fori,lineinenumerate(selected_lines,startoffset):result_lines.append(f{i:4d}:{line.rstrip()})result\n.join(result_lines)# 如果截断了加提示ifend_linetotal_lines:resultf\n\n[文件共{total_lines}行已显示{offset}-{end_line}行]returnresultexceptExceptionase:returnf错误读取文件失败 -{str(e)}关键设计带行号输出f{i:4d}: {line.rstrip()}行号右对齐4位方便阅读。offsetlimit参数支持只读一部分避免大文件占满上下文。二进制文件检测尝试读取前1KB如果UnicodeDecodeError就报错。截断提示如果文件超过limit提示文件共X行已显示Y-Z行。write_file原子写入防丢失write_file的设计要考虑一个问题写到一半程序崩了怎么办如果直接open(path, w).write(content)写到一半程序崩了文件就毁了——旧内容没了新内容也没写完。解决方案原子写入。先写到临时文件写完后rename覆盖原文件。rename在同一个文件系统上是原子操作要么完全成功要么完全失败不会出现写了一半的情况。importosimporttempfilefrompathlibimportPathtooldefwrite_file(path:str,content:str)-str: 写入文件内容原子写入。 参数 - path: 文件路径 - content: 文件内容 返回成功/失败消息 file_pathPath(path)try:# 确保父目录存在file_path.parent.mkdir(parentsTrue,exist_okTrue)# 原子写入先写临时文件再rename# 临时文件和目标文件在同一目录确保在同一文件系统withtempfile.NamedTemporaryFile(modew,encodingutf-8,dirfile_path.parent,deleteFalse,suffix.tmp,)astmp:tmp.write(content)tmp_pathPath(tmp.name)# rename覆盖原文件原子操作tmp_path.rename(file_path)returnf成功已写入{path}{len(content)}字符exceptExceptionase:# 清理临时文件iftmp_pathinlocals()andtmp_path.exists():tmp_path.unlink()returnf错误写入文件失败 -{str(e)}关键设计原子写入先写临时文件.tmp后缀写完后rename覆盖原文件。同一文件系统临时文件放在目标文件的父目录确保rename是原子操作。自动创建父目录file_path.parent.mkdir(parentsTrue, exist_okTrue)。异常清理如果写入失败删除临时文件。edit_file精准替换old_str→new_stredit_file的设计思路不是在第X行插入Y而是把old_str替换成new_str。为什么不用行号因为行号容易变——你在第10行插入一行后面所有行号都变了模型不擅长数行号——让它在第42行插入它可能数错文本替换更直观——“把def foo()改成def bar()”模型更容易理解实现tooldefedit_file(path:str,old_str:str,new_str:str)-str: 编辑文件把 old_str 替换成 new_str。 参数 - path: 文件路径 - old_str: 要被替换的字符串必须精确匹配 - new_str: 替换后的字符串 返回成功/失败消息 file_pathPath(path)# 检查文件是否存在ifnotfile_path.exists():returnf错误文件不存在 {path}try:# 读取原文件contentfile_path.read_text(encodingutf-8)# 检查 old_str 是否存在ifold_strnotincontent:returnf错误在文件中找不到要替换的内容:\n{old_str}# 检查 old_str 是否出现多次countcontent.count(old_str)ifcount1:returnf错误要替换的内容出现了{count}次请提供更精确的 old_str# 替换new_contentcontent.replace(old_str,new_str,1)# 原子写入withtempfile.NamedTemporaryFile(modew,encodingutf-8,dirfile_path.parent,deleteFalse,suffix.tmp,)astmp:tmp.write(new_content)tmp_pathPath(tmp.name)tmp_path.rename(file_path)returnf成功已编辑{path}exceptExceptionase:iftmp_pathinlocals()andtmp_path.exists():tmp_path.unlink()returnf错误编辑文件失败 -{str(e)}关键设计精确匹配old_str必须完全匹配不支持正则避免复杂性和错误。唯一性检查如果old_str出现多次报错让模型提供更精确的内容。原子写入同write_file先写临时文件再rename。完整实现file_tools.py把三个工具整合成完整模块# deep_pilot/file_tools.py —— 文件三件套工具 v0.4from__future__importannotationsimporttempfilefrompathlibimportPathfromdeep_pilot.tool_registryimporttooltooldefread_file(path:str,offset:int1,limit:int1000)-str: 读取文件内容带行号。 参数 - path: 文件路径相对或绝对 - offset: 起始行号从1开始默认1 - limit: 最多读取多少行默认1000 返回带行号的文件内容格式为行号: 内容 file_pathPath(path)ifnotfile_path.exists():returnf错误文件不存在 {path}ifnotfile_path.is_file():returnf错误{path} 不是文件# 检查是否是文本文件try:withopen(file_path,r,encodingutf-8)asf:f.read(1024)exceptUnicodeDecodeError:returnf错误{path} 可能是二进制文件无法作为文本读取try:withopen(file_path,r,encodingutf-8)asf:linesf.readlines()total_lineslen(lines)ifoffset1:offset1ifoffsettotal_lines:returnf错误offset{offset}超出文件总行数{total_lines}end_linemin(offsetlimit-1,total_lines)selected_lineslines[offset-1:end_line]result_lines[]fori,lineinenumerate(selected_lines,startoffset):result_lines.append(f{i:4d}:{line.rstrip()})result\n.join(result_lines)ifend_linetotal_lines:resultf\n\n[文件共{total_lines}行已显示{offset}-{end_line}行]returnresultexceptExceptionase:returnf错误读取文件失败 -{str(e)}tooldefwrite_file(path:str,content:str)-str: 写入文件内容原子写入。 参数 - path: 文件路径 - content: 文件内容 返回成功/失败消息 file_pathPath(path)try:file_path.parent.mkdir(parentsTrue,exist_okTrue)withtempfile.NamedTemporaryFile(modew,encodingutf-8,dirfile_path.parent,deleteFalse,suffix.tmp,)astmp:tmp.write(content)tmp_pathPath(tmp.name)tmp_path.rename(file_path)returnf成功已写入{path}{len(content)}字符exceptExceptionase:iftmp_pathinlocals()andtmp_path.exists():tmp_path.unlink()returnf错误写入文件失败 -{str(e)}tooldefedit_file(path:str,old_str:str,new_str:str)-str: 编辑文件把 old_str 替换成 new_str。 参数 - path: 文件路径 - old_str: 要被替换的字符串必须精确匹配且只能出现一次 - new_str: 替换后的字符串 返回成功/失败消息 file_pathPath(path)ifnotfile_path.exists():returnf错误文件不存在 {path}try:contentfile_path.read_text(encodingutf-8)ifold_strnotincontent:returnf错误在文件中找不到要替换的内容:\n{old_str}countcontent.count(old_str)ifcount1:returnf错误要替换的内容出现了{count}次请提供更精确的 old_strnew_contentcontent.replace(old_str,new_str,1)withtempfile.NamedTemporaryFile(modew,encodingutf-8,dirfile_path.parent,deleteFalse,suffix.tmp,)astmp:tmp.write(new_content)tmp_pathPath(tmp.name)tmp_path.rename(file_path)returnf成功已编辑{path}exceptExceptionase:iftmp_pathinlocals()andtmp_path.exists():tmp_path.unlink()returnf错误编辑文件失败 -{str(e)}实测Agent读改写全流程在deep_pilot/tools.py里导入文件工具# deep_pilot/tools.py —— v0.4 加入文件工具fromdeep_pilot.file_toolsimportread_file,write_file,edit_file# 保留之前的工具tooldefget_weather(city:str)-str:获取指定城市今天的天气信息。returnf{city}晴天28°C实测Agent读改写全流程uv run python-c from deep_pilot.agent_loop import run # 测试1创建新文件 print( 测试1创建新文件 ) answer run(帮我创建一个 hello.py 文件内容是打印 Hello World) print(f\nAgent回答: {answer}) print() # 测试2读取文件 print( 测试2读取文件 ) answer run(读一下 hello.py 的内容) print(f\nAgent回答: {answer}) print() # 测试3编辑文件 print( 测试3编辑文件 ) answer run(把 hello.py 里的 Hello World 改成 Hello DeepSeek) print(f\nAgent回答: {answer}) print() # 测试4验证修改 print( 测试4验证修改 ) answer run(再读一下 hello.py确认修改成功) print(f\nAgent回答: {answer}) 控制台输出精简 测试1创建新文件 2026-09-12 21:00:01 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 21:00:01 | INFO | agent_loop | → 调用工具: write_file({path: hello.py, content: print(Hello World)\n}) 2026-09-12 21:00:01 | INFO | agent_loop | ← 工具结果: 成功已写入 hello.py22 字符 2026-09-12 21:00:01 | INFO | agent_loop | Loop 第 2 轮 ↻ Agent回答: 已创建 hello.py 文件内容是 print(Hello World)。 测试2读取文件 2026-09-12 21:00:02 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 21:00:02 | INFO | agent_loop | → 调用工具: read_file({path: hello.py}) 2026-09-12 21:00:02 | INFO | agent_loop | ← 工具结果: 1: print(Hello World) 2026-09-12 21:00:02 | INFO | agent_loop | Loop 第 2 轮 ↻ Agent回答: hello.py 的内容是 1: print(Hello World) 测试3编辑文件 2026-09-12 21:00:03 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 21:00:03 | INFO | agent_loop | → 调用工具: edit_file({path: hello.py, old_str: Hello World, new_str: Hello DeepSeek}) 2026-09-12 21:00:03 | INFO | agent_loop | ← 工具结果: 成功已编辑 hello.py 2026-09-12 21:00:03 | INFO | agent_loop | Loop 第 2 轮 ↻ Agent回答: 已将 hello.py 里的 Hello World 改成 Hello DeepSeek。 测试4验证修改 2026-09-12 21:00:04 | INFO | agent_loop | Loop 第 1 轮 ↻ 2026-09-12 21:00:04 | INFO | agent_loop | → 调用工具: read_file({path: hello.py}) 2026-09-12 21:00:04 | INFO | agent_loop | ← 工具结果: 1: print(Hello DeepSeek) 2026-09-12 21:00:04 | INFO | agent_loop | Loop 第 2 轮 ↻ Agent回答: 确认修改成功hello.py 的内容现在是 1: print(Hello DeepSeek)四个测试都通过了创建文件Agent调write_file创建hello.py读取文件Agent调read_file读取内容看到带行号的输出编辑文件Agent调edit_file把Hello World改成Hello DeepSeek验证修改Agent再次调read_file确认修改成功注意Agent的回答都是自然语言它理解了工具返回的结果然后用用户能理解的方式表达。小结文件三件套是Agent的双手read_file读代码、write_file写代码、edit_file改代码。read_file带行号f{i:4d}: {line.rstrip()}方便Agent引用具体行。支持offsetlimit读大文件的一部分。write_file原子写入先写临时文件再rename防止写到一半程序崩了毁文件。edit_file精准替换old_str→new_str不支持正则要求唯一匹配。比行号更直观。二进制文件检测read_file尝试读取前1KBUnicodeDecodeError就报错。错误消息友好文件不存在、不是文件、二进制文件、old_str出现多次——都给模型能理解的提示。DeepPilot v0.4文件工具完成——Agent从顾问变成工程师能真正帮你读改写代码。下节预告文件三件套搞定了但Agent还不能执行命令。你让它跑一下pytest它只能说你可以在终端运行…不能真的执行。下一节做bash执行器run_bash用subprocess.run封装命令执行支持超时杀死、输出截断、工作目录约束。从此Agent能跑测试、装依赖、编译代码真正成为你的编程助手。如果觉得本文对你有帮助欢迎点赞、收藏、关注三连本系列持续更新中关注不迷路~
阅读完成 · 觉得有帮助?
咨询建站