Roog's BLOG & TOOLS

[系统已在线] :: 2026-01-30 16:10:21 :: 终端 v1.0

Crush 代码修改工具学习

Crush 代码修改工具学习

概述: Crush是什么?

Crush 是由 Charm 开发的一款开源 AI 编程助手框架,它将大语言模型(LLM)与开发工具深度集成到终端环境中。本文将深入分析 Crush 中负责代码修改的核心工具实现,揭示其如何在保证安全性和可靠性的前提下,实现智能化的代码编辑功能。

项目背景

项目信息

  • 名称: Crush
  • 语言: Go 1.25.0
  • 类型: AI 编程助手框架
  • 核心特性:
    • 多模型支持(OpenAI、Anthropic、Google、Azure 等)
    • 会话式交互管理
    • LSP(Language Server Protocol)集成
    • MCP(Model Context Protocol)扩展支持
    • 跨平台支持(macOS、Linux、Windows、FreeBSD)

架构概览

Crush 的代码修改功能主要由三个核心工具组成,它们位于 internal/agent/tools/ 目录下:

Dir: internal/agent/tools/

  • edit.go # 基础编辑工具
  • multiedit.go # 批量编辑工具
  • write.go # 文件写入工具
  • view.go # 文件查看工具
  • diagnostics.go # LSP 诊断集成

这些工具共同构成了一个完整的代码编辑生态系统,每个工具都有明确的职责分工,同时共享核心的安全和验证逻辑。

核心工具详解

1. Edit Tool - 精确的字符串替换

Edit Tool 是最基础也是最常用的编辑工具,通过精确的字符串匹配和替换来修改文件内容。

参数定义

type EditParams struct {
    FilePath   string `json:"file_path"`   // 文件绝对路径
    OldString  string `json:"old_string"`  // 要替换的文本
    NewString  string `json:"new_string"`  // 替换后的文本
    ReplaceAll bool   `json:"replace_all"` // 是否替换所有出现
}

三种操作模式

Edit Tool 根据参数的不同组合,支持三种操作模式:

  • 创见

  • 更新

  • 删除

    crush实现的挺有意思,它在逻辑上相当于用一个新字符串在替换旧的字符串 如果说新的字符串为空,那么就是删除,如果旧的字符串为空,那么就是创建,如果新旧都不为空,那么就是编辑(想到养鱼替换操作)

1. 创建新文件 (OldString 为空)

func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) {
    // 检查文件是否已存在
    if _, err := os.Stat(filePath); err == nil {
        return error("file already exists")
    }

    // 创建父目录
    os.MkdirAll(filepath.Dir(filePath), 0o755)

    // 请求权限
    if !permissions.Request(...) {
        return ErrorPermissionDenied
    }

    // 写入文件
    os.WriteFile(filePath, []byte(content), 0o644) <=== 这里是固定的也许我们写的时候不会这么写

    // 创建历史记录
    files.Create(ctx, sessionID, filePath, "")
    files.CreateVersion(ctx, sessionID, filePath, content)
}

2. 删除内容 (NewString 为空)

func deleteContent(edit editContext, filePath, oldString string, replaceAll bool) {
    // 读取并验证文件
    content, _ := os.ReadFile(filePath)
    oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))

    // 执行删除
    if replaceAll {
        newContent = strings.ReplaceAll(oldContent, oldString, "")
    } else {
        // 查找并删除单个匹配
        index := strings.Index(oldContent, oldString)
        newContent = oldContent[:index] + oldContent[index+len(oldString):]
    }

    // 写入并更新历史
    os.WriteFile(filePath, []byte(newContent), 0o644)
}

3. 替换内容 (正常编辑)

func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool) {
    // 读取文件
    content, _ := os.ReadFile(filePath)
    oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))

    // 执行替换
    if replaceAll {
        newContent = strings.ReplaceAll(oldContent, oldString, newString)
    } else {
        // 单次替换,需要确保唯一性
        index := strings.Index(oldContent, oldString)
        lastIndex := strings.LastIndex(oldContent, oldString)
        if index != lastIndex {
            return error("old_string appears multiple times")
        }
        newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
    }
}

