在上一期视频中,我们了解了编码代理(coding agent)的工作原理。当时有一个名为 calculateTotal 的小函数,以及一段关于为何我的测试失败的说明。在本期视频中,我将展示如何仅用不到 330 行代码创建您自己的编码代理。好的,这是我为编码代理编写的函数。让我们运行它。我将让它解释这个实际项目是关于什么的。

如前所述,它会列出所有文件。它显示正在将文件读取为代码,包括 package.json、total.js 以及 total.test.js。这是一个小型演示项目,用于 Andrew Ford 的视频《编码代理如何工作》(How Coding Agents Work)。这是一个微小的 Node.js 代码库,看起来故意留下一个 bug,这是正确的,对吧?它解释了文件是什么、里面有什么内容,并给出了一些解释以及 bug 本身。这就是我们今天要在演示中构建的内容。

如果您观看了上一期视频,您会看到与此类似的图表。它显示我们的编码代理是一个循环。我们向代理发出提示,对话和工具调用随后发送到 Claude 的 API,然后 Claude 要么响应运行一个工具,要么打印一些文本。首先,您需要安装 Node.js。我使用的是 Node 版本 26,但您也可以使用 LTS 版本。我们需要为项目创建一个目录并进入该目录。

我们需要使用 npm 初始化我们的项目,并将包类型设置为 module(模块)。好的,现在我要安装依赖项。我们需要 Anthropic AI SDK。我们还将安装 TypeScript 和 TSX,以及一些 Node.js 的类型。就是这样。好的,包已经安装了。我将创建一个包含我们 Anthropic 密钥的 .env 文件。我不会展示那个,只需创建 .env 文件并创建一个包含您的密钥的 ANTHROPIC_API_KEY。您必须去 Anthropic 网站在那里设置 API 密钥。我会在下面的描述中添加链接。

好的,我们将从 main.ts 文件开始。在其中,我们需要导入 Anthropic SDK。之后,我将使用 Node.js 的 readline 接口(createInterface)。我之所以这样使用是因为它将允许我们轻松地从命令行获取行,并能够捕获特殊字符等。而且我们不必担心处理所有这些情况,这些将由 readline 接口为我们处理。

除此之外,我们需要一些类型定义。所以我将从 message(消息)类型开始,它将从该库中获取 Anthropic 的消息类型以及一些参数。我们需要一个模型来工作。我将使用我最喜欢的 Opus 5.5(注:此处原文为Opus 5.5,实际可能指代特定模型版本或口误,忠实翻译)。在我们的命令行工具中,最好为不同的提示添加一些颜色。这样我们就知道哪一个是我们的消息,哪一个是 Claude 的。所以我在那里创建一个包含“你”和“Claude”的颜色对象,你可以看到那里有一些颜色实现。

我们需要一个类。所以我称我们的代理为 agent(代理),如下所示。我还需要在那里添加构造函数。所以让我们添加一个构造函数。在该构造函数内部,我们将当然使用来自 Anthropic 的客户端(client)。好的,我们需要获取用户消息。getUserMessage(获取用户消息),我们可以为此设置一个类型为 string 或 null 的 promise。好的,像这样,酷。

现在我们有构造函数了。最重要的是我们需要一个异步函数来运行。里面将有一个循环(loop)。所以那里有一个 async 函数。在内部,我们需要 conversation array(对话数组),它将携带我们的消息参数在里面。并向最终用户输出控制台信息,告诉他们可以随时使用 Ctrl C 停止退出。接着是我们的 while 循环(当...时循环)。我们需要从获取用户输入开始,所以我使用那个 getUserMessage 函数。

我就在那里打字。让我们修复它。如果没有用户输入,我们需要跳出 while 循环。并且需要获取那个用户输入,将其放入我们的 conversation array(对话数组)中。我们需要在消息上运行推理过程(inference),也就是发送给 Claude。我们还没有那个用于运行推理的函数,但稍后我们会做那部分。然后我们需要将助手消息推入从 Claude 返回的数组中。

如果有任何理由停止,来自 Claude 的回复,因为它可以在那里返回一个 stop reason refusal(停止原因拒绝)。我们需要将其输出到控制台以便我们可以看到它已经发生。然后我们实际上需要输出返回的内容。所以我们可以创建一个 for 循环来做这件事,它将遍历消息内容(message content),获取 text(文本)类型并将其输出到控制台日志中。

