
构建一个功能完备、能够编写代码的 Agent 其实并非难事。
当你看到一个 Agent 正在编辑文件、执行命令、自动纠错、尝试不同策略时——似乎总感觉这背后隐藏着什么秘密。
其实并没有。它不过是一个大语言模型、一个循环,再加上足够多的 token 而已。至于剩下的部分不过是些苦力活。
然而,构建一个小的却极强大的 Agent 甚至都不需要这些苦力活。用不到 400 行代码就能搞定,其中大部分还只是基础框架代码。
接下来,让我们将一同编写代码,从零开始,直至达到让你惊叹“哇哦,这简直…颠覆…”的程度。
强烈建议跟着动手实践。你或许认为光看文章就够了,不必亲自敲代码,但这其实总共还不到 400 行。希望你能切身感受这代码量是多么的微乎其微,希望你能在自己的终端、自己的文件夹里亲眼见证这一切。
开始前我们需要准备:
1. Go 语言环境;
2. Anthropic API 密钥,需要将其设置为名为 ANTHROPIC_API_KEY 的环境变量。
还有个网友二创的 Python+Gemini 的方案:
https://github.com/voxmenthe/coding-agent/blob/main/tutorial.md

准备开始!
让我们直接开始,用四个简单的命令来搭建一个新的 Go 项目:
mkdir code-editing-agentcd code-editing-agentgo mod init agenttouch main.go
现在,打开 main.go 文件,第一步,先把我们需要的基本结构放进去:
// main.gopackage mainimport ("bufio""context""fmt""os""github.com/anthropics/anthropic-sdk-go")func main() {// 创建 Anthropic 客户端client := anthropic.NewClient()// 设置从标准输入读取用户消息的扫描器scanner := bufio.NewScanner(os.Stdin)getUserMessage := func() (string, bool) {if !scanner.Scan() {return "", false // 如果读取失败或结束,返回 false}return scanner.Text(), true // 返回读取到的文本和 true}// 创建一个新的 Agent 实例agent := NewAgent(&client, getUserMessage)// 运行 Agenterr := agent.Run(context.TODO()) // 使用 context.TODO() 作为临时上下文if err != nil {fmt.Printf("错误: %s\n", err.Error()) // 打印错误信息}}// NewAgent 是 Agent 的构造函数func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent {return &Agent{client: client, // Anthropic API 客户端getUserMessage: getUserMessage, // 获取用户输入的函数}}// Agent 结构体定义type Agent struct {client *anthropic.Client // 用于与 Anthropic API 交互的客户端getUserMessage func() (string, bool) // 用于从用户获取输入的函数}
这段代码目前还无法编译。但我们已经有了一个 Agent,它能够访问 Anthropic Client(默认情况下会查找 ANTHROPIC_API_KEY 环境变量),并且可以通过读取终端的 stdin 来获取用户消息。
现在,我们来添加缺失的 Run() 方法:// main.go// Run 方法是 Agent 的主执行循环func (a *Agent) Run(ctx context.Context) error {// 初始化对话历史conversation := []anthropic.MessageParam{}fmt.Println("开始与 Claude 聊天 (按 'ctrl-c' 退出)")// 无限循环,处理对话for {fmt.Printf("\u001b[94m你\u001b[0m: ") // 打印蓝色提示符 "你: "// 获取用户输入userInput, ok := a.getUserMessage()if !ok {break // 如果无法获取输入(例如用户按了 ctrl-c),则退出循环}// 将用户输入包装成 Anthropic API 需要的消息格式userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput))// 将用户消息添加到对话历史中conversation = append(conversation, userMessage)// 调用 runInference 方法将对话发送给 Claude 并获取回复message, err := a.runInference(ctx, conversation)if err != nil {return err // 如果 API 调用出错,返回错误}// 将 Claude 的回复添加到对话历史中conversation = append(conversation, message.ToParam())// 处理 Claude 回复中的内容块for _, content := range message.Content {switch content.Type {case "text": // 如果是文本内容fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text) // 打印黄色提示符 "Claude: " 和回复内容}}}return nil // 正常退出时返回 nil}// runInference 方法负责调用 Anthropic APIfunc (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) {// 调用 Anthropic 客户端的 Messages.New 方法message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{Model: anthropic.ModelClaude3_7SonnetLatest, // 指定使用的模型MaxTokens: int64(1024), // 设置最大生成 token 数量Messages: conversation, // 传入当前的对话历史})return message, err // 返回 API 的回复消息和可能的错误}
代码量不大,对吧?总共九十行,其中最核心的就是 Run() 方法里的那个循环,它让我们能够与 Claude 持续对话。但这已经是这个程序运转的核心了。
就其核心功能而言,这个流程相当直接明了:我们首先打印一个提示,等待用户输入内容,将其加入对话记录,发送给 Claude,再将 Claude 的回应加入对话记录,打印出回应,然后周而复始。
这其实就是你用过的所有 AI 聊天应用的基本模式,只不过这次它运行在你的终端里。
我们来运行一下:
export ANTHROPIC_API_KEY="我最后再说一次,务必设置这个环境变量"# 下载所需的依赖包go mod tidy# 运行程序go run main.go
然后你就可以像下面这样和 Claude 聊天了:
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: Hey! I'm Thorsten! How are you?Claude: Hi Thorsten! I'm doing well, thanks for asking. It's nice to meet you. How are you doing today? Is there something I can help you with?You: Can you come up with any horse-related nicknames that make fun of my first name?Claude: I can try to come up with some playful horse-related nicknames based on "Thorsten":* Thorough-bred Thorsten* Trotsten* Thorsten the Stallion* Neighsten* Horseten* Thorsten the Galloper* Saddle-sten* Thorsten of the Mane* Canter-sten* ThorsteedThese are meant to be lighthearted - I hope they give you a chuckle!You:
注意,我们在多轮对话中一直保持着上下文。它从第一条消息就记住了我的名字。conversation 这个切片在每一轮对话后都会增长,我们每次都会把整个对话历史发送出去。The Server——也就是 Anthropic 的服务器——本身是无状态的。它只知道当前 conversation 切片里的内容。维护对话状态是我们客户端的责任。
好了,我们继续往下看,目前这还算不上一个 Agent。那什么才算是 Agent 呢?
这是我的定义:一个能够接入并使用工具的 LLM,这赋予了它修改其自身上下文窗口之外信息的能力。