唯一性检查

Edit Tool 的一个关键特性是唯一性检查。当 ReplaceAllfalse 时,它会确保 OldString 在文件中只出现一次,防止模糊匹配导致的错误替换:

这里很明显,它的意图是说,如果万一要完成多出替换,或者仅做单个替换,那么这里要区别处理,很好的提醒,如果没看到这个我在写ARS一定会犯这个错,我觉得AI肯定也会。

index := strings.Index(oldContent, oldString)
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
    return fantasy.NewTextErrorResponse(
        "old_string appears multiple times in the file. " +
        "Please provide more context to ensure a unique match, " +
        "or set replace_all to true"
    )
}

2. MultiEdit Tool - 批量编辑利器

MultiEdit Tool 支持在单个文件上顺序执行多个编辑操作,这在需要进行多处修改时特别有用。

参数定义

type MultiEditParams struct {
    FilePath string               `json:"file_path"`
    Edits    []MultiEditOperation `json:"edits"`
}

type MultiEditOperation struct {
    OldString  string `json:"old_string"`
    NewString  string `json:"new_string"`
    ReplaceAll bool   `json:"replace_all"`
}

核心特性

1. 顺序执行与部分失败容错

MultiEdit Tool 会顺序应用每个编辑操作,如果某个操作失败,不会影响其他操作的执行:

// 应用所有编辑,记录失败的操作
var failedEdits []FailedEdit
for i, edit := range params.Edits {
    newContent, err := applyEditToContent(currentContent, edit)
    if err != nil {
        failedEdits = append(failedEdits, FailedEdit{
            Index: i + 1,
            Error: err.Error(),
            Edit:  edit,
        })
        continue  // 继续执行下一个编辑
    }
    currentContent = newContent
}

它的实际用意其实是担心由于权限啊,文件变化等原因,会导致编辑失败,如果失败了,那就接着弄其他文件。 这样听起来不合理,但是把tool执行结果抛回给Agent的时候其实Agent肯定会进行进一步的策略(比如调用Bash修改权限) 这种情况还蛮常见。

2. 支持创建时编辑

第一个编辑操作的 OldString 可以为空,表示创建文件:

if len(params.Edits) > 0 && params.Edits[0].OldString == "" {
    response, err = processMultiEditWithCreation(editCtx, params, call)
} else {
    response, err = processMultiEditExistingFile(editCtx, params, call)
}

3. 详细的执行报告

返回详细的执行统计信息:

type MultiEditResponseMetadata struct {
    Additions    int          `json:"additions"`     // 新增行数
    Removals     int          `json:"removals"`      // 删除行数
    OldContent   string       `json:"old_content"`   // 原始内容
    NewContent   string       `json:"new_content"`   // 修改后内容
    EditsApplied int          `json:"edits_applied"` // 成功应用的编辑数
    EditsFailed  []FailedEdit `json:"edits_failed"`  // 失败的编辑列表
}

编辑应用逻辑

func applyEditToContent(content string, edit MultiEditOperation) (string, error) {
    if edit.OldString == "" {
        return "", fmt.Errorf("old_string cannot be empty for content replacement")
    }

    if edit.ReplaceAll {
        newContent = strings.ReplaceAll(content, edit.OldString, edit.NewString)
        replacementCount := strings.Count(content, edit.OldString)
        if replacementCount == 0 {
            return "", fmt.Errorf("old_string not found in content")
        }
    } else {
        // 单次替换,需要确保唯一性
        index := strings.Index(content, edit.OldString)
        if index == -1 {
            return "", fmt.Errorf("old_string not found in content")
        }

        lastIndex := strings.LastIndex(content, edit.OldString)
        if index != lastIndex {
            return "", fmt.Errorf("old_string appears multiple times")
        }

        newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
    }

    return newContent, nil
}

3. Write Tool - 完整文件写入

