Amp:如何构建一个代理
构建一个小型却非常令人印象深刻代理甚至不需要那么多。你可以在不到400行的代码中完成,其中大部分是样板代码。
构建一个功能齐全的代码编辑代理并不难。
看起来似乎很难。当你看到一个代理在编辑文件、运行命令、摆脱错误、尝试不同的策略时——似乎一定有某种秘密。
没有。它只是一个LLM,一个循环和足够的token。这正是我们在播客上从一开始就所说的。其余的,使Amp如此令人上瘾和印象深刻的东西?是苦工。
但构建一个小型却非常令人印象深刻代理甚至不需要那么多。你可以在不到400行的代码中完成,其中大部分是样板代码。
我将向你展示如何做到这一点,现在。我们将一起编写一些代码,并从零行代码开始到“哦哇,这真是个游戏规则改变者”。
我恳求你跟着做。不,真的。你可能会认为你可以只是阅读本文而不需要输入代码,但它不到400行代码。我需要你感受到它有多么少的代码,并希望你亲眼在自己的终端和文件夹中看到它。
我们需要以下内容:
- Go
- Anthropic API密钥,将其设置为环境变量
ANTHROPIC_API_KEY
1、铅笔拿出来!
让我们直接开始,用四个简单的命令设置一个新的Go项目:
mkdir code-editing-agent
cd code-editing-agent
go mod init agent
touch main.go
现在,让我们打开main.go,并首先放入我们所需要的代码结构:
package main
import (
"bufio"
"context"
"fmt"
"os"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
client := anthropic.NewClient()
scanner := bufio.NewScanner(os.Stdin)
getUserMessage := func() (string, bool) {
if !scanner.Scan() {
return "", false
}
return scanner.Text(), true
}
agent := NewAgent(&client, getUserMessage)
err := agent.Run(context.TODO())
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
}
}
func NewAgent(client *anthropic.Client, getUserMessage func() (string, bool)) *Agent {
return &Agent{
client: client,
getUserMessage: getUserMessage,
}
}
type Agent struct {
client *anthropic.Client
getUserMessage func() (string, bool)
}
是的,这还不能编译。但在这里我们有一个Agent,它具有对anthropic.Client(默认情况下会查找ANTHROPIC_API_KEY)的访问权限,并且可以通过读取终端中的stdin获取用户消息。
现在让我们添加缺失的Run()方法:
// main.go
func (a *Agent) Run(ctx context.Context) error {
conversation := []anthropic.MessageParam{}
fmt.Println("与Claude聊天(使用'ctrl-c'退出)")
for {
fmt.Print("\u001b[94mYou\u001b[0m: ")
userInput, ok := a.getUserMessage()
if !ok {
break
}
userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput))
conversation = append(conversation, userMessage)
message, err := a.runInference(ctx, conversation)
if err != nil {
return err
}
conversation = append(conversation, message.ToParam())
for _, content := range message.Content {
switch content.Type {
case "text":
fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text)
}
}
}
return nil
}
func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) {
message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaude3_7SonnetLatest,
MaxTokens: int64(1024),
Messages: conversation,
})
return message, err
}
不多,对吧?90行,其中最重要的部分是Run()中的这个循环,它让我们与Claude交谈。但这就是这个程序的心跳。
对于心跳来说,它非常简单:我们首先打印一个提示,要求用户输入,将其添加到对话中,发送给Claude,将Claude的响应添加到对话中,然后打印响应,继续循环。
这是你曾经使用过的每一个AI聊天应用程序,只不过是在终端中。
让我们运行它:
export ANTHROPIC_API_KEY="这是最后一次我告诉你设置这个"
# 下载依赖项
go mod tidy
# 运行它
go run main.go
然后你可以像这样与Claude交谈:
$ go run main.go
Chat 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
* Thorsteed
These are meant to be lighthearted - I hope they give you a chuckle!
You: 注意我们是如何在多轮对话中保持相同的对话。它记得我第一次消息中的名字。每次对话都会增长,我们每次都发送整个对话。服务器 —— Anthropic的服务器 —— 是无状态的。它只看到conversation切片中的内容。由我们来维护它。
好了,让我们继续,因为昵称很糟糕,这还不是代理。什么是代理?这里是我的定义: 一个具有工具访问权限的LLM,使其能够修改上下文窗口之外的某些东西。
2、第一个工具
具有工具访问权限的LLM?什么是工具?基本的想法是这样的:你向模型发送一个提示,说如果它想要使用“工具”,应该以特定方式回复。然后,作为接收该消息的人,你通过执行它并用结果回复来“使用该工具”。就是这样。其他的一切我们看到的都是对其上的抽象。
想象一下,你和一个朋友说话,然后你告诉他们:“在接下来的对话中,如果你想让我抬起手臂,请眨眼。” 很奇怪的一句话,但概念很容易理解。
我们甚至可以在不更改任何代码的情况下尝试它。
$ go run main.go
Chat 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)我们告诉Claude在想了解天气时用get_weather眨眼。下一步是抬起我们的手臂并回复“工具的结果”:
You: hot and humid, 28 degrees celcius
Claude: Thank you for providing that information. The current weather in Munich is hot and humid at 28 degrees Celsius (about 82 degrees Fahrenheit). These are fairly warm conditions for Munich, especially with the added humidity which can make it feel even warmer than the temperature indicates.
If you're in Munich or planning to visit, it would be good to stay hydrated, wear light clothing, and perhaps plan indoor activities during the hottest parts of the day. The combination of heat and humidity can be quite uncomfortable, particularly if you're not accustomed to such weather.
Would you like to know about the weather in any other location?这在第一次尝试时就非常顺利,不是吗?
这些模型经过训练和微调,可以使用“工具”,并且非常渴望这样做。到目前为止,2025年,它们“知道”它们不知道一切,并可以使用工具获取更多信息。(当然这不是确切发生的情况,但目前这是一个足够好的解释。)
总结一下,工具和工具使用只有两件事:
- 你告诉模型有哪些工具可用
- 当模型想要执行工具时,它会告诉你,你执行工具并发送结果
为了使(1)更容易,主要的模型提供商已经建立了内置API来发送工具定义。
好的,现在让我们构建我们的第一个工具:read_file
3、read_file 工具
为了定义read_file工具,我们将使用Anthropic SDK建议的类型,但请记住:在底层,这一切最终都会变成发送给模型的字符串。一切都“如果想让我使用read_file就眨眼”。
我们要添加的每个工具都需要以下内容:
- 名称
- 描述,告诉模型该工具做什么,何时使用它,何时不要使用它,它返回什么等等
- 输入模式,描述该工具期望的输入以及形式
- 实际执行该工具的函数,使用模型发送给我们的输入并返回结果
所以让我们将这些添加到我们的代码中:
// main.go
type ToolDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema anthropic.ToolInputSchemaParam `json:"input_schema"`
Function func(input json.RawMessage) (string, error)
}
现在我们给Agent工具定义:
// main.go
// `tools` 是在这里添加的:
type Agent struct {
client *anthropic.Client
getUserMessage func() (string, bool)
tools []ToolDefinition
}
// 并在这里:
func NewAgent(
client *anthropic.Client,
getUserMessage func() (string, bool),
tools []ToolDefinition,
) *Agent {
return &Agent{
client: client,
getUserMessage: getUserMessage,
tools: tools,
}
}
// 并在这里:
func main() {
// [... previous code ...]
tools := []ToolDefinition{}
agent := NewAgent(&client, getUserMessage, tools)
// [... previous code ...]
}
并在runInference中发送给模型:
// main.go
func (a *Agent) runInference(ctx context.Context, conversation []anthropic.MessageParam) (*anthropic.Message, error) {
anthropicTools := []anthropic.ToolUnionParam{}
for _, tool := range a.tools {
anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{
OfTool: &anthropic.ToolParam{
Name: tool.Name,
Description: anthropic.String(tool.Description),
InputSchema: tool.InputSchema,
},
})
}
message, err := a.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaude3_7SonnetLatest,
MaxTokens: int64(1024),
Messages: conversation,
Tools: anthropicTools,
})
return message, err
}
有很多类型操作,我不太擅长Go泛型,所以我不会试图向你解释anthropic.String和ToolUnionParam。但是,我真的发誓,它非常简单:
我们连同我们的工具定义一起发送,Anthropic服务器然后将这些定义包装在这个系统提示(不是很复杂)中,将其添加到我们的conversation中,然后模型会以特定的方式回复如果它想使用该工具。
好的,工具定义被发送了,但我们还没有定义一个工具。让我们定义read_file:
// main.go
var ReadFileDefinition = ToolDefinition{
Name: "read_file",
Description: "读取给定相对路径文件的内容。当您想查看文件内容时使用此工具。不要使用目录名称。",
InputSchema: ReadFileInputSchema,
Function: ReadFile,
}
type ReadFileInput struct {
Path string `json:"path" jsonschema_description:"工作目录中的文件的相对路径。"`
}
var ReadFileInputSchema = GenerateSchema[ReadFileInput]()
func ReadFile(input json.RawMessage) (string, error) {
readFileInput := ReadFileInput{}
err := json.Unmarshal(input, &readFileInput)
if err != nil {
panic(err)
}
content, err := os.ReadFile(readFileInput.Path)
if err != nil {
return "", err
}
return string(content), nil
}
func GenerateSchema[T any]() anthropic.ToolInputSchemaParam {
reflector := jsonschema.Reflector{
AllowAdditionalProperties: false,
DoNotReference: true,
}
var v T
schema := reflector.Reflect(v)
return anthropic.ToolInputSchemaParam{
Properties: schema.Properties,
}
}
不多,对吧?它是一个单函数ReadFile,以及模型将看到的两个描述:我们描述工具本身的Description(“读取给定相对路径文件的内容。...”)和该工具拥有的单个输入参数的描述(“文件的相对路径...”)。
ReadFileInputSchema和GenerateSchema这些东西?我们需要它们以便生成我们发送给模型的工具定义的JSON模式。为此,我们使用jsonschema包,我们需要导入和下载:
// main.go
package main
import (
"bufio"
"context"
// 添加这个:
"encoding/json"
"fmt"
"os"
"github.com/anthropics/anthropic-sdk-go"
// 添加这个:
"github.com/invopop/jsonschema"
)
然后运行以下命令:
go mod tidy
然后,在main函数中,我们需要确保我们使用该定义:
func main() {
// [... previous code ...]
tools := []ToolDefinition{ReadFileDefinition}
// [... previous code ...]
}
是时候尝试它了!
$ go run main.go
Chat 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.go
func (a *Agent) Run(ctx context.Context) error {
conversation := []anthropic.MessageParam{}
fmt.Println("与Claude聊天(使用'ctrl-c'退出)")
readUserInput := true
for {
if readUserInput {
fmt.Print("\u001b[94mYou\u001b[0m: ")
userInput, ok := a.getUserMessage()
if !ok {
break
}
userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(userInput))
conversation = append(conversation, userMessage)
}
message, err := a.runInference(ctx, conversation)
if err != nil {
return err
}
conversation = append(conversation, message.ToParam())
toolResults := []anthropic.ContentBlockParamUnion{}
for _, content := range message.Content {
switch content.Type {
case "text":
fmt.Printf("\u001b[93mClaude\u001b[0m: %s\n", content.Text)
case "tool_use":
result := a.executeTool(content.ID, content.Name, content.Input)
toolResults = append(toolResults, result)
}
}
if len(toolResults) == 0 {
readUserInput = true
continue
}
readUserInput = false
conversation = append(conversation, anthropic.NewUserMessage(toolResults...))
}
return nil
}
func (a *Agent) executeTool(id, name string, input json.RawMessage) anthropic.ContentBlockParamUnion {
var toolDef ToolDefinition
var found bool
for _, tool := range a.tools {
if tool.Name == name {
toolDef = tool
found = true
break
}
}
if !found {
return anthropic.NewToolResultBlock(id, "tool not found", true)
}
fmt.Printf("\u001b[92mtool\u001b[0m: %s(%s)\n", name, input)
response, err := toolDef.Function(input)
if err != nil {
return anthropic.NewToolResultBlock(id, err.Error(), true)
}
return anthropic.NewToolResultBlock(id, response, false)
}
眯着眼睛,你会看到它90%是样板代码,10%是重要的内容:当我们从Claude得到一个message时,我们通过检查content.Type == "tool_use"来查看Claude是否要求我们执行一个工具,如果是的话,我们把它交给executeTool,在我们的本地注册表中查找工具,解码输入,执行它,返回结果。如果出错,我们翻转一个布尔值。就是这样。
(是的,有一个循环在循环中,但这没关系。)
我们执行工具,将结果返回给Claude,然后再次请求Claude的回复。真正地说,那就是它。让我向你展示。
准备,运行这个:
echo 'what animal is the most disagreeable because it always says neigh?' >> secret-file.txt
这会在我们的目录中创建一个名为secret-file.txt的文件,包含一个神秘的谜题。
在同一目录中,让我们运行我们的新工具使用代理,并让它查看文件:
$ go run main.go
Chat with Claude (use 'ctrl-c' to quit)
You: Claude, buddy, help me solve the riddle in the secret-file.txt file
Claude: 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.go
Chat 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 exits
The code essentially builds a simple CLI-based chatbot that can interact with the file system when requested by Claude.
You: - 使用Anthropic Go SDK创建了一个命令行聊天界面与Claude
- 实现了一个带有“read_file”工具的工具系统,用于读取文件内容
- 管理与Claude的对话流程,处理常规文本响应和工具使用请求
- 当Claude请求使用工具时,代理执行它并返回结果
- 程序持续循环以维持对话,直到用户退出 该代码本质上构建了一个简单的基于CLI的聊天机器人,可以在Claude需要时与文件系统交互。You:
准确无误。好了,现在我们知道如何让Claude使用工具,让我们再添加几个。
4、list_files 工具
如果你和我一样,登录到新电脑的第一件事就是运行ls——列出文件。
让我们给Claude同样的能力,一个列出文件的工具。这里是list_files工具的完整实现:
// main.go
var ListFilesDefinition = ToolDefinition{
Name: "list_files",
Description: "列出给定路径下的文件和目录。如果没有提供路径,则列出当前目录中的文件。",
InputSchema: ListFilesInputSchema,
Function: ListFiles,
}
type ListFilesInput struct {
Path string `json:"path,omitempty" jsonschema_description:"可选的相对路径,用于列出文件。如果没有提供,则默认为当前目录。"`
}
var ListFilesInputSchema = GenerateSchema[ListFilesInput]()
func ListFiles(input json.RawMessage) (string, error) {
listFilesInput := ListFilesInput{}
err := json.Unmarshal(input, &listFilesInput)
if err != nil {
panic(err)
}
dir := "."
if listFilesInput.Path != "" {
dir = listFilesInput.Path
}
var files []string
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
}
result, err := json.Marshal(files)
if err != nil {
return "", err
}
return string(result), nil
}
没什么花哨的:list_files返回当前文件夹中的文件和目录列表。如果我们认真对待这件事,我们可以进行一千种优化,但既然我只是想向你展示魔术师帽子里的东西,这就足够了。
需要注意的一点是:我们返回一个字符串列表,并用尾部斜杠表示目录。这并不是必须的,只是我决定这么做。没有固定格式。只要Claude能理解它,以及你需要通过实验来确定。你也可以在每个目录前加上“directory: ”或者返回一个带有两个标题“directories”和“files”的Markdown文档。有很多选项,你选择哪一个取决于Claude能理解最多的内容,需要多少token,生成和读取的速度如何等等。
在这里,我们只需要创建一个小型的list_files工具,最简单的选项胜出。
当然我们也需要告诉Claudelist_files:
// main.go
func main() {
// [... previous code ...]
tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition}
// [... previous code ...]
}
就这样。让我们问Claude它在这个目录中能看到什么。
$ go run main.go
Chat 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` file
2. Go project files:
- `main.go` - the main Go source code file
- `go.mod` - Go module definition file
- `go.sum` - Go module checksum file
3. Documentation:
- `blogpost.md` - a markdown file, likely containing blog content
4. Environment configuration:
- `.envrc` - likely an environment configuration file
Would you like me to examine the contents of any specific file from this list?
You: 有效!它可以读取目录。
但这里有个问题:Claude知道如何组合这些工具。我们只需要以一种能激发它的方法来提示它:
$ go run main.go
Chat 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.1
This 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,然后它用read_file两次调用了Go相关的文件,这是我要求它检查的。
就像我们一样,对吧?我的意思是,如果我问你这个项目中使用的是什么版本的Go,你会怎么做?这就是Claude为我做的:
$ go run main.go
Chat 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,并得到了答案。
我们现在大约有190行代码。让这个沉下来。一旦你有了,让我们添加另一个工具。
5、edit_file
我们要添加的最后一个工具是edit_file——一个让Claude编辑文件的工具。
“天哪”,你现在想,“这就是关键时刻,这就是他把兔子从帽子里拿出来的时候。”好吧,让我们看看,好吗?
首先,让我们添加我们新edit_file工具的定义:
// main.go
var EditFileDefinition = ToolDefinition{
Name: "edit_file",
Description: `对文本文件进行编辑。
将'old_str'替换为'new_str'。'old_str'和'new_str'必须不同。
如果指定的路径文件不存在,它将被创建。
`,
InputSchema: EditFileInputSchema,
Function: EditFile,
}
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替换的文本"`
}
var EditFileInputSchema = GenerateSchema[EditFileInput]()
是的,我又知道你在想什么:“字符串替换来编辑文件?”Claude 3.7喜欢替换字符串(实验是发现它们喜欢或不喜欢的方法),所以我们通过告诉Claude它可以使用字符串替换来编辑文件来实现edit_file。
现在这里是Go中EditFile函数的实现:
func EditFile(input json.RawMessage) (string, error) {
editFileInput := EditFileInput{}
err := json.Unmarshal(input, &editFileInput)
if err != nil {
return "", err
}
if editFileInput.Path == "" || editFileInput.OldStr == editFileInput.NewStr {
return "", fmt.Errorf("无效的输入参数")
}
content, err := os.ReadFile(editFileInput.Path)
if err != nil {
if os.IsNotExist(err) && editFileInput.OldStr == "" {
return createNewFile(editFileInput.Path, editFileInput.NewStr)
}
return "", err
}
oldContent := string(content)
newContent := strings.Replace(oldContent, editFileInput.OldStr, editFileInput.NewStr, -1)
if oldContent == newContent && editFileInput.OldStr != "" {
return "", fmt.Errorf("未在文件中找到old_str")
}
err = os.WriteFile(editFileInput.Path, []byte(newContent), 0644)
if err != nil {
return "", err
}
return "OK", nil
}
它检查输入参数,读取文件(如果存在则创建它),并将OldStr替换为NewStr。然后它将内容写回磁盘并返回"OK"。
仍然缺少的是createNewFile,这只是一个小小的辅助函数,如果这不是Go,它会短70%:
func createNewFile(filePath, content string) (string, error) {
dir := path.Dir(filePath)
if dir != "." {
err := os.MkdirAll(dir, 0755)
if err != nil {
return "", fmt.Errorf("无法创建目录: %w", err)
}
}
err := os.WriteFile(filePath, []byte(content), 0644)
if err != nil {
return "", fmt.Errorf("无法创建文件: %w", err)
}
return fmt.Sprintf("成功创建文件 %s", filePath), nil
}
最后一步:将它添加到我们发送给Claude的工具列表中。
// main.go
func main() {
// [... previous code ...]
tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, EditFileDefinition}
// [... previous code ...]
}
并且……我们准备好了,但你准备好吗?你准备好让它大展身手了吗?
想一想,让我们这么做。让我们告诉Claude在JavaScript中创建一个FizzBuzz函数。
$ go run main.go
Chat 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 it
Claude: 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 limit
2. 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 itself
4. The script automatically executes the function for numbers 1 to 100
You can run this script using Node.js with the following command in your terminal:
node fizzbuzz.js
This will output the FizzBuzz sequence from 1 to 100 to your console.对吧?它很令人印象深刻,不是吗?而且那是edit_file——或者说代理的最基本实现——你可能能想到的。
但是,它有效吗?是的,它有效:
$ node fizzbuzz.js
运行FizzBuzz:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
[...]
令人惊叹。但嘿,让我们让它实际编辑一个文件,而不仅仅只是创建一个。
当我要求它“请将fizzbuzz.js编辑成只打印到15”时,Claude是怎么做的:
$ go run main.go
Chat with Claude (use 'ctrl-c' to quit)
You: Please edit fizzbuzz.js so that it only prints until 15
Claude: 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 change
The 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.js
运行FizzBuzz:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
好的,让我们再做一个,要求它做以下事情:
创建一个 congrats.js 脚本,该脚本对以下字符串 'Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!' 进行 rot13 解码并打印它
也许是个高难度任务。让我们看看:
$ go run main.go
Chat 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 it
Claude: 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 it
2. Takes the provided string 'Pbatenghyngvbaf ba ohvyqvat n pbqr-rqvgvat ntrag!'
3. Decodes it using the function
4. Prints the decoded message to the console
You 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.js
恭喜你构建了一个代码编辑代理!
它确实有效!
6、这不是很惊人吗?
如果你和我过去几个月谈过的所有工程师一样, 你很可能在阅读本文时一直在等待兔子被拉出帽子,等着我说“嗯,实际上它比这要困难得多。” 但事实并非如此。
这就是代码编辑代理内部循环的全部内容。 当然,将其集成到你的编辑器中,调整系统提示,适时给予反馈,围绕它设计一个漂亮的UI,更好的工具周围工具,支持多个代理等等——我们在Amp中都构建了这些,但不需要天才时刻。所需的一切只是提示工程和苦工。
这些模型现在非常强大。300行代码和三个工具,现在你可以与一个编辑你代码的外星智能交流。如果你觉得“嗯,但我们真的没有…”——去尝试吧!去看看你能走多远。我打赌它比你想象的远得多。
那就是为什么我们认为一切都在改变。
汇智网翻译整理,转载请标明出处