news 2026/10/2 18:06:21

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

作者头像

张小明

前端开发工程师

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

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_file:Agent能读代码、读配置、读日志
  • write_file:Agent能创建新文件、写脚本、写配置
  • edit_file:Agent能改代码、修bug、加功能

有了这三个工具,Agent就能真正帮你干活:

  1. 读你的项目代码,理解结构
  2. 写新代码,实现功能
  3. 改旧代码,修bug或优化

这是AI编程Agent的核心能力。Cursor、Claude Code、Copilot Workspace——所有AI编程工具底层都有这三个文件工具。

read_file:带行号的智能读取

read_file的设计要考虑三个问题:

问题1:大文件怎么办?
一个10万行的文件,全部读出来会占满上下文窗口。解决方案:支持offset和limit参数,只读一部分。

问题2:怎么定位到具体行?
读出来的内容要带行号,方便Agent引用。比如"第42行有个bug",Agent能精确定位。

问题3:二进制文件怎么办?
图片、PDF、编译后的文件不能当文本读。解决方案:检测文件类型,二进制文件返回错误提示。

实现:

frompathlibimportPathfromdeep_pilot.tool_registryimporttool@tooldefread_file(path:str,offset:int=1,limit:int=1000)->str:""" 读取文件内容,带行号。 参数: - path: 文件路径(相对或绝对) - offset: 起始行号(从1开始),默认1 - limit: 最多读取多少行,默认1000 返回:带行号的文件内容,格式为"行号: 内容" """file_path=Path(path)# 检查文件是否存在ifnotfile_path.exists():returnf"错误:文件不存在 '{path}'"# 检查是否是文件(不是目录)ifnotfile_path.is_file():returnf"错误:'{path}' 不是文件"# 检查是否是文本文件(简单判断:尝试读取前1KB)try:withopen(file_path,"r",encoding="utf-8")asf:f.read(1024)exceptUnicodeDecodeError:returnf"错误:'{path}' 可能是二进制文件,无法作为文本读取"# 读取指定范围try:withopen(file_path,"r",encoding="utf-8")asf:lines=f.readlines()total_lines=len(lines)# 参数校验ifoffset<1:offset=1ifoffset>total_lines:returnf"错误:offset={offset}超出文件总行数{total_lines}"# 截取范围end_line=min(offset+limit-1,total_lines)selected_lines=lines[offset-1:end_line]# 格式化输出:行号: 内容result_lines=[]fori,lineinenumerate(selected_lines,start=offset):result_lines.append(f"{i:4d}:{line.rstrip()}")result="\n".join(result_lines)# 如果截断了,加提示ifend_line<total_lines:result+=f"\n\n[文件共{total_lines}行,已显示{offset}-{end_line}行]"returnresultexceptExceptionase:returnf"错误:读取文件失败 -{str(e)}"

关键设计:

  1. 带行号输出:f"{i:4d}: {line.rstrip()}",行号右对齐4位,方便阅读。
  2. offset+limit参数:支持只读一部分,避免大文件占满上下文。
  3. 二进制文件检测:尝试读取前1KB,如果UnicodeDecodeError就报错。
  4. 截断提示:如果文件超过limit,提示"文件共X行,已显示Y-Z行"。

write_file:原子写入防丢失

write_file的设计要考虑一个问题:写到一半程序崩了怎么办?

如果直接open(path, "w").write(content),写到一半程序崩了,文件就毁了——旧内容没了,新内容也没写完。

解决方案:原子写入。先写到临时文件,写完后rename覆盖原文件。rename在同一个文件系统上是原子操作,要么完全成功,要么完全失败,不会出现"写了一半"的情况。

importosimporttempfilefrompathlibimportPath@tooldefwrite_file(path:str,content:str)->str:""" 写入文件内容(原子写入)。 参数: - path: 文件路径 - content: 文件内容 返回:成功/失败消息 """file_path=Path(path)try:# 确保父目录存在file_path.parent.mkdir(parents=True,exist_ok=True)# 原子写入:先写临时文件,再rename# 临时文件和目标文件在同一目录,确保在同一文件系统withtempfile.NamedTemporaryFile(mode="w",encoding="utf-8",dir=file_path.parent,delete=False,suffix=".tmp",)astmp:tmp.write(content)tmp_path=Path(tmp.name)# rename覆盖原文件(原子操作)tmp_path.rename(file_path)returnf"成功:已写入{path}({len(content)}字符)"exceptExceptionase:# 清理临时文件if"tmp_path"inlocals()andtmp_path.exists():tmp_path.unlink()returnf"错误:写入文件失败 -{str(e)}"

