Spring AI 教程(下篇)

面向 Java 开发者的 Spring AI 完整入门到进阶指南(下篇)

版本 :Spring AI 1.1.2 · Spring Boot 3.5.x · Java 17+

下篇涵盖:第 7~14 章 ------ Function Calling、向量数据库、RAG、多模态、MCP、可观测性与评测、参数调整、关键 API 速查。


第三部分 · 让模型「动手」与「找资料」

第 7 章 Function Calling(工具调用)

模型本身无法查实时数据、调业务系统。Function Calling 让模型在需要时发起工具调用,由你的代码执行并回传结果。

7.1 完整交互流程

Function Calling 的「调用」发生在你的代码里,而不是模型里。模型只负责决定要不要调用、调用哪个工具,真正的执行始终由本地 Java 代码完成。一次完整的工具调用流程如下:

text 复制代码
用户提问 → LLM 判断 → 输出 tool_calls → SDK 匹配本地工具 → 执行 Java → 封装 ToolMessage → 回传 LLM → 最终回答
  1. 注册工具(开发阶段) :开发者在本地注册工具,每个工具只提交三部分元信息 ------name(工具名)、description(功能描述)、入参 JSON Schema (参数名称、类型、约束等)。注意:传给大模型的是元信息,Java 源代码本身不会被传给模型。

  2. 随请求提交工具清单 :每次请求大模型时,Spring AI SDK 会把所有已注册工具的元信息一并提交给 LLM,模型由此「知道」自己现在有哪些工具可用、各自能干什么。

  3. 模型决策:LLM 结合用户问题的语义,判断是否需要调用工具:

    • 不需要 → 直接返回普通文本回答;
    • 需要 → 输出 tool_calls 结构,携带 function.name(要调用的工具名)与 arguments(参数 JSON,例如 {"city":"北京"})。
  4. 本地匹配并执行 :Spring AI SDK 拿到响应后,根据 function.name本地已注册的工具回调 中匹配,找到对应方法并执行真正的 Java 业务逻辑。模型本身不能运行任何代码,它只是「点名」要哪个工具、传什么参数。

  5. 回传结果并生成回答 :SDK 把工具的执行结果封装成 ToolMessage 再次发给大模型,由模型结合工具返回结果整理、润色,输出最终回答给用户。

核心要点:整个流程里,模型只负责「选择」工具,代码的「执行」始终在本地完成------这正是 Function Calling 安全可控的原因。

7.2 用 @Tool 定义工具

Spring AI 1.1 推荐用 @Tool / @ToolParam 注解,旧 FunctionCallback 已废弃。

java 复制代码
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

@Component
public class WeatherTools {

    @Tool(description = "查询指定城市的当前天气,返回温度和天气状况")
    public String getWeather(@ToolParam(description = "城市名称,例如:北京") String city) {
        // 真实场景这里调用天气 API / 数据库
        return city + " 今天晴,28℃,微风。";
    }
}

@Tool@ToolParam 常用属性

注解 属性 默认 说明
@Tool name 方法名 暴露给 LLM 的工具名,可自定义避免重名
@Tool description 触发条件与返回值说明,直接影响工具选择
@Tool returnDirect false true 时结果直接返回用户,不再让模型二次加工
@Tool resultConverter JSON 自定义结果转换器
@ToolParam name 参数名 参数名
@ToolParam description 参数说明,帮助模型正确取值
@ToolParam required true 是否必填,可选参数设 false 或用 @Nullable

注意:用 record 作为参数时,字段默认可选,必填字段需在方法体内自行校验。

7.3 注册工具并调用

java 复制代码
@RestController
public class WeatherController {

    private final ChatClient chatClient;

    public WeatherController(ChatClient.Builder builder, WeatherTools weatherTools) {
        // 方式一:默认工具,所有请求都带上
        this.chatClient = builder.defaultTools(weatherTools).build();
    }

    @GetMapping("/weather")
    public String weather(@RequestParam("message") String message) {
        // 方式二:按请求注册(更灵活,按需加载)
        // return chatClient.prompt().user(message).tools(weatherTools).call().content();
        return chatClient.prompt().user(message).call().content();
    }
}

测试:

shell 复制代码
curl "http://localhost:8080/weather?message=北京今天天气怎么样"
# 模型识别到需要查天气 → 触发 getWeather 工具 → 回传结果 → 生成最终回答

7.4 返回对象与多参数(进阶)

工具方法不一定要返回 String ,可以直接返回 Java 对象(record / DTO),Spring AI 会通过 ToolCallResultConverter 自动序列化成 JSON 回传给模型,模型再据此组织自然语言回答:

java 复制代码
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

// 返回类型:record,会被自动转成 JSON 传给模型
public record StockInfo(String symbol, String name, double price, String currency) {}

@Component
public class StockTools {

    // 一个工具方法支持多个参数
    @Tool(description = "查询股票实时价格,返回代码、名称、价格与币种")
    public StockInfo getStockPrice(
            @ToolParam(description = "股票代码,如 AAPL") String symbol,
            @ToolParam(description = "币种,如 USD 或 CNY") String currency) {
        // 真实场景这里调用行情 API
        return new StockInfo(symbol, "Apple Inc.", 189.5, currency);
    }
}

说明:返回对象被序列化为 JSON 传给模型。若要对回传格式做定制(例如改成 YAML/XML),可自定义 ToolCallResultConverter;参数校验可用 JSR-303(如 @NotNull)配合 @Validated 开启方法校验。

7.5 实战:一次请求触发多个工具

当用户的一句话涉及多个能力时,模型会在同一轮里发起多个工具调用,Spring AI 逐个执行后汇总回传,模型再综合生成最终回答。

一次注册多个工具:

java 复制代码
@RestController
public class AssistantController {

    private final ChatClient chatClient;

    public AssistantController(ChatClient.Builder builder,
                               WeatherTools weatherTools,   // 见 7.2
                               StockTools stockTools) {     // 见 7.4
        this.chatClient = builder
                .defaultTools(weatherTools, stockTools)    // 一次注册多个工具
                .build();
    }

    @GetMapping("/assistant")
    public String assistant(@RequestParam String message) {
        return chatClient.prompt()
                .user(message)
                .call()
                .content();
    }
}

用户提问(一条 prompt 同时需要「天气」和「股价」两类工具):

shell 复制代码
curl "http://localhost:8080/assistant?message=帮我查一下北京今天的天气,再查一下 AAPL 和 TSLA 的股价,并说说今天更适合关注哪只股票"

执行流程:

scss 复制代码
用户提问(一句话)
   → 模型识别出 3 个工具调用:
        getWeather("北京") 、 getStockPrice("AAPL") 、 getStockPrice("TSLA")
   → Spring AI 逐个串行执行这些工具(1.x 默认)
   → 3 个结果汇总后回传模型
   → 模型综合生成最终回答(如:北京今天晴、AAPL 涨 TSLA 跌,建议关注 AAPL......)

说明:模型在一次响应里可以同时发起 多个工具调用------既可以是不同工具 ,也可以是同一工具的不同参数 (如上面两次 getStockPrice);具体调哪些、怎么调由模型根据提问自行决策,业务代码无需干预。

⚠️ 执行方式 :Spring AI 1.x 默认串行执行------即使模型一次返回多个工具调用,底层也是循环逐个执行;要并发执行需升级到 Spring AI 2.0 的并行工具管理器(Parallel Tool Manager),且需手动配置开启。

7.6 不确定性与应对

Function Calling 本身就存在天然不确定性,并不是 100% 可靠 ------这是工程落地时最头疼的点。先明确:这不是 Spring AI 框架的问题,而是大模型本身的行为特性------模型是基于概率的生成,而非确定性程序,因此「要不要调工具、调哪个、参数对不对」每一步都可能出错。

  1. 不确定性来自哪里

    • 选错工具:多个工具语义相近时,模型可能选错或漏选;
    • 参数填错:参数类型、取值不符合预期,或该填的字段没填、格式不对;
    • 漏调用 / 过度调用:该调用时不调用(转而「编造」答案),或不该调用时乱调用;
    • 返回不稳定:同样的输入,每次输出可能不完全一致;
    • 描述歧义 :工具的 description 写得含糊,模型理解产生偏差。
  2. 生产工程上如何降低不确定性

    写好工具描述(正例 vs 反例)description 是模型判断「何时用、用什么」的唯一依据,务必写清触发场景、输入输出与边界。

    java 复制代码
    // ❌ 反例:描述太泛,模型容易误判触发条件、填错参数
    @Tool(description = "处理订单")
    public String handleOrder(@ToolParam(description = "参数") String param) {
        // ...
    }
    
    // ✅ 正例:写清「何时用、返回什么、不做什么」
    @Tool(description = "根据订单号查询订单状态(待支付/已发货/已签收)。"
            + "仅在用户询问「订单到哪了 / 发货了吗 / 订单状态」时调用;"
            + "不处理退款、改地址、下单等操作")
    public String queryOrderStatus(
            @ToolParam(description = "订单号,20 位数字,例如 20240101000000000001") String orderNo) {
        // ...
    }

    Prompt 层强制约束(SystemPrompt):在系统提示词里给模型设定全局行为边界------什么时候必须调工具、什么时候禁止硬答。

    text 复制代码
    你是一个订单助手。规则:
    1. 涉及实时数据(订单状态、库存、价格)时必须调用对应工具,不得凭记忆编造;
    2. 工具未返回结果前,禁止输出确定性的结论;
    3. 匹配不到合适工具时,直接说明「该问题超出我的能力范围」,不要硬答。

    做校验层:不要信任模型输出 :模型返回的 arguments 只是「建议」,正式执行前必须校验。工具内部用 JSR-303(@NotNull@Pattern)或手动校验入参;对 tool_calls 里的工具名做白名单匹配,非法工具名 / 参数直接拒绝,并把错误信息回传模型,让它修正重试。

    区分业务重要程度:不同工具不能同等对待------

    • 低风险工具(查天气、查资料等读操作):可放宽校验,失败重试即可;
    • 高风险工具(转账、下单、删除、发短信等写操作):需二次确认、权限校验、限额控制,必要时不让模型直接触发,只让模型「填表」再由人工确认。

    兜底策略 :模型调不出结果或调用失败时,要有「最后一道防线」------返回明确的错误提示引导用户换种问法、降级为人工客服、或走默认只读逻辑,而不是抛异常中断整个链路。同时要限制工具连环调用的次数,防止模型在「调用 → 结果不理想 → 再调用」中陷入死循环:

    java 复制代码
    ChatOptions options = ChatOptions.builder()
            .toolCallMaxIterations(3)   // 最多连环调用 3 次工具,防死循环
            .build();
    
    String answer = chatClient.prompt()
            .user(message)
            .options(options)
            .call()
            .content();

    选型层面 :对 function-call 要求高的场景,优先选指令遵循强的模型(如 DeepSeek、Qwen 等);很多开源小模型的 function-call 能力很差,工具一多就容易选错或漏调------此时要么换更强的模型,要么收敛工具数量、简化描述来迁就模型能力。

一句话总结:不确定性无法彻底消除,只能靠「清晰的工具描述 + Prompt 约束 + 严格校验 + 分级授权 + 兜底降级」层层兜住,把它压到可接受的范围。

7.7 工具异常处理

工具方法抛异常时,由 ToolExecutionException 包装,ToolExecutionExceptionProcessor 处理。关键开关 spring.ai.tools.throw-exception-on-error(默认 false):

yaml 复制代码
spring:
  ai:
    tools:
      throw-exception-on-error: false   # false:异常以错误消息回传模型;true:直接抛给调用方

最佳实践:

  • 工具内部 try-catch,不要让堆栈信息泄露给用户;
  • 优先 return 错误描述(而非抛异常),让模型能据此调整或向用户解释;
  • 对外部 API 调用加超时保护,捕获 TimeoutException

