什麼是編碼代理?它們如何運作以及如何建立一個

什麼是編碼代理?它們如何運作以及如何建立一個

編碼代理是一種 AI 系統,它以大型語言模型作為推理核心,自主地編寫、執行和迭代程式碼。與在編輯器中提供建議的程式碼助手不同,編碼代理執行一個完整的「觀察-決定-行動」循環:它讀取檔案、撰寫變更、執行指令、檢查輸出,並持續修正直到任務完成。

本文將說明這個循環如何運作——包括規劃器、LLM 推理層、工具以及沙盒化執行環境——然後展示如何使用 Novita 的 LLM API 和代理沙盒來建置一個。

什麼構成了一個編碼代理

程式碼助手與編碼代理之間的差別在於執行。程式碼助手會產生建議然後停止。而 AI 編碼代理則會產生程式碼、執行它、讀取結果,並持續進行直到目標達成——或者直到它卡住為止。

這種執行能力有三個具體要求:

  • 工具存取 — 能夠讀取檔案、寫入檔案和執行 shell 指令
  • 沙盒化環境 — 一個執行程式碼的地方,若出問題也不會損壞主機系統
  • 持續的上下文 — 工具輸出會回饋到模型的上下文中,以便它能夠推理發生了什麼事

缺少這三者,你只會得到一個能寫程式碼的聊天機器人。三者兼具,你就擁有了一個代理。

「程式碼代理」這個詞被廣泛使用,涵蓋從 IDE 內嵌建議到完全自主的系統,後者能夠接受一個模糊指定的任務、找出涉及哪些檔案、進行修改並驗證其運作——而無需在每個步驟都有人類介入。當開發者比較「最佳程式碼代理」選項時,通常指的是後者:能夠可靠地完成多步驟編碼任務,且幾乎不需要手動介入的系統。

編碼代理的四個層次

每個達到生產水準的 AI 編碼代理都有四個可識別的元件。實作細節各不相同——不同的框架、不同的 LLM、不同的沙盒提供者——但架構是一致的。

1. 規劃器

規劃器接收任務描述,並將其分解為代理將要執行的步驟。對於簡單的任務,這會在模型推理內部隱式完成。對於複雜的任務——例如「將此服務遷移到使用新的驗證函式庫」——一個明確的規劃步驟會產生一個編號的任務清單,模型會依序執行,並在每個步驟後更新狀態。

規劃也決定了何時停止。一個沒有完成標準的代理會無限期地持續添加改進,或者更糟的是,在一個失敗的步驟上不斷循環。大多數實作會將任務成功條件編碼在系統提示中,讓模型自行決定何時完成。

2. LLM 推理層

LLM 是任何 AI 編碼代理的推理核心。它決定接下來要呼叫哪個工具、傳遞什麼參數,以及如何解讀結果。這個決定以結構化的工具呼叫形式表達——一個包含函式名稱和參數的 JSON 物件——然後由框架分派到實際的執行層。

對於程式碼代理,LLM 需要可靠地處理長上下文(工具結果會快速累積)、一致地回傳格式良好的工具呼叫(在一個 10 步驟工作流程的第 6 步出現結構不良的 JSON 會破壞整個執行過程),並且能夠在許多連續的工具呼叫中推理狀態變化。

推理提供者在此扮演關鍵角色。你需要一個 API,它支援 OpenAI 相容格式的函式呼叫、結構化輸出以在模型層級強制執行有效的 JSON,以及足夠的並發限制來處理產生並行子任務的代理工作負載。Novita AI 的 LLM API 透過一個 OpenAI 相容的端點涵蓋了這三者,這意味著你可以在不重寫工具呼叫解析邏輯的情況下切換模型。

3. 工具層

工具是代理與世界互動的介面。一個最小化的編碼代理需要四個工具:

工具 功能
read_file 回傳指定路徑檔案的內容
write_file 將字串寫入到檔案路徑
run_command 執行 shell 指令並回傳 stdout + stderr
list_directory 列出指定路徑的檔案和目錄

每個工具都必須回傳完整的輸出。截斷的結果或靜默的失敗會破壞代理對程式碼庫的認知模型,並在後續導致複合錯誤。特別是 run_command 工具需要同時擷取 stdout 和 stderr——代理通常從錯誤輸出中學到的比從成功輸出中學到的更多。

有些代理會添加一個 search_files 工具用於在程式碼庫中進行類似 grep 的查詢,或一個 fetch_url 工具用於讀取外部文件。具體的組合取決於任務領域。對於純程式碼工作,上述四個工具涵蓋了大多數情況。

4. 沙盒

沙盒是一個完全隔離的 Linux 環境,代理的指令實際在其中執行。這之所以重要,有兩個原因。

首先,安全性。代理會根據使用者提示、檢索到的文件以及推斷的模式來產生程式碼。即使是一個善意的代理也可能產生刪除檔案、開啟網路連線或消耗無限資源的程式碼。沙盒能將任何損害限制在隔離的環境內。

