Spring AI Advisor为什么不生效?defaultAdvisors、执行顺序与流式调用完整排查
文章摘要
Spring AI项目中,Advisor常被用于日志、Memory、RAG、权限、内容审核和工具执行,但开发者经常遇到“明明注册了却没有执行”“请求生效但响应处理顺序不对”“同步接口正常、流式接口失效”等问题。本文从ChatClient实例、defaultAdvisors与运行时advisors、getOrder顺序、CallAdvisor与StreamAdvisor、Context参数和日志观测六个方面给出完整排查方法。
一、先确认你调用的是不是同一个ChatClient
最常见的问题不是Advisor代码错误,而是:
Advisor注册在A客户端,业务调用的却是B客户端。
例如:
@Bean
ChatClient ragChatClient(
ChatClient.Builder builder,
QuestionAnswerAdvisor advisor
) {
return builder
.defaultAdvisors(advisor)
.build();
}
业务类却重新使用Builder:
@Service
public class AiService {
private final ChatClient chatClient;
public AiService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
}
这里创建的是一个新ChatClient,没有注册RAG Advisor。
正确做法:
@Service
public class AiService {
private final ChatClient ragChatClient;
public AiService(
@Qualifier("ragChatClient")
ChatClient ragChatClient
) {
this.ragChatClient = ragChatClient;
}
}
多ChatClient项目必须明确命名。
二、defaultAdvisors和advisors有什么区别
defaultAdvisors
在构建ChatClient时注册,对这个客户端的所有调用生效:
ChatClient chatClient = builder
.defaultAdvisors(
loggingAdvisor,
memoryAdvisor
)
.build();
适合:
- 通用日志;
- 安全检查;
- 租户上下文;
- 默认Memory;
- 默认RAG;
- 成本统计。
advisors
在单次请求中注册或传递参数:
String answer = chatClient.prompt()
.advisors(
advisorSpec -> advisorSpec
.advisors(customAdvisor)
.param("tenantId", tenantId)
)
.user(message)
.call()
.content();
适合:
- 某一次请求临时启用;
- 传递conversationId;
- 动态RAG过滤条件;
- 用户级策略;
- 临时审核规则。
常见错误是只传参数,却没有注册对应Advisor。
三、Advisor执行顺序不是“越大越先执行”
Spring AI按照 getOrder() 排序:
数值越小
→ 优先级越高
→ 请求阶段越早执行
例如:
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 100;
}
请求阶段:
安全Advisor
→ 租户Advisor
→ Memory Advisor
→ RAG Advisor
→ 模型
响应阶段会像栈一样反向返回。
如果两个Advisor返回相同order,执行顺序不保证稳定。
推荐集中定义:
public final class AdvisorOrders {
public static final int SECURITY = -1000;
public static final int TENANT = -800;
public static final int MEMORY = -500;
public static final int RAG = -200;
public static final int LOGGING = 1000;
private AdvisorOrders() {
}
}
四、同步接口和流式接口需要不同能力
Spring AI Advisor核心接口包括:
CallAdvisor
StreamAdvisor
如果自定义Advisor只实现 CallAdvisor,它只会参与 .call(),不会自动参与 .stream()。
一个简化的同步Advisor:
@Component
public class RequestLoggingAdvisor
implements CallAdvisor {
private static final Logger log =
LoggerFactory.getLogger(
RequestLoggingAdvisor.class
);
@Override
public ChatClientResponse adviseCall(
ChatClientRequest request,
CallAdvisorChain chain
) {
log.info(
"AI request context={}",
request.context()
);
ChatClientResponse response =
chain.nextCall(request);
log.info("AI response received");
return response;
}
@Override
public String getName() {
return "requestLoggingAdvisor";
}
@Override
public int getOrder() {
return AdvisorOrders.LOGGING;
}
}
最容易遗漏的是:
chain.nextCall(request)
如果没有调用后续链,又没有自己构造响应,请求就会被阻断。
五、Advisor可能主动阻断请求
安全Advisor可以直接返回受控响应,因此当模型没有被调用时,要检查:
- 是否有Advisor提前返回;
- 是否抛出异常;
- 是否命中缓存;
- 是否触发安全策略;
- 是否错误判断输入为空;
- 是否忘记调用下一条链。
建议在每个Advisor中记录:
advisor_name
request_enter
request_exit
response_enter
response_exit
blocked
duration_ms
六、运行时参数名称是否一致
传参:
.advisors(spec -> spec
.param("tenantId", tenantId)
.param("conversationId", conversationId)
)
Advisor读取:
String tenantId =
(String) request.context().get("tenantId");
名称不一致时不会自动报错,只会得到null。
建议使用常量。
七、Context更新后是否传入下一条链
自定义Advisor如果增加上下文,需要创建更新后的请求,再传给后续链。
伪代码:
ChatClientRequest updatedRequest =
request.mutate()
.context(
AdvisorContextKeys.TENANT_ID,
tenantId
)
.build();
return chain.nextCall(updatedRequest);
不要只修改局部Map,然后仍然把旧request传给下一条链。
八、Prompt修改是否真的写回请求
错误做法:
String enhancedPrompt =
originalPrompt + "\n补充上下文";
return chain.nextCall(request);
虽然生成了新字符串,但没有更新request。
核心原则是:
修改结果必须进入传给下一条链的ChatClientRequest。
九、TemplateRenderer不会自动影响Advisor内部模板
ChatClient可以配置:
.templateRenderer(customRenderer)
但它只影响直接通过ChatClient链定义的 user 和 system 模板,不会自动影响QuestionAnswerAdvisor或自定义RAG模板。
如果主Prompt正常、RAG增强内容异常,需要单独检查Advisor模板配置。
十、Memory Advisor不生效的常见原因
- conversationId没有传递;
- 每次请求生成新的conversationId;
- ChatMemoryRepository没有持久化;
- Advisor顺序不合理;
- 历史消息被上下文裁剪。
稳定会话ID示例:
.advisors(spec -> spec.param(
ChatMemory.CONVERSATION_ID,
conversationId
))
十一、RAG Advisor不生效的常见原因
检查:
VectorStore是否有数据
Embedding维度是否一致
检索过滤条件是否过严
相似度阈值是否过高
tenantId是否正确
检索结果是否进入Prompt
Advisor是否注册到实际ChatClient
建议把检索结果数量写入Context,便于从Trace判断问题发生在哪一层。
十二、开启可观测性
生产项目建议接入:
- Actuator;
- Micrometer;
- OpenTelemetry;
- Trace ID;
- 日志MDC。
日志示例:
traceId=abc123
advisor=tenantAdvisor
phase=request
order=-800
durationMs=2
不要记录完整敏感Prompt,可以记录Prompt哈希、字符数、Token估算、模型、Advisor名称与执行耗时。
十三、最小排查清单
□ 业务调用的是注册Advisor的ChatClient
□ defaultAdvisors确实执行
□ 单次advisors参数名称正确
□ getOrder没有重复
□ 数值越小优先级越高
□ 同步Advisor用于call
□ 流式Advisor用于stream
□ 调用了nextCall或nextStream
□ 修改后的request传入下一条链
□ Context更新被正确写回
□ TemplateRenderer作用范围正确
□ Memory使用稳定conversationId
□ RAG检索结果非空
□ Trace中能看到Advisor
总结
Advisor“不生效”通常集中在四类问题:
注册错ChatClient
执行顺序理解错误
同步与流式接口不匹配
修改结果没有写回请求链
先沿着ChatClient实例、Advisor注册、order、Context和链式调用逐层检查,比反复修改Prompt更有效。
延伸阅读
如果你正在关注企业级 AI 应用、Agent、RAG、MCP 与大模型工程化落地,欢迎访问 智元界:
https://www.zyentor.com/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。