7.8 ToolContext 传递上下文

某些数据(tenantId、当前用户身份)不该交给模型当参数 ,而应在工具执行时由服务端注入------多租户、权限校验的推荐做法。ToolContext 里装的是不发给模型、仅在工具执行侧使用 的数据,工具方法通过 call(String toolInput, ToolContext toolContext) 拿到它。

为什么不能让模型传 :如果 tenantId / userId 变成 @ToolParam,模型就掌握了对它们的「决定权」,恶意 prompt 可以诱导模型改传别人的租户或越权操作(prompt 注入)。所以这类数据必须由服务端从登录态里取,塞进 ToolContext

① 调用侧:从登录态取数,注入 ToolContext

java 复制代码
import org.springframework.ai.tool.context.ToolContext;

@GetMapping("/order")
public String query(@RequestParam("message") String message) {
    // 服务端可信数据:从登录态/请求上下文取(示例,真实项目换成你的安全框架)
    String tenantId = "tenant-001";
    String userId = "u_10086";

    ToolContext toolContext = ToolContext.builder()
            .with("tenantId", tenantId)
            .with("userId", userId)
            .build();

    return chatClient.prompt()
            .user(message)
            .toolContext(toolContext)   // 随请求注入,但不进入发给模型的 JSON
            .call()
            .content();
}

② 工具侧:用 ToolContext 参数读取(不进 JSON Schema)

java 复制代码
@Tool(description = "查询当前租户下的订单状态")
public String queryOrder(
        @ToolParam(description = "订单号") String orderNo,
        ToolContext toolContext) {   // 特殊参数:框架自动注入,不出现在工具的入参 Schema 里

    String tenantId = toolContext.getContext().get("tenantId");
    String userId = toolContext.getContext().get("userId");

    // 用 tenantId 做数据隔离、userId 做权限校验------它们都来自服务端,模型碰不到
    return orderService.query(orderNo, tenantId, userId);
}

对比:模型在 arguments 里只会看到 {"orderNo":"..."},永远看不到 tenantId / userId;这两个值由服务端注入、工具内部读取,天然防篡改。
核心原则:不要把用户身份/权限交给模型参数 (易被 prompt 注入篡改),而应通过 ToolContext 由服务端注入,工具执行时从 context 读取。

7.9 底层机制(二次开发速览)

了解 @Tool 背后的核心组件,方便二次开发与排错:

组件 职责
ToolCallback 一次工具调用的封装(名称 + 描述 + 入参 schema + 执行逻辑)
ToolCallbackProvider 提供一组 ToolCallback@Tool 注解扫描、MCP 工具都基于它)
ToolCallingManager 执行工具调用,并把结果回传模型
ToolCallResultConverter 把工具返回值转换成回传模型的字符串(默认 JSON)

7.10 全链路日志调试

Function Calling 排查问题时的最大痛点:你只看到最终答案,看不到中间「模型 → 工具 → 模型」的每一跳。工具选错、参数填错、漏调用时,光看结果根本定位不了。把整条链路打印出来,从粗到细有三种方式:

① 开启框架底层日志

最省事,改 application.yml 打开 Spring AI 相关包的 DEBUG 日志:

yaml 复制代码
logging:
  level:
    org.springframework.ai.chat.client: DEBUG   # 聊天客户端
    org.springframework.ai.chat.model: DEBUG     # 模型调用
    org.springframework.ai.tool: DEBUG           # 工具调用
    org.springframework.ai.openai.api: DEBUG     # 原始 HTTP 请求/响应(含 tool_calls);Ollama 换 org.springframework.ai.ollama.api

优点:零代码;缺点:日志量大、噪音多、偏底层 HTTP,不够聚焦业务。

② 自定义 Advisor 拦截全链路(推荐)

Advisor 是 Spring AI 的请求/响应拦截链aroundCall 会把「调用模型」这一步完整包裹:调用前能拿到发给模型的全部消息,调用后能拿到模型原始返回(含是否发起 tool_calls)。把日志 Advisor 挂在最外层,即可打印每一跳:

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatResponse;

public class FullChainLoggingAdvisor implements CallAroundAdvisor {

    private static final Logger log = LoggerFactory.getLogger(FullChainLoggingAdvisor.class);

    @Override
    public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
        // 1) 打印「发给模型」的完整消息列表(system / user / 历史 tool 消息)
        log.info("[请求] 发给模型的消息:{}", advisedRequest.request().messages());

        // 2) 放行:执行后续 Advisor 链,最终调用模型
        AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);

        // 3) 打印「模型返回」的原始响应(含是否发起工具调用)
        ChatResponse response = advisedResponse.response();
        AssistantMessage assistant = response.getResult().getOutput();
        log.info("[响应] 模型文本输出:{}", assistant.getText());
        log.info("[响应] 工具调用:{}", assistant.getToolCalls());

        return advisedResponse;
    }

    @Override
    public String getName() {
        return "FullChainLoggingAdvisor";
    }

    @Override
    public int getOrder() {
        return 0;   // 数值越小越靠外层;0 = 最外层,能包裹住其它 Advisor
    }
}

注册:

java 复制代码
this.chatClient = builder
        .defaultAdvisors(new FullChainLoggingAdvisor())
        .defaultTools(weatherTools)
        .build();

提示:Spring AI 也内置了 SimpleLoggerAdvisor,想快速看每一步消息可直接用;自定义 Advisor 的好处是可以按需裁剪字段、统一格式、接入自己的日志平台。

③ 包装 ToolCallback,打印本地工具调用的入参、返回值

Advisor 看到的是「模型侧」的消息,但工具真正执行的入参与返回发生在 ToolCallback.call()。用一个包装类代理原 ToolCallback,在调用前后各打一条日志,即可定位「模型传了什么参数进来、工具回了什么出去」:

java 复制代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.context.ToolContext;
import org.springframework.ai.tool.definition.ToolDefinition;

public class LoggingToolCallback implements ToolCallback {

    private static final Logger log = LoggerFactory.getLogger(LoggingToolCallback.class);
    private final ToolCallback delegate;

    public LoggingToolCallback(ToolCallback delegate) {
        this.delegate = delegate;
    }

    @Override
    public String call(String toolInput, ToolContext toolContext) {
        String name = getToolDefinition().name();
        log.info("[工具入参] {} -> {}", name, toolInput);
        String result = delegate.call(toolInput, toolContext);
        log.info("[工具返回] {} -> {}", name, result);
        return result;
    }

    @Override
    public ToolDefinition getToolDefinition() {
        return delegate.getToolDefinition();
    }
}

如何接入ChatClient.tools() / .defaultTools() 既能接收带 @Tool 注解的 bean,也能直接接收 ToolCallbackToolCallbackProvider。所以把 @Tool 方法手动转成 ToolCallback,套上日志包装再注册即可。

java 复制代码
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.method.MethodToolCallback;

// 方式一:单个工具 ------ MethodToolCallback 把 @Tool 方法转成 ToolCallback,再包装
ToolCallback logged = new LoggingToolCallback(
        MethodToolCallback.builder()
                .toolObject(weatherTools)   // 持有 @Tool 方法的 bean
                .toolMethod("getWeather")   // 要包装的方法名
                .build());

this.chatClient = builder
        .defaultTools(logged)               // 注册包装后的工具
        .build();

工具多时逐个写方法名较繁琐,用 ToolCallbackProvider 统一包装更省事:

java 复制代码
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.stereotype.Component;

@Component
public class LoggingToolCallbackProvider implements ToolCallbackProvider {

    private final List<ToolCallback> callbacks;

    public LoggingToolCallbackProvider(WeatherTools weatherTools, StockTools stockTools) {
        this.callbacks = List.of(
                new LoggingToolCallback(MethodToolCallback.builder()
                        .toolObject(weatherTools).toolMethod("getWeather").build()),
                new LoggingToolCallback(MethodToolCallback.builder()
                        .toolObject(stockTools).toolMethod("getStockPrice").build()));
    }

    @Override
    public ToolCallback[] getToolCallbacks() {
        return callbacks.toArray(ToolCallback[]::new);
    }
}
java 复制代码
this.chatClient = builder
        .defaultTools(loggingToolCallbackProvider)   // 传入 Provider,一次注册全部
        .build();

如果只想给 @Tool 方法加日志、不想引入包装类,也可直接用 Spring AOP 切 @Tool 注解方法(@Around("@annotation(org.springframework.ai.tool.annotation.Tool)")),更省事;包装 ToolCallback 的好处是能拿到 Spring AI 已解析好的 ToolDefinition(name、description、schema)和序列化后的入参 JSON。
三种方式互补:① 看底层框架/HTTP 日志,② 看「模型 ↔ 消息」的每一跳,③ 看「工具 ↔ 入参/返回」的细节。三者叠加即可还原一条请求的完整现场。

日志关键调试看哪些信息

排查时重点盯这几个字段:

关注点 看什么 能发现的问题
工具选择 tool_calls 里的 function.name 选错工具 / 漏调用
入参 arguments(JSON) 参数填错、缺字段、类型不符
调用次数 迭代轮数 / 是否反复调用同一工具 死循环、参数一直不对
工具返回 工具方法实际返回值 返回内容是否够模型组织回答、是否异常
最终回答 模型基于工具结果生成的文本 是否「编造」、是否偏离工具结果
性能 token 消耗、每跳耗时 工具过多导致 token 暴涨、慢接口拖累

一句话:Function Calling 的 bug 几乎都藏在「中间那几跳」,看不到链路就只能靠猜------所以先把日志打通,再谈优化。


第 8 章 向量数据库

向量数据库用于存储文本的向量表示 ,支撑语义检索(RAG 的核心)。Spring AI 用 VectorStore 接口统一抽象,业务代码不感知底层是哪种库------本文以 pgvector 为例。

8.1 概念与接口

  • 嵌入(Embedding) :把文本转成高维浮点向量(本质是一串数字),语义相近的文本向量距离更近。这一步由**嵌入模型(EmbeddingModel)**完成------它就是专门负责「文本 → 向量」的模型,和生成回答的聊天模型(ChatModel)是两回事。
  • VectorStore 核心方法:add(List<Document>) 写入、similaritySearch(SearchRequest) 相似度检索。

所谓「embedding 配置里指定模型」(如 spring.ai.ollama.embedding.options.model: nomic-embed-text),就是告诉程序用哪个模型来做文本向量化 。同一件「文本 → 向量」的事,不同模型算出的向量维度不同(nomic-embed-text 768 维、text-embedding-3-small 1536 维),所以要保证向量库的 dimensions 和它一致。

先厘清三个词的关系:插件 / 数据库 / 向量数据库

本文以 pgvector 为例,很多人误以为「pgvector 就是一个独立的向量数据库」------其实它是「数据库 + 插件」的组合:

text 复制代码
向量数据库(本文方案) = 数据库(PostgreSQL) + 插件(pgvector 扩展)
概念 是什么 角色
数据库(PostgreSQL) 老牌关系型数据库 提供存储、事务、权限等基础能力,原生没有向量类型
插件(pgvector) PostgreSQL 的扩展(extension) 给 PG 补上 vector 类型、相似度算子、HNSW/IVFFLAT 索引
向量数据库 泛指「专门存向量、做相似度检索」的库 分两条路线:原生向量库 (Milvus、Qdrant、Chroma)与关系库 + 插件(本文)

关键:pgvector 不是能单独运行的数据库 ,必须装进 PostgreSQL 才能用。第 8.3 节的 CREATE EXTENSION vector; 就是在「这个数据库里安装这个插件」,装上之后 PostgreSQL 才变成一个有向量检索能力的向量数据库。

