解析

引擎系统

上下文、工具调用与唤醒三族引擎的分工、组合与扩展

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 记录一条错误并停在等待状态,服务可用后继续装配。

内置变体

族变体行为
contextstandard线性历史,超预算后把旧段摘要成 ishiki.compact 条目
contextv3<world_state> 视窗,注入 memory/ 记忆块
wakeupstandard私聊、@、引用与关键词四条规则
wakeupwillingness响应意愿累计、衰减与概率开口
wakeupjev调用轻量模型判断是否值得接话
toolcallnative原生 function call
toolcallv3、hermes、qwen3coder、morph-xml、yaml-xml文本工具协议的解析与生成
toolcallnarrativeNarrator 推进剧情,按意图分支到 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 参考。

继续阅读

On this page