Spring AI 应用开发实践课程教导站 · Spring AI 2.0 课程首页
课程首页 / 附录

APPENDIX速查手册与排错表

这一页是你学习过程中最常回来翻的。命令、配置模板、异常对照、术语解释,都集中在这里。它也是一份备课级速查手册:上课前用它核对 API 写法是否落在 2.0 约定内,实验/答辩前用它做自查。建议把本页锚点存进收藏夹。

定位
课后/实验/答辩随身速查
教学重点
2.0 破坏变更与排错路径
教学难点
旧教程写法与 2.0 混淆
建议用法
打印贴工位 + 答辩前自查

教学设计说明(教师参考):本附录不进讲课主线,但要求学生在第 1 章起就打开对照。两个检查点:① 学生抄配置/代码前,先确认用的是 2.0 约定(conversationId 必填、ToolCallingAdvisor 接管循环、FunctionToolCallback、entity()、@McpTool、embedding 无 .options),旧博客的 1.x 写法一律判错;② 实验与答辩遇到报错,先要求学生在 A.3 排错表里按关键词定位,再允许提问,培养「先查手册」的习惯。本页也是教学审核的对照基准:凡与下表不一致的写法,视为超出本课程版本边界。

本附录的版本边界:全部示例基于 Spring AI 2.0.1 + Spring Boot 4.1.x + Java 17,包名 com.example.springai,端口 8080。凡是 1.x 写法(如 PromptChatMemoryAdvisor、toolNames()、internalToolExecutionEnabled()、Embedding 的 .options 层)在本课程中一律视为过时、不可用。下面所有「什么时候用」都针对 2.0 写法。

A.0Spring AI 1.x → 2.0 破坏性变更速查表

这是全站最应该打印出来贴在显示器旁的一页。2.0 是架构级重构,下面每一行都是「照抄旧代码会出问题」的地方。左列是 1.x 的写法,右列是 2.0 的正确写法。考试/答辩里凡出现左列写法,直接判为使用了过时 API。

什么时候用这张表:当你从网上/旧项目复制了一段 Spring AI 代码却编译不过或运行报「找不到类/方法」时,先回这里逐行对照;也用于把 1.x 工程迁移到 2.0 前的差异盘点。

一、底座与依赖

项1.x2.0
Spring Boot3.x4.0 / 4.1(与 3.x 不兼容,不能混用)
Spring Framework6.x7.0,Jakarta EE 11
Java17+17+(官方支持到 26)。
Java 21 只是从源码构建 Spring AI 时的要求,不是运行时要求
JacksonJackson 2Jackson 3(包名 com.fasterxml.jackson → tools.jackson,日期序列化默认行为也变了)
RAG Advisor 模块spring-ai-advisors-vector-storespring-ai-vector-store-advisor(改名)
MCP 传输模块 groupIdio.modelcontextprotocol.sdkorg.springframework.ai
MCP Java SDK0.18.x1.0.x 起(随 2.0.0 发布)
已移除的模块Azure OpenAI / openai-sdk / OCI GenAI / Vertex / CosmosDB / Hanadb移出主仓库,改由厂商或外置仓库维护
模型供应商(核心)十几种,各自实现收敛为 7 家:OpenAI、Anthropic、Bedrock、Google GenAI、Mistral、DeepSeek、Ollama。
OpenAI / Anthropic / Google 改用厂商官方 SDK
不再内置的供应商智谱 AI、Azure OpenAI、OCI、Vertex智谱需引其独立仓库;Azure OpenAI 并入 spring-ai-openai;MiniMax 走 Anthropic 兼容协议
什么时候用这段:新建工程或在 start.spring.io 选型时,确认 Boot 选 4.1.x、Java 选 17,依赖坐标别引到已移除的模块(如 spring-ai-advisors-vector-store)。

二、工具调用(改动最大的一块)

项1.x2.0
执行循环在哪 藏在每个 ChatModel 实现内部,无法干预 ToolCallingAdvisor,Advisor 链中的一等公民,可观察、可组合、可替换
关闭自动执行 .internalToolExecutionEnabled(false)
属性 spring.ai.*.chat.internal-tool-execution-enabled
两者都已删除。改用:
单次 → .advisors(AdvisorParams.toolCallingAdvisorAutoRegister(false))
全局 → spring.ai.chat.client.tool-calling.enabled=false
函数式工具注册 裸 Function bean + .toolNames("name")
(SpringBeanToolCallbackResolver 按名字解析)
toolNames() 已删除。必须注册显式 ToolCallback:
FunctionToolCallback.builder(name, fn).inputType(X.class).build()
FunctionCallback存在已彻底移除(1.0 起由 ToolCallback 取代)
ToolCallAdvisor类名改名为 ToolCallingAdvisor
Advisor 数量约束无链中只能有一个 ToolAdvisor,重复注册直接报错
工具调用次数上限无(可无限调用)默认生效:单工具 40 次 / 合计 150 次,可用 spring.ai.tools.limits.* 调整
工具解析回退默认开启默认关闭:只有请求里显式附带的工具可执行(安全默认值)
@Tool 方法可见性有约束放宽:public / package-private / private、静态或实例方法都可
工具 span 名tool_call <名>execute_tool <名>;gen_ai.operation.name 从 framework 改为 execute_tool
★ 按 span 名配的告警规则会静默失效,升级后必须回归
2.0 正确写法速记:函数式工具用 FunctionToolCallback.builder(name, fn).inputType(X.class).build() 得到 ToolCallback,再 .tools(callback) 挂上去;工具循环由 ToolCallingAdvisor 自动驱动;想自己写 Agent 循环就 AdvisorParams.toolCallingAdvisorAutoRegister(false) 关掉自动执行。凡是看到 .toolNames(...) 或 internalToolExecutionEnabled(...) 都是 1.x,编译不过。

三、结构化输出

项1.x2.0
调用入口 ChatClient 配 OutputConverter / BeanOutputConverter 自己转 .entity(X.class):prompt().user(...).call().entity(Order.class)
校验与重试 自己写 try/catch + 重试 + 把错误回喂模型 .entity(X.class, spec -> spec.validateSchema())
框架自动带错误信息重试,默认最多 3 次
厂商原生约束 无统一入口 spec.useProviderStructuredOutput():把 schema 作为 API 级约束发给厂商,比写在提示词里硬
BeanOutputConverter有 postProcessSchema(JsonNode) 扩展点该扩展点已移除;schema 生成改由 JsonSchemaGenerator,会带 OpenAPI 风格 format 提示
可选字段判定Kotlin 可空字段会被当必填已修正;@JsonProperty(required=false) 也不再算必填
什么时候用这段:需要模型返回 JSON / 对象时,优先用 .entity(类) 而不是手写正则抠文本;要硬约束字段格式就用 useProviderStructuredOutput();要防模型返回非法值就加 validateSchema()。

四、会话记忆