Spring AI 用 VectorStore 接口把底层统一抽象,所以无论底层是 pgvector(插件路线)还是 Milvus(原生路线),业务代码都是同一个 add / similaritySearch / delete------这也是 8.9 节能「换 starter + 配置即可切换」的原因。

8.2 用 SimpleVectorStore 快速验证(免装库)

在接数据库之前,可以用 Spring AI 内置的内存向量库 SimpleVectorStore 先把 RAG 流程跑通------零外部数据库依赖,适合本地开发、单元测试。

依赖说明:SimpleVectorStorespring-ai-vector-store 模块里(Spring AI 1.0 起从 spring-ai-core 拆出)。若只引入了聊天模型 starter(如第 2 章的 Ollama),需要额外补一个依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-vector-store</artifactId>
</dependency>

依赖就绪后,写一个 @Bean 即可:

java 复制代码
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DevVectorStoreConfig {

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        // 纯内存实现:数据存在 JVM 里,重启即丢
        return SimpleVectorStore.builder(embeddingModel).build();
    }
}

若同时引入了多个模型 starter(第 2 章的 Ollama + OpenAI 就是如此),容器里会有多个 EmbeddingModel,此时该用哪个、维度如何对齐,见 8.4 的「嵌入模型怎么选」。

用法与 pgvector 完全一致(同一个 VectorStore 接口),add / similaritySearch / delete 照写不误。验证通过后,只需把上面的 @Bean 换成 PgVectorStore 或其它实现,业务代码零改动。

特点:无需外部数据库、秒级启动、随时可跑;缺点是内存存储、重启即丢、不支持多实例共享,只用于开发验证,生产必须换持久化向量库。

8.3 准备 pgvector 环境

验证流程没问题后,生产要换成真正持久化的向量库。本文以 pgvector(PostgreSQL + 扩展)为例,先用 Docker 把它跑起来:

shell 复制代码
# 用官方 pgvector 镜像起一个 PostgreSQL(内置 pgvector 扩展)
docker run -d --name pgvector \
  -e POSTGRES_PASSWORD=postgres \
  -p 5432:5432 \
  pgvector/pgvector:pg16

# 连接后启用扩展
docker exec -it pgvector psql -U postgres -c "CREATE EXTENSION vector;"

8.4 依赖与配置

引入 pgvector 的 starter 和 JDBC 驱动,再在 application.yml 里配置连接与向量参数:

xml 复制代码
<!-- pgvector 向量存储 starter -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

spring-ai-starter-vector-store-pgvector 已传递包含 spring-ai-vector-store,所以直接从 pgvector 开始时,无需再单独加 8.2 的那个依赖。

yaml 复制代码
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres
  ai:
    vectorstore:
      pgvector:
        initialize-schema: true        # 启动时自动建表
        index-type: HNSW               # 索引类型
        distance-type: COSINE_DISTANCE # 距离度量:余弦
        dimensions: 768                # 必须与嵌入模型维度一致

⚠️ 维度必须匹配dimensions 要和嵌入模型产出的向量维度一致,否则检索会静默失败或报错。若未显式指定,PgVectorStore 会自动从 EmbeddingModel 读取;但改维度需重建表并重新向量化。

嵌入模型怎么选(多模型并存时) :第 2 章同时引入了 Ollama 和 OpenAI,容器里会有多个 EmbeddingModelollamaEmbeddingModelopenAiEmbeddingModel)。框架用 spring.ai.model.embedding 决定默认注入哪个,也可以在注入时用 @Qualifier 显式指定:

yaml 复制代码
spring:
  ai:
    model:
      embedding: ollama             # 决定默认注入的 EmbeddingModel
    ollama:
      embedding:
        options:
          model: nomic-embed-text   # 768 维
    openai:
      embedding:
        options:
          model: text-embedding-3-small  # 1536 维
java 复制代码
// 多模型并存时,用 @Qualifier 显式指定嵌入模型(SimpleVectorStore / PgVectorStore 同理)
@Bean
public VectorStore vectorStore(@Qualifier("ollamaEmbeddingModel") EmbeddingModel embeddingModel) {
    return SimpleVectorStore.builder(embeddingModel).build();
}

@Qualifier 的 bean 名:Ollama 是 ollamaEmbeddingModel,OpenAI 是 openAiEmbeddingModel。选哪个模型,dimensions 就设成对应维度(768 或 1536)。

chat 与 embedding 供应商不同时 :如果 chat 用某个账号/端点、embedding 想走另一个端点(不同账号或不同的 OpenAI 兼容服务),可以给 embedding 单独配 base-url / api-key,它们会覆盖共用的 spring.ai.openai.base-url / api-key

yaml 复制代码
spring:
  ai:
    openai:
      base-url: https://api.openai.com      # chat 用的公共地址
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini
      # embedding 单独走另一个端点(覆盖上面的公共 base-url / api-key)
      embedding:
        base-url: https://your-embedding-endpoint
        api-key: ${EMBEDDING_API_KEY}
        options:
          model: text-embedding-3-small

若 chat 与 embedding 是完全不同的供应商 (如 chat=OpenAI、embedding=智谱),则各自配 spring.ai.<provider>.*,再用 spring.ai.model.chat / spring.ai.model.embedding 分别指定即可,两者互不影响。

下面分别说明 distance-type(距离度量)和 index-type(索引类型)怎么选:

距离度量选型

距离度量 说明 适用
COSINE_DISTANCE 余弦距离(默认) 大多数场景
EUCLIDEAN_DISTANCE L2 欧氏距离 数值型向量
NEGATIVE_INNER_PRODUCT 内积(取负),适合归一化向量 OpenAI 等归一化嵌入,性能更优

索引类型选型

索引类型 构建速度 查询性能 内存 说明
HNSW 慢(默认) 快、召回高 可在空表上建索引
IVFFLAT 较低 需先有数据聚类,需调 lists/probes
NONE --- 精确检索 --- 不建索引

生产建议:默认 HNSW;数据量大、写入频繁或内存受限时可考虑 IVFFLAT。注意 pgvector 的 HNSW 索引最多支持 2000 维。

⚠️ 多数据源冲突 :项目里若配置了多个 DataSource,pgvector 自动配置可能报「required single bean but found 2」,需排除 DataSourceAutoConfiguration 或显式指定用哪个数据源。

8.5 编程式创建 VectorStore

除了 application.yml 自动配置,也可以直接用代码创建 PgVectorStore,适合需要动态传参(如从配置中心读维度)的场景:

java 复制代码
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.ai.vectorstore.pgvector.PgDistanceType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class VectorStoreConfig {

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel, JdbcTemplate jdbcTemplate) {
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
                .dimensions(768)                       // 与嵌入模型一致
                .distanceType(PgDistanceType.COSINE_DISTANCE)
                .build();
    }
}

8.6 写入与检索

VectorStore 建好后,写入和检索各是一个方法调用------写入时框架会自动调用嵌入模型把文本向量化,你只传文本和元数据即可:

java 复制代码
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;

// 写入文档(文本 + 元数据)
vectorStore.add(List.of(
        new Document("Spring AI 是 Spring 官方的 AI 框架。", Map.of("source", "intro.md")),
        new Document("pgvector 是 PostgreSQL 的向量扩展。", Map.of("source", "vector.md"))));

// 语义检索:topK 取前 3,similarityThreshold 过滤低相似度
List<Document> hits = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query("Spring 的 AI 框架是什么")
                .topK(3)
                .similarityThreshold(0.5)
                .build());

for (Document doc : hits) {
    System.out.println(doc.getText() + " -> " + doc.getMetadata());
}

上面的 Document 是手工 new 的,实际项目里文本通常来自文件。Spring AI 提供 DocumentReader 系列解析器(统一实现 DocumentReader 接口),能把磁盘文件 / MultipartFile 直接解析成 List<Document>

java 复制代码
import org.springframework.ai.reader.tika.TikaDocumentReader;   // 底层 Apache Tika,支持 PDF/Word/HTML/TXT
import org.springframework.web.multipart.MultipartFile;

// 磁盘文件
List<Document> docs = new TikaDocumentReader("file:./docs/handbook.pdf").get();
vectorStore.add(docs);

// 上传的 MultipartFile
MultipartFile file = ...;
vectorStore.add(new TikaDocumentReader(file.getResource()).get());

TikaDocumentReader 外,还有 TextReader(纯文本)、JsonReader(JSON)、PagePdfDocumentReader / ParagraphPdfDocumentReader(PDF 逐页/逐段)等;长文档先切块(见第 9 章 ETL)再 add

8.7 Document 结构与相似度分数

Document 有四个字段:idtextmetadatascore

java 复制代码
Document doc = Document.builder()
        .id("doc-1")                       // 唯一标识,删除/更新靠它
        .text("Spring AI 是 Spring 官方的 AI 框架。")
        .metadata(Map.of("source", "intro.md"))
        .score(0.92)                       // 相似度分数(检索时由框架填充)
        .build();

检索返回的 Document 自带 score(相似度)和 metadata["distance"](原始距离)。以 pgvector 为例,score = 1 - distance------余弦距离越小越相似,score 越高越相似:

java 复制代码
for (Document doc : hits) {
    System.out.println(doc.getScore() + " / " + doc.getText());
    // 输出示例:0.91 / Spring AI 是 Spring 官方的 AI 框架。
}

similarityThreshold 相似度阈值 :检索时只返回 score ≥ 阈值 的文档,低于阈值的直接丢弃。它是「相关」与「不相关」的分界------设太低会把无关文档混进来,设太高会漏掉本该命中的文档。

阈值 效果
< 0.4 太松,几乎不过滤,噪声多
0.4 ~ 0.6 偏松,适合「召回优先」、结果多再精排
0.65 ~ 0.8 常用区间,平衡准确与召回
> 0.85 过严,容易一条都查不到

阈值是「最低可接受的相似度」,不同嵌入模型分数分布略有差异,以实际打印为准。

开发实操建议

  1. 开发调试阶段先设 0.65 ,把检索返回的文档逐条打印 score,观察真实业务数据的分数分布(相关文档大概多少分、无关文档大概多少分)。
  2. 根据打印出来的实际 score 再上调到 0.7 ~ 0.8不要拍脑袋写死 0.9------阈值过高很容易「查不到任何文档」,模型拿不到资料只能瞎编,反而产生幻觉。

一句话:阈值靠数据说话,不靠猜------先打印分数分布,再在「相关」和「无关」的分数之间取阈值。

8.8 元数据过滤与删除文档

检索时按元数据过滤(例如只查某个来源),避免跨领域误召回:

java 复制代码
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;

// 方式一:字符串表达式(SQL 风格)
List<Document> hits1 = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query("Spring AI")
                .topK(3)
                .filterExpression("source == 'intro.md'")
                .build());

// 方式二:FilterExpressionBuilder 流式构建(可组合 and/or)
FilterExpressionBuilder b = new FilterExpressionBuilder();
List<Document> hits2 = vectorStore.similaritySearch(
        SearchRequest.builder()
                .query("Spring AI")
                .topK(3)
                .filterExpression(b.and(
                        b.eq("source", "intro.md"),
                        b.gte("year", 2024)).build())
                .build());

删除文档:

java 复制代码
// 按 id 删除(id 在写入时通过 Document.builder().id(...) 指定,未指定则自动生成 UUID)
vectorStore.delete(List.of("doc-1", "doc-2"));

// 按过滤条件批量删除
vectorStore.delete("source == 'old.md'");

更新文档 = 先删后加:

java 复制代码
vectorStore.delete(List.of("doc-1"));
vectorStore.add(List.of(Document.builder()
        .id("doc-1")
        .text("更新后的内容...")
        .metadata(Map.of("source", "intro.md"))
        .build()));

常用操作符:eq / ne / gt / gte / lt / lte / in / nin,可用 and / or / not 组合。两种写法等价------字符串表达式 (SQL 风格)与 FilterExpressionBuilder(流式):

