插件 LLM 访问
ctx.llm 是插件发起 LLM 调用的官方支持方式。
支持聊天补全、结构化提取、同步/异步、带/不带图片——接口一致、信任门控一致、凭证由宿主管理。
当插件需要做一些涉及模型、但不属于 Agent 对话本身的事情时,就会用到它。例如: 一个将工具错误重写为非技术人员也能看懂的 hook; 一个在入队前对入站消息进行翻译的网关适配器; 一个对大段粘贴文本进行摘要的斜杠命令; 一个对昨天的活动进行评分并将一行结果写入状态看板的定时任务; 一个判断某条消息是否值得唤醒 Agent 的预过滤器。
这些工作都不应该让 Agent 参与主循环。它们只需要一次 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 格式输出,在 provider 未返回有效 JSON 时本地回退解析,并在安装了 jsonschema 时根据提供的 schema 进行校验,最终在 result.parsed 上返回一个 Python 对象。如果模型未能生成有效的 JSON,则 result.parsed 为 None,result.text 携带原始响应内容。
这个通道为你提供了什么
- 一次调用,四种形态。
complete()用于聊天,complete_structured()用于类型化 JSON,acomplete()和acomplete_structured()用于 asyncio。参数一致,返回对象一致。 - 凭证由宿主管理。 OAuth token、刷新流程、凭证池、按任务的辅助凭证覆盖——Hermes 已有的所有凭证概念都适用。插件永远看不到 token;宿主通过
result.audit对调用进行归属。 - 有边界。 单次同步或异步调用。无流式输出、无工具循环、无需要管理的对话状态。输入即状态,获取结果,返回。
- 默认拒绝的信任机制。 一个你从未配置过的插件,不能自行选择 provider、模型、Agent 或存储的凭证。默认姿态是"使用用户正在使用的配置"。运维人员可以在
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 仓库(参考插件的配套仓库——不随 hermes-agent 本体捆绑分发)。关于异步接口(acomplete() / acomplete_structured() 配合 asyncio.gather()),请参见同一仓库中的 plugin-llm-async-example。
何时使用哪种方法
| 你的需求… | 使用 |
|---|---|
| 自由文本响应(翻译、摘要、改写、生成) | complete() |
| 多轮提示(system + few-shot 示例 + user) | complete() |
| 返回类型化字典,并根据 schema 校验 | complete_structured() |
| 图片或文本输入,返回类型化字典 | complete_structured() |
| 从异步代码中发起同样的调用(网关适配器、异步 hook) | 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": "..."} 字典组成的列表。多轮提示(system + few-shot user/assistant 对 + 最终 user)的工作方式与 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, # 设为 True 可在无 schema 时仍请求 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)
输入是类型化的文本或图片块(原始字节会自动以 data: URL 形式进行 base64 编码)。当提供了 json_schema 或 json_mode=True 时,宿主通过 response_format 请求 JSON 输出,在 provider 未返回有效 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=...)
参数和返回类型与其同步对应方法一致。从网关适配器、异步 hook 或任何已在 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"
# 当提供了 schema_name 时,audit 中也携带此字段
usage 携带 input_tokens、output_tokens、total_tokens、cache_read_tokens、cache_write_tokens,以及当 provider 返回这些字段时的 cost_usd。
信任门控
默认行为是默认拒绝。在没有 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:
# 允许此插件选择不同的 Hermes provider
#(必须是 Hermes 已已知的 provider——与
# `hermes model` 和 config.yaml 中的 model.provider 名称相同)
allow_provider_override: true
# 可选:限制允许的 provider 列表。使用 ["*"] 表示允许任意 provider
allowed_providers:
- openrouter
- anthropic
# 允许此插件请求特定的模型
allow_model_override: true
# 可选:限制允许的模型列表。使用 ["*"] 表示允许任意模型
# 模型字符串会与插件发送的内容进行字面匹配——
# Hermes 不会进行任何查询解析
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku
# 允许跨 Agent 调用(罕见)
allow_agent_id_override: false
# 允许插件请求特定的已存储认证配置文件
#(例如同一 provider 上的不同 OAuth 账号)
allow_profile_override: false
插件 ID 对于扁平插件是清单文件中的 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 密钥、OAuth token 或刷新 token,包括已配置的凭证池。插件永远看不到这些凭证。 - 视觉路由。 当提供了图片输入且用户当前激活的文本模型仅支持文本时,宿主会自动回退到已配置的视觉模型。
- 回退链。 如果用户的主 provider 返回 5xx 或 429,请求会先经过 Hermes 常用的聚合器感知回退机制,然后才向插件返回错误。
- 超时。 尊重你传入的
timeout=参数,回退到auxiliary.<task>.timeout配置或全局辅助默认超时。 - JSON 塑形。 当你请求 JSON 输出时,向 provider 发送
response_format;如果 provider 返回了代码块包裹的响应,则在本地重新解析。 - Schema 校验。 当安装了
jsonschema时,根据你的json_schema进行校验;否则记录一条调试日志并跳过严格校验。 - 审计日志。 每次调用向
agent.log写入一行 INFO 级别日志,包含插件 ID、provider/模型、purpose 和 token 总计。
插件负责的内容
- 请求形态。 聊天使用
messages,结构化使用instructions+input。插件构建提示词;宿主运行它。 - Schema。 你想要返回的任何形态。宿主不会为你推断它。
- 错误处理。
complete_structured()在输入为空或 schema 校验失败时引发ValueError。当信任门控拒绝覆盖请求时引发PluginLlmTrustError。其他任何情况(provider 5xx、未配置凭证、超时)都会引发auxiliary_client.call_llm()所引发的异常。 - 成本。 每次调用都针对用户的付费 provider 运行。不要在对每一条网关消息都调用
complete()的循环中不顾 token 消耗地运行。
在插件接口体系中的位置
已有的 ctx.* 方法扩展了 Hermes 的现有子系统:
| ctx.register_tool | 添加一个 Agent 可以调用的工具 |
| ctx.register_platform | 接入一个新的网关适配器 |
| ctx.register_image_gen_provider | 替换图片生成后端 |
| ctx.register_memory_provider | 替换记忆后端 |
| ctx.register_context_engine | 替换上下文压缩器 |
| ctx.register_hook | 观察生命周期事件 |
ctx.llm 是第一个允许插件在带外(out of band)运行与用户正在对话的同一个模型的接口,且无需依赖上述任何一种注册机制。这就是它的唯一职责。如果你的插件需要注册一个由 Agent 调用的工具,使用 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。