企業級 Agent Harness 七大能力實戰拆解

企業級 Agent Harness 七大能力實戰拆解

📌 本文重點

  • 單一 Agent PoC 聰明,上線即暴露治理問題
  • 需要 Agent Harness 統一管控工具、成本與政策
  • 先建可觀測、可回放的 Harness,再談多 Agent 擴展
  • 將治理與業務邏輯解耦,避免生產環境變實驗場

企業在玩 PoC 時,單一 Agent 看起來很聰明;一上線就暴露本質問題:工具亂叫、成本失控、記憶混亂、錯誤難重現、政策無法落地。這不是模型不夠強,而是缺少一層專門管控 Agent 行為的 Agent Harness(Agent 的 runtime & control plane)

💡 關鍵: 問題不在模型本身,而在缺少專門負責治理、成本與行為管控的中介層。

這一層做的事很務實:集中管控 工具註冊與版本 / 權限、step-level tracing + replay、記憶與知識更新、錯誤恢復與重試、成本與 Token Budget、政策注入(policy engine)、標準化的 human-in-the-loop 介面。有了它,你才真的能把多 Agent 系統放進生產環境,而不是「高級 demo」。


重點說明:Agent Harness 的 7 大能力

1. 工具 / 函式註冊與版本管理

把所有可被 Agent 呼叫的 API/工具收斂到一個 工具註冊中心,統一管理:

  • 白名單 + Scope:不同 Agent 只能看到被授權的工具,避免「直接讓模型自由呼叫所有內部 API」。
  • 版本管理:工具有 versiondeprecation 概念,支援灰度切換與回滾。
  • 結構化 schema:對應到 LLM 的 tool schema / function calling,並強制要求輸入輸出型別。

2. 觀察、Tracing 與 Replay

沒有 step-level tracing,任何「為什麼產生這個請款請求?」都變成佛系排查。Harness 要提供:

  • 每一步 prompt / tool call / response 的結構化 log
  • replay 能力:拿同一套 input/context/tool version 在測試環境重放。
  • 與 APM/日誌系統整合(如 OpenTelemetry, Datadog)。

💡 關鍵: 有了 step-level tracing 與 replay,才能在出錯時精準重現並修復 Agent 行為,而不是盲目排查。

3. 記憶與知識源更新

多數系統踩的坑是「把 RAG/記憶全塞進 context」,結果是:

  • token 爆炸 → 成本高、延遲高
  • 資訊新鮮度難管控

Harness 要把記憶拆成:

  • 短期工作記憶(task-local state):存在 run/session 中。
  • 長期記憶(user / account profile, conversation history):向量庫或 KV 存儲,按策略查再放入 context
  • 權威知識源(DB / data warehouse / API):由工具抽象,不直接塞原始資料進 prompt

4. 錯誤恢復與重試策略

Agent 在實際環境裡會遇到:工具 5xxtimeoutschema mismatch、LLM 回傳壞 JSON 等。Harness 要集中:

  • 可配置重試策略(per tool 的 max_attemptsbackoff)。
  • 降級策略:改用備援工具、或中止並要求人工介入。
  • 把錯誤與重試記錄在 tracing 中,方便事後分析。

5. 成本與 Token Budget 控制

程式化 Agent(CI、排程、webhook 觸發)最容易「安靜地燒錢」。Harness 應實作:

  • per-run token budget:單一任務總 token / cost 上限。
  • per-tool cost 上限:避免某個向量搜尋或報表 API 被無限 loop 呼叫。
  • 依租戶 / team 維度聚合成本,餵到帳務系統或告警系統。

💡 關鍵: 透過 per-run 與 per-tool 的 token 與成本上限,可以在不影響功能的前提下,防止自動化流程悄悄造成巨額開銷。

6. 策略 / 合規政策注入(Policy Engine)

只在 prompt 放幾條「不要外流 PII」是不夠的。企業需要一個 policy engine

  • 針對工具呼叫、輸入輸出內容,做 策略判斷與拒絕
  • 支援條件式規則,如「財務資料工具只能在工作時間、由 Finance-Agent 使用」。
  • 配合零信任閘道(如 SolonGate 類產品)做額外的身分驗證與審計。

7. Human-in-the-loop 標準化接口

有些動作永遠不該全自動(退款、合約變更…)。Harness 要提供:

  • 標準化的 approval 任務結構:包含 who, what, diff, risks
  • 挂在 ticket 系統 / 內部前端(如模仿 LangGraphhuman node)。
  • Agent 在收到人類決策後能繼續流程,而不是整個 run 重來。