Write Tool 用于写入完整的文件内容,适合创建新文件或完全重写文件的场景。

参数定义

type WriteParams struct {
    FilePath string `json:"file_path"` // 文件路径
    Content  string `json:"content"`   // 完整的文件内容
}

与 Edit Tool 的区别

特性Edit ToolWrite Tool
输入方式需要提供 OldString 上下文只需提供完整内容
使用场景局部修改创建新文件或完全重写
性能更高效(只传输差异)传输完整内容
安全性更安全(明确修改位置)较低(可能误覆盖)

核心实现

func NewWriteTool(...) fantasy.AgentTool {
    return fantasy.NewAgentTool(
        WriteToolName,
        string(writeDescription),
        func(ctx context.Context, params WriteParams, call fantasy.ToolCall) {
            // 1. 检查文件是否存在且已修改
            fileInfo, err := os.Stat(filePath)
            if err == nil {
                modTime := fileInfo.ModTime()
                lastRead := getLastReadTime(filePath)
                if modTime.After(lastRead) {
                    return error("File has been modified since last read")
                }

                // 检查内容是否相同
                oldContent, _ := os.ReadFile(filePath)
                if string(oldContent) == params.Content {
                    return error("File already contains the exact content")
                }
            }

            // 2. 创建父目录
            os.MkdirAll(filepath.Dir(filePath), 0o755)

            // 3. 生成 Diff
            diff, additions, removals := diff.GenerateDiff(
                oldContent, params.Content, filePath,
            )

            // 4. 请求权限
            if !permissions.Request(...) {
                return ErrorPermissionDenied
            }

            // 5. 写入文件
            os.WriteFile(filePath, []byte(params.Content), 0o644)

            // 6. 更新历史
            files.CreateVersion(ctx, sessionID, filePath, params.Content)

            // 7. 通知 LSP 并返回诊断信息
            notifyLSPs(ctx, lspClients, params.FilePath)
            return getDiagnostics(filePath, lspClients)
        },
    )
}

完整的编辑工作流程

下面这个流程我让Codex帮我梳理的,我仔细对照了代码,并做了部分调整和补充,确保它我们总结的是有依有据的。

所有编辑工具都遵循相同的标准化工作流程,确保操作的安全性和可追溯性:

┌─────────────────────────────────────────────────────────────┐
│  1. 参数验证                                                  │
│     - FilePath 非空检查                                       │
│     - 其他必需参数验证                                         │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  2. 路径处理                                                  │
│     filepathext.SmartJoin(workingDir, params.FilePath)      │
│     - 相对路径转换为绝对路径                                   │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  3. 文件状态检查                                              │
│     ├─ 检查文件是否已读取: getLastReadTime()                  │
│     ├─ 检查修改时间: modTime.After(lastRead)                  │
│     ├─ 检查是否是目录                                         │
│     └─ 检查文件大小限制                                       │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  4. 读取并处理文件内容                                         │
│     content, _ := os.ReadFile(filePath)                     │
│     oldContent, isCrlf := fsext.ToUnixLineEndings(content)  │
│     - 统一为 Unix 行结束符便于处理                             │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  5. 执行编辑操作                                              │
│     ├─ strings.Index() 查找匹配位置                          │
│     ├─ 检查唯一性(非 ReplaceAll 模式)                       │
│     └─ 执行字符串替换                                         │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  6. 生成 Diff                                                │
│     diff, additions, removals := diff.GenerateDiff(...)     │
│     - 生成 unified diff 格式                                  │
│     - 计算添加/删除的行数                                      │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  7. 权限请求                                                  │
│     p := permissions.Request(                                │
│         SessionID, Path, ToolCallID, ToolName,              │
│         Action, Description, Params                         │
│     )                                                        │
│     - 发送到 UI 等待用户确认                                   │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  8. 恢复行结束符格式                                           │
│     if isCrlf {                                              │
│         newContent = fsext.ToWindowsLineEndings(newContent) │
│     }                                                        │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  9. 写入文件                                                  │
│     os.WriteFile(filePath, []byte(newContent), 0o644)       │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  10. 更新文件历史                                             │
│      ├─ files.GetByPathAndSession() 获取历史                 │
│      ├─ files.Create() 如果不存在                            │
│      ├─ files.CreateVersion() 保存中间版本(如有手动修改)     │
│      └─ files.CreateVersion() 保存新版本                     │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  11. 记录读写时间                                             │
│      recordFileWrite(filePath)                              │
│      recordFileRead(filePath)                               │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  12. 通知 LSP 客户端                                          │
│      notifyLSPs(ctx, lspClients, filePath)                  │
│      - 触发 LSP 重新分析文件                                  │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  13. 收集诊断信息                                             │
│      diagnostics := getDiagnostics(filePath, lspClients)    │
│      - 获取语法错误、类型错误等                                │
└─────────────────────┬───────────────────────────────────────┘
                      ↓
