VSCode 插件 Cline 拆解(1)

733 阅读7分钟

Cline 插件

ClineVS Code Extension marketplace上爆火,在OpenRouter上排名第一;当然,其实排第二也是他,不过是他的变种,叫Roo Cline

Cline在2024年6月出道的时候,也是蹭了Claude热度的,当时叫Claude Dev,起飞之后改名Cline

Cline是怎么设计的

Cline是怎么设计的?这是个很大的问题。如果从代码架构出发,那么这会变得很复杂;况且本身Cline也从版本1到版本3发生过重大的重构,当然版本迭代快了,以后架构也会变;甚至于现在prompt流式处理上来说,也会随着LLM的变化而变化。

废话不多说。从使用的角度来观察这个插件,就是一个prompt流式处理过程。

设计

设计一个最简单的todolist的node.js应用,要求有增删改查功能,可以先用jsonl作为数据的保存。

拆解入手

首先,需要安装cline这个插件,并且配置好对应的LLM,实现正常的交互。 如今Cline已经实现了PlanAct两种模式。两个模式可以切换不同的模型,最近很火的组合就是使用DeepSeek-R1PM,而Claude-3.5充当Developer。在这个模式下就支持我们切换。 在plan模式下,输入需求,设计一个最简单的todolist的后端应用,有最简单的增删改查的能力。

第一次

在cline的历史记录中,可以查看每一步的prompt详情。 image.png

点击导出并保存后,就可以看到请求的prompt具体内容,这是一个markdown文件,内容大致为:

**User:**

<task>
Design a simple TodoList Node.js application with CRUD (Create, Read, Update, Delete) functionality, using JSONL as the data storage format.
</task>
<environment_details>
# VSCode Visible Files
(No visible files)

# VSCode Open Tabs
(No open tabs)

# Current Time
2/18/2025, 0:00:00 PM (Asia/Shenzhen, UTC+8:00)

# Current Working Directory (pwd) Files
No files found.

# Current Mode
PLAN MODE
……
</environment_details>
**Assistant:**

<thinking>
To design a simple TodoList Node.js application with CRUD functionality using JSONL (JSON Lines) as the data storage format, **I need to gather some details and then architect a solution. JSONL is a format where each line in a file is a separate JSON object, making it suitable for appending and reading individual records efficiently.**

The application will likely include:
1. **Core Features**: ……

2. **Structure**: ……

3. **Implementation Details**: ……

4. **Questions**: ……

Once I have these details, I can create a detailed plan for the application.
</thinking>

# Next Steps

If you have completed the user's task, use the attempt_completion tool. 
If you require additional information from the user, use the ask_followup_question tool. 
Otherwise, if you have not completed the task and do not need additional information, then proceed with the next step of the task. 
(This is an automated message, so do not respond to it conversationally.)

---

**Assistant:**

<ask_followup_question>
<question>
Should the TodoList application be a RESTful API (accessible via HTTP endpoints) or a CLI-based tool? Additionally, do you want any extra features like filtering/searching todos or authentication for the API?
</question>
</ask_followup_question>

以上便是Plan模式下的prompt内容。 这个时候,我们可以直接从Plan切换到Act模式,那么cline就会直接帮我们生成对应代码。

代码生成

将cline从Plan切换到Act,回车之后就会直接生成项目代码。用户可以选择Auto-approve,这样就省去很多的pending,代码会生成对应的文件app.js。目前使用了VSCode内置的LLM模型claude-3.5后,app.js中使用express创建了这个简单的后端服务。并生成了对应的增删改查的api。

查看第二次的prompt

我们再次将第二次运行的prompt导出后,会看到原封不动的第一次内容,以及切换成Act之后的第二次内容。

// 续上面第一次的内容
----------
**User:**

[TASK RESUMPTION] This task was interrupted 9 minutes ago. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now 'pwd'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.

Note: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.
<environment_details>
......
# Current Mode
ACT MODE
</environment_details>

// 根据用户输入来生成code
---
**Assistant:**
<thinking>
The task was interrupted after I asked a question about whether the TodoList application should be a RESTful API or a CLI-based tool, and whether the user wants any extra features. Now the mode has been switched to ACT MODE, so I should proceed with implementing the application based on reasonable defaults. I'll assume the user wants a RESTful API without authentication or extra features, as this is a simple starting point.

Here's the plan:
1.  **Create `app.js`**: This will be the main entry point for the Express application.
2.  **Create `todos.jsonl`**: This file will store the todo items in JSONL format.
3.  **Implement CRUD operations in `app.js`**:
    *   `Create`: POST endpoint to add a new todo.
    *   `Read`: GET endpoint to list all todos.
    *   `Update`: PUT endpoint to modify an existing todo.
    *   `Delete`: DELETE endpoint to remove a todo.

I'll start by creating the `app.js` file with the basic Express setup.
</thinking>
```xml
<write_to_file>
<path>app.js</path>
<content>
    ......
</content>
</write_to_file>
---
// 确认用户保存
**User:**
[write_to_file for 'app.js'] Result:
The content was successfully saved to app.js.
Here is the full, updated content of the file that was saved:
<final_file_content path="app.js">
……
</final_file_content>
IMPORTANT: For any future changes to this file, use the final_file_content shown above as your reference. This content reflects the current state of the file, including any auto-formatting (e.g., if you used single quotes but the formatter converted them to double quotes). Always base your SEARCH/REPLACE operations on this final version to ensure accuracy.

<environment_details>
# Current Mode
ACT MODE
</environment_details>

---

**Assistant:**

<thinking>
The `app.js` file was created successfully. Now I need to create the `todos.jsonl` file. This file will initially be empty.
</thinking>

```xml
<write_to_file>
<path>todos.jsonl</path>
<content>
......
</content>
</write_to_file>
---

