LLM 邏輯進階筆記:從一問一答到分工協作,六個層級
這是一篇學習筆記,用虛擬碼(pseudo code)整理我對 LLM 邏輯的理解——從最陽春的「問一句答一句」,一路疊加到「多個角色分工、平行處理很多工作」。全篇用同一個例子貫穿:幫忙規劃東京旅行的 AI 助理,每一層只加一個新概念,方便對照差異在哪裡。
六層都有對應的可執行 .js demo,跑之前先設定環境變數:
export GEMINI_API_KEY="你的 key" # 去 https://aistudio.google.com/apikey 免費申請
export GEMINI_MODEL="gemini-3.1-flash-lite" # 選填,預設就是這個
第一層:單次問答——沒有記憶,問完就忘
最基礎的型態:你丟一句話進去,AI 吐一句話出來,兩者之間沒有任何關聯。
const reply1 = await ai("推薦三個東京景點")
console.log(reply1)
// "淺草寺、明治神宮、澀谷十字路口"
const reply2 = await ai("那第一個要怎麼去?")
console.log(reply2)
// "不好意思,請問您是指哪個景點呢?"
第二句失敗了,因為每次呼叫 ai() 都是全新的、獨立的一次對話——AI 不知道「第一個」是指誰。這就是「單次問答」的本質:沒有上下文,每次都是白紙一張。
對應這個資料夾裡的 qa.js,分兩次執行就能重現這個問題:
node qa.js "推薦三個東京景點"
node qa.js "那第一個要怎麼去?"
兩次是各自獨立的 process,沒有共享的 history,第二次自然答不出「第一個」是誰。
第二層:多輪對話——有記憶,靠 history 串起來
加一個 history,把每一輪的問與答都存起來,下次呼叫時整包帶給 AI。
let history = []
async function ask(question) {
history.push({ role: "user", text: question })
const reply = await ai(history) // 把整段歷史都傳進去,不是只傳這一句
history.push({ role: "ai", text: reply })
return reply
}
await ask("推薦三個東京景點")
// "淺草寺、明治神宮、澀谷十字路口"
await ask("那第一個要怎麼去?")
// "淺草寺最近的車站是淺草站,搭地鐵銀座線或淺草線都能到。"
同樣一句「那第一個要怎麼去?」,這次答對了——差別只在於 AI 看得到完整的 history,知道「第一個」指的是淺草寺。跟第一層的差別:輸出還是純文字,但輸入多了「之前說過什麼」。
對應這個資料夾裡的 chat.js:
node chat.js
依序輸入「推薦三個東京景點」、「那第一個要怎麼去」,這次 history 一直在累積,AI 答得出來。
第三層:Agent——不只回答,還會自己動手做事
再加一樣東西:工具(tools)。AI 不再只吐文字,而是自己判斷「這件事需要用工具才能完成」,然後呼叫對應的函式。
const result = await ai("幫我查一下禮拜五東京的天氣", {
tools: [checkWeather, addCalendarReminder],
})
// AI 內部自己想:
// 1. 先呼叫 checkWeather({ city: "東京", date: "禮拜五" }) 查天氣
// 2. 判斷會下雨
// 3. 回報結果,並提醒使用者帶傘
console.log(result)
// "禮拜五東京有陣雨、氣溫 24°C,記得帶傘。"
跟第二層的差別:第二層的 AI 只能「講」,第三層的 AI 可以「做」——它自己決定要不要用工具、用哪個、用幾次,你只需要把工具準備好交給它。
對應這個資料夾裡的 agent-tools.js(簡化版:先問 AI 要呼叫哪個工具、程式收到決定才真的執行,再把工具結果拿回去換一句自然語言回覆):
node agent-tools.js "幫我查一下禮拜五東京的天氣"
第四層:多角色協作——兩個 AI 輪流講話,互相把關
單一 agent 換成兩個各自扮演角色的 agent,輪流發言,每輪都把目前為止的討論整包帶給下一位——這樣後面發言的角色才看得到前面說過什麼。
let discussion = "客戶需求:東京三天兩夜,行程盡量排滿但不要太趕。"
for (let round = 0; round < 4; round++) {
const plannerSays = await ai(`${discussion}\n你是行程規劃師,提出一版行程。`)
discussion += `\n規劃師:${plannerSays}`
const checkerSays = await ai(`${discussion}\n你是行程把關者,檢查時間會不會太趕,太趕就指出要拿掉哪個行程。`)
discussion += `\n把關者:${checkerSays}`
if (checkerSays.includes("時間允許")) break
}
「規劃師」先提一版行程,「把關者」檢查時間會不會太趕、太趕就點名要拿掉哪個景點或調整順序;兩人來回幾輪,直到把關者說「時間允許」為止。跟第三層的差別:第三層是一個 AI 自己決定要不要用工具;第四層是兩個 AI 用對話互相修正對方。
對應這個資料夾裡的 team.js:
node team.js "客戶需求:東京三天兩夜,行程盡量排滿但不要太趕"
第五層:Workflow——把步驟寫死,變成固定流程
跟第四層不一樣的地方:多角色協作是「你一言我一語,走到哪算哪」,是即興的;Workflow 是先把整套步驟定死(像生產線),每一步做什麼、順序是什麼都寫在流程定義裡,上一步的輸出直接餵給下一步當輸入。
const workflow = [
{ name: "查天氣", run: (ctx) => ai(`查「${ctx.city}」${ctx.date}的天氣`, { tools: [checkWeather] }) },
{ name: "查開放時間", run: (ctx, prev) => ai(`已知天氣「${prev}」,查主要景點的開放時間`, { tools: [checkOpeningHours] }) },
{ name: "排行程", run: (ctx, prev) => ai(`依照天氣與開放時間「${prev}」,排一份三天兩夜的行程表`) },
{ name: "寫提醒信", run: (ctx, prev) => ai(`把這份行程「${prev}」整理成一封提醒信,內容包含建議帶的物品`) },
]
let output = null
for (const step of workflow) {
output = await step.run({ city: "東京", date: "禮拜五" }, output)
console.log(`【${step.name}】完成`)
}
固定跑「查天氣 → 查開放時間 → 排行程 → 寫提醒信」四步,每一步都吃上一步的產出。跟第四層的差別:第四層沒有固定終點,靠角色對話「聊」出結果;第五層路徑是死的,適合已經想清楚、每次流程都一樣的重複性工作。
對應這個資料夾裡的 workflow.js(前兩步用假資料模擬查詢 API,後兩步真的呼叫 AI):
node workflow.js "東京" "禮拜五"
第六層:Workflows——同時開很多張票,平行分工處理
規模再大一點,常常不是只服務一位客人,而是同時有一批工作要處理——這時候會先「開票」,把每一份工作拆成一張票,多個 worker 平行認領處理。
const tickets = await pm.listTickets({ status: "todo" })
// 例如:10 位客人,各自的東京行程需求
await Promise.all(
tickets.map(async (ticket) => {
await pm.setStatus(ticket.id, "in_progress")
// 每張票各自完整跑一次第五層的 workflow
let output = ticket.request
for (const step of workflow) {
output = await step.run(ticket, output)
}
await pm.setStatus(ticket.id, "done")
})
)
10 位客人的需求同時開成 10 張票,Promise.all 讓它們平行處理,彼此不互相等待——不是「一組一組排隊做」,而是「同時開工」。跟第五層的差別:第五層是一份工作內部的固定步驟;第六層是很多份工作之間怎麼分配、怎麼平行跑,兩者可以疊在一起用(每張票內部照樣走第五層的固定流程)。
對應這個資料夾裡的 workflows.js:用一個寫死的陣列假裝三張票(不用真的 PM 系統),每張票各自跑一次第五層的四步驟,Promise.all 讓它們平行處理:
node workflows.js
跑起來會看到三張票的 log 交錯出現,不是票 A 全部做完才輪到票 B——這就是「平行」跟「排隊」的差別。
workflows.js 用寫死的陣列假裝票務系統,真實世界已經有現成的正式對應:Claude Code 內建的 Workflow 工具——本質上就是這段 pseudo code 的正式版,agent() 對應這裡的 ai(),pipeline() 對應「上一步輸出接下一步輸入」,parallel() 對應多張票同時跑。差別是它把 review/驗證這類「懷疑自己」的步驟也內建成標準寫法(獨立 agent 反駁、多數決才算過)。
小結:六層疊加表
| 層級 | 新增的能力 | 例子裡多了什麼 | Demo |
|---|---|---|---|
| 1. 單次問答 | 無 | 問完就忘,答不出「第一個」是誰 | qa.js |
| 2. 多輪對話 | 記憶(history) | 記得上一句提過淺草寺 | chat.js |
| 3. Agent | 工具呼叫 | 自己查天氣、自己加提醒 | agent-tools.js |
| 4. 多角色協作 | 角色間對話、互相修正 | 規劃師提案、把關者砍太趕的行程 | team.js |
| 5. Workflow | 固定步驟串接 | 查天氣→查開放時間→排行程→寫提醒信 | workflow.js |
| 6. Workflows | 多份工作平行分工 | 3 張票(3 個城市)同時開工 | workflows.js |
每一層都是在前一層的基礎上加一件事,不是推翻重來——真正在用的系統,往往是把這六層疊在一起:一個開了很多票的 workflows 系統,每張票內部走固定 workflow,某幾步又是由多角色協作或單一 agent 完成的。
一個重要提醒:這幾支 demo 到底哪裡是真的?
GEMINI_API_KEY 打的是真的 Gemini API,AI「想」的那一步是真的;假的只有「工具」本身——checkWeather()、checkOpeningHours() 是我們自己寫死回傳假資料,沒有真的去問氣象局。換句話說:AI 的大腦是真的,手腳(跟外部世界打交道的動作)是假的,換成真的 API 其他程式碼完全不用改。
也因此 AI 沒辦法分辨工具回報是真的查出來的、還是寫死的——不管哪一種,AI 看到的都只是同一句文字,只能照單全收。查證資料真假的責任在寫程式的人身上,不在 AI 身上。