文章摘要
Spring AI项目中,Advisor常被用于日志、Memory、RAG、权限、内容审核和工具执行,但开发者经常遇到“明明注册了却没有执行”“请求生效但响应处理顺序不对”“同步接口正常、流式接口失效”等问题。本文从ChatClient实例、defaultAdvisors与运行时advisors、getOrder顺序、CallAdvisor与StreamAdvisor、Context参数和日志观测六个方面给出完整排查方法。
一、先确认你调用的是不是同一个ChatClient
最常见的问题不是Advisor代码错误,而是:
Advisor注册在A客户端,业务调用的却是B客户端。
例如:
@BeanChatClientragChatClient(ChatClient.Builderbuilder,QuestionAnswerAdvisoradvisor){returnbuilder.defaultAdvisors(advisor).build();}业务类却重新使用Builder:
@ServicepublicclassAiService{privatefinalChatClientchatClient;publicAiService(ChatClient.Builderbuilder){this.chatClient=builder.build();}}这里创建的是一个新ChatClient,没有注册RAG Advisor。
正确做法:
@ServicepublicclassAiService{privatefinalChatClientragChatClient;publicAiService(@Qualifier("ragChatClient")ChatClientragChatClient){this.ragChatClient=ragChatClient;}}多ChatClient项目必须明确命名。
二、defaultAdvisors和advisors有什么区别
defaultAdvisors
在构建ChatClient时注册,对这个客户端的所有调用生效:
ChatClientchatClient=builder.defaultAdvisors(loggingAdvisor,memoryAdvisor).build();适合:
- 通用日志;
- 安全检查;
- 租户上下文;
- 默认Memory;
- 默认RAG;
- 成本统计。
advisors
在单次请求中注册或传递参数:
Stringanswer=chatClient.prompt().advisors(advisorSpec->advisorSpec.advisors(customAdvisor).param("tenantId",tenantId)).user(message).call().content();适合:
- 某一次请求临时启用;
- 传递conversationId;
- 动态RAG过滤条件;
- 用户级策略;
- 临时审核规则。
常见错误是只传参数,却没有注册对应Advisor。
三、Advisor执行顺序不是“越大越先执行”
Spring AI按照getOrder()排序:
数值越小 → 优先级越高 → 请求阶段越早执行例如:
@OverridepublicintgetOrder(){returnOrdered.HIGHEST_PRECEDENCE+100;}请求阶段:
安全Advisor → 租户Advisor → Memory Advisor → RAG Advisor → 模型响应阶段会像栈一样反向返回。
如果两个Advisor返回相同order,执行顺序不保证稳定。
推荐集中定义:
publicfinalclassAdvisorOrders{publicstaticfinalintSECURITY=-1000;publicstaticfinalintTENANT=-800;publicstaticfinalintMEMORY=-500;publicstaticfinalintRAG=-200;publicstaticfinalintLOGGING=1000;privateAdvisorOrders(){}}四、同步接口和流式接口需要不同能力
Spring AI Advisor核心接口包括:
CallAdvisorStreamAdvisor如果自定义Advisor只实现CallAdvisor,它只会参与.call(),不会自动参与.stream()。
一个简化的同步Advisor:
@ComponentpublicclassRequestLoggingAdvisorimplementsCallAdvisor{privatestaticfinalLoggerlog=LoggerFactory.getLogger(RequestLoggingAdvisor.class);@OverridepublicChatClientResponseadviseCall(ChatClientRequestrequest,CallAdvisorChainchain){log.info("AI request context={}",request.context());ChatClientResponseresponse=chain.nextCall(request);log.info("AI response received");returnresponse;}@OverridepublicStringgetName(){return"requestLoggingAdvisor";}@OverridepublicintgetOrder(){returnAdvisorOrders.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读取:
StringtenantId=(String)request.context().get("tenantId");名称不一致时不会自动报错,只会得到null。
建议使用常量。
七、Context更新后是否传入下一条链
自定义Advisor如果增加上下文,需要创建更新后的请求,再传给后续链。
伪代码:
ChatClientRequestupdatedRequest=request.mutate().context(AdvisorContextKeys.TENANT_ID,tenantId).build();returnchain.nextCall(updatedRequest);不要只修改局部Map,然后仍然把旧request传给下一条链。
八、Prompt修改是否真的写回请求
错误做法:
StringenhancedPrompt=originalPrompt+"\n补充上下文";returnchain.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更有效。