跳到主要内容

对话拦截器 ChatInterceptor

概述

ChatInterceptor 用于在 Chat 请求执行前后插入自定义逻辑,适合处理多个模型调用都需要的通用能力,例如:

  • 调整 Prompt、模型参数或请求头
  • 注入用户、租户和认证信息
  • 记录日志、耗时和审计数据
  • 根据当前请求动态启用某项能力
  • 缓存命中、权限拒绝等请求短路
  • 处理同步响应或监听流式响应

拦截器采用责任链模式。调用 chain.proceed(...) 会继续执行后续拦截器,并最终请求大模型;同步调用还可以在 proceed(...) 返回后处理响应。

text
Interceptor A before
  Interceptor B before
    ChatModel
  Interceptor B after
Interceptor A after

同步和流式请求分别对应两个入口:

请求方式拦截方法结果处理方式
chat(...)intercept(...)返回 AiMessageResponse
chatStream(...)interceptStream(...)包装 StreamResponseListener

适用场景

  • 统一请求策略:为一组模型调用追加系统约束、限制输出长度或调整采样参数。
  • 多租户与动态认证:根据 accountId、租户或业务套餐写入 Header 和路由参数。
  • 审计与监控:统计耗时、记录调用结果,并把业务标识传给可观测系统。
  • 条件能力:只对某类账号、模型或请求启用缓存、内容审核等逻辑。
  • 响应后处理:同步请求返回后统一清洗结果,或包装流式 Listener 监听结束与失败。

如果只是某一次调用需要设置 temperaturemodel 等参数,直接使用 ChatOptions 更简单;当同一逻辑需要覆盖多个调用点时,再使用拦截器。

快速开始

下面用一个最小示例,在请求模型前统一调整 temperature,并记录同步调用耗时。

1. 创建拦截器

java
public class TimingChatInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        long startNanos = System.nanoTime();
        context.getOptions().setTemperature(0.2f);

        try {
            return chain.proceed(chatModel, context);
        } finally {
            long elapsedNanos = System.nanoTime() - startNanos;
            System.out.println("Chat elapsed: " + elapsedNanos + " ns");
        }
    }
}

2. 注册拦截器

java
OpenAIChatModel chatModel = new OpenAIChatModel(config);
chatModel.addInterceptor(new TimingChatInterceptor());

3. 发起请求

java
String result = chatModel.chat("介绍一下 Agents-Flex");

调用 chat(...) 时,TimingChatInterceptor 会先调整参数,再继续执行模型请求。try/finally 可以确保请求成功或抛出异常时都记录耗时。

WARNING

如果拦截器没有调用 chain.proceed(...),后续拦截器和模型请求都不会执行。只有在缓存命中、权限拒绝等需要主动中断的场景中,才应跳过该调用。

完整 Demo

下面以多租户客服为例:

  • ChatContext 读取租户信息
  • 为请求添加租户 Header
  • 为高级套餐调整模型参数
  • 仅在匹配当前套餐时执行策略拦截器

定义租户拦截器

java
public class TenantHeaderInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        String tenantId = (String) context.getAttribute("tenantId");
        context.getRequestSpec().addHeader("X-Tenant-Id", tenantId);
        return chain.proceed(chatModel, context);
    }
}

定义高级套餐策略

java
public class PremiumModelInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        context.getOptions().setModel("qwen-plus");
        context.getOptions().setTemperature(0.1f);
        context.getOptions().setMaxTokens(1200);
        return chain.proceed(chatModel, context);
    }
}

注册并调用

java
OpenAIChatModel chatModel = new OpenAIChatModel(config);

chatModel.addInterceptor(new TenantHeaderInterceptor());

chatModel.addInterceptorRegistration(
    ChatInterceptorRegistration.builder(
            "premium-model-policy",
            new PremiumModelInterceptor()
        )
        .matcher(context ->
            "premium".equals(context.getAttribute("plan"))
        )
        .order(ChatInterceptorOrders.DEFAULT)
        .build()
);

Map<String, Object> attributes = new HashMap<>();
attributes.put("tenantId", "tenant-01");
attributes.put("plan", "premium");

ChatOptions options = ChatOptions.builder()
    .contextAccountId("user-1001")
    .contextAttributes(attributes)
    .build();

String result = chatModel.chat("帮我查询订单状态", options);

