引擎系统
上下文、工具调用与唤醒三族引擎的分工、组合与扩展
Ishiki 的模型侧行为分成三族可替换的引擎。profile 的 context、toolcall、wakeup 三段各选一个变体,同一个 runtime 在每一族里只使用选中的那个实例。
上下文:组装模型看到的 messages
上下文引擎读取 runtime 的事实流,把每次请求要交给模型的内容整理成 messages。事实条目留在 events.jsonl 里,引擎给出的是读取时的投影:投影可以在每次请求前重新计算,也可以把结果作为新条目追加回去。
ContextEngineInstance 的方法都是可选的:
prepareEntries(entries, request?):在请求前整理条目,返回本次请求使用的序列。renderMessages(messages, request?):把条目对应的消息渲染成模型输入,可以合并、改写或插入消息。instructions():追加本引擎需要的系统提示词。attach(agent):订阅轮次事件,返回释放函数。
request 携带 turnId 与 signal。内置 standard 保留线性历史,估算超出 maxTokens 时把较旧的一段摘要成新的 ishiki.compact 条目,下一次请求从摘要之后继续;v3 把窗口渲染成 <world_state>,并按配置注入 memory/ 下的记忆块。standard 的压缩以追加条目表达,v3 的视窗在投影里完成,原始消息始终可以回放。
工具调用:包装 LanguageModelV4
工具调用引擎接收 profile 的模型,交回一个实现同一接口的 LanguageModelV4。Agent 只通过这个接口生成内容;工具的执行仍然由宿主完成,引擎改变的是模型如何表达工具调用、以及一次调用内部经过哪些阶段。
middleware()返回 AI SDK 的LanguageModelV4Middleware,基类的wrap(model)用它包装模型。- 覆盖
wrap(model)可以返回自定义实现,把一次doGenerate/doStream展开成多阶段、多模型的编排。
native 不提供 middleware,模型直接输出原生 function call。v3、hermes、qwen3coder、morph-xml、yaml-xml 在 middleware 里完成文本协议与标准工具调用之间的转换。narrative 覆盖 wrap:Narrator 推进剧情并选择走向,回话交给 Replyer,行动交给 Action Decoder,每一步只进入其中一个分支,最后由包装器组装成标准的 reasoning 与工具调用。
三族引擎实例都按 runtime 创建。工具调用引擎可以读取当前 runtime 的 ExtensionHost,并在包装层承载该 runtime 的编排状态;底层模型入口仍由 profile 共享。
唤醒:决定何时运行
唤醒引擎接收 runtime 的 Agent,自己决定什么时候开始下一轮。实例只有一个必选方法:attach(agent: Agent): () => void。
attach 在 runtime 初始化时调用一次,返回的函数在 runtime 停止时执行。引擎可以订阅已提交的输入,也可以自己计时,或者把两者组合起来。接口没有输入前提:定时器到点后调用 agent.continue(),Agent 就会基于已有历史开始一轮;传入文本时,这段文本作为该轮开场的 user 消息写入事实流。
订阅回调会被 core 等待,耗时的判断要另起任务并自行捕获错误。开启轮次前检查 agent.isIdle();异步判断返回后再检查一次。忙碌时等待、跳过还是排队,由引擎按自己的策略决定。
内置的 standard、willingness、jev 都在消息提交后判定,是三种具体的消息判定策略:私聊、@、引用与关键词命中;响应意愿累计后按概率开口;调用轻量模型判断是否值得接话。接口本身不限定触发来源。模型声明的 wait 条件与扩展或宿主调用的 runtime.wake() 是独立触发路径,直接调用 core 的 continue,不经过唤醒引擎。
组合:同族互斥,跨族独立
- 每一族选一个变体:同一族里没有叠加或链式组合,profile 里写出的那个变体就是该 runtime 在该族使用的实现。
- 三族分别选择:
wakeup.engine: willingness可以搭配context.engine: standard和toolcall.engine: native,每一族的参数只作用于自己。 narrative的两个变体由独立插件koishi-plugin-ishiki-narrative提供,并按约定配套:叙事上下文在输入前放入带标记的消息,叙事工具调用读取该标记来划分历史与本轮证据。只启用toolcall.engine: narrative时,包装器找不到标记并在该轮报错;只启用context.engine: narrative可以运行,但模型拿不到叙事约定,流程不会按叙事方式推进。- 唤醒引擎可以独立选择,根据自己的策略安排 Agent 的运行时机。
发现与装配
引擎是 Koishi Service,服务名由基类的 GetName 生成,形如 ishiki.engine.context.standard、ishiki.engine.wakeup.jev。主插件在构造时注册内置变体;第三方变体作为普通 Koishi 插件安装,并由它自己的 ctx.plugin 注册。
profile 激活时,内核按三段配置里的名字取服务并调用 [Service.invoke]。三族引擎都先接收该 runtime 的 ExtensionHost,再接收变体配置;Context、Wakeup 与 Toolcall 实例都按 runtime 创建。缺少服务时,该 profile 记录一条错误并停在等待状态,服务可用后继续装配。
内置变体
| 族 | 变体 | 行为 |
|---|---|---|
| context | standard | 线性历史,超预算后把旧段摘要成 ishiki.compact 条目 |
| context | v3 | <world_state> 视窗,注入 memory/ 记忆块 |
| wakeup | standard | 私聊、@、引用与关键词四条规则 |
| wakeup | willingness | 响应意愿累计、衰减与概率开口 |
| wakeup | jev | 调用轻量模型判断是否值得接话 |
| toolcall | native | 原生 function call |
| toolcall | v3、hermes、qwen3coder、morph-xml、yaml-xml | 文本工具协议的解析与生成 |
| toolcall | narrative | Narrator 推进剧情,按意图分支到 Action Decoder 或 Replyer |
三族的 v3 变体可以分别选择,例如 v3 上下文可以搭配 native 工具调用。
写一个自主调度的唤醒引擎
这个引擎按固定间隔检查 Agent 的运行状态,空闲时开启下一轮。实例创建后,调度便可以独立于外部输入持续进行。
import { Schema, Service } from "koishi";
import type { Context } from "koishi";
import { WakeupEngine } from "koishi-plugin-ishiki";
import type { EngineConfig, ExtensionHost, WakeupEngineInstance, WakeupEngines } from "koishi-plugin-ishiki";
interface RhythmConfig {
everySeconds: number;
}
const RhythmConfig: Schema<RhythmConfig> = Schema.object({
everySeconds: Schema.number().min(1),
});
declare module "koishi-plugin-ishiki" {
interface WakeupEngines {
rhythm: RhythmConfig;
}
}
export class RhythmWakeupEngine extends WakeupEngine<"rhythm"> {
public constructor(ctx: Context) {
super(ctx, "rhythm");
}
public [Service.invoke](_host: ExtensionHost, config: EngineConfig<Pick<WakeupEngines, "rhythm">>): WakeupEngineInstance {
const settings = RhythmConfig({ everySeconds: config.rhythm?.everySeconds ?? 60 });
return {
attach(agent) {
const timer = setInterval(() => {
if (agent.isIdle()) agent.continue();
}, settings.everySeconds * 1000);
timer.unref();
return () => clearInterval(timer);
},
};
}
}
export default RhythmWakeupEngine;将该模块作为 Koishi 插件加载,再在 profile 中选择:
wakeup:
engine: rhythm
rhythm:
everySeconds: 60几点约定:
attach返回的函数在 runtime 停止时执行。清除定时器、取消订阅、放弃未完成的判断都在这里完成。- runtime 按需创建;当前平台接入会在首次输入路由到该频道时创建实例。计时器从
attach开始,随该 runtime 的停止结束。 - 每次触发前检查
agent.isIdle()。忙碌时跳过当前检查,在下一个间隔继续。 agent.continue()不带参数时,这一轮基于已有历史开始,不写占位消息。希望这一轮带上说明时,把说明写进instructions引用的文件或扩展的extendInstructions;模型的系统提示词每轮都会读到。也可以调用agent.continue("..."),这段文本会作为该轮开场的一条 user 消息写入事实流。- 一个 runtime 在唤醒族只有一个实例,与其他唤醒变体互斥。需要同时响应输入和定时任务时,在同一个实例里组合订阅与计时器。
上下文与工具调用引擎同样继承各自的基类并实现 [Service.invoke],接口细节见扩展 API 参考。