繁中 ▾

OpenAI 相容的無審查 LLM APIuncensoredapis.com

首頁Agent

無審查 LLM API 上的 Agent:函式呼叫、框架與防護措施

Agent 是一個迴圈:模型請求函式,您的程式碼執行它,然後將結果放回對話中。由於無審查 LLM API 以標準聊天格式支援函式呼叫,您可以僅用四十行程式碼建立該迴圈,然後將相同的端點接入 LangChain 或 LlamaIndex。本指南涵蓋這兩者,以及確保工具執行安全的檢查機制。

更新於

重點摘要

  1. 在請求中傳遞工具,監控 finish_reason 等於 tool_calls 的情況,執行函式,附加 tool 訊息,並重複直到模型以文字回答為止。
  2. LangChain 需要帶有 base_url 的 ChatOpenAI;LlamaIndex 需要帶有 api_base 且已設定函式呼叫旗標的 OpenAILike。
  3. 模型會提出呼叫建議;你的程式碼決定是否執行。使用白名單、驗證、限制時間並限制迴圈次數。
  4. Agent 會增加請求數量,因此請為每分鐘 300 次請求的限制以及 100,000 token 的上下文視窗做好預算。

函式呼叫迴圈的結構

去除框架後,每個 Agent 都會執行相同的四件事。你傳送對話加上 tools 列表。模型回覆文字(表示已完成),或回覆 tool_calls(表示希望執行某些動作)。你執行每次呼叫,並針對每次呼叫附加一則 tool 訊息,引用該呼叫的 id。然後帶著更長的對話再次詢問模型。

分支的訊號是 finish_reason。當它等於 tool_calls 時,助手訊息的 content 設為 null,並包含 tool_calls 陣列,每個項目包含 id、函式名稱以及 JSON 參數字串。參考頁面顯示確切的結構。兩個細節常讓人困擾:參數是必須解析的字串,且包含呼叫的助手訊息必須依序保留在歷史記錄中,位於你的工具結果之前。

從頭建立完整迴圈

下方的腳本定義了一個函式,以 JSON 結構描述它,並迴圈直到模型產生純文字為止。它使用指向 API 的官方 Python SDK。每次迭代都會附加助手訊息,然後針對每次呼叫附加一則 tool 訊息,並使用步驟計數器防止行為失控。

import json, os
from openai import OpenAI

client = OpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])

TICKETS = {"T-101": "open", "T-102": "closed"}

def ticket_status(ticket_id: str) -> str:
    return TICKETS.get(ticket_id, "unknown ticket")

TOOLS = [{
    "type": "function",
    "function": {
        "name": "ticket_status",
        "description": "Look up the status of a support ticket by id.",
        "parameters": {
            "type": "object",
            "properties": {"ticket_id": {"type": "string"}},
            "required": ["ticket_id"],
        },
    },
}]
REGISTRY = {"ticket_status": ticket_status}

messages = [{"role": "user", "content": "Are tickets T-101 and T-102 still open?"}]

for step in range(6):                      # hard cap on loop turns
    r = client.chat.completions.create(
        model="uncensored", messages=messages, tools=TOOLS, max_tokens=400)
    msg = r.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))
    if r.choices[0].finish_reason != "tool_calls":
        print(msg.content)
        break
    for call in msg.tool_calls:
        fn = REGISTRY.get(call.function.name)
        try:
            args = json.loads(call.function.arguments)
            result = fn(**args) if fn else f"error: unknown tool {call.function.name}"
        except Exception as exc:
            result = f"error: {exc}"
        messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
else:
    print("stopped: step limit reached")

注意迴圈在失敗時所做的處理:錯誤會變成字串形式的工具結果。這讓模型可以讀取問題並嘗試修正,例如修正錯誤的票證 id,而不是讓你的程式當機。for ... else 子句僅在達到上限且沒有最終答案時觸發,這是你偵測模型陷入無窮迴圈的方式。

保持提示詞專注。簡短的系統訊息說明何時呼叫工具以及何時直接回答,可減少不必要的呼叫,每次跳過的呼叫都能節省往返時間、token 以及速率限制的一個槽位。

防護工具執行

