插件 LLM 訪問
ctx.llm 是插件進行 LLM 調用的支持方式。
聊天補全、結構化提取、同步、異步、帶或不帶圖像——相同的接口表面,相同的信任網關,相同的主機託管憑據。
當插件需要執行涉及模型但不屬於代理對話的任務時,會使用此功能。例如:將工具錯誤重寫為非工程師可讀內容的鉤子;在入隊前翻譯傳入消息的網關適配器;總結長粘貼文本的斜槓命令;對昨日活動進行評分並向狀態板寫入一行的定時任務;或者決定消息是否值得喚醒代理的預過濾器。
這些是代理不應參與循環的任務。它們只需要一次 LLM 調用、一個類型化的答案,然後完成。
最小可能的調用
result = ctx.llm.complete(messages=[{"role": "user", "content": "ping"}])
return result.text
這就是整個 API,只需一行代碼。無需密鑰,無需提供商配置,無需 SDK 初始化。插件針對用戶當前使用的任何提供商和模型運行——當用戶切換提供商時,插件會自動跟隨。
更完整的聊天示例
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)
主機向提供商請求 JSON 輸出,作為回退在本地解析,如果安裝了 jsonschema 則根據你的模式進行驗證,並在 result.parsed 上返回 Python 對象。如果模型無法生成有效的 JSON,result.parsed 為 None,而 result.text 攜帶原始響應。
此通道提供的功能
- 一次調用,四種形態。
complete()用於聊天,complete_structured()用於類型化 JSON,acomplete()和acomplete_structured()用於 asyncio。參數相同,結果對象相同。 - 主機託管憑據。 OAuth 令牌、刷新流程、憑據池、每任務輔助覆蓋——Hermes 已有的每個憑據概念均適用。插件永遠看不到令牌;主機通過
result.audit歸因調用。 - 有界。 單次同步或異步調用。無流式傳輸,無工具循環,無需管理對話狀態。陳述輸入,獲取結果,返回。
- 故障關閉信任。 你從未配置過的插件無法選擇自己的提供商、模型、代理或存儲的憑據。默認姿態是“使用用戶正在使用的內容”。操作員在
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 攜帶令牌計數;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 倉庫中(參考插件的配套倉庫——未與 hermes-agent 本身捆綁)。對於異步接口(帶有 asyncio.gather() 的 acomplete() / acomplete_structured()),請參閱同一倉庫中的 plugin-llm-async-example。
何時使用哪種
| 你想要… | 使用 |
|---|---|
| 自由格式文本響應(翻譯、總結、重寫、生成) | complete() |
| 多輪提示(系統 + 少樣本示例 + 用戶) | complete() |
| 針對模式驗證的類型化字典返回 | complete_structured() |
| 帶有類型化字典返回的圖像或文本輸入 | complete_structured() |
| 從異步代碼發起相同調用(網關適配器、異步鉤子) | acomplete() / acomplete_structured() |
其他所有內容——提供商選擇、模型解析、身份驗證、回退、超時、視覺路由——在所有四種情況下都是相同的。
API 表面
ctx.llm 是 agent.plugin_llm.PluginLlm 的一個實例。
complete()
result = ctx.llm.complete(
messages=[{"role": "user", "content": "Hi"}],
provider=None, # optional, gated — Hermes provider id (e.g. "openrouter")
model=None, # optional, gated — whatever string that provider expects
temperature=None,
max_tokens=None,
timeout=None, # seconds
agent_id=None, # optional, gated
profile=None, # optional, gated — explicit auth-profile name
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= 以使用用戶的活躍提供商及其上的不同模型。同時設置兩者以完全切換提供商。如果沒有操作員選擇加入,任一參數都會引發 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={...}, # optional — triggers parsed result + validation
json_mode=False, # set True without a schema to ask for JSON anyway
schema_name=None, # optional human-readable schema name
system_prompt=None,
provider=None, # optional, gated
model=None, # optional, gated
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以獲取原始模型響應。
異步 (Async)
result = await ctx.llm.acomplete(messages=...)
result = await ctx.llm.acomplete_structured(instructions=..., input=...)
參數和結果類型與其同步對應項相同。從網關適配器、異步鉤子或任何已在 asyncio 循環中運行的插件代碼中使用這些方法。
結果屬性 (Result attributes)
@dataclass
class PluginLlmCompleteResult:
text: str # the assistant's response
provider: str # e.g. "openrouter", "anthropic"
model: str # whatever the provider returned for this call
agent_id: str # whose model/auth was used
usage: PluginLlmUsage # tokens + cache + cost estimate
audit: Dict[str, Any] # plugin_id, purpose, profile
@dataclass
class PluginLlmStructuredResult(PluginLlmCompleteResult):
parsed: Optional[Any] # JSON object when content_type == "json"
content_type: str # "json" or "text"
# audit also carries schema_name when supplied
當提供商返回這些字段時,usage 包含 input_tokens、output_tokens、total_tokens、cache_read_tokens、cache_write_tokens 和 cost_usd。
信任網關 (Trust gate)
默認行為是故障關閉(fail-closed)。如果沒有 plugins.entries 配置塊,插件可以:
- 針對用戶活動的提供商和模型運行四種方法中的任何一種,
- 設置請求整形參數(
temperature、max_tokens、timeout、system_prompt、purpose、messages、instructions、input、json_schema),
……僅此而已。provider=、model=、agent_id= 和 profile= 參數會拋出 PluginLlmTrustError,直到操作員選擇啟用為止。
大多數插件永遠不需要本節。 一個僅調用 ctx.llm.complete(messages=...) 且無覆蓋的插件會針對用戶當前活動的配置運行,無需任何配置即可工作。下面的塊僅在插件特別希望固定到與用戶不同的模型或提供商時才相關。
plugins:
entries:
my-plugin:
llm:
# Allow this plugin to choose a different Hermes provider
# (must be one Hermes already knows about — same names as
# `hermes model` and config.yaml model.provider).
allow_provider_override: true
# Optionally restrict which providers. Use ["*"] for any.
allowed_providers:
- openrouter
- anthropic
# Allow this plugin to ask for a specific model.
allow_model_override: true
# Optionally restrict which models. Use ["*"] for any.
# Models are matched literally against whatever string the
# plugin sends — Hermes does not look anything up.
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku
# Allow cross-agent calls (rare).
allow_agent_id_override: false
# Allow the plugin to request a specific stored auth profile
# (e.g. a different OAuth account on the same provider).
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 — 即使插件被信任可以選擇模型,除非它也獲得了提供商網關權限,否則仍會被限制在用戶活動的提供商上。
網關不需要強制執行的內容
- 請求整形參數 —
temperature、max_tokens、timeout、system_prompt、purpose、messages、instructions、input、json_schema、schema_name、json_mode— 始終允許;它們不涉及選擇憑據或路由。 - 默認的拒絕姿態意味著未配置的插件仍然可以執行有用的工作 — 它只是針對活動的提供商和模型運行。操作員只需要為想要更精細路由的插件考慮
plugins.entries。
宿主擁有的內容
以下是 ctx.llm 為插件執行的完整事項列表,因此你無需自行處理:
- 提供商解析。 從用戶的配置中讀取
model.provider+model.model(或在受信任時使用顯式覆蓋)。 - 認證。 從
~/.hermes/auth.json/ 環境變量中提取 API 密鑰、OAuth 令牌或刷新令牌,包括在配置了憑據池時的情況。插件永遠不會看到這些憑據。 - 視覺路由。 當提供圖像輸入且用戶活動的文本模型僅支持文本時,宿主會自動回退到配置的視覺模型。
- 回退鏈。 如果用戶的主要提供商返回 5xx 或 429 錯誤,請求會在向插件返回錯誤之前經過 Hermes 常規的聚合器感知回退流程。
- 超時。 遵守你的
timeout=參數,回退到auxiliary.<task>.timeout配置或全局 aux 默認值。 - JSON 整形。 當你請求 JSON 時,向提供商發送
response_format,如果提供商返回了代碼圍欄響應,則在本地重新解析。 - Schema 驗證。 當安裝了
jsonschema時,根據你的json_schema進行驗證;否則記錄調試行並跳過嚴格驗證。 - 審計日誌。 每次調用都會向
agent.log寫入一行 INFO 日誌,包含插件 ID、提供商/模型、用途和令牌總數。
插件擁有的內容
- 請求形狀(Request shape)。 聊天使用
messages,結構化使用instructions+input。插件構建提示詞;宿主執行它。 - 模式(Schema)。 你希望返回的任何形狀。宿主不會為你推斷它。
- 錯誤處理。 當輸入為空或模式驗證失敗時,
complete_structured()會拋出ValueError。當信任網關拒絕覆蓋時,會觸發PluginLlmTrustError。其他任何情況(提供商 5xx 錯誤、未配置憑據、超時)都會拋出auxiliary_client.call_llm()所拋出的任何異常。 - 成本。 每次調用都針對用戶付費的提供商運行。不要在不考慮 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 是第一個讓插件能夠out of band(帶外)運行與用戶交談的相同模型的表面,而不涉及上述任何內容。這是它唯一的工作。如果你的插件需要註冊一個由代理調用的工具,請使用 register_tool。如果它需要對生命週期事件做出反應,請使用 register_hook。如果它需要進行自己的模型調用——無論出於何種原因,無論是結構化還是非結構化——使用 ctx.llm。
參考
- 實現:
agent/plugin_llm.py - 測試:
tests/agent/test_plugin_llm.py - 參考插件(配套倉庫):
plugin-llm-example— 帶有圖像輸入的同步結構化提取plugin-llm-async-example— 使用asyncio.gather()的異步示例
- 輔助客戶端(底層引擎):參見 Provider Runtime。