從零手把手教你寫一個簡易版 Claude Code:基礎篇

作者:ITPostman
日期:2026年4月24日 上午3:11
來源:WeChat 原文

整理版優先睇

速讀 5 個重點 高亮

前言: 最近 Claude Code 源代碼泄露,全網都在扒它的代碼設計思想。但直接啃源碼對初學者不友好,十幾萬行、近兩千個文件,一上來被各種設計模式淹死,看完只記得幾個名詞。其實 Claude Code 的內核極其簡單,簡單到可以濃縮成一句話——不過這句話我放文章最後講。本文的目標:用 100 行 Python 復刻出這個內核,讓你看完就能跑起來、改得動。至於編輯文件、搜索、子 Agent 這些外圍功能,會在我的 my-claude-code 倉庫裏給到完整實現,有興趣自己翻。今天只是開篇,後續我還會寫系列文章,來解讀 Claude Code 的設計細節,和它是怎麼實現各種功能的。一、先看效果Claude Code 效果圖讓我寫的 my-claude-code 寫了一個的貪吃蛇:用 my-claude-code 搓貪吃蛇就一句「幫我寫一個貪吃蛇網頁版小遊戲」,它自己決定建哪些文件、每個文件寫什麼、跑起來報錯了自己回頭改。全程我一行代碼都沒碰過。後面的章節會告訴你——讓它跑成這樣的核心,到底是什麼。再來一張讓它總結指定網頁內容的效果圖:總結網頁內容當然,還有很多功能。子 Agent、Memory、Hooks、edit_file / grep / web_fetch 等完整工具箱,以及 /compact、/resume、/clear 這些 slash 命令——整體架構和 Claude Code 已經很接近了,實現細節當然有出入,但思路一脈相承。但這些不是今天的重點。完整功能實現會在後續文章中詳細介紹。今天我們先把核心的循環跑起來,這個循環才是 Agent 的真正內核。二、什麼是 Agent?假設我們需要完成這樣一個任務:任務: 幫我搜索今天 Hacker News 的熱點新聞。如果我們直接把這個任務交給一個語言模型,可能會得到這樣的回答:回答: 抱歉,我無法直接訪問互聯網來搜索最新的 Hacker News 熱點新聞。為什麼會這樣呢?因為語言模型本身沒有訪問互聯網的能力。而且,它也不知道今天是什麼日期,它只能根據訓練數據來生成回答,而不能執行實際的操作。這時候,我們就需要一箇中間層,來幫助語言模型執行實際的操作。這個中間層就是 Agent。你可以把 Agent 想象成一個智能的指揮官,它負責協調各種工具來完成用戶的任務。它會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。在上面的例子中,我們看看 Agent 是怎麼工作的:當你向 Agent 輸入指令:“幫我搜索今天 Hacker News 的熱點新聞” 時,Agent 並沒有立刻把這句話發給大模型,而是先在後台做了一次拼上下文。它會在後台構建一個龐大的上下文環境(Context),你可以把它理解為給大模型的輸入數據。這個上下文環境裏包含了系統設定、工具箱、用戶指令等等信息。發給大模型的完整數據大概長這樣:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞大模型在接收到這個上下文環境後,會根據系統設定和工具箱的信息,來理解用戶的指令,並且選擇合適的工具來執行。在這個例子中:第一輪大模型會先推理(Think)分析用戶的指令,發現了一個變量——“今天”。它心想:“作為語言模型,我沒有內置時間,但我看到了我的工具箱裏有一個叫 get_current_date() 的工具,我正好需要它!”下達行動指令 (Act): 於是,大模型暫停了文本回復,而是向 Agent 返回了一段標準的 JSON 格式指令,要求執行工具調用:{ "tool": "get_current_date", "args": {}}框架執行 (Verify/Observe):Agent 接收到這個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 get_current_date() 函數來獲取當前日期。工具執行完成後,會把結果返回給 Agent。比如,get_current_date() 可能返回了“2026-04-20”。Agent 默默把這個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞[工具調用記錄]get_current_date() → 2026-04-20第二輪大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了。大模型結合新的上下文,確認了目標:“好,現在我知道要找 2026 年 4 月 20 日的 HN 新聞了。下一步,我需要調用搜索(search_web)工具下達行動指令 (Act):於是,大模型再次向 Agent 框架返回了一段標準的 JSON 格式指令,要求執行工具調用:{ "tool": "search_web", "args": { "query": "2026-04-20 Hacker News 熱點新聞" }}框架執行 (Verify/Observe):Agent 接收到這個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 search_web("2026-04-20 Hacker News 熱點新聞") 函數來搜索互聯網內容。工具執行完成後,會把結果返回給 Agent。比如,search_web() 可能返回了一個包含熱點新聞標題和連結的列表。Agent 把這個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞[工具調用記錄]get_current_date() → 2026-04-20search_web("2026-04-20 Hacker News 熱點新聞") →[ {"rank": 1, "title": "XXX", "url": "YYY", "points": 842, "comments": 267, "domain": "...", "posted_ago": "7h", "snippet": "一大段原文摘錄..."}, {"rank": 2, "title": "AAA", "url": "BBB", "points": 613, "comments": 189, "domain": "...", "posted_ago": "5h", "snippet": "..."}, {"rank": 3, "title": "MMM", "url": "NNN", "points": 402, ...}, ... 另外 27 條結果,摻着廣告、轉帖、無關外鏈 ...]注意看這兩段「上下文快照」的變化——[用戶指令] 從頭到尾是同一句「幫我搜索今天 Hacker News 的熱點新聞」,沒有被改寫過。日期和搜索結果都是往下追加的新條目。這是 ReAct 的一個硬性契約:歷史只讀、只增不改。模型是在讀完整條累加下來的歷史之後、自己在腦子裏把「今天 ≈ 2026-04-20」連起來的,而不是某個環節把用戶原話替換掉了。這個不變量後面 3.4 的代碼裏還會再遇到一次——全程 messages.append,沒有任何地方動過舊消息。第三輪大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了,也拿到了搜索結果了。大模型結合新的上下文,確認了目標:“好,30 條原始數據在手了。下一步,我要從這堆 JSON 裏挑出熱度最高的幾條、扔掉 rank/points/comments/snippet 這些用戶不關心的字段,用人話重新排版發回去。”大模型生成回答 (Respond):於是,大模型根據新的上下文環境,把原始搜索結果過濾、去噪、重排後,生成了一個乾淨的回答:2026-04-20 Hacker News 熱度最高的兩條:1. 標題:XXX,連結:YYY(842 分 / 267 評論)2. 標題:AAA,連結:BBB(613 分 / 189 評論)Agent 把這個回答發回給用戶,完成了整個任務。看完了上面這個 Hacker News 的抓取例子,那麼恭喜你——你已經掌握了當下幾乎所有 AI 編程工具的底層核心邏輯。無論是 Claude Code、Cursor、opencode、Cline,還是 OpenAI 的 Codex CLI、Google 的 Gemini CLI,它們在最底層跑的都是同一個閉環:ReAct(Reason + Act),也就是“感知-思考-行動-驗證”四步循環。你會發現,大模型(大腦)和 Agent 框架(外骨骼)在這裏面有着極其明確的分工:感知 (Perceive): 不僅僅是“聽指令”,更是“讀環境”。大模型在每一步都在感知當前的上下文環境,包括用戶指令、工具箱信息、系統設定、工具調用結果等等。它需要不斷地更新自己的認知模型,來理解當前的任務和目標。思考 (Think): 這是大模型的“主場”。它在每一步都在做邏輯判斷——“我缺時間”、“我缺聯網能力”、“數據拿到了可以排版了”。它清楚自己的邊界在哪裏。行動 (Act): 當大模型發現自己做不到時,它會輸出調用工具的指令(Tool Calling)。讓外圍的 Agent 真正去執行 get_date 或 search_web。驗證 (Verify): 工具調完了沒算完,Agent 必須把調用的結果(無論成功還是報錯)拿回來驗證。就像查日期那一步,有了真實日期作為驗證結果,循環才能繼續推進。這個循環不斷地進行,直到大模型生成了一個完整的回答,或者達到了某個終止條件。這個過程就是 Agent 的核心工作原理,也是 Claude Code 的核心設計思路。三、手搓 Claude Code接下來,我會一步步帶你手搓一個簡易版的 Claude Code。但我不打算從零講到一個功能完整的版本——Claude Code 源碼泄露出來就十幾萬行、接近兩千個文件,我自己寫的這個簡化版也有將近三千行,文章裏塞不下,你也沒耐心看。所以這一章的目標只有一個:用 100 行代碼復現第二章那個 ReAct 循環。只做工具調用,別的什麼都不做。等你真的跑起來一次,後面再去看 Claude Code 源碼、或者我倉庫裏那個稍微完整點的版本,就沒什麼障礙了。完整代碼在倉庫的 mini.py 裏,100 行出頭,文末給連結。下面一段一段拆。3.1 先把環境裝好一個 Python 環境,一個 OpenAI SDK,一個模型的 API key。就這三樣。等等——Claude Code 不是 Anthropic 家的嗎,怎麼用 OpenAI SDK? 因為 OpenAI 的 chat.completions 早就是工具調用事實上的行業標準,DeepSeek、GLM、Kimi、本機 Ollama 全都兼容它,換模型只改 base_url 就行。真想用 Anthropic 原生 API 也可以,把 openai 換成 anthropic 包就是——ReAct 循環的邏輯一字不變,變的只是 SDK 怎麼調。本文只是挑了最通用的那條路。pip install openai這裏我用的是 DeepSeek,便宜,跑工具調用夠用。你想用 OpenAI 官方、通義、或者本機 Ollama 都行——只要它兼容 OpenAI 的 chat.completions 接口,改個 base_url 就能切。from openai import OpenAIclient = OpenAI( api_key="sk-xxx", base_url="https://api.deepseek.com",)MODEL = "deepseek-chat"3.2 先搭一個最普通的對話 loop加工具之前,我們先寫個最普通的命令行 chatbot。這一步和 Agent 半毛錢關係都沒有,但它是後面一切東西的骨架。messages = [{"role": "system", "content": "你是一個編程助手。"}]while True: user = input("> ") messages.append({"role": "user", "content": user}) resp = client.chat.completions.create(model=MODEL, messages=messages) reply = resp.choices[0].message.content messages.append({"role": "assistant", "content": reply}) print(reply)這時候你跑起來,和它說"你好",它會回你"你好"。但你問它"我的 config.json 裏寫了啥"——它會回你"抱歉,我無法直接訪問你的文件系統來讀取 config.json 的內容。"。這時候你就知道了——它只能說,不能做。因為模型沒有"手"。它看不到你的文件,也跑不了你的命令。我們接下來要做的,就是給它裝一雙手。3.3 給它兩個工具挑兩個最小的:讀文件 和 跑命令。為什麼是這兩個?因為它們已經足夠讓模型"摸清"你的項目——能讀文件意味着能看代碼,能跑命令意味着能 ls / grep / git log。Claude Code 自己的 Edit / Grep / Glob 這些工具,本質上都是在這兩個能力的基礎上再包一層。import subprocessdef read_file(path: str) -> str: with open(path, "r", encoding="utf-8") as f: return f.read()def run_bash(command: str) -> str: r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30) return f"[exit={r.returncode}]\n{r.stdout}{r.stderr}"TOOLS = {"read_file": read_file, "run_bash": run_bash}光有函數不夠,還得告訴大模型"你有這些工具、每個工具怎麼調"。這部分是標準的 OpenAI function calling schema,照着寫就行:TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "read_file", "description": "讀取一個本地文件的全部內容", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "run_bash", "description": "在本機執行一條 shell 命令,返回 stdout+stderr", "parameters": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }, },]這段 JSON 寫着煩,但你可以把它想成:就是第二章那個"工具箱"介紹文本換成了機器可讀的格式。大模型之所以知道該怎麼調 read_file("foo.py"),就是因為它看到了這段 schema。3.4 把 while loop 改成 ReAct loop這一節是全文的關鍵,慢一點看。3.2 的 loop 是"用戶說一句、模型回一句"。現在我們要改成——"用戶說一句、模型可能來回用好幾次工具、最後給用戶一個答案"。差別就在中間多了一個內循環:import jsondef agent_turn(messages): while True: resp = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOL_SCHEMAS, # 關鍵:把工具箱傳進去 ) msg = resp.choices[0].message messages.append(msg.model_dump(exclude_none=True)) # 模型沒要求調工具,說明它覺得可以直接回答了——跳出循環 if not msg.tool_calls: return msg.content or "" # 模型要求調工具:一個個執行,結果喂回去,然後下一輪 for call in msg.tool_calls: args = json.loads(call.function.arguments or "{}") print(f" [tool] {call.function.name}({args})") try: result = str(TOOLS[call.function.name](**args)) except Exception as e: result = f"[error] {type(e).__name__}: {e}" messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, })你仔細看,這段代碼和第二章那個"三輪推理"的故事是一一對應的:client.chat.completions.create(...) —— 大模型思考if not msg.tool_calls: return —— 模型覺得夠了,直接回答for call in msg.tool_calls —— 模型要求行動TOOLS[...](**args) —— Agent 真的去執行messages.append({"role": "tool", ...}) —— 把驗證結果喂回去外層 while True —— 循環直到模型說"我夠了"Claude Code 整個核心,就是這 20 來行。後面所有的功能,都是在這個循環的基礎上往外加枝加葉。你把這段代碼吃透了,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?另外有個很容易被忽略的細節:工具報錯,不能讓 loop 崩掉。所以 try/except 裏我們沒有拋異常,而是把錯誤字符串當成工具輸出喂回給模型,讓它自己去判斷這個工具調用成功了還是失敗了,下一步該怎麼辦。這一點反直覺但非常重要——錯誤是 agent 的下一個輸入,不是終止條件。3.5 把外殼拼起來最後把 3.2 的外循環接上 3.4 的內循環,就成了一個完整的 Agent 了:def main(): messages = [{"role": "system", "content": "你是一個運行在終端裏的編程助手。需要看文件或跑命令時就調工具。"}] while True: try: user = input("> ").strip() except (EOFError, KeyboardInterrupt): break if not user: continue messages.append({"role": "user", "content": user}) print(agent_turn(messages), "\n")if __name__ == "__main__": main()跑一把試試:> 看看當前目錄有哪些 python 文件,然後告訴我最大的那個是幹嘛的 [tool] run_bash({'command': 'ls -la *.py'}) [tool] read_file({'path': 'agent.py'})agent.py 是最大的文件(544 行),它實現了 ReAct 循環的主調度邏輯 ……你只說了一句話,它自己決定先 ls 一下、看到哪個最大、再 read_file 讀進來、最後給你總結。這就是 Agent。 就這麼回事。3.6 然後呢?到這兒,這篇的核心任務已經結束了——你有了一個能跑的 Agent。剩下的都是在這 100 行外面往外加功能、加健壯性、加用戶體驗的工作了。你完全可以停在這裏,自己想想還能加點什麼功能,或者改進哪裏不夠好,然後自己動手試試。但核心,始終是你剛剛寫的這 100 行。四、寫在最後如果這篇文章只讓你帶走一句話,我希望是這句:Agent = 一個 while 循環 + 一個 try/except。真的,就這麼簡單。你聽過的那些——Tool Use、ReAct、Multi-step、Agentic Workflow——全都是從這個結構上長出來的枝葉。等你把 mini.py 這 100 行吃透,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?這篇只講了最核心的循環。真正讓 Claude Code 從"能跑"變成"好用"的那些東西,這篇裏我一個都沒提。比如:子 Agent:讓模型能在主 Agent 裏再開一個小 Agent 去專門處理某個子任務,主 Agent 和子 Agent 之間也是通過同樣的工具調用接口來通信的。Memory:讓模型能把一些重要信息(比如用戶的偏好、之前的對話內容、工具調用的結果)存到一個專門的記憶庫裏,後續需要的時候再調出來用。Hooks:在工具調用前後、或者模型生成回答前後,插入一些自定義的邏輯來增強功能或者改寫輸入輸出。Skills:把一些複雜的功能(比如寫代碼、總結信息、分析日誌)封裝成一個個技能。更多工具:比如 edit_file 讓模型直接改代碼,grep 讓模型在文件裏搜索關鍵詞,web_fetch 讓模型直接抓取網頁內容……這些都是在上面那個循環的基礎上加的功能而已。但它們都是在那個 while 循環的基礎上加的功能而已。你完全可以先把這個循環吃透了,後面再去看我倉庫裏那個稍微完整點的版本,看看我是怎麼加這些功能的。完整代碼和後續功能更新都在這個倉庫裏: my-claude-code:https://github.com/developerchengang/my-claude-code