java 复制代码
// ------ 字符串表达式(SQL 风格,支持 == != > >= < <= && || ! 和 in [...])------
.filterExpression("source == 'intro.md'")                 // 等于
.filterExpression("category != 'news'")                   // 不等于
.filterExpression("year >= 2024")                         // 大于等于(也支持 > / < / <=)
.filterExpression("category in ['tech', 'ai']")           // 属于集合
.filterExpression("tag nin ['spam']")                     // 不属于集合
.filterExpression("source == 'intro.md' && year >= 2024") // 且
.filterExpression("category == 'ai' || category == 'tech'") // 或
.filterExpression("!(tag == 'spam')")                     // 非

// ------ FilterExpressionBuilder 流式(等价于上面)------
FilterExpressionBuilder b = new FilterExpressionBuilder();
b.eq("source", "intro.md");          // ==
b.ne("category", "news");            // !=
b.gt("year", 2023);                  // >
b.gte("year", 2024);                 // >=
b.lt("price", 100);                  // <
b.lte("price", 100);                 // <=
b.in("category", "tech", "ai");      // in
b.nin("tag", "spam");                // not in

b.and(b.eq("source", "intro.md"), b.gte("year", 2024));   // 且
b.or(b.eq("category", "ai"), b.eq("category", "tech"));   // 或
b.not(b.eq("tag", "spam"));                               // 非

提示:字符串写起来快、适合简单条件;条件多、需要动态拼接时用 FilterExpressionBuilder 更清晰。

8.9 其他向量库实现

Spring AI 提供同一套 VectorStore 接口,切换存储只需换 starter + 配置:spring-ai-starter-vector-store-redis...-chroma...-milvus...-elasticsearch...-cassandra 等。

选型建议:先用 SimpleVectorStore 验证 RAG 流程 → 再按团队技术栈选持久化库(PostgreSQL 用 pgvector,追求大规模检索性能用 Milvus / Qdrant 等原生向量库)。


第 9 章 RAG(检索增强生成)

RAG = 先从知识库检索 相关文档,再把这些文档作为上下文注入提示词,让模型基于真实资料回答,减少幻觉。

text 复制代码
离线入库:文件 → 读取 → 切分 → 向量化 → 存入向量库
在线问答:用户提问 → 检索相关文档 → 注入提示词 → 模型生成回答

RAG 分离线在线两段:离线把知识「切块 + 向量化」灌进向量库(9.1),在线把提问「检索 + 注入」交给模型回答(9.2 起)。

9.1 ETL:数据摄入

三步走:读取 → 切分 → 写入向量库。其中「读取」用 TikaDocumentReader(底层 Apache Tika)解析 PDF/Word/PPT/HTML 等文件------RAG 知识库导入文件必须,需要先引入依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
java 复制代码
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;

// 1. 读取(支持 PDF/Word/HTML 等,底层用 Apache Tika)
var reader = new TikaDocumentReader("file:./docs/handbook.pdf");

// 2. 切分(按 token 分块,避免单块过长)
TokenTextSplitter splitter = new TokenTextSplitter(800, 350, 5, 10000, true);

// 3. 写入向量库(自动 embedding)
List<Document> chunks = splitter.apply(reader.get());
vectorStore.add(chunks);

⚠️ 图片不会被自动解析TikaDocumentReader 这类普通解析器只提取文本,碰到图片既不报错、也不会识别,而是直接跳过------图片里承载的信息就丢了。框架不会自动帮你判断文档里有没有图片,需要业务层自己处理:图片带业务信息(如架构图、流程图、截图里的关键内容)时,要手动把图片抽出来,调用多模态模型转成文字描述,再参与切块和向量化;纯装饰的配图则无需处理。

切分(Chunk)策略 :长文档必须切成小块再入库,块大小直接决定检索质量。上面 TokenTextSplitter(800, 350, 5, 10000, true) 五个参数:

参数 含义
defaultChunkSize 800 每块的目标大小(token 数)
minChunkSizeChars 350 块的最小字符数,避免切得太碎
minChunkLengthToEmbed 5 短于此的碎片不向量化,直接丢弃
maxNumChunks 10000 单文档最多切多少块(防超大文档)
keepSeparator true 切分时是否保留换行等分隔符

块大小怎么选:块太大 → 检索结果噪声多、语义不聚焦;块太小 → 一句话被切到两个块、上下文断裂。一般 300~1000 token 起步,没有银弹------先用默认值跑通,再按检索效果微调。

进阶分块策略:固定大小切块简单,但容易把完整语义切碎。对检索质量要求高时,常用三种进阶策略:

策略 思路 适用
父子分块(parent-child) 检索用小块 (精准命中),喂模型时带回父块(补全上下文) 文档结构清晰、需要完整上下文
语义分块 按语义边界(段落/标题)切,而非按 token 硬切 避免把一句话切到两个块
small-to-big 检索小片段 → 扩展回更大的原文块 兼顾精准命中 + 上下文完整

入门先用固定大小切块跑通即可;发现「检索命中了但答案缺上下文」时,再考虑父子分块。
入库去重 :重复导入同一文档会产生重复向量、污染检索结果。给 Document 指定稳定的 id(如「文件名 + 块序号」),配合「更新 = 先删后加」(见 8.8)即可避免重复入库。

9.2 朴素 RAG:QuestionAnswerAdvisor

最简单的方式,检索 + 注入 + 问答一气呵成。需要引入 spring-ai-advisors-vector-store 依赖(和 6.4 的长期记忆 VectorStoreChatMemoryAdvisor 是同一个模块):

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;

QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder()
                .topK(5)
                .similarityThreshold(0.45)
                .build())
        .build();

ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(qaAdvisor)
        .build();

// 提问时会自动检索并注入上下文
String answer = chatClient.prompt()
        .user("我们的产品支持哪些支付方式?")
        .call()
        .content();

9.3 自定义提示词模板

QuestionAnswerAdvisor 默认用英文模板合并「问题 + 检索结果」,中文/业务场景应自定义。模板必须含 {query}{question_answer_context} 两个占位符

java 复制代码
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.vectorstore.VectorStore;

PromptTemplate customTemplate = PromptTemplate.builder()
        .template("""
                请基于以下资料回答问题,资料中没有的直接说「我不知道」。

                问题:{query}

                资料:
                {question_answer_context}
                """)
        .build();

QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .promptTemplate(customTemplate)
        .build();

自定义模板常用于:① 中文提示;② 强制「资料里没有就说不确定」;③ 控制回答格式。

引用溯源(Citation) :企业知识库常要回答「依据哪份文档」。关键是入库时把来源存进 Documentmetadata(如 source,见 9.1),回答时把来源带出来。注意:QuestionAnswerAdvisor 默认只注入正文、不注入 metadata,要做溯源通常用 9.5 的手动 RAG(docs 自带 metadata)把来源拼进提示词或随回答返回------核心 Spring AI 不内置 citation。

9.4 元数据过滤

按文档元数据缩小检索范围(例如只查某个来源):

java 复制代码
String answer = chatClient.prompt()
        .user("退货政策是什么?")
        .advisors(a -> a.param(
                QuestionAnswerAdvisor.FILTER_EXPRESSION, "source == 'policy.md'"))
        .call()
        .content();

9.5 模块化 RAG:RetrievalAugmentationAdvisor

1.1.0 新增,属于 spring-ai-rag 模块,需要引入依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-rag</artifactId>
</dependency>

先理解「揉在一起」是什么意思------对比手动 RAG,每一步都自己写:

java 复制代码
// 手动 RAG:检索、注入、生成三步都自己来
List<Document> docs = vectorStore.similaritySearch(
        SearchRequest.builder().query(question).topK(5).build());

// 把检索到的文档拼成上下文
String context = docs.stream().map(Document::getText).collect(Collectors.joining("\n"));

// 注入提示词 + 生成回答
String answer = chatClient.prompt()
        .system("基于资料回答:\n" + context)
        .user(question)
        .call()
        .content();

手动 RAG 每一步都看得见、能单独调:先打印 docs 看检索质量、再改提示词模板。而 QuestionAnswerAdvisor 把「检索 → 注入 → 生成」封装成一个 Advisor,你只给 vectorStore 和问题,它内部自动跑完,中间的检索结果、提示词拼接都被藏起来了,想单独调某一环就只能靠少数参数(如 FILTER_EXPRESSION)间接干预:

java 复制代码
// 朴素 RAG(QuestionAnswerAdvisor):同样的事,封装成一个 Advisor 一步到位
QuestionAnswerAdvisor qaAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder().topK(5).build())
        .build();

String answer = chatClient.prompt()
        .advisors(qaAdvisor)   // 只挂 Advisor,内部自动「检索 → 注入 → 生成」
        .user(question)
        .call()
        .content();

模块化 RAG 就是折中------把流程拆成四段,每段可独立开关、独立配置:

阶段 作用 对应 builder 方法 必填
① 查询改写 用 LLM 把口语化提问改写成更利于检索的形式 queryTransformers(...)
② 检索 从向量库召回最相关的 topK 条 documentRetriever(...)
③ 后处理 对召回结果再加工(如 Rerank 重排序) documentPostProcessors(...)
④ 增强 把检索结果拼进提示词、控制空上下文 queryAugmenter(...)

每段什么时候才需要加(不一定要全加------查询改写 / Rerank 都要额外调一次 LLM,有成本)。以电商客服知识库为例,用户问「这东西咋退啊」:

  1. ① 查询改写:用户提问口语化、和文档用词不一致时能明显提升命中率。

    • 例:用户「这东西咋退啊」→ 改写为「如何申请退货」,才能对上库里文档「退货流程与政策」;提问已经很规范就不必加。
  2. ② 检索:始终生效------它就是真正从库里捞文档的那一步。

    • 例:拿「如何申请退货」去检索,返回「退货政策」「退款说明」等 topK 条。
  3. ③ 后处理(Rerank):向量召回「排序不够准」时,用更精准的 reranker 重排取前几条。

    • 例:向量检索可能把「发货时效」排在「退货政策」前面(语义相近但非目标),Rerank 后把「退货政策」提到第 1 位;数据量小、召回已够准就别加。
  4. ④ 增强:需要控制检索结果怎么拼进提示词、或检索为空时怎么处理时再加。

    • 例:把重排后的文档拼进「请基于资料回答」的提示词;一条都没检索到时 allowEmptyContext(false) 让它回「我不知道」而不是瞎编。

关于 Rerank 的通俗理解:它就是「把检索到的文档重新排个序」。你不用在业务代码里手动排序 ,而是写一个「重排器」(DocumentPostProcessor,内部调用某个 reranker 模型给每篇文档打分)交给框架;框架会在检索之后、交给模型之前,自动用它把结果排一遍。核心 Spring AI 不自带现成的重排器,需要你自己实现 DocumentPostProcessor(内部通过 HTTP 调你部署的 rerank 服务)。
Reranker 是什么 :一类专门训练的重排序模型------既不是 Embedding 模型,也不是生成大模型。它和 Embedding 的区别:

  • Embedding(双塔 / bi-encoder) :把 query 和文档分别编码成向量再算相似度,快但粗糙,用于「粗召回」;
  • Reranker(交叉编码器 / cross-encoder) :把 query + 文档拼在一起输入,直接输出一个相关度分数,慢但准,用于「精排」。

主流开源 Rerank 模型:BAAI bge-reranker(base / large / v2-m3,中文好)、阿里 gte-rerank / qwen3-rerank(百炼)、jina-reranker-v2、轻量英文 ms-marco-MiniLM;商业的有 Cohere Rerank。

