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链定义的 usersystem 模板,不会自动影响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/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。