在 Cursor 或 Claude Code 叫 AI 把專案裡的 User 改名成 Client,它回報改完了,跑 yarn dev 卻一片紅字。常見的是這三種:
有個檔案的 import { User } 沒改到。 註解 // notify the User 也被改成 Client,但那裡的 User 只是一般英文。 名字裡剛好有 User 的 UserCard 元件被一起改名,引用它的檔案全部壞掉。 三個錯的原因一樣:AI 用字串搜尋取代來改名。改名要改的是程式裡的 symbol,哪些地方引用了它,要靠語法分析才找得準,光比對文字做不到。
上一篇講 Bug Fix Paradox ,這篇講重構:為什麼改名、搬檔要交給 LSP 或 AST 工具,以及在 Claude Code 裡怎麼下指令。
在探討怎麼叫 AI 做事前,我們要先理解為什麼傳統的 Find & Replace(也就是預設 AI 幫你改程式碼的方式)一定會出包。
重構是一件要找出 AST(抽象語法樹)關聯 的精確工程。你「只改一個名字」或「搬移一個檔案」,背後就牽扯到 imports 和 exports、linter 規則,還有 module resolution。
文字替換只認得字串,LSP rename 認得 symbol,所以同一個專案裡的 User,兩種改法會改到不同的地方。
User → Client 文字替換 LSP rename
models/user.ts · 定義 改對 改對 export interface Client { export interface Client { api.ts · 引用 改對 改對 import { Client } from './models/user' import { Client } from './models/user' billing.ts · 引用 漏改 改對 import { User } from './models/user' import { Client } from './models/user' logger.ts · 註解 誤改 正確不動 // notify the Client // notify the User Profile.tsx · 畫面文字 誤改 正確不動 <h2>Client 資料</h2> <h2>User 資料</h2> UserCard.tsx · 別的名稱 誤改 正確不動 export function ClientCard() { export function UserCard() {
文字替換在做什麼
AI 用 grep 找出字串 User,一個檔案一個檔案改。它只認得字,不知道哪個 User 是型別、哪個只是剛好同名的文字。
點任一列,看它為什麼被改或沒被改。
原本
export interface User {
文字替換之後
export interface Client {
字串剛好就是型別名稱,改對。
清除選取 原本
import { User } from './models/user'
文字替換之後
import { Client } from './models/user'
grep 有找到這個檔案,改對。
清除選取 原本
import { User } from './models/user'
文字替換之後
import { User } from './models/user'
結果太多,AI 沒改到這個檔案。回報「修改完成」,build 才報錯。
清除選取 原本
// notify the User
文字替換之後
// notify the Client
註解裡的 User 是一般英文,跟型別無關,被一起換掉。
清除選取 原本
<h2>User 資料</h2>
文字替換之後
<h2>Client 資料</h2>
使用者看到的文字被改了,測試通常也抓不到。
清除選取 原本
export function UserCard() {
文字替換之後
export function ClientCard() {
UserCard 是另一個元件,只是名字裡有 User。改了之後,import 它的地方全壞。
清除選取 LSP rename在做什麼
跟在 VS Code 按 F2 一樣:language server 已經把專案解析成語法樹,知道 interface User 定義在哪、被誰引用,只改這些地方。
點任一列,看它為什麼被改或沒被改。
原本
export interface User {
LSP rename之後
export interface Client {
rename 的起點,定義本身。
清除選取 原本
import { User } from './models/user'
LSP rename之後
import { Client } from './models/user'
language server 記得這個 import 指向同一個型別。
清除選取 原本
import { User } from './models/user'
LSP rename之後
import { Client } from './models/user'
引用清單來自語法樹,不靠 AI 自己記得改哪些檔。
清除選取 原本
// notify the User
LSP rename之後
// notify the User
註解不是 symbol,rename 不會碰。
清除選取 原本
<h2>User 資料</h2>
LSP rename之後
<h2>User 資料</h2>
字串內容不是 symbol,維持原樣。
清除選取 原本
export function UserCard() {
LSP rename之後
export function UserCard() {
不同的 symbol,不在這次 rename 範圍。
清除選取 你叫 AI 改名,它(如果沒有配備好工具)就是拿著你給的字串,用正規表示式去整個專案裡撈。這種「純文字替換」沒有上下文觀念,它不知道哪個 User 是一個 interface,哪個 User 只是一個恰巧同名的變數。所以誤改 (改錯註解)跟漏改 (沒改到動態 import)幾乎是必然的。
如果你平時用 VSCode,在一個變數上按下 F2 (重新命名),IDE 是不會猜測的。它透過 LSP (Language Server Protocol) 去問 language server。language server 早就把你的檔案解析成語法樹並建好索引,它知道這個 User 的定義在哪,被哪 15 個檔案引用。
重構要照這份索引找位置,不該交給只會文字接龍的 LLM 去猜。
既然知道了問題出在哪,我們該怎麼解決?身為開發者,你需要幫 AI 準備對的工具。
目前有兩種工具:LSP 與 AST CLI 工具 。很多開發者會把它們搞混,認為只要是操作 AST 就好。
該用哪種工具,看改動的性質:IDE 內建的重構動作交給 LSP,同一種轉換要套到大量檔案時用 AST codemod,跟 symbol 無關的內容才直接改文字。
這個重構交給誰 LSP AST codemod 直接文字編輯
rename symbol authHelper → securityCore 首選 可行 不適合 搬移檔案 Auth.tsx → shared/ 首選 可行 不適合 extract function 把一段邏輯抽成共用函式 首選 不適合 可行 class → hooks React Class Component 全部改成 Hooks 不適合 首選 可行 換函式庫 moment → dayjs 不適合 首選 不適合 有 breaking change 的升版 框架或套件升 major 版 不適合 首選 不適合 typo、markdown、設定檔 文案錯字、README、config 值 不適合 不適合 首選
LSP在做什麼
language server 已經把整個專案解析成語法樹。rename、搬檔、extract 這類 IDE 內建的重構動作,它會回一份 WorkspaceEdit,列出每個檔案要改的位置。
點任一列,看該怎麼跟 agent 交代。
rename symbol
用 LSP
首選 跟按 F2 同一套機制,定義和所有引用一次改到。
跟 agent 這樣說
用有 rename 能力的工具改名(IDE、agent 的 rename 工具或專案的 refactor CLI),不要用 sed 或逐檔手改。改完跑 tsc 和 lint。
清除選取 搬移檔案
用 LSP
首選 搬檔時 language server 會算出所有要跟著改的 import 路徑。
跟 agent 這樣說
用 IDE 的搬檔功能搬,讓它順便更新 import 路徑。搬完跑 tsc,確認沒有斷掉的 import。
清除選取 extract function
用 LSP
首選 language server 的 refactor code action 會算好參數和回傳值。
跟 agent 這樣說
用 extract function 的 code action 抽出來,不要重寫那段內容。
清除選取 class → hooks
用 LSP
不適合 IDE 沒有這種重構按鈕,LSP 做不到。
跟 agent 這樣說
先寫一條 codemod 規則,在兩三個檔案上試跑給我看 diff,確認後再套到全專案。
清除選取 換函式庫
用 LSP
不適合 這是換 API,不是改名,LSP 沒有對應的動作。
跟 agent 這樣說
寫 codemod 把 moment 的呼叫換成 dayjs,對不上的 API 列出來給我,不要自己猜。
清除選取 有 breaking change 的升版
用 LSP
不適合 LSP 不知道新版改了什麼。
跟 agent 這樣說
先查這個套件有沒有官方 codemod,有就跑官方的;沒有再寫規則。跑完對照 changelog 逐條確認。
清除選取 typo、markdown、設定檔
用 LSP
不適合 這些不是程式 symbol,LSP 用不上。
跟 agent 這樣說
直接改,改完給我看 diff。
清除選取 AST codemod在做什麼
用 ast-grep 或 jscodeshift 寫規則,照語法結構比對再改寫。適合 IDE 沒有現成按鈕、同一種改法又要套到很多地方的轉換。
點任一列,看該怎麼跟 agent 交代。
rename symbol
用 AST codemod
可行 做得到,但要自己處理作用域,才不會改到同名的其他變數。
跟 agent 這樣說
用有 rename 能力的工具改名(IDE、agent 的 rename 工具或專案的 refactor CLI),不要用 sed 或逐檔手改。改完跑 tsc 和 lint。
清除選取 搬移檔案
用 AST codemod
可行 可以寫規則改 import 路徑,但 IDE 已經內建了。
跟 agent 這樣說
用 IDE 的搬檔功能搬,讓它順便更新 import 路徑。搬完跑 tsc,確認沒有斷掉的 import。
清除選取 extract function
用 AST codemod
不適合 只抽一處,寫規則比直接做還費工。
跟 agent 這樣說
用 extract function 的 code action 抽出來,不要重寫那段內容。
清除選取 class → hooks
用 AST codemod
首選 同一種結構轉換要套到大量元件,正是 codemod 的用途。
跟 agent 這樣說
先寫一條 codemod 規則,在兩三個檔案上試跑給我看 diff,確認後再套到全專案。
清除選取 換函式庫
用 AST codemod
首選 呼叫的形狀固定,適合照語法結構一條一條換。
跟 agent 這樣說
寫 codemod 把 moment 的呼叫換成 dayjs,對不上的 API 列出來給我,不要自己猜。
清除選取 有 breaking change 的升版
用 AST codemod
首選 很多框架升版會附官方 codemod,先用現成的。
跟 agent 這樣說
先查這個套件有沒有官方 codemod,有就跑官方的;沒有再寫規則。跑完對照 changelog 逐條確認。
清除選取 typo、markdown、設定檔
用 AST codemod
不適合 寫規則比直接改還久。
跟 agent 這樣說
直接改,改完給我看 diff。
清除選取 直接文字編輯在做什麼
agent 自己讀檔、改字。跟程式語意無關的內容這樣最快;牽涉到 symbol 的改動,就容易誤改或漏改。
點任一列,看該怎麼跟 agent 交代。
rename symbol
用 直接文字編輯
不適合 分不出哪個是同一個 symbol,註解和字串會被誤改。
跟 agent 這樣說
用有 rename 能力的工具改名(IDE、agent 的 rename 工具或專案的 refactor CLI),不要用 sed 或逐檔手改。改完跑 tsc 和 lint。
清除選取 搬移檔案
用 直接文字編輯
不適合 相對路徑一個一個算,檔案一多就漏。
跟 agent 這樣說
用 IDE 的搬檔功能搬,讓它順便更新 import 路徑。搬完跑 tsc,確認沒有斷掉的 import。
清除選取 extract function
用 直接文字編輯
可行 可以,但參數和回傳值要自己想對,改完一定要跑測試。
跟 agent 這樣說
用 extract function 的 code action 抽出來,不要重寫那段內容。
清除選取 class → hooks
用 直接文字編輯
可行 只有幾個元件時可以逐檔改,每改一個就跑一次測試。
跟 agent 這樣說
先寫一條 codemod 規則,在兩三個檔案上試跑給我看 diff,確認後再套到全專案。
清除選取 換函式庫
用 直接文字編輯
不適合 兩邊 API 不完全對應,用字串取代會改出錯的呼叫。
跟 agent 這樣說
寫 codemod 把 moment 的呼叫換成 dayjs,對不上的 API 列出來給我,不要自己猜。
清除選取 有 breaking change 的升版
用 直接文字編輯
不適合 改動散在各處,逐檔手改很難確定有沒有漏。
跟 agent 這樣說
先查這個套件有沒有官方 codemod,有就跑官方的;沒有再寫規則。跑完對照 changelog 逐條確認。
清除選取 typo、markdown、設定檔
用 直接文字編輯
首選 跟語意無關的內容,直接改最快。
跟 agent 這樣說
直接改,改完給我看 diff。
清除選取 LSP 負責大部分日常重構 :如果你只是要重新命名檔案/符號、搬移資料夾、把一段 code 提取成共用函式,直接交給 LSP 就好。它已經建好整個專案的索引,跟你手動按 F2 是同一套機制。AST 工具負責大規模客製重構 :如果你遇到 LSP 預設按鈕沒有的功能,比如「把整個專案的 React Class Component 全部轉換成 Hooks」,LSP 辦不到。這時候才需要讓 AI 用 ast-grep 或 jscodeshift 寫腳本轉換。在終端機用 Claude Code 時,prompt 要換個寫法。
❌ 容易出事的 Prompt:
「幫我把整個專案的 authHelper 改名成 securityCore,還有幫我把 Auth.tsx 移到 shared 資料夾。」 (結果:Claude 會開始生成 bash 腳本,用 sed 指令或是手動幫你改檔,跑完後常常一堆破圖跟路徑錯誤。)
✅ Agentic Workflow 的正確 Prompt:
「我要把 authHelper 重命名為 securityCore。請你不要手動去改文字 ,先用 LSP 找出 authHelper 所有的 references 列給我,再用當前專案的 refactor CLI 或 codemod 執行改名。改完後請跑 npx tsc --noEmit 和 yarn lint,確保所有的 import 都有被更新。」
Claude Code 內建的 LSP 工具只能查詢(跳到定義、找 references、看型別),沒有 rename,所以這個 prompt 讓它用 LSP 盤點位置,實際改名交給 CLI。如果你使用的環境已經把 IDE 的 rename API 包成 agent 工具,AI 就能自己去「按 F2」。Kiro 就是這樣做:它的 semantic rename 工具呼叫 VS Code 的 vscode.executeDocumentRenameProvider,搬檔工具則用 WorkspaceEdit.renameFile 順便更新 import。
如果你的 CLI 環境沒有串接 LSP,你可以直接指使 Claude Code 去跑你設定好的 eslint --fix 或是你寫好的 ast-grep rule。 讓 AI 下指令給驗證過的底層工具,由工具去改 code。
一個設計優良的代理(Agent)工作流,不應該是什麼都叫 LLM 自己硬刻。
一次安全的 rename 裡,agent 負責傳話和驗收,要改哪幾行由 language server 算出來。
Rename 委派 · authHelper → securityCore
「把 authHelper 改名成 securityCore」
已把 authHelper 改成 securityCore:4 個檔案、9 處。tsc 與 lint 都通過。
01 / 06 給意圖
你只說要改什麼。改哪幾個檔案、哪幾行,不用 agent 自己去找。
user: 把 authHelper 改名成 securityCore
02 / 06 請求改名
agent 指出 authHelper 定義的位置和新名字,交給 language server。這一步需要 agent 手上有 rename 工具,例如 Kiro 的 semantic rename。
{ "method": "textDocument/rename", "params": { "textDocument": { "uri": "file:///src/auth/authHelper.ts" }, "position": { "line": 3, "character": 16 }, "newName": "securityCore" } }
03 / 06 回傳 WorkspaceEdit
language server 從語法樹找出定義和所有引用,回一份清單:哪個檔案、哪個範圍、換成什麼字。註解和字串不在裡面。
{ "result": { "changes": { "file:///src/auth/authHelper.ts": [ { "range": { … }, "newText": "securityCore" } ], "file:///src/api.ts": [ … ], … } } }
04 / 06 套用修改
agent 照 WorkspaceEdit 寫進檔案。每一處的位置都是 language server 算好的,agent 不自己搜尋、不自己決定改哪裡。
edit src/auth/authHelper.ts · src/api.ts · src/billing.ts · src/routes.ts
05 / 06 驗證
改完跑型別檢查和 lint。有漏改的 import,這一步會直接報錯。
$ npx tsc --noEmit && yarn lint
0 errors
06 / 06 回報
agent 回報改了哪些檔案、驗證結果是什麼。你看 diff 決定要不要收。
assistant: 4 個檔案、9 處已更新,tsc 與 lint 通過。
一個重構回合裡,三方各做一件事。
開發者 (你) :給出重構意圖(我要重組架構、改名)。AI Agent (Claude Code) :負責理解意圖 與統籌流程 。它知道要改名,但不自己動手改字。AST Tool / LSP :負責實際的修改 。AI 將任務委託給它,由它在語法樹上找到節點,一次更新全部檔案裡的引用與路徑。AI 負責下指令和檢查結果,實際改檔交給工具。把「創意與意圖」交給 AI,把「精確與結構」交給編輯器底層工具,AI 做重構才安全。