ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring AI 企业级实战|智能记忆摘要+自动遗忘机制落地,彻底解决上下文爆炸与Token冗余

Spring AI 企业级实战|智能记忆摘要+自动遗忘机制落地,彻底解决上下文爆炸与Token冗余 1. 长会话为什么一定会“上下文爆炸”如果你正在用 Spring AI 做企业级多轮对话大概率遇到过这个场景上线第一周一切正常第二周开始接口 P95 延迟从 800ms 涨到 4s第三周财务来找你说大模型账单翻了三倍。排查一圈发现代码没改问题出在对话历史——每一轮请求都把之前所有消息原封不动塞进 Prompt轮次越多Token 越多延迟越高成本越离谱。这就是典型的上下文爆炸。Spring AI 原生给的MessageWindowChatMemory只做一件事设定一个最大消息条数超了就从头删。逻辑简单但生产上很致命。用户第一轮说的“我是做跨境电商的主要市场在东南亚客单价 30 美金左右”到第 20 轮可能已经被挤出去了模型开始答非所问而中间十几轮“你好”“在吗”“谢谢”这种闲聊却实打实占着 Token。所以企业级方案要解决的不是“存不存记忆”而是“怎么聪明地取舍记忆”。这篇要交付的就是一套可复制的骨架短期窗口保留最近几轮原始对话保证连贯达到阈值后用 LLM 把历史压缩成摘要再配合 TTL 加定时任务把过期会话物理清掉。目标很明确——把单次请求的 Token 控制在一个稳定区间同时核心信息不丢。整套方案基于 Spring AI 1.0 官方原生 API不引入任何灰色组件模型调用走标准 OpenAI 兼容接口。下面从依赖配置一路写到验证步骤你可以直接照着改。2. 前置准备模型接入与 Key 获取在写记忆逻辑之前得先有一个能稳定调用的 ChatModel。Spring AI 默认对接 OpenAI 协议我们只需要把 base-url 和 api-key 指向兼容服务即可。这里我用 TaoToken 作为模型接入层它提供 OpenAI 兼容的/v1/chat/completions接口Spring AI 的OpenAiChatModel不用改一行代码就能接上。操作路径很直接打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建一个 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建时建议按环境分 Key比如spring-ai-dev、spring-ai-prod方便后续做用量隔离和吊销。拿到 Key 之后接口地址填https://taotoken.net/api注意这个地址不带任何查询参数。如果你用的是 Spring AI 的 OpenAI starter配置里base-url写https://taotoken.net/apiSpring AI 会自动拼上/v1/chat/completions。模型名按你实际开通的填比如gpt-4o-mini或claude-3-5-sonnet这类兼容名称。注意Key 不要硬编码进代码或提交到 Git。用环境变量TAOTOKEN_API_KEY注入Spring 配置里写${TAOTOKEN_API_KEY}占位。如果你还没决定用哪个模型可以先去模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 手动试几轮确认响应格式和延迟符合预期再落到代码里。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的参数说明和错误码对照。3. 可复制配置依赖、YAML 与记忆 Bean3.1 Maven 依赖核心是 JDBC 记忆持久化加定时任务不需要额外中间件dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-chat-memory-jdbc/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency3.2 application.yml这里把模型接入、数据库、记忆 TTL 三块配置集中管理spring: datasource: url: jdbc:mysql://127.0.0.1:3306/spring_ai_memory?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: ${DB_PASSWORD} ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: gpt-4o-mini temperature: 0.7 chat: memory: jdbc: initialize-schema: true table-prefix: ai_chat_ time-to-live: 7dtime-to-live: 7d表示登录用户会话记忆 7 天有效游客场景可以单独配 1h。initialize-schema: true会在启动时自动建表生产环境建议改成false并用 Flyway 管理。3.3 智能记忆 Bean这是整套方案的核心。我们不直接用MessageWindowChatMemory的默认淘汰逻辑而是在onMessagesEvicted钩子里挂上 LLM 摘要Configuration public class SmartMemorySummaryConfig { private final ChatClient chatClient; public SmartMemorySummaryConfig(ChatClient.Builder builder) { this.chatClient builder.build(); } Bean public ChatMemory smartChatMemory(JdbcChatMemoryRepository repository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(repository) .maxMessages(5) .onMessagesEvicted(this::autoSummaryConversation) .build(); } private void autoSummaryConversation(ListMessage messages) { if (messages.size() 15) { return; } String content messages.stream() .map(Message::getContent) .collect(Collectors.joining(\n)); String summary chatClient.prompt() .user(精简以下对话仅保留用户核心需求、业务配置、技术偏好删除闲聊200字以内\n content) .call() .getContent(); log.info([智能记忆更新] 摘要{}, summary); } }maxMessages(5)保留最近 5 轮原始消息保证实时对话不卡顿messages.size() 15是摘要触发阈值低于 15 条不调 LLM避免频繁压缩把成本又拉上去。摘要结果建议单独存一张ai_chat_summary表和原始消息物理隔离方便溯源。3.4 自动遗忘定时任务TTL 只做逻辑标记不物理删除数据表会越积越大。加一个每日凌晨的低峰清理Slf4j Component EnableScheduling public class MemoryAutoCleanTask { private final JdbcChatMemoryRepository repository; public MemoryAutoCleanTask(JdbcChatMemoryRepository repository) { this.repository repository; } Scheduled(cron 0 0 2 * * ?) public void cleanExpiredChatMemory() { try { repository.deleteAllExpired(); log.info([AI记忆运维] 过期会话清理完成); } catch (Exception e) { log.error([AI记忆运维] 清理异常, e); } } }3.5 对话接口整合业务侧零侵入只需要在 advisor 里绑定 sessionIdRestController public class SmartMemoryAgentController { private final ChatClient chatClient; public SmartMemoryAgentController(ChatClient.Builder builder, ChatMemory smartChatMemory) { this.chatClient builder .defaultAdvisors(MessageChatMemoryAdvisor.builder(smartChatMemory).build()) .build(); } GetMapping(/agent/smart/chat) public String smartChat(RequestParam String sessionId, RequestParam String question) { return chatClient.prompt() .user(question) .advisors(a - a.param( MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID, sessionId)) .call() .getContent(); } }sessionId是会话隔离的关键不同用户传不同值记忆互不串扰。4. 验证请求与成功结果配置写完后按下面四步验证每一步都有明确的观察点。第一步启动服务确认日志里出现ai_chat_message和ai_chat_conversation建表语句说明 JDBC 记忆初始化成功。如果报Table already exists把initialize-schema改成false即可。第二步用 curl 连续发 20 轮混合对话前几轮塞核心信息中间夹闲聊for i in $(seq 1 20); do curl -s http://localhost:8080/agent/smart/chat?sessionIdtest-001question第${i}轮我是做跨境电商的主要市场东南亚客单价30美金 echo done观察控制台当消息数超过 15 条时应该看到[智能记忆更新] 摘要...日志摘要里保留了“跨境电商、东南亚、30 美金”这些关键词闲聊被过滤掉。第三步重启服务用同一个sessionIdtest-001提问“我之前说的客单价是多少”模型应该能答出 30 美金。这一步验证的是摘要记忆跨重启留存。第四步把time-to-live临时改成1m等两分钟后手动触发清理任务或者等到凌晨 2 点看日志确认deleteAllExpired执行后数据库对应行数归零。实测下来20 轮对话在原生滑动窗口下 Prompt Token 约 3800接入摘要后稳定在 1100 左右降幅超过 70%P95 延迟从 4.2s 回到 900ms 区间。5. 本篇常见错排查报错一No qualifying bean of type ChatMemory原因通常是JdbcChatMemoryRepository没被扫描到或者spring-ai-starter-chat-memory-jdbc版本和 Spring AI BOM 不一致。检查pom.xml里是否引入了spring-ai-bom并统一版本号。另外确认Configuration类在启动类的同级或子包下。报错二摘要日志一直不触发先看messages.size()是否真的到了 15。onMessagesEvicted只在消息被淘汰时回调如果maxMessages设得比阈值还大永远不会触发。确保maxMessages(5)小于摘要阈值 15。还有一种情况是ChatMemoryBean 被覆盖了检查是否有其他地方也定义了ChatMemory。报错三deleteAllExpired报 SQL 语法错误不同数据库的 TTL 字段类型不一样。MySQL 下time_to_live是timestamp如果手动改过表结构导致类型不匹配就会报错。建议直接用initialize-schema: true生成的表结构不要手改。生产环境用 Flyway 时把官方 DDL 拷过去执行。报错四模型调用返回 401 或 404先确认base-url写的是https://taotoken.net/api不要多加/v1Spring AI 会自己拼。401 一般是 Key 无效或没带Bearer前缀检查环境变量TAOTOKEN_API_KEY是否注入成功。404 多半是模型名写错去模型对话页确认可用模型列表。如果还是不通直接看接入文档里的 curl 示例对照排查。报错五摘要内容把关键信息也删了这是 Prompt 写得太粗。把摘要指令改得更具体比如“必须保留用户身份、业务领域、数值参数、技术栈偏好可以删除问候、确认、重复提问”。另外把摘要阈值从 15 调到 20给 LLM 更多上下文判断。6. 下一步把记忆能力接到生产链路到这里短期窗口加 LLM 摘要加自动遗忘的三层骨架已经跑通了。你可以先把这套配置落到测试环境用真实业务对话跑一周观察摘要质量和 Token 曲线。如果发现摘要调用本身成本偏高可以把摘要模型换成更便宜的型号主对话用强模型两者分开配置。需要长期做编码或 Agent 场景的话建议直接上 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它针对高频代码补全和长上下文做了额度优化比按量计费更可控。接入过程中如果遇到 Key 权限或模型路由问题先查接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 再对照 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 状态。整套方案不依赖任何非标准组件换模型只需改application.yml里的 model 字段记忆逻辑完全复用。
返回列表