部署注意 :不要试图在 Java 进程里跑 Rerank 模型------Java 没有成熟的 transformer 推理生态,加载 / 推理 Rerank 模型很别扭。一般把 Rerank 部署成独立的 Python 服务 (如 FastAPI + sentence-transformers / FlagEmbedding),Java 侧通过 HTTP 调用;或者直接用云厂商的 rerank API(百炼 qwen3-rerank、Cohere Rerank)。

最简用法------只配「检索」(②),效果和朴素 RAG 差不多:

java 复制代码
import org.springframework.ai.advisor.Advisor;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;

Advisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
        .documentRetriever(VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .topK(5)
                .similarityThreshold(0.45)
                .build())
        .build();

按需再加「查询改写」(①)、「后处理 Rerank」(③)、「增强」(④):

java 复制代码
// 新增 import(其余同上):RewriteQueryTransformer、DocumentPostProcessor、ContextualQueryAugmenter、RestTemplate
import org.springframework.ai.rag.augmentation.ContextualQueryAugmenter;
import org.springframework.ai.rag.postretrieval.document.DocumentPostProcessor;
import org.springframework.ai.rag.query.transformation.RewriteQueryTransformer;
import org.springframework.web.client.RestTemplate;

// ③ 后处理:写一个「重排器」,内部通过 HTTP 调你自己部署的 rerank 服务给文档打分,按相关度取前 3
@Component
public class RerankPostProcessor implements DocumentPostProcessor {

    private final RestTemplate restTemplate;   // 普通 Spring 的 HTTP 客户端

    public RerankPostProcessor(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public List<Document> process(Query query, List<Document> documents) {
        if (documents.size() <= 3) {
            return documents;
        }
        // 把 query + 各文档文本 POST 给 rerank 服务,返回按相关度降序的文本列表
        Map<String, Object> req = Map.of(
                "query", query.text(),
                "documents", documents.stream().map(Document::getText).toList());
        List<String> ranked = restTemplate.postForObject(
                "http://localhost:8001/rerank", req, List.class);   // 换成你的 rerank 服务地址

        Map<String, Document> byText = documents.stream()
                .collect(Collectors.toMap(Document::getText, d -> d, (a, b) -> a));
        return ranked.stream().map(byText::get).filter(Objects::nonNull).limit(3).toList();
    }
}

// 构建时把重排器注册到「后处理」阶段
Advisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
        // ① 查询改写:内部要调 LLM 重写提问,需传入注入的 ChatClient.Builder(见 1.3)
        .queryTransformers(RewriteQueryTransformer.builder()
                .chatClientBuilder(chatClientBuilder)   // chatClientBuilder = 注入的 ChatClient.Builder
                .build())
        // ② 检索:必填
        .documentRetriever(VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .topK(5)
                .similarityThreshold(0.45)
                .build())
        // ③ 后处理:交给 RerankPostProcessor 精排
        .documentPostProcessors(rerankPostProcessor)
        // ④ 增强:allowEmptyContext 控制检索为空时是否回答
        .queryAugmenter(ContextualQueryAugmenter.builder()
                .allowEmptyContext(false)   // 检索为空时拒绝回答,防幻觉
                .build())
        .build();

核心 Spring AI 不自带 reranker 模型,所以上面 RerankPostProcessor 里是通过 HTTP 调你自己部署的 rerank 服务 (部署方式见上面「部署注意」)。如果你用的是 Spring AI Alibaba 生态,它有现成的 RerankModel(如 DashScopeRerankModel)和开箱即用的 RetrievalRerankAdvisor,可以直接用、省去自己写 HTTP 调用。

最后把它挂到 ChatClient 上,和普通 Advisor 一样用:

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(ragAdvisor)   // 或每次请求 .advisors(ragAdvisor)
        .build();

String answer = chatClient.prompt()
        .user("我们的产品支持哪些支付方式?")
        .call()
        .content();

chatClientBuilder 是注入的 ChatClient.Builder(Spring AI 自动配置,见 1.3),在 @Bean 方法里作为参数注入即可------RewriteQueryTransformer 要用 LLM 改写提问,所以需要它。
常用组件:RewriteQueryTransformer(改写查询)、MultiQueryExpander(扩展成多个语义变体,.numberOfQueries(3))、ContextualQueryAugmenter(控制上下文拼接 + allowEmptyContext 空上下文开关)。查询改写建议用低温(如 0.0)让结果更确定;多查询扩展会显著增加检索次数,按需使用。
选型小结 :手动 RAG 全可控但啰嗦;QuestionAnswerAdvisor 省事但黑盒。推荐 RetrievalAugmentationAdvisor 这个折中方案------先只配「检索」快速跑通,需要时再逐段加「查询改写 / 后处理 / 增强」,省事和可控兼得。

9.6 混合检索(向量 + 关键词)

纯向量检索对精确关键词 不敏感------SKU 编号、型号、专有名词、缩写(「P0 故障」)、人名地名,靠语义向量经常召回不准。生产上常用混合检索:向量检索 + 关键词检索(BM25)两条路召回,再融合或交给 Rerank 精排。

text 复制代码
用户提问
   ├─ 向量检索(语义相近)──────────┐
   └─ BM25 关键词检索(精确匹配)────┤→ 融合(如 RRF)→ 精排(Rerank)→ 注入提示词
  • 什么时候上:知识库里编号、型号、专有名词多,纯向量查不准时。
  • 怎么落地 :核心 VectorStore 接口只做向量检索,关键词检索 / 融合需配合具体向量库(Milvus / Elasticsearch 原生支持混合检索),或用 Spring AI Alibaba 的混合检索方案------比纯向量重,按需引入。

简单场景先用「纯向量 + 查询改写 + Rerank」就够了;发现「编号/型号查不准」再上混合检索。

9.7 多轮对话 RAG(RAG + 记忆)

用户会追问(「那它的价格呢?」「和上一个比呢?」),指代消解需要结合对话记忆 (第 6 章)。做法:给 ChatClient 同时挂「记忆 Advisor」和「RAG Advisor」即可:

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(memory).build(),   // 记忆:带上历史对话
                qaAdvisor)                                         // RAG:检索 + 注入
        .build();

关键:① 历史对话(含指代)会随记忆进到 RAG 的检索/查询改写里,帮助消解「它」指什么;② 配 RewriteQueryTransformer 时多轮指代消解效果更好;③ 记忆会让 token 持续增长,配合第 6 章的窗口截断/摘要使用。

9.8 RAG 常见问题与调优

RAG 落地最常遇到三类问题,先判断属于哪一种,再对症下药:

症状 根因 对策
检索不到(召回低) 相关文档没被搜出来 降低 similarityThreshold;用 RewriteQueryTransformer 改写查询;换更强的嵌入模型
检索不准(精准低) 搜出一堆无关文档 提高阈值;元数据过滤(9.4)缩小范围;Rerank 重排序精排
幻觉 资料里没有却乱编 allowEmptyContext(false) 拒绝空上下文;提示词强制「没有就说不知道」(9.3);降低温度

几点补充:

  • 阈值是检索的「闸门」 :查不到先降阈值、查太杂先升阈值,配合打印 score 观察分布(见 8.7)。
  • 查询改写 :用户口语化提问(「这东西咋退」)和文档书面语(「退货流程」)有差距,RewriteQueryTransformer 用 LLM 把提问改写成更利于检索的形式,是提升命中率最有效的手段之一。
  • Rerank(重排序):先粗召回一批(如 topK=20),再用更精准的 reranker 模型重排取前几条。适合对检索质量要求高的场景。

排查顺序:先看检索 (该命中的有没有命中、score 分布对不对),再看生成(拿到资料后是否老实回答)------八成问题出在检索侧。

9.9 两种 RAG 对比

场景 推荐
快速验证、代码最少 QuestionAnswerAdvisor
需要查询改写、多路检索、空上下文控制 RetrievalAugmentationAdvisor

第四部分 · 进阶主题

第 10 章 多模态

多模态 = 输入/输出不限于纯文本 。Spring AI 通过 Messagemedia 字段支持图片/音频输入,用独立模型接口支持图像生成与语音。

多模态能力高度依赖具体厂商与模型,接入前请先确认所选模型(本地/云端)是否支持对应模态。
Ollama 现状 :本地 Ollama 支持多模态看图(llava),但没有官方文生图(ImageModel)实现 ,也没有内置 ASR/TTS 语音接口(语音要自己接 whisper 等外部服务)。

视觉输入、图像生成、语音原则上都需要对应能力的专用模型 ,普通纯文本大模型做不了。Spring AI 只是把能力做了接口抽象、封装了调用,底层还是依赖模型本身具备该能力,框架不会自己做 AI 运算。

10.1 视觉输入(图片理解)

java 复制代码
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.MimeTypeUtils;

String answer = chatClient.prompt()
        .user(u -> u.text("这张图片里有什么?请详细描述。")
                .media(MimeTypeUtils.IMAGE_PNG, new ClassPathResource("/photo.png")))
        .call()
        .content();

也可用 UserMessage + Media 构建:

java 复制代码
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.model.Media;

UserMessage msg = UserMessage.builder()
        .text("解释这张图")
        .media(new Media(MimeTypeUtils.IMAGE_JPEG, new ClassPathResource("/photo.jpg")))
        .build();

注意:本地视觉需用多模态模型(如 llava),纯文本模型不支持图片输入。

视觉输入(图片理解 Vision)属于 ChatModel,不是 ImageModel ------它和普通聊天共用同一个 ChatModel,只是把模型名换成多模态模型(如 llavaqwen-vl)。yml 不需要单独开「图片配置」,只需指定多模态模型名;Media 传图片是代码层面做的事,yml 主要配置 api-key、模型名、参数。

10.2 图像生成(文生图)

ImageModel 接口,OpenAI 实现为 OpenAiImageModel。先配置图像模型(OpenAI 为 DALL-E / gpt-image-1):

yaml 复制代码
spring:
  ai:
    model:
      image: openai            # 指定 image 模型供应商(类似 chat / embedding)
    openai:
      api-key: ${OPENAI_API_KEY}
      image:
        options:
          model: dall-e-3      # 图像生成模型,也可用 gpt-image-1
          size: 1024x1024
          quality: standard

注意:本地 Ollama 没有官方文生图实现(见章首「Ollama 现状」),文生图需用云端图像模型。

ImageModel 由 Spring AI 自动配置,注入即可用:

java 复制代码
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse;
import org.springframework.stereotype.Service;

@Service
public class ImageService {

    private final ImageModel imageModel;   // 自动配置,直接注入

    public ImageService(ImageModel imageModel) {
        this.imageModel = imageModel;
    }

    public String generateImage(String prompt) {
        ImageResponse response = imageModel.call(new ImagePrompt(prompt));
        return response.getResult().getOutput().getUrl();   // 返回图片 URL
    }
}

10.3 语音(转写与合成)

Spring AI 提供 TranscriptionModel(语音转文字)与 SpeechModel(文字转语音),OpenAI 实现为 OpenAiAudioTranscriptionModel(Whisper)与 OpenAiAudioSpeechModel(TTS),同样通过 spring-ai-starter-model-openai 提供。先配置语音模型:

yaml 复制代码
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      audio:
        speech:                          # TTS 文字转语音
          options:
            model: tts-1                  # 也可 tts-1-hd / gpt-4o-mini-tts
            voice: alloy                  # alloy / echo / fable / onyx / nova / shimmer
            response-format: mp3          # 音频格式
        transcription:                    # ASR 语音转写
          options:
            model: whisper-1
            response-format: text

注意:本地 Ollama 没有内置 ASR/TTS 语音接口(见章首「Ollama 现状」),语音能力需用云端模型。

两个接口都由 Spring AI 自动配置,注入即可用:

java 复制代码
import org.springframework.stereotype.Service;

@Service
public class AudioService {
    // TranscriptionModel(语音转文字)/ SpeechModel(文字转语音)由 Spring AI 自动配置,直接注入
    private final TranscriptionModel transcriptionModel;
    private final SpeechModel speechModel;