关键设计:

  1. 原子写入:先写临时文件(.tmp后缀),写完后rename覆盖原文件。
  2. 同一文件系统:临时文件放在目标文件的父目录,确保rename是原子操作。
  3. 自动创建父目录:file_path.parent.mkdir(parents=True, exist_ok=True)。
  4. 异常清理:如果写入失败,删除临时文件。

edit_file:精准替换old_str→new_str

edit_file的设计思路:不是"在第X行插入Y",而是"把old_str替换成new_str"。

为什么不用行号?因为:

  1. 行号容易变——你在第10行插入一行,后面所有行号都变了
  2. 模型不擅长数行号——让它"在第42行插入",它可能数错
  3. 文本替换更直观——“把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_path=Path(path)# 检查文件是否存在ifnotfile_path.exists():returnf"错误:文件不存在 '{path}'"try:# 读取原文件content=file_path.read_text(encoding="utf-8")# 检查 old_str 是否存在ifold_strnotincontent:returnf"错误:在文件中找不到要替换的内容:\n{old_str}"# 检查 old_str 是否出现多次count=content.count(old_str)ifcount>1:returnf"错误:要替换的内容出现了{count}次,请提供更精确的 old_str"# 替换new_content=content.replace(old_str,new_str,1)# 原子写入withtempfile.NamedTemporaryFile(mode="w",encoding="utf-8",dir=file_path.parent,delete=False,suffix=".tmp",)astmp:tmp.write(new_content)tmp_path=Path(tmp.name)tmp_path.rename(file_path)returnf"成功:已编辑{path}"exceptExceptionase:if"tmp_path"inlocals()andtmp_path.exists():tmp_path.unlink()returnf"错误:编辑文件失败 -{str(e)}"

关键设计:

  1. 精确匹配:old_str必须完全匹配,不支持正则(避免复杂性和错误)。
  2. 唯一性检查:如果old_str出现多次,报错让模型提供更精确的内容。
  3. 原子写入:同write_file,先写临时文件再rename。

完整实现:file_tools.py

把三个工具整合成完整模块:

# deep_pilot/file_tools.py —— 文件三件套工具 v0.4from__future__importannotationsimporttempfilefrompathlibimportPathfromdeep_pilot.tool_registryimporttool@tooldefread_file(path:str,offset:int=1,limit:int=1000)->str:""" 读取文件内容,带行号。 参数: - path: 文件路径(相对或绝对) - offset: 起始行号(从1开始),默认1 - limit: 最多读取多少行,默认1000 返回:带行号的文件内容,格式为"行号: 内容" """file_path=Path(path)ifnotfile_path.exists():returnf"错误:文件不存在 '{path}'"ifnotfile_path.is_file():returnf"错误:'{path}' 不是文件"# 检查是否是文本文件try:withopen(file_path,"r",encoding="utf-8")asf:f.read(1024)exceptUnicodeDecodeError:returnf"错误:'{path}' 可能是二进制文件,无法作为文本读取"try:withopen(file_path,"r",encoding="utf-8")asf:lines=f.readlines()total_lines=len(lines)ifoffset<1:offset=1ifoffset>total_lines:returnf"错误:offset={offset}超出文件总行数{total_lines}"end_line=min(offset+limit-1,total_lines)selected_lines=lines[offset-1:end_line]result_lines=[]fori,lineinenumerate(selected_lines,start=offset):result_lines.append(f"{i:4d}:{line.rstrip()}")result="\n".join(result_lines)ifend_line<total_lines:result+=f"\n\n[文件共{total_lines}行,已显示{offset}-{end_line}行]"returnresultexceptExceptionase:returnf"错误:读取文件失败 -{str(e)}"@tooldefwrite_file(path:str,content:str)->str:""" 写入文件内容(原子写入)。 参数: - path: 文件路径 - content: 文件内容 返回:成功/失败消息 """file_path=Path(path)try:file_path.parent.mkdir(parents=True,exist_ok=True)withtempfile.NamedTemporaryFile(mode="w",encoding="utf-8",dir=file_path.parent,delete=False,suffix=".tmp",)astmp:tmp.write(content)tmp_path=Path(tmp.name)tmp_path.rename(file_path)returnf"成功:已写入{path}({len(content)}字符)"exceptExceptionase:if"tmp_path"inlocals()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_path=Path(path)ifnotfile_path.exists():returnf"错误:文件不存在 '{path}'"try:content=file_path.read_text(encoding="utf-8")ifold_strnotincontent:returnf"错误:在文件中找不到要替换的内容:\n{old_str}"count=content.count(old_str)ifcount>1:returnf"错误:要替换的内容出现了{count}次,请提供更精确的 old_str"new_content=content.replace(old_str,new_str,1)withtempfile.NamedTemporaryFile(mode="w",encoding="utf-8",dir=file_path.parent,delete=False,suffix=".tmp",)astmp:tmp.write(new_content)tmp_path=Path(tmp.name)tmp_path.rename(file_path)returnf"成功:已编辑{path}"exceptExceptionase:if"tmp_path"inlocals()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.py(22 字符) 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')