整理版摘要

前言: 最近 Claude Code 源代碼泄露,全網都在扒它的代碼設計思想。但直接啃源碼對初學者不友好,十幾萬行、近兩千個文件,一上來被各種設計模式淹死,看完只記得幾個名詞。其實 Claude Code 的內核極其簡單,簡單到可以濃縮成一句話——不過這句話我放文章最後講。本文的目標:用 100 行 Python 復刻出呢個內核,讓你看完就能跑起來、改得動。

至於編輯文件、搜索、子 Agent 呢啲外圍功能,會在我的 my-claude-code 倉庫裏給到完整實現,有興趣自己翻。今天只是開篇,後續我還會寫系列文章,來解讀 Claude Code 的設計細節,和它是怎麼實現各種功能的。一、先看效果Claude Code 效果圖讓我寫的 my-claude-code 寫了一個的貪吃蛇:用 my-claude-code 搓貪吃蛇就一句「幫我寫一個貪吃蛇網頁版小遊戲」,它自己決定建哪些文件、每個文件寫什麼、跑起來報錯了自己回頭改。

全程我一行代碼都沒碰過。後面的章節會告訴你——讓它跑成咁樣的核心,到底是什麼。再來一張讓它總結指定網頁內容的效果圖:總結網頁內容當然,還有很多功能。子 Agent、MemoryHooks、edit_file / grep / web_fetch 等完整工具箱,同埋 /compact、/resume、/clear 呢啲 slash 命令——整體架構和 Claude Code 已經很接近了,實現細…

  • 從零手把手教你寫一個簡易版 Claude Code:基礎篇
  • 從零手把手教你寫一個簡易版 Claude Code:基礎篇|重點 2
  • 從零手把手教你寫一個簡易版 Claude Code:基礎篇|重點 3
  • 從零手把手教你寫一個簡易版 Claude Code:基礎篇|重點 4
  • 從零手把手教你寫一個簡易版 Claude Code:基礎篇|重點 5
