为语言模型配置与文件系统交互的能力,拓展了广泛的用途,从保存生成的代码和研究笔记,到读取配置文件或数据集,都涵盖在内。本节将详细说明如何构建工具,以使大型语言模型安全地执行文件操作。主要的难点和关注点是安全性:无限制的文件访问是主要的安全隐患。因此,我们将着重强调细致的沙盒隔离和路径处理。作为工具的核心文件系统操作为了让大型语言模型代理能够有效管理文件,它通常需要一些基本能力。这些能力可以转化为不同的工具:读取文件:这使代理能够将文件内容加载到其上下文,可能用于分析数据、理解现有代码或获取信息。写入文件:这使代理能够保存其输出、创建新文档或修改现有文档。这对于代理生成成果的任务来说是根本。列出目录内容:为了导航和理解其运行环境,代理需要查看给定目录中存在哪些文件和文件夹。创建目录:为了更好地组织文件,代理可能需要创建新文件夹来存储其生成的文件。删除文件/目录:这是一项功能强大且可能具有破坏性的操作。如果提供此功能,必须极其谨慎地处理,并且通常需要额外的确认步骤或限制。在为大型语言模型构建文件系统操作工具时,通常建议将每项功能实现为独立的、细致的工具(例如 read_file 工具、write_file 工具),而不是一个带有子命令的单一复杂工具。这样做可以简化对大型语言模型的工具描述,减少歧义,并允许为每项操作制定更有针对性的安全策略。设计工具的输入和输出每个文件操作工具都需要明确定义输入(参数)和输出(结果)。下面看一些示例:read_file 工具:输入参数:file_path (字符串):文件的路径,必须是相对于指定的沙盒目录。encoding (字符串,可选):文件的字符编码(例如 'utf-8')。默认为 'utf-8'。输出结构:一个 JSON 对象,包含文件的 content(字符串)数据或说明任何问题的 error(字符串)。write_file 工具:输入参数:file_path (字符串):文件应在沙盒中创建或覆盖的相对路径。content (字符串):要写入文件的实际数据或文本。mode (字符串,可选):指定写入模式,通常 'w' 表示覆盖,'a' 表示追加。默认为 'w'。输出结构:一个 JSON 对象,成功时包含 status(字符串)消息(例如“文件 'example.txt' 写入成功。”),失败时包含 error(字符串)。list_directory 工具:输入参数:directory_path (字符串,可选):沙盒内目录的相对路径。如果省略,则默认为沙盒的根目录。输出结构:一个 JSON 对象,包含 items(字符串列表,每个字符串都是文件或目录名称)或 error(字符串)。安全的必要性:沙盒隔离和路径验证构建文件系统工具最重要的方面是确保它们安全运行。大型语言模型,根据其特点,可能会生成或被提示文件路径,这些路径如果未经检查,可能导致对敏感系统文件的未经授权的访问或修改。沙盒隔离沙盒隔离是一种技术,其中大型语言模型代理的文件操作被严格限制在特定目录或一组目录(即“沙盒”)内。代理不应感知或访问此预定义区域之外的文件系统任何部分。大型语言模型指定的所有文件路径都必须被解释为相对于此沙盒的根目录。digraph G { rankdir=TB; bgcolor="transparent"; fontname="Arial"; node [shape=box, style="rounded,filled", fontname="Arial"]; edge [fontname="Arial"]; LLM [label="大型语言模型代理", fillcolor="#ffec99", color="#ffe066"]; subgraph cluster_tool { label="文件系统工具"; style="filled"; fillcolor="#e9ecef"; color="#ced4da"; node [fillcolor="#d0bfff", color="#b197fc"]; ToolInterface [label="工具接口\n(例如 read_file, write_file)"]; SecurityChecks [label="安全检查\n(路径验证,沙盒隔离)"]; ToolInterface -> SecurityChecks [label="验证路径"]; } subgraph cluster_filesystem { label="操作系统文件系统"; style="filled"; fillcolor="#f8f9fa"; color="#dee2e6"; SandboxDir [label="沙盒目录\n(例如 /var/agent_workspace/user_session_xyz)", shape=folder, fillcolor="#a5d8ff", color="#74c0fc", style="filled,rounded"]; SensitiveDir [label="敏感系统目录\n(例如 /etc, /usr/bin)", shape=folder, fillcolor="#ffc9c9", color="#ff8787", style="filled,rounded"]; // Using shapes for files to make it clearer they are entities within the folders file_in_sandbox [label="project_data.csv", shape=note, fillcolor="#d0bfff", parentcluster=cluster_filesystem]; file_in_sensitive [label="system_config.conf", shape=note, fillcolor="#ff8787", parentcluster=cluster_filesystem]; } LLM -> ToolInterface [label="请求:\nread_file('project_data.csv')"]; SecurityChecks -> SandboxDir [label="在沙盒内解析路径\n允许访问", color="#37b24d"]; LLM_malicious_attempt [label="大型语言模型代理(或受损输入)", fillcolor="#ffec99", color="#ffe066"]; LLM_malicious_attempt -> ToolInterface [label="请求:\nread_file('../../../etc/passwd')", color="#f03e3e"]; SecurityChecks -> SensitiveDir [label="检测到路径穿越\n阻止访问", color="#f03e3e", style=dashed]; }此图显示文件系统工具作为访问中介。来自大型语言模型的请求会经过验证;指定沙盒内的操作被允许,而尝试访问沙盒外敏感区域的请求则被阻止。路径验证和规范化即使在沙盒内,大型语言模型提供(或从外部输入派生)的路径也需要严格的验证:确定沙盒根目录:您的工具必须有一个明确定义的沙盒根目录绝对路径(例如 /srv/app/agent_files/user123/)。此路径不应由大型语言模型更改。组合与规范化:大型语言模型提供的相对路径(例如 reports/report.txt)与沙盒根目录合并。生成的路径随后应进行规范化,以处理如 .(当前目录)和 ..(父目录)等结构。Python 的 os.path.join() 和 os.path.normpath() 在此很有用。解析为绝对路径:将组合并规范化后的路径转换为规范的、文件系统绝对路径。Python 的 pathlib.Path.resolve() 在此表现出色,因为它还能处理符号链接(尽管您可能希望禁止对指向沙盒外部的符号链接进行操作)。验证包含性:这是最重要的检查。最终解析的绝对路径必须位于沙盒根目录内。如果 resolved_path.is_relative_to(sandbox_root_path)(Python 3.9+)为 false,或等效检查失败,则必须拒绝该操作。这可以防止路径遍历攻击(例如 ../../../../../etc/passwd)。在 Python 中实现安全文件工具以下是 FileSystemTools 类在 Python 中的实现概述,侧重于安全方面:import os import pathlib # 这应安全配置,可能按代理或按会话。 # 例如,来自环境变量或安全配置服务。 # 确保此目录存在并具有适当的权限。 SANDBOX_ROOT_DIR = pathlib.Path(os.getenv("AGENT_SANDBOX_ROOT", "./default_agent_sandbox")).resolve() SANDBOX_ROOT_DIR.mkdir(parents=True, exist_ok=True) # 如果不存在则创建 def _get_sandboxed_path(relative_path_str: str) -> pathlib.Path | None: """ 将相对路径字符串安全解析为沙盒内的绝对路径。 如果路径无效或在沙盒外部,则返回 None。 """ if not relative_path_str or not isinstance(relative_path_str, str): # print("安全:无效的路径输入类型或空路径。") return None # 不允许绝对路径或尝试在开头使用反斜杠/斜杠 # 这可能会绕过简单的连接。 if os.path.isabs(relative_path_str) or relative_path_str.startswith(('/', '\\')): # print(f"安全:绝对路径尝试被拒绝:{relative_path_str}") return None # 通过将沙盒根目录与相对路径连接来创建完整路径。 # 在解析和检查之前,这仍然可能不安全。 candidate_path = SANDBOX_ROOT_DIR / relative_path_str try: # 将路径解析为其规范的绝对形式。 # 这会处理 '..' 和符号链接。 # strict=False 允许解析不存在的文件的路径以进行“写入”操作。 # 对于“读取”或“列出”操作,如果路径必须存在,您可能希望使用 strict=True。 resolved_path = candidate_path.resolve(strict=False) except FileNotFoundError: # 如果 strict=True 且路径不存在(例如,对于读取操作) # print(f"安全/信息:路径不存在无法解析(严格模式):{candidate_path}") return None # 或专门作为“文件未找到”处理 except Exception as e: # 其他解析错误(例如,某些操作系统上的符号链接循环、无效字符) # print(f"安全:路径 '{candidate_path}' 解析错误:{e}") return None # 最重要的安全检查:解析后的路径是否仍在沙盒内? # 适用于 Python 3.9+ if hasattr(resolved_path, 'is_relative_to'): if resolved_path.is_relative_to(SANDBOX_ROOT_DIR): return resolved_path else: # 旧版 Python 的回退方案 # 确保 resolved_path 以 SANDBOX_ROOT_DIR 字符串开头, # 注意尾部斜杠以进行准确比较。 # 或者检查 SANDBOX_ROOT_DIR 是否是 resolved_path 的父目录之一。 if SANDBOX_ROOT_DIR == resolved_path or SANDBOX_ROOT_DIR in resolved_path.parents: return resolved_path # print(f"安全:路径遍历尝试。解析后的路径 '{resolved_path}' 在沙盒 '{SANDBOX_ROOT_DIR}' 外部。") return None class FileSystemTools: MAX_FILE_SIZE = 10 * 1024 * 1024 # 示例:10MB 限制 def read_file(self, file_path: str, encoding: str = 'utf-8') -> dict: """从代理的沙盒中读取文件。""" resolved_path = _get_sandboxed_path(file_path) if not resolved_path: return {"error": f"访问被拒绝或路径无效: {file_path}"} if not resolved_path.is_file(): return {"error": f"文件未找到或不是常规文件: {file_path}"} try: # 在读取之前检查文件大小以防止大型文件导致内存问题 if resolved_path.stat().st_size > self.MAX_FILE_SIZE: return {"error": f"文件超出允许的最大大小: {file_path}"} content = resolved_path.read_text(encoding=encoding) return {"content": content} except Exception as e: return {"error": f"无法读取文件 '{file_path}': {str(e)}"} def write_file(self, file_path: str, content: str, mode: str = 'w') -> dict: """将内容写入代理沙盒中的文件。""" resolved_path = _get_sandboxed_path(file_path) if not resolved_path: return {"error": f"访问被拒绝或写入路径无效: {file_path}"} if mode not in ['w', 'a']: return {"error": "无效的写入模式。请使用 'w'(覆盖)或 'a'(追加)。"} # 在写入之前检查内容大小 if len(content.encode(errors='ignore')) > self.MAX_FILE_SIZE: return {"error": "内容超出允许的最大文件大小。"} try: # 确保父目录存在 resolved_path.parent.mkdir(parents=True, exist_ok=True) with open(resolved_path, mode, encoding='utf-8') as f: f.write(content) return {"status": f"文件 '{file_path}' {'写入' if mode == 'w' else '追加'}成功。"} except Exception as e: return {"error": f"无法写入文件 '{file_path}': {str(e)}"} def list_directory(self, directory_path: str = ".") -> dict: """列出代理沙盒中目录的内容。""" # 使用 "." 作为默认值意味着列出沙盒的根目录 resolved_path = _get_sandboxed_path(directory_path) if not resolved_path: return {"error": f"访问被拒绝或目录路径无效: {directory_path}"} if not resolved_path.is_dir(): return {"error": f"路径不是目录或不存在: {directory_path}"} try: items = [item.name for item in resolved_path.iterdir()] return {"items": items} except Exception as e: return {"error": f"无法列出目录 '{directory_path}': {str(e)}"} 在 _get_sandboxed_path 函数中,candidate_path.resolve(strict=False) 通常是合适的,因为对于 write_file 操作,文件本身可能尚不存在。strict=False 参数允许解析路径直到最后一个不存在的组件。随后使用 is_relative_to(或旧版 Python 的父目录检查)进行的检查是最终的守门员。在示例中添加安全事件的打印语句(已注释掉)有助于开发和调试,但在生产环境中应替换为适当的日志记录。代理交互和错误处理大型语言模型代理将通过指定工具名称及其参数来调用这些工具。例如,读取文件: 大型语言模型请求:{"tool_name": "read_file", "parameters": {"file_path": "project_notes/alpha_phase.md"}}工具的响应应结构化且提供信息:成功:{"content": "alpha_phase.md 的内容..."}文件未找到:{"error": "文件未找到或不是常规文件:project_notes/alpha_phase.md"}安全违规/路径遍历:{"error": "访问被拒绝或路径无效:../../sensitive_config.ini"}清晰、明确的错误消息有助于大型语言模型(和开发人员)理解结果。安全相关的错误应明确说明访问被拒绝,而不是仅仅说“文件未找到”,因为这可以指导大型语言模型后续的操作或提示澄清。进一步的安全和操作改进资源限制:对文件大小(如示例中的 MAX_FILE_SIZE)、代理可创建的文件数量以及可能分配给代理沙盒的总磁盘空间实施限制。权限细化:除了简单的沙盒隔离,您可能需要基于角色的访问控制。例如,某些代理可能只有读取访问权限,而其他代理可以写入或删除。这通常需要与身份和授权系统集成。审计:对所有文件系统工具调用实施全面的日志记录。日志应包括代理标识符、时间戳、请求的操作、输入参数(特别是路径)、解析后的路径和结果。这对安全分析和调试非常有价值。文件名/扩展名拒绝列表:您可能希望阻止代理创建或与某些类型的文件(例如可执行文件)或具有特定名称的文件进行交互,即使在沙盒内也是如此。通过以安全为首要考虑来细致设计文件系统工具,您可以显著增强大型语言模型代理执行有用任务的能力,这些任务涉及数据持久化和与本地环境的交互。务必严格测试这些工具,特别是它们在各种极端情况下的安全强制机制。