iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
AI Engineering

RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰系列 第 21 篇

Day 21:Agent(一) — 能用工作流就不要用代理

  • 分享至 

  • xImage
  •  

RAG 讓模型「知道」,Agent 讓模型「能做」。今天進入第二常出現的任務訊號(47.6%)。

但我要先講一個可能讓你意外的建議:你需要的多半不是 Agent。

今天要解決的問題

2025 年以來,「Agent」變成了一個什麼都能塞的詞。實務上多數的失敗是這樣的:

團隊做了一個「文件處理 Agent」,讓它自己決定要先分類還是先擷取、要不要查資料庫、要不要再確認一次。結果:同樣的文件,這次花 4 次 LLM 呼叫、下次花 11 次;成本浮動三倍,處理時間從 8 秒到 40 秒不等;出錯時完全不知道為什麼。

而這個流程本來是固定的——分類 → 擷取欄位 → 寫入資料庫。三個步驟,寫死就好。

用不確定性換來了不需要的彈性。

📊 職缺訊號

「Agent、工作流程與工具串接」出現在 47.6% 的生成式 AI 職缺中,僅次於 RAG。注意這個任務的完整名稱——它包含「工作流程」。市場要的不是「會做 autonomous agent」的人,是能判斷什麼時候該用哪一種的人。


一、現象:自主性是一道光譜

從工作流到代理的自主性光譜

圖 21-1:從工作流到代理的自主性光譜(三條曲線為作者依實務經驗歸納的示意評分)。每往右一步,換到彈性,失去可預測性與可除錯性,成本也上升。

這張圖的讀法:

  • 左半部(工作流區間):流程由你決定,模型只負責它擅長的單點任務(分類、擷取、生成文字)。可測試、可預期、成本穩定。
  • 右半部(代理區間):流程由模型決定。能處理你沒預想到的情境,代價是每次執行路徑都可能不同。

判斷準則很簡單:

如果你能事先畫出流程圖,就用工作流。
只有當「下一步該做什麼」真的取決於前一步的結果,且組合太多無法窮舉時,才需要代理。

二、原理:五種設計模式,先試前四種

Anthropic 在《Building Effective Agents》整理了一組實用的模式體系。前四種都屬於工作流:

image

圖 21-2:從工作流到代理的六種設計模式(示意架構)。實務上八成的需求,前四種就能解決。

模式 適用情境 例子
提示鏈 步驟固定、每步都需要 LLM 翻譯 → 潤稿 → 檢查術語一致性
路由 有明確分類、各類處理方式不同 客服分流:退貨/技術問題/帳務
平行化 多個獨立子任務、可同時進行 同時檢查一份合約的五個風險面向
協調者-工作者 子任務數量不固定,但類型已知 依查詢改寫出的 N 個子問題分別檢索
評估者-最佳化者 有明確的品質標準可自我檢查 生成程式碼 → 跑測試 → 依錯誤修正
自主代理 步驟數與順序無法預先決定 「幫我調查這個客訴的來龍去脈」

三、動手:手刻一個 ReAct 迴圈

即使你最後用框架,手刻一次也是值得的——因為所有框架都是在包裝這 30 行。

"""agent.py —— ReAct(Reasoning + Acting)的最小完整實作。"""
import json

SYSTEM = """你是一個能使用工具的助理。你可以呼叫工具來取得資訊或執行動作。

工作方式:
1. 思考需要什麼資訊才能回答
2. 呼叫適當的工具(一次一個)
3. 觀察結果後,決定要繼續呼叫工具還是給出最終答案

原則:
- 只用工具回傳的事實回答,不要編造
- 若工具回傳錯誤,讀懂錯誤訊息後調整參數重試(最多兩次)
- 若三次仍無法取得資訊,誠實告知使用者"""

TOOLS = [
    {"type": "function", "function": {
        "name": "search_kb",
        "description": "在公司知識庫中搜尋資料。當需要公司政策、產品規格、"
                       "作業流程等內部資訊時使用。",
        "parameters": {"type": "object", "properties": {
            "query": {"type": "string", "description": "搜尋關鍵字或問題"}},
            "required": ["query"]}}},
    {"type": "function", "function": {
        "name": "calculate",
        "description": "執行數學運算。當需要精確計算時使用——不要自己心算。",
        "parameters": {"type": "object", "properties": {
            "expression": {"type": "string", "description": "Python 算式,如 (1200*0.05)"}},
            "required": ["expression"]}}},
]

def execute_tool(name: str, args: dict, ctx: dict) -> dict:
    """工具執行:所有安全檢查都在這裡,不在提示裡。"""
    if name == "search_kb":
        chunks = retrieve_and_rerank(args["query"], user=ctx["user"])  # 帶權限
        if not chunks:
            return {"error": "查無相關資料,請換個關鍵字或確認問題範圍"}
        return {"results": [c["text"][:500] for c in chunks[:3]]}
    if name == "calculate":
        try:
            # 正式環境請用安全的算式解析器,不要用 eval
            return {"result": safe_eval(args["expression"])}
        except Exception as e:
            return {"error": f"算式無效:{e}"}
    return {"error": f"未知的工具:{name}"}