模型輸出是未經信任的輸入。模型選擇的工具名稱或參數可能是錯誤、奇怪或受模型從網頁或使用者讀取的文本影響。將每次呼叫視為來自公共網際網路的請求。

  • 依名稱建立白名單。 在你建立的註冊表中查找函式。切勿使用字串來匯入或評估程式碼。
  • 區分讀取與寫入。 唯讀工具可自由執行。任何發送郵件、花費金錢、刪除資料或觸及檔案的操作都需要明確的人類核准。
  • 根據你宣告的結構描述驗證參數:必要鍵、無額外鍵、類型與長度限制。
  • 限制時間與輸出。 套用超時時間,並在將結果回傳前進行截斷,以免巨大的工具輸出耗盡你的 100,000 token 上下文。
  • 限制迭代次數與成本。 限制每個任務的步驟數與每個工作階段的 token 數,使用 usage 區塊。
  • 記錄每次呼叫,包含名稱、參數與結果,以便稽核行為。
import json
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutTimeout

ALLOWED = {"ticket_status", "lookup_invoice"}      # read-only tools only
NEEDS_APPROVAL = {"refund_order"}                  # side effects wait for a human
POOL = ThreadPoolExecutor(max_workers=4)

def validate(args: dict, schema: dict) -> None:
    props = schema["properties"]
    for key in schema.get("required", []):
        if key not in args:
            raise ValueError(f"missing argument: {key}")
    for key, val in args.items():
        if key not in props:
            raise ValueError(f"unexpected argument: {key}")
        if props[key]["type"] == "string" and not isinstance(val, str):
            raise ValueError(f"{key} must be a string")
        if isinstance(val, str) and len(val) > 200:
            raise ValueError(f"{key} too long")

def run_tool(name, raw_args, registry, schemas, approve=lambda n, a: False):
    if name in NEEDS_APPROVAL:
        args = json.loads(raw_args)
        if not approve(name, args):
            return "denied: this action needs human approval"
    elif name not in ALLOWED:
        return f"error: tool {name} is not permitted"
    args = json.loads(raw_args)
    validate(args, schemas[name]["parameters"])
    future = POOL.submit(registry[name], **args)
    try:
        return str(future.result(timeout=5))[:2000]    # time and size limits
    except FutTimeout:
        return "error: tool timed out"

將工具結果也視為未經信任。如果工具回傳的爬取文本包含指令,模型可能會遵循它們。在 tool 訊息中將此類內容用明確的分隔符號包起來,並在系統提示詞中說明工具輸出是資料而非指令。

撰寫模型可用的工具結構描述

結構描述是模型看到的唯一文件,因此值得像公共 API 一樣精心設計。使用與任務相符的動詞和名詞命名函式,例如 ticket_status 或 lookup_invoice,而非 run 或 helper 等通用標籤。將 description 寫成指令:說明函式的作用、回傳內容以及何時不使用它。

  • 保持參數少且扁平。 兩三個簡單欄位的呼叫正確率遠高於包含十個欄位的巢狀物件。
  • 對閉集合使用列舉,例如 "enum": ["open", "closed"],這樣模型就不會發明值。
  • 描述每個屬性,加上單位或範例:"票證 id,例如 T-101" 勝過單純的字串。
  • 回傳精簡結果。字串或小 JSON 物件效果最佳;冗長的資料會消耗上下文卻無太大收穫。
  • 偏好多個窄義工具,而非帶有模式旗標的單一綜合工具。

當呼叫驗證失敗時,將驗證訊息作為工具結果回傳,而不是拋出異常。模型通常會在下一輪修正呼叫,因為它可以讀取哪裡出錯。如果它以相同方式失敗兩次,請重寫描述,而不是迴圈。

LangChain 設定

LangChain 的 ChatOpenAI 類別接受 base_url,因此不需要自訂供應商套件。將 model 設為 uncensored,傳遞來自環境的金鑰,並使用 bind_tools 綁定工具。安裝 langchain-openai 和 langchain-core。

import os
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = ChatOpenAI(
    model="uncensored",
    base_url="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    temperature=0.3,
    max_tokens=400,
)
llm_with_tools = llm.bind_tools([ticket_status])

ai = llm_with_tools.invoke("What is the status of ticket T-101?")
for call in ai.tool_calls:
    print(call["name"], call["args"], "->", ticket_status.invoke(call["args"]))

invoke 的結果是一個 AIMessage,其 tool_calls 清單包含已解析的名稱與參數,因此你無需手動處理 JSON 步驟。若要建立完整的自主迴圈,請將相同的 llm 物件傳入 LangChain 的 Agent 建構函式;端點只需支援函式呼叫即可,而它確實做到了。如果 Agent 似乎忽略了其工具,請檢查是否已提供文件字串與型別提示,因為它們會成為模型看到的結構描述。