四个测试都通过了:

  1. 创建文件:Agent调write_file创建hello.py
  2. 读取文件:Agent调read_file读取内容,看到带行号的输出
  3. 编辑文件:Agent调edit_file把Hello World改成Hello DeepSeek
  4. 验证修改:Agent再次调read_file确认修改成功

注意Agent的回答都是自然语言,它理解了工具返回的结果,然后用用户能理解的方式表达。


小结

  1. 文件三件套是Agent的"双手":read_file读代码、write_file写代码、edit_file改代码。
  2. read_file带行号:f"{i:4d}: {line.rstrip()}",方便Agent引用具体行。支持offset+limit读大文件的一部分。
  3. write_file原子写入:先写临时文件再rename,防止写到一半程序崩了毁文件。
  4. edit_file精准替换:old_str→new_str,不支持正则,要求唯一匹配。比行号更直观。
  5. 二进制文件检测:read_file尝试读取前1KB,UnicodeDecodeError就报错。
  6. 错误消息友好:文件不存在、不是文件、二进制文件、old_str出现多次——都给模型能理解的提示。
  7. DeepPilot v0.4文件工具完成——Agent从"顾问"变成"工程师",能真正帮你读改写代码。

下节预告

文件三件套搞定了,但Agent还不能执行命令。你让它"跑一下pytest",它只能说"你可以在终端运行…",不能真的执行。下一节做bash执行器run_bash:用subprocess.run封装命令执行,支持超时杀死、输出截断、工作目录约束。从此Agent能跑测试、装依赖、编译代码,真正成为你的编程助手。


如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 18:05:50

ICSE 2026论文趋势解读:AI驱动软件工程的全生命周期变革

ICSE 永远是软件工程圈子里绕不开的名字。作为CCF A类、软件工程领域公认的顶级会议&#xff0c;ICSE每年的录用论文基本就代表了未来两三年这个行业的研究风向。2026年的会议还没正式开场&#xff0c;但已经陆续放出了部分接收论文和预印本&#xff0c;我翻完这些公开材料&…

作者头像 李华
网站建设 2026/10/2 18:05:25

AI创业者通识日报 | 2026年9月20日

AI创业者通识日报 | 2026年9月20日 &#x1f4d6; 首屏导读 本教程配套付费专栏&#xff1a;《大模型工程师修炼手记》 19.9 元&#xff08;AI 编程 Agent 实战 本文同主题系统课程&#xff09; 《AI时代程序员的自我提升》 49.9 元&#xff08;AI 时代成长方法论&#xff0…

作者头像 李华
网站建设 2026/10/2 18:04:45

网盘直链解析还能更快?LinkSwift 的一次完整旅程

网盘直链解析还能更快&#xff1f;LinkSwift 的一次完整旅程 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云盘 …

作者头像 李华
网站建设 2026/10/2 18:02:19

Ubuntu 22.04源码升级OpenSSH 9.6p1与OpenSSL 3.2.0实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 18:02:16

从零吃透 C++ 异常:抛出捕获、栈展开、异常重抛与编码规范详解

目录 一.异常的概念及使用 1.1异常的概念 1.2异常的抛出和捕获 1.3栈展开 1.4查找匹配的处理代码 1.5异常重新抛出 1.6 异常安全问题 1.7异常规范 一.异常的概念及使用 1.1异常的概念 异常机制核心作用&#xff1a;分离「错误检测」和「错误处理」&#xff0c;异常把程…

作者头像 李华
网站建设 2026/10/2 18:00:19

Xshell 向 Linux 虚拟机传文件:可靠方式与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华