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 不知道「第一個」是指誰。這就是「單次問答」的本質:沒有上下文,每次都是白紙一張

第一層示意圖:單次問答,沒有記憶 第 1 次呼叫 使用者:推薦東京景點 AI:淺草寺等 3 個 ✕ 兩次互不相干 第 2 次呼叫(全新、無關) 使用者:第一個怎麼去? 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,知道「第一個」指的是淺草寺。跟第一層的差別:輸出還是純文字,但輸入多了「之前說過什麼」

第二層示意圖:多輪對話,靠 history 累積記憶 使用者:推薦東京景點 AI:淺草寺等 3 個 history history:存下 Q1 + A1 使用者:第一個怎麼去? 連同歷史送出 AI:淺草站,銀座線

對應這個資料夾裡的 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 自己決定並呼叫工具 使用者:查天氣 AI:需要工具 checkWeather() 結果:陣雨 24°C 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,固定順序的直線流程 ① 查天氣 ② 查開放時間 ③ 排行程 ④ 寫提醒信

對應這個資料夾裡的 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 讓它們平行處理,彼此不互相等待——不是「一組一組排隊做」,而是「同時開工」。跟第五層的差別:第五層是一份工作內部的固定步驟;第六層是很多份工作之間怎麼分配、怎麼平行跑,兩者可以疊在一起用(每張票內部照樣走第五層的固定流程)。

第六層示意圖:多張票平行分工,每張票內部走固定流程 同時開工(Promise.all) 票 A ①查天氣 ②開放時間 ③排行程 ④提醒信 票 B ①查天氣 ②開放時間 ③排行程 ④提醒信 票 C ①查天氣 ②開放時間 ③排行程 ④提醒信

對應這個資料夾裡的 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 身上。