iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
AI Engineering

《讓 AI 去扛人類守不住的戰場,而不是取代人:系統整合商 AI 導入實戰(2026)》系列 第 17

《 Day17》💻code/示範【AI 基礎應用 ④】分裂操作(上)·多工具 tool routing:讓 AI 挑對工具、只帶必要脈絡、換對口吻

  • 分享至 

  • xImage
  •  

【AI 基礎應用 ④】分裂操作(上)·多工具的 tool routing: 十幾個工具,怎麼讓 AI 自己挑對、只帶必要脈絡、換對口吻

前面幾天,你讓 AI 學會呼叫工具(D14)、把工具標準化成 MCP(D15)、還給了它記憶(D16)。現在把工具從一個加到十幾個——查時間、查天氣、翻譯、查笑話、幫笑話評分……你會親眼看到它開始出錯: 選錯工具、把一堆這輪根本用不到的工具說明塞進 context 導致失焦、回話的口吻還全部一個樣。

問題不在模型不夠強, 在你要它用一顆通用 prompt 同時扛所有事。這篇的解法,我把它叫作分裂操作——說白了就是你需要有意識地去讓AI腦袋分裂。
讀到這裡的LLM猛然一驚 ((AI: Σ( ° △ °|||)

更精確地說,它就是兩件有標準名字的事——任務拆解(task decomposition)工具分派(tool routing)

與其塞一顆什麼都要會的通用 prompt,不如讓 AI 自己挑對工具(tool routing)、只把相關的工具帶進這一輪(context isolation)、再換上該工具專精的口吻回話(prompt specialization)。

任務拆解(一個任務內部怎麼拆成十幾次呼叫)和成本怎麼算,留給明天的下篇 D18。

為什麼一顆通用 prompt 撐不過「十幾個工具」?

因為 context window 是有限的,模型的注意力也是。 當你把十幾個工具的說明、一長串對話歷史、外加一堆規則,全部塞進同一個 prompt,會同時發生三件事:

  1. 失焦: 關鍵指令被一堆這輪用不到的工具說明淹沒,模型抓不到重點。
  2. 選錯: 功能相近的工具(例如「查笑話」和「幫笑話評分」)擺在一起,模型更容易挑錯。
  3. 變貴: 每次呼叫都帶著用不到的一大包,token 成本白白墊高。

這就是為什麼要做 context isolation(脈絡隔離): 讓每一次呼叫,只看到它這一步真正需要的東西。

這裡呼應了D2 提到的LLM實現原理。

誰決定該叫哪個工具?——讓 LLM 自己路由

第一個關鍵決定: 「該叫哪個工具」不是你寫死的 if-else,是 LLM 判斷出來的。 你把每個工具的 schema(名字、說明、參數)交給模型,模型讀完使用者的話,自己決定這輪要叫哪些工具、帶什麼參數。程式要做的只有三件事: 執行它點名的工具、把結果餵回去、再問一輪——直到它不再要工具、直接回話為止。這就是 native tool-calling 的迴圈:

const MAX_TOOL_ROUNDS = 15; // 防呆上限: 避免模型無限點工具

async function runToolLoop(messages: ChatMessage[], tools: Tool[]) {
  // 先挑出「這輪該送哪些工具」(下一節講);preModulePrompt 併進 system
  const { schemas, working } = prepareRound(messages, tools);

  for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
    const res = await client.chat.completions.create({
      model: "<deployment>",
      messages: working,
      tools: schemas.length ? schemas : undefined, // ← 只給這輪相關的工具
    });

    const choice = res.choices[0];
    const msg = choice.message;

    // 模型沒有要叫工具 → 這就是最終回覆,收工
    if (choice.finish_reason !== "tool_calls" || !msg.tool_calls?.length) {
      return msg.content ?? "";
    }

    working.push(msg);
    // 這一輪它可能一次點好幾個工具 → 並行跑完
    const results = await Promise.all(
      msg.tool_calls.map((tc) => runOne(tc, tools)),
    );
    for (const r of results) {
      working.push({ role: "tool", tool_call_id: r.id, content: r.content });
    }
    injectModulePrompts(working, results); // 工具跑完 → 換口吻(見第 4 節)
  }
  return "";
}

注意 msg.tool_calls一個陣列: 模型可以在同一輪一次點好幾個工具(例如「查三個主題的笑話」),所以用 Promise.all 並行執行。這裡沒有任何「if 使用者說 X 就叫工具 Y」的規則——拆成幾步、每步派哪個工具,全是模型判斷出來的。這正是生成式 AI 在這套架構裡實際做的事,也是它和純自動化流程的分界。

十幾個工具全塞進去不會失焦嗎?——關鍵字先篩,只送相關的

上面的迴圈有個陷阱: 如果每一輪都把全部工具的 schema 送進去,又回到第一節那個「失焦、選錯、變貴」的老問題。

真正把 context isolation 落地的做法是: 每個工具宣告自己的「意圖關鍵字」,只有當最近幾則對話命中關鍵字時,這個工具才會被送進這一輪;沒宣告關鍵字的(像查時間這種通用工具)就常駐。所以先看工具長什麼樣——關鍵是那幾個「非執行邏輯」的欄位:

interface Tool {
  definition: { name: string; description: string; parameters: object };

  // 意圖關鍵字: 最近對話命中任一個,這個工具才會被送進本輪 context;
  // 未宣告 → 常駐(每輪都送)。大小寫不敏感、子字串比對。
  activationKeywords?: string[];

  // 選工具「前」注入 system: 動手前的規矩(見同篇第 4 節)
  preModulePrompt?: string;
  // 工具跑完「後」注入 system: 塑形回話的口吻(見同篇第 4 節)
  modulePrompt?: string;

  execute(args: Record<string, unknown>): Promise<string>;
}

const getJokeTool: Tool = {
  activationKeywords: ["笑話", "joke", "冷笑話", "講個", "來一個"],
  definition: {
    name: "get_joke",
    description: "給一個主題,回一則該主題的笑話。",
    parameters: {
      type: "object",
      properties: { topic: { type: "string", description: "笑話主題" } },
      required: ["topic"],
    },
  },
  async execute(args) {
    return fetchJoke(String(args["topic"] ?? "")); // 你的查笑話實作
  },
};

篩選就是一段很樸素的字串比對——不需要再多花一次 LLM 呼叫去分類:

function selectActiveTools(messages: ChatMessage[], tools: Tool[]) {
  const hay = messages
    .filter((m) => m.role !== "system")
    .slice(-12) // 只看最近 12 則: 夠涵蓋多輪流程(例如還在收集參數中)
    .map((m) => m.content)
    .join("\n")
    .toLowerCase();

  const active = tools.filter(
    (t) =>
      !t.activationKeywords?.length || // 沒宣告 → 常駐
      t.activationKeywords.some((k) => hay.includes(k.toLowerCase())),
  );

  return {
    schemas: active.map(toOpenAITool), // 只有這些進 tools 參數
    prePrompts: active
      .map((t) => t.preModulePrompt)
      .filter((p): p is string => !!p), // 一起帶進 system
  };
}

一句「來個工程師笑話」進來,只有 get_joke(和常駐工具)會被送進這輪;查天氣、翻譯那些根本不會出現在模型眼前。模型每輪只在少數幾個相關工具裡挑,選錯率跟 token 都一起降下來。這是「寧可多放幾個關鍵字」的 fail-safe 設計——寧可誤送、不可漏送,漏送會讓該用的工具消失。

同一顆迴圈,怎麼「換上不同工具的專業口吻」?——兩段式專精 prompt

到這裡還差最後一塊,也是「專精 prompt(prompt specialization)」的核心: 同一顆迴圈,怎麼讓查笑話那步用查笑話的規矩、評分那步用評分的口吻? 答案是把「專精」拆成前後兩段,各管一件事:

  • preModulePrompt(選工具「前」): 注入 system 的行為指引——動手前的規矩。例如「使用者想聽笑話時,主題不明就先問一句」。模型是在還沒決定叫哪個工具之前就讀到它,才知道怎麼把參數收齊。
  • modulePrompt(工具跑完「後」): 注入 system 的回話塑形——工具回傳結果後,讓模型換上這個工具該有的口吻/範本。例如「把查到的笑話原樣說出來,不要幫它解釋笑點」。

用 toy 工具宣告出來就很直白:

const rateJokeTool: Tool = {
  activationKeywords: ["評分", "多冷", "打分", "rate", "幾分"],
  // 動手前: 先確認評分標準,避免模型自由心證
  preModulePrompt: "幫笑話評分時,固定用「冷度 1–10」這一把尺,不要自創維度。",
  // 跑完後: 塑形回話,只報結論
  modulePrompt:
    "只回一句: 『冷度 X/10』,後面接一句 20 字內的理由,不要長篇分析。",
  definition: {
    name: "rate_joke",
    description: "給一則笑話,回它的冷度分數(1–10)。",
    parameters: {
      type: "object",
      properties: { joke: { type: "string", description: "要評分的笑話" } },
      required: ["joke"],
    },
  },
  async execute(args) {
    return rateColdness(String(args["joke"] ?? ""));
  },
};

preModulePromptselectActiveTools 那步就跟著相關工具一起併進了 system(所以只有相關工具的規矩會出現)。modulePrompt 則在工具跑完後才注入,而且去重——同一輪叫了三次 get_joke,它的口吻指引只注入一次:

function injectModulePrompts(working: ChatMessage[], results: ToolResult[]) {
  const seen = new Set<string>();
  for (const r of results) {
    if (r.modulePrompt && !seen.has(r.modulePrompt)) {
      seen.add(r.modulePrompt);
      working.push({ role: "system", content: r.modulePrompt }); // 換上該工具的口吻
    }
  }
}

關鍵是: 這些「專精」的規矩,一句都沒寫進最初那顆跟使用者對話的通用 prompt。 通用 prompt 只管「聽懂人話、決定要不要用工具」;每個工具動手前後的規矩,綁在工具自己身上,用到才出現。這就是 prompt specialization——乾淨、專注,而且工具越加越多,通用 prompt 也不會跟著腫。

整條巨觀路由,長什麼樣?

把上面四節串起來,就是分裂操作在「巨觀」層的完整運作——一句話進來,到一句話回去:
巨觀路由(tool routing)的完整流程圖:使用者輸入 → 關鍵字篩選 selectActiveTools → 只把相關工具的 schema + preModulePrompt 送進本輪 → LLM 判斷要不要叫工具。判斷「要」就由 LLM 自己吐出 tool_calls(tool routing)、並行執行工具(Promise.all)、再注入 modulePrompt 換上該工具的口吻,然後回到 LLM 重新判斷,形成一個迴圈;判斷「不用」則直接產出最終回覆,並寫回記憶(見 D16)。整條路徑上沒有任何一條 if-else 決定該叫哪個工具——挑哪個、挑幾個、叫幾輪,全是模型的判斷

一張圖看完你會發現: 巨觀路由不是什麼黑魔法,是「先看這輪跟哪些工具有關、只把那幾個攤在模型面前、讓它自己挑、挑完換上對的口吻」——這恰好是一個好的接待人員本來就會的分寸,只是這次交給 AI 去判斷。

這篇的總結

  • 通用 prompt 有天花板: 工具一多就失焦、選錯、變貴——這不是模型不夠強,是脈絡沒隔離。
  • tool routing 交給 LLM: 叫哪個工具、叫幾次、一次叫幾個,是模型的判斷,不是寫死的 if-else——這是生成式 AI 在架構裡實際做的事。
  • context isolation 用關鍵字落地: 每個工具宣告意圖關鍵字,只把「這輪相關」的送進 context;一段樸素字串比對就夠,不必多花一次 LLM 分類。
  • prompt specialization 分兩段: preModulePrompt 管動手前的規矩、modulePrompt 管回話的口吻——專精規矩綁在工具身上,通用 prompt 不會越加越腫。
  • 巨觀路由是可遷移的骨架: 今天拿笑話練手,換成正經工具一模一樣; 正式版就是 D19 起要登場的真實系統。

今天這半, 解的是**工具「之間」**的問題: 十幾個工具,怎麼讓 AI 自己挑對、只帶必要脈絡、換對口吻。

但還有另一半: 如果一個工具「內部」也複雜到得拆成十幾次 LLM 呼叫呢?——例如要把一份很長的原始資料切塊、逐塊分析、再綜述成一份報告。這叫任務拆解(task decomposition),是明天 D18 的上半場。而當呼叫從一次變成幾十次,這些錢怎麼當場算清楚、當場記下來(cost tracking),是下半場。把巨觀的「工具之間」和微觀的「工具之內」湊齊,這套「拆解 → 分派 → 專精 prompt」的骨架就完整了——它的正式版,就是接下來三個真實上線的系統。


上一篇
《 Day16》💻code/示範【AI 基礎應用 ③】AI 記憶的原理與實作:memory + tool_call、Azure Redis 存工作階段
系列文
《讓 AI 去扛人類守不住的戰場,而不是取代人:系統整合商 AI 導入實戰(2026)》17
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言