建構 AI 代理始於一個簡單的概念:讓模型能夠採取行動、觀察結果,並進行迴圈。模型會針對目標進行推理、決定下一步行動、呼叫工具、取得結果,然後繼續執行,直到任務完成。這個「讀取-評估-行動」迴圈是您在建構任何代理時的核心 — 無論是單步驟工具呼叫器,還是能撰寫程式碼、執行程式且自行修正錯誤的多步驟系統。
本教學將逐步介紹讓代理運作的四個元件:規劃、工具使用、程式碼執行與評估。您將獲得一套可運作的心智模型,以及一個您今天就能執行的最小化實作。
是什麼讓某個東西成為 AI 代理?
AI 代理與基本的 LLM 呼叫之間有一個關鍵差異:它會採取行動並觀察結果。
單純的聊天補全只會產生文字。而代理則會利用這些文字來呼叫函式、查詢 API、執行指令或讀取檔案 — 並將結果回饋作為下一步的輸入。這種回饋迴圈正是讓代理能處理無法在單一回應中解答之任務的原因。
代理具備三項能力:
- 工具呼叫 — 以結構化引數按名稱呼叫已定義的函式
- 脈絡累積 — 隨著對話增長,將工具結果向前傳遞
- 目標導向迴圈 — 持續執行直到滿足停止條件,而非僅在一個回應後停止
如果一個系統具備以上三者,您就是在建構代理。如果缺少脈絡累積或迴圈,那它就是一個增強工具的交談機器人。
相同架構可處理多種任務:能搜尋與統整的研究代理、能撰寫與執行程式碼的編碼代理、能查詢資料庫並產生報告的資料代理。在每種情況下,實作模式幾乎完全相同。
步驟 1:規劃 — 推理層
規劃層是模型決定下一步要做什麼的地方。您不需要獨立的規劃模型 — 同一個產生文字的模型就能在每一步對目標進行推理,並產生結構化的動作(工具呼叫)。
系統提示承擔了大部分工作。一個好的規劃提示會告訴模型:
- 它正在努力達成什麼目標
- 有哪些工具可用,以及每個工具的功能
- 呼叫工具時應使用何種格式
- 何時應停止(完成條件)
使用 Novita 的 LLM API(與 OpenAI 相容)時,程式碼如下:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.novita.ai/v3/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
tools = [
{
"type": "function",
"function": {
"name": "run_code",
"description": "執行 Python 程式碼並回傳 stdout/stderr。",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "要執行的 Python 程式碼"
}
},
"required": ["code"]
}
}
}
]
messages = [
{
"role": "system",
"content": "您是一個編碼助理。請使用 run_code 工具來執行 Python 並解決任務。當您有最終答案時請停止。"
},
{
"role": "user",
"content": "找出 50 以下的所有質數。"
}
]
response = client.chat.completions.create(
model="meta-llama/llama-3.1-8b-instruct",
messages=messages,
tools=tools,
tool_choice="auto",
)
模型會回傳一個工具呼叫 — 包含函式名稱與引數的 JSON 物件 — 而非純文字回應。您的程式碼會提取這個資訊、執行函式,然後將結果附加到 messages 中並進行迴圈。
步驟 2:工具使用 — 將模型連結到世界
工具是代理實際執行的動作。沒有工具,模型只能產生文字。有了工具,它可以讀取檔案、呼叫 API、執行程式碼、搜尋資料庫,或觸發您定義的任何函式。
定義工具
每個工具都是一個描述函式的 JSON schema。模型會讀取這個 schema 以正確格式化其呼叫。幾個實用規則:
- 保持描述簡短精確。 模型在每一步都會讀取它們。冗長的描述會浪費 token。
- 將參數名稱與函式實際接受的內容匹配。 Schema 與實作之間的錯配會導致無聲的失敗。
- 回傳結構化輸出。 如果您的工具回傳原始 HTML 或未格式化的散文,模型會浪費 token 來解析它。請回傳乾淨的字串或帶有清楚標籤的 JSON。
工具呼叫迴圈
在模型發出工具呼叫後,您的程式碼會:
- 從回應中解析工具名稱與引數
- 查找對應的 Python 函式
- 使用解析出的引數呼叫該函式
- 將結果以
role: "tool"附加到messages中 - 發出另一個補全請求
迴圈會持續進行,直到模型回傳一個沒有工具呼叫的純訊息(完成條件)。
import json
def run_agent_loop(client, model, messages, tools, tool_fns, max_steps=10):
for _ in range(max_steps):
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="auto",
)
choice = response.choices[0]
# 完成 — 沒有工具呼叫
if choice.finish_reason == "stop":
return choice.message.content
# 執行工具呼叫
tool_call = choice.message.tool_calls[0]
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
result = tool_fns[fn_name](**fn_args)
# 附加模型的工具呼叫與結果
messages.append(choice.message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": str(result),
})
return "已達最大步驟數,但無最終答案。"
這就是完整的代理迴圈。max_steps 防護措施可防止失控的迴圈 — 如果模型卡在同一個工具上重複呼叫,迴圈會終止而非無限執行。其他所有功能 — 多工具編排、記憶體、串流 — 都建立在這個模式之上。
常見工具模式
檔案存取: 在代理的工作目錄內讀取與寫入檔案。將檔案內容以字串形式回傳。
網頁搜尋: 將搜尋結果回傳為 {title, url, snippet} 物件的列表。保持摘要簡短。
程式碼執行: 在隔離環境中執行程式碼並回傳 stdout/stderr(將在下一節中說明)。
結構化資料查詢: 查詢資料庫或 API,並將記錄回傳為 JSON。模型負責總結。
每個工具應只做一件事,並在相同輸入下回傳確定性輸出。試圖將過多邏輯塞入單一工具的代理會更難除錯與評估。
步驟 3:程式碼執行 — 安全執行程式碼
如果您的代理會產生並執行程式碼,您需要一個隔離環境。直接在您的機器上執行模型產生的程式碼存在安全風險:模型可能產生破壞性的 shell 指令、無限迴圈,或讀取不該存取的敏感檔案。
標準方法是使用 沙盒 — 一個短暫、隔離的 Linux 環境,讓代理可以在其中自由執行程式碼,而不會影響主機系統。Novita 的 Agent Sandbox 正是為此使用案例而設計:一個雲端執行環境,可在 200ms 內啟動、執行代理產生的任何程式碼,並在工作階段結束時銷毀該環境。
以下是規劃部分中的 run_code 工具如何使用沙盒實作:
from novita_sandbox.code_interpreter import Sandbox
sandbox = Sandbox.create()
def run_code(code: str) -> str:
result = sandbox.run_code(code)
output = ""
if result.logs.stdout:
output += "\n".join(result.logs.stdout)
if result.logs.stderr:
output += "\nSTDERR: " + "\n".join(result.logs.stderr)
return output or "(no output)"
當代理呼叫 run_code 時,此函式會在隔離環境中執行程式碼,並以字串形式回傳 stdout/stderr。代理看到結果後,決定下一步要做什麼 — 無論是修正錯誤、繼續處理輸出,還是宣告任務完成。
這種架構清楚地區分了推理層與執行層。LLM 負責規劃與呼叫工具;沙盒則在不會暴露容器外任何內容的情況下執行程式碼。
沙盒能處理什麼
- Shell 指令(
bash、git、python、node) - 沙盒內的檔案系統操作
- 容器內部的網路請求
- 長時間執行的背景任務,支援非同步結果擷取
- 暫停與恢復,適用於多工作階段的工作流程
它所不允許的:存取主機系統、其他沙盒,或您未明確傳入的憑證。這種隔離正是讓模型可以安全地撰寫與執行任意程式碼的原因。
步驟 4:評估 — 判斷代理何時正確
評估是代理開發中最常被跳過的一步。一個會產生輸出的代理,並不等同於一個會產生正確輸出的代理。
評估的三個層級
1. 確定性檢查
對於有已知正確答案的任務 — 解析日期、計算數值、擷取欄位 — 針對代理的輸出撰寫單元測試。這類測試執行成本低,且能快速發現回歸錯誤。
2. 基於執行的檢查
對於編碼代理,最可靠的訊號是:程式碼是否真的能執行並產生正確結果?在沙盒中執行代理的輸出,並將 stdout 與預期值進行比較。這比比較程式碼文字更強大,因為它測試的是行為而非語法。
def evaluate_code_task(agent_code: str, expected_stdout: str) -> bool:
result = run_code(agent_code)
return result.strip() == expected_stdout.strip()
3. LLM 作為評審
對於開放式任務 — 撰寫摘要、回答研究問題 — 使用第二次 LLM 呼叫,根據評分標準對輸出進行評分。評分標準應具體明確:「答案是否包含可運作的程式碼範例?」優於「答案好嗎?」模糊的評分標準會產生不一致的分數。
追蹤失敗模式
代理會以一系列反覆出現的模式失敗:
| 失敗 | 原因 | 修正 |
|---|---|---|
| 工具呼叫迴圈 | 模型不斷呼叫同一個工具 | 設定 max_steps;記錄中間步驟 |
| 引數不匹配 | Schema 與實作不一致 | 在呼叫前驗證工具引數 |
| 脈絡溢位 | 工作階段增長超過模型脈絡限制 | 總結或截斷較舊的工具結果 |
| 幻覺工具名稱 | 模型發明了一個不存在的工具 | 使用嚴格的工具列表,搭配 tool_choice: "auto" |
評估不是最後的獨立階段 — 它是開發的一部分。每次擴充代理能力時,至少撰寫一個能捕捉到新行為回歸的測試。
為代理推理選擇模型
大多數教學預設使用封閉原始碼模型進行代理推理。這在原型開發時效果很好,但在生產環境中 — 代理每個任務可能需要進行 20 次 LLM 呼叫 — 模型成本與速率限制會成為實際的限制因素。
透過 Novita LLM API 託管的開放原始碼模型,在代理實際需要的任務上(函式呼叫、結構化輸出、多步驟推理),越來越能與封閉原始碼模型匹敵。像 Llama 3.3 70B 與 DeepSeek-V3 這類模型,在編碼與指令遵循方面的基準測試達到 GPT-4 等級,但執行成本卻低得多。對於高吞吐量的代理工作負載,這種差異很快就會累積起來。
Novita API 完全與 OpenAI 相容,因此從封閉原始碼模型切換到開放原始碼模型只是一個單行變更:
# 之前
model = "gpt-4o"
# 之後 — 相同的 API 契約,更低的成本
model = "meta-llama/llama-3.3-70b-instruct"
對於延遲敏感的代理迴圈,較小的模型(如 meta-llama/llama-3.1-8b-instruct)回應更快,每次步驟成本更低,這在每個使用者任務涉及多個連續呼叫時非常重要。一個實用模式:使用更快、更小的模型進行路由與分類步驟,並使用較大的模型處理需要更多能力的複雜推理步驟。
Novita 模型庫也針對特定的代理子任務提供強選項 — 用於基於影像的工具結果的視覺模型、用於檢索的嵌入模型,以及用於編碼代理的程式碼專用模型 — 均可從同一個 API 端點存取。
組合起來 — 一個最小化的可運作代理
以下是一個完整的代理,它接受一個數學問題、產生 Python 程式碼來解決它、在沙盒中執行,並回傳驗證過的結果:
import os
import json
from openai import OpenAI
from novita_sandbox.code_interpreter import Sandbox
client = OpenAI(
base_url="https://api.novita.ai/v3/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
sandbox = Sandbox.create()
def run_code(code: str) -> str:
result = sandbox.run_code(code)
stdout = "\n".join(result.logs.stdout)
stderr = "\n".join(result.logs.stderr)
return stdout or stderr or "(no output)"
tool_fns = {"run_code": run_code}
tools = [
{
"type": "function",
"function": {
"name": "run_code",
"description": "執行 Python 程式碼。回傳 stdout 或 stderr。",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string"}
},
"required": ["code"]
}
}
}
]
messages = [
{
"role": "system",
"content": "透過撰寫並執行 Python 程式碼來解決任務。使用 run_code 來執行它。當您有經過驗證的答案時請停止。"
},
{
"role": "user",
"content": "小於 1,000,000 的所有偶數費氏數列的總和是多少?"
}
]
for _ in range(10):
response = client.chat.completions.create(
model="meta-llama/llama-3.1-8b-instruct",
messages=messages,
tools=tools,
tool_choice="auto",
)
choice = response.choices[0]
if choice.finish_reason == "stop":
print(choice.message.content)
break
tool_call = choice.message.tool_calls[0]
fn_args = json.loads(tool_call.function.arguments)
result = tool_fns[tool_call.function.name](**fn_args)
messages.append(choice.message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
sandbox.kill()
這涵蓋了約 50 行內的完整代理迴圈:規劃、工具呼叫、執行、脈絡累積、最大步驟防護,以及乾淨的沙盒銷毀。代理會撰寫程式碼、執行它、查看輸出,然後繼續或給出最終答案。
要擴充它:在 tool_fns 與 tools 中加入更多工具,為更困難的任務換上更大的模型,或將沙盒 run_code 替換為任何其他函式 — 網路搜尋、資料庫查詢、API 呼叫。迴圈結構保持不變。
常見問題
AI 代理與聊天機器人有何不同?
聊天機器人產生文字回應。代理則採取行動 — 呼叫工具、執行程式碼、查詢 API — 並使用結果繼續朝目標推理。工具呼叫與回饋迴圈的存在,就是構成代理的關鍵。
我需要像 LangChain 這樣的框架來建構 AI 代理嗎?
不需要。核心代理迴圈已經足夠簡單,可以直接針對任何與 OpenAI 相容的 API 實作。框架在複雜編排(多代理系統、持久記憶體、串流輸出)方面有其價值,但它們會增加抽象層,可能讓除錯變得更困難。從原始 API 開始,然後在您確實需要框架提供的功能時再加入。
如何防止代理執行危險的程式碼?
使用沙盒執行環境。代理的程式碼在隔離容器內執行,該容器無法存取您的主機系統或其他程序。沙盒在工作階段結束後會被銷毀,因此代理所做的任何變更都僅限於該容器內。
開放原始碼模型能否可靠地處理函式呼叫?
可以 — 但需視模型大小而定。Llama 3.3 70B、DeepSeek-V3 與 Qwen 2.5 72B 都能很好地處理結構化工具呼叫。較小的模型(7B–8B)適用於簡單、定義明確的工具 schema,但在模糊指令或複雜的多步驟推理鏈上可能會遇到困難。對於生產環境的代理,請在您實際的任務分佈上進行測試,然後再決定使用哪個模型。
代理每個任務應採取多少步驟?
設定一個硬上限 — 對大多數任務來說,10 到 20 步是典型的。記錄中間步驟,以便您能看到代理確切的動作並除錯失敗。始終需要更多步驟的任務,通常更適合分解為子代理或子任務,而非延長迴圈限制。