實作範例:最小可用 Agent Harness(Python)

以下是簡化版的「Agent 執行器」,示範:

  • 工具註冊 + 白名單
  • logging + tracing id
  • 超時 / 重試
  • per-run token budget
  • per-tool 次數上限
import time
import uuid
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional

# === 1. 工具註冊中心 ===

@dataclass
class ToolConfig:
    name: str
    func: Callable[[Dict[str, Any]], Any]
    version: str = "v1"
    max_calls_per_run: int = 5
    timeout_sec: float = 10.0
    cost_per_call: float = 0.001  # 自訂邏輯用
    enabled: bool = True


class ToolRegistry:
    def __init__(self):
        self._tools: Dict[str, ToolConfig] = {}

    def register(self, tool: ToolConfig):
        key = f"{tool.name}:{tool.version}"
        self._tools[key] = tool

    def get_allowed_tools(self, whitelist: List[str]) -> Dict[str, ToolConfig]:
        # whitelist 用的是 name,不帶 version
        out = {}
        for key, tool in self._tools.items():
            if tool.name in whitelist and tool.enabled:
                out[key] = tool
        return out


# === 2. Harness 執行設定 ===

@dataclass
class RunBudget:
    max_tokens: int
    max_cost: float
    used_tokens: int = 0
    used_cost: float = 0.0

    def charge_tokens(self, tokens: int):
        self.used_tokens += tokens
        if self.used_tokens > self.max_tokens:
            raise RuntimeError("Run token budget exceeded")

    def charge_cost(self, cost: float):
        self.used_cost += cost
        if self.used_cost > self.max_cost:
            raise RuntimeError("Run cost budget exceeded")


@dataclass
class AgentRunContext:
    run_id: str
    user_id: str
    allowed_tools: Dict[str, ToolConfig]
    budget: RunBudget
    tool_call_counts: Dict[str, int] = field(default_factory=dict)


# === 3. LLM 客戶端包一層(示意) ===

class LLMClient:
    def __init__(self, model: str):
        self.model = model

    def chat(self, messages: List[Dict[str, str]], tools_schema: List[Dict]) -> Dict[str, Any]:
        """
        回傳格式假設:
        {
          "content": "...",
          "tool_calls": [
            {"tool": "search:v1", "arguments": {"q": "foo"}}
          ],
          "usage": {"input_tokens": 200, "output_tokens": 150}
        }
        """
        # 這裡應呼叫實際 LLM API;為示意略過
        return {
            "content": "stub",
            "tool_calls": [],
            "usage": {"input_tokens": 50, "output_tokens": 30},
        }


# === 4. Harness 核心執行器 ===

class AgentHarness:
    def __init__(self, llm: LLMClient, tool_registry: ToolRegistry, logger):
        self.llm = llm
        self.tool_registry = tool_registry
        self.logger = logger

    def run(self, *, user_id: str, messages: List[Dict[str, str]],
            tool_whitelist: List[str], max_steps: int = 8,
            max_tokens: int = 8000, max_cost: float = 0.5) -> Dict[str, Any]:

        run_id = str(uuid.uuid4())
        allowed_tools = self.tool_registry.get_allowed_tools(tool_whitelist)
        ctx = AgentRunContext(
            run_id=run_id,
            user_id=user_id,
            allowed_tools=allowed_tools,
            budget=RunBudget(max_tokens=max_tokens, max_cost=max_cost),
        )

        self.logger.info(f"run_start", extra={"run_id": run_id, "user_id": user_id})

        for step in range(max_steps):
            step_id = step + 1
            self.logger.info("llm_step_start", extra={"run_id": run_id, "step": step_id})

            tools_schema = [
                {"name": t.name, "version": t.version} for t in allowed_tools.values()
            ]
            resp = self.llm.chat(messages, tools_schema)

            usage = resp.get("usage", {})
            ctx.budget.charge_tokens(usage.get("input_tokens", 0) + usage.get("output_tokens", 0))

            tool_calls = resp.get("tool_calls", [])
            if not tool_calls:
                # 任務完成
                self.logger.info("run_complete", extra={"run_id": run_id, "step": step_id})
                return {"run_id": run_id, "final": resp["content"]}

            # 執行工具呼叫
            for call in tool_calls:
                tool_key = self._resolve_tool_key(call["tool"], allowed_tools)
                tool_cfg = allowed_tools[tool_key]

                count = ctx.tool_call_counts.get(tool_key, 0) + 1
                if count > tool_cfg.max_calls_per_run:
                    raise RuntimeError(f"tool {tool_key} call limit exceeded")
                ctx.tool_call_counts[tool_key] = count

                result = self._call_tool_with_retry(tool_cfg, call["arguments"], ctx)
                messages.append({"role": "tool", "name": tool_cfg.name, "content": str(result)})

        raise RuntimeError("max_steps_exceeded")

    def _resolve_tool_key(self, tool_name: str, allowed_tools: Dict[str, ToolConfig]) -> str:
        # 簡化:同名只允許一個版本
        matches = [k for k, t in allowed_tools.items() if t.name == tool_name]
        if not matches:
            raise RuntimeError(f"tool {tool_name} not allowed")
        return matches[0]

    def _call_tool_with_retry(self, tool_cfg: ToolConfig, args: Dict[str, Any], ctx: AgentRunContext):
        attempts = 0
        while True:
            attempts += 1
            start = time.time()
            try:
                # 超時控制示意:可用 asyncio.wait_for 或 thread + join
                result = tool_cfg.func(args)
                elapsed = time.time() - start
                self.logger.info(
                    "tool_call",
                    extra={
                        "run_id": ctx.run_id,
                        "tool": tool_cfg.name,
                        "version": tool_cfg.version,
                        "attempts": attempts,
                        "elapsed_sec": elapsed,
                    },
                )
                ctx.budget.charge_cost(tool_cfg.cost_per_call)
                return result
            except Exception as e:
                if attempts >= 3:
                    self.logger.error("tool_failed", extra={"tool": tool_cfg.name, "err": str(e)})
                    raise
                time.sleep(0.5 * attempts)

