315 行代码构建编程助手,Go大佬揭开智能体的「神秘面纱」


选自ampcode.com

作者:Thorsten Ball

机器之心编译


知名 Go 大佬 Thorsten Ball 最近用 315 行代码构建了一个编程智能体,并表示「它运行得非常好」且「没有护城河」(指它并非难以复制)。



Thorsten Ball 在编程领域以其对系统编程和编程语言的深入研究而闻名,尤其擅长解释器、编译器和虚拟机等主题。他撰写的《用 Go 语言自制编译器》和《用 Go 语言自制解释器》则被视为编译原理领域的「入门平替」。



虽然这个编程智能体无法和 Claude、Gemini 等推出的编码功能相媲美,却为初学者提供了一个探索智能体的良好学习范例。这反映了他一贯的理念:通过实践和开源项目揭开技术的「神秘面纱」。


Thorsten Ball 在博客中分享了他的具体操作步骤。(注:本文中的代码截图可能并不完整,详细内容请参阅原博客。)


博客地址:https://ampcode.com/how-to-build-an-agent


乍看之下,智能体编辑文件、运行命令、自行解决错误似乎很复杂,但实际上只需一个大语言模型、一个循环和足够的 tokens。构建一个小型的智能体并不需要太多工作,少于 400 行代码即可实现,且大部分是样板代码。


接下来将展示如何从零开始逐步构建一个「game changer」,读者可以尝试亲自动手编写代码。


准备工作


首先准备好我们的「文具」:


  • Go

  • ANTHROPIC_API_KEY


铅笔出场!让我们直接开始,用四个简单的命令来设置一个新的 Go 项目:



现在,打开 main.go,作为第一步,将需要的东西的框架放入其中:



是的,这还没有编译。但是我们这里有一个 Agent,它可以访问 anthropic.Client(默认情况下,它会查找 ANTHROPIC_API_KEY),并且可以通过从终端上的 stdin 读取来获取用户消息。


现在让我们添加缺少的 Run() 方法:



这并不多,对吧?90 行代码,而其中最重要的就是 Run() 中的这个循环,它让我们能够与 Claude 对话,但这已经是这个程序的核心了。


对于一个核心来说,这个过程相当简单:我们首先打印一个提示,询问用户输入内容,将其添加到对话中,发送给 Claude,然后将 Claude 的回复添加到对话中,打印出回复,然后再循环进行。


你日常使用的 AI 聊天应用其实就是这样的,只不过这是在终端中实现的。


运行它:



然后你可以和 Claude 对话了,就像这样:



注意到我们在多个回合中保持了同一个对话吗?它记住了我们在第一条消息中的名字。每次回合对话都在增长,我们每次都发送整个对话。服务器——准确来说是 Anthropic 的服务器——是无状态的。它只看到 conversation 片段中的内容,维护这一点由我们来负责。


现在继续,因为输出结果很糟糕,这还不是一个智能体。什么是智能体?可以这样定义:一个具有访问工具能力的大语言模型(LLM),这些工具使其能够修改上下文窗口之外的内容。


添加工具


一个具有工具访问能力的大语言模型(LLM)是什么呢?


工具的定义是这样的:你向模型发送一个 prompt,告知它在想要使用「工具」时应以特定方式回复。然后,你接收消息后「使用工具」执行该指令,并返回结果。其他一切都是在这一基础上进行的抽象。


想象一下,你正在与朋友交谈,你告诉他们:「在接下来的交流中,如果你想让我举起手臂,就眨眼。」这种表达方式虽然有些奇怪,但概念非常容易理解。


我们已经能够在不改变任何代码的情况下尝试这种方法。



我们告诉 Claude,当它想知道天气时,就用 get_weather 来「眨眼」。接下来的步骤是举起我们的手臂,并回复「工具的结果」。



第一次尝试非常成功!


这些模型经过训练和微调,能够使用「工具」,并且非常注重利用这些工具。到 2025 年,它们在一定程度上「知道」自己不具备所有信息,因此可以借助工具获取更多信息。(虽然这不是完全准确的描述,但目前这个解释足够了。)


