APPENDIX速查手册与排错表
这一页是你学习过程中最常回来翻的。命令、配置模板、异常对照、术语解释,都集中在这里。它也是一份备课级速查手册:上课前用它核对 API 写法是否落在 2.0 约定内,实验/答辩前用它做自查。建议把本页锚点存进收藏夹。
教学设计说明(教师参考):本附录不进讲课主线,但要求学生在第 1 章起就打开对照。两个检查点:① 学生抄配置/代码前,先确认用的是 2.0 约定(conversationId 必填、ToolCallingAdvisor 接管循环、FunctionToolCallback、entity()、@McpTool、embedding 无 .options),旧博客的 1.x 写法一律判错;② 实验与答辩遇到报错,先要求学生在 A.3 排错表里按关键词定位,再允许提问,培养「先查手册」的习惯。本页也是教学审核的对照基准:凡与下表不一致的写法,视为超出本课程版本边界。
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。
一、底座与依赖
| 项 | 1.x | 2.0 |
|---|---|---|
| Spring Boot | 3.x | 4.0 / 4.1(与 3.x 不兼容,不能混用) |
| Spring Framework | 6.x | 7.0,Jakarta EE 11 |
| Java | 17+ | 17+(官方支持到 26)。 Java 21 只是从源码构建 Spring AI 时的要求,不是运行时要求 |
| Jackson | Jackson 2 | Jackson 3(包名 com.fasterxml.jackson → tools.jackson,日期序列化默认行为也变了) |
| RAG Advisor 模块 | spring-ai-advisors-vector-store | spring-ai-vector-store-advisor(改名) |
| MCP 传输模块 groupId | io.modelcontextprotocol.sdk | org.springframework.ai |
| MCP Java SDK | 0.18.x | 1.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.x | 2.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 名配的告警规则会静默失效,升级后必须回归 |
FunctionToolCallback.builder(name, fn).inputType(X.class).build() 得到 ToolCallback,再 .tools(callback) 挂上去;工具循环由 ToolCallingAdvisor 自动驱动;想自己写 Agent 循环就 AdvisorParams.toolCallingAdvisorAutoRegister(false) 关掉自动执行。凡是看到 .toolNames(...) 或 internalToolExecutionEnabled(...) 都是 1.x,编译不过。
三、结构化输出
| 项 | 1.x | 2.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) 也不再算必填 |
.entity(类) 而不是手写正则抠文本;要硬约束字段格式就用 useProviderStructuredOutput();要防模型返回非法值就加 validateSchema()。四、会话记忆
| 项 | 1.x | 2.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 替换内置实现 |
chatClient.prompt().advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)).user(msg).call()...; 记忆 Advisor 用 MessageChatMemoryAdvisor。不要再写 PromptChatMemoryAdvisor,那个类在 2.0 已经被删。
五、配置与 Options
| 项 | 1.x | 2.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-option | spring.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.x | 2.0 |
|---|---|---|
| 服务端注解 | 用 @Tool 暴露 | @McpTool / @McpToolParam / @McpResource / @McpPrompt / @McpComplete,注解扫描自动注册 |
| 注解包路径 | org.springaicommunity.mcp.annotation.* | org.springframework.ai.mcp.annotation.* |
| 默认传输 | SSE | Streamable HTTP(SSE 已 deprecated,STDIO 用于本地) |
| 客户端配置写法 | 外部 JSON + servers-configuration | spring.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 项目 |
@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,不要为了追新强行迁移。
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。
| 分支 | 首次发布 | OSS 支持结束 | Java 范围 | 状态 |
|---|---|---|---|---|
4.2.x | 2026-11(计划) | 2027-12 | — | 未发布 |
4.1.x | 2026-06 | 2027-07 | 17 ~ 26 | ✅ 当前推荐 |
4.0.x | 2025-11 | 2026-12 | 17 ~ 25 | 维护中(快到期) |
3.5.x | 2025-05 | 2026-06(已 EOL) | 17 ~ 25 | ❌ 已停止 OSS |
3.4.x | 2024-11 | 2025-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 多写了 /v1 | DeepSeek 用 https://api.deepseek.com,去掉末尾 /v1 | 1 |
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),按 用户/会话 维度生成唯一 id | 2/6 |
| 重启后记忆丢失 | 用了 InMemory 实现 | 换 JdbcChatMemory / RedisChatMemory | 6 |
| 熔断一直不恢复(CircuitBreaker) | 下游真故障 | 人工介入检查密钥/配额/网络;别盲目调大窗口 | 6 |
| 降级方法没被调用(fallback) | 降级方法签名不对 | 降级方法必须 public、参数与原方法一致、末尾多一个 Throwable | 6 |
| 模型不使用工具 | 描述太模糊 / 模型不支持 function calling / 工具太多 | 写清工具用途;换支持工具调用的模型;工具超过 15 个考虑 ToolSearchAdvisor | 7 |
| 模型用错工具 | 工具职责重叠 | 合并重叠工具;在描述里写「不要用于……」 | 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=true | 8 |
| 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 等依赖真正 healthy | 11 |
| 容器日志时间差 8 小时 | 容器默认 UTC | 镜像加 tzdata 并设置 TZ=Asia/Shanghai | 11 |
IllegalArgumentException: conversationId must not be null | 2.0 记忆 conversationId 必填,漏传/传 null | 每次请求显式 .param(ChatMemory.CONVERSATION_ID, id),id 非空且按用户+会话唯一 | 2/6 |
There is already a ToolCallingAdvisor registered | Advisor 链里重复注册了工具循环 | 链中只能有一个 ToolCallingAdvisor;自研循环时关掉自动注册,别再手动加一个 | 7/9 |
Expected 1 tool callback provider but found 0(MCP) | 没把 SyncMcpToolCallbackProvider 挂上 | 显式 new provider 并 .tools(provider);确认 spring.ai.mcp.client.toolcallback.enabled=true | 8 |
UnsupportedOperationException(改 Options 集合) | 2.0 Options 严格不可变 | 不要用 setter/直接改集合;用 builder 或 .mutate() 生成新对象 | 5 |
PromptTemplate ... could not resolve placeholder | 模板变量名与 create(Map) 的 key 不一致 | 模板 {name} 与 Map 的 key 必须完全一致;JSON 花括号用 \{ \} 转义 | 2 |
IllegalArgumentException: conversationId 必是漏传 conversationId;报「工具列表为空」必是 MCP provider 没挂上——这两类 90% 是 2.0 约定没遵守。A.4核心 API 速查
2.0 写法速查。每个类后加一行「什么时候用」。★ 标记的是 2.0 与 1.x 差异点,答辩前必背。
| 接口 / 类 | 关键方法 | 什么时候用 | 章节 |
|---|---|---|---|
ChatModel | call(Prompt) / call(String) / stream(Prompt) | 只需要最原始的一次对话/流式,不想引入 Advisor 链时 | 1 |
ChatClient | prompt().user(...).call().content() / .stream() / .entity(Class) | ★ 课程主用。要拼 Advisor、记忆、工具、结构化输出都用它 | 1 |
ChatClient.Builder | defaultSystem / defaultAdvisors / defaultTools / defaultOptions | 把系统提示、通用 Advisor、默认工具在构建时固化,避免每次请求重复写 | 1 |
PromptTemplate | builder().template(...).build() → create(Map) | 提示词带占位符、要动态填参数时(注意 JSON 花括号转义) | 2 |
Message 家族 | SystemMessage / UserMessage / AssistantMessage / ToolResponseMessage | 手动组装多轮消息列表(而非用 advisor)时 | 2 |
Media | new Media(MimeType, Resource/URI/byte[]) | 多模态:把图片/音频作为用户消息附件发送 | 2 |
ChatMemory | add / get / clear;ChatMemory.CONVERSATION_ID | ★ 记忆读写;conversationId 必填,用 .param(CONVERSATION_ID, id) 传 | 2/6 |
MessageChatMemoryAdvisor | builder(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 |
CallAdvisor | adviseCall(request, chain) / getOrder() / getName() | 自己写横切逻辑(日志、权限、限流)时实现 | 3 |
ToolCallingAdvisor | builder().build();可重写 doBeforeCall 等钩子 | ★ 2.0 工具循环由它接管;自定义 Agent 时继承它加审批/预算闸 | 7/9 |
FunctionToolCallback | builder(name, fn).inputType(X.class).build() | ★ 函数式工具;toolNames() 已删,必须用它产出 ToolCallback | 7 |
@Tool / @ToolParam | 注解方法即可;returnDirect 支持结果直出 | 把 Spring bean 的方法暴露成工具(类级/方法级注解) | 7 |
ToolCallbacks | ToolCallbacks.from(bean...) | 把带 @Tool 的 bean 批量转成 ToolCallback[] 再 .tools() | 7 |
ToolContext | 方法参数注入;toolContext.getContext().get("userId") | 工具方法里取请求级上下文(当前用户等) | 7 |
ToolExecutionExceptionProcessor | DefaultToolExecutionExceptionProcessor.builder().alwaysThrow(false) | 工具报错时让模型拿到错误自行补救,而不是 500 | 7 |
@McpTool / @McpToolParam | 注解方法即暴露为 MCP 工具 | ★ 2.0 MCP 服务端用这组注解(替代 1.x 的 @Tool 暴露方式) | 8 |
SyncMcpToolCallbackProvider | builder(mcpSyncClient).build() | ★ 客户端侧必须显式 new 它并 .tools(provider),MCP 工具才可用 | 8 |
MethodToolCallbackProvider | builder().toolObjects(bean).build() | 把本地 bean 方法批量注册成工具 provider(非 MCP 场景) | 8 |
EmbeddingModel | embed(String) / embedForResponse(List) | 把文本转向量(入库或做相似度时用) | 4 |
VectorStore | add / delete / similaritySearch(SearchRequest) | 向量增删查;RAG 检索的核心接口 | 4 |
SearchRequest | builder().query().topK().similarityThreshold().filterExpression() | 构造检索请求;阈值与过滤都在这设 | 4 |
Document | new Document(text, metadata);getText/getMetadata/getScore | 向量库里的一条记录(文本 + 元数据 + 分数) | 4 |
TokenTextSplitter | builder().withChunkSize().withKeepSeparator()...build();apply(docs) | 把长文档切成 chunk 再入库 | 5 |
QuestionAnswerAdvisor | builder(vectorStore).searchRequest().promptTemplate().build() | RAG 最简实现:一个 Advisor 包揽检索+拼上下文+问 | 5 |
RetrievalAugmentationAdvisor | builder().documentRetriever().queryTransformers().queryAugmenter() | 需要查询改写/重排/混合检索等高级 RAG 流水线时 | 5 |
AdvisorParams | AdvisorParams.toolCallingAdvisorAutoRegister(false) | ★ 自研 Agent 循环时关掉自动工具执行,避免双重执行 | 9 |
ObservationRegistry | Observation.createNotStarted(name, registry).observe(...) | 手动打点 / 自定义链路 span | 10 |
MeterRegistry | counter(...) / timer(...) / summary(...) | 埋业务指标(成本、命中率) | 10 |
RelevancyEvaluator | builder().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 的工具「搬」进你的 ChatClient | 2.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 GitHub | https://github.com/spring-projects/spring-ai | 源码与示例;spring-ai-examples 目录很值得读 |
| Maven Central | https://central.sonatype.com | 确认依赖坐标与最新版本号 |
| Spring Initializr | https://start.spring.io | 生成项目骨架(选 Boot 4.1.x / Java 17) |
| MCP 官方文档 | https://modelcontextprotocol.io | 协议规范与官方 Server 列表 |
| MCP Inspector | @modelcontextprotocol/inspector | 调试 MCP Server 的可视化工具 |
| pgvector | https://github.com/pgvector/pgvector | 索引类型与参数调优参考 |
| Milvus 文档 | https://milvus.io/docs | 索引选型与性能调优 |
| Ollama | https://ollama.com/library | 本地可用的模型列表(含中文模型) |
| Prompt Engineering Guide | https://www.promptingguide.ai | 提示词技巧系统梳理,有中文版 |
| Micrometer Tracing | https://docs.micrometer.io/tracing/reference/ | 链路追踪配置参考 |
| DeepSeek 开放平台 | https://platform.deepseek.com | 课程默认模型服务,申请 key / 看用量 |
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,无 bean | FunctionToolCallback.builder(name, fn)... | ★ 2.0 函数式工具唯一正确写法(toolNames() 已删) |
| 工具在另一个进程/语言/第三方 | @McpTool + SyncMcpToolCallbackProvider | ★ 跨进程标准化;必须显式挂 provider |
④ 记忆实现:InMemory vs JDBC vs Redis
| 场景 | 选哪个 | 理由 |
|---|---|---|
| 单测 / 临时 Demo | InMemoryChatMemory | 重启即丢,仅演示 |
| 多用户持久化(课程默认) | JdbcChatMemory | 落库,重启不丢;配 MySQL/PG |
| 高并发、要 TTL | RedisChatMemory | 快、自带过期;需 Redis |
⑤ 向量库选型
| 规模 | 选哪个 | 理由 |
|---|---|---|
| 小(< 百万向量)、有 PG | PgVectorStore | 复用现有 PG,HNSW 索引够用 |
| 大(千万级)、要性能 | Milvus | 专门向量库,IVFLAT/HNSW 可调 |
| 本地试玩 | SimpleVectorStore(内存) | 零依赖,演示用 |
⑥ Advisor 顺序与组合
| 想要的效果 | 挂哪个 Advisor |
|---|---|
| 记忆(在工具循环外) | MessageChatMemoryAdvisor(默认顺序 +200) |
| 工具循环 | ToolCallingAdvisor(默认顺序 +300,链中只能一个) |
| RAG 检索拼接 | QuestionAnswerAdvisor / RetrievalAugmentationAdvisor |
| 工具上百个省 token | ToolSearchToolCallingAdvisor |
| 自研 Agent 循环 | 关自动执行:AdvisorParams.toolCallingAdvisorAutoRegister(false) |
@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」
- 动手前先翻 A.0,确认你要写的 API 在 2.0 里长什么样(尤其记忆、工具、MCP、embedding 配置)。
- 代码里若出现
PromptChatMemoryAdvisor、.toolNames(...)、internalToolExecutionEnabled(...)、embedding.options任一,立刻停手——这是 1.x,编译/运行会出问题。 - 拿不准方法名,回 A.4 按类名查「关键方法」与「什么时候用」。
② 报错时:按关键词定位
- 复制红色堆栈里的关键报错串,Ctrl+F 本页:先在 A.3 按「现象」找;
- 若 A.3 没有,去 A.0 看是不是「1.x 写法在 2.0 被删/改」;
- 还不明白,去 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),且换模型要重灌。
③ 答辩 / 上线前:对照清单
- API 合规:代码里没有 A.0 列出的任何 1.x 写法(重点查记忆/工具/MCP/配置)。
- 排错准备:能口头说出 A.3 里三类最高频报错(conversationId、工具重复执行、MCP 空列表)的原因与解决。
- 方案有据:对 RAG/长上下文/微调、ChatClient/ChatModel、工具实现方式的选择,能用 A.9 讲清理由。
- 安全稳定:过一遍 A.8 四张卡(安全/稳定性/可观测/效果),至少打勾 80%。
- 术语过关:A.6 里带 ★ 的 2.0 术语(ToolCallingAdvisor、FunctionToolCallback、@McpTool、SyncMcpToolCallbackProvider、ConversationId、entity)能用自己的话解释。
这 11 章的内容,本质上是在回答一个问题:怎么把一个「能跑通」的 AI Demo,变成「敢上生产」的系统。
答案不在某个高深的技术里,而在这些看起来很朴素的工程细节中: 身份要从服务端取、检索要有阈值、高风险操作要人工确认、每个 Agent 循环都要有刹车、 每次改动都要跑评测、每个功能都要能被观测。
把这份指南里的每一个「验收标准」都真正做到了,你就已经具备企业级 AI 应用开发的实战能力。 剩下的,交给时间和项目去打磨。