    public AudioService(TranscriptionModel transcriptionModel, SpeechModel speechModel) {
        this.transcriptionModel = transcriptionModel;
        this.speechModel = speechModel;
    }

    // 语音转文字(ASR)
    public String transcribe(Resource audio) {
        return transcriptionModel.transcribe(audio);
    }

    // 文字转语音(TTS)
    public byte[] synthesize(String text) {
        return speechModel.call(new SpeechPrompt(text)).getResult().getOutput();
    }
}

10.4 多模态 RAG(知识库里的图片)

先澄清一个关键误区:把推理模型换成多模态模型(如 Qwen-VL)≠ 自动变成多模态 RAG。

TikaDocumentReader 只会提取文档文字 ,文档里的图片、图表会被直接丢弃。就算你把后端聊天模型换成 Qwen-VL,向量库里依然只有文本切片,图片信息照样丢失------因为图片压根没进库。

分清两个概念:

概念 能力 作用阶段
多模态推理模型(Qwen-VL) 接收「文本 + 图片 URL/base64」,看得懂图片,能问答、看图分析 生成回答阶段看图
多模态 RAG 入库时把文档图片/图表提取出来、向量化入库,检索时召回图片片段 入库 + 检索阶段

Spring AI 原生支持传 Media 图片对象给 Qwen-VL(见 10.1),但没有开箱即用的多模态 RAG 流水线,需要自己开发。下面两种方案是工程上的主流做法。

方案 A:图片转文字摘要(最常用、改造最小,推荐优先用)

兼容现有文本 RAG 整套链路(RetrievalAugmentationAdvisor、向量库都不用改):

  1. 文档解析时,把 docx / PDF 里的图片提取出来;
  2. Qwen-VL 描述图片/图表(如「详细描述这张图,包含图表数据、趋势,输出文本描述」);
  3. 把生成的文本摘要当成普通 Document 切片存入向量库;
  4. 查询阶段走普通文本检索,给 LLM 的上下文是图片的文字描述。
  • ✅ 优点:复用现有全部 RAG 代码,改动小、性能好;
  • ❌ 缺点:模型看不到原图,只能看文字摘要,复杂图表细节会丢失。

下面是方案 A 的完整实现,五步照抄即可:

① 依赖:提取文档内图片,docx 用 POI、PDF 用 pdfbox(Tika 提取不了内嵌图片):

xml 复制代码
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>
<dependency>
    <groupId>org.apache.pdfbox</groupId>
    <artifactId>pdfbox</artifactId>
    <version>2.0.32</version>
</dependency>

② 调 Qwen-VL 把图片转成文字摘要(输入图片二进制,输出专供检索用的文字描述):

java 复制代码
public String imageToSummary(byte[] imageBytes) {
    // 原生 Spring AI:直接把图片字节作为 Media 传给多模态 ChatModel
    return chatClient.prompt()
            .user(u -> u.text("""
                    你是文档图片解析助手。请详细描述这张图片/图表。
                    如果是图表,把数据、趋势、关键信息完整输出;
                    如果是流程图,输出流程步骤;
                    输出内容用于知识库检索,保证信息完整,不要多余客套话。
                    """)
                    .media(MimeTypeUtils.IMAGE_JPEG, imageBytes))   // 直接传图片字节,MIME 按实际图片格式填
            .options(ChatOptions.builder()
                    .model("qwen-vl-max")              // 换成你的多模态模型名
                    .build())
            .call()
            .content();
}

③ 提取 docx 里的图片 (docx 是 zip 包,图片都在 word/media/ 下):

java 复制代码
public List<byte[]> extractDocxImages(Path docxFilePath) throws IOException {
    List<byte[]> imageBytesList = new ArrayList<>();
    try (OPCPackage opcPackage = OPCPackage.open(docxFilePath.toFile())) {
        XWPFDocument document = new XWPFDocument(opcPackage);
        for (XWPFPictureData pictureData : document.getAllPictures()) {
            imageBytesList.add(pictureData.getData());
        }
    }
    return imageBytesList;
}

PDF 的图片提取用 pdfbox,遍历每一页、拿出页面内的图片字节即可。

④ 入库:文本切片 + 图片摘要一起写

java 复制代码
// 1. 普通文本切片(tikaDocs = TikaDocumentReader.get() 读出的 List<Document>)
List<Document> textDocuments = textSplitter.split(tikaDocs);

// 2. 提取文档里全部图片 → 逐张转文字摘要
List<Document> imageSummaryDocs = new ArrayList<>();
for (byte[] imgBytes : extractDocxImages(filePath)) {
    String summary = imageToSummary(imgBytes);
    Document imgDoc = new Document(summary);
    imgDoc.getMetadata().put("source_type", "image_summary");   // 标记来源是图片摘要,方便调试
    imageSummaryDocs.add(imgDoc);
}

// 3. 合并后统一入库
List<Document> allDocs = new ArrayList<>();
allDocs.addAll(textDocuments);
allDocs.addAll(imageSummaryDocs);
vectorStore.add(allDocs);

⑤ 查询阶段完全不用改 :直接复用 RetrievalAugmentationAdvisor,检索时图片摘要和普通文本一样被召回------因为向量库存的是图片的文字描述,不是原图。

方案 B:完整多模态 RAG(检索能召回原图)

  1. 入库:提取图片 → 调多模态 Embedding 生成图片向量 → 图片文件存 OSS → 向量库保存「图片向量 + 图片 URL」;
  2. 检索:支持「文本搜图片、图片搜文本」;
  3. 命中后:把图片 URL 和文本一起塞给 Qwen-VL,模型拿到真实图片直接看图回答。

⚠️ 坑:普通文本 Embedding 不能编码图片,必须用专门的多模态 Embedding;向量库要同时存「文本向量 + 图片向量」两套。

方案 B 完整实现(架构 + 代码骨架,Java + Spring AI + 阿里云百炼):

text 复制代码
入库:文档 → 提取图片(POI/PDFBox)
              ├→ 上传 OSS → 图片 URL
              └→ 多模态 Embedding → 图片向量
              → 写入向量库(图片向量 + URL 元数据)

查询:提问 → 文本向量化 → 检索向量库 → 命中图片文档(含 URL)
            → 图片 URL + 文本 传给 Qwen-VL → 看图回答
java 复制代码
// ① 入库:图片上传 OSS + 多模态向量化 + 写库
public void ingestImage(byte[] imageBytes, String docId) {
    String imageUrl = ossService.upload("knowledge/" + docId + ".png", imageBytes);  // 存 OSS,拿到 URL
    float[] imageVector = multimodalEmbedding.embedImage(imageBytes);   // 百炼多模态 Embedding 生成图片向量
    imageVectorStore.save(docId, imageVector, Map.of("url", imageUrl));  // 图片向量 + URL 元数据写库
}

// ② 检索:文本搜图片
public List<String> searchImages(String question) {
    float[] queryVector = embeddingModel.embed(question);   // 文本向量化(普通 Embedding 即可)
    return imageVectorStore.search(queryVector, 5);          // 返回命中的图片 URL 列表
}

// ③ 生成:命中图片后,把图片 URL + 文本 给 Qwen-VL 看图回答
public String answerWithImages(String question, List<String> imageUrls) {
    return chatClient.prompt()
            .user(u -> u.text(question)
                    .media(MimeTypeUtils.IMAGE_PNG, imageUrls.get(0)))   // 多张图可链式 .media() 或传 List<Media>
            .options(ChatOptions.builder().model("qwen-vl-max").build())
            .call()
            .content();
}

说明:① multimodalEmbedding.embedImage()百炼多模态 Embedding (不是 Spring AI 内置的文本 EmbeddingModel,文本 Embedding 编码不了图片);② ossService 用阿里云 OSS SDK;③ imageVectorStore 需要向量库支持存图片向量(普通 pgvector 只存文本向量,通常单独建一张图片向量表或存两套向量)。这三处正是完整多模态 RAG 比方案 A 多出来的、成本高的部分,也是它生产用得少的原因。

选型结论 :现实中绝大多数企业 RAG 项目优先用「图片转文字摘要」方案(方案 A);完整多模态 RAG(检索出图片、传给 VL 看原图)在生产用得不多------成本高、坑多,先用方案 A 够用,再考虑升级。

生产要区分两种情况

  1. 用户对话时上传图片 (聊天框上传)------这是对话侧多模态 ,直接用 Media 传给 Qwen-VL(见 10.1),不走向量库、不属于 RAG 知识库。用户临时传一张图提问,直接给 VL 看就行。

  2. 知识库文档内部自带图片 (PDF/Word 里的图)------优先图片摘要方案(方案 A),不要指望多模态模型自动识别,因为 Tika 只提取文字、图片压根没进库。

生产注意事项

  1. 千万不要同步做「文档解析 + VL 调用」------文件上传接口必须异步化,否则大文件直接超时;
  2. VL 调用有成本:图片多 token 消耗很高,要做限流、开关、失败降级;
  3. Tika 提取不了内嵌图片,docx 用 POI、PDF 用 PDFBox;
  4. 普通 Embedding 不能编码图片------方案 A 只存图片的文字摘要,不需要多模态 Embedding;
  5. 完整多模态 RAG 开发维护成本高、不是必选项,优先图片摘要够用再考虑升级。

一句话:Qwen-VL 这类多模态模型只是推理侧支持图文输入,直接替换模型不会自动实现多模态 RAG;知识库图片要「进库」,必须走「图片转文字摘要」或「多模态 Embedding 完整方案」。


第 11 章 MCP(Model Context Protocol)

MCP 是一个开放的「模型-工具」标准协议,让 AI 应用能按统一方式接入海量现成工具服务(数据库、GitHub、文件系统等)。Spring AI 1.1 深度集成 MCP。

MCP 协议提供三类能力:

能力 说明
tools 可调用的工具(查数据库、读写文件等),Spring AI 主要集成这一类
resources 资源(文件内容、数据库 schema),供客户端读取
prompts 提示词模板

社区已有数千个现成 MCP server(GitHub、PostgreSQL、Slack 等),按统一协议接入成本极低。

Spring AI 当前完整支持 tools (自动转成 ToolCallback 参与 Function Calling);resources 和 prompts 没有自动集成 ,需要手动用 McpSyncClient 调用(见 11.2)。

11.1 客户端:把 MCP 工具自动转成 ToolCallback

引入客户端 starter:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

配置 MCP 服务器连接(STDIO / SSE 两种传输):

yaml 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        toolcallback:
          enabled: true          # 自动把 MCP 工具转成 ToolCallback
        # STDIO 方式:本地启动一个 MCP server 进程
        stdio:
          connections:
            filesystem:
              command: npx
              args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
        # SSE 方式:连接远程 MCP server
        # sse:
        #   connections:
        #     remote:
        #       url: http://localhost:8080
        # Streamable HTTP 方式(生产推荐)
        # streamable-http:
        #   connections:
        #     remote:
        #       url: http://localhost:8080
        #       endpoint: /mcp        # 可选,默认 /mcp

传输方式选择:

传输 说明 适用
STDIO 本地进程,通过 stdin/stdout 通信 本地工具、开发调试
SSE 基于 HTTP 的远程服务 远程 MCP server
Streamable HTTP 1.1 新增的新一代传输 生产环境更推荐

本地工具用 STDIO 最简单;生产连远程 server 优先考虑 Streamable HTTP。MCP 客户端是懒连接 的------第一次调用工具时才建立连接、初始化握手;enabled 控制是否启用客户端,toolcallback.enabled 控制是否把 MCP 工具自动暴露成 ToolCallback

注入 ToolCallbackProvider 即可使用:

java 复制代码
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.chat.client.ChatClient;