其次,狀態性。一個好的沙盒能在會話期間跨工具呼叫保留檔案系統狀態。如果代理在步驟 2 建立了一個檔案,它在步驟 8 時仍需存在。無狀態的容器方法——每個指令都在全新的環境中執行——對於真實的編碼任務是行不通的。

Novita 代理沙盒 建構於 Firecracker 微虛擬機之上,提供比標準容器更強的內核層級隔離。會話最長可運行 24 小時,檔案系統狀態在指令間持續保留,且冷啟動時間低於 200 毫秒。這速度夠快,因此等待沙盒啟動不會中斷互動式工作流程。

執行循環如何運作

一個具體的範例能讓循環更容易理解。假設任務是:「為 /login 端點添加速率限制。」

  1. 規劃 — 模型讀取任務並識別其需求:找到登入路由、了解目前的處理器、添加速率限制中介軟體、透過測試運行進行驗證。
  2. 觀察 — 代理呼叫 list_directory 來尋找路由檔案,然後呼叫 read_file 讀取登入處理器。檔案內容被附加到模型上下文中。
  3. 決定 — 模型對目前的程式碼進行推理,並決定要做什麼:安裝速率限制函式庫、修改處理器、添加測試。
  4. 行動 — 代理呼叫 run_command("pip install slowapi"),然後使用修改後的處理器呼叫 write_file,接著呼叫 run_command("pytest tests/test_login.py")
  5. 再次觀察 — 測試輸出回饋到上下文中。如果測試失敗,模型會讀取追蹤資訊、識別錯誤,並寫入一個修正後的檔案。
  6. 完成 — 當測試通過且模型沒有待處理的步驟時,它會回傳最終摘要。

這個循環在單一會話內運行。上下文視窗是代理的工作記憶體——每次讀取的檔案、每個指令輸出、每次工具呼叫都會累積在其中。這就是為什麼上下文長度對編碼代理如此重要:一個真實的重構任務在步驟 15 時很容易就填滿 100K 個 token。進一步了解代理如何以與單輪對話不同的方式考驗推理提供者。

使用 Novita 建置一個編碼代理

以下範例將 Novita 的 LLM API 和代理沙盒整合在一起。它使用指向 Novita 端點的 Python OpenAI SDK——Novita 的模型使用與 OpenAI API 相同的函式呼叫介面,因此整合無需自訂解析。

import os
import json
from openai import OpenAI
from novita_sandbox.code_interpreter import Sandbox

client = OpenAI(
    base_url="https://api.novita.ai/openai",
    api_key=os.environ["NOVITA_API_KEY"],
)

sandbox = Sandbox.create(timeout=1800)


def read_file(path: str) -> str:
    try:
        return sandbox.files.read(path)
    except Exception as e:
        return f"Error: {e}"


def write_file(path: str, content: str) -> str:
    try:
        sandbox.files.write(path, content)
        return f"Written to {path}"
    except Exception as e:
        return f"Error: {e}"


def run_command(cmd: str) -> str:
    try:
        result = sandbox.commands.run(cmd)
        return str(result)
    except Exception as e:
        return f"Error: {e}"


tools = [
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "Read the contents of a file",
            "parameters": {
                "type": "object",
                "properties": {"path": {"type": "string"}},
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": "Write content to a file",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "run_command",
            "description": "Run a shell command in the sandbox and return output",
            "parameters": {
                "type": "object",
                "properties": {"cmd": {"type": "string"}},
                "required": ["cmd"],
            },
        },
    },
]

dispatch = {
    "read_file": read_file,
    "write_file": write_file,
    "run_command": run_command,
}


def run_agent(task: str, model: str) -> str:
    messages = [
        {
            "role": "system",
            "content": (
                "You are a coding agent with access to a Linux sandbox. "
                "Complete tasks by calling tools. When done, return a plain-text summary."
            ),
        },
        {"role": "user", "content": task},
    ]

    while True:
        response = client.chat.completions.create(
            model=model,
            messages=messages,
            tools=tools,
            tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:
            fn = dispatch[call.function.name]
            args = json.loads(call.function.arguments)
            result = fn(**args)
            messages.append(
                {
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": result,
                }
            )


# Replace <model-id> with a function-calling model from novita.ai/docs
result = run_agent(
    task="Write a Python script that counts words in a text file and run it on a sample input",
    model="<model-id>",
)
print(result)
sandbox.kill()

關於這個實作有幾點需要注意:

  • while True 迴圈會持續運行,直到模型回傳一個沒有工具呼叫的訊息——這表示代理認為任務已完成。
  • 工具結果會以 role: tool 條目的形式附加到 messages 中。這就是跨步驟建立共享上下文的方式。
  • sandbox.kill() 會釋放計算資源。請務必在會話結束時呼叫它。

有關支援的函式呼叫模型 ID,請查閱 Novita 函式呼叫文件。如需包含 Gradio UI 的更完整逐步說明,請參閱使用 Novita 的代理沙盒建置編碼代理

為編碼代理選擇合適的 LLM

HumanEval 和 SWE-bench 衡量的是單輪程式碼生成。代理的工作負載則不同——實際上會導致生產環境中程式碼代理出問題的是工具呼叫格式的失敗。一個在基準測試中得分很高,但在複雜的多輪會話中偶爾會回傳格式錯誤 JSON 的模型,會以難以除錯的方式失敗。

AI 編碼代理的實務評估標準:

  • 工具呼叫可靠性 — 模型在 20 步以上的會話中,回傳格式良好的工具呼叫的一致性如何?
  • 上下文保留 — 模型能否正確引用它在 40 步之前讀取過的檔案?
  • 指令遵循 — 代理是否專注於任務,還是會開始修改不相關的檔案?
  • 程式碼正確性 — 生成的程式碼是否實際能運行,還是需要多次修正循環?

使用一組具有代表性的真實編碼任務並衡量任務完成率,比任何公開基準測試都更有參考價值。從你自己的程式碼庫中挑選 20–30 個任務,針對候選模型進行測試,並統計在沒有人工介入的情況下能完成多少個。

在代理規模下,推理定價會迅速疊加。一個單一會話可能在所有輪次中消耗 200K–500K 個 token。當你每天運行數百個代理會話時,提供提示快取和具競爭力的每 token 費率的供應商,會顯著改變成本結構。

開源模型作為一條具成本效益的路徑

封閉源碼的前沿模型在編碼基準測試中一直處於領先地位,但與頂尖開源模型的差距已大幅縮小。像 DeepSeek V3Qwen3 這樣的模型現在在程式碼生成和工具使用任務上表現出色——而且由於它們是透過 OpenAI 相容的 API 提供服務,切換時只需更改 model 參數這一行程式碼。

兩者皆可透過 Novita 的 LLM API 取得。你可以獲得相同的端點、相同的函式呼叫介面、以及相同的代理沙盒整合——無需自行管理 GPU 基礎設施。這點很重要,因為 GPU 編排、批次處理和可靠性工程並非易事;將它們委託給受管 API 能讓你專注於代理邏輯本身。

這對編碼代理之所以重要,是因為:每個會話的 token 成本對代理工作負載的經濟效益影響遠大於授權費用。一個每天運行 200 個編碼代理會話的團隊,若能使用開源模型達到可比的任務完成率,則可以在完全不更改整合程式碼的情況下大幅減少推理支出。

實務測試:用你的目標模型運行 50 個代表性的編碼任務,衡量工具呼叫成功率和任務完成率,然後與每個會話的成本進行比較。基準測試數字無法回答這個問題——你的實際工作負載才能。

常見問題

編碼代理和程式碼助手有什麼不同?

程式碼助手(如 GitHub Copilot 的內嵌建議)會產生補全建議然後停止。編碼代理則會執行程式碼、讀取輸出並進行迭代。其定義性特徵是執行循環:讀取、決定、行動、觀察、重複。請參閱 CLI 與 IDE 編碼代理,以了解不同的代理形式因素如何運用此循環。

我需要沙盒來建置編碼代理嗎?

是的,如果代理將要執行由使用者輸入或外部來源產生的程式碼。沒有隔離機制,一個有缺陷的程式碼生成可能會損壞主機檔案系統或消耗無限資源。即使是僅限內部使用的情境,沙盒也能防止失控的行程影響主機。容器提供基本的隔離;像 Novita 這樣基於微虛擬機的沙盒則為多租戶或安全敏感的工作負載提供更強的內核層級隔離。

編碼代理可以在沒有網路存取的情況下運作嗎?

對於大多數純編碼任務,可以。檔案讀寫和本地指令執行涵蓋了大多數工作流程。在沙盒內限制對外連線實際上是一個很好的預設做法——它可以防止生成的程式碼發出意外的外部請求,並簡化你的威脅模型。

什麼決定了特定任務的最佳編碼代理?

工具呼叫的可靠性以及在你實際工作負載上的任務完成率。公開基準測試排名是初步篩選模型的起點,而不是最終答案。運行你的代表性任務,衡量完成率,並將每個會話的 token 成本納入考量。對於一個進行輕量重構的小型新創公司來說,最佳編碼代理可能與一個企業團隊在規模化自動 PR 審查中所使用的最佳選項截然不同。

一個編碼代理會話可以運行多久?

這取決於沙盒提供者。Novita 代理沙盒支援最長 24 小時的會話,且檔案系統狀態在指令間持續保留,這涵蓋了即使是長時間的重構或遷移任務,也無需在你的代理程式碼中實現檢查點/恢復邏輯。


推薦文章