本地语义路由
离线和标准模式使用 modeScores.functions 阈值,扩展模式使用较高的 modeScores.freeChat 阈值;扩展先尝试本地与 AI 路由再聊天,自由模式跳过路由。策略由 ConversationStrategy / ConversationMode 决定,完整流程见 执行模式。
当前使用纯 Java 的领域词表、稀疏 TF-IDF 匹配和玩家独立的精确意图缓存。默认分发包不包含 ONNX Runtime、DJL、Caffeine、native 库、外部词典或训练模型,不下载运行依赖。路由实现、声明、结果和诊断统一位于 com.iafenvoy.system.agent.routing。
目标是兼顾可分发性与模型调用次数。模型服务默认 DeepSeek,也可配置其他 OpenAI 兼容服务,详见 模型接口。完整系统 JAR 无需携带推理运行时,jar 打包任务强制检查整个产物不超过 10,000,000 字节。这是系统自身及其内嵌资源/库的上限,Minecraft、NeoForge 和原有 UI 模组等安装依赖另计;没有将路由运行库移到外部下载以规避体积。
如何减少调用
结构化按钮 / 当前编号
→ 明确指令和短语
→ 玩家自己的精确意图缓存
→ 同义词归一化 + 稀疏 TF-IDF 匹配
→ 低把握时交给模型服务判断一次
→ 模块按自己的预算执行
结构化请求与明确指令优先。本地匹配成功只省略“选择哪个模块”的远程请求;模块仍读取实时数据。签到仍由模型现场选择奖励。领奖、付款与其他写能力不通过本地匹配或意图缓存直达。
API Key 不作为发送消息的前置条件。缺少模型密钥或玩家关闭外部 AI 时,仍先执行指令、意图缓存和高置信度本地匹配;只有需要模型兜底而无法调用时,才返回“指令待确认”的选项卡片。卡片优先列出本次向量候选中已注册、玩家可用的只读操作,不能独立解析参数的候选不生成操作;没有合适候选时展示可用的只读快捷入口,并提示使用 /help。点击按钮通过既有 CardActionRequest 作为新的明确请求执行,不再让 AI 重做路由,也不会自动选择低分候选。
候选的文案默认使用 ModuleRouting.shortcut 标签;没有无参数快捷入口的查询可在 RoutingProfile.read(...).choice("翻译键") 声明选项标签,复用同一个参数提取器。物品查询、物品来源、说明书和帮助已声明标签;说明书同时提供按钮参数解码。提示模式 hints、未解析参数和未启用能力不会因此获得本地直达权限;点击后照常检查卡片版本与模块权限。签到等工作流自身必需的模型调用仍要求配置模型。
例如“商店/商城/商铺”和英语“shop/store/market”在各自语言词表中归一化到相同领域特征。中文未知字符、英文未知词也保留,加入相邻特征并计算 TF-IDF;陌生内容不会被全部丢弃后只凭一个熟悉关键词命中。物品注册 ID 在匹配特征中占位,但实际参数必须从本次原文重新解析出唯一 ID,不从例句复制。
相似度按每模块最相似例句聚合,比较第一名 s1 和第二个不同模块的 s2。本地直达需要同时满足:
- 模块明确注册
direct=true,允许交互、writeSteps=0,且声明的全部工具只读。 - 分数、不同模块分差、输入特征覆盖率达到阈值,输入涉及的主题/未知词也被候选例句覆盖。
- 本次参数能独立解析,且没有触发否定、条件、指代、复合请求等保守过滤。
- 已识别的执行/领取/支付等写动作词不能走本地直达;事件规则仍直接指定模块,跳过聊天路由。
相似度与覆盖率都不是正确率或概率。这套实现擅长已覆盖的领域表达与同义改写,复杂语义泛化弱于神经网络;不熟悉的表达让 AI 判断。默认 0.9 分数、0.15 分差、0.9 覆盖率是保守的工程起点,没有标注集验收,不承诺具体命中率或节省比例。执行层还会拒绝来自本地匹配/意图缓存的写工具,商城确认也检查显式路由来源。
内置直达范围为帮助、本人概况、收件箱、商城浏览、物品 ID 查询,以及安装 FTB 后的任务查询。无参数查询默认打开概况/列表;搜索、筛选和分页使用明确指令或当前选项。FTB 默认查询只列出进行中任务简表;“查看 1”“查看 任务名”与 /view 由指令层直达详情模块,不依赖模型或对话历史。物品别名无法独立解析时交给模型。只读分派仍可能理解有误,阈值不能替代实际语料验收。
通知按钮向量
LexicalIndex<T> 提供共享的 TF-IDF 编码、预存例句向量与余弦检索;服务端 SemanticIndex 保留原模块配置和只读保护,客户端 NotificationVoiceCommands 则索引当前通知的固定按钮文案及资源包例句。按钮动作不限定为确认、详情或打开;新的本地化键自动按其文案匹配,可在 assets/system/routing/notification-buttons.json 按键、语种补充说法,资源重载后重建缓存。无模型文件、额外依赖或 embedding API 调用。
通知先匹配完整文案及例句,再比较不同按钮文案的向量分数;要求相似度至少 0.9、覆盖率至少 0.9、领先第二名至少 0.15,未知主题或否定、条件等复杂表达回到普通聊天路由。按钮采用保留原动词的字词特征,不复用服务端把领取、购买等统一为 write 的领域别名,以免不同写动作被误认为同一个操作。低分不会自动确认通知,重复文案对应多个动作时同样回到主聊天匹配。
向量只保存按钮模板,不保存玩家录音或查询原文;相同按钮集合与语言复用内存索引。命中只能执行当前可见通知已携带的可信动作,仍受忙碌状态、原通知/连接生命周期以及服务端业务授权约束。该路径相当于明确操作当前按钮,不放开服务端本地路由对任意写请求的限制。
配置与升级
首次生成的 config/system/system-semantic.json 默认 mode=ENFORCE,直接使用内置小词表和例句,不需要额外部署步骤。完整示例见 system-semantic.json。模式、匹配阈值、文本限制、缓存容量和新条目 TTL 从配置实例直接读取,资源重载后使用当前值;索引仍只在启动时建立,修改例句、索引语言或从启动时的 OFF 开启本地索引需要正常重启。
{
"mode": "ENFORCE",
"maxCharacters": 240,
"minCoverage": 0.9,
"defaultThreshold": {"score": 0.9, "margin": 0.15},
"languages": ["zh", "en"],
"lexicalThresholds": {},
"examples": {},
"cacheEntries": 1024,
"cacheSeconds": 300,
"entriesPerPlayer": 32
}
上述是独立的 system-semantic.json 文件。模块/语言的覆盖项可用 lexicalThresholds["system:shop@zh"]={"score":0.95,"margin":0.2};语言级键优先于模块级键,未覆盖时使用 defaultThreshold。分数/分差范围 0..1,覆盖率范围 0.5..1。降低门槛会增加误路由,先用实际表达评估。
| 模式 | 行为 |
|---|---|
ENFORCE | 高把握的只读请求直达,其余单次模型回退;成功校验后的部分 LLM 只读路由可缓存 |
SHADOW | 计算候选和计数,既不直达,也不缓存或向模型提供候选;用于观察 |
OFF | 跳过本地索引与缓存,保留明确指令和模型路由 |
路由配置只读取当前 lexical-tfidf-v1 定义,不提供 ONNX 配置迁移,也不会下载或使用旧模型权重。
语言仍从 Minecraft 已同步的客户端设置读取;模型上下文统一注入可信 requestContext.playerLanguage,不增加客户端语言发包。默认仅配置中英词表,其他语言或不匹配文字回退。不把相似分数用作工具授权。
精确意图缓存与隔离
缓存只记录“这段完整规范化输入已被成功判定为某个只读模块”。只有 LLM 返回的模块完成执行,且该 profile 允许本地直达、当前参数能由解析器独立提取并与 LLM 参数一致、没有陌生主题缺口时才记入。不会把 AI 的所有判断自动变成公共训练样本,也不缓存错误、写操作、领取/报价引用或动态业务回答。
命中时仍从当前文本重新解析参数,再次校验只读模块,执行工具查询最新状态。键包含玩家独立作用域、运行代次、记忆代次、语言与规范化文本 SHA-256,内存中不保留原文。两个玩家不共享意图缓存,实例只绑定本次启动的模块/词表/策略。
使用有界 JDK LinkedHashMap,默认全局最多 1024 项、每玩家最多 32 项、TTL 300 秒,按访问顺序淘汰。无磁盘缓存或额外依赖。清空记忆、修改偏好、撤销、掉线和关闭服务端会清理对应作用域;迟到写入检查作用域和玩家代次,关闭历史保存不读写意图缓存。本地词表和公共例句不含玩家信息,可以继续使用。
关闭外部 AI 后,明确入口和本地只读直达仍可用;需要模型的回退及 AI 签到会被原有偏好检查拒绝。没有密钥同样不影响本地处理。
扩展与诊断
扩展在模块的 routing() 中声明 ModuleRouting<I>,然后在 AssistantRegistrationEvent 只注册一次模块。指令、语义 profile、模型目录和只读快捷入口由 ModuleRegistry 统一建立,不再有独立的内置 profile 安装列表。一个模块一个 profile,可通过 system-semantic.json 的 examples 追加例句;模块、语言和例句数量不设额外硬上限。额外例句只对已注册 profile 生效;可选 FTB 模块不存在时不生成其可执行 profile。匹配文本长度、缓存容量、每玩家容量和 TTL 由配置决定,配置解析只要求这些数值为正数;相似度、分差和覆盖率在 0..1 范围内。
例如在 system-semantic.json 的 examples 中增加 {"system:shop":{"zh":["逛逛系统商铺"]}}。不必为了补一句常见表达更换推理引擎或修改路由业务分支;新模块也可以用自己的原词例句匹配。词表只是少量领域别名,额外例句可使用未出现在词表中的词,未知词保留在特征中。
@Override
public ModuleRouting<EmptyInput> routing() {
return ModuleRouting.<EmptyInput>defaults()
.command(AgentCommand.literal(EmptyInput.INSTANCE, "extension", "扩展状态"))
.semantic(RoutingProfile.read(text -> EmptyInput.INSTANCE,
List.of("查看扩展状态"), List.of("Show extension status")))
.shortcut("example.action.status", EmptyInput.INSTANCE);
}
此例只适合没有参数的概况查询。业务参数解析器必须校验当前请求;缺参数或有歧义就返回 null,不能从历史补参数。受信 Java 扩展不得在工具之外隐藏副作用。所有注册在 engine.start() 前完成,启动后冻结。RoutingProfile.hints(...) 只贡献候选样例,不能直接执行;explicitOnly() 仍向模型公开能力并附上 explicitOnly: true,要求模型非常确信才选择,同时禁止语义匹配和意图缓存直达。structuredOnly() 从模型目录和返回值解码中排除该模块,适合确认付款。拒绝样例通过 engine.modules().reject(identifier, RoutingProfile.hints(...)) 注册,参与不同意图的分差计算,但没有可调用模块,也不会作为模型候选发送。
/system routing 需要管理员权限,控制台也可使用。输出算法 lexical-tfidf-v1、模式、状态、例句数、缓存条数、各分派/回退原因计数,以及最近一次匹配的模块/语言/分数/分差/覆盖率。没有玩家原文或 UUID;最近匹配为全服共享快照。DIRECT、CACHE_HIT 是省去路由请求的本地路径计数,SHADOW_DIRECT 是候选数量,均不能证明判断正确;本地耗时不含模型和后续模块执行。
本轮只编译和静态检查,没有启动游戏、调用模型服务、训练分类器或执行准确率/性能测试。需用独立标注语料和实际服务器测量覆盖率、误路由及调用次数,再决定是否引入可选的小型监督分类器。