@Bean
CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
    return args -> {
        String answer = builder.build().prompt()
                .user("列出 /tmp 目录下有哪些文件")
                .toolCallbacks(mcpTools)   // 注入 MCP 工具
                .call()
                .content();
        System.out.println(answer);
    };
}

注意:接入多个 MCP server 时,工具可能和本地 @Tool 重名、或互相重名。Spring AI 提供 McpToolNamePrefixGenerator(给 MCP 工具统一加前缀)和 McpToolFilter(按需过滤掉不想要的工具),多 server 接入时按需配置。

11.2 手动用法:McpSyncClient(读 resources / prompts)

自动转 ToolCallback 只覆盖 tools 能力;要读 resources (文件、数据库 schema)或 prompts (提示词模板),需要用 McpSyncClient(同步)或 McpAsyncClient(异步):

java 复制代码
// import 来自 MCP SDK(Spring AI 封装,类名 McpClient / McpSyncClient / McpSchema / HttpClientStreamableHttpTransport)

// 1. 建一个 Streamable HTTP 传输,连到 MCP server
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
        .builder("http://localhost:8080")   // MCP server 地址
        .build();

// 2. 建同步客户端并初始化握手
McpSyncClient client = McpClient.sync(transport)
        .requestTimeout(Duration.ofSeconds(60))
        .build();
client.initialize();

// 3. 列工具 / 调工具
McpSchema.ListToolsResult tools = client.listTools();
McpSchema.CallToolResult result = client.callTool(
        McpSchema.CallToolRequest.builder("get-weather")
                .arguments(Map.of("city", "西安"))
                .build());

// 4. 读资源(自动转 ToolCallback 覆盖不到的能力)
McpSchema.ListResourcesResult resources = client.listResources();
McpSchema.ReadResourceResult resource = client.readResource(
        McpSchema.ReadResourceRequest.builder("resource://uri").build());

// 5. 用完关闭
client.closeGracefully();

关键:① McpClient.sync(transport) 建同步客户端,异步用 McpClient.async(...) 返回 McpAsyncClient;② listTools / listResources / listPrompts 都支持分页(传 cursor,循环到 nextCursor 为空);③ 大多数场景用 11.1 的自动 ToolCallback 就够了,只有要读 resources / prompts、或精细控制连接时才走到这一步。

11.3 服务端:暴露你自己的 MCP 工具

服务端 starter 有三种:spring-ai-starter-mcp-server(STDIO)、...-server-webmvc(SSE / Streamable HTTP)、...-server-webflux(响应式)。

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

@Tool 标注的方法即可被自动暴露为 MCP 工具(与第 7 章 Function Calling 复用同一套注解),配置服务端传输协议:

yaml 复制代码
spring:
  ai:
    mcp:
      server:
        protocol: STREAMABLE   # 服务端传输协议:SSE / STREAMABLE

生产安全:对外暴露 MCP server 时要做鉴权(网关层加 API Key / OAuth 等),MCP 协议本身不带认证,别把工具裸暴露到公网。

11.4 MCP 与 Function Calling 的关系

  • Function Calling :你的应用内直接定义的 @Tool 方法。
  • MCP :通过标准协议接入的外部/第三方 工具,经 ToolCallbackProvider 自动转成同样的 ToolCallback,最终都走同一条工具调用链路。

第 12 章 可观测性与评测

12.1 可观测性

Spring AI 内置 Micrometer 观测埋点,接入 Actuator 与 OpenTelemetry 即可采集指标与链路:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
yaml 复制代码
spring:
  ai:
    chat:
      client:
        observations:
          include-prompt: true        # 是否在观测中记录提示词(含敏感信息需谨慎)
          include-completion: true    # 是否记录补全结果
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

接入后,能采集到两类信息:

  • 指标(Metrics):token 用量、调用耗时、调用次数、错误率等;
  • 链路(Tracing) :一次请求的完整调用链------ChatModel 调用 → 工具调用 → 向量检索 → 生成,定位慢在哪一步、错在哪一步。

和日志(第 7 章「全链路日志调试」)的关系:日志是「出问题时手动翻」的排查手段,观测是「系统化自动采集」的指标/链路,两者互补------日常看指标,出问题看日志 + 链路。

12.2 评测

评测是离线 的质量评估(区别于 12.1 的运行时观测):构造带标准答案的数据集,打分后迭代提示词/工具/检索参数。评测要分两个维度分开测:

维度 测什么 常见指标
检索质量 该命中的文档有没有被召回 Recall@kMRRHit Rate
生成质量 拿到资料后答没答对、有没有幻觉 Faithfulness(忠实度)、Answer Relevance(相关性)、Correctness(正确性)

关键:先测检索、再测生成------检索错了,生成一定错(呼应第 9.6 章「八成问题出在检索侧」)。

自动化评分基线:Spring AI 生态可结合 EmbeddingModel 算「回答 vs 标准答案」的语义相似度做近似打分(快、但只是基线,不能替代人工标注);更完整的评测可用 RAGAS、LangSmith、DeepEval 等框架。

三层能力递进:调试 (第 7 章日志)→ 观测 (12.1 指标/链路)→ 评测(12.2 打分迭代),分别对应「出问题排错 → 线上看运行 → 离线迭代质量」。


第 13 章 参数调整难点

参数调优是 AI 应用「从能跑到好用」的关键。本章把散落在各环节的关键参数集中梳理,给出调优方向。

13.1 模型生成参数

控制模型「怎么生成」,直接影响输出质量。以 OpenAI 为例,配置前缀 spring.ai.openai.chat.options.*(本地 Ollama 对应 spring.ai.ollama.chat.options.*),也可在代码里用 OpenAiChatOptions.builder() 按次覆盖:

参数 说明 调优方向
temperature 随机性(0~2),越低越确定 代码/SQL/提取用 00.3;创意用 0.71.0
top-p 核采样(0~1),候选词累计概率 与 temperature 二选一调,勿同时大幅调
top-k 只从概率前 k 个词采样 降低可减少跑题(部分模型支持)
max-tokens 输出长度上限 防超长/截断,按需设置
frequency-penalty 惩罚已出现词,抑制重复 出现复读/重复时调大
presence-penalty 惩罚出现过的词,鼓励新话题 与 frequency-penalty 配合
stop 命中即停止的字符串 需精确控制结尾时使用
seed 随机种子 需要结果可复现时设置

经验:「乱码/复读」先降 temperature 并加 frequency-penalty;「答非所问/不精确」先降 temperature 并写清提示词。多数问题先改提示词,再动参数。

13.2 网络 & 重试参数

大模型 API 常有瞬时失败(限流 429、网关 502/503/504、超时)。Spring AI 内置重试,前缀 spring.ai.retry.*

参数 默认值 说明
max-attempts 10 最大重试次数
on-client-errors false 为 false 时 4xx 不重试(视为不可重试)
on-http-codes 强制触发重试的状态码,如 429,502,503,504
exclude-on-http-codes 明确不重试的状态码,如 401,403,404
backoff.initial-interval 2s 首次重试等待
backoff.multiplier 5 指数退避倍数
backoff.max-interval 3m 退避上限
yaml 复制代码
spring:
  ai:
    retry:
      max-attempts: 5
      on-http-codes: 429,503,502,504
      exclude-on-http-codes: 401,403,400,404
      backoff:
        initial-interval: 2s
        multiplier: 5
        max-interval: 3m

退避公式:min(initial-interval × multiplier^(次数-1), max-interval)。 要点:4xx 客户端错误重试无意义 (401 鉴权失败、404 模型不存在),默认不重试;429/5xx 才值得重试 。连接超时等网络异常抛出 ResourceAccessException,也会被自动重试。

13.3 Function-Calling 工具调用参数

工具调用的核心开关是 internalToolExecutionEnabled(内部工具执行模式):

参数 取值 说明
internalToolExecutionEnabled true(默认) 自动模式:Spring AI 内部完成「调模型→执行工具→回传→再调模型」多轮循环,一次 .call() 搞定
internalToolExecutionEnabled false 手动模式:由你控制多轮工具调用,适合复杂 Agent、条件分支、逐步调试
toolChoice auto / none / 指定 强制、禁止或指定工具选择
@Tool(description) --- 直接决定模型选哪个工具,务必写清触发条件
java 复制代码
import org.springframework.ai.openai.OpenAiChatOptions;

// 手动模式:自己编排多轮工具调用(复杂 Agent 场景)
OpenAiChatOptions options = OpenAiChatOptions.builder()
        .internalToolExecutionEnabled(false)
        .build();

要点:① 简单场景用默认自动模式即可;② 复杂多轮工具编排、需要自定义错误处理/条件分支时,切手动模式自己编排;③ 工具描述质量比参数更重要------描述不清,模型会选错工具或传错参。

13.4 RAG 向量库参数

RAG 效果对参数极敏感,重点在「分块」和「检索」:

参数 说明 调优方向
chunkSize 分块 token 数 最关键:太大稀释语义、太小丢上下文
chunkOverlap 块间重叠 token(默认 50) 取 chunkSize 的 10%~20%,保跨块连贯
SearchRequest.topK 检索返回条数 太大引入噪声、太小漏信息,常取 3~8
SearchRequest.similarityThreshold 相似度阈值 过低召回无关内容,过高漏检,按数据实测
SearchRequest.filterExpression 元数据过滤 按 source 等过滤,缩小检索范围
index-type HNSW / IVFFLAT 生产默认 HNSW,数据量极大可试 IVFFLAT
distance-type 余弦/欧氏/内积 归一化嵌入用 NEGATIVE_INNER_PRODUCT 更优
dimensions 向量维度 必须与嵌入模型一致

经验:RAG 效果不好,先调分块(chunkSize/chunkOverlap),再调检索(topK/threshold),最后才考虑换嵌入模型或加重排。

13.5 框架自身 Advisor / 记忆参数

Advisor 与记忆的调优集中在「顺序」和「窗口」:

参数 说明 调优方向
MessageWindowChatMemory.maxMessages 记忆窗口(条数) 太大 token 超限、太小丢上下文
MessageChatMemoryAdvisor conversationId 会话隔离 id 必须按请求覆盖,禁止写死
Advisor order 执行顺序 记忆→RAG→日志
spring.ai.chat.client.observations.include-prompt 是否记录提示词 含敏感信息时关闭
RetrievalAugmentationAdvisor.allowEmptyContext 空上下文是否作答 检索为空时避免幻觉可关闭
java 复制代码
ChatMemory memory = MessageWindowChatMemory.builder()
        .maxMessages(10)   // 按「条数」粗粒度控制窗口
        .build();

要点:① 记忆窗口按「条数」截断是粗粒度,需按 token 精确控制要自行实现计数截断;② Advisor 顺序错误会导致历史未注入或检索未生效;③ 长期记忆(VectorStoreChatMemoryAdvisor)注意 prompt 注入风险。


第五部分 · 速查

第 14 章 关键 API 与供应商切换速查表

14.1 关键 API 速查

按「使用链路」分类,每个类标注干什么 + 关键方法怎么用

① 聊天入口:ChatClient

java 复制代码
ChatClient client = ChatClient.builder(chatModel).build();   // 或用容器里的 ChatClient.Builder 注入

client.prompt()
        .system("你是......")                    // system 角色
        .user("......")                          // 用户输入
        .user(u -> u.text("......")              // 多模态输入(图片)
                .media(MimeTypeUtils.IMAGE_PNG, resource))
        .options(chatOptions)                 // 覆盖模型参数
        .advisors(advisor)                    // 挂 Advisor(记忆/RAG/日志)
        .tools(toolObj)                       // 挂工具
        .toolContext(ctx)                     // 注入 ToolContext
        // ------ 执行 + 取结果(四选一)------
        .call().content();                    // 同步 → String
        .call().entity(Person.class);         // 同步 → 反序列化对象
        .call().chatResponse();               // 同步 → 完整 ChatResponse(含 token 用量)
        .stream().content();                  // 流式 → Flux<String>