普通 ChatInterceptor 默认对所有请求生效。PremiumModelInterceptor 使用 Registration 注册,只有 planpremium 时才会进入责任链。

典型场景

修改 Prompt 和 ChatOptions

拦截器可以修改当前请求的 Prompt 和模型参数:

java
public class RequestRewriteInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        ChatOptions options = context.getOptions();
        options.setModel("qwen-plus");
        options.setTemperature(0.1f);
        options.setMaxTokens(800);

        Prompt rewritten = rewritePrompt(context.getPrompt());
        context.setPrompt(rewritten);

        return chain.proceed(chatModel, context);
    }
}

适合用于统一系统提示词、模型路由、输出长度限制和内容预处理。

动态认证

认证信息通常与当前账号或租户相关,可以在请求发出前动态写入 Header:

java
public class AuthHeaderInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        String token = tokenService.load(context.getAccountId());
        context.getRequestSpec().addHeader(
            "Authorization",
            "Bearer " + token
        );
        return chain.proceed(chatModel, context);
    }
}

如果同步和流式请求都需要认证,应同时实现 intercept(...)interceptStream(...),或者将添加 Header 的逻辑提取为两个入口共用的方法。

按条件启用拦截器

ChatInterceptorRegistration 可以根据完整的 ChatContext 决定当前请求是否启用拦截器:

java
ChatInterceptorRegistration registration =
    ChatInterceptorRegistration.builder(
            "admin-model-policy",
            new AdminModelPolicyInterceptor()
        )
        .matcher(context -> {
            String model = context.getOptions().getModelOrDefault(
                context.getConfig().getModel()
            );

            return "admin".equals(context.getAttribute("role"))
                && model.startsWith("qwen");
        })
        .build();

chatModel.addInterceptorRegistration(registration);

Matcher 可以读取 Prompt、模型、账号、会话、业务属性和流式状态:

java
.matcher(context ->
    context.getAccountId() != null
    && context.isStreaming()
        && containsSearchIntent(context.getPrompt())
)

Matcher 应只负责判断,避免在其中修改 ChatContext。需要补充上下文时,应使用一个执行顺序更早的拦截器。

缓存命中或主动短路

同步拦截器可以直接返回结果,不再请求模型:

java
public class CacheChatInterceptor implements ChatInterceptor {

    @Override
    public AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        AiMessageResponse cached = cache.get(context.getPrompt());
        if (cached != null) {
            return cached;
        }

        AiMessageResponse response = chain.proceed(chatModel, context);
        cache.put(context.getPrompt(), response);
        return response;
    }
}

权限拒绝、内容检查失败等场景也可以不调用 proceed(...),直接返回业务结果或抛出异常。

修改同步响应

调用 proceed(...) 后,可以基于模型响应创建新的 AiMessageResponse

java
AiMessageResponse response = chain.proceed(chatModel, context);
AiMessage filtered = filter(response.getMessage());

return new AiMessageResponse(
    response.getContext(),
    response.getRawText(),
    filtered
);

适合进行敏感信息脱敏、统一格式化或输出内容检查。

处理流式响应

流式内容通过回调异步到达。统计完整流式耗时或处理最终状态时,应包装 StreamResponseListener

java
public class StreamTimingInterceptor implements ChatInterceptor {

    @Override
    public void interceptStream(
        BaseChatModel<?> chatModel,
        ChatContext context,
        StreamResponseListener listener,
        StreamChain chain
    ) {
        long startNanos = System.nanoTime();

        StreamResponseListener wrapped = new StreamResponseListener() {
            @Override
            public void onOpen(StreamContext streamContext) {
                listener.onOpen(streamContext);
            }

            @Override
            public void onMessage(
                StreamContext streamContext,
                AiMessageResponse response
            ) {
                listener.onMessage(streamContext, response);
            }

            @Override
            public void onError(
                StreamContext streamContext,
                Throwable throwable
            ) {
                try {
                    listener.onError(streamContext, throwable);
                } finally {
                    recordFailure(throwable);
                }
            }

            @Override
            public void onClose(StreamContext streamContext) {
                try {
                    listener.onClose(streamContext);
                } finally {
                    recordLatency(System.nanoTime() - startNanos);
                }
            }
        };

        chain.proceed(chatModel, context, wrapped);
    }
}

包装 Listener 时应完整代理 onOpenonMessageonCloseonError,避免上层无法正确结束请求或释放资源。

GlobalChatInterceptors