┌─────────────────────────────────────────────────────────────┐
│  14. 返回响应                                                 │
│      - 操作结果消息                                           │
│      - 元数据(Diff、统计信息)                                │
│      - LSP 诊断信息                                           │
└─────────────────────────────────────────────────────────────┘

关键安全机制

Crush 的编辑工具实现了多层安全机制,确保代码修改操作的安全性和可靠性。

1. 读取前置检查

所有编辑操作都要求先使用 View Tool 读取文件,这样可以:

  • 确保 AI 看到了最新的文件内容
  • 记录文件的读取时间用于后续验证
  • 避免盲目修改文件
if getLastReadTime(filePath).IsZero() {
    return fantasy.NewTextErrorResponse(
        "you must read the file before editing it. Use the View tool first"
    )
}

2. 修改时间验证

防止编辑被其他进程修改过的文件,避免覆盖未知的更改:

modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
    return fantasy.NewTextErrorResponse(
        fmt.Sprintf(
            "file %s has been modified since it was last read\n" +
            "mod time: %s, last read: %s",
            filePath,
            modTime.Format(time.RFC3339),
            lastRead.Format(time.RFC3339),
        )
    )
}

3. 唯一性检查

ReplaceAllfalse 时,确保 OldString 在文件中只出现一次:

index := strings.Index(oldContent, oldString)
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
    return fantasy.NewTextErrorResponse(
        "old_string appears multiple times in the file. " +
        "Please provide more context to ensure a unique match, " +
        "or set replace_all to true"
    )
}

4. 权限系统

每次编辑操作都需要通过权限服务的验证,支持:

  • 会话级别的权限管理
  • 工具和操作级别的控制
  • 用户交互式确认
type CreatePermissionRequest struct {
    SessionID   string // 会话 ID
    ToolCallID  string // 工具调用 ID
    ToolName    string // 工具名称 (edit, write, multiedit)
    Description string // 人类可读的操作描述
    Action      string // 操作类型 (read, write)
    Params      any    // 工具参数
    Path        string // 文件路径
}

p := permissions.Request(permissionRequest)
if !p {
    return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}

权限请求会包含完整的 Diff 信息,用户可以在批准前看到具体的修改内容。

5. 文件历史追踪

Crush 为每个会话维护完整的文件修改历史,支持版本回溯:

// 检查文件是否已在历史中
file, err := files.GetByPathAndSession(ctx, filePath, sessionID)
if err != nil {
    // 首次编辑,创建历史记录
    files.Create(ctx, sessionID, filePath, oldContent)
}

// 如果用户手动修改过文件,保存中间版本
if file.Content != oldContent {
    files.CreateVersion(ctx, sessionID, filePath, oldContent)
}

// 保存新版本
files.CreateVersion(ctx, sessionID, filePath, newContent)

历史记录数据结构:

type File struct {
    ID        string // 唯一标识
    SessionID string // 所属会话
    Path      string // 文件路径
    Content   string // 文件内容
    Version   int64  // 版本号
    CreatedAt int64  // 创建时间
    UpdatedAt int64  // 更新时间
}