你可以在這層再接上:

  • policy engine:在 _call_tool_with_retry 前後做輸入輸出檢查。
  • human-in-the-loop:當特定工具被呼叫,改成發出審核任務,而不是直接執行。
  • tracing 平台:把 run_idsteptool 當成 span/trace id,上報到 OpenTelemetry

建議與注意事項

1. 工具權限與多 Agent 協作

  • 不同 Agent(或不同租戶)應有獨立的 tool whitelist 和輸入遮罩。
  • 多 Agent 團隊協作時,要明確:誰能看哪些記憶 / 哪些知識源,避免「助理 Agent 無意間看到 CEO 的對話紀錄」。

2. 記憶與 RAG 的分層設計

  • 不要「一次把所有 RAG 結果塞進 context」,應做:
  • 第一階段檢索:向量庫 / keyword 檢索
  • 第二階段過濾 / re-rank:只把 top-k、與當前任務強相關的放入 prompt
  • 對長期記憶,引入 TTL / 熱度分級,過舊資料只保存在冷存儲,需要時再查。

3. Tracing / Replay 一開始就要做

  • 踩坑:先上線,出事再補 tracing → 你根本不知道 Agent 做了什麼。
  • 最少要記:run_id、step、messages(摘要即可)、tool_calls、錯誤堆疊、tool version
  • 為 replay 保留「當時的工具版本與政策版本」,不然重放行為會不一致。

4. 成本 / Budget 不只是一個大數字

  • run / user / team / workflow 類型 切分配額,結合告警(例如超過預估平均三倍就發訊息)。
  • 對「自動觸發型」Agent(CI、webhook)尤其要設 per-run token budget + max_steps,避免 prompt/工具錯誤導致無限 loop。

5. 安全與政策落地要有「硬限制」

  • 除了 prompt 提醒,還要有:
  • policy engine:直接阻擋不符規則的工具呼叫 / 輸出內容。
  • 零信任閘道(如 SolonGate 類)在真正的企業 API 前再加一層,做身分、範圍、頻率控制。

總結: 不要把「Agent 邏輯」和「治理、成本、安全控制」寫死在同一份業務程式碼裡。先抽出一層可組態、可觀測、可回放的 Agent Harness,你才能放心地擴到多 Agent、跨團隊、跨租戶,而不把整個公司變成昂貴的實驗場。

🚀 你現在可以做的事

  • 審視現有 Agent PoC,列出目前缺少的 tracingbudgetpolicy 能力,畫出一層簡易 Harness 設計草圖
  • 在現有程式碼中加入 run_id、step-level log 與最小可用的 replay 機制,先讓行為可觀測
  • 實作一個簡單的 ToolRegistry 與 per-run token budget,從一個 Agent 開始逐步遷移到 Harness 架構

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *