iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

純文字、標準輸入輸出與高可組合性,讓 CLI 不再只是工程師操作系統的工具,也是 AI Agent 理解環境、執行命令與進行自我修正的介面。過去 CLI 的設計核心是以人為本,追求豐富的彩色表格、進度條與互動選單;然而當呼叫端換成 Agent 或自動化腳本時,這些介面會造成意外中斷。

Agent 呼叫 CLI 時透過標準輸入(stdin)、標準輸出(stdout)、標準錯誤(stderr)與 Exit Code 進行溝通。當 CLI 在執行途中彈出方向鍵選單,Agent 會因為無法提供互動輸入而卡死直到逾時;當 ANSI 控制碼混入輸出,解析欄位就會產生雜訊;當錯誤訊息只寫 Invalid argument,Agent 便無法判斷該如何修正。

這套針對 Agent 的 CLI 設計思維,主要參考了 Eric Zakariasson 與 Google Workspace CLI(gws)作者 Justin Poehnelt 的經驗分享。

讓 Agent 從 --help 探索功能

Agent 執行命令前需要了解命令名稱、位置參數與 Flag 用法。--help 因此不只是給人類看的說明書,更是 CLI 對外公開的 API 介面規格。

每個命令的說明至少需要包含:

  • 必要參數、合法值範圍與預設值。
  • 輸入與輸出格式。
  • 可直接複製執行的範例。
Connect to a database server.

Usage:
  dbtool connect --server <name> --user <name> [flags]

Flags:
      --server string   Server name. Valid values: db-1, db-2, db-3.
      --user string     Database user name.
      --output string   Output format: table or json. (default "table")

Examples:
  dbtool connect --server db-1 --user admin
  dbtool connect --server db-1 --user admin --output json

單純標註「指定伺服器」缺乏具體的合法值資訊。把 db-1, db-2, db-3 與範例寫進 --help,Agent 才能在出錯時參考說明自行修復。

提供完整的非互動執行路徑

非互動路徑指的是命令執行時能一次性接收所有參數,不需要等待使用者在終端機輸入回答。

當人類在終端機操作時,CLI 可能跳出選單或問答提示:

$ dbtool init
? Select database engine: (use arrow keys)
> postgres
  mysql
? Set max connection pool size: 50
? Enable read replica pool? (Y/n) y

這種互動問答適合人類用鍵盤選擇,但當呼叫端是 AI Agent 或自動化腳本時,程式會停在 stdin 等待使用者輸入,最後因為久未收到回答而逾時中斷。

為了支援自動化呼叫,CLI 應提供完整的非互動執行路徑,讓呼叫端在下達命令時一次傳入所有參數:

  • 透過 Flag 與自動確認傳參:
    $ dbtool init --engine postgres --pool-size 50 --read-replica --yes
    
  • 透過結構化 JSON 酬載傳參:
    當初始化設定包含多層級結構(例如連線池上限、閒置連線數、副本節點清單)時,若全部攤平成獨立 Flag,會使參數解析與說明文件變得龐大且難以維護。直接支援傳入 JSON 酬載,能完整對齊背後的設定模型:
    $ dbtool init --json '{"engine":"postgres","pool":{"max":50,"idle":10},"replicas":["ro-1","ro-2"]}'
    

在 Go 與 Cobra 中,可以用 term.IsTerminal 檢查 stdin 是否連接到真實終端機。如果缺少必要參數且處於非 TTY 環境(例如被 Agent 呼叫或在管道中),程式應立即報錯終止,而不是顯示互動表單:

package main

import (
	"fmt"
	"os"

	"golang.org/x/term"
)

func canPrompt(f *os.File) bool {
	return term.IsTerminal(int(f.Fd()))
}

func runConnect(server, user string) error {
	if server == "" {
		if !canPrompt(os.Stdin) {
			return fmt.Errorf("missing required flag --server in non-interactive mode")
		}
		// 人類模式:啟動互動選單詢問 server
		server = promptUserForServer()
	}
	return connectServer(server, user)
}

為了清楚區分自動化的控制行為,CLI 應定義明確的控制 Flag:

  • --non-interactive:強制停用所有互動選單;缺乏必要輸入時直接回傳失敗。
  • --yes(或 -y):對所有確認詢問預設回答同意,適用於批次執行。
  • --force:強制執行危險或跳過特定保護檢查的操作(例如覆寫已有檔案)。

建立穩定的結構化輸出

文字表格適合人類視覺閱讀,但字串邊界與空白對齊對程式解析不友善。例如以下輸出:

NAME                  STATUS    UPTIME
production database   running   1h

呼叫端無法單憑空白切割精準判斷 production database 是一個完整的名稱還是兩個獨立欄位。

當指定 --output json 時,CLI 應輸出格式合法的 JSON 結構,寫入 stdout:

$ dbtool status --output json
{
  "name": "production database",
  "status": "running",
  "uptime_seconds": 3600
}

在 Go 中實作結構化輸出時,把資料寫入 cmd.OutOrStdout() 能確保輸出可被測試與重導向:

type StatusResponse struct {
	Name          string `json:"name"`
	Status        string `json:"status"`
	UptimeSeconds int64  `json:"uptime_seconds"`
}

