315 行代碼構建編程助手,Go大佬揭開智能體的「神秘面紗」
知名 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 編輯文件的工具)的方法,感興趣的讀者可以閱讀博客原文。