值得記低
Skill api.deepseek.com",

可記低 Skill

前言: 最近 Claude Code 源代碼泄露,全網都在扒它的代碼設計思想。但直接啃源碼對初學者不友好,十幾萬行、近兩千個文件,一上來被各種設計模式淹死,看完只記得幾個名詞。其實 Claude Code 的內核極其簡單,簡單到可以濃縮成一…

結構示例

內容片段

內容片段 text
[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞
整理重點

整理版

前言: 最近 Claude Code 源代碼泄露,全網都在扒它的代碼設計思想。但直接啃源碼對初學者不友好,十幾萬行、近兩千個文件,一上來被各種設計模式淹死,看完只記得幾個名詞。其實 Claude Code 的內核極其簡單,簡單到可以濃縮成一句話——不過這句話我放文章最後講。本文的目標:用 100 行 Python 復刻出呢個內核,讓你看完就能跑起來、改得動。至於編輯文件、搜索、子 Agent 呢啲外圍功能,會在我的 my-claude-code 倉庫裏給到完整實現,有興趣自己翻。今天只是開篇,後續我還會寫系列文章,來解讀 Claude Code 的設計細節,和它是怎麼實現各種功能的。一、先看效果Claude Code 效果圖讓我寫的 my-claude-code 寫了一個的貪吃蛇:用 my-claude-code 搓貪吃蛇就一句「幫我寫一個貪吃蛇網頁版小遊戲」,它自己決定建哪些文件、每個文件寫什麼、跑起來報錯了自己回頭改。全程我一行代碼都沒碰過。後面的章節會告訴你——讓它跑成咁樣的核心,到底是什麼。再來一張讓它總結指定網頁內容的效果圖:總結網頁內容當然,還有很多功能。子 Agent、Memory、Hooks、edit_file / grep / web_fetch 等完整工具箱,同埋 /compact、/resume、/clear 呢啲 slash 命令——整體架構和 Claude Code 已經很接近了,實現細節當然有出入,但思路一脈相承。但呢啲不是今天的重點。完整功能實現會在後續文章中詳細介紹。今天我們先把核心的循環跑起來,呢個循環才是 Agent 的真正內核。二、什麼是 Agent?假設我們需要完成咁樣一個任務:任務: 幫我搜索今天 Hacker News 的熱點新聞。如果我們直接把呢個任務交給一個語言模型,可能會得到咁樣的回答:回答: 抱歉,我無法直接訪問互聯網來搜索最新的 Hacker News 熱點新聞。為什麼會咁樣呢?因為語言模型本身沒有訪問互聯網的能力。而且,它也不知道今天是什麼日期,它只能根據訓練數據來生成回答,而不能執行實際的操作。這時候,我們就需要一箇中間層,來幫助語言模型執行實際的操作。呢個中間層就是 Agent。你可以把 Agent 想象成一個智能的指揮官,它負責協調各種工具來完成用戶的任務。它會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。在上面的例子中,我們看看 Agent 是怎麼工作的:當你向 Agent 輸入指令:“幫我搜索今天 Hacker News 的熱點新聞” 時,Agent 並沒有立刻把這句話發給大模型,而是先在後台做了一次拼上下文。它會在後台構建一個龐大的上下文環境(Context),你可以把它理解為給大模型的輸入數據。呢個上下文環境裏包含了系統設定、工具箱、用戶指令等等信息。發給大模型的完整數據大概長咁樣:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞大模型在接收到呢個上下文環境後,會根據系統設定和工具箱的信息,來理解用戶的指令,並且選擇合適的工具來執行。在呢個例子中:第一輪大模型會先推理(Think)分析用戶的指令,發現了一個變量——“今天”。它心想:“作為語言模型,我沒有內置時間,但我看到了我的工具箱裏有一個叫 get_current_date() 的工具,我正好需要它!”下達行動指令 (Act): 於是,大模型暫停了文本回復,而是向 Agent 返回了一段標準的 JSON 格式指令,要求執行工具調用:{ "tool": "get_current_date", "args": {}}框架執行 (Verify/Observe):Agent 接收到呢個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 get_current_date() 函數來獲取當前日期。工具執行完成後,會把結果返回給 Agent。比如,get_current_date() 可能返回了“2026-04-20”。Agent 默默把呢個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞[工具調用記錄]get_current_date() → 2026-04-20第二輪大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了。大模型結合新的上下文,確認了目標:“好,而家我知道要找 2026 年 4 月 20 日的 HN 新聞了。下一步,我需要調用搜索(search_web)工具下達行動指令 (Act):於是,大模型再次向 Agent 框架返回了一段標準的 JSON 格式指令,要求執行工具調用:{ "tool": "search_web", "args": { "query": "2026-04-20 Hacker News 熱點新聞" }}框架執行 (Verify/Observe):Agent 接收到呢個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 search_web("2026-04-20 Hacker News 熱點新聞") 函數來搜索互聯網內容。工具執行完成後,會把結果返回給 Agent。比如,search_web() 可能返回了一個包含熱點新聞標題和連結的列表。Agent 把呢個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:[系統設定]你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。[工具箱]1. 搜索工具(search_web):可以用來搜索互聯網內容。2. 日期工具(get_current_date):可以用來獲取當前日期。[用戶指令]幫我搜索今天 Hacker News 的熱點新聞[工具調用記錄]get_current_date() → 2026-04-20search_web("2026-04-20 Hacker News 熱點新聞") →[ {"rank": 1, "title": "XXX", "url": "YYY", "points": 842, "comments": 267, "domain": "...", "posted_ago": "7h", "snippet": "一大段原文摘錄..."}, {"rank": 2, "title": "AAA", "url": "BBB", "points": 613, "comments": 189, "domain": "...", "posted_ago": "5h", "snippet": "..."}, {"rank": 3, "title": "MMM", "url": "NNN", "points": 402, ...}, ... 另外 27 條結果,摻着廣告、轉帖、無關外鏈 ...]注意看這兩段「上下文快照」的變化——[用戶指令] 從頭到尾是同一句「幫我搜索今天 Hacker News 的熱點新聞」,沒有被改寫過。日期和搜索結果都是往下追加的新條目。這是 ReAct 的一個硬性契約:歷史只讀、只增不改。模型是在讀完整條累加下來的歷史之後、自己在腦子裏把「今天 ≈ 2026-04-20」連起來的,而不是某個環節把用戶原話替換掉了。呢個不變量後面 3.4 的代碼裏還會再遇到一次——全程 messages.append,沒有任何地方動過舊消息。第三輪大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了,也拿到了搜索結果了。大模型結合新的上下文,確認了目標:“好,30 條原始數據在手了。下一步,我要從這堆 JSON 裏挑出熱度最高的幾條、扔掉 rank/points/comments/snippet 呢啲用戶不關心的字段,用人話重新排版發回去。”大模型生成回答 (Respond):於是,大模型根據新的上下文環境,把原始搜索結果過濾、去噪、重排後,生成了一個乾淨的回答:2026-04-20 Hacker News 熱度最高的兩條:1. 標題:XXX,連結:YYY(842 分 / 267 評論)2. 標題:AAA,連結:BBB(613 分 / 189 評論)Agent 把呢個回答發回給用戶,完成了整個任務。看完了上面呢個 Hacker News 的抓取例子,那麼恭喜你——你已經掌握了當下幾乎所有 AI 編程工具的底層核心邏輯。無論是 Claude Code、Cursor、opencode、Cline,還是 OpenAI 的 Codex CLI、Google 的 Gemini CLI,它們在最底層跑的都是同一個閉環:ReAct(Reason + Act),也就是“感知-思考-行動-驗證”四步循環。你會發現,大模型(大腦)和 Agent 框架(外骨骼)在這裏面有着極其明確的分工:感知 (Perceive): 不僅僅是“聽指令”,更是“讀環境”。大模型在每一步都在感知當前的上下文環境,包括用戶指令、工具箱信息、系統設定、工具調用結果等等。它需要不斷地更新自己的認知模型,來理解當前的任務和目標。思考 (Think): 這是大模型的“主場”。它在每一步都在做邏輯判斷——“我缺時間”、“我缺聯網能力”、“數據拿到了可以排版了”。它清楚自己的邊界在哪裏。行動 (Act): 當大模型發現自己做不到時,它會輸出調用工具的指令(Tool Calling)。讓外圍的 Agent 真正去執行 get_date 或 search_web。驗證 (Verify): 工具調完了沒算完,Agent 必須把調用的結果(無論成功還是報錯)拿回來驗證。就像查日期那一步,有了真實日期作為驗證結果,循環才能繼續推進。呢個循環不斷地進行,直到大模型生成了一個完整的回答,或者達到了某個終止條件。呢個過程就是 Agent 的核心工作原理,也是 Claude Code 的核心設計思路。三、手搓 Claude Code接下來,我會一步步帶你手搓一個簡易版的 Claude Code。但我不打算從零講到一個功能完整的版本——Claude Code 源碼泄露出來就十幾萬行、接近兩千個文件,我自己寫的呢個簡化版也有將近三千行,文章裏塞不下,你也沒耐心看。所以這一章的目標只有一個:用 100 行代碼復現第二章嗰個 ReAct 循環。只做工具調用,別的什麼都不做。等你真的跑起來一次,後面再去看 Claude Code 源碼、或者我倉庫裏嗰個稍微完整點的版本,就沒什麼障礙了。完整代碼在倉庫的 mini.py 裏,100 行出頭,文末給連結。下面一段一段拆。3.1 先把環境裝好一個 Python 環境,一個 OpenAI SDK,一個模型的 API key。就這三樣。等等——Claude Code 不是 Anthropic 家的嗎,怎麼用 OpenAI SDK? 因為 OpenAI 的 chat.completions 早就是工具調用事實上的行業標準,DeepSeek、GLM、Kimi、本機 Ollama 全都兼容它,換模型只改 base_url 就行。真想用 Anthropic 原生 API 也可以,把 openai 換成 anthropic 包就是——ReAct 循環的邏輯一字不變,變的只是 SDK 怎麼調。本文只是挑了最通用的那條路。pip install openai這裏我用的是 DeepSeek,便宜,跑工具調用夠用。你想用 OpenAI 官方、通義、或者本機 Ollama 都行——只要它兼容 OpenAI 的 chat.completions 接口,改個 base_url 就能切。from openai import OpenAIclient = OpenAI( api_key="sk-xxx", base_url="https://api.deepseek.com",)MODEL = "deepseek-chat"3.2 先搭一個最普通的對話 loop加工具之前,我們先寫個最普通的命令行 chatbot。這一步和 Agent 半毛錢關係都沒有,但它是後面一切東西的骨架。messages = [{"role": "system", "content": "你是一個編程助手。"}]while True: user = input("> ") messages.append({"role": "user", "content": user}) resp = client.chat.completions.create(model=MODEL, messages=messages) reply = resp.choices[0].message.content messages.append({"role": "assistant", "content": reply}) print(reply)這時候你跑起來,和它說"你好",它會回你"你好"。但你問它"我的 config.json 裏寫了啥"——它會回你"抱歉,我無法直接訪問你的文件系統來讀取 config.json 的內容。"。這時候你就知道了——它只能說,不能做。因為模型沒有"手"。它看不到你的文件,也跑不了你的命令。我們接下來要做的,就是給它裝一雙手。3.3 給它兩個工具挑兩個最小的:讀文件 和 跑命令。為什麼是這兩個?因為它們已經足夠讓模型"摸清"你的項目——能讀文件意味着能看代碼,能跑命令意味着能 ls / grep / git log。Claude Code 自己的 Edit / Grep / Glob 呢啲工具,本質上都是在這兩個能力的基礎上再包一層。import subprocessdef read_file(path: str) -> str: with open(path, "r", encoding="utf-8") as f: return f.read()def run_bash(command: str) -> str: r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30) return f"[exit={r.returncode}]\n{r.stdout}{r.stderr}"TOOLS = {"read_file": read_file, "run_bash": run_bash}光有函數不夠,還得告訴大模型"你有呢啲工具、每個工具怎麼調"。這部分是標準的 OpenAI function calling schema,照着寫就行:TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "read_file", "description": "讀取一個本地文件的全部內容", "parameters": { "type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"], }, }, }, { "type": "function", "function": { "name": "run_bash", "description": "在本機執行一條 shell 命令,返回 stdout+stderr", "parameters": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }, },]這段 JSON 寫着煩,但你可以把它想成:就是第二章嗰個"工具箱"介紹文本換成了機器可讀的格式。大模型之所以知道該怎麼調 read_file("foo.py"),就是因為它看到了這段 schema。3.4 把 while loop 改成 ReAct loop這一節是全文的關鍵,慢一點看。3.2 的 loop 是"用戶說一句、模型回一句"。而家我們要改成——"用戶說一句、模型可能來回用好幾次工具、最後給用戶一個答案"。差別就在中間多了一個內循環:import jsondef agent_turn(messages): while True: resp = client.chat.completions.create( model=MODEL, messages=messages, tools=TOOL_SCHEMAS, # 關鍵:把工具箱傳進去 ) msg = resp.choices[0].message messages.append(msg.model_dump(exclude_none=True)) # 模型沒要求調工具,說明它覺得可以直接回答了——跳出循環 if not msg.tool_calls: return msg.content or "" # 模型要求調工具:一個個執行,結果喂回去,然後下一輪 for call in msg.tool_calls: args = json.loads(call.function.arguments or "{}") print(f" [tool] {call.function.name}({args})") try: result = str(TOOLS[call.function.name](**args)) except Exception as e: result = f"[error] {type(e).__name__}: {e}" messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, })你仔細看,這段代碼和第二章嗰個"三輪推理"的故事是一一對應的:client.chat.completions.create(...) —— 大模型思考if not msg.tool_calls: return —— 模型覺得夠了,直接回答for call in msg.tool_calls —— 模型要求行動TOOLS[...](**args) —— Agent 真的去執行messages.append({"role": "tool", ...}) —— 把驗證結果喂回去外層 while True —— 循環直到模型說"我夠了"Claude Code 整個核心,就是這 20 來行。後面所有的功能,都是在呢個循環的基礎上往外加枝加葉。你把這段代碼吃透了,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?另外有個很容易被忽略的細節:工具報錯,不能讓 loop 崩掉。所以 try/except 裏我們沒有拋異常,而是把錯誤字符串當成工具輸出喂回給模型,讓它自己去判斷呢個工具調用成功了還是失敗了,下一步該怎麼辦。這一點反直覺但非常重要——錯誤是 agent 的下一個輸入,不是終止條件。3.5 把外殼拼起來最後把 3.2 的外循環接上 3.4 的內循環,就成了一個完整的 Agent 了:def main(): messages = [{"role": "system", "content": "你是一個運行在終端裏的編程助手。需要看文件或跑命令時就調工具。"}] while True: try: user = input("> ").strip() except (EOFError, KeyboardInterrupt): break if not user: continue messages.append({"role": "user", "content": user}) print(agent_turn(messages), "\n")if __name__ == "__main__": main()跑一把試試:> 看看當前目錄有哪些 python 文件,然後告訴我最大的嗰個是幹嘛的 [tool] run_bash({'command': 'ls -la *.py'}) [tool] read_file({'path': 'agent.py'})agent.py 是最大的文件(544 行),它實現了 ReAct 循環的主調度邏輯 ……你只說了一句話,它自己決定先 ls 一下、看到哪個最大、再 read_file 讀進來、最後給你總結。這就是 Agent。 就這麼回事。3.6 然後呢?到這兒,這篇的核心任務已經結束了——你有了一個能跑的 Agent。剩下的都是在這 100 行外面往外加功能、加健壯性、加用戶體驗的工作了。你完全可以停在這裏,自己想想還能加點什麼功能,或者改進哪裏不夠好,然後自己動手試試。但核心,始終是你剛剛寫的這 100 行。四、寫在最後如果這篇文章只讓你帶走一句話,我希望是這句:Agent = 一個 while 循環 + 一個 try/except。真的,就這麼簡單。你聽過的嗰啲——Tool Use、ReAct、Multi-step、Agentic Workflow——全都是從呢個結構上長出來的枝葉。等你把 mini.py 這 100 行吃透,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?這篇只講了最核心的循環。真正讓 Claude Code 從"能跑"變成"好用"的嗰啲東西,這篇裏我一個都沒提。比如:子 Agent:讓模型能在主 Agent 裏再開一個小 Agent 去專門處理某個子任務,主 Agent 和子 Agent 之間也是通過同樣的工具調用接口來通信的。Memory:讓模型能把一些重要信息(比如用戶的偏好、之前的對話內容、工具調用的結果)存到一個專門的記憶庫裏,後續需要的時候再調出來用。Hooks:在工具調用前後、或者模型生成回答前後,插入一些自定義的邏輯來增強功能或者改寫輸入輸出。Skills:把一些複雜的功能(比如寫代碼、總結信息、分析日誌)封裝成一個個技能。更多工具:比如 edit_file 讓模型直接改代碼,grep 讓模型在文件裏搜索關鍵詞,web_fetch 讓模型直接抓取網頁內容……呢啲都是在上面嗰個循環的基礎上加的功能而已。但它們都是在嗰個 while 循環的基礎上加的功能而已。你完全可以先把呢個循環吃透了,後面再去看我倉庫裏嗰個稍微完整點的版本,看看我是怎麼加呢啲功能的。完整代碼和後續功能更新都在呢個倉庫裏: my-claude-code:https://github.com/developerchengang/my-claude-code

前言: 最近 Claude Code 源代碼泄露,全網都在扒它的代碼設計思想。但直接啃源碼對初學者不友好,十幾萬行、近兩千個文件,一上來被各種設計模式淹死,看完只記得幾個名詞。其實 Claude Code 的內核極其簡單,簡單到可以濃縮成一句話——不過這句話我放文章最後講。本文的目標:用 100 行 Python 復刻出這個內核,讓你看完就能跑起來、改得動。至於編輯文件、搜索、子 Agent 這些外圍功能,會在我的 my-claude-code 倉庫裏給到完整實現,有興趣自己翻。

今天只是開篇,後續我還會寫系列文章,來解讀 Claude Code 的設計細節,和它是怎麼實現各種功能的。

一、先看效果

圖片

Claude Code 效果圖

讓我寫的 my-claude-code 寫了一個的貪吃蛇:

圖片
用 my-claude-code 搓貪吃蛇


就一句「幫我寫一個貪吃蛇網頁版小遊戲」,它自己決定建哪些文件、每個文件寫什麼、跑起來報錯了自己回頭改。全程我一行代碼都沒碰過。

後面的章節會告訴你——讓它跑成這樣的核心,到底是什麼

再來一張讓它總結指定網頁內容的效果圖:

圖片
總結網頁內容


當然,還有很多功能。子 Agent、Memory、Hooks、edit_file / grep / web_fetch 等完整工具箱,以及 /compact/resume/clear 這些 slash 命令——整體架構和 Claude Code 已經很接近了,實現細節當然有出入,但思路一脈相承。

但這些不是今天的重點。完整功能實現會在後續文章中詳細介紹。今天我們先把核心的循環跑起來,這個循環才是 Agent 的真正內核。

二、什麼是 Agent?

假設我們需要完成這樣一個任務:

任務: 幫我搜索今天 Hacker News 的熱點新聞。

如果我們直接把這個任務交給一個語言模型,可能會得到這樣的回答:

回答: 抱歉,我無法直接訪問互聯網來搜索最新的 Hacker News 熱點新聞。

為什麼會這樣呢?因為語言模型本身沒有訪問互聯網的能力。而且,它也不知道今天是什麼日期,它只能根據訓練數據來生成回答,而不能執行實際的操作。

這時候,我們就需要一箇中間層,來幫助語言模型執行實際的操作。這個中間層就是 Agent。

你可以把 Agent 想象成一個智能的指揮官,它負責協調各種工具來完成用戶的任務。它會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。

在上面的例子中,我們看看 Agent 是怎麼工作的:

當你向 Agent 輸入指令:“幫我搜索今天 Hacker News 的熱點新聞” 時,Agent 並沒有立刻把這句話發給大模型,而是先在後台做了一次拼上下文。

它會在後台構建一個龐大的上下文環境(Context),你可以把它理解為給大模型的輸入數據。這個上下文環境裏包含了系統設定、工具箱、用戶指令等等信息。

發給大模型的完整數據大概長這樣:

[系統設定]
你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。

[工具箱]
1. 搜索工具(search_web):可以用來搜索互聯網內容。
2. 日期工具(get_current_date):可以用來獲取當前日期。

[用戶指令]
幫我搜索今天 Hacker News 的熱點新聞

大模型在接收到這個上下文環境後,會根據系統設定和工具箱的信息,來理解用戶的指令,並且選擇合適的工具來執行。

在這個例子中:

第一輪

  1. 大模型會先推理(Think)分析用戶的指令,發現了一個變量——“今天”。它心想:“作為語言模型,我沒有內置時間,但我看到了我的工具箱裏有一個叫 get_current_date() 的工具,我正好需要它!”

  2. 下達行動指令 (Act): 於是,大模型暫停了文本回復,而是向 Agent 返回了一段標準的 JSON 格式指令,要求執行工具調用:

{
  "tool""get_current_date",
  "args"{}
}
  1. 框架執行 (Verify/Observe):Agent 接收到這個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 get_current_date() 函數來獲取當前日期。工具執行完成後,會把結果返回給 Agent。比如,get_current_date() 可能返回了“2026-04-20”。

  2. Agent 默默把這個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:

[系統設定]
你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。

[工具箱]
1. 搜索工具(search_web):可以用來搜索互聯網內容。
2. 日期工具(get_current_date):可以用來獲取當前日期。

[用戶指令]
幫我搜索今天 Hacker News 的熱點新聞

[工具調用記錄]
get_current_date() → 2026-04-20

第二輪

  1. 大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了。大模型結合新的上下文,確認了目標:“好,現在我知道要找 2026 年 4 月 20 日的 HN 新聞了。下一步,我需要調用搜索(search_web)工具

  2. 下達行動指令 (Act):於是,大模型再次向 Agent 框架返回了一段標準的 JSON 格式指令,要求執行工具調用:

{
  "tool""search_web",
  "args"{
    "query""2026-04-20 Hacker News 熱點新聞"
  }
}
  1. 框架執行 (Verify/Observe):Agent 接收到這個指令後,會解析出工具名稱和參數,然後調用對應的工具函數來執行操作。比如,它會調用 search_web("2026-04-20 Hacker News 熱點新聞") 函數來搜索互聯網內容。工具執行完成後,會把結果返回給 Agent。比如,search_web() 可能返回了一個包含熱點新聞標題和連結的列表。

  2. Agent 把這個結果加到了對話歷史記錄中,準備發回給大模型。這時,發回給大模型的上下文環境就變成了:

[系統設定]
你是一個智能的指揮官,負責協調各種工具來完成用戶的任務。你會根據用戶的輸入,選擇合適的工具,並且把工具的輸出結果返回給用戶。

[工具箱]
1. 搜索工具(search_web):可以用來搜索互聯網內容。
2. 日期工具(get_current_date):可以用來獲取當前日期。

[用戶指令]
幫我搜索今天 Hacker News 的熱點新聞

[工具調用記錄]
get_current_date() → 2026-04-20
search_web("2026-04-20 Hacker News 熱點新聞") →
[
  {"rank": 1, "title""XXX""url""YYY""points": 842,
   "comments": 267, "domain""...""posted_ago""7h",
   "snippet""一大段原文摘錄..."},
  {"rank": 2, "title""AAA""url""BBB""points": 613,
   "comments": 189, "domain""...""posted_ago""5h",
   "snippet""..."},
  {"rank": 3, "title""MMM""url""NNN""points": 402, ...},
  ... 另外 27 條結果,摻着廣告、轉帖、無關外鏈 ...
]

注意看這兩段「上下文快照」的變化——[用戶指令] 從頭到尾是同一句「幫我搜索今天 Hacker News 的熱點新聞」,沒有被改寫過。日期和搜索結果都是往下追加的新條目。這是 ReAct 的一個硬性契約:歷史只讀、只增不改。模型是在讀完整條累加下來的歷史之後、自己在腦子裏把「今天 ≈ 2026-04-20」連起來的,而不是某個環節把用戶原話替換掉了。這個不變量後面 3.4 的代碼裏還會再遇到一次——全程 messages.append,沒有任何地方動過舊消息。

第三輪

  1. 大模型再次推理(Think)分析用戶的指令,這次它已經知道了“今天”的具體日期了,也拿到了搜索結果了。大模型結合新的上下文,確認了目標:“好,30 條原始數據在手了。下一步,我要從這堆 JSON 裏挑出熱度最高的幾條、扔掉 rank/points/comments/snippet 這些用戶不關心的字段,用人話重新排版發回去。”

  2. 大模型生成回答 (Respond):於是,大模型根據新的上下文環境,把原始搜索結果過濾、去噪、重排後,生成了一個乾淨的回答:

2026-04-20 Hacker News 熱度最高的兩條:
1. 標題:XXX,連結:YYY(842 分 / 267 評論)
2. 標題:AAA,連結:BBB(613 分 / 189 評論)
  1. Agent 把這個回答發回給用戶,完成了整個任務。

看完了上面這個 Hacker News 的抓取例子,那麼恭喜你——你已經掌握了當下幾乎所有 AI 編程工具的底層核心邏輯。無論是 Claude Code、Cursor、opencode、Cline,還是 OpenAI 的 Codex CLI、Google 的 Gemini CLI,它們在最底層跑的都是同一個閉環:ReAct(Reason + Act),也就是“感知-思考-行動-驗證”四步循環。你會發現,大模型(大腦)和 Agent 框架(外骨骼)在這裏面有着極其明確的分工:

感知 (Perceive): 不僅僅是“聽指令”,更是“讀環境”。大模型在每一步都在感知當前的上下文環境,包括用戶指令、工具箱信息、系統設定、工具調用結果等等。它需要不斷地更新自己的認知模型,來理解當前的任務和目標。

思考 (Think): 這是大模型的“主場”。它在每一步都在做邏輯判斷——“我缺時間”、“我缺聯網能力”、“數據拿到了可以排版了”。它清楚自己的邊界在哪裏。

行動 (Act): 當大模型發現自己做不到時,它會輸出調用工具的指令(Tool Calling)。讓外圍的 Agent 真正去執行 get_date 或 search_web。

驗證 (Verify): 工具調完了沒算完,Agent 必須把調用的結果(無論成功還是報錯)拿回來驗證。就像查日期那一步,有了真實日期作為驗證結果,循環才能繼續推進。

這個循環不斷地進行,直到大模型生成了一個完整的回答,或者達到了某個終止條件。這個過程就是 Agent 的核心工作原理,也是 Claude Code 的核心設計思路。

三、手搓 Claude Code

接下來,我會一步步帶你手搓一個簡易版的 Claude Code。

但我不打算從零講到一個功能完整的版本——Claude Code 源碼泄露出來就十幾萬行、接近兩千個文件,我自己寫的這個簡化版也有將近三千行,文章裏塞不下,你也沒耐心看。

所以這一章的目標只有一個:用 100 行代碼復現第二章那個 ReAct 循環。只做工具調用,別的什麼都不做。等你真的跑起來一次,後面再去看 Claude Code 源碼、或者我倉庫裏那個稍微完整點的版本,就沒什麼障礙了。

完整代碼在倉庫的 mini.py 裏,100 行出頭,文末給連結。下面一段一段拆。

3.1 先把環境裝好

一個 Python 環境,一個 OpenAI SDK,一個模型的 API key。就這三樣。

等等——Claude Code 不是 Anthropic 家的嗎,怎麼用 OpenAI SDK? 因為 OpenAI 的 chat.completions 早就是工具調用事實上的行業標準,DeepSeek、GLM、Kimi、本機 Ollama 全都兼容它,換模型只改 base_url 就行。真想用 Anthropic 原生 API 也可以,把 openai 換成 anthropic 包就是——ReAct 循環的邏輯一字不變,變的只是 SDK 怎麼調。本文只是挑了最通用的那條路。

pip install openai

這裏我用的是 DeepSeek,便宜,跑工具調用夠用。你想用 OpenAI 官方、通義、或者本機 Ollama 都行——只要它兼容 OpenAI 的 chat.completions 接口,改個 base_url 就能切。

from openai import OpenAI

client = OpenAI(
    api_key="sk-xxx",
    base_url="https://api.deepseek.com",
)
MODEL = "deepseek-chat"

3.2 先搭一個最普通的對話 loop

加工具之前,我們先寫個最普通的命令行 chatbot。這一步和 Agent 半毛錢關係都沒有,但它是後面一切東西的骨架。

messages = [{"role""system""content""你是一個編程助手。"}]

while True:
    user = input("> ")
    messages.append({"role""user""content": user})
    resp = client.chat.completions.create(model=MODEL, messages=messages)
    reply = resp.choices[0].message.content
    messages.append({"role""assistant""content": reply})
    print(reply)

這時候你跑起來,和它說"你好",它會回你"你好"。但你問它"我的 config.json 裏寫了啥"——它會回你"抱歉,我無法直接訪問你的文件系統來讀取 config.json 的內容。"。這時候你就知道了——它只能說,不能做

因為模型沒有"手"。它看不到你的文件,也跑不了你的命令。我們接下來要做的,就是給它裝一雙手。

3.3 給它兩個工具

挑兩個最小的:讀文件 和 跑命令

為什麼是這兩個?因為它們已經足夠讓模型"摸清"你的項目——能讀文件意味着能看代碼,能跑命令意味着能 ls / grep / git log。Claude Code 自己的 Edit / Grep / Glob 這些工具,本質上都是在這兩個能力的基礎上再包一層。

import subprocess

def read_file(path: str) -> str:
    with open(path, "r", encoding="utf-8"as f:
        return f.read()

def run_bash(command: str) -> str:
    r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=30)
    return f"[exit={r.returncode}]\n{r.stdout}{r.stderr}"

TOOLS = {"read_file": read_file, "run_bash": run_bash}

光有函數不夠,還得告訴大模型"你有這些工具、每個工具怎麼調"。這部分是標準的 OpenAI function calling schema,照着寫就行:

TOOL_SCHEMAS = [
    {
        "type""function",
        "function": {
            "name""read_file",
            "description""讀取一個本地文件的全部內容",
            "parameters": {
                "type""object",
                "properties": {"path": {"type""string"}},
                "required": ["path"],
            },
        },
    },
    {
        "type""function",
        "function": {
            "name""run_bash",
            "description""在本機執行一條 shell 命令,返回 stdout+stderr",
            "parameters": {
                "type""object",
                "properties": {"command": {"type""string"}},
                "required": ["command"],
            },
        },
    },
]

這段 JSON 寫着煩,但你可以把它想成:就是第二章那個"工具箱"介紹文本換成了機器可讀的格式。大模型之所以知道該怎麼調 read_file("foo.py"),就是因為它看到了這段 schema。

3.4 把 while loop 改成 ReAct loop

這一節是全文的關鍵,慢一點看。

3.2 的 loop 是"用戶說一句、模型回一句"。現在我們要改成——"用戶說一句、模型可能來回用好幾次工具、最後給用戶一個答案"。

差別就在中間多了一個內循環

import json

def agent_turn(messages):
    while True:
        resp = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOL_SCHEMAS,   # 關鍵:把工具箱傳進去
        )
        msg = resp.choices[0].message
        messages.append(msg.model_dump(exclude_none=True))

        # 模型沒要求調工具,說明它覺得可以直接回答了——跳出循環
        if not msg.tool_calls:
            return msg.content or ""

        # 模型要求調工具:一個個執行,結果喂回去,然後下一輪
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments or "{}")
            print(f"  [tool] {call.function.name}({args})")
            try:
                result = str(TOOLS[call.function.name](**args))
            except Exception as e:
                result = f"[error] {type(e).__name__}{e}"
            messages.append({
                "role""tool",
                "tool_call_id": call.id,
                "content": result,
            })

