前言
大家好,我是倔强青铜三 。欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!
随着 Vibe Coding(氛围编码)的兴起,软件开发领域对编码智能体(Coding Agents)的使用显著增加,尤其是基于终端或 IDE 的智能体(如 Claude Code 或 Cursor)。
伴随着这种日益增长的应用,一个挑战凸显出来:文件系统的访问权限。
具体来说:
- 处理写入或编辑文件的权限,避免这些操作导致代码库或其他重要文件的意外删除
- 为智能体提供对非结构化文档(PDF、演示文稿、Google/Word 文档)的深度理解能力,以便它们能够正确处理自动化和知识工作
在本文中,我们将尝试找到这两个问题的解决方案,我们将使用 LlamaParse、LlamaIndex Agent Workflows、Claude Agent SDK 和 AgentFS 来实现。
本文的所有代码可在以下地址获取:github.com/run-llama/agentfs-claude
文件系统虚拟化和其他魔法技巧
我们列出的第一个挑战与给编码智能体访问文件系统的权限有关,同时仍要保持高水平的控制。
解决这个问题的一种方法是频繁使用人机协作(human-in-the-loop):虽然这是一种高成功率的策略(大多数人可以识别危险操作并在发生之前阻止它们),但它破坏了编码智能体应该提供的自主性。不断地让人参与意味着智能体无法在后台运行,并且始终需要一定程度的关注。
第二种解决方法,反直觉的是,禁止智能体访问你的实际 文件系统,让它在虚拟化副本中工作。这个选项允许智能体执行各种操作,即使是最具破坏性的操作,也不会损坏你的文件,因为一切都是用副本进行的,而不是真实文档。
为了演示这第二种选项,我们将使用 AgentFS,这是一个由 Turso 设计的高性能、基于 SQLite 的虚拟文件系统,它也可以作为键值缓存和工具调用注册表使用。
使用 AgentFS TypeScript SDK,我们可以构建多个实用程序来从 AgentFS 数据库加载、检索和修改文件。以下是一个示例:
typescript
export async function readFile(
filePath: string,
agentfs: AgentFS,
): Promise<string | null> {
let content: string | null = null;
try {
content = (await agentfs.fs.readFile(filePath, "utf-8")) as string;
} catch (error) {
console.error(error);
}
return content;
}
一旦我们定义了所有文件系统操作的功能(读取、写入、编辑、检查存在性和列出目录中的文件),我们就使用它们在 Claude Code 的 SDK MCP 中创建自定义工具,即 filesystem MCP。以下是如何实现的代码片段:
typescript
// 定义 Zod schema 形状
const readSchemaShape = {
filePath: z.string().describe("Path of the file to read"),
};
// 将 schema 转换为 Zod 对象
const readSchema = z.object(readSchemaShape);
// 创建一个辅助函数来连接 AgentFS 数据库
export async function getAgentFS({
filePath = null,
}: {
filePath?: string | null;
}): Promise<AgentFS> {
if (!filePath) {
filePath = "fs.db";
}
const agentfs = await AgentFS.open({ id: "claude-agentfs", path: filePath });
return agentfs;
}
// 基于上面的读取函数定义读取工具
async function readTool(
input: z.infer<typeof readSchema>,
): Promise<CallToolResult> {
const agentfs = await getAgentFS({});
const content = await readFile(input.filePath, agentfs);
if (typeof content == "string") {
return { content: [{ type: "text", text: content }] };
} else {
return {
content: [
{
type: "text",
text: `Could not read ${input.filePath}. Please check that the file exists and submit the request again.`,
},
],
isError: true,
};
}
}
// 转换为 Claude Agent SDK Tool
const mcpReadTool = tool(
"read_file",
"Read a file by passing its path.",
readSchemaShape,
readTool,
);
// 在 MCP 中使用
export const fileSystemMCP = createSdkMcpServer({
name: "filesystem-mcp",
version: "1.0.0",
tools: [mcpReadTool, ...],
});
由于所有工具现在都加载到 MCP 中,Claude 不需要使用其内置的文件系统工具(Read、Write、Edit 和 Glob),我们可以在智能体选项中禁用它们。为了确保智能体不会因为幻觉或不对齐而绕过这个防护措施,我们还可以定义一个特定的 PreToolUse hook(在工具执行之前运行的一些自定义逻辑)来拒绝上述文件系统工具的每次调用。
typescript
// 定义函数
async function denyFileSystemToolsHook(
_input: PreToolUseHookInput,
_toolUseId: string | undefined,
_options: { signal: AbortSignal },
): Promise<HookJSONOutput> {
return {
async: true,
hookSpecificOutput: {
permissionDecision: "deny",
permissionDecisionReason:
"You cannot use standard file system tools, you should use the ones from the filesystem MCP server.",
hookEventName: "PreToolUse",
},
};
}
// 将其列为 hook
const hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>> = {
PreToolUse: [
{
matcher: "Read|Write|Edit|Glob",
hooks: [denyFileSystemToolsHook],
} as HookCallbackMatcher,
],
};
使用 hooks,我们还可以添加两个在 filesystem MCP 的 write 和 edit 工具之后 运行的 hook(PostToolUse),这样我们就可以询问用户是否要将更改持久化到真实文件系统中,而不仅仅是虚拟文件系统中。你可以在这个文件中找到所有 hooks 以及其他配置选项。
所有工具设置完成后,现在重要的是指导智能体如何使用它,我们可以通过自定义系统提示来实现:
typescript
export const systemPrompt = `
你是一位专家级程序员,任务是协助用户在当前工作目录中实现他们的需求。
为了执行文件系统操作,你**不得**使用内置工具(Read、Write、Glob、Edit),而**必须**使用 'filesystem' MCP 服务器,它提供以下工具:
- 'read_file':读取文件,需提供文件路径
- 'write_file':写入文件,需提供文件路径和内容
- 'edit_file':编辑文件,需提供旧字符串和要替换的新字符串
- 'list_files':列出所有可用文件
- 'file_exists':检查文件是否存在,需提供文件路径
使用这些工具,你应该能够为用户提供所需的帮助。
`;
现在 Claude Code 不仅能够使用 filesystem MCP 工具,而且它还会始终选择它们而不是内置工具:如果这没有发生,我们仍然有我们设置的 hook 来保护。
唯一的问题是,我们配置的这个文件系统只适用于基于文本的文件(如 .txt 或 .md),因此,如果智能体想要访问 PDF 文件或其他非文本格式,它将无法做到:这个问题把我们带到了下一步,它允许我们将非结构化文件转换为机器可读的文本。
让非结构化文档变得可访问
理解复杂文档对于许多用例至关重要,尤其是当我们试图实现的最终产品绑定到特定数据集时:许多编码智能体,包括 Claude Code,对 PDF 提供基本的智能支持,但随着文件复杂性的增加,它们的性能会下降。
在我们使用 AgentFS 的演示中,我们可以利用我们已经在使用虚拟文件系统这一事实,以纯文本形式加载非结构化文件。为此,我们可以使用 LlamaParse,这是一种最先进的 OCR 和智能解析解决方案,可以从 PDF、Word 和 Google 文档、Excel 表格以及更多文件格式中提取高质量文本内容。
在为编码智能体准备文件系统环境时,我们按原样加载基于文本的文件,并使用 LlamaParse 解析非结构化文件以加载其文本内容,使 Claude Code 能够访问高质量提取的文本,并更好地理解如果项目涉及文档时的需求。
这是我们用来解析文件的函数,利用 llama-cloud-services typescript 包:
typescript
const apiKey = process.env.LLAMA_CLOUD_API_KEY;
const reader = new LlamaParseReader({
resultType: "text",
apiKey: apiKey,
fastMode: true,
checkInterval: 4,
verbose: true,
});
export async function parseFile(filePath: string): Promise<string> {
let text = "";
try {
const documents = await reader.loadData(filePath);
for (const document of documents) {
text += document.text;
}
return text;
} catch (error) {
console.log(error);
return text;
}
}
使用 Workflow 作为框架
现在我们了解了如何使用 AgentFS 设置虚拟文件系统以及如何使用 LlamaParse 加载非结构化文件,我们只需要用一个框架将所有内容整合在一起,为 Claude Code 提供一个适合编码的环境。
我们使用 LlamaIndex Workflows 来实现这一点,通过 @llamaindex/workflows-core typescript 包,它为我们提供了两个主要优势:
- 分步执行:Claude Code 在一个独立的步骤中运行,该步骤仅在前两个步骤(在虚拟文件系统中加载文件和收集提示)成功完成后才触发
- 人机协作 :workflow 提供了简单的人机协作架构模式,它受益于维护可快照和可恢复状态的可能性。此功能允许我们在 workflow 执行期间从用户那里收集提示(以及其他选项,如计划模式激活和恢复之前的会话)
让我们看看如何实现这个 workflow:
typescript
async function main() {
const { withState } = createStatefulMiddleware(() => ({}));
const workflow = withState(createWorkflow());
const startEvent = workflowEvent<{ workingDirectory: string | undefined }>();
const filesRegisteredEvent = workflowEvent<void>();
const requestPromptEvent = workflowEvent<void>();
const promptEvent = workflowEvent<{
prompt: string;
resume: string | undefined;
plan: boolean;
}>();
const stopEvent = workflowEvent<{ success: boolean; error: string | null }>();
const notFromScratch = fs.existsSync("fs.db");
const agentFs = await getAgentFS({});
workflow.handle([startEvent], async (_context, event) => {
if (notFromScratch) {
return filesRegisteredEvent.with();
}
const wd = event.data.workingDirectory;
let dirPath: string | undefined = wd;
if (typeof wd === "undefined") {
dirPath = "./";
}
const success = await recordFiles(agentFs, { dirPath: dirPath });
if (!success) {
return stopEvent.with({
success: success,
error:
"Could not register the files within the AgentFS file system: check writing permissions in the current directory",
});
} else {
return filesRegisteredEvent.with();
}
});
// eslint-disable-next-line
workflow.handle([filesRegisteredEvent], async (_context, _event) => {
console.log(
bold(
"All the files have been uploaded to the AgentFS filesystem, what would you like to do now?",
),
);
return requestPromptEvent.with();
});
workflow.handle([promptEvent], async (_context, event) => {
const prompt = event.data.prompt;
const agent = new Agent(queryOptions, {
resume: event.data.resume,
plan: event.data.plan,
});
try {
await agent.run(prompt);
return stopEvent.with({ success: true, error: null });
} catch (error) {
return stopEvent.with({ success: false, error: JSON.stringify(error) });
}
});
const { sendEvent, snapshot, stream } = workflow.createContext();
sendEvent(startEvent.with({ workingDirectory: "./" }));
await stream.until(requestPromptEvent).toArray();
const snapshotData = await snapshot();
const humanResponse = await consoleInput("Your prompt: ");
console.log(
bold("Would you like to resume a previous session? Leave blank if not"),
);
const resumeSession = await consoleInput("Your answer: ");
let sessionId: string | undefined = undefined;
if (resumeSession.trim() != "") {
sessionId = resumeSession;
}
console.log(bold("Would you like to activate plan mode? [y/n]"));
const activatePlan = await consoleInput("Your answer: ");
let planMode = false;
if (["yes", "y", "yse"].includes(activatePlan.trim().toLowerCase())) {
planMode = true;
}
const resumedContext = workflow.resume(snapshotData);
resumedContext.sendEvent(
promptEvent.with({
prompt: humanResponse,
resume: sessionId,
plan: planMode,
}),
);
await resumedContext.stream.until(stopEvent).toArray();
}
完整的定义可以在这里 github.com/run-llama/a... 找到。
现在 workflow 已经准备就绪,剩下的就是启动第一个编码智能体会话!如果你一直在跟随仓库进行操作,你只需要运行:
bash
pnpm run start
对于以后的会话,如果你想清理智能体文件系统数据库,你也可以运行 pnpm run clean-start。
总结
在这篇博文中,我们探讨了:
- 当前编码智能体处理文件系统管理的方法所面临的挑战
- 使用虚拟化文件系统的优势(以及如何使用 AgentFS 设置一个)
- 编码智能体的文档理解问题,以及如何使用 LlamaParse 作为解决方案
- 如何将所有内容打包到 LlamaIndex Agent Workflow 中,以获得完美的受控环境
欢迎关注我的微信公众号:倔强青铜三,获取更多 AI 自动化和开发技巧分享!