先講結論,後面很長,其實不一定要看,我只是筆記
很多人在看 Agent 系統時,會不小心把所有事情都算在 LLM 身上。
最後文章就會寫成一種很神奇的敘事:彷彿 LLM 看完一句話,突然就自己把專案 build 好、跑起來、開瀏覽器、順便截圖,像一個住在 GPU 裡的 DevOps 工程師。
但實際上不是這樣。
LLM 比較像「負責判斷下一步要做什麼的人」,真正去動手做事的是 Agent。
所以這種系統不是這樣:
使用者輸入
-> LLM
-> 回答而比較接近這樣:
使用者輸入
-> Agent 組 context + 注入 tools
-> LLM 判斷下一步
-> Agent 執行工具
-> Agent 把結果丟回 LLM
-> LLM 再判斷下一步
-> 重複幾輪
-> 最後才輸出人類看得懂的回答
一句話:LLM 負責想,Agent 負責做。
這件事如果一開始沒分清楚,後面你在看 code、debug、或寫文章時,很容易全部混在一起。然後最後每個元件都被誤會,連 bug 都不知道該怪誰。
整篇用同一個例子講,不然一旦開始抽象化,大家的大腦就會自動切去別的地方
幫我把專案編譯,執行起來,用 Edge 開啟,截圖給我看這句話對人來說只有一句,但對系統來說,它其實是一個多步驟 workflow。
因為這裡面至少隱含了下面幾件事:
1. build 專案
2. run 專案
3. 拿到 URL
4. 用 Edge 開啟
5. 截圖
6. 回結果關鍵點在於:使用者不會幫你拆步驟,系統要自己拆。
而拆步驟的過程,有些是 Agent 做的,有些是 LLM 做的。這就是整篇要講的核心。
先把責任分清楚,不然後面會一直混
在進入 transcript 前,先把邊界講清楚。
1. 收使用者輸入
2. 組 system prompt / project context / history
3. 把可用 tools 告訴 LLM
4. 收到 tool call 後真的執行工具
5. 做權限檢查與 guardrails
6. 壓縮過長的 tool output
7. 控制 loop,不讓系統鬼打牆LLM 通常負責這些事情:
1. 讀懂使用者要什麼
2. 根據上下文判斷下一步
3. 選擇要呼叫哪個 tool
4. 產生 tool arguments
5. 根據 tool result 調整後續策略
6. 最後輸出自然語言答案所以你可以先記一個非常實用的判斷方式:
凡是偏 orchestration、execution、guardrails 的,多半是 Agent;凡是偏 reasoning、planning、tool selection 的,多半是 LLM。
Round 0:Agent 送給 LLM 的原文,長什麼樣子?
這一段是最容易被忽略,但其實最值得打開來看的部分。
因為 LLM 並不是直接看到「使用者原封不動的一句話」,而是看到 Agent 幫它整理過的一整包 context。
也就是說,LLM 看到的世界,通常是 Agent 預先整理過的版本。
[
{
"role": "system",
"content": "You are an AI agent that can operate on a software project. Use tools when necessary. Do not pretend actions succeeded unless tool results confirm success."
},
{
"role": "system",
"content": "[project]\\nPath: /workspace/MyApp\\nStack: ASP.NET Core"
},
{
"role": "system",
"content": "[capability]\\nCan build, run, open browser, take screenshot"
},
{
"role": "system",
"content": "[workflow]\\nUser intent likely includes build, run, browser open, and screenshot"
},
{
"role": "user",
"content": "幫我把專案編譯,執行起來,用 Edge 開啟,截圖給我看"
}
]
除了 messages 之外,Agent 還會把可用 tools 一起交給 LLM。
[
{ "name": "build_project" },
{ "name": "run_project" },
{ "name": "open_browser" },
{ "name": "take_screenshot" }
]
這裡有兩個很重要的點。
第一,LLM 能看到哪些資訊,不是它自己決定的,是 Agent 組給它的。
第二,LLM 能用哪些工具,也不是它自己發明的,是 Agent 暴露給它的。
所以如果你哪天看到模型選錯工具,先別急著怪模型,先看是不是 tools 設計就很迷惑人。
Round 1:LLM 第一次回來,通常不是回答,而是 tool call
在這個案例裡,LLM 第一次大概率不會直接回答你「好的我幫你完成了」。
因為它根本還沒做任何事,它只會先決定第一步該做什麼。
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"function": {
"name": "build_project",
"arguments": "{}"
}
}
]
}
這個 response 的真正意思是:
「我判斷第一步應該先 build_project。」
注意,它沒有真的 build。
它只是產生了一個格式正確的 tool call,要求 Agent 去做事。
Round 1.5:Agent 才真的去 build
收到上面的 tool call 之後,真正開始工作的才是 Agent。
這一步通常包含:
1. 找到 build_project 對應的 tool
2. 檢查這輪是否允許執行
3. 檢查 command 是否安全
4. 真正執行 build
5. 取得輸出
6. 必要時壓縮 log假設 build 成功,Agent 拿到的結果可能會被整理成這樣:
Build succeeded
這裡要再講一次:全部都不是 LLM 做的。
LLM 不會自己開 terminal,不會自己下 dotnet build,也不會自己檢查 shell command 安不安全。
Round 2:Agent 把 build 結果丟回 LLM
Agent 執行完 tool 之後,不是直接把結果拿去回使用者。
它會先把結果包成一則 role=tool 的訊息,再送回 LLM,讓 LLM 繼續判斷下一步。
{
"role": "tool",
"tool_call_id": "call_1",
"name": "build_project",
"content": "Build succeeded."
}
這段內容不是寫給使用者看的,而是寫給 LLM 看的。
它的意思是:剛剛你要求的 build_project,我真的幫你做了,結果成功。
Round 3:LLM 根據 build 結果,決定下一步是 run
現在 LLM 的上下文裡,已經有「build succeeded」這個訊號了。
所以它很自然會往下一步走:
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_2",
"function": {
"name": "run_project",
"arguments": "{}"
}
}
]
}
這裡它做的事情不是執行 run,而是:
根據目前上下文,判斷下一步應該叫 Agent 去 run_project。
然後 Agent 真的去執行之後,可能拿到原始輸出:
Application started. Listening on http://localhost:5000
但很多 runtime 不會把完整 output 原封不動塞回去,因為 log 很容易長到失控。
比較常見的是 Agent 先做一次壓縮,只留下對下一輪真正有用的資訊:
{
"role": "tool",
"tool_call_id": "call_2",
"name": "run_project",
"content": "Run succeeded. URL: http://localhost:5000"
}
這一步也很重要,因為它再次說明了一件事:
工具輸出的整理與壓縮,也是 Agent 在做,不是 LLM 在做。
Round 4:LLM 讀到 URL 之後,才知道該開哪裡
現在 LLM 看到了這條訊息
Run succeeded. URL: http://localhost:5000
所以它下一步就能很合理地要求打開瀏覽器,而且把參數補齊:
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_3",
"function": {
"name": "open_browser",
"arguments": "{\"browser\":\"edge\",\"url\":\"http://localhost:5000\"}"
}
}
]
}
這裡就能很清楚看出 LLM 的價值:它不是單純照表操課,而是會根據前一輪 tool result 把參數接起來。
但再次強調,它還是沒有真的打開 Edge。那件事依然是 Agent 去做。
Round 5:Agent 開完 Edge,再把結果送回去
假設瀏覽器順利打開,Agent 會把結果包成下一條 tool message:
{
"role": "tool",
"tool_call_id": "call_3",
"name": "open_browser",
"content": "Edge opened successfully."
}
這一輪之後,LLM 就會知道:瀏覽器已經開好了,接下來可以截圖。
Round 6:LLM 要求截圖
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_4",
"function": {
"name": "take_screenshot",
"arguments": "{}"
}
}
]
}
然後 Agent 真正去截圖,成功後再回一條 tool result
{
"role": "tool",
"tool_call_id": "call_4",
"name": "take_screenshot",
"content": "Screenshot saved: /artifacts/screen.png"
}
到了這裡,整個任務才算真的完成。
不是因為 LLM 說完成了,而是因為工具真的執行過,而且結果回來了。
最後一輪:LLM 才把整件事整理成給人看的回答
前面幾輪其實都不是在跟使用者講話,而是在做系統內部協作。
直到所有必要步驟都完成後,LLM 才會輸出最後一段人類可讀的答案
{
"role": "assistant",
"content": "專案已成功編譯並執行,已使用 Edge 開啟並完成截圖。"
}
所以你在產品 UI 上最後看到的,通常只是一個收尾。
真正精彩、也真正麻煩的地方,其實都藏在前面的 tool-calling loop 裡。
把整件事濃縮成一張圖
使用者: 幫我把專案編譯,執行起來,用 Edge 開啟,截圖給我看
Agent:
- 組 context
- 提供 tools
LLM:
- 先選 build_project
Agent:
- 真的去 build
- 回傳 Build succeeded
LLM:
- 再選 run_project
Agent:
- 真的去 run
- 回傳 URL
LLM:
- 再選 open_browser
Agent:
- 真的開 Edge
LLM:
- 再選 take_screenshot
Agent:
- 真的截圖
LLM:
- 最後整理成人話回答
這個順序不能倒過來看。
你如果只看最後一句話,就會以為整件事是 LLM 一次完成的;但你如果把 transcript 攤開來看,就會發現 LLM 比較像是在指揮流程,Agent 才是在工地裡真的搬磚的人。
最重要的理解
LLM 不會真的做事
LLM 只是在決定下一步
真正做事的是 Agent + Tools
真正把系統串起來的是 Orchestration
所以你之後在看任何 Agent codebase 時,最值得先問的不是「它用哪個 model」,而是下面這三題:
1. Agent 到底送了什麼給 LLM?
2. LLM 回來的是文字,還是 tool call?
3. tool 是誰真的執行的?結果又是誰整理後再送回去的?
這三題一旦分清楚,你就比較不會把 Agent orchestration 跟 LLM reasoning 寫成一鍋粥。
也比較不會在模型連續 build 三次時,第一反應就是覺得模型智商有問題。很多時候,那其實是 runtime 在提醒你:哥,您的 loop control 還沒寫完。
---
The bug existed in all possible states.
Until I ran the code.