在大模型工程化落地中,记忆(State) 、工具调用(Action) 与 标准协议(Connection) 构成了应用与模型交互的三驾马车。本文从底层存储到协议规范,为你拆解生产级避坑指南与面试高频考点。
一、 ChatMemory:策略层与存储层分离
Spring AI 将"记忆"优雅地拆分成了两层,很多开发者只停留在第一层,容易在深度面试中失分:
-
ChatMemory(策略层):决定留多少条、怎么裁剪。
-
ChatMemoryRepository(存储层):决定存在哪、怎么序列化。
1. 默认配置的"三个致命问题"
默认情况下,Spring AI 使用 MessageWindowChatMemory 配合 InMemoryChatMemoryRepository(基于 ConcurrentHashMap 存在 JVM 堆内存)。
这只能用于 Demo,生产环境必踩以下三个坑:
-
重启即丢:服务发布一次,用户多轮对话全清空。
-
多实例不共享:网关负载均衡到另一台,历史断层。
-
OOM 风险 :
conversationId只增不删,内存无限膨胀。
2. 配置方式一:JDBC 与它的"巨坑"
官方内置了 JDBC 存储方案。配置如下:
yaml
spring:
ai:
chat:
memory:
repository:
jdbc:
initialize-schema: always # 自动建表;生产建议 never,走 DB 变更脚本
🚨 必须知道的巨坑:
官方默认表结构(SPRING_AI_CHAT_MEMORY)只存 content + type + timestamp,不存 toolCalls 和 metadata。
-
后果 :如果你的会话发生过工具调用,从 JDBC 恢复出来的是"残缺"的
AssistantMessage(只有文本,没有toolCalls),后续发请求直接 400(与ToolResponseMessage无法配对)。 -
解法:使用了 Tool Calling 的场景,要么自定义存储结构,要么将工具轮次从记忆中剔除。
3. 配置方式二:自定义存储(企业级方案:Redis)
实现 ChatMemoryRepository 接口,核心方法包括 findByConversationId、saveAll 等。
java
@Override
public void saveAll(String cid, List<Message> messages) {
String key = "ai:chat:memory:" + cid;
// 序列化必须保留完整 Message(含 toolCalls、metadata),不能只存纯文本
redis.opsForList().rightPushAll(key, messages.stream().map(this::toJson).toList());
redis.expire(key, Duration.ofHours(2)); // 必须设 TTL
redis.opsForList().trim(key, -MAX_MESSAGES, -1); // 配合策略层做条数裁剪
}
4. 挂载与最佳实践
java
ChatMemory memory = MessageWindowChatMemory.builder()
.chatMemoryRepository(redisRepository) // 替换默认内存实现
.maxMessages(20)
.build();
ChatClient client = ChatClient.builder(chatModel)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
.build();
client.prompt()
.user(q)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, orderId)) // 会话 ID 由业务决定
.call().content();
-
Advisor 的区别 :
MessageChatMemoryAdvisor(历史作为多条 Message 插入,语义最准) vsPromptChatMemoryAdvisor(历史拼成一段文本塞进 system,兼容性做法但损失结构)。 -
会话 ID 选择 :必须用业务主键 (如工单号、订单号),绝对不要用
sessionId或随机 UUID------否则刷新页面历史就断,且无法做客服回溯。
二、 Tool Calling:明确安全边界与降级策略
LLM 只输出"调用意图",从不执行 。执行权在应用侧(Spring AI 的 ToolCallback),这是核心安全边界。
1. 触发时机与标准降级链
调用工具不只是在"模型想调"的时候,常见场景包括:实时数据(库存、汇率)、精确计算、结构化查库、副作用操作(下单)。
标准降级链(面试重点):
text
向量检索 → 命中则直接答
→ 空/低分 → 调联网搜索工具 → 结果回填 LLM 组织答案
→ 仍为空 → 明确告知“资料未覆盖”,不编造
2. 结果的两种去向
| 去向 | 实现方式 | 适用场景 |
|---|---|---|
| 回填给 LLM 二次组织 | 默认行为,框架自动再调一次模型 | 需要模型消化、总结、多工具串联 |
| 直接返回给用户 | 配置 @Tool(returnDirect = true) |
结果本身就是答案:跳转链接、订单卡片、无需模型改写 |
3. 工程落地 8 条军规
-
description 是 Prompt:写得模糊,模型就会乱调、传错参。
-
服务端强校验 :模型会传
null、负数、超长字符串,绝不信任模型参数。 -
幂等性:模型可能重复调用同一个工具。
-
超时与熔断:外部 API 设 3~5 秒超时,防拖死整个对话。
-
身份隔离 :使用
toolContext传用户身份,绝不用模型参数传userId(防伪造)。 -
循环上限:限制连续调用轮次(如 5 次),防死循环烧 Token。
-
结果截断:工具返回 10 万字会直接打爆上下文,必须截断 + 摘要。
-
人在环(HITL):下单、退款等危险操作,必须拦截并让用户人工确认。
三、 MCP:破除误区与架构视角
1. 核心纠偏:MCP 不需要 Python/Node 环境
错误认知 :"接入 MCP 需要 Python 或 Node 环境"。
正确认知 :MCP 是协议(JSON-RPC 2.0),与语言无关。官方/社区 SDK 覆盖 Java、Python、TS、Go 等。
-
之所以感觉"需要 npx / uvx",是因为部分现成的社区 Server 恰好是 Node/Python 写的。
-
Spring AI 支持 :官方提供了
spring-ai-starter-mcp-server-webmvc等,Java 完全可以直接写 MCP Server,跟现有微服务共用同一套部署和鉴权体系。
2. MCP 的本质:M×N 降为 M+N
MCP 不是"模型的协议",而是**"应用 ↔ 能力提供方"的协议**。类比 LSP(Language Server Protocol)之于编辑器。
-
没有 MCP:M 个 AI 应用 × N 个数据源 = M×N 个定制集成。
-
有了 MCP :M 个 Client + N 个 Server = M+N 个实现。
(注:MCP 并不替代 Function Calling,Function Calling 是"模型让应用调什么",MCP 是"应用去哪找工具、怎么描述工具"。)
3. Server 原语与传输方式
-
三类原语 :
Tools(可调用函数,有副作用)、Resources(只读数据)、Prompts(可复用模板)。 -
传输方式:
-
stdio:本地子进程通信,IDE 插件常用。 -
Streamable HTTP(含旧版 SSE):远程服务,支持鉴权、可水平扩展,企业集成首选。
-
4. Spring AI 集成代码
写 Server(Java 纯后端):
java
@Configuration
public class McpServerConfig {
@Bean
ToolCallbackProvider dtcTools(DtcToolService svc) {
// 加了 @Tool 的方法自动变成 MCP Tool
return MethodToolCallbackProvider.builder().toolObjects(svc).build();
}
}
接 Client(远程调用):
yaml
spring.ai.mcp.client.sse.connections.ats-dtc.url: https://mcp.internal/ats/dtc
连上之后,MCP 的工具会自动注册成 ToolCallback,直接传给 ChatClient 使用。这就是完整的"业务能力 → MCP Server → ToolCallback → 模型调用"链路。
5. MCP 的安全红线
-
防工具投毒 :
description也是 Prompt,可能被注入。 -
远程必须鉴权:OAuth 2.1 / mTLS,不能裸奔。
-
stdio 命令防注入 :
command绝不能由用户控制。 -
路径穿越校验 :
Resources读取要做../校验。 -
能力最小化:一个 Server 只暴露一类能力,别做万能网关。
四、 面试 60 秒高能串讲版
"ChatMemory 分策略和存储两层。策略默认按条数裁剪,存储默认在 JVM 堆里------重启丢、多实例不共享,只能 Demo 用。生产环境我换 JDBC 或自定义 Redis 实现,会话 ID 用业务主键,设 TTL,且必须完整序列化 Message 含 toolCalls ,否则工具调用恢复后请求非法。
Tool Calling 里,模型只输出调用意图,执行在应用侧 ,这是安全边界。触发场景常见于知识库召回为空时的联网兜底。结果默认回填模型二次组织,
returnDirect = true则直接返回。工程上守住:服务端参数校验、幂等、超时熔断、通过toolContext传身份。MCP 的本质是应用与能力提供方的标准协议(JSON-RPC 2.0),把 M×N 集成降成 M+N。它跨语言,用 Spring AI 我完全可以用 Java 写 Server,走 Streamable HTTP 传输,和我们现有的微服务鉴权体系无缝集成。连上后工具自动注册为 ToolCallback,实现模型侧完全解耦。"