315 行代碼構(gòu)建編程助手,Go大佬揭開智能體的「神秘面紗」
知名 Go 大佬 Thorsten Ball 最近用 315 行代碼構(gòu)建了一個編程智能體,并表示「它運行得非常好」且「沒有護(hù)城河」(指它并非難以復(fù)制)。
Thorsten Ball 在編程領(lǐng)域以其對系統(tǒng)編程和編程語言的深入研究而聞名,尤其擅長解釋器、編譯器和虛擬機等主題。他撰寫的《用 Go 語言自制編譯器》和《用 Go 語言自制解釋器》則被視為編譯原理領(lǐng)域的「入門平替」。
雖然這個編程智能體無法和 Claude、Gemini 等推出的編碼功能相媲美,卻為初學(xué)者提供了一個探索智能體的良好學(xué)習(xí)范例。這反映了他一貫的理念:通過實踐和開源項目揭開技術(shù)的「神秘面紗」。
Thorsten Ball 在博客中分享了他的具體操作步驟。(注:本文中的代碼截圖可能并不完整,詳細(xì)內(nèi)容請參閱原博客。)
博客地址:https://ampcode.com/how-to-build-an-agent
乍看之下,智能體編輯文件、運行命令、自行解決錯誤似乎很復(fù)雜,但實際上只需一個大語言模型、一個循環(huán)和足夠的 tokens。構(gòu)建一個小型的智能體并不需要太多工作,少于 400 行代碼即可實現(xiàn),且大部分是樣板代碼。
接下來將展示如何從零開始逐步構(gòu)建一個「game changer」,讀者可以嘗試親自動手編寫代碼。
準(zhǔn)備工作
首先準(zhǔn)備好我們的「文具」:
- Go
- ANTHROPIC_API_KEY
鉛筆出場!讓我們直接開始,用四個簡單的命令來設(shè)置一個新的 Go 項目:
現(xiàn)在,打開 main.go,作為第一步,將需要的東西的框架放入其中:
是的,這還沒有編譯。但是我們這里有一個 Agent,它可以訪問 anthropic.Client(默認(rèn)情況下,它會查找 ANTHROPIC_API_KEY),并且可以通過從終端上的 stdin 讀取來獲取用戶消息。
現(xiàn)在讓我們添加缺少的 Run() 方法:
這并不多,對吧?90 行代碼,而其中最重要的就是 Run() 中的這個循環(huán),它讓我們能夠與 Claude 對話,但這已經(jīng)是這個程序的核心了。
對于一個核心來說,這個過程相當(dāng)簡單:我們首先打印一個提示,詢問用戶輸入內(nèi)容,將其添加到對話中,發(fā)送給 Claude,然后將 Claude 的回復(fù)添加到對話中,打印出回復(fù),然后再循環(huán)進(jìn)行。
你日常使用的 AI 聊天應(yīng)用其實就是這樣的,只不過這是在終端中實現(xiàn)的。
運行它:
然后你可以和 Claude 對話了,就像這樣:
注意到我們在多個回合中保持了同一個對話嗎?它記住了我們在第一條消息中的名字。每次回合對話都在增長,我們每次都發(fā)送整個對話。服務(wù)器——準(zhǔn)確來說是 Anthropic 的服務(wù)器——是無狀態(tài)的。它只看到 conversation 片段中的內(nèi)容,維護(hù)這一點由我們來負(fù)責(zé)。
現(xiàn)在繼續(xù),因為輸出結(jié)果很糟糕,這還不是一個智能體。什么是智能體?可以這樣定義:一個具有訪問工具能力的大語言模型(LLM),這些工具使其能夠修改上下文窗口之外的內(nèi)容。
添加工具
一個具有工具訪問能力的大語言模型(LLM)是什么呢?
工具的定義是這樣的:你向模型發(fā)送一個 prompt,告知它在想要使用「工具」時應(yīng)以特定方式回復(fù)。然后,你接收消息后「使用工具」執(zhí)行該指令,并返回結(jié)果。其他一切都是在這一基礎(chǔ)上進(jìn)行的抽象。
想象一下,你正在與朋友交談,你告訴他們:「在接下來的交流中,如果你想讓我舉起手臂,就眨眼。」這種表達(dá)方式雖然有些奇怪,但概念非常容易理解。
我們已經(jīng)能夠在不改變?nèi)魏未a的情況下嘗試這種方法。
我們告訴 Claude,當(dāng)它想知道天氣時,就用 get_weather 來「眨眼」。接下來的步驟是舉起我們的手臂,并回復(fù)「工具的結(jié)果」。
第一次嘗試非常成功!
這些模型經(jīng)過訓(xùn)練和微調(diào),能夠使用「工具」,并且非常注重利用這些工具。到 2025 年,它們在一定程度上「知道」自己不具備所有信息,因此可以借助工具獲取更多信息。(雖然這不是完全準(zhǔn)確的描述,但目前這個解釋足夠了。)
總結(jié)關(guān)于工具使用的關(guān)鍵點有:
- 你告訴模型有哪些工具是可用的。
- 當(dāng)模型想要使用工具時,它會通知你,你執(zhí)行工具并將響應(yīng)發(fā)送回模型。
為簡化步驟(1),大型模型提供商已經(jīng)內(nèi)置了 API,用于發(fā)送工具定義。
現(xiàn)在,讓我們開始構(gòu)建我們的第一個工具:read_file。
read_file 工具
為了定義 read_file 工具,我們將使用 Anthropic SDK 建議的類型,但請記?。涸诘讓樱@一切最終都會變成發(fā)送給模型的字符串。這一切都是「如果你希望我使用 read_file,就眨眼」。
我們要添加的每個工具都需要以下內(nèi)容:
? 名稱
? 描述,告訴模型這個工具的功能、何時使用、何時不使用、返回什么等等。
? 輸入模式,描述為 JSON schema,說明該工具期望什么輸入以及輸入的形式。
? 一個實際執(zhí)行工具的函數(shù),使用模型發(fā)送給我們的輸入并返回結(jié)果。
那么讓我們把這些添加到我們的代碼中。
現(xiàn)在我們給出 Agent 工具定義:
并將它們發(fā)送到 runInference 中的模型:
用戶發(fā)送工具定義,Anthropic 在服務(wù)器上將這些定義包裝在這個系統(tǒng)提示中(并不多),然后將其添加到對話中,如果模型想要使用該工具,它就會以特定的方式回復(fù)。
好的,所以工具定義正在發(fā)送,但我們還沒有定義任何工具。讓我們來定義 read_file 工具。
這并不多,是不是?這只是一個函數(shù),ReadFile,以及模型將看到的兩個描述:一個是描述工具本身的 Description(Read the contents of a given relative file path. ...),另一個是該工具擁有的單一輸入?yún)?shù)的描述(The relative path of a ...)。
ReadFileInputSchema 和 GenerateSchema 之類的工作是做什么的?我們需要這些來為工具定義生成一個 JSON 模式(schema),然后發(fā)送給模型。為此,我們使用 jsonschema 包,需要進(jìn)行導(dǎo)入和下載:
然后運行以下命令:
go mod tidy
然后,在 main 函數(shù)中,我們需要確保我們使用定義:
是時候嘗試一下了!
哇哦,它想要使用這個工具!顯然,你的輸出可能會有些不同,但聽起來 Claude 確實知道它可以讀取文件,對吧?
問題是我們沒能聆聽!當(dāng) Claude 給出提示時,我們沒有去注意這一點,我們需要解決這個問題。
通過一個簡單、快捷且異常敏捷的動作,我們可以通過替換智能體的 Run 方法來實現(xiàn):
可以說,這段過程 90% 是固定格式,只有 10% 是關(guān)鍵部分:當(dāng)我們從 Claude 收到消息時,我們會檢查 Claude 是否要求我們執(zhí)行某個工具,通過查看內(nèi)容的類型是否為「tool_use」來判斷;如果是這樣,我們就交給 executeTool 處理,在本地注冊表中通過名稱查找該工具,解析(unmarshal)輸入,執(zhí)行它,并返回結(jié)果。如果出現(xiàn)錯誤,我們會翻轉(zhuǎn)一個布爾值。就是這樣。
(是的,的確有一個循環(huán)套在另一個循環(huán)里,但這不重要。)
我們執(zhí)行工具,將結(jié)果發(fā)回給 Claude,然后再次請求 Claude 的響應(yīng),就是這么簡單。
echo 'what animal is the most disagreeable because it always says neigh?' >> secret-file.txt
這會在我們的目錄中生成一個名為 secret-file.txt 的文件,里面包含一個神秘的謎題。
就在同一個目錄中,我們運行新的工具使用智能體,要求它查看該文件:
你只需要給它一個工具,它就會在認(rèn)為有助于解決任務(wù)時使用它。我們沒有說「當(dāng)用戶詢問文件時,閱讀文件」,也沒有說「如果某個東西看起來像是文件名,找出如何讀取它」。我們說的是「幫我解決這個文件里的問題」,Claude 就意識到它可以讀取文件來回答這個問題,然后就去做了。
當(dāng)然,我們可以加以具體引導(dǎo)并鼓勵使用某個工具,但它基本上可以自主完成這些任務(wù):
作者接下來還介紹了添加 list_files(列出文件的工具)和 edit_file(讓 Claude 編輯文件的工具)的方法,感興趣的讀者可以閱讀博客原文。