func renderStatus(cmd *cobra.Command, resp StatusResponse, format string) error {
	if format == "json" {
		encoder := json.NewEncoder(cmd.OutOrStdout())
		encoder.SetIndent("", "  ")
		return encoder.Encode(resp)
	}
	// 預設人類模式:格式化表格
	return printTable(cmd.OutOrStdout(), resp)
}

當 CLI 的輸出直接進入 LLM 的 Context Window 時,未經篩選的巨量資料會快速消耗模型容量,並因雜訊過多而降低推理精度。例如查詢資料庫清單若一次回傳上千個欄位的完整 Schema,模型很容易在雜訊中漏看關鍵屬性。

為此,CLI 應具備控制輸出規模的機制:

  • 欄位過濾:提供 --fields id,name,status Flag,只回傳下游決策所需的鍵值,主動剔除龐大的除錯日誌與中繼資料。
  • 數量上限與分頁:列表查詢命令預設限制傳回筆數(例如 --limit 20),避免單次傾倒大量 JSON 陣列。
  • 預設摘要與截斷:印出日誌或除錯資訊時,預設只輸出前 50 行摘要;只有帶上 --full 或 --verbose Flag 時才輸出完整內容。

結構化輸出配合視窗保護,讓命令能無缝融入 Shell 管道。結合 jq 等工具,Agent 能精準揀選欄位傳給下一個命令:

$ dbtool list --fields id --output json | jq -r '.[].id' | dbtool delete --stdin --dry-run

用錯誤訊息與 Exit Code 指導下一步修復

當命令執行失敗時,籠統的錯誤訊息(如 Error: invalid parameter)無法幫助呼叫端修復問題。可行動的錯誤訊息應同時說明原因、合法選項與修復建議。在非互動模式下,錯誤訊息應輸出至 stderr:

$ dbtool connect --user admin --output json

輸出至 stderr 的內容:

{
  "error": {
    "code": "MISSING_REQUIRED_FLAG",
    "message": "required flag \"--server\" is missing",
    "hint": "specify a valid server with --server: db-1, db-2, db-3",
    "help_command": "dbtool connect --help"
  }
}

這種結構化錯誤設計包含以下層次:

  • code:機器可讀的錯誤代碼,供程式進行分支判定。
  • message:清楚說明問題所在。
  • hint:提供修復問題所需的下一步操作與合法參數清單。

同時,CLI 必須使用正確的 Exit Code:

  • 0:成功執行。
  • 1:一般業務邏輯失敗(例如資源找不到、連線逾時)。
  • 2:命令語法或參數錯誤(Misuse of Shell builtin / flags)。
  • 3:認證或權限不足。

明確的 Exit Code 讓 Agent 無需解析複雜文字,即可快速決定要重試、修正參數還是尋求人類協助。

用 --dry-run 預覽破壞性操作

刪除資料、修改設定或停用服務等具副作用的操作,應提供 --dry-run 預覽 Flag。

執行 --dry-run 時,CLI 會完成完整的參數驗證、權限檢查與變更比對,並回傳預計執行的變更細節,但不會對系統造成實際異動:

$ dbtool delete --user-id u-12345 --dry-run --output json
{
  "action": "delete",
  "target_resource": "user",
  "resource_id": "u-12345",
  "affected_records": 1,
  "executed": false
}

Agent 可以先執行 --dry-run 取得 JSON 報告,供人類確認無誤後,再移除 --dry-run 進行正式變更。這是在自動化環境中防止破壞性操作的重要防護機制。

提供 Agent Skills 檔案

終端機的 --help 只能定義單一命令的語法與參數型別,無法表達跨命令的業務規則與環境防護要求。

為了讓 AI Agent 理解整體工具的操作約束(Operational Constraints),可以在專案儲存庫中提供 AGENTS.md 或 SKILL.md。例如 Google Workspace CLI(gws)透過隨附的 Skills 檔案宣告自動化呼叫的標準規範。針對像 dbtool 這類資料庫維護工具,可在檔案中明確約定:

  • 執行 delete 或 truncate 等破壞性操作前,必須先執行 --dry-run 比對影響筆數。
  • 查詢大表時一律強制加上 --limit 與 --fields,禁止無條件全表掃描。
  • 資料庫密碼與連線憑證一律走環境變數,禁止以 Flag 明文傳入。

小結

現代 CLI 的設計並非要在人類操作與機器自動化之間選邊站,兩者同樣重要:

  • Human-friendly 保障了人工操作的直覺與安全:工程師依然負責系統除錯、架構探勘與關鍵決策的最後審核。對齊表格、狀態上色、互動選單與清晰的 --help 說明,能大幅降低人腦的認知負擔,避免在手動操作時出錯。
  • Agent-friendly 奠定了自動化與自主執行的基石:當呼叫端換成 AI Agent,CLI 便成為它感知環境與執行命令的 API。完整的非互動路徑、無雜訊的結構化輸出、精確的 Exit Code 與操作約束(Skills),是讓 Agent 能夠自主解析狀態、推進任務並自我修正的必要前提。

透過標準串流分流、TTY 自動偵測與 --output json 等控制 Flag,同一套命令列工具既能在終端機裡為工程師提供舒適的操作體驗,也能在背景無縫對接 AI Agent,打造出兼具直覺回饋與自動化效率的現代 CLI。


上一篇
Human-friendly CLI 的持續互動:REPL 模式
下一篇
CLI 架構設計
系列文
30 天學會做一個 CLI:打造人類與 AI 都友善的現代 CLI 應用 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言