String.content();拿强类型对象用 .entity();要 token 用量/元数据用 .chatResponse()

② 核心数据模型

java 复制代码
// Message 体系:SystemMessage / UserMessage / AssistantMessage
Message msg = new UserMessage("你好");

// ChatResponse:完整响应(上面 .chatResponse() 返回)
ChatResponse resp = client.prompt().user("......").call().chatResponse();
AssistantMessage out = resp.getResult().getOutput();   // 模型输出消息
resp.getResult().getMetadata().getFinishReason();      // 结束原因(STOP/TOOL_CALL...)
resp.getMetadata().getUsage();                         // token 用量

// ChatOptions:模型参数(第 13 章)
ChatOptions options = ChatOptions.builder()
        .model("qwen2.5").temperature(0.7).topP(0.8).maxTokens(1000)
        .toolCallMaxIterations(3)                       // 工具连环调用上限
        .build();

③ 结构化输出

java 复制代码
// 1.1 推荐:.entity() 原生反序列化
Person p = client.prompt().user("......").call().entity(Person.class);

// 泛型(List/Map)必须用 ParameterizedTypeReference,不能用 List.class
List<Person> ps = client.prompt().user("......").call()
        .entity(new ParameterizedTypeReference<List<Person>>() {});

// 旧方式:BeanOutputConverter 手动转换(了解即可)
var conv = new BeanOutputConverter<>(new ParameterizedTypeReference<List<Person>>() {});
List<Person> ps2 = conv.convert(client.prompt().user("......").call().content());

④ 提示词模板:PromptTemplate

java 复制代码
PromptTemplate tpl = new PromptTemplate("把这段翻译成 {lang}:{text}");
String rendered = tpl.render(Map.of("lang", "英文", "text", "你好"));   // 渲染成 String
Prompt prompt = tpl.create(Map.of("lang", "英文", "text", "你好"));     // 渲染成 Prompt 对象

⑤ 对话记忆

java 复制代码
// 内存窗口记忆(最常用)
ChatMemory memory = MessageWindowChatMemory.builder().maxMessages(10).build();
client = builder.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build()).build();

// 长期记忆(写入向量库,语义检索历史)
VectorStoreChatMemoryAdvisor.builder(vectorStore).build();

// 手动 API:预置 / 读取 / 清空
memory.add(conversationId, List.of(new UserMessage("......")));
List<Message> history = memory.get(conversationId);
memory.clear(conversationId);

⑥ Function Calling 工具

java 复制代码
// 定义工具:@Tool / @ToolParam 标注方法
@Tool(description = "查询天气")
public String getWeather(@ToolParam(description = "城市") String city) { ... }

// 注册:默认(所有请求)或按请求
builder.defaultTools(weatherTools);
client.prompt().user("......").tools(weatherTools).call().content();

// 服务端注入上下文(不进模型,防 prompt 注入)
ToolContext ctx = ToolContext.builder().with("tenantId", "t1").build();
client.prompt().user("......").toolContext(ctx).call().content();

// 限制连环调用次数(防死循环)
ChatOptions.builder().toolCallMaxIterations(3).build();

⑦ 向量库与 RAG

java 复制代码
// 写入 / 检索 / 删除
vectorStore.add(List.of(new Document("文本", Map.of("source", "x.md"))));
List<Document> hits = vectorStore.similaritySearch(
        SearchRequest.builder().query("......").topK(5).similarityThreshold(0.5).build());
vectorStore.delete(List.of("doc-1"));

// Document 四字段:id / text / metadata / score
Document.builder().id("1").text("......").metadata(...).score(0.9).build();

// 嵌入
float[] vec = embeddingModel.embed("一段文本");                       // 单条 → float[]
EmbeddingResponse er = embeddingModel.embedForResponse(List.of("a","b")); // 批量

// 朴素 RAG
QuestionAnswerAdvisor.builder(vectorStore)
        .searchRequest(SearchRequest.builder().topK(5).similarityThreshold(0.45).build())
        .build();

// 模块化 RAG(查询改写 + 检索 + 增强)
RetrievalAugmentationAdvisor.builder()
        .documentRetriever(VectorStoreDocumentRetriever.builder().vectorStore(vectorStore).build())
        .queryAugmenter(ContextualQueryAugmenter.builder().allowEmptyContext(false).build())
        .build();

⑧ 多模态

java 复制代码
// 视觉输入:user() 里挂 media,或 UserMessage + Media
client.prompt().user(u -> u.text("描述图片").media(MimeTypeUtils.IMAGE_PNG, resource)).call().content();

// 文生图
ImageResponse imgResp = imageModel.call(new ImagePrompt("一只金毛"));
String url = imgResp.getResult().getOutput().getUrl();

// 语音:接口由厂商实现(OpenAI 为 OpenAiAudio* 系列)
SpeechModel / SpeechPrompt / SpeechResponse            // 文字 → 语音(TTS)
TranscriptionModel / AudioTranscriptionPrompt          // 语音 → 文字(STT)

⑨ MCP

java 复制代码
// 配置 spring.ai.mcp.client.* 后,直接注入已转好的工具(ToolCallbackProvider)
@Bean CommandLineRunner demo(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {
    return args -> builder.build().prompt()
            .user("列出 /tmp 下的文件")
            .toolCallbacks(mcpTools)          // MCP 工具已自动转成 ToolCallback
            .call().content();
}

⑩ Advisor 扩展机制

java 复制代码
// 自定义 Advisor:aroundCall 包裹「调用模型」这一步(前置看请求、后置看响应)
public class MyAdvisor implements CallAroundAdvisor {
    public AdvisedResponse aroundCall(AdvisedRequest req, CallAroundAdvisorChain chain) {
        req.request().messages();                     // 前置:发给模型的全部消息
        AdvisedResponse resp = chain.nextAroundCall(req);
        resp.response();                              // 后置:模型原始返回
        return resp;
    }
    public String getName() { return "MyAdvisor"; }
    public int getOrder() { return 0; }               // 数值越小越靠外层
}

// 内置 Advisor:MessageChatMemoryAdvisor(记忆)、QuestionAnswerAdvisor(RAG)、
//              SafeGuardAdvisor(防注入)、SimpleLoggerAdvisor(日志)

14.2 模型来源切换对照表

模型来源 starter 依赖 配置前缀 模型名示例 需 API Key
Ollama(本地) spring-ai-starter-model-ollama spring.ai.ollama.* qwen2.5 / llama3.2
OpenAI(云端) spring-ai-starter-model-openai spring.ai.openai.* gpt-4o-mini / gpt-4o
Anthropic(云端) spring-ai-starter-model-anthropic spring.ai.anthropic.* claude-sonnet-4-5

切换方式:换依赖 + 改 spring.ai.model.chat + 填对应配置前缀即可,业务代码零改动。

国内云端大模型:智谱(spring-ai-starter-model-zhipuai)、月之暗面 Kimi(spring-ai-starter-model-moonshot)、百度千帆(spring-ai-starter-model-qianfan)等亦有官方 starter;阿里云通义千问 DashScope 由 Spring AI Alibaba 生态(spring-ai-alibaba-starter-dashscope)提供,用法与上文一致。

14.3 application.yml 关键配置

按主题汇总常用配置(按需取用,不需要的删掉即可):

yaml 复制代码
spring:
  ai:
    # ① 模型选择:多 starter 并存时决定激活哪个(换供应商只改这里 + 换依赖)
    model:
      chat: ollama            # ollama / openai / anthropic / none
      embedding: ollama

    # ② 本地 Ollama
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5
          temperature: 0.7
      embedding:
        options:
          model: nomic-embed-text      # 768 维

    # ③ 云端 OpenAI(Anthropic 类似,前缀 spring.ai.anthropic.*)
    openai:
      api-key: ${OPENAI_API_KEY}       # 从环境变量读,不要写死
      base-url: https://...            # 兼容端点(阿里百炼等),官方地址可省略
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.7
      embedding:
        options:
          model: text-embedding-3-small   # 1536 维

    # ④ 重试(429/5xx 瞬时失败自动重试)
    retry:
      max-attempts: 5
      on-http-codes: 429,502,503,504
      exclude-on-http-codes: 401,403,400,404
      backoff:
        initial-interval: 2s
        multiplier: 5
        max-interval: 3m

    # ⑤ 工具调用(Function Calling)
    tools:
      throw-exception-on-error: false  # false:异常回传模型;true:抛给调用方

    # ⑥ 向量库 pgvector
    vectorstore:
      pgvector:
        initialize-schema: true
        index-type: HNSW                 # HNSW / IVFFLAT / NONE
        distance-type: COSINE_DISTANCE   # COSINE / EUCLIDEAN / NEGATIVE_INNER_PRODUCT
        dimensions: 768                  # 必须与嵌入模型维度一致

    # ⑦ MCP 客户端
    mcp:
      client:
        enabled: true
        toolcallback:
          enabled: true                  # 自动把 MCP 工具转成 ToolCallback
        stdio:
          connections:
            filesystem:
              command: npx
              args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]

  # ⑧ pgvector 依赖的数据源
  datasource:
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: postgres

记住三点:① spring.ai.model.chat/embedding 决定激活哪个供应商;② 每个供应商在自己的 spring.ai.<provider>.* 下配 key / base-url / model;③ 重试、工具、向量库、MCP 各有独立前缀。切换供应商 = 换 starter + 改 model.chat + 填对应前缀,业务代码零改动。

14.4 嵌入模型维度对照

嵌入模型 维度
nomic-embed-text(Ollama 本地) 768
text-embedding-3-small(OpenAI) 1536
text-embedding-3-large(OpenAI) 3072
text-embedding-ada-002(OpenAI) 1536

配置 pgvector 等向量库的 dimensions 时,务必与所选嵌入模型维度一致。


结语

本文按「快速开始 → 核心能力 → 动手与找资料 → 进阶主题 → 速查」的顺序,覆盖了 Spring AI 1.1.2 的核心能力:从零搭建对话机器人、Advisors 扩展机制、提示词与结构化输出、对话记忆、Function Calling、向量数据库与 RAG,再到多模态、MCP 与可观测性。

学习建议 :先用本地 Ollama 把第 2 章跑通,再逐章叠加第 6(记忆)、7(工具)、8-9(RAG)章能力;进阶部分(10-13 章)可按需查阅。所有能力都收敛在 ChatClient 一个入口上,掌握它即可举一反三。

相关推荐
Flynt2 小时前
我给 Claude Code 装了 Ponytail,代码量直接砍了一半
agent·ai编程·claude
ServBay2 小时前
谷歌发布 Gemini 3.8 Flash,官方跑分很厉害,但又被嘲了?
aigc·ai编程·gemini
全栈弄潮儿5 小时前
AI 编程进阶实战:我为什么要做这个专栏
chatgpt·openai·ai编程
晴天小庭5 小时前
Skynet AI 论坛邀请你参加一场AI觉醒实验
aigc·openai·ai编程
打呵欠的猫6 小时前
让 AI 帮你写 Git Commit Message:从"fix bug"到语义化提交只需一个 Hook
前端·ai编程
桃西西呀6 小时前
同一个模型 30% 到 100%?拆解 Harness 工程的 5 个机制,附 8 个坑的自检清单
人工智能·llm·ai编程
Behavior6 小时前
Meta 发布 Muse Glimmer:30B 参数、一张 24GB 显卡就能常驻的本地 Agent 模型
llm·aigc·ai编程
吴佳浩6 小时前
为什么现在越来越多的开源模型,都“毕业“于 Qwen?
人工智能·llm·ai编程
小虎AI生活6 小时前
FDE 爆火背后:AI 落地最后一公里的工程化拆解(附真实案例与四步方法论)
ai编程