现在我们还没有那个私有 inference 函数,run inference(运行推理)。所以让我们现在添加它。这就是将调用客户端的部分。我相信这是正确的。我可能实际上需要在那里外面,那样更好。像这样,酷。所以我们有 while 循环(当...时循环),我们获取用户的输入,将其推送到 conversation array(对话数组)中,我们要运行 run inference。

下一部分是我们需要能够实际调用整个过程。所以我将创建一个 main(主)函数。在其中,我们必须在那里从 Anthropic 创建客户端。像这样。我们需要创建那个接口(interface)。所以正如我所说的,它将处理命令行、标准输入和标准输出。好的。我们还需要设置它是否关闭。所以让我们给自己一个变量来做到这一点。

我们需要获取用户的消息,这里有一个用于该功能的函数:Get user message(获取用户消息)。然后我们需要创建代理。像这样。之后,我们需要围绕它进行 try catch(尝试捕获)处理。所以我们等待代理运行,并捕获任何错误,或者最后关闭 readline 接口(read line)。我们需要调用我们的 main 函数。

请记住,模型在每次调用之间不记得任何东西(没有记忆)。所以每次我们调用它时,我们都会再次发送整个对话。而这个数组就是代理的完整记忆(memory)。好的。现在我们需要给我们的代理一些工具(tools)。这些工具有四个组成部分:它们需要一个名称,一个描述,这是模型用来决定何时使用它的描述。它需要输入必须发送的 JSON schema(JSON 模式),最后它需要一些代码,这将返回一个字符串或者抛出一个错误。

所以我们给代理一些工具,并用它们的名称、描述、输入以及它们实际要运行的内容来提供这些工具。所以我们要

我们要创建四个工具。第一个是读取文件,我们将给它一个路径,然后它会返回文件的文本内容。第二个是给它的列出文件工具,我们也会给它提供一个路径参数。第三个是递归列表功能,这将返回一组包含某些文件夹信息的 JSON 数据。第四个将是编辑文件工具,它会接收实际文件的旧字符串和新字符串作为参数。这意味着我们将完全替换文本内容,或者创建一个新文件。最后我们还有一个 Bash 命令工具。这个工具有一个安全门控机制,它会询问用户是否允许使用;只有当用户的回答是“yes”时我们才会执行该 Bash 命令。

现在让我们设置项目以包含这些工具。我将创建一个名为 tools.ts 的文件,其中将存放所有工具的定义。首先我们需要引入 Anthropic SDK。然后根据我们在图表中看到的,需要定义我们的工具结构:包括接口、工具名称、描述以及输入模式(input schema)。我们从 Anthropic SDK 中获取这些定义,并准备好运行逻辑。回到主程序 main.ts,我们需要在构造函数中添加私有工具定义的属性。当然还需要导入它——虽然我应该先做这一步的。现在我们有了来自 tools.ts 的类型和工具定义,在推理运行部分也准备好了消息传递。

目前我们还没有定义具体的工具实现,但这正是必须提供给代理(Agent)的关键部分。让我从之前的示例中复制一些代码过来,好像还缺了颜色显示的部分?再加一点。好了,这样看起来更好了。这段代码将运行推理过程,并返回我们刚刚映射过的工具列表(在 tools.ts 文件中)。在执行工具的逻辑中,我们可以看到代理目前还没有被赋予具体的工具定义。所以现在我们要解决这个问题。当然现在我们的代理会报错,因为之前修改了代码结构。回到 main 函数中,我们实例化了代理并传入了参数,一切看起来都很正常。我认为这一步已经完成了所有必要的设置。

目前还没有任何代码调用执行工具,而且 Claude 也没有可用的工具让我们要求它运行——因为定义列表是空的。我们只是在代码里放了个占位符。现在回到推理部分,你会发现一个关键点:在我们的工具定义中包含了名称、描述和输入模式,但我们从未真正将具体的执行逻辑传递给 Claude。相反,我们只给了它输入模式的定义,并没有传递实际要运行的代码。这些逻辑永远不会离开我们的机器。

接下来我们要添加第一个工具,这是最简单的那个,叫做“读取文件”。我们需要导出这个函数并使用相应的工具定义。让我们逐步分析:我们有了读取文件的名称(read_file),描述是“根据相对于工作目录的路径返回文件内容。仅用于文件,不应用于目录”。我们可以看到这里调用了 Node.js 的读取功能,需要导入相关模块。通过调用 Node.read file,我们可以读入文件内容,并返回 UTF-8 编码的字符串。这就是我们的第一个工具。当然我们需要在 main.ts 中使其可用,让我们导入这个读取文件工具并检查是否已正确引入。

接下来我们需要对循环函数做一些修改,以便处理用户输入和工具结果。如果在 while 循环之前查看运行逻辑,我会添加一个变量 read_user_input。只有当这个值为 true 时,我们才询问用户输入。我们将这段逻辑包裹在对话推送(conversation push)周围并整理一下格式。接下来需要在拒绝检查部分处理 read_user_input,将其设为 true 以继续循环。在循环结束后我们需要收集工具结果——之前我们漏掉了这一步,这些结果是来自 Anthropic 的响应。

目前我们只是从 Claude 获取文本块。接下来我们要处理不同的区块类型,即工具使用(tool use)部分。在这个部分内部,我们需要推送来自工具执行的结果。我们将调用 this.execute_tool 方法——虽然这个方法我们还没有实际构建出来,所以需要先添加它。这意味着我之前的输入更新可能有问题,比如缺少颜色代码等细节。我们需要为工具调用添加颜色标识,就像我们在开始时看到的那样,这样可以更清楚地识别何时执行了读取文件等操作。

这里的关键点在于:如果 Claude 请求使用某个工具,我们运行它并将结果发送回去。然后立即再次调用 Claude,而不需要用户进行任何额外操作。只有当 Claude 没有其他任务需要我们执行时,我们才将控制权交还给键盘输入。现在让我们实现“列出文件”工具。首先我们需要添加读取目录的功能,并从 Node.js 模块中获取实际函数。我们定义了名称和描述:“递归列出路径下的每个文件和目录,以斜杠结尾表示目录。如果未提供路径则列出工作目录”。

随着数据传入,我们获取路径名并整理目录名称。代码会检查字符串是否为空,如果不为空则添加当前目录标识符(如“.”)。然后读取该目录本身并遍历其中的条目,最后将其作为排序后的字符串返回。让我们回到 main.ts 文件,因为现在我们需要导入这个新工具。在 tools 数组中查找读取文件工具的位置,并将列出文件的工具添加进去,否则系统不会知道它的存在。这个架构的好处是:对于此类新增功能,我们不需要修改循环逻辑——添加工具只是一个额外的函数定义过程。这使得整个系统具有高度的模块化特性。

通过从数组中获取信息,我们要看的下一个工具是编辑文件。这里有一个流程图展示了发生的过程:输入来自 Claude,我们提供路径。如果路径为空或现有字符串与新字符串相同,则会输出错误,即无效输入参数。如果不是这种情况,它会检查文件是否存在;如果不存在且旧字符串为空,则创建新文件;否则返回错误。或者,如果文件存在,则将旧字符串替换为新字符串,并检查内容是否发生变化:如果是“是”,表示文件中未找到旧字符串;否则写入文件。关键在于这些错误很重要,它们会被标记为 error equal true,模型会读取该错误并尝试重试。这就是返回这种错误类型的优势所在。

现在让我们编辑我们的工具,我们需要添加 Make Directory 和 writeFile。因为我们要尝试编辑文件,所以需要添加用于编辑文件的函数工具。我已经预先创建了其中一个。它实际上包含两个部分:首先是 Edit File Tool,通过用新字符串替换旧文本来编辑文本文件。旧字符串和新字符串必须不同;要创建新文件,请传递尚不存在的 path 和空旧字符串。这里有一个 schema,告诉 Claude 在属性方面需要什么。然后我们可以看到我们在检查文件是否存在之后有了创建文件的逻辑。

我还会添加创建新文件的功能,如下所示,非常相似但很简单:如果是目录则处理目录,否则在根目录下创建文件并使用 writeFile。这相对直接。当然,我们需要告诉主程序现在有了这个 Edit File Tool 可用,并再次更新工具数组以将其传递给 Claude。回到该工具的关键部分:当出现错误时,我们实际上将该错误返回给 Claude,因为提供了这个错误,我们可以让 Claude 修复它。这可能是 Claude 为我们提供的最聪明的部分:如果有错误……

