ctx.llm 是外掛進行 LLM 呼叫的受支援方式。
聊天補全、結構化擷取、同步、非同步、含或不含圖片 — 相同的介面、相同的信任閘門、相同的主機端認證。
外掛在需要做一些涉及模型但不屬於代理程式對話一部分的工作時會使用它。一個將工具錯誤改寫為非工程師可讀內容的鉤子。一個在佇列前翻譯入站訊息的閘道器適配器。一個摘要化長篇貼上的斜線命令。一個評分昨天活動並在狀態板上寫入一行的排程任務。一個決定訊息是否值得喚醒代理程式的預過濾器。
這些是代理程式不應該參與其中的工作。它們只需要一次 LLM 呼叫、一個有類型的答案,然後完成。
最小的呼叫
result = ctx.llm.complete(messages=[{"role": "user", "content": "ping"}])
return result.text
這就是完整的 API,一行搞定。無需金鑰、無需 provider 設定、無需 SDK 初始化。外掛針對使用者目前使用的 provider 和模型運行 — 當他們切換 provider 時,外掛會自動跟隨。
更完整的聊天範例
result = ctx.llm.complete(
messages=[
{"role": "system", "content": "Rewrite errors as one short sentence a non-engineer can act on."},
{"role": "user", "content": traceback_text},
],
max_tokens=64,
purpose="hooks.error-rewrite",
)
return result.text
purpose 是一個自由格式的稽核字串 — 它會出現在 agent.log 和 result.audit 中,以便運維人員可以看到哪個外掛做了哪個呼叫。選用但建議用於任何頻繁觸發的場景。
結構化輸出
當外掛需要有類型的答案時,切換到結構化通道:
result = ctx.llm.complete_structured(
instructions="Score this support reply for urgency (0–1) and pick a category.",
input=[{"type": "text", "text": message_body}],
json_schema=TRIAGE_SCHEMA,
purpose="support.triage",
temperature=0.0,
max_tokens=128,
)
if result.parsed["urgency"] > 0.8:
await dispatch_to_oncall(result.parsed["category"], message_body)
主機向 provider 請求 JSON 輸出,在本地回退解析,如果安裝了 jsonschema 則根據你的 schema 驗證,並將 Python 物件放在 result.parsed 上回傳。如果模型無法產生有效的 JSON,result.parsed 為 None,result.text 攜帶原始回應。
這個通道給你什麼
- 一次呼叫,四種形式。
complete()用於聊天,complete_structured()用於有類型的 JSON,acomplete()和acomplete_structured()用於 asyncio。相同的參數,相同的結果物件。 - 主機端擁有的認證。 OAuth 權杖、刷新流程、認證池、每個任務的輔助覆寫 — Hermes 已有的每個認證概念都適用。外掛永遠看不到權杖;主機透過
result.audit歸屬呼叫。 - 有限範圍。 單次同步或非同步呼叫。無串流、無工具迴圈、無需管理的對話狀態。陳述輸入,取得結果,返回。
- 失敗即關閉的信任。 你從未設定過的外掛無法選擇自己的 provider、模型、代理程式或儲存的認證。預設姿態是「使用使用者正在使用的」。運維人員在
config.yaml中按外掛選擇特定覆寫。
快速入門
以下是兩個完整的外掛 — 一個聊天,一個結構化。兩者都在單個 register(ctx) 函式內發佈,運行時零額外設定,針對使用者啟用的任何模型。
聊天補全 — /tldr
def register(ctx):
ctx.register_command(
name="tldr",
handler=lambda raw: _tldr(ctx, raw),
description="Summarise the supplied text in one paragraph.",
args_hint="<text>",
)
def _tldr(ctx, raw_args: str) -> str:
text = raw_args.strip()
if not text:
return "Usage: /tldr <text to summarise>"
result = ctx.llm.complete(
messages=[
{"role": "system",
"content": "Summarise the user's text in one tight paragraph. No preamble."},
{"role": "user", "content": text},
],
max_tokens=256,
temperature=0.3,
purpose="tldr",
)
return result.text
result.text 是模型的回應;result.usage 攜帶 token 計數;result.provider 和 result.model 攜帶歸屬。
結構化擷取 — /paste-to-tasks
def register(ctx):
ctx.register_command(
name="paste-to-tasks",
handler=lambda raw: _paste_to_tasks(ctx, raw),
description="Turn freeform meeting notes into structured tasks.",
args_hint="<text>",
)
_TASKS_SCHEMA = {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"owner": {"type": "string"},
"action": {"type": "string"},
"due": {"type": "string", "description": "ISO date or empty"},
},
"required": ["action"],
},
},
},
"required": ["tasks"],
}
def _paste_to_tasks(ctx, raw_args: str) -> str:
if not raw_args.strip():
return "Usage: /paste-to-tasks <meeting notes>"
result = ctx.llm.complete_structured(
instructions=(
"Extract concrete action items from these meeting notes. "
"One task per actionable line. If no owner is named, leave 'owner' blank."
),
input=[{"type": "text", "text": raw_args}],
json_schema=_TASKS_SCHEMA,
schema_name="meeting.tasks",
purpose="paste-to-tasks",
temperature=0.0,
max_tokens=512,
)
if result.parsed is None:
return f"Couldn't parse a response. Raw output:\n{result.text}"
lines = [f"- [{t.get('owner') or '?'}] {t['action']}" for t in result.parsed["tasks"]]
return "\n".join(lines) or "(no tasks found)"
第三個完整的範例(這次帶有圖片輸入)位於 hermes-example-plugins repo(參考外掛的附屬 repo — 不隨 hermes-agent 本身一起發佈)。關於非同步介面(acomplete() / acomplete_structured() 搭配 asyncio.gather()),請見同 repo 中的 plugin-llm-async-example。
何時使用哪個
| 你需要… | 使用 |
|---|---|
| 自由格式的文字回應(翻譯、摘要、改寫、生成) | complete() |
| 多回合提示詞(系統 + 少量範例 + 使用者) | complete() |
| 回傳有類型的字典,根據 schema 驗證 | complete_structured() |
| 圖片或文字輸入,回傳有類型的字典 | complete_structured() |
| 從非同步程式碼中進行相同的呼叫(閘道器適配器、非同步鉤子) | acomplete() / acomplete_structured() |
其他一切 — provider 選擇、模型解析、認證、後備、逾時、視覺路由 — 在四者之間都是相同的。
API 介面
ctx.llm 是 agent.plugin_llm.PluginLlm 的實例。
complete()
result = ctx.llm.complete(
messages=[{"role": "user", "content": "Hi"}],
provider=None, # 選用,有門控 — Hermes provider ID(例如 "openrouter")
model=None, # 選用,有門控 — 該 provider 期望的任何字串
temperature=None,
max_tokens=None,
timeout=None, # 秒
agent_id=None, # 選用,有門控
profile=None, # 選用,有門控 — 明確的認證設定檔名稱
purpose="optional-audit-string",
)
# → PluginLlmCompleteResult(text, provider, model, agent_id, usage, audit)
純聊天補全。messages 是標準的 OpenAI 格式 — {"role": "...", "content": "..."} 字典的列表。多回合提示詞(系統 + 少量範例使用者/助理配對 + 最終使用者)的工作方式與 OpenAI SDK 完全相同。
provider= 和 model= 是獨立的,遵循與主機主設定(model.provider + model.model)相同的格式。只設置 model= 以使用使用者的啟用 provider 但使用不同的模型。兩者都設置以完全切換 provider。任何參數在沒有運維人員選擇的情況下會引發 PluginLlmTrustError。
complete_structured()
result = ctx.llm.complete_structured(
instructions="What you want extracted.",
input=[
{"type": "text", "text": "..."},
{"type": "image", "data": b"...", "mime_type": "image/png"},
{"type": "image", "url": "https://..."},
],
json_schema={...}, # 選用 — 觸發已解析結果 + 驗證
json_mode=False, # 在沒有 schema 時設為 True 以請求 JSON
schema_name=None, # 選用的人類可讀 schema 名稱
system_prompt=None,
provider=None, # 選用,有門控
model=None, # 選用,有門控
temperature=None,
max_tokens=None,
timeout=None,
agent_id=None,
profile=None,
purpose=None,
)
# → PluginLlmStructuredResult(text, provider, model, agent_id,
# usage, parsed, content_type, audit)
輸入是有類型的文字或圖片區塊(原始位元組會自動 base64 編碼為 data: URL)。當提供 json_schema 或 json_mode=True 時,主機透過 response_format 請求 JSON 輸出,在本地回退解析,如果安裝了 jsonschema 則根據你的 schema 驗證。
result.content_type == "json"—result.parsed是符合你的 schema 的 Python 物件。result.content_type == "text"— 解析或驗證失敗;檢查result.text以取得原始模型回應。
非同步
result = await ctx.llm.acomplete(messages=...)
result = await ctx.llm.acomplete_structured(instructions=..., input=...)
與其同步對應項相同的參數和結果類型。從閘道器適配器、非同步鉤子或任何已在 asyncio 迴圈上運行的外掛程式碼中使用。
結果屬性
@dataclass
class PluginLlmCompleteResult:
text: str # 助理的回應
provider: str # 例如 "openrouter"、"anthropic"
model: str # 該 provider 本次呼叫回傳的模型
agent_id: str # 使用了誰的模型/認證
usage: PluginLlmUsage # token + 快取 + 成本估算
audit: Dict[str, Any] # plugin_id、purpose、profile
@dataclass
class PluginLlmStructuredResult(PluginLlmCompleteResult):
parsed: Optional[Any] # 當 content_type == "json" 時的 JSON 物件
content_type: str # "json" 或 "text"
# 當提供時 audit 也攜帶 schema_name
usage 攜帶 input_tokens、output_tokens、total_tokens、cache_read_tokens、cache_write_tokens 和 cost_usd(當 provider 回傳這些欄位時)。
信任閘門
預設行為是失敗即關閉。在沒有 plugins.entries 設定區塊的情況下,外掛可以:
- 針對使用者的啟用 provider 和模型運行四個方法中的任何一個,
- 設定請求塑造參數(
temperature、max_tokens、timeout、system_prompt、purpose、messages、instructions、input、json_schema),
…就這些了。provider=、model=、agent_id= 和 profile= 參數在運維人員選擇之前會引發 PluginLlmTrustError。
大多數外掛永遠不需要這個區塊。 只要呼叫 ctx.llm.complete(messages=...) 而不帶覆寫的外掛,會針對使用者啟用的任何設定運行且零設定。以下區塊僅在特定外掛想要固定到不同於使用者的模型或 provider 時相關。
plugins:
entries:
my-plugin:
llm:
# 允許此 plugin 選擇不同的 Hermes provider
# (必須是 Hermes 已知的 — 與 `hermes model` 和
# config.yaml model.provider 相同的名稱)。
allow_provider_override: true
# 選用:限制哪些 provider。使用 ["*"] 表示任何。
allowed_providers:
- openrouter
- anthropic
# 允許此 plugin 請求特定的模型。
allow_model_override: true
# 選用:限制哪些模型。使用 ["*"] 表示任何。
# 模型與 plugin 發送的字串完全匹配 — Hermes 不查詢任何內容。
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku
# 允許跨代理程式呼叫(少見)。
allow_agent_id_override: false
# 允許 plugin 請求特定的已儲存認證設定檔
# (例如同一 provider 上的不同 OAuth 帳號)。
allow_profile_override: false
外掛 ID 是扁平外掛的 manifest name: 欄位,或巢狀外掛的路徑衍生金鑰(image_gen/openai、memory/honcho 等)。
閘門執行什麼
| 覆寫 | 預設值 | 設定金鑰 |
|---|---|---|
provider= | 拒絕 | allow_provider_override: true |
| ↳ 允許清單 | — | allowed_providers: [...] |
model= | 拒絕 | allow_model_override: true |
| ↳ 允許清單 | — | allowed_models: [...] |
agent_id= | 拒絕 | allow_agent_id_override: true |
profile= | 拒絕 | allow_profile_override: true |
每個覆寫是獨立門控的。授予 allow_model_override 不會同時授予 allow_provider_override — 被信任選擇模型的外掛仍然固定在使用者的啟用 provider 上,除非它也取得 provider 門控。
閘門不需要執行什麼
- 請求塑造參數 —
temperature、max_tokens、timeout、system_prompt、purpose、messages、instructions、input、json_schema、schema_name、json_mode— 始終允許;它們不選擇認證或路由。 - 預設拒絕姿態意味著未設定的外掛仍然可以做有用的工作 — 它只針對啟用的 provider 和模型運行。運維人員只需要為想要更精細路由的外掛考慮
plugins.entries。
主機擁有的內容
ctx.llm 為外掛完成的所有事情的完整清單,這樣你就不必自己做:
- Provider 解析。 從使用者設定(或受信任時的明確覆寫)讀取
model.provider+model.model。 - 認證。 從
~/.hermes/auth.json/ 環境變數取得 API key、OAuth 權杖或刷新權杖,包括設定認證池時。外掛永遠看不到它們。 - 視覺路由。 當提供圖片輸入而使用者的啟用文字模型僅支援文字時,主機自動回退到設定的視覺模型。
- 後備鏈。 如果使用者的主要 provider 5xx 或 429,請求會經過 Hermes 通常的聚合器感知後備,然後才向回傳錯誤給外掛。
- 逾時。 遵循你的
timeout=參數,回退到auxiliary.<task>.timeout設定或全域輔助預設值。 - JSON 塑造。 當你要求 JSON 時向 provider 發送
response_format,然後如果 provider 回傳了程式碼區塊回應則在本地重新解析。 - Schema 驗證。 當安裝了
jsonschema時根據你的json_schema驗證;否則記錄偵測行並跳過嚴格驗證。 - 稽核日誌。 每次呼叫向
agent.log寫入一行 INFO,包含外掛 ID、provider/model、purpose 和 token 總計。
外掛擁有的內容
- 請求格式。 聊天用
messages,結構化用instructions+input。外掛建構提示詞;主機執行它。 - Schema。 你想要回傳的任何格式。主機不會為你推斷。
- 錯誤處理。
complete_structured()在空輸入和 schema 驗證失敗時引發ValueError。當信任閘門拒絕覆寫時引發PluginLlmTrustError。其他任何情況(provider 5xx、無認證設定、逾時)引發auxiliary_client.call_llm()引發的任何異常。 - 成本。 每次呼叫都針對使用者付費的 provider 運行。不要在沒有考慮 token 花費的情況下對每個閘道器訊息在
complete()上迴圈。
在外掛介面中的定位
現有的 ctx.* 方法擴展了現有的 Hermes 子系統:
| ctx.register_tool | 新增代理程式可呼叫的工具 |
| ctx.register_platform | 連接新的閘道器適配器 |
| ctx.register_image_gen_provider | 替換圖片生成後端 |
| ctx.register_memory_provider | 替換記憶體後端 |
| ctx.register_context_engine | 替換上下文壓縮器 |
| ctx.register_hook | 觀察生命週期事件 |
ctx.llm 是第一個讓外掛運行使用者正在對話的相同模型的介面,帶外運行,不依賴上述任何功能。這就是它唯一的工作。如果你的外掛需要註冊一個代理程式呼叫的工具,使用 register_tool。如果需要反應生命週期事件,使用 register_hook。如果需要進行自己的模型呼叫 — 無論什麼原因,結構化或非結構化 — 使用 ctx.llm。
參考
- 實作:
agent/plugin_llm.py - 測試:
tests/agent/test_plugin_llm.py - 參考外掛(附屬 repo):
plugin-llm-example— 同步結構化擷取含圖片輸入plugin-llm-async-example— 非同步搭配asyncio.gather()
- 輔助客戶端(底層引擎):請見 Provider Runtime。