大多數開發者工具的起步方式都大同小異。你需要重複執行某項任務,於是你寫了一個腳本。腳本逐漸增加旗標,旗標又衍生出子命令。不知不覺中,你就擁有了一個命令列介面(CLI)。

我們在建構一個內部平台工具時,也經歷了同樣的過程,這個工具旨在幫助我們的解決方案團隊更快地交付。它負責專案鷹架、啟動開發環境、生成設定檔,並部署到預備環境。

隨著我們的 CLI 範圍擴大,內部使用者數量也隨之增加。但隨後,一件有趣的事情發生了:突然之間,我們不再只是為人類開發者建構工具,也開始為程式碼代理人(coding agents)服務。

什麼是好的開發者體驗(DX)

在討論代理人之前,讓我們先談談什麼能讓 CLI 對人類開發者來說真正愉快。

Spaces 對於繁瑣的事務有獨到見解。它選擇了合理的目錄結構,讓你無需費心。它會生成你原本需要從上一個專案複製的設定檔。它將服務連接起來,讓你的 API 和前端在第一天就能溝通,而不是在花費一小時編輯 YAML 或「憑感覺」之後。

以下是一個典型的工作階段:

```shell

$ spaces init my-project

$ cd my-project

$ spaces dev

```

三個命令。你就能從無到有,啟動一個包含熱重載、資料庫和自動生成 Docker 設定檔的多服務專案。這就是我們的標準。

重要的命令通常分為三大類:

  • 鷹架(Scaffolding) -- 這些命令用於建立結構、提問、顯示選項、讓你探索。
  • 開發(Development) -- 這些是你的內部循環。它們只是「運行」。
  • 操作(Operations) -- 這些命令會觸及生產環境。操作前請仔細檢查。

這些都是基本要求。但隨後我們的第二個使用者出現了,我們發現我們需要一些稍微不同且更有趣的東西。

第二個使用者:代理人

我們為 init 命令建構了一個文字使用者介面(TUI)模組選擇器。它有花俏的變更、命令,看起來很棒。

然後一個代理人嘗試使用它。