async def run_agent(question: str, ctx: dict,
                    max_iterations: int = 6, max_cost_tokens: int = 20_000):
    """ReAct 迴圈。兩個上限是必要的護欄,不是可選項。"""
    messages = [{"role": "system", "content": SYSTEM},
                {"role": "user", "content": question}]
    trace, used_tokens = [], 0

    for i in range(max_iterations):
        resp = await client.chat.completions.create(
            model=MODEL, messages=messages, tools=TOOLS)
        used_tokens += resp.usage.total_tokens
        msg = resp.choices[0].message

        if not msg.tool_calls:                      # 模型認為可以回答了
            trace.append({"step": i, "action": "final_answer"})
            return {"answer": msg.content, "trace": trace,
                    "iterations": i + 1, "tokens": used_tokens}

        messages.append(msg)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            result = execute_tool(call.function.name, args, ctx)
            trace.append({"step": i, "tool": call.function.name,
                          "args": args, "ok": "error" not in result})
            messages.append({"role": "tool", "tool_call_id": call.id,
                             "content": json.dumps(result, ensure_ascii=False)})

        if used_tokens > max_cost_tokens:           # 成本上限:防止失控燒錢
            return {"answer": "這個問題較複雜,已轉由專人處理。",
                    "trace": trace, "stopped": "cost_limit"}

    return {"answer": "我無法在合理步驟內完成這個查詢,已轉由專人處理。",
            "trace": trace, "stopped": "max_iterations"}

這段程式碼有三個生產級的細節,比迴圈本身更重要:

  1. max_iterations 與 max_cost_tokens——沒有這兩個上限,一個迴圈錯誤就能燒掉你整個月的預算。這不是可選項。
  2. trace——記錄每一步做了什麼。出問題時,這是唯一能讓你重建現場的東西(Day 22 詳談)。
  3. 權限在 execute_tool 裡——不在提示裡。這點 Day 17 強調過,Day 25 會看到不這樣做的後果。

四、取捨:什麼時候升級到代理

訊號 判斷
「流程圖畫得出來」 用工作流
「大部分情況固定,少數需要判斷」 工作流 + 一個路由節點
「步驟數不固定,但工具種類已知」 協調者-工作者,或限制迭代次數的代理
「連要用哪些工具都不確定」 自主代理——但先問自己這個需求是不是定義不清
「使用者的請求千奇百怪」 通常代表還沒收斂需求,先做前 20% 高頻情境

4.1 從工作流開始,逐步放寬的實際做法

即使最終要做代理,也建議這樣分階段推進:

階段一:全部寫死。 流程固定,模型只做單點任務(分類、擷取、生成文字)。此時你會累積兩樣東西:真實的使用者請求分布,以及哪些情境是流程處理不了的。

階段二:加一個路由。 從階段一的資料中,你會發現請求其實集中在少數幾類。用一個 LLM 路由節點分流,各類走各自的固定流程。這一步通常能覆蓋 80–90% 的請求。

階段三:對剩下的長尾開放代理。 那 10–20% 無法歸類的請求,才交給有工具、有迭代上限的代理處理。而且要記錄它的每一次執行——這些紀錄會告訴你,其中有沒有哪些又可以被收斂成固定流程。

這個順序的好處是:每一階段都能上線、都能產生價值,而且風險是遞增而非一次到位。它也符合 Day 11 談過的原則——自動化程度應該隨著你對系統的理解而提高。

💡 一個我常用的說服方式

當有人堅持要做全自主代理時,我會問:「這個代理如果做錯,最壞會怎樣?」

  • 如果答案是「回答錯誤,使用者再問一次」——那可以放心試。
  • 如果答案是「刪掉資料 / 寄錯信 / 下錯單」——那就先做工作流,把不可逆的動作交給人確認。

自主性的上限,應該由「錯誤的代價」決定,而不是由技術的可能性決定。


今日小結

  • 自主性是光譜不是開關:每往右一步,換到彈性,失去可預測性與可除錯性,成本也上升。
  • 能畫出流程圖就用工作流;只有當「下一步取決於前一步結果、組合無法窮舉」時才需要代理。
  • 六種模式中,前四種(提示鏈、路由、平行化、協調者-工作者)就能解決八成需求。
  • ReAct 迴圈的核心只有 30 行,所有框架都是在包裝它——手刻一次會讓你在除錯時知道從哪查。
  • 三個生產級細節比迴圈本身更重要:迭代上限與成本上限(不是可選項)、trace(唯一能重建現場的東西)、權限在執行層而非提示裡。
  • 自主性的上限應由「錯誤的代價」決定,不是由技術可能性決定。

延伸閱讀

  • 📄 Anthropic, Building Effective Agents——本文模式體系的來源,「先用簡單的」這個核心主張值得每個做 agent 的人讀一次
  • 📄 Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models (2022)——ReAct 的原始論文

上一篇
Day 20:RAG(三)生成與評測 — 引用、拒答與量化品質
系列文
RE: 從 4,343 筆職缺到 AI Engineer:MLOps × GenAI Engineering 雙主軸實戰 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言