LlamaIndex 設定

LlamaIndex 內建了 OpenAILike 以支援遵循聊天完成格式的服務器。有三個設定很重要。api_base 指向 API,is_chat_model=True 選擇聊天路由,is_function_calling_model=True 告訴框架它可以傳送工具。將 context_window 設為 100000,以便讓文字分割器和記憶體緩衝區正確設定大小。安裝 llama-index-llms-openai-like。

import os
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.tools import FunctionTool

def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = OpenAILike(
    model="uncensored",
    api_base="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    is_chat_model=True,
    is_function_calling_model=True,
    context_window=100000,
    max_tokens=400,
)

tool = FunctionTool.from_defaults(fn=ticket_status)
response = llm.predict_and_call([tool], "Check ticket T-102 for me.")
print(response)

從這裡你可以將相同的 llm 傳遞給 Agent 工作節點或查詢引擎。記住,檢索管線可能會將許多區塊塞入單一提示詞;在提示詞與完成之間共享 100,000 token 視窗的情況下,請限制檢索區塊的數量,並保持 max_tokens 適中。

在預算內操作 Agent

Agent 會將一個使用者訊息轉換為多個請求,因此請規劃容量。一個具有五次工具輪次的任務會消耗六次模型呼叫。因此,十個同時進行的任務會在短時間內產生六十次請求,每分鐘 300 次請求的上限會比聊天應用程式更快達到。將 錯誤與速率限制指南 中的令牌桶節流器放在你的客戶端前面。

由於每次工具結果都會被附加,上下文會隨著每一輪而增長。一旦舊的工具輸出已被使用,請進行摘要或捨棄,並留意 usage.prompt_tokens;當其接近 50,000 時,請壓縮歷史記錄。就成本而言,假設一個示範任務包含 6 次呼叫,平均每次 3,000 個提示 token 和 200 個完成 token:這代表 18,000 個輸入 token(約 $0.0045,每百萬 token $0.25)和 1,200 個輸出 token(約 $0.0012,每百萬 token $1.00)。這些 token 計數是總和的假設,而非測量值。如果您是該端點的新手,請從 文件 和免費試用額度開始。

測試與除錯 Agent

Agent 的失敗方式往往是單一函式的單元測試無法涵蓋的,因此請測試對話。記錄每次執行的完整訊息列表,包括工具參數和結果,並從日誌中重播失敗。三個習慣能迅速帶來回報。

  1. 模擬模型。 模擬客戶端,使其回傳腳本化的 tool_calls 訊息。這可在無需網路和無需花費的情況下測試你的註冊表、驗證和迴圈終止機制。
  2. 固定採樣。 對於工具驅動的任務,請使用較低的 temperature,例如 0.2。較低的隨機性會產生更具可重複性的參數 JSON,您可以在後續的寫作密集型步驟中再提高它。
  3. 驗證執行路徑。對於少數固定提示詞,請檢查呼叫了哪些工具以及呼叫順序,而不僅是最終文字。

同時請監控 finish_reason。在工具呼叫中間出現 length 值表示參數被截斷;請提高 max_tokens(最高 32,000),JSON 就會完成。串流輸出同樣支援工具,但請針對每個呼叫索引累積參數片段,並僅在串流關閉後進行解析。若要重新了解這些區塊,請參閱參考資料的 串流輸出章節。

問答

API 是否支援在一次回覆中進行並行工具呼叫?

訊息包含 tool_calls 陣列,因此請處理其中的每一筆項目,並針對每個呼叫 ID 傳送一則工具訊息。

為什麼我的 Agent 會無限迴圈?

通常是工具錯誤訊息不夠明確,或是系統提示詞未說明何時停止。請加入步驟上限,並回傳清晰的錯誤字串。

我需要 LangChain 或 LlamaIndex 嗎?

不需要。使用 OpenAI SDK 的簡單迴圈即可,框架僅是在相同的端點之上增加協調功能。

如何防止模型執行破壞性工具?

切勿在未經過核准步驟的情況下註冊它們。只有你註冊表中的函式可以執行,因此請將寫入操作保留給人類確認。

只差一張表單,即可取得金鑰

建立帳戶、複製金鑰、更改基礎 URL。這就是完整的設定。