你仔細看,這段代碼和第二章那個"三輪推理"的故事是一一對應的:

  • client.chat.completions.create(...) —— 大模型思考
  • if not msg.tool_calls: return —— 模型覺得夠了,直接回答
  • for call in msg.tool_calls —— 模型要求行動
  • TOOLS[...](**args) —— Agent 真的去執行
  • messages.append({"role": "tool", ...}) —— 把驗證結果喂回去
  • 外層 while True —— 循環直到模型說"我夠了"

Claude Code 整個核心,就是這 20 來行。後面所有的功能,都是在這個循環的基礎上往外加枝加葉。你把這段代碼吃透了,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?

另外有個很容易被忽略的細節:工具報錯,不能讓 loop 崩掉。所以 try/except 裏我們沒有拋異常,而是把錯誤字符串當成工具輸出喂回給模型,讓它自己去判斷這個工具調用成功了還是失敗了,下一步該怎麼辦。

這一點反直覺但非常重要——錯誤是 agent 的下一個輸入,不是終止條件

3.5 把外殼拼起來

最後把 3.2 的外循環接上 3.4 的內循環,就成了一個完整的 Agent 了:

def main():
    messages = [{"role""system""content""你是一個運行在終端裏的編程助手。需要看文件或跑命令時就調工具。"}]
    while True:
        try:
            user = input("> ").strip()
        except (EOFError, KeyboardInterrupt):
            break
        if not user:
            continue
        messages.append({"role""user""content": user})
        print(agent_turn(messages), "\n")

