插件 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。