项1.x2.0
conversationId 有默认值(ChatMemory.DEFAULT_CONVERSATION_ID = "default"),忘了传会静默混用 必填。每次调用必须显式传 .param(ChatMemory.CONVERSATION_ID, id),缺失或传 null 立刻抛 IllegalArgumentException。
注意区分两个东西:参数键常量 CONVERSATION_ID 仍然存在、必须使用; 被删除的是默认值常量 DEFAULT_CONVERSATION_ID(2.0 不再提供「不传就用 default」这条退路,正是为了避免多用户静默串话)
Advisor builder.conversationId("x")该方法已删除,改为每次调用传参
PromptChatMemoryAdvisor存在(省 Token,信息有损)整个类被移除,统一用 MessageChatMemoryAdvisor
默认顺序常量DEFAULT_CHAT_MEMORY_PRECEDENCE_ORDER = HIGHEST_PRECEDENCE + 1000改为 + 200;ToolCallingAdvisor 在 + 300
默认记忆在工具循环之外,看不到中间工具消息
JDBC 记忆表结构靠 timestamp 排序新增 sequence_id BIGINT 列与索引(MySQL 时间戳精度只到秒,同秒消息排序不确定)。旧库需回填
替代方案—社区项目 spring-ai-session 用事件溯源重做记忆,支持全部消息类型与自动压缩,计划 2.1 替换内置实现
2.0 记忆正确写法:chatClient.prompt().advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)).user(msg).call()...; 记忆 Advisor 用 MessageChatMemoryAdvisor。不要再写 PromptChatMemoryAdvisor,那个类在 2.0 已经被删。

五、配置与 Options

项1.x2.0
.options 层级 spring.ai.openai.embedding.options.model 已移除:spring.ai.openai.embedding.model
范围:embedding / image / audio / moderation / OCR。chat 层仍保留 .options
Ollama 思考开关spring.ai.ollama.chat.think-optionspring.ai.ollama.chat.think
Options 可变性 有 setter、copy()、*Options#fromOptions() 严格不可变,必须用 builder;改值用 .mutate()。
集合返回不可修改视图,改它抛 UnsupportedOperationException
Options 合并ModelOptionUtils.merge()移到 ChatClient 层:ChatClient.Builder#combineWith()
ChatModel 取默认选项getDefaultOptions()弃用,改 getOptions()
默认 temperature模型实现里内置默认值默认值被移除,需要时显式配置,否则可能报错或行为变化
OpenAI 默认模型gpt-4o 系列默认改为 gpt-5-mini,依赖隐式默认值的代码行为会变
JSON 工具类ModelOptionsUtils 的 JSON 方法、JsonParser 改用 JsonHelper / JacksonUtils / JsonSchemaUtils
什么时候用这段:写 application.yml 时,embedding/image/audio/moderation/OCR 的模型名直接写在供应商下一级,不要再套 .options。只有 chat.options 保留。默认 temperature 被移除,temperature 必须显式给。

六、MCP

项1.x2.0
服务端注解用 @Tool 暴露@McpTool / @McpToolParam / @McpResource / @McpPrompt / @McpComplete,注解扫描自动注册
注解包路径org.springaicommunity.mcp.annotation.*org.springframework.ai.mcp.annotation.*
默认传输SSEStreamable HTTP(SSE 已 deprecated,STDIO 用于本地)
客户端配置写法外部 JSON + servers-configurationspring.ai.mcp.client.stdio.connections.<名>.command/args
工具是否自动可用较宽松不会自动注册到 ChatClient,必须显式传 SyncMcpToolCallbackProvider 给 .tools() / .defaultTools()
服务端入参校验默认关闭默认开启,失败返回 isError=true;用 validateToolInputs(false) 关闭
暴露面控制—新增 McpToolFilter bean;spring.ai.mcp.server.expose-mcp-client-tools
安全与观测需自己接内置 Micrometer span / OpenTelemetry 指标;OAuth 2.0 与 API Key 通过 mcp-security 项目
2.0 MCP 正确写法:服务端用 @McpTool 标注方法;客户端侧必须自己 new 一个 SyncMcpToolCallbackProvider 并显式传给 .tools(provider)。MCP 工具不会自动冒出来,报「工具列表为空」十有八九是忘了这一步。

七、2.0 新增、值得专门学的三个能力

① 渐进式工具披露

ToolSearchToolCallingAdvisor。工具上百个时不再全量塞给模型, 而是给它一个 toolSearchTool 按需检索。
官方实测省 34%~64% token,准确率无可测下降。 加 starter + 一行配置即可启用(第 7 章)。

② 结构化思考

AugmentedToolCallbackProvider + AgentThinking。 给工具 schema 动态追加 innerThought 字段, 模型调工具时顺便把推理填进来,你的代码拿到的是类型安全对象, 不用再从回复文本里用正则抠(第 9 章)。

③ 可组合的循环钩子

继承 ToolCallingAdvisor 重写 doInitializeLoop / doBeforeCall / doAfterCall / doFinalizeLoop, 就能实现审批门、预算闸、事件推送,不必推翻整个循环(第 9 章)。

迁移决策建议(一句话版):
  • 新项目 / 教学项目 → 直接 Spring Boot 4.1 + Spring AI 2.0,没有历史包袱。
  • 存量 Boot 3.5.x 项目 → 先评估 Boot 4 的迁移成本(Boot 3.5 与 Framework 6.2 已于 2026-06-30 EOL), 在迁移完成前继续用 Spring AI 1.1.x 维护版本。
  • 只想小修小补 → 留在 1.1.x,不要为了追新强行迁移。
常见困惑:为什么 start.spring.io 里选不到 Spring Boot 3.x?

Spring Initializr 只列出仍在 OSS 支持期内的版本线。3.5.x 已于 2026-06-30 停止 OSS 支持,所以被移出了下拉列表。

实测官方 start.spring.io 当前提供的选项只有 4.x: 4.2.0 (M1/SNAPSHOT)、4.1.2 (SNAPSHOT)、 4.1.1(默认)、4.0.9 (SNAPSHOT)、4.0.8。 Java 可选 17 / 21 / 25 / 26。

网上写着「Project 选 Maven,Language Java,Spring Boot 选当前稳定 3.x」的教程, 都写于 4.x 发布之前,已经过时。照做的话 Spring AI 2.0 根本用不了 (2.0 基于 Spring Framework 7.0 与 Jakarta EE 11,与 Boot 3.x 不兼容)。 选 4.1.1。

Spring Boot 版本支持时间线(官方支持页,2026-09 核对):
分支首次发布OSS 支持结束Java 范围状态
4.2.x2026-11(计划)2027-12—未发布
4.1.x2026-062027-0717 ~ 26✅ 当前推荐
4.0.x2025-112026-1217 ~ 25维护中(快到期)
3.5.x2025-052026-06(已 EOL)17 ~ 25❌ 已停止 OSS
3.4.x2024-112025-12(已 EOL)17 ~ 24❌ 已停止 OSS

规律:每个小版本约有 12 个月 OSS 支持,Spring Boot 没有「LTS」概念 (这点和 Java 不同,容易误解)。所以「当前稳定版」是会变的——请以 spring.io/projects/spring-boot 的支持页为准,不要照抄教程里的版本号。

A.1命令速查

下面每条命令都可直接复制。每个小节后附一行「什么时候用」。注意:命令里的路径、密码、端口都是课程配套工程 springai-lab 的默认值,按你自己的环境替换。

Docker / 基础设施

# ---------- 启动与停止 ----------
cd D:/springai-lab
docker compose up -d                      # 后台启动全部服务
docker compose up -d mysql redis pgvector zipkin   # 只启动核心四个
docker compose ps                          # 查看状态(等 STATUS 变 healthy)
docker compose logs -f milvus              # 跟踪某个服务日志
docker compose restart app                 # 重启应用容器
docker compose down                        # 停止并删除容器(保留数据卷)
docker compose down -v                     # ★ 同时删除数据卷(数据全丢,慎用)

