踐(39):第一次實(shí)現(xiàn)——先做一個(gè)最小 Agent)
發(fā)布時(shí)間2026-09-1標(biāo)簽AI Agent工程實(shí)踐MVP最小實(shí)現(xiàn)架構(gòu)畫(huà)得再漂亮也只是紙上的東西。上一篇我畫(huà)了四個(gè)節(jié)點(diǎn)Task Router、Planner、Analyzer、Reviewer。聽(tīng)起來(lái)很完整對(duì)吧但我決定一個(gè)都不實(shí)現(xiàn)。這一篇我只寫(xiě)最窄的一條通路用戶問(wèn) → 調(diào)一個(gè)工具 → 讀結(jié)果 → 回答。原因很簡(jiǎn)單我想看看這個(gè)最小可跑的版本到底會(huì)怎么死。系列導(dǎo)航上一篇AI Agent 工程實(shí)踐38從需求到 Agent 架構(gòu)——為什么需要這些節(jié)點(diǎn)下一篇AI Agent 工程實(shí)踐40第一次失敗——Agent 為什么會(huì)做錯(cuò)問(wèn)題背景這是第五階段的第四篇也是第一次真正寫(xiě)代碼。很多人會(huì)犯一個(gè)錯(cuò)架構(gòu)圖里畫(huà)了四個(gè)節(jié)點(diǎn)就非得把四個(gè)都寫(xiě)出來(lái)才罷休。但我的經(jīng)驗(yàn)是——先做一個(gè)故意很蠢的最小版本讓它跑起來(lái)然后用它去暴露問(wèn)題。這個(gè)最小版本有個(gè)學(xué)名叫 MVPMinimum Viable Product但在這里它的意義不是證明能跑而是用最快的速度暴露它會(huì)怎么死。為什么暴露死亡這么重要因?yàn)?Agent 項(xiàng)目最大的風(fēng)險(xiǎn)不是寫(xiě)不出來(lái)而是寫(xiě)了很多但里面的假設(shè)全是錯(cuò)的。你架構(gòu)圖里畫(huà)的 Planner、Reviewer可能根本解決不了真實(shí)問(wèn)題——而這些問(wèn)題只有讓最小版本跑起來(lái)、撞上真實(shí)問(wèn)句才會(huì)顯現(xiàn)。所以我這一篇砍掉 Task Router、砍掉 Planner、砍掉 Reviewer只保留一個(gè) LLM 兩個(gè)工具grep 和 read_file讓整條鏈路能跑通。錯(cuò)誤嘗試我差點(diǎn)犯了兩個(gè)反方向的錯(cuò)。第一個(gè)錯(cuò)想一步到位。想把四個(gè)節(jié)點(diǎn)、六個(gè)工具、Memory、評(píng)估集全部寫(xiě)完再跑。結(jié)果就是寫(xiě)了一個(gè)月一行能跑的代碼都沒(méi)有還積累了一堆我以為對(duì)的假設(shè)。這一堆以為對(duì)的假設(shè)是最貴的。比如我以為L(zhǎng)LM 會(huì)自己選對(duì)工具直到真實(shí)跑起來(lái)才發(fā)現(xiàn)它經(jīng)常選錯(cuò)我以為讀到文件就能定位 bug直到真實(shí)跑起來(lái)才發(fā)現(xiàn)它會(huì)編造不存在的函數(shù)。這些假設(shè)只有讓代碼真的跑起來(lái)才會(huì)被證偽——而一步到位的寫(xiě)法讓你把所有假設(shè)都攢到最后一起爆。第二個(gè)錯(cuò)覺(jué)得太簡(jiǎn)單不值得跑。一個(gè) LLM 兩個(gè)工具這有什么好跑的 但我告訴你恰恰是這個(gè)最簡(jiǎn)單的版本暴露了后面整整十篇要解決的問(wèn)題。你只有真的跑起來(lái)才能看到它選錯(cuò)工具、讀錯(cuò)文件、憑空編造結(jié)論的樣子。兩個(gè)錯(cuò)誤殊途同歸都推遲了第一次看到真實(shí)失敗的時(shí)間點(diǎn)。這里我要特別強(qiáng)調(diào)一個(gè)心態(tài)在 Agent 開(kāi)發(fā)里先跑起來(lái)的優(yōu)先級(jí)高于架構(gòu)正確。因?yàn)?Agent 的行為極度依賴真實(shí)執(zhí)行環(huán)境你畫(huà)在紙上的架構(gòu)有一半會(huì)在第一次真實(shí)運(yùn)行時(shí)被推翻。與其花一個(gè)月搭一個(gè)看起來(lái)正確的架構(gòu)不如花兩天搭一個(gè)一定能跑的最小版然后用真實(shí)失敗去修正架構(gòu)。關(guān)鍵觀察所以這篇的核心動(dòng)作就一個(gè)用最短的路徑讓 Agent 第一次真正跑起來(lái)然后誠(chéng)實(shí)地記錄它為什么不能直接上線。最小版本長(zhǎng)這樣沒(méi)有任何花哨的東西。一個(gè)循環(huán)LLM 決定調(diào)工具 → 工具執(zhí)行 → 結(jié)果塞回上下文 → LLM 繼續(xù)直到它覺(jué)得該回答了。核心洞察MVP 的意義不是證明能跑而是用最快的速度暴露它會(huì)怎么死。這個(gè)最小版的價(jià)值不在于它能做什么而在于它把Agent 運(yùn)行的每一個(gè)環(huán)節(jié)都攤開(kāi)在你面前LLM 決策、工具選擇、參數(shù)傳遞、結(jié)果解析、終止判斷——每一個(gè)環(huán)節(jié)都可能出問(wèn)題而這些問(wèn)題只有最小版能讓你一個(gè)一個(gè)看清楚。最終方案100 行的最小 Agent下面是 Repo Doctor v0 的完整實(shí)現(xiàn)用 Python 手寫(xiě)一個(gè) tool-calling loop不依賴任何框架就是為了看清每一環(huán)# repo_doctor/v0/main.py —— 最小可跑版本約 100 行 import subprocess, json from openai import OpenAI client OpenAI(base_urlhttps://api.deepseek.com, api_key...) SYSTEM 你是倉(cāng)庫(kù)診斷助手。你可以調(diào)用工具來(lái)調(diào)查代碼庫(kù)。 可用工具 - grep(keyword): 在倉(cāng)庫(kù)中搜索關(guān)鍵字返回匹配的文件和行 - read_file(path): 讀取指定文件內(nèi)容 調(diào)查充分后直接輸出結(jié)論。 TOOLS [ {type: function, function: { name: grep, description: 在倉(cāng)庫(kù)中搜索關(guān)鍵字返回匹配的文件和行, parameters: {type: object, properties: { keyword: {type: string}}, required: [keyword]}}}, {type: function, function: { name: read_file, description: 讀取指定文件內(nèi)容, parameters: {type: object, properties: { path: {type: string}}, required: [path]}}}, ] def call_tool(name, args, repo): if name grep: r subprocess.run([grep, -rn, args[keyword], repo], capture_outputTrue, textTrue, timeout10) return r.stdout[:3000] or (無(wú)匹配) if name read_file: with open(f{repo}/{args[path]}, encodingutf-8, errorsignore) as f: return f.read()[:3000] return (未知工具) def run_agent(query, repo, max_steps6): messages [{role: system, content: SYSTEM}, {role: user, content: f倉(cāng)庫(kù)路徑 {repo}問(wèn)題{query}}] for _ in range(max_steps): resp client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTOOLS) msg resp.choices[0].message if msg.tool_calls: messages.append(msg) for tc in msg.tool_calls: result call_tool(tc.function.name, json.loads(tc.function.arguments), repo) messages.append({role: tool, tool_call_id: tc.id, content: result}) else: return msg.content return 達(dá)到最大步數(shù)仍未完成 if __name__ __main__: print(run_agent(這個(gè)項(xiàng)目里支付相關(guān)的邏輯在哪, /path/to/hello-agents))跑一次輸出大概長(zhǎng)這樣結(jié)論支付相關(guān)邏輯在 payment.py 中核心函數(shù)是 payment_process() 它負(fù)責(zé)訂單支付和回調(diào)處理。建議查看該函數(shù)附近的 payment_callback()。這一版能跑。但先別急著高興我們仔細(xì)看這個(gè)結(jié)論——它有一個(gè)致命的問(wèn)題payment_process()這個(gè)函數(shù)倉(cāng)庫(kù)里根本不存在。它是 Agent 編出來(lái)的。下一篇會(huì)專門(mén)解剖這個(gè)失敗。在進(jìn)入為什么不能上線之前先把這 100 行代碼的每個(gè)關(guān)鍵環(huán)節(jié)點(diǎn)一遍你就知道最小版到底暴露了哪些環(huán)節(jié)代碼片段環(huán)節(jié)潛在問(wèn)題最小版就埋著SYSTEMTOOLS提示與工具描述工具描述含糊LLM 會(huì)選錯(cuò)工具第 41 篇修call_tool工具執(zhí)行參數(shù)不校驗(yàn)、結(jié)果截?cái)?3000 字符第 44 篇修for _ in range(max_steps)終止控制最大 6 步可能沒(méi)查完就停或燒光第 41 篇修messages.append上下文管理無(wú)限增長(zhǎng)長(zhǎng)任務(wù)爆上下文第 45 篇修r(nóng)eturn msg.content輸出結(jié)論無(wú)證據(jù)校驗(yàn)幻覺(jué)直接進(jìn)答案第 40、42 篇修這段 100 行代碼每一行都對(duì)應(yīng)著后面一篇要解決的問(wèn)題。這就是最小版的價(jià)值——它不是最終產(chǎn)品的閹割版而是問(wèn)題清單的具象化。架構(gòu)圖 / 流程圖把上面代碼的執(zhí)行流程畫(huà)出來(lái)你會(huì)看到它的樸素看著很正常對(duì)吧但問(wèn)題就藏在最后一步——那個(gè)payment_process()到底存不存在Agent 根本沒(méi)驗(yàn)證。第二張圖這個(gè)循環(huán)的隱患標(biāo)注版發(fā)布提示可用 draw.io 重畫(huà)成正式圖與 Mermaid 圖形成雙圖組合用戶問(wèn)句 │ ▼ ┌────────────┐ ① 工具描述含糊 → 可能選錯(cuò)工具40/41 篇 │ Agent(LLM) │──────────────────────────────┐ └─────┬──────┘ │ │ ② 參數(shù)不校驗(yàn) → 可能傳錯(cuò)參數(shù)41 篇 │ ▼ │ ┌────────────┐ ③ 結(jié)果截?cái)?3000 字符 │ │ call_tool │ 可能丟關(guān)鍵信息44 篇 │ └─────┬──────┘ │ │ ④ 結(jié)果塞回上下文無(wú)校驗(yàn) │ ▼ │ ┌────────────┐ ⑤ 結(jié)論無(wú)證據(jù)檢查 │ │ Agent(LLM) │ 幻覺(jué)直接進(jìn)答案40/42 篇 │ └─────┬──────┘ │ │ ⑥ 輸出結(jié)論 │ ▼ │ 答案 ?───────────────────────────────────┘ payment_process() 可能是編的這張圖把最小版的每一個(gè)薄弱環(huán)節(jié)都標(biāo)了出來(lái)。后面整整十篇就是逐個(gè)把這些隱患標(biāo)注換成已修復(fù)。為什么不能直接上線這一版跑通了但我把它能跑和能上線分得很清。下面這張清單就是我故意留的技術(shù)債也是后面整整十一篇的伏筆#技術(shù)債后果后面哪篇解決1沒(méi)有 Task Router三種任務(wù)混在一起該定位時(shí)去解釋472沒(méi)有 Planner查夠了沒(méi)全憑感覺(jué)草率下結(jié)論 / 燒 Token40、413工具描述太模糊參數(shù)沒(méi)約束選錯(cuò)工具、傳錯(cuò)參數(shù)41、424沒(méi)有 Reviewer結(jié)論無(wú)證據(jù)鏈幻覺(jué)成災(zāi)payment_process()可能不存在40、425沒(méi)有 State調(diào)查過(guò)程不記錄無(wú)法回溯為什么這樣想426沒(méi)有評(píng)估集改完好壞不知道越改越玄學(xué)437上下文無(wú)上限讀文件會(huì)撐爆長(zhǎng)文件直接溢出44、498出錯(cuò)就死無(wú)兜底工具報(bào)錯(cuò)整個(gè)流程崩449無(wú)法復(fù)現(xiàn)模型/Prompt/工具都沒(méi)鎖版本同一個(gè)問(wèn)題兩次答案不同4510沒(méi)有成本/延遲監(jiān)控?zé)嗌?Token 全靠猜46這一篇的價(jià)值就是把上面這十個(gè)坑提前擺在明面上。它們不是以后可能會(huì)出問(wèn)題而是現(xiàn)在就已經(jīng)埋下了只等真實(shí)問(wèn)句來(lái)引爆。設(shè)計(jì)權(quán)衡候選方案優(yōu)點(diǎn)缺點(diǎn)為什么不選一步到位寫(xiě)完整架構(gòu)一次成型一個(gè)月跑不起來(lái)積累一堆未驗(yàn)證假設(shè)推遲了看到真實(shí)失敗的時(shí)間用 LangGraph 框架搭省事、規(guī)范掩蓋底層細(xì)節(jié)出了問(wèn)題看不懂先手寫(xiě) loop看清每一環(huán)100 行手寫(xiě)最小版快、透明、暴露問(wèn)題功能殘缺最快暴露它會(huì)怎么死關(guān)于為什么不用 LangGraph值得單獨(dú)說(shuō)不是 LangGraph 不好而是在這個(gè)階段你需要的不是框架的便利而是對(duì)每一環(huán)的可見(jiàn)性。手寫(xiě) loop你能精確看到LLM 這次調(diào)了什么工具、傳了什么參數(shù)、工具回了什么。等這套東西你想清楚了第 49 篇再談要不要換成框架。常見(jiàn)誤區(qū)FAQQ1MVP 越少越好是不是連工具都只留一個(gè)最少兩個(gè)。一個(gè)工具比如只留 read_file無(wú)法暴露工具選擇這個(gè)環(huán)節(jié)的問(wèn)題——而工具選錯(cuò)恰恰是 Agent 最常見(jiàn)的失敗之一第 40 篇的 Tool Selection Error。Q2為什么不用 LangGraph 搭 MVP對(duì)初學(xué)者來(lái)說(shuō)框架會(huì)掩蓋每一環(huán)發(fā)生了什么。手寫(xiě) 100 行 loop你被迫面對(duì)工具調(diào)用、參數(shù)解析、結(jié)果回填這些最底層的問(wèn)題——這些問(wèn)題在框架里被封裝了你直到出 bug 才知道它們存在。Q3這 100 行代碼最后會(huì)被扔掉嗎不會(huì)全扔。它的工具調(diào)用循環(huán)骨架會(huì)被保留后續(xù)的 Task Router、Planner、Reviewer 都是在這個(gè)骨架上加節(jié)點(diǎn)而不是推倒重來(lái)。MVP 不是一次性用品是后續(xù)版本的可運(yùn)行的基線。Q4怎么判斷 MVP夠了一個(gè)簡(jiǎn)單標(biāo)準(zhǔn)它能完整跑完一次端到端任務(wù)哪怕結(jié)果錯(cuò)誤。能跑 會(huì)錯(cuò)就是最好的 MVP 狀態(tài)——因?yàn)橄乱徊骄褪侨ソ馄仕趺村e(cuò)的第 40 篇??偨Y(jié)? 先做最小可跑版本故意砍掉所有高級(jí)節(jié)點(diǎn)。? 100 行手寫(xiě) loop就是為了看清每一環(huán)。? 這 100 行里每一行都對(duì)應(yīng)一篇后續(xù)要解決的問(wèn)題。? 這一版能跑但埋了 10 個(gè)技術(shù)債。? 鐵律MVP 的意義不是證明能跑而是最快暴露它會(huì)怎么死。? 下一篇讓這個(gè)最小版跑真實(shí)案例看它第一次做錯(cuò)。參考資料OpenAI Function Calling 文檔 → 為什么引用tool-calling loop 的 API 用法是這 100 行的基礎(chǔ)?!禩he Pragmatic Programmer》tracer bullet 概念 → 為什么引用先打通一條最小鏈路再擴(kuò)展正是本篇的方法論來(lái)源。系列導(dǎo)航上一篇AI Agent 工程實(shí)踐38從需求到 Agent 架構(gòu)——為什么需要這些節(jié)點(diǎn)下一篇AI Agent 工程實(shí)踐40第一次失敗——Agent 為什么會(huì)做錯(cuò)本文是 [AI Agent 工程實(shí)踐] 系列的第 39 篇。