总结关于工具使用的关键点有:


  • 你告诉模型有哪些工具是可用的。

  • 当模型想要使用工具时,它会通知你,你执行工具并将响应发送回模型。


为简化步骤(1),大型模型提供商已经内置了 API,用于发送工具定义。


现在,让我们开始构建我们的第一个工具:read_file。


read_file 工具


为了定义 read_file 工具,我们将使用 Anthropic SDK 建议的类型,但请记住:在底层,这一切最终都会变成发送给模型的字符串。这一切都是「如果你希望我使用 read_file,就眨眼」。


我们要添加的每个工具都需要以下内容:


• 名称

• 描述,告诉模型这个工具的功能、何时使用、何时不使用、返回什么等等。

• 输入模式,描述为 JSON schema,说明该工具期望什么输入以及输入的形式。

• 一个实际执行工具的函数,使用模型发送给我们的输入并返回结果。


那么让我们把这些添加到我们的代码中。



现在我们给出 Agent 工具定义:



并将它们发送到 runInference 中的模型:



用户发送工具定义,Anthropic 在服务器上将这些定义包装在这个系统提示中(并不多),然后将其添加到对话中,如果模型想要使用该工具,它就会以特定的方式回复。


好的,所以工具定义正在发送,但我们还没有定义任何工具。让我们来定义 read_file 工具。



这并不多,是不是?这只是一个函数,ReadFile,以及模型将看到的两个描述:一个是描述工具本身的 Description(Read the contents of a given relative file path. …),另一个是该工具拥有的单一输入参数的描述(The relative path of a …)。


ReadFileInputSchema 和 GenerateSchema 之类的工作是做什么的?我们需要这些来为工具定义生成一个 JSON 模式(schema),然后发送给模型。为此,我们使用 jsonschema 包,需要进行导入和下载:



然后运行以下命令:


go mod tidy


然后,在 main 函数中,我们需要确保我们使用定义:



是时候尝试一下了!



哇哦,它想要使用这个工具!显然,你的输出可能会有些不同,但听起来 Claude 确实知道它可以读取文件,对吧?


问题是我们没能聆听!当 Claude 给出提示时,我们没有去注意这一点,我们需要解决这个问题。


通过一个简单、快捷且异常敏捷的动作,我们可以通过替换智能体的 Run 方法来实现:



可以说,这段过程 90% 是固定格式,只有 10% 是关键部分:当我们从 Claude 收到消息时,我们会检查 Claude 是否要求我们执行某个工具,通过查看内容的类型是否为「tool_use」来判断;如果是这样,我们就交给 executeTool 处理,在本地注册表中通过名称查找该工具,解析(unmarshal)输入,执行它,并返回结果。如果出现错误,我们会翻转一个布尔值。就是这样。


(是的,的确有一个循环套在另一个循环里,但这不重要。)


我们执行工具,将结果发回给 Claude,然后再次请求 Claude 的响应,就是这么简单。


echo 'what animal is the most disagreeable because it always says neigh?' >> secret-file.txt


这会在我们的目录中生成一个名为 secret-file.txt 的文件,里面包含一个神秘的谜题。


就在同一个目录中,我们运行新的工具使用智能体,要求它查看该文件:



你只需要给它一个工具,它就会在认为有助于解决任务时使用它。我们没有说「当用户询问文件时,阅读文件」,也没有说「如果某个东西看起来像是文件名,找出如何读取它」。我们说的是「帮我解决这个文件里的问题」,Claude 就意识到它可以读取文件来回答这个问题,然后就去做了。


当然,我们可以加以具体引导并鼓励使用某个工具,但它基本上可以自主完成这些任务:



作者接下来还介绍了添加 list_files(列出文件的工具)和 edit_file(让 Claude 编辑文件的工具)的方法,感兴趣的读者可以阅读博客原文。


©

(文:机器之心)

发表评论

×

下载每时AI手机APP

 

和大家一起交流AI最新资讯!

立即前往