# ---------- 连通性验证 ----------
docker exec -it sai-mysql mysql -uroot -proot123456 -e "SELECT VERSION();"
docker exec -it sai-redis redis-cli -a redis123456 ping              # 期望 PONG
docker exec -it sai-pgvector psql -U ai -d vectordb -c "\dx"        # 看 vector 扩展
curl http://localhost:9411/health                                    # Zipkin
curl http://localhost:9091/healthz                                   # Milvus

# ---------- 清理(磁盘不够时) ----------
docker system df                           # 看磁盘占用
docker system prune -a                     # 清理无用镜像与容器(不会删卷)
docker volume ls                           # 列出数据卷

什么时候用 第 4/5/11 章用到 pgvector、redis、mysql、zipkin 等中间件时;启动后先用「连通性验证」三条确认服务 healthy,再跑应用,否则会卡在连接超时。

Maven

mvn -v                                              # 版本 + JDK 版本
mvn clean compile                                   # 编译
mvn clean package -DskipTests                       # 打包(跳过测试,快)
mvn spring-boot:run                                 # 直接运行

# 看 Spring AI 依赖树(排查"依赖没进来")
mvn -q dependency:tree -Dincludes=org.springframework.ai

# 强制更新依赖(镜像换了之后用)
mvn -U clean package -DskipTests

# 清本地仓库里某个包的缓存(解决"jar 损坏")
rm -rf ~/.m2/repository/org/springframework/ai

# 只跑某个测试类 / 某个方法
mvn test -Dtest=RagEvaluationTest
mvn test -Dtest=RagEvaluationTest#跑一遍RAG评测并输出报告

什么时候用 改了 pom.xml 或换了镜像后用 mvn -U 强制刷新;依赖冲突/版本不对先看 dependency:tree;jar 损坏先清本地仓库缓存再编译。

接口验证(curl)

# 普通 POST
curl -X POST http://localhost:8080/api/v1/chat/ask \
  -H "Content-Type: application/json" \
  -d '{"message":"退款要几天到账"}'

# 流式(-N 关闭缓冲才能看到逐字效果)
curl -N -X POST http://localhost:8080/api/v1/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"从1数到10"}'

# GET 带参数
curl "http://localhost:8080/api/v1/vector/search?q=退款&topK=5&threshold=0.5"

# 只看 HTTP 状态码
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/actuator/health

# 格式化 JSON 输出(装了 jq 的话)
curl -s ... | jq .

# 直连模型厂商 API 验证密钥(排除代码因素)
curl https://api.deepseek.com/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{"model":"deepseek-chat","messages":[{"role":"user","content":"你好"}]}'

什么时候用 调通一个接口最快的方式不是开前端,而是 curl。先 -w "%{http_code}" 确认 200,再贴完整 body 看返回;报 401 先用最后那条直连厂商验证 key 是否有效,区分「密钥问题」还是「代码问题」。

数据库

# ---------- PostgreSQL / pgvector ----------
docker exec -it sai-pgvector psql -U ai -d vectordb

-- 看每个租户灌了多少块
SELECT metadata->>'tenantId' AS tenant, COUNT(*)
FROM vector_store GROUP BY 1;

-- 看某个来源的块数与内容预览
SELECT id, LEFT(content, 60) FROM vector_store
WHERE metadata->>'source' = 'refund-policy.md' LIMIT 5;

-- 清空某个租户的数据(重建索引前)
DELETE FROM vector_store WHERE metadata->>'tenantId' = 't1';

-- 看索引是否建上(HNSW)
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'vector_store';

# ---------- MySQL ----------
docker exec -it sai-mysql mysql -uroot -proot123456 springai_lab

-- 看会话记忆(排查"记忆串味")
SELECT conversation_id, type, LEFT(content, 50) AS preview, timestamp
FROM spring_ai_chat_memory
WHERE conversation_id = 't1|u1001|s1'
ORDER BY timestamp DESC LIMIT 10;

-- 成本归因:按意图统计
SELECT intent, COUNT(*), ROUND(AVG(cost_yuan), 4) AS avg_cost
FROM qa_audit GROUP BY intent ORDER BY avg_cost DESC;

-- 找出"检索没命中"的问题(知识盲区)
SELECT question, COUNT(*) FROM qa_audit
WHERE citation_count = 0
GROUP BY question ORDER BY COUNT(*) DESC LIMIT 20;

什么时候用 RAG/记忆相关 bug 排查时:检索为空先去 vector_store 看块数和索引;A 用户看到 B 用户对话去 spring_ai_chat_memory 看 conversation_id 是否串了;成本异常用 qa_audit 按意图归因。

应用运行 / 运维 / 提交

# ---------- 打好的 jar 直接跑(第 11 章部署用) ----------
java -jar target/springai-lab-0.0.1.jar
java -jar target/springai-lab-0.0.1.jar --spring.profiles.active=prod   # 指定环境

# ---------- 看进程与端口占用 ----------
jps                                       # 看 Java 进程 PID
netstat -ano | findstr :8080             # Windows 看谁占了 8080
lsof -i :8080                            # macOS/Linux 等价

# ---------- 指标端点(第 10 章) ----------
curl -s http://localhost:8080/actuator/prometheus | grep ai_   # 只看 AI 相关指标
curl -s http://localhost:8080/actuator/health              # 健康检查

# ---------- 容器 IP(容器间互访排错用) ----------
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' sai-pgvector

# ---------- 作业提交前(不要提交密钥!) ----------
git status                                # 确认没有 application-local.yml / .env 被加进来
git log --oneline -5                      # 确认提交记录干净

什么时候用 部署/排错时用 java -jar 而不是 IDEA 跑;端口被占先 netstat/lsof 找 PID 再结束;提交作业前必跑 git status 确认密钥文件没进暂存区。

A.2配置模板全家桶

这是一份可以直接用的完整 application.yml,把课程里所有章节需要的配置都收进来了。用不到的段落注释掉即可。下面先给「最小可跑版」,再给「全功能版」。

最小可跑版(第 1~3 章够用)

spring:
  application:
    name: springai-lab
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:}        # ★ 密钥走环境变量,绝不硬编码
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7               # ★ 2.0 默认温度已移除,必须显式给
          max-tokens: 2048
        connect-timeout: 30s
        read-timeout: 120s
      # ★ 2.0:embedding 没有 .options 层,直接写 model
      embedding:
        model: text-embedding-3-small

server:
  port: 8080

logging:
  level:
    root: info
    org.springframework.ai: warn

什么时候用 第 1 章刚跑通 ChatClient、第 2 章提示词、第 3 章结构化输出时,用这个最小版即可,别一上来把全功能版贴进去增加排查面。

全功能版(第 4~11 章逐段打开)