if __name__ == "__main__":
    main()

跑一把試試:

> 看看當前目錄有哪些 python 文件,然後告訴我最大的那個是幹嘛的
  [tool] run_bash({'command''ls -la *.py'})
  [tool] read_file({'path''agent.py'})
agent.py 是最大的文件(544 行),它實現了 ReAct 循環的主調度邏輯 ……

你只說了一句話,它自己決定先 ls 一下、看到哪個最大、再 read_file 讀進來、最後給你總結。

這就是 Agent。 就這麼回事。

3.6 然後呢?

到這兒,這篇的核心任務已經結束了——你有了一個能跑的 Agent。剩下的都是在這 100 行外面往外加功能、加健壯性、加用戶體驗的工作了。你完全可以停在這裏,自己想想還能加點什麼功能,或者改進哪裏不夠好,然後自己動手試試。

但核心,始終是你剛剛寫的這 100 行。

四、寫在最後

如果這篇文章只讓你帶走一句話,我希望是這句:

Agent = 一個 while 循環 + 一個 try/except。

真的,就這麼簡單。你聽過的那些——Tool Use、ReAct、Multi-step、Agentic Workflow——全都是從這個結構上長出來的枝葉。等你把 mini.py 這 100 行吃透,以後再去讀任何 Agent 框架的源碼,你都只是在問同一個問題:它的 while 循環裏,多塞了點什麼?


