AiProviderAccessor

AiProviderAccessor 是 plugin 调用平台托管 LLM Provider 的对外通道。它让 plugin 发起 chat completion 时,完全不接触 LlmProviderFactory、租户解析、密钥加载、Provider 协议适配——这些 host 负责的事都留在平台边界内,accessor 暴露的是稳定的、副作用清晰的接口。

这是你已经在用的 AI 原生能力背后的 SPI:Aura Bot、ChatBI、Agent workflow、RAG 增强、plugin handler 内的 command 端富化。如果你的 plugin 需要调 LLM,这是契约——不是 Spring 自动注入。

概念

accessor 通过 host-bridge 接口投递给 plugin 代码,与 FileAccessorBackgroundDataAccessor 同一模式。Plugin 不 autowire LlmProviderFactory——它在 command 执行期通过 CommandContext 拿到 host 绑定的实例。host 把 accessor 放进 CommandContext.settings map,约定的 key 是:

String SETTINGS_KEY = "__aiProviderAccessor";

Host 实现(LlmProviderAccessorImpl)按当前租户上下文解析此次调用:哪个 llm_config profile 适用、哪个 Provider 凭据可达、哪个 cloud_config override 生效、哪个限速与 entitlement 守护起作用、上游 payload 按 Provider 怎么塑形。

正是这套切分让 plugin 的 AI 调用在构造上就是 AI 安全的。Plugin 描述意图,平台决定身份、密钥、协议。

接口签名

package com.auraboot.framework.plugin.extension;

public interface AiProviderAccessor {

    String SETTINGS_KEY = "__aiProviderAccessor";

    ChatResponse chat(ChatRequest request) throws Exception;

    record ChatRequest(
            String useCase,
            String providerProfileCode,
            String providerCode,
            String modelName,
            String systemPrompt,
            List<Message> messages,
            int maxTokens,
            Map<String, Object> metadata
    ) {}

    record Message(String role, String content) {
        public static Message user(String content);
        public static Message assistant(String content);
    }

    record ChatResponse(
            String providerCode,
            String modelName,
            String text,
            int inputTokens,
            int outputTokens,
            int totalTokens,
            String rawResponseJson
    ) {}
}

Record 不可变;Message 上的辅助构造覆盖最常见的两类。rawResponseJson 用于诊断和审计——不要用它跑业务逻辑,业务逻辑用 text / inputTokens / outputTokens 等强类型字段。

在 command handler 内取到 accessor

@Extension
public class SummarizeIssueHandler implements CommandHandlerExtension {

    @Override
    public String getCommandType() {
        return "crm:case_summarize";
    }

    @Override
    public Object execute(CommandContext context) throws Exception {
        AiProviderAccessor llm = context.aiProviderAccessor();

        var req = new AiProviderAccessor.ChatRequest(
            "crm.case.summarize",                              // useCase
            null,                                              // providerProfileCode (host 自选)
            null,                                              // providerCode (host 自选)
            null,                                              // modelName (host 自选)
            "用一段话总结客户工单。",                            // systemPrompt
            List.of(AiProviderAccessor.Message.user(
                String.valueOf(context.payload().get("caseDescription")))),
            512,                                               // maxTokens
            Map.of("auditCorrelationId", context.recordId())   // metadata
        );

        AiProviderAccessor.ChatResponse resp = llm.chat(req);
        return Map.of("summary", resp.text());
    }
}

三点要注意:

  • 不 Spring autowire。Plugin 永远看不到 LlmProviderFactoryOpenAiClientAnthropicClient
  • useCase 必填。它控制此次调用归属哪个 llm_config profile、哪个限速桶、哪个审计类别。漏填则平台落到默认 profile,默认 profile 通常更严
  • profile / provider / model 可为 null。留 null 让平台为当前租户选最合适的。只在 plugin 有硬需求时显式设(如合同要求托管型企业模型)

何时设 providerProfileCode / providerCode / modelName

字段设它的场景留 null 的场景
providerProfileCodePlugin 约定特定 llm_config profile(如隐私受限的租户自有密钥集)走 host 策略 + 成本优化
providerCodeUse-case 因合同原因锁定具体 Provider走 failover 与 Provider 轮转
modelName需要具体模型版本以保可重现性或能力走 host 为该 profile 推荐的模型

大多数 plugin 三项全留 null。设 useCase 让平台来选。

错误语义

chat(...) 抛 checked Exception,因为 LLM 调用跨网络、跨密钥、跨 Provider 边界——失败必须对调用方可见。接口只声明一个通用 throws Exception,不暴露细分的异常类型;具体失败原因由 host 端在异常 message / cause 里携带。常见失败语义:

  • Entitlement 缺失 — 当前租户对此 useCase 没有 entitlement(如未购买相应的 Aura Bot 套餐)。Fail command 透传到用户,不要静默重试
  • Provider 不可用 — 上游 Provider 返回 5xx / 超时。再抛出会中止 command 管线
  • Provider profile 无效providerProfileCode 在该租户未注册。改配置,不要绕过
  • 限速触发 — 租户或 use-case 桶已耗尽。透传给用户;不要在 plugin 端做重试循环(限速由平台负责)

只 catch 你能恢复的。把 Exception 包成通用错误是反模式——它把 entitlement 与 config 漂移从平台监控面隐藏了。

审计与关联

每次调用都写到平台 LLM 审计轨迹,索引 key 为 useCaseproviderCodemodelNameinputTokensoutputTokens,以及 metadata 里你自带的关联字段(如 auditCorrelationId)。把当前记录主键 context.recordId() 放进 metadata,让 LLM 调用与触发它的业务记录在审计里能 join 起来——这就是审计 join 的设计意图。

敏感 prompt 内容按平台脱敏策略在审计存储里掩码。Plugin 不要重复做——不要从自己的 handler 里把完整 prompt 写日志。

常见反模式

模式为什么坏
直接 autowire LlmProviderFactory绕过租户 + entitlement + 审计;过不了平台代码评审
硬编码 providerCode = "openai"Plugin 用不上客户自有模型部署
包成 RuntimeException("LLM failed")丢了 entitlement 与 Provider 错误的区分
紧循环里 chat(...) 不 backoff触发平台限速;accessor 的 host 端 backoff 不是无限的
rawResponseJson 给下游解析Provider 响应结构会变,用强类型字段

与其他 SPI 的关系

  • CommandHandlerExtension — 接入 Command 管线的方式;通常在这里调 chat()
  • FileAccessor — host-bridge 同款模式,用于文件字节
  • BackgroundAccessor — host-bridge 同款模式,用于后台任务系统字段
  • Agent 就绪command 表面上的 agentHint / riskLevel / idempotent 与 command 内部的 LLM 调用如何呼应

企业版能力 — 多租户 LLM 预算上限、prompt-provenance 签名、跨租户 Agent Control Plane 编排、Observability Pro 的按调用延迟看板,均为商业版能力。社区版 AiProviderAccessor 接口在所有版本一致;围绕它的运维表面不同。

下一步