现在来看最后一个工具,即 Bash 工具。这可能是风险最大的工具,因为我们将运行 shell 命令,如果运行 shell 命令,理论上可以做任何事情。让我们这样做,但会加入一些安全措施。首先添加 bash 超时:如果脚本运行时间过长,我们可以让它停止。另一个是限制字符数量不超过一定限度。这些是一些限制条件。让我们放置实际函数本身,首先需要几样东西:需要一个 make bash tool 函数和一个 run tool 函数。让我们添加它们,make bash tool 的功能是在工作目录中运行 Bash 命令并返回其输出和退出代码。

用于运行脚本、测试和构建,用户必须批准每个命令。命令在 30 秒时停止,因此不要启动服务器或任何等待输入的东西。它还需要一个运行命令的功能,让我们也添加这个。这有一些其他依赖项:第一步是获取确认。我们需要获得用户同意运行该命令的确认,一旦运行它,我们必须构建出来。我们将需要 spawn 因为我们要在那里生成一个命令,这是 Node child process(子进程)。它以独立的方式运行该命令,使用标准 IO。

我们还有超时设置:如果超过我们在上方常量中设置的超时时间,它将终止该进程。我们需要能够这样做,还需要一个 truncate 函数来截断过长的输出(文本长度小于或等于最大输出字符数),并添加一条小消息,以免使终端过载。这是一个高风险操作,因为用户委托在各自的终端中运行 Bash 命令。时间限制和输出限制可以防止代理陷入困境或自我淹没,但这并不能阻止 Bash 命令本身。唯一能阻止 Bash 命令的是用户,确保他们阅读该命令并通过按 Y 键决定是否运行。

我们仍然只将此添加到了工具文件中,现在有了这个新函数,我们需要更新主程序。让我们回到 main 那里,我们必须导入工具本身:make bash tool。并确保它执行确认操作,添加 get user message 来获取用户消息。我们需要在这里添加我们的确认逻辑:每个 Bash 命令在运行前都需要你的 Y 批准。我们让它提出这个问题,进行颜色编码并提示使用否(No),默认值为“不”,否则只接受 Y。

在工具数组中,我们现在需要包含这个 make bash tool 以及确认步骤。这样我们就为用户设置了一个权限门控,确保他们确认想要运行工具请求。让我们运行我们的代理。我们可以看到它正在运行 list files 工具,并返回了文件夹和文件:Notes.txt、package.json、total.js 和 total.test.js。这是我们在“代理如何工作”中进行的现有测试。现在我想展示危险权限,因为 Bash 当然可以做任何事情,让我们让它为我们做点什么:“你能删除 Notes.txt 文件吗?”

我们可以看到它给出了一个 Bash 命令,它将 rm(移除)Notes.txt 文件,然后列出文件。这潜在地很危险,对吧?如果我们删除所有文件或在那里使用通配符,可能存在风险。但我们将这种风险转嫁给用户,为此他们必须确认。现在我们可以看到它已删除了该文件,甚至为我们检查并说文件夹中现在有 package.json 文件和现有的其他文件。

这是关于编码代理工作原理的演示。现在我们的代理已经准备就绪,让我们实际让它为我们修复测试。这里有一个问题:我的测试失败了。它询问是否需要读取文件,但它已经遍历了那个循环,读取了文件,并且已经表示它将进行编辑。我们可以看到它包含了 total.js 文件的路径、它正在寻找的旧字符串以及它将用来替换的新字符串。之后,它想要运行 bash 脚本来执行 npm test。所以让我们输入 yes。

好的,所以我修复了 total.js 文件,现在两个测试都通过了。问题在于 calculateTotal 函数将每个商品的价格相加,但忽略了其数量。测试通过时的返回值是 3 加 6 等于 9,而不是预期的 3 乘以 2 加上 6 乘以 1 等于 12。修复后的逻辑是,每个商品的价格现在乘以其数量,这样结果就符合预期了;如果某个项目没有 quantity 字段,line 将显示为 null。

太好了。这就是如何使用 Anthropic SDK 构建一个非常简单的编码代理,并赋予它读取、编辑、列出以及当然运行 bash 脚本等工具的能力。我希望这对你有帮助。现在你理解了编码代理的工作原理,你也可以编写自己的代码了。我不建议你在日常生活中使用它,因为显然需要实施更多的检查措施来确保安全性。因此,我建议你继续使用你熟悉的工具,如 Claude Code 或 Codex CLI,并试着思考下次工作时这些工具正在做什么。

感谢观看,请点赞和订阅频道,这真的很有帮助。我们下期再见。