這篇只講了最核心的循環。真正讓 Claude Code 從"能跑"變成"好用"的那些東西,這篇裏我一個都沒提。比如:

  • 子 Agent:讓模型能在主 Agent 裏再開一個小 Agent 去專門處理某個子任務,主 Agent 和子 Agent 之間也是通過同樣的工具調用接口來通信的。

  • Memory:讓模型能把一些重要信息(比如用戶的偏好、之前的對話內容、工具調用的結果)存到一個專門的記憶庫裏,後續需要的時候再調出來用。

  • Hooks:在工具調用前後、或者模型生成回答前後,插入一些自定義的邏輯來增強功能或者改寫輸入輸出。

  • Skills:把一些複雜的功能(比如寫代碼、總結信息、分析日誌)封裝成一個個技能。

  • 更多工具:比如 edit_file 讓模型直接改代碼,grep 讓模型在文件裏搜索關鍵詞,web_fetch 讓模型直接抓取網頁內容……這些都是在上面那個循環的基礎上加的功能而已。

但它們都是在那個 while 循環的基礎上加的功能而已。你完全可以先把這個循環吃透了,後面再去看我倉庫裏那個稍微完整點的版本,看看我是怎麼加這些功能的。


完整代碼和後續功能更新都在這個倉庫裏: my-claude-code:https://github.com/developerchengang/my-claude-code