第一个工具
一个能够接入并使用工具的 LLM?什么是工具?
基本思路是这样的:你向模型发送一个 Prompt,明确告知如果它想使用“某个工具”,就应该以某种特定的方式进行回复。然后,你作为接收这条回复的一方,通过执行相应的操作来“使用该工具”,并将执行结果回复给模型。就这么简单。我们接下来将看到的所有内容,都只是建立在这个基础之上的抽象。
想象一下,你正和朋友聊天,你对他说:“在接下来的谈话里,如果你想让我抬起胳膊,你就眨一下眼睛。”这话听起来有点怪,但这个概念很容易理解。
我们现在就可以在不修改任何代码的情况下尝试一下。
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: You are a weather expert. When I ask you about the weather in a given location, I want you to reply with `get_weather(<location_name>)`. I will then tell you what the weather in that location is. Understood?Claude: I understand your instructions. When you ask me about the weather in a specific location, I will reply with `get_weather(<location_name>)`. Then you'll provide me with the weather information for that location, and I can discuss it further with you. I'm ready whenever you'd like to ask about the weather somewhere.You: Hey, what's the weather in Munich?Claude: get_weather(Munich)
效果非常好,第一次尝试就成功了,不是吗?
这些模型都经过了专门的训练和微调,能够熟练使用“工具”,并且它们非常乐意这样做。到了 2025 年的今天,它们在某种程度上已经“意识”到自己并非全知全能,并且可以利用工具来获取更多信息(当然,这并非其内部运作的精确描述,但作为当前的解释已经足够形象了)。
总结来说,关于工具和工具使用的核心要素只有两点:
1. 你告知模型有哪些可用的工具。
2. 当模型表示想要执行某个工具时,它会通知你,你来执行这个工具,并将执行结果返回给模型。
为了让第一步(告知模型工具信息)更简便,主流的模型提供商都内置了相应的 API,允许你将工具的定义信息随请求一起发送。

Read_file 工具
为了定义 read_file 这个工具,我们将遵循 Anthropic SDK 推荐的类型结构。但请记住:归根结底,这一切最终都会转化为字符串发送给模型。本质上还是那套“如果你想让我用 read_file,就眨眨眼”的逻辑。
我们接下来要添加的每一个工具都需要包含以下要素:
1. 一个名称(Name);
2. 一段描述(Description),用于告知模型该工具的功能、适用场景、不适用场景、返回值等信息;
3. 一个输入模式(Input Schema),采用 JSON Schema 格式,精确描述该工具期望接收的输入参数及其形式;
4. 一个函数(Function),该函数负责接收模型传来的输入,实际执行工具对应的操作,并返回执行结果。
那么,让我们将这些要素添加到代码中:
// main.go// ToolDefinition 结构体用于定义一个工具type ToolDefinition struct {Name string `json:"name"` // 工具的名称Description string `json:"description"` // 工具的描述InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"` // 工具的输入参数模式 (使用 Anthropic SDK 类型)Function func(input json.RawMessage) (string, error) // 执行工具逻辑的函数,输入为原始 JSON,输出为字符串结果和错误}
现在,我们让 Agent 结构体能持有这些工具定义:
// main.go// Agent 结构体定义,增加了 tools 字段type Agent struct {client *anthropic.Client // Anthropic API 客户端getUserMessage func() (string, bool) // 获取用户输入的函数tools []ToolDefinition // Agent 可用的工具列表}// NewAgent 构造函数,增加了 tools 参数func NewAgent(client *anthropic.Client,getUserMessage func() (string, bool),tools []ToolDefinition, // 接收工具定义列表) *Agent {return &Agent{client: client,getUserMessage: getUserMessage,tools: tools, // 将传入的工具列表存储到 Agent 实例中}}func main() {// [...之前的代码...]// 初始化一个空的工具列表 (稍后会添加具体工具)tools := []ToolDefinition{}// 创建 Agent 实例时传入工具列表agent := NewAgent(&client, getUserMessage, tools)// [...之前的代码...]}
并且在 runInference 方法中,将这些工具定义发送给模型:
// main.gofunc (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) {// 将我们定义的 ToolDefinition 转换为 Anthropic API 需要的格式anthropicTools := []anthropic.ToolUnionParam{}for _, tool := range a.tools {anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropic.ToolParam{Name: tool.Name, // 工具名称Description: anthropic.String(tool.Description), // 工具描述 (使用 anthropic.String 包装)InputSchema: tool.InputSchema, // 工具输入模式},})}// 调用 Anthropic API,这次在参数中加入了 Tools 字段message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{Model: anthropic.ModelClaude3_7SonnetLatest,MaxTokens: int64(1024),Messages: conversation,Tools: anthropicTools, // 将转换后的工具列表传递给 API})return message, err}
我们将工具定义发送过去,Anthropic 的服务器端会用这个 system prompt 将这些定义包装起来,然后加到我们的 conversation 对话历史中。之后,如果模型决定要使用某个工具,它就会以一种特定的格式进行回复。
好了,工具定义现在能够被发送了,但我们还没有真正定义任何工具。让我们现在就来定义 read_file 工具:
// main.go// 定义 read_file 工具var ReadFileDefinition = ToolDefinition{Name: "read_file", // 工具名称Description: "读取指定相对文件路径的内容。当你需要查看文件内部有什么时使用此工具。请勿对目录名称使用此工具。", // 工具描述InputSchema: ReadFileInputSchema, // 输入模式Function: ReadFile, // 执行函数}// 定义 read_file 工具的输入结构体type ReadFileInput struct {Path string `json:"path" jsonschema_description:"工作目录下文件的相对路径。"` // 文件路径参数,并附带 JSON Schema 描述}// 使用泛型函数 GenerateSchema 生成 ReadFileInput 的 JSON Schemavar ReadFileInputSchema = GenerateSchema[ReadFileInput]()// ReadFile 函数实现了读取文件的具体逻辑func ReadFile(input json.RawMessage) (string, error) {// 解析 JSON 输入到 ReadFileInput 结构体readFileInput := ReadFileInput{}err := json.Unmarshal(input, &readFileInput)if err != nil {panic(err) // 简单处理错误,实际应用中应更健壮}// 使用 os.ReadFile 读取文件内容content, err := os.ReadFile(readFileInput.Path)if err != nil {return "", err // 如果读取出错,返回错误}// 返回文件内容字符串return string(content), nil}// GenerateSchema 是一个泛型函数,用于从 Go 结构体生成 Anthropic 兼容的输入模式func GenerateSchema[T any]() anthropic.ToolInputSchemaParam {// 使用 jsonschema 库来反射结构体并生成 JSON Schemareflector := jsonschema.Reflector{AllowAdditionalProperties: false, // 不允许额外的属性DoNotReference: true, // 不使用 $ref 引用}var v T // 创建一个类型 T 的零值实例schema := reflector.Reflect(v) // 生成 JSON Schema// 将生成的 Schema 转换为 Anthropic SDK 需要的格式return anthropic.ToolInputSchemaParam{Properties: schema.Properties, // 只取其中的 Properties 部分}}
代码不多,不是吗?只有一个核心函数 ReadFile,以及两个模型会看到的描述信息:一个是描述工具本身的 Description ,另一个是描述该工具唯一输入参数(文件路径)的 jsonschema_description。
而 ReadFileInputSchema 和 GenerateSchema 这部分代码?我们需要它们来为工具定义生成一个 JSON Schema,这个 Schema 会被发送给模型。为了实现这个功能,我们引入了 jsonschema 这个包,所以需要先导入并下载它:
// main.gopackage mainimport ("bufio""context"// 添加这个导入:+ "encoding/json""fmt""os"// 需要添加对 path/filepath 的导入,用于处理路径 (译者注:原文未加,但 ListFiles 会用到)+ "path/filepath"// 需要添加对 strings 的导入,用于 EditFile (译者注:原文未加,但 EditFile 会用到)+ "strings"// 需要添加对 path 的导入,用于 EditFile 中的 createNewFile (译者注:原文未加)+ "path""github.com/anthropics/anthropic-sdk-go"// 添加这个导入:+ "github.com/invopop/jsonschema")
然后运行:go mod tidy
接着,在 main 函数中,确保我们把刚才定义的 ReadFileDefinition 加入到工具列表中:
func main() {// [...之前的代码...]// 将 ReadFileDefinition 添加到 tools 列表中tools := []ToolDefinition{ReadFileDefinition}// [...之前的代码...]}
是时候检验成果了!$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: what's in main.go?Claude: I'll help you check what's in the main.go file. Let me read it for you.You:
等等,发生了什么?它表示要使用工具了!当然,你的具体输出可能会略有不同,但这听起来确实像是 Claude 知道自己可以读取文件了,对吧?
问题在于,当 Claude “眨眼”示意(即返回工具使用请求)时,我们的程序并没有做出响应。我们需要修复这一点。
将 Agent 的 Run 方法替换为以下版本:
// main.gofunc (a *Agent) Run(ctx context.Context) error {conversation := []anthropic.MessageParam{}fmt.Println("开始与 Claude 聊天 (按 'ctrl-c' 退出)")readUserInput := true // 标志位,指示下一轮是否需要读取用户输入for {// 仅当需要时才读取用户输入if readUserInput {fmt.Printf("\u001b[94m你\u001b[0m: ")userInput, ok := a.getUserMessage()if !ok {break}userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput))conversation = append(conversation, userMessage)}// 调用 API 获取 Claude 的回复message, err := a.runInference(ctx, conversation)if err != nil {return err}// 将 Claude 的回复(可能是文本或工具使用请求)添加到对话历史conversation = append(conversation, message.ToParam())// 用于存储本轮需要执行的工具结果toolResults := []anthropic.ContentBlockParamUnion{}// 遍历 Claude 回复的内容块for _, content := range message.Content {switch content.Type {case "text": // 如果是普通文本回复fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text)case "tool_use": // 如果是工具使用请求// 调用 executeTool 执行工具result := a.executeTool(content.ID, content.Name, content.Input)// 将工具执行结果添加到 toolResults 列表中toolResults = append(toolResults, result)}}// 检查是否有工具被执行if len(toolResults) == 0 {// 如果没有工具执行,说明 Claude 只是纯文本回复,下一轮需要读取用户输入readUserInput = truecontinue // 继续下一轮循环}// 如果有工具被执行readUserInput = false // 下一轮不需要读取用户输入,因为要先处理工具结果// 将所有工具的执行结果包装成一条用户消息,添加到对话历史中// 这相当于告诉 Claude:“这是你刚才请求的工具的执行结果”conversation = append(conversation, anthropic.NewUserMessage(toolResults...))}return nil}// executeTool 方法负责查找并执行指定的工具func (a *Agent) executeTool(id, name string, input json.RawMessage) anthropic.ContentBlockParamUnion {var toolDef ToolDefinitionvar found bool// 在 Agent 的工具列表中查找名称匹配的工具定义for _, tool := range a.tools {if tool.Name == name {toolDef = toolfound = truebreak}}// 如果找不到对应的工具if !found {// 返回一个表示错误的工具结果块return anthropic.NewToolResultBlock(id, "未找到指定工具", true) // isError 设置为 true}// 打印工具执行信息(绿色提示符)fmt.Printf("\u001b[92m工具\u001b[0m: %s(%s)\n", name, input)// 调用工具定义中的 Function 来执行实际操作response, err := toolDef.Function(input)if err != nil {// 如果工具执行出错,返回包含错误信息的工具结果块return anthropic.NewToolResultBlock(id, err.Error(), true) // isError 设置为 true}// 如果工具执行成功,返回包含结果的工具结果块return anthropic.NewToolResultBlock(id, response, false) // isError 设置为 false}
眯起眼睛仔细看,你会发现这大部分都是模板化的代码:当我们从 Claude 那里得到 message 回复时,我们检查 Claude 是否要求我们执行工具 (content.Type == “tool_use”),如果是,就转交给 executeTool 方法处理。该方法查找对应的工具定义,反序列化输入参数,执行工具函数,然后返回结果。如果执行过程中发生错误,我们会在结果中标记出来。最后,我们将工具的执行结果(无论成功还是失败)发送回给 Claude,然后继续对话循环。
千真万确,核心逻辑就这些。让我演示给你看。
# 在当前目录下创建一个名为 secret-file.txt 的文件,并写入一个谜语echo 'what animal is the most disagreeable because it always says neigh?' >> secret-file.txt
这条命令会在你的工作目录下创建一个 secret-file.txt 文件,里面包含一个神秘的谜语。
就在这个目录下,运行我们更新后的、具备工具使用能力的 Agent,让它去看看这个文件里的内容:
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: Claude, buddy, help me solve the riddle in the secret-file.txt fileClaude: I'll help you solve the riddle in the secret-file.txt file. Let me first read the contents of this file to see what the riddle is.tool: read_file({"path":"secret-file.txt"})Claude: Great! I've found the riddle.The answer to the riddle is: **A horse**The riddle is playing on wordplay where "neigh" (the sound a horse makes) sounds like "nay" (a way to say "no" or disagree with something). So horses are described as disagreeable because they always say "neigh/nay" - implying they're always disagreeing.You:
让我们深吸一口气,一起惊叹吧。
你只是给了它一个工具,它就…在认为这个工具有助于完成任务的时候主动去使用它。记住:我们自始至终都没有明确指示,比如“如果用户问起某个文件,你就去读取它”。我们也没有说“如果看到像文件名一样的字符串,就想办法读取它”。不,完全没有。我们只是说“帮我解开这个文件里的谜题”,然后 Claude 就自己意识到可以通过读取文件来回答这个问题,并且就这样去做了。
当然,我们给出更明确的指令,它基本上能够自主完成任务。
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: What's going on in main.go? Be brief!Claude: I'll check the main.go file to see what's going on.tool: read_file({"path":"main.go"})Claude: Based on my review, main.go implements a Claude AI assistant agent that can use tools. Here's a brief summary:- It creates a command-line chat interface with Claude using the Anthropic Go SDK- Implements a tool system with a single "read_file" tool for reading file contents- Manages the conversation flow with Claude, handling both regular text responses and tool use requests- When Claude requests to use a tool, the agent executes it and returns the results- The program loops continuously to maintain a conversation until the user exitsThe code essentially builds a simple CLI-based chatbot that can interact with the file system when requested by Claude.You:
分析得非常到位。既然我们已经掌握了如何让 Claude 使用工具,那就让我们再添加几个吧。

list_files 工具
如果你和我一样,每次登录到一台新电脑,第一件事通常是运行 ls 命令——列出文件——来快速了解当前环境。
让我们也赋予 Claude 这种能力。下面是一个 list_files 工具的完整实现:
// main.go// 定义 list_files 工具var ListFilesDefinition = ToolDefinition{Name: "list_files", // 工具名称Description: "列出指定路径下的文件和目录。如果未提供路径,则列出当前目录的内容。", // 工具描述InputSchema: ListFilesInputSchema, // 输入模式Function: ListFiles, // 执行函数}// 定义 list_files 工具的输入结构体type ListFilesInput struct {// omitempty 表示如果该字段为空,则在 JSON 序列化时忽略它// jsonschema_description 提供给模型的参数描述Path string `json:"path,omitempty" jsonschema_description:"可选的相对路径,用于指定列出文件的起始位置。如果未提供,则默认为当前目录。"`}// 生成 ListFilesInput 的 JSON Schemavar ListFilesInputSchema = GenerateSchema[ListFilesInput]()// ListFiles 函数实现了列出文件和目录的逻辑func ListFiles(input json.RawMessage) (string, error) {// 解析 JSON 输入listFilesInput := ListFilesInput{}err := json.Unmarshal(input, &listFilesInput)if err != nil {panic(err) // 简化错误处理}// 确定要列出的目录,默认为当前目录 "."dir := "."if listFilesInput.Path != "" {dir = listFilesInput.Path}// 用于存储文件和目录路径的切片var files []string// 使用 filepath.Walk 遍历目录err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {if err != nil {return err // 处理遍历过程中的错误}// 获取相对于起始目录的路径relPath, err := filepath.Rel(dir, path)if err != nil {return err}// 忽略起始目录本身 (".")if relPath != "." {if info.IsDir() {// 如果是目录,在末尾添加斜杠 "/"files = append(files, relPath+"/")} else {// 如果是文件,直接添加相对路径files = append(files, relPath)}}return nil})if err != nil {return "", err // 返回遍历过程中遇到的错误}// 将文件列表序列化为 JSON 字符串result, err := json.Marshal(files)if err != nil {return "", err // 返回 JSON 序列化错误}// 返回 JSON 字符串形式的文件列表return string(result), nil}
这里没什么花哨的操作:list_files 函数返回指定文件夹(默认为当前文件夹)下的文件和目录列表。如果这是一个正式的项目,我们可能(而且应该)进行成百上千项优化,但只是想揭示魔法帽里的秘密,这样实现就足够了。
需要注意的一点是:我们返回的是一个字符串列表,并且用末尾的斜杠来标识目录。这里没有固定的格式要求;只要 Claude 能够解析,任何格式都可以。
当然,我们还需要把 list_files 这个新工具告知 Claude:
func main() {// [...之前的代码...]// 将 ListFilesDefinition 添加到工具列表中- tools := []ToolDefinition{ReadFileDefinition}+ tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition}// [...之前的代码...]}
就这样。让我们问问 Claude 这个目录里都有些什么。
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: what do you see in this directory?Claude: I'll help you see what's in the current directory. Let me list the files and directories for you.tool: list_files({})Claude: I can see several files and directories in the current directory:1. Git-related files and directories:- `.git/` directory with various Git configuration and tracking files- `.gitignore` file2. Go project files:- `main.go` - the main Go source code file- `go.mod` - Go module definition file- `go.sum` - Go module checksum file3. Documentation:- `blogpost.md` - a markdown file, likely containing blog content4. Environment configuration:- `.envrc` - likely an environment configuration fileWould you like me to examine the contents of any specific file from this list?You:
搞定!它成功读取了目录内容。
但更厉害的是:Claude 知道如何组合使用这些工具。我们只需要提出一个能够引导它进行组合操作的问题即可:
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: Tell me about all the Go files in here. Be brief!Claude: I'll examine the Go files in the current directory for you. Let me check what files are available first.tool: list_files({})Claude: I found a Go file in the current directory. Let me examine it:tool: read_file({"path":"main.go"})tool: read_file({"path":"go.mod"})Claude: Here's a brief overview of the Go files in this project:**main.go**:- Implements a Claude AI agent that can interact with the local filesystem- Uses the Anthropic SDK for Go to communicate with Claude AI models- Provides two tools: `read_file` and `list_files` that allow Claude to read file contents and list directory contents- Manages a conversation flow between the user and Claude, handling tool execution when Claude needs to access files**go.mod**:- Defines the module dependencies- Main dependencies are the Anthropic SDK for Go and a JSON schema library- Uses Go 1.24.1This appears to be a simple terminal-based application that allows users to chat with Claude while giving the AI access to read local files and list directories.You:
它先调用了 list_files,然后对找到的 Go 相关文件(main.go 和 go.mod)分别调用了 read_file。就像一个真人开发者会做的那样。
我们这个项目用的是哪个 Go 版本呢?看看 Claude 是如何找到答案的:
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: What go version are we using in this project?Claude: I'll help you find the Go version being used in this project. Let me check the files to locate where the Go version might be specified.tool: list_files({})Claude: I can see there's a Go project here with several important files like `go.mod`, `go.sum`, and `main.go`. Let me check the `go.mod` file first, as it typically contains the Go version information.tool: read_file({"path":"go.mod"})Claude: According to the `go.mod` file, this project is using Go version 1.24.1.
Claude 先查看了目录内容,然后读取了 go.mod 文件,轻松找到了答案。

edit_file 工具我们要添加的最后一个工具是 edit_file,一个能让 Claude 编辑文件的工具。首先,为我们的新 edit_file 工具添加定义:// main.go// 定义 edit_file 工具var EditFileDefinition = ToolDefinition{Name: "edit_file", // 工具名称Description: `编辑文本文件。在指定的文件中,将所有出现的 'old_str' 替换为 'new_str'。'old_str' 和 'new_str' 的值必须不同。如果通过 path 指定的文件不存在,将会创建该文件(此时 old_str 必须为空字符串)。`, // 工具描述,详细说明了功能、约束和创建文件的条件InputSchema: EditFileInputSchema, // 输入模式Function: EditFile, // 执行函数}// 定义 edit_file 工具的输入结构体type EditFileInput struct {Path string `json:"path" jsonschema_description:"要编辑或创建的文件的路径"`OldStr string `json:"old_str" jsonschema_description:"要搜索并替换的文本 - 必须精确匹配。如果文件不存在,则此项必须为空字符串。"`NewStr string `json:"new_str" jsonschema_description:"用于替换 old_str 的新文本。"`}// 生成 EditFileInput 的 JSON Schemavar EditFileInputSchema = GenerateSchema[EditFileInput]()
没错——我们通过字符串替换来实现文件编辑。Claude 3.7 模型特别擅长处理字符串替换任务,所以我们就告诉 Claude,它可以通过替换文本的方式来编辑文件,以此来实现 edit_file 工具。下面是 EditFile 函数的实现代码:// EditFile 函数实现了编辑或创建文件的逻辑func EditFile(input json.RawMessage) (string, error) {// 解析 JSON 输入editFileInput := EditFileInput{}err := json.Unmarshal(input, &editFileInput)if err != nil {// 如果 JSON 解析失败,返回错误return "", fmt.Errorf("无法解析输入参数: %w", err)}// 输入参数校验if editFileInput.Path == "" || editFileInput.OldStr == editFileInput.NewStr {// 路径不能为空,且新旧字符串必须不同(允许 old_str 为空以创建文件)return "", fmt.Errorf("无效的输入参数:路径不能为空,且 old_str 和 new_str 必须不同")}// 尝试读取文件内容content, err := os.ReadFile(editFileInput.Path)if err != nil {// 如果读取文件时出错if os.IsNotExist(err) && editFileInput.OldStr == "" {// 如果错误是“文件不存在”且 old_str 为空,则尝试创建新文件return createNewFile(editFileInput.Path, editFileInput.NewStr)}// 其他读取错误,直接返回return "", fmt.Errorf("读取文件 '%s' 失败: %w", editFileInput.Path, err)}// 文件存在,执行替换操作oldContent := string(content)// 使用 strings.Replace 进行全局替换(-1 表示替换所有匹配项)newContent := strings.Replace(oldContent, editFileInput.OldStr, editFileInput.NewStr, -1)// 检查是否有实际替换发生if oldContent == newContent && editFileInput.OldStr != "" {// 如果内容未改变且 old_str 不为空,说明没有找到要替换的字符串return "", fmt.Errorf("在文件 '%s' 中未找到要替换的字符串 '%s'", editFileInput.Path, editFileInput.OldStr)}// 将修改后的内容写回文件 (使用 0644 权限)err = os.WriteFile(editFileInput.Path, []byte(newContent), 0644)if err != nil {return "", fmt.Errorf("写入文件 '%s' 失败: %w", editFileInput.Path, err)}// 操作成功,返回 "OK"return "OK", nil}
这个函数会检查传入的参数,读取文件(如果文件不存在且符合创建条件则创建新文件),用 NewStr 替换所有出现的 OldStr,将修改后的内容写回文件,最后返回 "OK" 表示成功。还缺少一个辅助函数 createNewFile:// createNewFile 函数负责创建新文件及其可能需要的父目录func createNewFile(filePath, content string) (string, error) {// 获取文件所在目录的路径dir := path.Dir(filePath)// 如果目录不是当前目录 "."if dir != "." {// 尝试创建所有必需的父目录 (使用 0755 权限)err := os.MkdirAll(dir, 0755)if err != nil {// 如果创建目录失败,返回错误return "", fmt.Errorf("创建目录 '%s' 失败: %w", dir, err)}}// 将内容写入新创建的文件 (使用 0644 权限)err := os.WriteFile(filePath, []byte(content), 0644)if err != nil {// 如果写入文件失败,返回错误return "", fmt.Errorf("创建文件 '%s' 失败: %w", filePath, err)}// 文件创建成功,返回成功消息return fmt.Sprintf("成功创建文件 %s", filePath), nil}
最后一步:将新定义的工具添加到 main 函数的工具列表中:func main() {// [...之前的代码...]// 将 EditFileDefinition 添加到工具列表中- tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition}+ tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, EditFileDefinition}// [...之前的代码...]}
好了… 一切就绪。你准备好让这个具备编辑能力的 Agent 大展身手了吗?让我们指示 Claude 创建一个用 JavaScript 编写的 FizzBuzz 函数。$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: hey claude, create fizzbuzz.js that I can run with Nodejs and that has fizzbuzz in it and executes itClaude: I'll create a fizzbuzz.js file that you can run with Node.js. First, let me check if the file already exists:tool: list_files({})Claude: I don't see a fizzbuzz.js file, so I'll create one with a classic FizzBuzz implementation. This program will print numbers from 1 to 100, but for multiples of 3 it will print "Fizz", for multiples of 5 it will print "Buzz", and for numbers that are multiples of both 3 and 5 it will print "FizzBuzz".tool: edit_file({"path":"fizzbuzz.js","old_str":"","new_str":"/**\n * FizzBuzz implementation in JavaScript\n * Prints numbers from 1 to 100, but:\n * - For multiples of 3, print \"Fizz\" instead of the number\n * - For multiples of 5, print \"Buzz\" instead of the number\n * - For multiples of both 3 and 5, print \"FizzBuzz\"\n */\n\nfunction fizzBuzz(n) {\n for (let i = 1; i <= n; i++) {\n let output = '';\n \n if (i % 3 === 0) output += 'Fizz';\n if (i % 5 === 0) output += 'Buzz';\n \n console.log(output || i);\n }\n}\n\n// Execute FizzBuzz for numbers 1 to 100\nconsole.log('Running FizzBuzz:');\nfizzBuzz(100);"}Claude: I've created a fizzbuzz.js file that you can run with Node.js. The program:1. Defines a function called `fizzBuzz` that takes a parameter `n` for the upper limit2. Loops from 1 to n (set to 100 in the execution)3. For each number:- If it's divisible by 3, it adds "Fizz" to the output- If it's divisible by 5, it adds "Buzz" to the output- If it's divisible by both, it adds both resulting in "FizzBuzz"- If it's not divisible by either 3 or 5, it outputs the number itself4. The script automatically executes the function for numbers 1 to 100You can run this script using Node.js with the following command in your terminal:node fizzbuzz.jsThis will output the FizzBuzz sequence from 1 to 100 to your console.
对吧?!很厉害吧?这大概就是你能想到的 edit_file 最基本的实现 ,或者说,最基本的 Agent 实现。tool: $ node fizzbuzz.jsRunning FizzBuzz:12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz16[...]
不过,让我们来让它实际编辑一个已存在的文件,而不只是创建新文件。这样问它:请修改 fizzbuzz.js,让它只打印到 15。$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: Please edit fizzbuzz.js so that it only prints until 15Claude: I'll edit the fizzbuzz.js file to print only until 15 instead of its current limit. First, let me check the current content of the file to understand what changes are needed.tool: read_file({"path":"fizzbuzz.js"})Claude: I can see that the file currently runs FizzBuzz up to 100. I'll modify it to only run up to 15 instead by changing the parameter in the fizzBuzz function call.tool: edit_file({"path":"fizzbuzz.js","old_str":"// Execute FizzBuzz for numbers 1 to 100\nconsole.log('Running FizzBuzz:');\nfizzBuzz(100);","new_str":"// Execute FizzBuzz for numbers 1 to 15\nconsole.log('Running FizzBuzz:');\nfizzBuzz(15);"})Claude: I also need to update the comment at the top of the file to reflect this change:tool: edit_file({"path":"fizzbuzz.js","old_str":"/**\n * FizzBuzz implementation in JavaScript\n * Prints numbers from 1 to 100, but:","new_str":"/**\n * FizzBuzz implementation in JavaScript\n * Prints numbers from 1 to 15, but:"})Claude: The changes have been successfully made to the fizzbuzz.js file. The program will now:1. Only print FizzBuzz from 1 to 15 (instead of 1 to 100)2. The documentation has been updated to reflect this changeThe function itself remains the same, as it takes a parameter to determine the upper limit, and we've simply changed that parameter from 100 to 15.
修改后的代码依然能正常工作:
node fizzbuzz.jsRunning FizzBuzz:12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz
临别前再来最后一个例子:创建一个名为 congrats.js 的脚本,用 rot13 算法解码下面的字符串‘Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!’,然后打印出解码结果。
$ go run main.goChat with Claude (use 'ctrl-c' to quit)You: Create a congrats.js script that rot13-decodes the following string 'Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!' and prints itClaude: I'll create a congrats.js script that decodes the ROT13-encoded string and prints it. ROT13 is a simple letter substitution cipher that replaces a letter with the 13th letter after it in the alphabet.Let me create this file for you:tool: edit_file({"path":"congrats.js","old_str":"","new_str":"/**\n * A script to decode a ROT13-encoded string and print the result\n */\n\nfunction rot13Decode(encodedStr) {\n return encodedStr.replace(/[a-zA-Z]/g, function(char) {\n // Get the character code\n const charCode = char.charCodeAt(0);\n \n // Handle uppercase letters\n if (charCode >= 65 && charCode <= 90) {\n return String.fromCharCode(((charCode - 65 + 13) % 26) + 65);\n }\n \n // Handle lowercase letters\n if (charCode >= 97 && charCode <= 122) {\n return String.fromCharCode(((charCode - 97 + 13) % 26) + 97);\n }\n \n // Return non-alphabetic characters as is\n return char;\n });\n}\n\nconst encodedMessage = 'Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!';\nconst decodedMessage = rot13Decode(encodedMessage);\n\nconsole.log(decodedMessage);"})Claude: I've created a JavaScript file called congrats.js that:1. Defines a `rot13Decode` function that takes an encoded string and applies the ROT13 cipher to decode it2. Takes the provided string 'Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!'3. Decodes it using the function4. Prints the decoded message to the consoleYou can run this script using Node.js by typing `node congrats.js` in your terminal. The script will decode and display the hidden congratulatory message!
有用吗?我们来试试:
node congrats.jsCongratulations on building a code-editing agent!
是的!可行!
如果你和我在过去几个月里交流的所有工程师一样,那么在阅读这篇文章的过程中,你可能一直在期待我会在某个时刻说:“嗯,好吧,现实情况其实要复杂得多得多。”但事实并非如此。
这基本上就是构成一个代码编辑 Agent 核心循环的全部要素。诚然,要将它集成到你的编辑器里、精心调整系统提示、在恰当的时机提供反馈、设计用户界面、开发更完善的工具、支持多个 Agent 同时运行——这些都需要扎实的工程实践和大量的辛勤付出,而非依赖什么天才般的灵光乍现。
如今的这些大语言模型已经拥有了令人难以置信的能力。仅仅三百行代码和三个简单的工具,你现在就能与一个能够编辑你代码的“外星智能”进行对话了。如果你还在想“但是我们其实并没有真正做到什么了不起的…”。
——那就亲自去试试看!看看用我们刚才构建的基础你能走多远。我敢打赌,结果会远远超出你的预期。
这,就是我们坚信一切都将因此而改变的原因所在。



(文:特工宇宙)