**User:**
<environment_details>
# Current Mode
ACT MODE
</environment_details>
---

**Assistant:**

<thinking>
The `todos.jsonl` file was created successfully. Now that the application files are set up, I need to install the `express` package.
</thinking>
```xml
<execute_command>
<command>npm install express</command>
<requires_approval>true</requires_approval>
</execute_command>

从第二次生成的内容看到,其实Cline会把之前的Plan都发送给LLM(是的,非常消耗token,推荐Roo Cline)

在第二次的prompt流中,我们看到其实给了四次调用,都是以Act模式的Assistant角色。第一次是拆解需求,生成对应的code;第二次是将code写入文件;第三次是生成对应的jsonl文件,最后考虑工程化的构建。

prompt怎么来的

我们可以看到第一次和第二的prompt在格式上都非常一致,那么必然有个prompt模板,这个模板在core/prompts/system.ts中,你会看到一堆引用的文字,我们将这部分文字对应生成一个markdown文件,并去除符号处理。

You are Cline, a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.
====
TOOL USE

You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.

# Tool Use Formatting

Tool use is formatted using XML-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags. Here's the structure:

<tool_name>
<parameter1_name>value1</parameter1_name>
<parameter2_name>value2</parameter2_name>
...
</tool_name>

For example:
.......

# Tools

## execute_command
Description:......
Parameters:.......
Usage:
<execute_command>
<command>Your command here</command>
<requires_approval>true or false</requires_approval>
</execute_command>

## read_file
Description:....
Parameters:
Usage:
<read_file>
<path>File path here</path>
</read_file>

## replace_in_file
Description:……
Parameters:
- path:(required)....
- diff: (required)....
Usage:
<replace_in_file>
<path>File path here</path>
<diff>
Search and replace blocks here
</diff>
</replace_in_file>
// 内容过多,忽略

从这份模板可以看到,Cline用的XML风格的工具调用模式,这里面明确了调用命名和参数要求,省略的后续部分中还有完整的错误处理机制。

代码调用

既然在prompt中定义了代码调用,比如replace_in_file,那Cline是怎么处理的呢?

  1. 在prompt定义replace_in_file,并指定了parameters中有2个参数,path和diff。path就是带路径的文件名,diff就是文件差异部分。
  2. Cline在获取LLM的response后,解析关键词replace_in_file和参数pathdiff,并验证path,处理diff中的特殊字符。
private async presentAssistantMessage() {
  switch (block.type) {
    case "tool_use":
      switch (block.name) {
        case "replace_in_file": {
          try {
            // 1. 构建新内容
            let newContent: string
            if (diff) {
              // 2. 处理特殊字符
              if (!this.api.getModel().id.includes("claude")) {
                diff = fixModelHtmlEscaping(diff)
                diff = removeInvalidChars(diff)
              }

              // 3. 确保编辑器已打开
              if (!this.diffViewProvider.isEditing) {
                await this.diffViewProvider.open(relPath)
              }

              // 4. 使用 diff 构建新内容
              newContent = await constructNewFileContent(
                diff,
                this.diffViewProvider.originalContent || "",
                !block.partial
              )
            }
            
            // 5. 更新编辑器内容
            await this.diffViewProvider.update(newContent, true)
            
            // 6. 处理保存和后续操作
            const { newProblemsMessage, userEdits, autoFormattingEdits, finalContent } = 
                await this.diffViewProvider.saveChanges()
          } catch (error) {
            // 错误处理
          }
        }
      }
  }
}      
  1. 在 DiffViewProvider 中的执行保存操作,以及显示差异部分。
export class DiffViewProvider {
  async saveChanges() {
    // 1. 获取保存前的内容
    const preSaveContent = updatedDocument.getText()
    
    // 2. 保存文件
    await updatedDocument.save()
    
    // 3. 获取保存后的内容(可能包含自动格式化的改动)
    const postSaveContent = updatedDocument.getText()
    
    // 4. 统一行尾处理
    const normalizedPreSaveContent = this.normalizeContent(preSaveContent)
    const normalizedPostSaveContent = this.normalizeContent(postSaveContent)
    
    // 5. 检查用户编辑
    let userEdits = this.checkUserEdits(normalizedPreSaveContent)
    
    // 6. 检查自动格式化
    let autoFormattingEdits = this.checkAutoFormatting(normalizedPreSaveContent, normalizedPostSaveContent)
    
    return {
      newProblemsMessage,
      userEdits,
      autoFormattingEdits,
      finalContent: normalizedPostSaveContent
    }
  }
}

调用完成

以上就是Cline实现了diff文件调用的一个action,其他的action也是。这里仅仅只是介绍了调用链,在diff中还有几个技术细节,包括差异块代码,内容匹配,安全处理机制,后处理。等等。

当然,Cline中不止定义了Action,还有其他的很多功能。

End

先讲到这里吧,大家感兴趣的话,后面再继续拆解~