代理人看到的是原始的 ANSI 逸出碼。\\x1b[36m?\\x1b[0m Select components。它無法發送方向鍵或切換選項。它完全被鎖在命令之外。

事後看來,解決方案似乎很明顯:添加一個 --components 旗標。但真正的洞見遠不止一個旗標那麼簡單。

每個提示詞都是偽裝的旗標

每當你的 CLI 提出一個互動式問題時,就存在一個隱含的契約:「我需要這條資訊才能繼續。」為了履行這個契約,你可以使用互動式提示詞。或者你可以使用旗標或設定檔。

訣竅是先思考資訊,再思考輸入方法。

```python

def init_command(

components: str | None = Option(None),

yes: bool = Option(False, "-y"),

):

if components:

selected = components.split(",")

elif yes:

selected = get_defaults()

else:

selected = show_picker()

Same logic from here

create_project(selected)

```

三條輸入路徑,一條執行路徑。業務邏輯不知道輸入是如何到達的。這意味著你只需測試一次,而不是三次。

-y 旗標值得特別關注。它不僅僅是「跳過確認」。它是一個契約,表明:_我正在以程式化方式提供你所需的一切,不要在標準輸入(stdin)上阻塞。_ 當你使用 -y 運行時,每個提示詞都會解析為旗標值或智慧預設值。如果無法解析,它會大聲失敗而不是掛起。

實踐範例:這篇文章中的互動元素。

這些元素是在一個全新的儲存庫中建構的。隨後要求一個代理人將專案連接到 [spaces cli] 進行部署。它首先對相關命令運行 --help 以了解介面。從中,它找出了所需的設定檔,生成了一個 config.yaml,並連接了 Docker 設定檔和註冊中心設定,無需人工干預。

在同一次操作中,它設定了 GitHub Actions CI 管線。從單一提示詞到即時部署花費不到 10 分鐘(我們正在積極努力縮短這個週期時間)。之後,儲存庫被完全配置,並且嵌入內容透過 Koyeb 部署到 staging 環境,作為一個 Space,透過 Spaces 本身進行鷹架和部署。

是的,這篇關於 Spaces 的部落格文章中嵌入的互動式演示正在作為一個 Space 運行。_Spaceception_。

因為每個互動式輸入都有一個等效的旗標,代理人可以端到端地自主操作。

結構化資料作為介面

我們的團隊正在建構的 CLI 幫助我們的應用 AI 工程師交付應用程式。但並非每個應用程式都相同。有些需要後端和向量資料庫。有些只需要關聯式資料庫、前端和一些 API。少數是沒有 UI 的僅限工作者的服務。

我們不打算硬編碼模組類型。因此,我們建構了一個外掛程式系統,其中每個組件都是一個聲明其自身屬性的外掛程式:

```python

class ModulePlugin(BaseModel):

type_id: str

category: str

default_port: int

def get_env_vars(self) -> list[EnvVarDef]: ...

def get_dev_command(self, port: int) -> str: ...

```

外掛程式是可內省的。你可以列出它們、序列化它們、比較它們。人類瀏覽 TUI 選擇器。代理人查詢註冊中心並獲得 JSON 回傳。相同的資料,不同的呈現方式。

這意外地解決了一個我們不知道存在的問題。以前,添加新的模組類型意味著更新選擇器、Docker 設定檔生成器、環境變數檔案寫入器和組合範本。現在,它意味著編寫一個外掛程式類別。註冊中心是唯一的真相來源,所有東西都從中讀取。

教導代理人關於你的專案

> 有句老話說內容為王。對於代理人來說,上下文為王。

我們為代理人可用性所做的最有影響力的事情是在每次 init 時生成兩個檔案:

context.json -- 專案的結構化快照:存在哪些模組、它們使用的埠、要運行的命令、它們需要的環境變數。

AGENTS.md -- 為大型語言模型(LLM)編寫的一組規則,比你常規的規則更具指令性。不是「這個專案使用 PostgreSQL」,而是「在測試資料庫變更之前運行 mycli dev --migrate」。

代理人在執行前閱讀這些檔案會顯著減少錯誤。基本上,它不會猜測埠號、運行錯誤的測試命令、嘗試安裝已經由工具鏈管理的依賴項。

上下文檔案也充當代理人過時假設的快取清除器。當你添加模組或更改部署目標時,上下文檔案會在下一次 dev 或 init 時自動更新。代理人每次都會讀取最新狀態。

隱式狀態是敵人

我們遇到的最微妙的問題是隱式狀態。我們的 add 命令從當前工作目錄(CWD)讀取 config.yaml。人類會不假思索地 cd 到正確的資料夾。從工作區根目錄運行命令的代理人卻不知道它需要位於子目錄中。

解決方案:

```python

之前:依賴 CWD

config = load_config(Path.cwd() / "config.yaml")

之後:明確指定並帶有備用方案

config = load_config(

path or find_config_in_parents(Path.cwd())

)

```

每個隱藏的假設,CWD、環境變數、$HOME 中的點檔案,都是代理人可能絆倒的地方。帶有合理備用方案的明確參數為代理人解決了這個問題,也讓人類更容易編寫腳本。

清單

回顧過去,這些改變單獨來看都很小。只是一套始終如一應用的原則:

  • 每個互動式輸入都有一個等效的旗標
  • 每個旗標都有一個用於無頭模式的智慧預設值
  • 狀態是明確的。CWD、環境變數和設定路徑是輸入,而不是假設
  • 外掛程式是資料模型,而不僅僅是程式碼。預設情況下可內省
  • 上下文檔案為代理人(以及 CI 和腳本)提供專案的結構化描述

為所有人打造更好的工具

有趣的是,這些改變都沒有讓 CLI 對人類來說變得更糟。TUI 選擇器仍然有效且看起來很花俏,進度指示器仍然旋轉,確認對話框仍然確認。我們只是增加了一扇「第二道門」。

而這扇第二道門結果是更重要的一扇。不是因為代理人比人類更重要,而是因為它們施加的限制與使 CLI 具有組合性、可腳本化和可測試性的限制是相同的。為代理人設計迫使我們為所有人建構更好的工具。

如果你現在正在建構開發者工具,你不需要一個單獨的代理人 API。你需要查看每個 input() 呼叫、每個 CWD 假設、每個僅限於美觀輸出的內容,然後問自己:如果另一端的使用者是一個程序,而不是一個人呢?

這個問題的答案無論如何都會改進你的工具。

***

Spaces CLI 由 Mistral AI 的 Lorenzo Signoretti、Riwa Hoteit 和 Sam Fenwick 建構。特別感謝我們的應用 AI 團隊,他們是 CLI 最早也是最嚴苛的使用者,他們的實際使用塑造了這裡描述的每個模式。我們很高興看到它將幫助他們與我們的客戶一起建構應用程式,以解決棘手的用例。

***

人類和代理人之間的工具層仍在探索中。如果你對在 AI 和基礎設施交叉點建構開發者工具有興趣,我們正在招募。