6. 内容相同检查

避免无意义的写入操作:

if oldContent == newContent {
    return fantasy.NewTextErrorResponse(
        "new content is the same as old content. No changes made."
    )
}

LSP 集成

Crush 与 Language Server Protocol (LSP) 深度集成,提供实时的代码诊断功能。

通知 LSP 客户端

编辑完成后自动通知 LSP 客户端文件已更改:

func notifyLSPs(
    ctx context.Context,
    lsps *csync.Map[string, *lsp.Client],
    filepath string,
) {
    // 遍历所有 LSP 客户端
    lsps.Range(func(lang string, client *lsp.Client) bool {
        // 检查文件是否匹配该语言
        if client.MatchesFile(filepath) {
            // 发送文件更改通知
            client.DidChange(ctx, filepath)
        }
        return true
    })
}

收集诊断信息

获取并格式化 LSP 诊断结果:

func getDiagnostics(
    filePath string,
    lsps *csync.Map[string, *lsp.Client],
) string {
    var diagnostics []lsp.Diagnostic

    // 从所有匹配的 LSP 客户端收集诊断
    lsps.Range(func(lang string, client *lsp.Client) bool {
        if client.MatchesFile(filePath) {
            diags := client.GetDiagnostics(filePath)
            diagnostics = append(diagnostics, diags...)
        }
        return true
    })

    if len(diagnostics) == 0 {
        return ""
    }

    // 格式化诊断信息
    var output strings.Builder
    output.WriteString("\n<diagnostics>\n")
    for _, diag := range diagnostics {
        output.WriteString(fmt.Sprintf(
            "%s:%d:%d: %s: %s\n",
            filePath,
            diag.Range.Start.Line,
            diag.Range.Start.Character,
            diag.Severity,
            diag.Message,
        ))
    }
    output.WriteString("</diagnostics>\n")

    return output.String()
}

集成效果

编辑完成后的响应示例:

<result>
Content replaced in file: /path/to/file.go
</result>

<diagnostics>
/path/to/file.go:15:5: error: undefined: variableName
/path/to/file.go:23:10: warning: unused variable 'temp'
</diagnostics>

这使得 AI 可以立即发现语法错误或类型错误,并在下一步操作中进行修正。

跨平台兼容性

行结束符处理

Crush 智能处理不同平台的行结束符差异(Windows 使用 CRLF \r\n,Unix/Linux 使用 LF \n):

// 1. 读取时转换为 Unix 格式
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))

// 2. 在统一的 Unix 格式上进行所有编辑操作
newContent := performEdits(oldContent)

// 3. 写入前恢复原始格式
if isCrlf {
    newContent, _ = fsext.ToWindowsLineEndings(newContent)
}

// 4. 写入文件
os.WriteFile(filePath, []byte(newContent), 0o644)

实现函数

// 转换为 Unix 行结束符,返回转换后的内容和是否原本是 CRLF
func ToUnixLineEndings(content string) (string, bool) {
    isCrlf := strings.Contains(content, "\r\n")
    return strings.ReplaceAll(content, "\r\n", "\n"), isCrlf
}

// 转换为 Windows 行结束符
func ToWindowsLineEndings(content string) (string, bool) {
    // 先确保是 Unix 格式(防止 \r\n\r\n 的情况)
    content = strings.ReplaceAll(content, "\r\n", "\n")
    return strings.ReplaceAll(content, "\n", "\r\n"), true
}

这种处理方式确保了:

  1. 编辑逻辑简单:只需处理一种行结束符
  2. 保持原格式:文件原本是什么格式就保持什么格式
  3. 跨平台协作:在不同平台间切换不会导致格式混乱

路径处理

智能路径连接,支持相对路径和绝对路径:

func SmartJoin(workingDir, filePath string) string {
    if filepath.IsAbs(filePath) {
        return filePath
    }
    return filepath.Join(workingDir, filePath)
}

工具初始化与依赖注入

依赖关系

编辑工具依赖以下核心服务:

type editContext struct {
    ctx         context.Context          // 上下文
    permissions permission.Service       // 权限服务
    files       history.Service          // 历史服务
    workingDir  string                   // 工作目录
}

func NewEditTool(
    lspClients  *csync.Map[string, *lsp.Client], // LSP 客户端
    permissions permission.Service,                // 权限服务
    files       history.Service,                   // 历史服务
    workingDir  string,                            // 工作目录
) fantasy.AgentTool {
    // ...
}

工具注册

所有工具在 Coordinator 中初始化并注册:

// internal/agent/coordinator.go
func (c *Coordinator) initializeTools() []fantasy.AgentTool {
    allTools := []fantasy.AgentTool{
        // 文件查看
        tools.NewViewTool(
            c.lspClients,
            c.permissions,
            c.cfg.WorkingDir(),
        ),

        // 文件编辑
        tools.NewEditTool(
            c.lspClients,
            c.permissions,
            c.history,
            c.cfg.WorkingDir(),
        ),

        // 批量编辑
        tools.NewMultiEditTool(
            c.lspClients,
            c.permissions,
            c.history,
            c.cfg.WorkingDir(),
        ),

        // 文件写入
        tools.NewWriteTool(
            c.lspClients,
            c.permissions,
            c.history,
            c.cfg.WorkingDir(),
        ),

        // 其他工具...
        tools.NewBashTool(...),
        tools.NewGlobTool(...),
        tools.NewGrepTool(...),
    }

    return allTools
}

Fantasy 框架集成

Crush 使用 Charm 的 Fantasy 框架来定义和管理工具:

func NewEditTool(...) fantasy.AgentTool {
    return fantasy.NewAgentTool(
        "edit",                    // 工具名称
        string(editDescription),   // 工具描述(来自 embed 的 markdown)
        func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
            // 工具执行逻辑
            // ...

            return fantasy.WithResponseMetadata(
                fantasy.NewTextResponse("Content replaced in file: " + filePath),
                EditResponseMetadata{
                    OldContent: oldContent,
                    NewContent: newContent,
                    Additions:  additions,
                    Removals:   removals,
                },
            ), nil
        },
    )
}

Fantasy 框架提供:

  • 自动参数验证和类型转换
  • 统一的响应格式
  • 元数据附加支持
  • 错误处理机制

实际使用示例

示例 1:修改函数实现

场景:将一个函数中的变量名从 oldName 改为 newName

// AI 发起的工具调用
{
    "tool": "edit",
    "parameters": {
        "file_path": "internal/service/user.go",
        "old_string": "func UpdateUser(oldName string) error {\n    return db.Update(oldName)\n}",
        "new_string": "func UpdateUser(newName string) error {\n    return db.Update(newName)\n}",
        "replace_all": false
    }
}

执行流程

  1. ✅ 验证文件已被读取
  2. ✅ 检查文件未被修改
  3. ✅ 查找并确认唯一匹配
  4. ✅ 生成 Diff 并请求权限
  5. ✅ 用户确认后执行替换
  6. ✅ 保存历史版本
  7. ✅ 通知 LSP 并获取诊断

响应

<result>
Content replaced in file: internal/service/user.go
</result>

<diagnostics>
No errors found.
</diagnostics>

示例 2:批量修改导入语句

场景:在一个文件中替换多个旧的包导入路径

// AI 发起的工具调用
{
    "tool": "multiedit",
    "parameters": {
        "file_path": "cmd/main.go",
        "edits": [
            {
                "old_string": "\"github.com/old/package1\"",
                "new_string": "\"github.com/new/package1\"",
                "replace_all": false
            },
            {
                "old_string": "\"github.com/old/package2\"",
                "new_string": "\"github.com/new/package2\"",
                "replace_all": false
            },
            {
                "old_string": "old.Function",
                "new_string": "new.Function",
                "replace_all": true
            }
        ]
    }
}

响应

<result>
Applied 3 edits to file: cmd/main.go
</result>

<diagnostics>
No errors found.
</diagnostics>

