对话拦截器 ChatInterceptor
概述
ChatInterceptor 用于在 Chat 请求执行前后插入自定义逻辑,适合处理多个模型调用都需要的通用能力,例如:
- 调整 Prompt、模型参数或请求头
- 注入用户、租户和认证信息
- 记录日志、耗时和审计数据
- 根据当前请求动态启用某项能力
- 缓存命中、权限拒绝等请求短路
- 处理同步响应或监听流式响应
拦截器采用责任链模式。调用 chain.proceed(...) 会继续执行后续拦截器,并最终请求大模型;同步调用还可以在 proceed(...) 返回后处理响应。
Interceptor A before
Interceptor B before
ChatModel
Interceptor B after
Interceptor A after同步和流式请求分别对应两个入口:
| 请求方式 | 拦截方法 | 结果处理方式 |
|---|---|---|
chat(...) | intercept(...) | 返回 AiMessageResponse |
chatStream(...) | interceptStream(...) | 包装 StreamResponseListener |
适用场景
- 统一请求策略:为一组模型调用追加系统约束、限制输出长度或调整采样参数。
- 多租户与动态认证:根据
accountId、租户或业务套餐写入 Header 和路由参数。 - 审计与监控:统计耗时、记录调用结果,并把业务标识传给可观测系统。
- 条件能力:只对某类账号、模型或请求启用缓存、内容审核等逻辑。
- 响应后处理:同步请求返回后统一清洗结果,或包装流式 Listener 监听结束与失败。
如果只是某一次调用需要设置 temperature、model 等参数,直接使用 ChatOptions 更简单;当同一逻辑需要覆盖多个调用点时,再使用拦截器。
快速开始
下面用一个最小示例,在请求模型前统一调整 temperature,并记录同步调用耗时。
1. 创建拦截器
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. 注册拦截器
OpenAIChatModel chatModel = new OpenAIChatModel(config);
chatModel.addInterceptor(new TimingChatInterceptor());3. 发起请求
String result = chatModel.chat("介绍一下 Agents-Flex");调用 chat(...) 时,TimingChatInterceptor 会先调整参数,再继续执行模型请求。try/finally 可以确保请求成功或抛出异常时都记录耗时。
WARNING
如果拦截器没有调用 chain.proceed(...),后续拦截器和模型请求都不会执行。只有在缓存命中、权限拒绝等需要主动中断的场景中,才应跳过该调用。
完整 Demo
下面以多租户客服为例:
- 从
ChatContext读取租户信息 - 为请求添加租户 Header
- 为高级套餐调整模型参数
- 仅在匹配当前套餐时执行策略拦截器
定义租户拦截器
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);
}
}定义高级套餐策略
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);
}
}注册并调用
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 注册,只有 plan 为 premium 时才会进入责任链。
典型场景
修改 Prompt 和 ChatOptions
拦截器可以修改当前请求的 Prompt 和模型参数:
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:
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 决定当前请求是否启用拦截器:
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、模型、账号、会话、业务属性和流式状态:
.matcher(context ->
context.getAccountId() != null
&& context.isStreaming()
&& containsSearchIntent(context.getPrompt())
)Matcher 应只负责判断,避免在其中修改 ChatContext。需要补充上下文时,应使用一个执行顺序更早的拦截器。
缓存命中或主动短路
同步拦截器可以直接返回结果,不再请求模型:
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:
AiMessageResponse response = chain.proceed(chatModel, context);
AiMessage filtered = filter(response.getMessage());
return new AiMessageResponse(
response.getContext(),
response.getRawText(),
filtered
);适合进行敏感信息脱敏、统一格式化或输出内容检查。
处理流式响应
流式内容通过回调异步到达。统计完整流式耗时或处理最终状态时,应包装 StreamResponseListener:
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 时应完整代理 onOpen、onMessage、onClose 和 onError,避免上层无法正确结束请求或释放资源。
GlobalChatInterceptors
GlobalChatInterceptors 用于注册应用级 Chat 拦截器。它适合所有 ChatModel 都需要执行的公共逻辑,例如:
- 统一添加认证、租户和 Trace Header
- 审计模型请求和响应
- 执行内容安全检查
- 采集应用级指标
- 根据账号或业务属性应用统一策略
全局拦截器应在应用初始化阶段、创建任何 ChatModel 之前完成注册。
注册普通全局拦截器
GlobalChatInterceptors.addInterceptor(
new AuthHeaderInterceptor()
);
OpenAIChatModel chatModel = new OpenAIChatModel(config);通过 addInterceptor(...) 注册的拦截器会被包装为始终匹配、order 为 ChatInterceptorOrders.DEFAULT 的 Registration。
也可以按列表顺序批量注册:
GlobalChatInterceptors.addInterceptors(Arrays.asList(
new AuthHeaderInterceptor(),
new AuditChatInterceptor(),
new TimingChatInterceptor()
));当多个拦截器具有相同 order 时,保持注册顺序执行。每次调用都会追加新的拦截器,框架不会按类型或名称自动去重,因此应避免重复执行初始化逻辑。
注册带条件和顺序的全局拦截器
需要按请求激活或指定执行顺序时,注册 ChatInterceptorRegistration:
ChatInterceptorRegistration premiumAudit =
ChatInterceptorRegistration.builder(
"premium-audit",
new PremiumAuditInterceptor()
)
.matcher(context ->
"premium".equals(context.getAttribute("plan"))
)
.order(-100)
.build();
GlobalChatInterceptors.addRegistration(premiumAudit);批量注册可以使用:
GlobalChatInterceptors.addRegistrations(Arrays.asList(
tenantResolverRegistration,
premiumAuditRegistration
));全局、实例和框架 Registration 最终使用同一套 order 规则排序。“全局”表示作用范围,不代表它一定在实例级拦截器之前执行;当 order 不同时,以 order 为准。
生效时机
创建 BaseChatModel 时,框架会复制当时的全局 Registration:
注册 Global A
↓
创建 ChatModel 1 → 包含 Global A
↓
注册 Global B
↓
创建 ChatModel 2 → 包含 Global A、Global B因此:
ChatModel 1不会自动获得之后注册的Global B。- 需要修改已有模型时,应调用该模型的
addInterceptor(...)或addInterceptorRegistration(...)。 GlobalChatInterceptors.clear()只清空全局注册表,不会移除已有模型中的快照。
这种快照机制让模型实例的拦截器链在创建后保持稳定,也意味着不应把 GlobalChatInterceptors 当作运行时动态开关。
查询全局注册
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)。
测试清理
全局注册表是进程级静态状态。测试用例应在执行后清理,避免影响其他测试:
public class ChatInterceptorTest {
@After
public void clearGlobalInterceptors() {
GlobalChatInterceptors.clear();
}
}生产环境中不建议在请求处理期间调用 clear() 或追加注册。虽然管理方法是线程安全的,但已经创建的 ChatModel 使用各自的快照,运行时修改会使不同模型实例具有不同配置。
进阶使用
ChatInterceptor 接口
同步和流式方法都有默认透传实现,只需要覆盖实际使用的入口:
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 会被包装为始终匹配、order 为 0 的 Registration。需要条件激活或调整顺序时,可以显式创建 Registration。
| 属性 | 作用 |
|---|---|
name | Registration 的稳定名称,用于识别和诊断 |
interceptor | Matcher 命中后执行的拦截器 |
matcher | 根据当前 ChatContext 判断是否执行 |
order | 责任链顺序,数值越小越早进入 |
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:
- Prompt 自身实现的
ChatInterceptorProvider。 - 通过
prompt.addChatInterceptorProvider(...)显式添加的 Provider。 prompt.getToolGroups()中的ToolGroup。prompt.addTool(...)添加且实现了ChatInterceptorProvider的 Tool。
因此,ToolGroup 只会在当前 Prompt 实际使用它时提供解析拦截器,不会进入 ChatModel 的共享配置。
简单场景可以只返回 ChatInterceptor,框架会将其包装为默认 Registration:
prompt.addChatInterceptorProvider(new ChatInterceptorProvider() {
@Override
public List<ChatInterceptor> getChatInterceptors() {
return Collections.singletonList(new TimingChatInterceptor());
}
});需要稳定名称、执行顺序或 Matcher 时,直接返回 Registration:
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 | -10000 | OpenTelemetry 可观测性 |
ChatInterceptorOrders.DEFAULT | 0 | 普通应用拦截器 |
ChatInterceptorOrders.REQUEST_PREPARATION | 10000 | Tool Group 等请求准备逻辑 |
这些值是推荐值,不是边界。应用可以根据需要使用任意整数:
// 在框架可观测性之前加载 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:
OpenAIChatModel chatModel = new OpenAIChatModel(config);
chatModel.addInterceptor(new TimingChatInterceptor());
chatModel.addInterceptorRegistration(registration);也可以在构造模型时传入多个普通拦截器:
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 解析之前或之后。
完整执行流程
创建 ChatContext
↓
合并 Framework、Global、Instance Registration 与 Prompt Provider
↓
按 order 从小到大稳定排序
↓
依次执行 Matcher 和命中的 ChatInterceptor
↓
调用 ChatModel
↓
同步返回响应 / 流式触发 Listener顺序较小的同步拦截器更早执行前置逻辑,并更晚执行后置逻辑。Matcher 只会看到排在它之前的拦截器已经完成的前置修改。
最佳实践
- Interceptor 应保持无状态或线程安全,请求数据放在局部变量或
ChatContext中。 - Matcher 只负责判断,不在其中修改 Context。
- 使用
order表达执行顺序;相同order时依赖注册顺序。 - 同步后置逻辑使用
try/finally包裹chain.proceed(...)。 - 流式结束逻辑通过完整包装
StreamResponseListener实现。 - 全局注册在应用初始化阶段完成,避免请求期间修改共享注册表。
- 不记录 API Key、Authorization、完整 Prompt 等敏感信息。
实现说明
ChatRequestSpec 只保存 URL、Header 和重试配置,不包含请求 Body。需要调整模型输入时,应修改 ChatContext 中的 Prompt、ChatOptions 或 BaseChatConfig。
模型请求 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 的解析时机。