spring:
  application:
    name: springai-lab
  profiles:
    active: dev              # dev / local / prod

  # ==================== 模型配置 ====================
  ai:
    openai:
      api-key: ${OPENAI_API_KEY:}
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat
          temperature: 0.7
          max-tokens: 2048
        connect-timeout: 30s
        read-timeout: 120s
      embedding:
        # ★ 2.0 移除了 .options 这一层,直接写 model
        model: text-embedding-3-small
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b
      embedding:
        model: nomic-embed-text
      init:
        pull-model-strategy: never

    # ==================== 2.0:工具调用循环与工具检索 ====================
    chat:
      client:
        tool-calling:
          # 默认 true:ChatClient 自动注册 ToolCallingAdvisor 处理工具循环。
          # 自研 Agent 要自己驱动循环时设为 false,或单次调用用
          # AdvisorParams.toolCallingAdvisorAutoRegister(false)。
          enabled: true
          # advisor-order: 100   # 默认 HIGHEST_PRECEDENCE + 300
        tool-search-advisor:
          # 工具 > 50 个(尤其接了多个 MCP Server)时打开,省 34%~64% token
          enabled: false
          tool-index-type: regex        # regex / lucene / vector
    tools:
      limits:
        # 2.0 新增的默认保护:单工具每轮 40 次、合计 150 次
        max-calls-per-tool-default: 40
        max-total-tool-calls: 150
        on-limit-exceeded: THROW      # 或 RETURN_ERROR_RESPONSE
      resolution:
        fallback:
          # 默认 false:只有本次请求显式附带的工具可执行。
          # 打开会让"模型报出任何工具名就能执行",放大越权风险,生产保持 false。
          enabled: false

    # ==================== 重试(第 6 章) ====================
    retry:
      max-attempts: 3
      on-client-errors: false       # ★ 4xx 不重试
      backoff:
        initial-interval: 2s
        multiplier: 2
        max-interval: 10s

    # ==================== 会话记忆(第 6 章) ====================
    chat:
      memory:
        repository:
          jdbc:
            initialize-schema: always
          # redis:
          #   time-to-live: 24h

    # ==================== 向量库(第 4 章) ====================
    vectorstore:
      pgvector:
        initialize-schema: true
        table-name: vector_store
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimensions: 1536            # ★ 必须等于模型输出维度
        max-document-batch-size: 10000
      # milvus:
      #   client: { host: localhost, port: 19530 }
      #   collection-name: vector_store
      #   embedding-dimension: 1536
      #   index-type: IVF_FLAT
      #   metric-type: COSINE
      #   initialize-schema: true

    # ==================== MCP 客户端(第 8 章) ====================
    mcp:
      client:
        enabled: true
        name: springai-lab
        version: 1.0.0
        initialized: true
        toolcallback:
          enabled: true             # ★ 自动把 MCP 工具注册为 ToolCallback
        stdio:
          servers-configuration: classpath:mcp-servers.json
          request-timeout: 60s

    # ==================== 可观测性(第 10 章) ====================
    chat:
      observations:
        log-prompt: false           # 生产必须 false!
        log-completion: false

  # ==================== 数据源 ====================
  datasource:
    url: jdbc:mysql://localhost:3306/springai_lab?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
    username: root
    password: root123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 5000
      max-lifetime: 1200000

  data:
    redis:
      host: localhost
      port: 6379
      password: redis123456
      timeout: 3s

  # ==================== 上传限制(知识库文档) ====================
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB

server:
  port: 8080
  shutdown: graceful             # 优雅停机,避免正在处理的请求被中断
  tomcat:
    threads:
      max: 200
    connection-timeout: 10s

# ==================== 管理端点 ====================
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  tracing:
    sampling:
      probability: 0.2           # 生产 0.1~0.2;开发用 1.0
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans

# ==================== Resilience4j(第 6 章) ====================
resilience4j:
  retry:
    instances:
      aiChat:
        max-attempts: 3
        wait-duration: 1s
        retry-exceptions:
          - org.springframework.web.client.ResourceAccessException
  circuitbreaker:
    instances:
      aiChat:
        sliding-window-size: 20
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s

# ==================== 业务自定义配置 ====================
app:
  ai:
    enabled: true                # ★ 应急开关:false 则 AI 功能整体关闭
  knowledge:
    docs-path: classpath:docs/*.md
    top-k: 5
    similarity-threshold: 0.5
    chunk-size: 600
  agent:
    max-steps: 8
    max-tokens: 30000
    max-millis: 120000
    pending-action-expire-minutes: 15
    high-risk-tools: submitRefund,createTicket,sendEmail,cancelOrder
  pricing:
    models:
      deepseek-chat: { input: 1.0, output: 2.0 }
      qwen-plus:     { input: 0.8, output: 2.0 }
      ollama-local:  { input: 0.0, output: 0.0 }

# ==================== 日志 ====================
logging:
  level:
    root: info
    com.example.springai: info
    org.springframework.ai: warn        # 生产用 warn,开发用 debug
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] %logger{36} - %msg%n"
  file:
    name: logs/springai-lab.log
  logback:
    rollingpolicy:
      max-file-size: 100MB
      max-history: 30
      total-size-cap: 5GB

什么时候用 第 4 章起逐段打开对应配置(向量库、记忆、MCP、可观测)。每开一段先确认依赖 starter 已引入,再贴配置;dimensions 必须与 embedding 模型输出维度一致,否则建表直接报错。

A.3异常排查对照表(按报错关键词找)

每行的「现象」是学生真实会看到的报错原文。遇到红色堆栈,先按关键词 Ctrl+F 本表,再动手。找不到就往上翻 A.0 看是不是 1.x 写法。

现象(报错原文)原因解决章节
No qualifying bean of type 'org.springframework.ai.chat.model.ChatModel'缺 chat starter,或 api-key 为空导致自动配置未生效检查 pom.xml 是否引入 spring-ai-starter-model-openai;确认环境变量 OPENAI_API_KEY 已被进程读到(IDEA 勾 Include system env 后重启)1
401 Unauthorized / Invalid API key密钥错/过期,或环境变量未被读取用 A.1 直连厂商命令验证 key;确认没硬编码、没多空格;充值到账1
404 Not Found(模型接口)base-url 多写了 /v1DeepSeek 用 https://api.deepseek.com,去掉末尾 /v11
429 Too Many Requests触发限流加退避重试(A.2 的 spring.ai.retry)、降低并发、升级配额1/6
java.net.SocketTimeoutException: Read timed out读超时太短设 read-timeout: 120s(长文本/工具循环场景尤其要加)1
Not enough variable values available to expand模板占位符没传参数检查 PromptTemplate.create(Map) 的 key 与模板 {name} 是否一一对应;提示词里 JSON 花括号需用 \{ ... \} 转义或换定界符2
com.fasterxml.jackson.core.JsonParseException: Unexpected character模型用 ```json 包裹了输出,或前面有解释文字提示词明确「只输出 JSON,不要代码块标记」;或用 .entity() 让框架处理3
Cannot deserialize value of type ... from String "899元"模型带单位/格式不符字段描述写死格式(如「金额只输出数字」);用 @JsonProperty 映射3
对象字段全是 null,但模型确实返回了内容JSON key 与 Java 字段名不一致用 @JsonProperty("中文key") 映射;或让模型输出英文字段名3
expected 1536 dimensions, but got 1024建表维度与模型维度不一致把 spring.ai.vectorstore.pgvector.dimensions 改成模型实际维度,或换模型并重灌向量4
extension "vector" is not available用了普通 postgres 镜像必须换 pgvector/pgvector:pg16 镜像4
relation "vector_store" does not exist表没建设 initialize-schema: true,或手工执行 init SQL 后重启4
检索结果永远为空(分数都很低)阈值太高 / 查询与语料语言不匹配先把 similarity-threshold 设 0 看分数分布;确认查询语言与入库语言一致4/5
检索结果大量重复重复灌库灌前先 vectorStore.delete(filter) 做幂等4
ERROR: syntax error at or near ...(元数据过滤)字符串没加单引号 / 类型不统一元数据过滤表达式里字符串用单引号;同一字段类型保持一致4
allowEmptyContext ... IllegalArgumentException(QuestionAnswerAdvisor)默认 allowEmptyContext=false,检索为空抛异常设 true 并在提示词里定义「无资料时如何回答」5
回答里出现知识库根本没有的信息提示词没禁止编造 / 阈值太低 / temperature 太高提示词加「只依据资料」;调高阈值;temperature 降到 0.3 以下5
A 用户看到 B 用户的对话conversationId 固定值或未传每次请求显式传 .param(ChatMemory.CONVERSATION_ID, id),按 用户/会话 维度生成唯一 id2/6
重启后记忆丢失用了 InMemory 实现换 JdbcChatMemory / RedisChatMemory6
熔断一直不恢复(CircuitBreaker)下游真故障人工介入检查密钥/配额/网络;别盲目调大窗口6
降级方法没被调用(fallback)降级方法签名不对降级方法必须 public、参数与原方法一致、末尾多一个 Throwable6
模型不使用工具描述太模糊 / 模型不支持 function calling / 工具太多写清工具用途;换支持工具调用的模型;工具超过 15 个考虑 ToolSearchAdvisor7
模型用错工具工具职责重叠合并重叠工具;在描述里写「不要用于……」7
Tool execution failed ... 500工具内部异常直接抛出配 DefaultToolExecutionExceptionProcessor.alwaysThrow(false) 让模型拿到错误自行补救7
工具被重复执行(一次变两次)自动注册的 ToolCallingAdvisor 执行了一次、你的循环又执行一次自定义 Agent 时 AdvisorParams.toolCallingAdvisorAutoRegister(false);或写操作做幂等7/9
MCP 客户端「Failed to parse message / 无法解析消息」Server 的 stdout 被日志污染设 spring.main.web-application-type=none,禁止 System.out.println,只走 stderr/日志框架8
MCP 工具列表为空没把 provider 传给 ChatClient显式 new SyncMcpToolCallbackProvider 并 .tools(provider);确认 toolcallback.enabled=true8
Agent 只调一次工具就结束没关掉自动工具调用 / 提示词没说明可多步自研循环时关自动执行;提示词明确「可连续多步」9
Agent 死循环(超过步数)没设最大步数加 max-steps 限制;加重复调用检测9
Agent 卡住不动工具返回空/异常,模型无法判断下一步工具返回要带明确状态与下一步建议9
确认接口「编号不存在 / 已过期」待确认操作存在内存里,重启丢了持久化待确认项(库表 + 有效期字段)9
Zipkin 看不到 trace采样率 0 / 端点错 / 缺依赖开发设 tracing.sampling.probability=1.0;核对 zipkin 端点;确认引入了 zipkin 依赖10
Prometheus 内存暴涨指标标签基数过高删掉 userId/query 等高基数标签,只留固定枚举维度10
MDC 里 traceId 串了finally 没清理在 finally 里 MDC.remove("traceId")10
容器内连不上数据库(Connection refused)用了 localhost必须用 docker compose 里的 service 名(如 mysql、pgvector)11
应用启动时数据库连接失败依赖未就绪用 depends_on: condition: service_healthy 等依赖真正 healthy11
容器日志时间差 8 小时容器默认 UTC镜像加 tzdata 并设置 TZ=Asia/Shanghai11
IllegalArgumentException: conversationId must not be null2.0 记忆 conversationId 必填,漏传/传 null每次请求显式 .param(ChatMemory.CONVERSATION_ID, id),id 非空且按用户+会话唯一2/6
There is already a ToolCallingAdvisor registeredAdvisor 链里重复注册了工具循环链中只能有一个 ToolCallingAdvisor;自研循环时关掉自动注册,别再手动加一个7/9
Expected 1 tool callback provider but found 0(MCP)没把 SyncMcpToolCallbackProvider 挂上显式 new provider 并 .tools(provider);确认 spring.ai.mcp.client.toolcallback.enabled=true8
UnsupportedOperationException(改 Options 集合)2.0 Options 严格不可变不要用 setter/直接改集合;用 builder 或 .mutate() 生成新对象5
PromptTemplate ... could not resolve placeholder模板变量名与 create(Map) 的 key 不一致模板 {name} 与 Map 的 key 必须完全一致;JSON 花括号用 \{ \} 转义2
排查顺序口诀:先看现象是不是 A.0 里的 1.x 写法 → 再看本表关键词 → 最后才是搜索引擎。报 IllegalArgumentException: conversationId 必是漏传 conversationId;报「工具列表为空」必是 MCP provider 没挂上——这两类 90% 是 2.0 约定没遵守。

A.4核心 API 速查

2.0 写法速查。每个类后加一行「什么时候用」。★ 标记的是 2.0 与 1.x 差异点,答辩前必背。

接口 / 类关键方法什么时候用章节
ChatModelcall(Prompt) / call(String) / stream(Prompt)只需要最原始的一次对话/流式,不想引入 Advisor 链时1
ChatClientprompt().user(...).call().content() / .stream() / .entity(Class)★ 课程主用。要拼 Advisor、记忆、工具、结构化输出都用它1
ChatClient.BuilderdefaultSystem / defaultAdvisors / defaultTools / defaultOptions把系统提示、通用 Advisor、默认工具在构建时固化,避免每次请求重复写1
PromptTemplatebuilder().template(...).build() → create(Map)提示词带占位符、要动态填参数时(注意 JSON 花括号转义)2
Message 家族SystemMessage / UserMessage / AssistantMessage / ToolResponseMessage手动组装多轮消息列表(而非用 advisor)时2
Medianew Media(MimeType, Resource/URI/byte[])多模态:把图片/音频作为用户消息附件发送2
ChatMemoryadd / get / clear;ChatMemory.CONVERSATION_ID★ 记忆读写;conversationId 必填,用 .param(CONVERSATION_ID, id) 传2/6
MessageChatMemoryAdvisorbuilder(memory).build()★ PromptChatMemoryAdvisor 已删,记忆统一用它6
BeanOutputConverter<T>getFormat() / convert(String)需要把提示词里的 JSON schema 拼进文本、再手动转对象时3
.entity(Class)call().entity(Order.class) / entity(Class, spec->spec.validateSchema())★ 结构化输出首选入口;要硬约束加 useProviderStructuredOutput()3
CallAdvisoradviseCall(request, chain) / getOrder() / getName()自己写横切逻辑(日志、权限、限流)时实现3
ToolCallingAdvisorbuilder().build();可重写 doBeforeCall 等钩子★ 2.0 工具循环由它接管;自定义 Agent 时继承它加审批/预算闸7/9
FunctionToolCallbackbuilder(name, fn).inputType(X.class).build()★ 函数式工具;toolNames() 已删,必须用它产出 ToolCallback7
@Tool / @ToolParam注解方法即可;returnDirect 支持结果直出把 Spring bean 的方法暴露成工具(类级/方法级注解)7
ToolCallbacksToolCallbacks.from(bean...)把带 @Tool 的 bean 批量转成 ToolCallback[] 再 .tools()7
ToolContext方法参数注入;toolContext.getContext().get("userId")工具方法里取请求级上下文(当前用户等)7
ToolExecutionExceptionProcessorDefaultToolExecutionExceptionProcessor.builder().alwaysThrow(false)工具报错时让模型拿到错误自行补救,而不是 5007
@McpTool / @McpToolParam注解方法即暴露为 MCP 工具★ 2.0 MCP 服务端用这组注解(替代 1.x 的 @Tool 暴露方式)8
SyncMcpToolCallbackProviderbuilder(mcpSyncClient).build()★ 客户端侧必须显式 new 它并 .tools(provider),MCP 工具才可用8
MethodToolCallbackProviderbuilder().toolObjects(bean).build()把本地 bean 方法批量注册成工具 provider(非 MCP 场景)8
EmbeddingModelembed(String) / embedForResponse(List)把文本转向量(入库或做相似度时用)4
VectorStoreadd / delete / similaritySearch(SearchRequest)向量增删查;RAG 检索的核心接口4
SearchRequestbuilder().query().topK().similarityThreshold().filterExpression()构造检索请求;阈值与过滤都在这设4
Documentnew Document(text, metadata);getText/getMetadata/getScore向量库里的一条记录(文本 + 元数据 + 分数)4
TokenTextSplitterbuilder().withChunkSize().withKeepSeparator()...build();apply(docs)把长文档切成 chunk 再入库5
QuestionAnswerAdvisorbuilder(vectorStore).searchRequest().promptTemplate().build()RAG 最简实现:一个 Advisor 包揽检索+拼上下文+问5
RetrievalAugmentationAdvisorbuilder().documentRetriever().queryTransformers().queryAugmenter()需要查询改写/重排/混合检索等高级 RAG 流水线时5
AdvisorParamsAdvisorParams.toolCallingAdvisorAutoRegister(false)★ 自研 Agent 循环时关掉自动工具执行,避免双重执行9
ObservationRegistryObservation.createNotStarted(name, registry).observe(...)手动打点 / 自定义链路 span10
MeterRegistrycounter(...) / timer(...) / summary(...)埋业务指标(成本、命中率)10
RelevancyEvaluatorbuilder().chatClientBuilder(...).build();evaluate(request)评测检索相关性10
FactCheckingEvaluator同上,用于查幻觉评测回答是否有资料支撑(查幻觉)10
答辩高频易错点:① 记忆用 MessageChatMemoryAdvisor,不是已删除的 PromptChatMemoryAdvisor;② 函数式工具用 FunctionToolCallback,不是 toolNames();③ 结构化输出用 .entity(Class);④ MCP 工具要显式 SyncMcpToolCallbackProvider;⑤ embedding 配置没有 .options 层。这五条错任意一条都算「用了过时 API」。

A.5提示词模板库(直接抄)

六套可直接套用的提示词骨架,每段都标注「什么时候用」。变量用 {name} 占位,调 PromptTemplate.create(Map) 填。

① 通用系统提示骨架

你是{公司}{角色},服务于{服务对象}。

## 你的职责
{用 2~3 条列出能做的事}

## 硬性规则
1. {最重要的一条:什么绝对不能做}
2. {信息不足时怎么办}
3. {越界问题时怎么办}

## 输出风格
{长度限制、语气、格式要求}

## 兜底
如果{无法处理的情况},请{具体动作}。

什么时候用 任何对话机器人/客服机器人的 defaultSystem,先搭这个骨架再填业务。

② RAG 问答模板

请严格依据下方【参考资料】回答用户问题。

【参考资料】
{question_answer_context}

【回答要求】
1. 只使用参考资料中的信息;资料不足时回答"根据现有资料无法确定"。
2. 结论后用 [序号] 标注来源。
3. 用自己的话组织,不要照抄原文。
4. 回答不超过 300 字。

【用户问题】
{query}

什么时候用 接了知识库的问答(QuestionAnswerAdvisor 的 promptTemplate)。防幻觉全靠「只使用资料」这条。

③ 信息抽取模板

你是一名信息抽取助手。请从下面的文本中提取结构化信息。

【抽取规则】
1. 找不到的字段返回 null(列表返回 []),严禁编造或推测。
2. {字段级特殊规则,如"金额只输出数字,不带单位"}。
3. 只输出 JSON 对象本身,不要解释,不要代码块标记。

【待抽取文本】
{input}

{format}

什么时候用 配 .entity(Class) 使用:从合同/工单/邮件里抽字段。格式约束写死能大幅减少「899元」这类反序列化失败。

④ 意图分类模板

你是一个意图分类器。请判断用户问题属于哪一类。

【分类规则】
1. {类别 A 的特征} → CATEGORY_A
2. {类别 B 的特征} → CATEGORY_B
3. {类别 C 的特征} → CATEGORY_C

【注意】
- {歧义情况的判定规则,如"同时涉及 A 和 B 时归为 B"}。
- 不要输出解释,只输出 JSON。

【用户问题】
{question}

【最近对话】
{recentHistory}

什么时候用 多路路由:先分类再决定走哪个 Agent / 工具 / 知识库。

⑤ Agent 任务规划模板

你是任务规划专家。请把用户需求拆解成可执行的步骤。

【可用工具】
{toolList}

【输出格式】
严格输出 JSON 数组:step / action / args / desc / risk
risk 取值 low(查询)medium(修改数据)high(删除·付款·发信)

【规则】
1. 步骤控制在 2~6 步,不要过度拆分。
2. 只使用列出的工具,禁止编造。
3. 不要输出 JSON 以外的内容。

【用户需求】
{requirement}

什么时候用 Planner 模式(先规划再执行)的 Agent;risk=high 的步骤要接 HITL 人工确认。

⑥ 对话摘要模板

请把下面的对话压缩成结构化摘要。

摘要必须保留(如果存在):
- 用户身份关键信息
- 涉及的所有编号、金额、日期
- 用户核心诉求与已承诺方案
- 尚未解决的问题

摘要不超过 150 字,要点式,不要客套话。

【已有摘要】
{previousSummary}

【新增对话】
{historyText}

什么时候用 长对话压缩后写回记忆,避免 context window 爆掉、省 token。

⑦ 工具选择说明模板(给模型的工具说明)

你是一个能调用工具的助手。可用工具如下,请按需调用:
- getWeather(city): 查某城市天气,仅用于天气类问题
- queryOrder(id): 查订单状态,需要订单号
- submitRefund(id, reason): 提交退款,属于高风险操作,调用前必须先向用户确认

规则:
1. 不相关的工具不要调用。
2. submitRefund 必须先获得用户明确确认再调用。

什么时候用 工具太多导致模型用错/不用工具时,把职责边界写进系统提示(配合 @Tool 的 description 双保险)。

A.6术语表

每个术语先给一句大白话(像跟同学解释),再给一句准确定义(像写进文档)。带 ★ 的是 2.0 重点术语。

术语大白话准确定义
LLM就是那个能聊天、写代码的大模型本身Large Language Model,大语言模型;本课程里「模型」默认指它
Token模型按「词片」算钱和长度,中文大概一个字不到一个 token模型的计费与长度基本单位,中英文约 1 字 ≈ 0.7~1 token,输入输出都计费
Prompt你发给模型的完整请求提示词:一次请求的完整输入,包含消息列表与参数
System / User / Assistant Message三种角色:定规则 / 用户说 / 模型回消息的三种角色:规则设定 / 用户输入 / 模型输出
Temperature温度越低越「死板靠谱」,越高越「放飞自我」采样温度 0~2;越低越确定(抽取、分类用 0),越高越发散(创作可 0.8+)。2.0 默认值已移除,须显式配置
Context Window模型一次能「看见」的最大字数上限上下文窗口:一次请求能容纳的最大 Token 数(含输入输出)
Embedding把一句话变成一串数字,意思相近的句子数字也相近嵌入:把文本转成高维向量的过程;语义相近的文本向量距离也近
向量维度那串数字的长度,比如 1536 个数向量的长度(如 1536);必须与模型一致,换模型必须重灌
余弦相似度比较两个向量「方向」像不像,不看长度只比较向量方向、忽略长度的相似度度量;文本检索的标准选择
ANN / HNSW / IVF用「近似」换「快」,检索百万向量也不卡近似最近邻及其常用索引结构;用少量召回率损失换取数量级的速度提升
Chunk长文档被切碎后的小段,每段单独入库文本切分后的片段;RAG 中向量化的基本单位
RAG先翻资料再回答,避免瞎编检索增强生成:先检索资料再让模型基于资料回答
TopK检索时取最像的几条检索返回的最相似结果条数
Similarity Threshold相似度太低的结果直接扔掉相似度阈值;低于它的检索结果被丢弃,是防幻觉的关键参数
Rerank初筛完再精排一遍,挑更准的重排:对初步召回的结果做二次精排,提升精度
Hybrid Search向量 + 关键词两手抓混合检索:向量检索 + 关键词检索(BM25)合并结果
Tool Calling ★模型说「我要调某个工具」,你的代码去真执行工具调用:模型输出「调用意图」,由你的代码真正执行;2.0 循环由 ToolCallingAdvisor 接管
FunctionToolCallback ★把一段 Java 函数包装成「模型能调的工具」2.0 函数式工具注册方式:FunctionToolCallback.builder(name, fn).inputType(X.class).build();取代 1.x 的 toolNames()
MCP ★给工具定了个「通用插头」标准,换语言换进程都能插Model Context Protocol:把工具标准化的协议,支持跨进程跨语言;2.0 服务端用 @McpTool
SyncMcpToolCallbackProvider ★把 MCP Server 的工具「搬」进你的 ChatClient2.0 中 MCP 工具不会自动注册,必须显式 new 它并传给 .tools()
Advisor ★像 Spring 的拦截器,套在对话外面做通用的事Spring AI 的切面机制,用于日志、记忆、RAG、限流、工具循环等横切逻辑;链中只能有一个 ToolCallingAdvisor
ConversationId ★给每段对话一个「身份证号」,互不串门2.0 记忆的会话标识,必填;用 .param(ChatMemory.CONVERSATION_ID, id) 传,缺失即抛异常
entity() ★让模型直接吐出一个 Java 对象,不用自己解析2.0 结构化输出入口:call().entity(Class);可加 validateSchema() 与 useProviderStructuredOutput()
Agent会自己一步步想办法干活的「智能员工」智能体:能自主决定多步执行的程序(模型 + 工具 + 循环 + 刹车)
ReAct想一步、做一步、看结果、再想下一步Reason + Act:思考→行动→观察的循环范式
Planner先列好步骤清单,再一步步干规划器:先出完整步骤清单再执行
HITL危险操作先问人,人点了头才执行Human-in-the-loop:人工确认环节,高风险操作必做
Observation / Span / Trace一次操作 / 操作里的一个环节 / 一条完整链路可观测性概念:一次操作 / 链路中的一个环节 / 一条完整链路
LLM-as-a-Judge找个模型当评委给另一个模型的回答打分用模型给模型输出打分,用于效果评估
Faithfulness(忠实度)回答是不是真的有资料撑腰回答是否有资料支撑的度量,是查幻觉的核心指标
幻觉 Hallucination模型一本正经地编瞎话模型编造看似合理但错误或不存在的信息
提示词注入用户用输入骗模型「忘了上面的规矩」用户通过输入试图覆盖系统指令的攻击方式
越权看了/用了自己不该碰的数据或功能访问了当前身份无权访问的数据或功能
幂等同一个操作点 N 次,效果跟点一次一样同一个请求执行多次与执行一次效果相同;写操作必须幂等
降级 Degradation主功能挂了,退回到还能用的备选主能力不可用时,退回到一个仍可用的替代方案
熔断 Circuit Breaker失败太多就先「拉闸」,给下游喘口气失败率过高时主动拒绝请求,给下游恢复时间
脱敏日志/数据里把手机号、密钥遮住日志/数据中隐藏敏感信息(手机号、身份证、密钥)
MessageChatMemoryAdvisor ★负责「把历史对话塞进这次请求」的那层2.0 记忆 Advisor;PromptChatMemoryAdvisor 已删,记忆统一用它
AdvisorParams ★一个开关集合,用来关掉自动工具执行含 toolCallingAdvisorAutoRegister(false);自研 Agent 循环时关闭自动执行避免重复调用
QuestionAnswerAdvisor ★一个 Advisor 把「检索+拼上下文+问」全包了RAG 最简实现:builder(vectorStore).searchRequest().promptTemplate().build()
TokenTextSplitter把长文档按 token 数切成小段文本切分器:builder().withChunkSize().withKeepSeparator()...build();RAG 入库前用
Relevancy / FactChecking Evaluator自动阅卷:查检索准不准、回答有没有瞎编第 10 章评测器:evaluate(request) 返回指标,接 CI 质量门禁
PromptTemplate带坑位的提示词,运行时填值提示词模板:builder().template(...).build().create(Map);注意占位符与 JSON 花括号转义

A.7学习资源地图

资源地址说明
Spring AI 官方文档https://docs.spring.io/spring-ai/reference/★ 遇到 API 变化第一时间查这里
Spring AI GitHubhttps://github.com/spring-projects/spring-ai源码与示例;spring-ai-examples 目录很值得读
Maven Centralhttps://central.sonatype.com确认依赖坐标与最新版本号
Spring Initializrhttps://start.spring.io生成项目骨架(选 Boot 4.1.x / Java 17)
MCP 官方文档https://modelcontextprotocol.io协议规范与官方 Server 列表
MCP Inspector@modelcontextprotocol/inspector调试 MCP Server 的可视化工具
pgvectorhttps://github.com/pgvector/pgvector索引类型与参数调优参考
Milvus 文档https://milvus.io/docs索引选型与性能调优
Ollamahttps://ollama.com/library本地可用的模型列表(含中文模型)
Prompt Engineering Guidehttps://www.promptingguide.ai提示词技巧系统梳理,有中文版
Micrometer Tracinghttps://docs.micrometer.io/tracing/reference/链路追踪配置参考
DeepSeek 开放平台https://platform.deepseek.com课程默认模型服务,申请 key / 看用量
遇到 API 冲突时的排查顺序: ① 查官方文档当前的 API 名称 → ② 在 IDEA 里 Ctrl+Alt+B 看接口实现 → ③ 去 GitHub 的 spring-ai-examples 找可运行示例 → ④ 最后才去搜索引擎。 前三步能解决 90% 的「找不到符号」问题,而且不会看到过时的博客内容。

A.8上线自检清单(打印出来贴在工位)

安全

  • 密钥全部走环境变量,仓库里搜不到 sk-
  • 身份从登录态取,不接受前端传的 userId
  • 多租户检索带 tenantId 过滤
  • 高风险操作 100% 人工确认
  • 确认接口校验归属与有效期
  • 日志脱敏,Prompt 内容不明文记录
  • 工具有超时、有返回条数限制
  • 写操作幂等

稳定性

  • 所有外部调用都有超时
  • 只重试 5xx 与网络超时(4xx 不重试)
  • 配了熔断与降级,且降级路径已实测
  • 有应急开关能一键关闭 AI 功能
  • 连接池上限合理,不超过数据库承载
  • 容器化部署用 service 名而非 localhost
  • depends_on 用 service_healthy
  • 优雅停机已开启

可观测性

  • Trace 链路可在 Zipkin 查到
  • 指标端点可访问,标签基数受控
  • 每条请求落审计表(含 Token 与成本)
  • 响应头带 traceId
  • 日志 pattern 包含 %X{traceId}
  • 生产采样率降到 0.1~0.2
  • 有成本看板与告警阈值

效果

  • 评测集 ≥ 30 条,含知识库外问题
  • 评测能一键跑,输出 Markdown 报告
  • 指标达标(命中率 ≥ 80%、通过率 ≥ 75%)
  • 质量门禁已接入 CI
  • Prompt 外置到文件并纳入版本管理
  • 检索阈值经数据调优,不是拍脑袋
  • 有真实反馈闭环(点赞/点踩)

A.9快速决策表(什么时候选哪个)

做方案选型和答辩时说不清「为什么这么选」时,回这里。每张表给判断依据,不是死规则。

① 知识方案:RAG vs 长上下文 vs 微调

维度RAG(检索增强)长上下文(直接塞全文)微调(Fine-tune)
适用场景知识频繁更新、有现成文档库单次任务、文本能塞进窗口(< 百页)要改变模型「风格/能力」且数据稳定
更新成本低:改文档重灌即可无:每次重发高:重新训练+评估
准确性高(有来源可追溯)中(长文易丢失中间信息)中(可能遗忘训练样本)
成本低~中(只发相关片段)高(每次发全文)训练贵、推理便宜
课程首选★ 第 4/5 章简单 Demo、单次分析不展开(需训练资源)

什么时候用 有「会变的资料库」→ RAG;只是临时分析一份能塞进窗口的文档 → 长上下文;想让模型长期具备某种专业口吻且样本充足 → 才考虑微调。

② 调用入口:ChatClient vs ChatModel

需求选哪个理由
只要一次问答/流式ChatModel.call/stream最轻量,无额外抽象
要记忆 / 工具 / Advisor / 结构化输出ChatClient这些能力都挂在 ChatClient 上
多模型路由、复用默认配置ChatClient.Builder 固化defaultSystem/defaultTools 一次设好全局复用

③ 工具实现:@Tool bean vs FunctionToolCallback vs MCP

场景选哪个理由
本地 Spring bean 方法想当工具@Tool + ToolCallbacks.from(bean)注解即暴露,最省事
已有 Function / Lambda,无 beanFunctionToolCallback.builder(name, fn)...★ 2.0 函数式工具唯一正确写法(toolNames() 已删)
工具在另一个进程/语言/第三方@McpTool + SyncMcpToolCallbackProvider★ 跨进程标准化;必须显式挂 provider

④ 记忆实现:InMemory vs JDBC vs Redis

场景选哪个理由
单测 / 临时 DemoInMemoryChatMemory重启即丢,仅演示
多用户持久化(课程默认)JdbcChatMemory落库,重启不丢;配 MySQL/PG
高并发、要 TTLRedisChatMemory快、自带过期;需 Redis

⑤ 向量库选型

规模选哪个理由
小(< 百万向量)、有 PGPgVectorStore复用现有 PG,HNSW 索引够用
大(千万级)、要性能Milvus专门向量库,IVFLAT/HNSW 可调
本地试玩SimpleVectorStore(内存)零依赖,演示用

⑥ Advisor 顺序与组合

想要的效果挂哪个 Advisor
记忆(在工具循环外)MessageChatMemoryAdvisor(默认顺序 +200)
工具循环ToolCallingAdvisor(默认顺序 +300,链中只能一个)
RAG 检索拼接QuestionAnswerAdvisor / RetrievalAugmentationAdvisor
工具上百个省 tokenToolSearchToolCallingAdvisor
自研 Agent 循环关自动执行:AdvisorParams.toolCallingAdvisorAutoRegister(false)
一句话决策原则:能不用模型就不调用模型(先检索/查库);能不微调就不微调(先用 RAG);工具能本地就不跨进程(先用 @Tool);高风险操作必须有 HITL。复杂度贵,只在确实需要时加。

⑦ 输出方式:结构化 entity() vs 流式 vs 普通文本

想要的结果怎么写理由
要一个 Java 对象call().entity(Class)★ 2.0 首选;框架处理 JSON 解析与校验重试
要逐字流式输出(打字机)stream().content() 接 Flux<String>前端体验好;但拿不到整体对象,需另行聚合
只要一段文本回答call().content()最简单,无需解析
既要流式又要结构化先 entity() 拿对象,再单独开流式通道二者不可同时,拆成两次调用或用 SSE 自行拼接

⑧ 记忆位置:循环内 vs 循环外

现象默认行为要不要改
记忆里看不到工具中间消息默认记忆在 ToolCallingAdvisor 之外(顺序 +200 < +300)一般不用改;要完整轨迹再调顺序
Agent 需要「记得刚才调了什么工具」默认记不到调高记忆 Advisor 顺序到工具循环之后,或用事件溯源方案

A.10附录使用说明(实验 / 答辩前自查)

本附录是「随身手册」,不是用来从头读到尾的。下面告诉你在三个关键时刻怎么用它。

① 写代码时:防「抄成 1.x」

  1. 动手前先翻 A.0,确认你要写的 API 在 2.0 里长什么样(尤其记忆、工具、MCP、embedding 配置)。
  2. 代码里若出现 PromptChatMemoryAdvisor、.toolNames(...)、internalToolExecutionEnabled(...)、embedding.options 任一,立刻停手——这是 1.x,编译/运行会出问题。
  3. 拿不准方法名,回 A.4 按类名查「关键方法」与「什么时候用」。

② 报错时:按关键词定位

  1. 复制红色堆栈里的关键报错串,Ctrl+F 本页:先在 A.3 按「现象」找;
  2. 若 A.3 没有,去 A.0 看是不是「1.x 写法在 2.0 被删/改」;
  3. 还不明白,去 A.7 的官方文档/Examples 按排查顺序查,最后才搜博客。

Q:报 IllegalArgumentException 提到了 conversationId 怎么办?

A:100% 是漏传 conversationId。补上 .param(ChatMemory.CONVERSATION_ID, id),id 要按用户+会话唯一生成。

Q:MCP 工具列表为空?

A:2.0 不会自动注册,确认你 new 了 SyncMcpToolCallbackProvider 并 .tools(provider)。

Q:embedding 建表报维度不匹配?

A:spring.ai.vectorstore.*.dimensions 必须等于模型输出维度(如 1536),且换模型要重灌。

③ 答辩 / 上线前:对照清单

写在最后。

这 11 章的内容,本质上是在回答一个问题:怎么把一个「能跑通」的 AI Demo,变成「敢上生产」的系统。

答案不在某个高深的技术里,而在这些看起来很朴素的工程细节中: 身份要从服务端取、检索要有阈值、高风险操作要人工确认、每个 Agent 循环都要有刹车、 每次改动都要跑评测、每个功能都要能被观测。

把这份指南里的每一个「验收标准」都真正做到了,你就已经具备企业级 AI 应用开发的实战能力。 剩下的,交给时间和项目去打磨。