GlobalChatInterceptors 用于注册应用级 Chat 拦截器。它适合所有 ChatModel 都需要执行的公共逻辑,例如:

  • 统一添加认证、租户和 Trace Header
  • 审计模型请求和响应
  • 执行内容安全检查
  • 采集应用级指标
  • 根据账号或业务属性应用统一策略

全局拦截器应在应用初始化阶段、创建任何 ChatModel 之前完成注册。

注册普通全局拦截器

java
GlobalChatInterceptors.addInterceptor(
    new AuthHeaderInterceptor()
);

OpenAIChatModel chatModel = new OpenAIChatModel(config);

通过 addInterceptor(...) 注册的拦截器会被包装为始终匹配、orderChatInterceptorOrders.DEFAULT 的 Registration。

也可以按列表顺序批量注册:

java
GlobalChatInterceptors.addInterceptors(Arrays.asList(
    new AuthHeaderInterceptor(),
    new AuditChatInterceptor(),
    new TimingChatInterceptor()
));

当多个拦截器具有相同 order 时,保持注册顺序执行。每次调用都会追加新的拦截器,框架不会按类型或名称自动去重,因此应避免重复执行初始化逻辑。

注册带条件和顺序的全局拦截器

需要按请求激活或指定执行顺序时,注册 ChatInterceptorRegistration

java
ChatInterceptorRegistration premiumAudit =
    ChatInterceptorRegistration.builder(
            "premium-audit",
            new PremiumAuditInterceptor()
        )
        .matcher(context ->
            "premium".equals(context.getAttribute("plan"))
        )
        .order(-100)
        .build();

GlobalChatInterceptors.addRegistration(premiumAudit);

批量注册可以使用:

java
GlobalChatInterceptors.addRegistrations(Arrays.asList(
    tenantResolverRegistration,
    premiumAuditRegistration
));

全局、实例和框架 Registration 最终使用同一套 order 规则排序。“全局”表示作用范围,不代表它一定在实例级拦截器之前执行;当 order 不同时,以 order 为准。

生效时机

创建 BaseChatModel 时,框架会复制当时的全局 Registration:

text
注册 Global A

创建 ChatModel 1  → 包含 Global A

注册 Global B

创建 ChatModel 2  → 包含 Global A、Global B

因此:

  • ChatModel 1 不会自动获得之后注册的 Global B
  • 需要修改已有模型时,应调用该模型的 addInterceptor(...)addInterceptorRegistration(...)
  • GlobalChatInterceptors.clear() 只清空全局注册表,不会移除已有模型中的快照。

这种快照机制让模型实例的拦截器链在创建后保持稳定,也意味着不应把 GlobalChatInterceptors 当作运行时动态开关。

查询全局注册

java
int count = GlobalChatInterceptors.size();

List<ChatInterceptor> interceptors =
    GlobalChatInterceptors.getInterceptors();

List<ChatInterceptorRegistration> registrations =
    GlobalChatInterceptors.getRegistrations();
方法说明
addInterceptor(...)添加一个始终生效的普通拦截器
addInterceptors(...)批量添加普通拦截器
addRegistration(...)添加带 Matcher 和 order 的 Registration
addRegistrations(...)批量添加 Registration
getInterceptors()返回当前普通拦截器视图的不可变快照
getRegistrations()返回包含 Matcher、name 和 order 的不可变快照
size()返回当前全局 Registration 数量
clear()清空全局注册表,主要用于测试

getInterceptors()getRegistrations() 只包含应用通过 GlobalChatInterceptors 添加的内容,不包含 FrameworkChatInterceptors 管理的 Observability,也不包含 Prompt 级 Provider(包括 ToolGroup)。

测试清理

全局注册表是进程级静态状态。测试用例应在执行后清理,避免影响其他测试:

java
public class ChatInterceptorTest {

    @After
    public void clearGlobalInterceptors() {
        GlobalChatInterceptors.clear();
    }
}

生产环境中不建议在请求处理期间调用 clear() 或追加注册。虽然管理方法是线程安全的,但已经创建的 ChatModel 使用各自的快照,运行时修改会使不同模型实例具有不同配置。

进阶使用

ChatInterceptor 接口

同步和流式方法都有默认透传实现,只需要覆盖实际使用的入口:

java
public interface ChatInterceptor {

    default AiMessageResponse intercept(
        BaseChatModel<?> chatModel,
        ChatContext context,
        SyncChain chain
    ) {
        return chain.proceed(chatModel, context);
    }

    default void interceptStream(
        BaseChatModel<?> chatModel,
        ChatContext context,
        StreamResponseListener listener,
        StreamChain chain
    ) {
        chain.proceed(chatModel, context, listener);
    }
}
参数说明
chatModel当前 BaseChatModel,可访问模型配置和客户端
context当前请求的上下文,可以读取或修改请求信息
chain责任链的下一个节点
listener流式响应监听器,仅流式入口具有此参数

ChatContext

拦截器通过 ChatContext 获取当前请求及业务上下文:

入口用途
getPrompt() / setPrompt()读取或替换 Prompt
getOptions() / setOptions()调整 model、temperature、maxTokens、thinking 等选项
getConfig() / setConfig()读取或替换本次请求使用的模型配置
getRequestSpec()调整 URL、Header 和重试配置
getAccountId()当前账号 ID
getConversationId()当前会话 ID
getBotId()当前业务 Bot ID
getTurnId()当前轮次 ID
getAttributes()在拦截器之间共享请求级业务数据

attributes 的生命周期仅限当前请求。不要缓存 ChatContext,也不要把请求级状态保存在拦截器实例字段中。

ChatInterceptorRegistration

普通 ChatInterceptor 会被包装为始终匹配、order0 的 Registration。需要条件激活或调整顺序时,可以显式创建 Registration。

属性作用
nameRegistration 的稳定名称,用于识别和诊断
interceptorMatcher 命中后执行的拦截器
matcher根据当前 ChatContext 判断是否执行
order责任链顺序,数值越小越早进入
java
ChatInterceptorRegistration.builder(
        "premium-audit",
        new AuditChatInterceptor()
    )
    .matcher(context ->
        "premium".equals(context.getAttribute("plan"))
    )
    .order(ChatInterceptorOrders.DEFAULT)
    .build();

Matcher 在责任链到达当前 Registration 时执行。因此,它可以读取顺序更早的拦截器已经写入 ChatContext 的数据。

ChatInterceptorProvider

ChatInterceptorProvider 用于让组件为单次 Prompt 请求贡献拦截器,而不修改 ChatModel 的共享配置。 当前 ChatModel 会从以下位置自动发现 Provider:

  1. Prompt 自身实现的 ChatInterceptorProvider
  2. 通过 prompt.addChatInterceptorProvider(...) 显式添加的 Provider。
  3. prompt.getToolGroups() 中的 ToolGroup
  4. prompt.addTool(...) 添加且实现了 ChatInterceptorProvider 的 Tool。

因此,ToolGroup 只会在当前 Prompt 实际使用它时提供解析拦截器,不会进入 ChatModel 的共享配置。

简单场景可以只返回 ChatInterceptor,框架会将其包装为默认 Registration:

java
prompt.addChatInterceptorProvider(new ChatInterceptorProvider() {
    @Override
    public List<ChatInterceptor> getChatInterceptors() {
        return Collections.singletonList(new TimingChatInterceptor());
    }
});

需要稳定名称、执行顺序或 Matcher 时,直接返回 Registration:

java
public class SearchTool implements Tool, ChatInterceptorProvider {
    @Override
    public List<ChatInterceptorRegistration> getChatInterceptorRegistrations() {
        return Collections.singletonList(
            ChatInterceptorRegistration.builder("search-request", interceptor)
                .order(ChatInterceptorOrders.REQUEST_PREPARATION)
                .build()
        );
    }
}

Provider 提供的 Registration 只加入当前请求的责任链。重复发现同一个 ChatInterceptor 实例时, 框架只保留第一次注册。Provider 不会修改 ChatModel 的实例级或全局 Registration。

执行顺序

每次请求会合并 Framework、Global、Instance 以及当前 Prompt 发现的 Provider Registration, 再按 order 从小到大稳定排序。

常量当前值默认用途
ChatInterceptorOrders.OBSERVABILITY-10000OpenTelemetry 可观测性
ChatInterceptorOrders.DEFAULT0普通应用拦截器
ChatInterceptorOrders.REQUEST_PREPARATION10000Tool Group 等请求准备逻辑

这些值是推荐值,不是边界。应用可以根据需要使用任意整数:

java
// 在框架可观测性之前加载 Trace Context
.order(ChatInterceptorOrders.OBSERVABILITY - 100)

// 在 Tool Group 解析后检查最终 Prompt
.order(ChatInterceptorOrders.REQUEST_PREPARATION + 100)

相同 order 的 Registration 按注册先后保持稳定顺序。默认情况下,同值时的来源顺序为 Framework、Global、Instance、Prompt 自身、Prompt 显式 Provider、Prompt ToolGroup、Prompt Tool,各来源内部保持注册顺序。

注册范围

实例级注册

实例级拦截器只作用于当前 ChatModel:

java
OpenAIChatModel chatModel = new OpenAIChatModel(config);
chatModel.addInterceptor(new TimingChatInterceptor());
chatModel.addInterceptorRegistration(registration);

也可以在构造模型时传入多个普通拦截器:

java
List<ChatInterceptor> interceptors = Arrays.asList(
    new AuthHeaderInterceptor(),
    new TimingChatInterceptor()
);

OpenAIChatModel chatModel = new OpenAIChatModel(config, interceptors);

全局注册

全局拦截器作用于注册完成后创建的 ChatModel,适合统一认证、审计和租户解析等应用级逻辑。注册方式、生效时机和管理 API 请参考 GlobalChatInterceptors

框架内置注册

框架内置 Registration 由 FrameworkChatInterceptors 管理,目前包括:

  • chat-observability:根据配置动态启用 OpenTelemetry。

ToolGroup 不再属于框架全局 Registration。ToolGroup 自身实现 ChatInterceptorProvider,在 Prompt 包含 ToolGroup 时按 REQUEST_PREPARATION 顺序提供 tool-group-resolver。它与应用 Registration 使用同一套排序规则,应用可以通过 order 将自己的拦截器放在 ToolGroup 解析之前或之后。

完整执行流程

text
创建 ChatContext

合并 Framework、Global、Instance Registration 与 Prompt Provider

按 order 从小到大稳定排序

依次执行 Matcher 和命中的 ChatInterceptor

调用 ChatModel

同步返回响应 / 流式触发 Listener

顺序较小的同步拦截器更早执行前置逻辑,并更晚执行后置逻辑。Matcher 只会看到排在它之前的拦截器已经完成的前置修改。

最佳实践

  1. Interceptor 应保持无状态或线程安全,请求数据放在局部变量或 ChatContext 中。
  2. Matcher 只负责判断,不在其中修改 Context。
  3. 使用 order 表达执行顺序;相同 order 时依赖注册顺序。
  4. 同步后置逻辑使用 try/finally 包裹 chain.proceed(...)
  5. 流式结束逻辑通过完整包装 StreamResponseListener 实现。
  6. 全局注册在应用初始化阶段完成,避免请求期间修改共享注册表。
  7. 不记录 API Key、Authorization、完整 Prompt 等敏感信息。

实现说明

ChatRequestSpec 只保存 URL、Header 和重试配置,不包含请求 Body。需要调整模型输入时,应修改 ChatContext 中的 PromptChatOptionsBaseChatConfig

模型请求 Body 会在拦截器链执行到末端时,基于最终的 Prompt、Options 和 Config 构建。因此,请在调用 chain.proceed(...) 之前完成请求信息修改;不需要手动刷新或重建 ChatRequestSpec

流式状态由框架根据 chat(...)chatStream(...) 设置。即使拦截器替换了整个 ChatOptions,最终请求也会使用正确的 streaming 值。

常见问题

为什么修改 Prompt 后没有生效?

确认修改发生在 chain.proceed(...) 之前,并检查后续拦截器是否再次替换了 Prompt。

为什么 Matcher 没有命中?

检查 Matcher 使用的数据是否已经写入 ChatContext,并确认提供这些数据的拦截器具有更小的 order

为什么同步调用生效,流式调用没有生效?

intercept(...)interceptStream(...) 是两个独立入口。如果需要支持两种请求方式,应分别实现;未覆盖的方法会使用默认透传逻辑。

为什么全局 Registration 没有影响已有 ChatModel?

全局 Registration 在 ChatModel 创建时复制。请在创建模型前完成全局注册,或者直接对已有模型调用 addInterceptorRegistration(...)

可以调整 Observability 和 Tool Group 的执行位置吗?

可以。可观测性是框架 Registration;Tool Group 是由 Prompt 自动发现的 Provider。两者与应用 Registration 使用相同的 order 排序规则。调整时需要同时考虑可观测范围以及 Tool Group 的解析时机。

下一步