示例 3:创建新配置文件

场景:创建一个新的 JSON 配置文件

// AI 发起的工具调用
{
    "tool": "write",
    "parameters": {
        "file_path": "config/database.json",
        "content": "{\n  \"host\": \"localhost\",\n  \"port\": 5432,\n  \"database\": \"myapp\",\n  \"ssl\": true\n}"
    }
}

执行流程

  1. ✅ 检查文件不存在
  2. ✅ 创建父目录 config/
  3. ✅ 请求写入权限
  4. ✅ 写入文件内容
  5. ✅ 创建历史记录

响应

<result>
File successfully written: config/database.json
</result>

设计亮点总结

1. 职责分离

三个编辑工具各司其职:

  • Edit: 精确的局部修改
  • MultiEdit: 批量顺序编辑
  • Write: 完整文件创建/重写

2. 安全第一

多层安全检查确保操作安全:

  • 读取前置检查
  • 修改时间验证
  • 唯一性检查
  • 权限系统
  • 历史追踪

3. 智能容错

  • MultiEdit 支持部分失败
  • 详细的错误信息
  • 失败编辑的统计报告

4. 跨平台兼容

  • 智能行结束符处理
  • 路径规范化
  • 跨平台文件系统兼容

5. LSP 深度集成

  • 自动诊断收集
  • 实时错误反馈
  • 多语言支持

6. 完整的可追溯性

  • 版本历史记录
  • Diff 生成
  • 详细的元数据

7. 优秀的可扩展性

  • 依赖注入设计
  • Fantasy 框架集成
  • 模块化架构

性能考量

1. 内存管理

// 大文件处理限制
const MaxFileSize = 250 * 1024 // 250KB

if fileInfo.Size() > MaxFileSize {
    return error("File too large")
}

2. 字符串操作优化

使用 Go 标准库的高效字符串函数:

  • strings.Index() - O(n*m) 查找
  • strings.ReplaceAll() - 单次遍历替换
  • strings.Builder - 高效字符串拼接

3. 并发安全

// 使用并发安全的 Map
type Map[K comparable, V any] struct {
    mu sync.RWMutex
    m  map[K]V
}

未来改进方向

看完之后感受还是挺强烈的,Crush把这个工具封装的思路简单到让我震惊,我还思考了很久他们是怎么做出左右拆分的git对比 但是一旦你接受了这个思路,它的实现几乎是天然的融入到了渲染和实际执行中,抽象概念完全对齐,太完美了。单即便如此我还是觉得有额外的优化空间。 起码我在做实现的时候可以再多做一点点更好的。以下是我和AI共同整理的一些思路,先记下,回头实现的时候可以捡起来。

1. 增量编辑支持

当前实现需要读取整个文件,对于大文件可以考虑:

  • 行号定位编辑
  • 字节偏移量编辑
  • 流式处理

2. 并行批量编辑

MultiEdit 当前是顺序执行,可以考虑:

  • 依赖分析
  • 无冲突编辑并行执行
  • 冲突检测和解决

3. 更智能的冲突处理

  • 三方合并算法
  • 自动冲突解决
  • 交互式冲突解决

4. 性能优化

  • 增量 Diff 算法
  • 文件内容缓存
  • LSP 诊断缓存

总结

Crush 的代码修改工具展示了一个生产级 AI 编程助手应该具备的核心特性:

  1. 安全性:多层验证确保不会误修改文件
  2. 可靠性:完整的错误处理和历史追踪
  3. 易用性:简洁的 API 和清晰的错误信息
  4. 可扩展性:模块化设计便于添加新功能
  5. 智能性:LSP 集成提供实时反馈

这些设计原则和实现细节为我构建 ARS 代码助手提供了宝贵的参考。无论是开发类似的工具,还是理解 AI 如何安全地操作代码,Crush 的实现都值得深入学习。

参考资源


本文分析基于 Crush 代码库当前版本(v0.21.0),部分实现细节可能随版本更新而变化。

Tags: