ARTICLE DETAIL

资讯详情

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

Spring AI实战:用Spring Boot快速构建RAG与工具调用AI应用

Spring AI实战:用Spring Boot快速构建RAG与工具调用AI应用 1. Spring AI 不是“另一个 LangChain”它解决的是 Spring 生态里真实存在的缝合痛点你有没有在 Spring Boot 项目里写过这样的代码——先用 RestTemplate 调一个大模型 API再手动把返回的 JSON 解析成对象为了加个 RAG 功能硬塞进一个独立的向量检索服务结果 Controller 层要同时处理 HTTP 请求、数据库查询、向量相似度计算、文本拼接、再发给 LLM更别提工具调用——每次想让模型调用订单查询接口就得自己写一套 Function Calling 的 schema 注册、参数校验、结果注入逻辑最后发现这堆胶水代码比业务逻辑还多。Spring AI 就是为这种场景而生的。它不是试图取代 LangChain 或 LlamaIndex而是在 Spring 的世界里把 AI 能力变成和 DataSource、RestTemplate、TransactionManager 一样自然的基础设施。它的核心价值从来不是“又一个 RAG 框架”而是“让 Java 工程师不用跳出 Spring 思维就能做 AI 集成”。你看它官方文档里反复强调的AiClient、ChatModel、EmbeddingModel、VectorStore这些接口命名方式、生命周期管理、配置习惯和JdbcTemplate、RestClient完全一致。这不是巧合是刻意设计。我去年在一家做 SaaS 餐饮系统的公司落地过 Spring AI当时团队里有 3 个后端没人专门学过 AI但所有人都熟悉 Spring Boot 的自动装配、条件化配置、AOP 切面。我们第一天就跑通了本地 LLMOllama Qwen2的对话第二天接入了 PostgreSQL 的 pgvector 扩展做向量检索第三天就把 ERP 系统里的菜品库存、优惠券规则、会员等级这些结构化数据通过自定义Tool注册成了模型可调用的能力。整个过程没有引入任何新概念所有配置都写在application.yml里所有 Bean 都由Configuration类声明所有异常都走 Spring 的ExceptionHandler统一处理。这才是它真正的“吃透”门槛你不需要成为 AI 专家但必须是一个合格的 Spring 开发者。所以当你看到“Spring AI 教学”“Spring AI 实战”这类热搜词时背后的真实需求其实是“如何用我每天写的 Spring 代码而不是从头学一套 Python 工具链来快速构建一个带知识库、能调 API、支持多轮上下文的 AI 应用” 这就是本文要拆解的全部——不讲抽象理论只讲你在 IntelliJ 里新建一个 Spring Boot 项目后接下来该敲哪几行代码、改哪几个配置、踩哪些坑、为什么这么选。2. RAG 不是“扔进去就完事”从切块策略到向量库选型的工程闭环RAG 的本质是把“外部知识”变成模型能理解的“上下文提示”。但很多人卡在第一步知识还没进系统就已经失真了。我在实际项目里见过最典型的错误就是直接把整份 PDF 或 Word 文档丢给TextSplitter然后用默认的RecursiveCharacterTextSplitter切成 1000 字符的 chunk。结果呢一段完整的菜品制作流程被硬生生切成三段第一段是“步骤一将五花肉切丁”第二段是“步骤二热锅冷油放入葱姜蒜爆香”第三段是“步骤三下入肉丁翻炒至变色”模型根本无法理解这是连贯的操作。这就是典型的“切块失真”。2.1 切块策略语义完整性 字符长度Spring AI 本身不提供切块逻辑它依赖langchain4j的TextSplitter。但关键在于怎么选、怎么配。我们最终采用的方案是Bean public TextSplitter textSplitter() { return new MarkdownHeaderTextSplitter( Map.of( #, Header1, ##, Header2, ###, Header3 ) ); }为什么是 Markdown因为我们所有的知识源菜品手册、SOP 流程、客服话术都是用 Markdown 写的标题层级天然表达了语义结构。MarkdownHeaderTextSplitter会以#为一级分隔确保每个 chunk 至少包含一个完整的小节。比如“【会员权益】”这个二级标题下的所有内容无论多长都会被保留在同一个 chunk 里。实测下来平均 chunk 长度在 800~1200 字符但语义完整率从 42% 提升到 96%。提示如果你的知识源是纯文本或 PDF不要迷信“按字符切”。先用PdfDocumentReader提取文本再用SentenceSplitter按句号、问号、感叹号切分最后用TokenTextSplitter基于 tiktoken按 token 数限制合并。Spring AI 的EmbeddingModel默认用text-embedding-3-small其 tokenizer 和 OpenAI 一致这样能保证切块和嵌入的一致性。2.2 向量库选型不是“越贵越好”而是“越贴合 Spring 生态越好”网络上总在争论“向量库该用什么”Milvus、Weaviate、Qdrant、pgvector……但对 Spring 开发者来说答案其实很朴素优先选你能用 JPA/Hibernate 直接操作的其次选有成熟 Spring Data Starter 的。我们对比过三种方案方案配置复杂度Spring 集成度本地开发体验生产部署成本我们的结论pgvector (PostgreSQL)★★☆☆☆ (需启用扩展)★★★★★ (Spring Data JDBC/ORM 全支持)★★★★★ (Docker 一键启动)★★☆☆☆ (无需额外服务)首选—— 用spring-boot-starter-data-jdbcpgvector扩展VectorStore实现类只需 20 行代码Qdrant (独立服务)★★★★☆ (需单独部署)★★★☆☆ (官方 Spring Data Starter 未 GA)★★★☆☆ (需维护 Docker Compose)★★★☆☆ (需额外服务器)备选 —— 适合已有 Qdrant 集群的团队内存版 InMemoryVectorStore★☆☆☆☆ (零配置)★★★★★★★★★★★☆☆☆☆仅用于单元测试和原型验证最终选择 pgvector原因非常实际我们的主数据库就是 PostgreSQL运维团队已经有一套成熟的备份、监控、扩容方案。新增一个vector类型的列就像加一个jsonb字段一样简单。VectorStore的实现核心代码如下Component public class PgVectorStore implements VectorStore { private final JdbcTemplate jdbcTemplate; public PgVectorStore(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override public void add(ListDocument documents) { String sql INSERT INTO document_embeddings (id, content, metadata, embedding) VALUES (?, ?, ?, ?::vector); for (Document doc : documents) { // 1. 先调用 EmbeddingModel 生成向量 ListDouble vector embeddingModel.embed(doc.getContent()); // 2. 插入 pgvector 表 jdbcTemplate.update(sql, doc.getId(), doc.getContent(), new ObjectMapper().writeValueAsString(doc.getMetadata()), vector.toString()); // pgvector 接受 [0.1,0.2,...] 格式 } } Override public ListDocument similaritySearch(String query, int topK) { ListDouble queryVector embeddingModel.embed(query); String sql SELECT content, metadata FROM document_embeddings ORDER BY embedding ?::vector LIMIT ? ; return jdbcTemplate.query(sql, (rs, rowNum) - { Document doc new Document(rs.getString(content)); doc.setMetadata(new ObjectMapper().readValue(rs.getString(metadata), Map.class)); return doc; }, queryVector.toString(), topK); } }这段代码的关键点在于它完全复用了 Spring 的JdbcTemplate事务、连接池、异常翻译全部由 Spring 管理。你不需要懂 pgvector 的 C 语言扩展原理只需要知道是余弦相似度操作符::vector是类型转换。这就是 Spring AI 的哲学把 AI 的复杂性封装在你已有的技术栈之下。2.3 RAG 检索增强不只是“找最像的”而是“找最相关的”单纯靠向量相似度排序很容易召回噪音。比如用户问“今天有什么优惠”向量检索可能返回一堆“满减活动细则”但真正需要的是“今日限时 8 折的海鲜套餐”。这就需要混合检索Hybrid Search。pgvector 本身支持全文检索to_tsvector和向量检索的结合。我们在document_embeddings表里加了一个tsv列ALTER TABLE document_embeddings ADD COLUMN tsv tsvector; UPDATE document_embeddings SET tsv to_tsvector(chinese, content); CREATE INDEX idx_tsv ON document_embeddings USING GIN(tsv);然后修改similaritySearch方法Override public ListDocument similaritySearch(String query, int topK) { ListDouble queryVector embeddingModel.embed(query); String sql SELECT content, metadata FROM document_embeddings WHERE tsv plainto_tsquery(chinese, ?) -- 先做关键词过滤 ORDER BY 0.7 * (embedding ?::vector) 0.3 * (ts_rank(tsv, plainto_tsquery(chinese, ?))) DESC LIMIT ? ; // ... 执行查询 }这里用了 0.7:0.3 的权重实测下来效果最好。关键词过滤能排除完全无关的文档ts_rank给关键词匹配度打分给语义相似度打分加权后排序。这个方案不需要引入 Elasticsearch纯 pgvector 就搞定且完全兼容 Spring 的数据访问层。3. 工具调用不是“函数注册”而是“领域服务编排”很多教程把工具调用Function Calling讲成“注册一个方法模型就会调”。但真实业务中问题远比这复杂。比如在餐饮 SaaS 里“查询订单”这个工具背后涉及权限校验当前用户能否查这个订单数据脱敏不能把顾客手机号、地址全返回错误映射数据库查不到订单是返回空还是抛异常上下文传递用户说“查我昨天的订单”模型需要把“我”解析成当前登录用户 IDSpring AI 的Tool接口本质上就是让你把一个 Spring Service 方法包装成模型可识别的 Schema。但关键在于包装的粒度和契约设计。3.1 Tool 设计一个 Tool 对应一个明确的业务意图而非一个 DAO 方法错误示范// ❌ 这不是一个好 Tool Tool public Order findOrderById(Long id) { ... }问题在于模型不知道id从哪来。用户不会说“调用 findOrderById参数是 12345”而是说“帮我查一下订单号是 ORD-2024-XXXXX 的状态”。所以 Tool 的参数必须是模型能自然推理出的字段。正确做法Tool public record QueryOrderTool( Description(顾客的手机号用于唯一标识用户) String phone, Description(订单号格式如 ORD-2024-XXXXX可选) String orderNo, Description(查询日期范围格式 YYYY-MM-DD可选) String dateRange ) implements Tool { Override public String invoke(MapString, Object arguments) { // 1. 从 phone 查用户 ID权限校验在此 Long userId userService.findIdByPhone(arguments.get(phone).toString()); // 2. 构建查询条件 OrderQuery query new OrderQuery(); query.setUserId(userId); if (arguments.containsKey(orderNo)) { query.setOrderNo(arguments.get(orderNo).toString()); } if (arguments.containsKey(dateRange)) { query.setDateRange(arguments.get(dateRange).toString()); } // 3. 调用业务 Service返回脱敏后的 JSON ListOrderSummary orders orderService.queryByCondition(query); return new ObjectMapper().writeValueAsString(orders); } }这个QueryOrderTool的 Schema 会被 Spring AI 自动转成 OpenAI 的 function schema{ name: QueryOrderTool, description: 查询用户的订单信息, parameters: { type: object, properties: { phone: {type: string, description: 顾客的手机号用于唯一标识用户}, orderNo: {type: string, description: 订单号格式如 ORD-2024-XXXXX可选}, dateRange: {type: string, description: 查询日期范围格式 YYYY-MM-DD可选} }, required: [phone] } }模型看到这个 schema就知道它需要提取phone而orderNo和dateRange是可选的。这比让它猜“id”是什么靠谱太多了。3.2 工具执行链用 Spring 的 AOP 和事务管理兜底Tool 的invoke方法本质上就是一个 Spring Bean 的方法调用。这意味着你可以用Transactional控制事务用Cacheable缓存结果用Retryable重试失败调用。我们在线上环境给所有 Tool 方法加了统一的熔断器Component public class ToolExecutionAspect { private final CircuitBreaker circuitBreaker CircuitBreaker.ofDefaults(tool-circuit-breaker); Around(annotation(org.springframework.ai.tool.Tool)) public Object executeWithCircuitBreaker(ProceedingJoinPoint joinPoint) throws Throwable { return circuitBreaker.executeSupplier(() - { try { return joinPoint.proceed(); } catch (Exception e) { throw new ToolExecutionException(Tool execution failed, e); } }); } }当订单查询服务不可用时熔断器会在连续失败 3 次后打开后续请求直接返回预设的友好提示如“订单系统暂时繁忙请稍后再试”而不是让模型拿到一个空字符串或错误堆栈。这才是生产级的工具调用。3.3 多工具协同不是“模型自己选”而是“我们引导模型选”模型有时会乱调工具。比如用户问“我的会员等级是多少”它可能先调QueryOrderTool再调QueryUserTool最后才得到答案。这不仅慢还可能触发不必要的权限检查。我们的解决方案是在 Prompt 中显式约束工具调用顺序。Spring AI 的SystemPromptTemplate支持变量注入Bean public SystemPromptTemplate systemPromptTemplate() { return new SystemPromptTemplate( 你是一个专业的餐饮 SaaS 客服助手。 你的能力包括 - 查询用户基本信息使用 QueryUserTool - 查询用户订单使用 QueryOrderTool - 查询菜品详情使用 QueryDishTool 请严格遵守以下规则 1. 如果问题涉及“我”、“我的”必须先调用 QueryUserTool 获取用户 ID 2. 如果问题涉及“订单”、“购买”在获得用户 ID 后再调用 QueryOrderTool 3. 如果问题涉及“菜品”、“菜单”直接调用 QueryDishTool。 不要跳过步骤也不要调用无关工具。 ); }这个 prompt 会被注入到每次 Chat 请求的 system message 中。实测下来工具调用准确率从 68% 提升到 94%。这不是在教模型“思考”而是在给它一份清晰的 SOP。4. 从单次问答到智能体Agentic RAG 的 Spring 式落地“Agentic RAG”这个词最近很火但它在 Spring 生态里其实就两件事状态管理State和决策循环Loop。LangChain 的 Agent 是一个复杂的Runnable链而 Spring AI 的AiClient本身不提供 Agent 抽象但你可以用 Spring 的Scope(prototype)Bean 和EventListener轻松实现。4.1 状态管理用 ThreadLocal Session 存储上下文多轮对话的核心是记住“上一轮发生了什么”。我们没用 Redis 或数据库存 session而是用 Spring 的SessionRegistry 自定义ConversationStateComponent Scope(prototype) public class ConversationState { private final String sessionId; private final ListMessage history new ArrayList(); private final MapString, Object context new HashMap(); // 存放中间结果如 user_id, last_order_id public ConversationState(String sessionId) { this.sessionId sessionId; } public void addMessage(Message message) { history.add(message); // 只保留最近 10 轮避免上下文爆炸 if (history.size() 10) { history.remove(0); } } // getter/setter... } Service public class ConversationService { private final SessionRegistry sessionRegistry; public ConversationState getOrCreateState(String sessionId) { return (ConversationState) sessionRegistry.getSessionAttributes(sessionId) .computeIfAbsent(state, k - new ConversationState(sessionId)); } }每次请求进来sessionId从 Header 或 Cookie 里读取ConversationState就是当前对话的“大脑”。它和AiClient完全解耦你可以把它存在内存、Redis、甚至数据库里只要getOrCreateState方法能拿到就行。4.2 决策循环用事件驱动代替硬编码 Loop传统 Agent 的 while 循环while not final answer很难和 Spring 的异步、响应式编程融合。我们的做法是把每一次“模型输出”当作一个事件由事件监听器决定下一步。Component public class ConversationEventListener { private final AiClient aiClient; private final ConversationService conversationService; EventListener public void onMessageReceived(ChatResponseEvent event) { ConversationState state conversationService.getOrCreateState(event.getSessionId()); Message userMessage event.getUserMessage(); // 1. 将用户消息加入历史 state.addMessage(userMessage); // 2. 构建带历史的 Prompt ListMessage fullHistory new ArrayList(state.getHistory()); fullHistory.add(userMessage); // 3. 调用 AiClient ChatResponse response aiClient.chat(fullHistory); // 4. 检查是否需要工具调用 if (response.getToolCalls() ! null !response.getToolCalls().isEmpty()) { // 执行工具调用并发布新事件 ToolResult result executeTool(response.getToolCalls().get(0)); state.addMessage(new ToolMessage(result.getContent())); // 发布 ToolResultEvent触发下一轮 ApplicationEventPublisher.publishEvent(new ToolResultEvent(state.getSessionId(), result)); } else { // 直接返回最终答案 state.addMessage(response.getResult().getMessage()); // 发布 FinalAnswerEvent ApplicationEventPublisher.publishEvent(new FinalAnswerEvent(state.getSessionId(), response.getResult().getMessage().getContent())); } } private ToolResult executeTool(ToolCall toolCall) { // 根据 toolCall.getName() 反射调用对应 Tool 的 invoke 方法 // ... } }这个设计的好处是完全异步、可扩展、易测试。你可以为ToolResultEvent写单独的监听器做日志审计、性能监控、甚至调用外部风控服务。整个 Agent 的“思考”过程变成了 Spring 事件总线上的消息流转而不是一个阻塞的 while 循环。4.3 Ontology RAG用领域本体固化知识结构“Ontology RAG”听起来很学术但在工程上它就是用预定义的 Schema 去约束知识入库和检索。比如在餐饮领域我们定义了核心实体Dish菜品id, name, category, price, ingredients, allergensOrder订单id, status, items[], total, createdAtUser用户id, phone, level, points所有知识源PDF、Excel、数据库在入库前都必须映射到这些实体。我们写了一个OntologyMapperComponent public class OntologyMapper { public Document mapToDish(Document rawDoc) { // 从 rawDoc.getContent() 中提取 name, price, ingredients 等字段 // 用正则、NLP 规则、甚至小模型做实体识别 Dish dish new Dish(); dish.setName(extractName(rawDoc.getContent())); dish.setPrice(extractPrice(rawDoc.getContent())); dish.setIngredients(extractIngredients(rawDoc.getContent())); // ... return new Document( objectMapper.writeValueAsString(dish), // content 存 JSON Map.of(entity_type, Dish, source, rawDoc.getMetadata().get(source)) ); } }这样向量库里的每一条记录都带有一个entity_type标签。检索时可以加过滤条件// 用户问“有哪些不含花生的菜品” String query 不含花生的菜品; ListDocument results vectorStore.similaritySearch(query, 5); // 过滤出 entity_type Dish 的结果 results results.stream() .filter(doc - Dish.equals(doc.getMetadata().get(entity_type))) .filter(doc - !new ObjectMapper().readValue(doc.getContent(), Dish.class).getAllergens().contains(花生)) .collect(Collectors.toList());这就是 Ontology RAG 的威力它把“语义检索”变成了“结构化查询 语义检索”的混合体准确率远高于纯向量匹配。5. 实战避坑指南那些官网不会告诉你的 Spring AI 坑Spring AI 的文档很简洁但真实落地时有 5 个坑几乎每个团队都会踩而且踩得特别深。5.1 坑一Maven 依赖版本冲突——Spring Boot 3.2 和 Spring AI 1.0.x 的 classpath 战争这是最普遍的坑。Spring AI 1.0.x 默认依赖spring-boot-starter-webflux而你的项目如果用的是spring-boot-starter-webServlet 容器就会出现Reactor和Servlet的WebClient/RestTemplate冲突。表现是AiClient初始化失败报NoSuchBeanDefinitionException: WebClient。解决方案强制排除 webflux显式引入 webdependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M5/version exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency注意1.0.0-M5是目前2024 年中最稳定的预发布版正式版1.0.0修复了这个问题但尚未 GA。别盲目升级到1.0.0-SNAPSHOT那个版本有新的 ClassLoader 问题。5.2 坑二EmbeddingModel 的 batch size 设置不当——OOM 和超时的根源默认的OpenAiEmbeddingModel一次只 embed 1 个文本。但如果你要批量导入知识库1000 个文档就要发 1000 次 HTTP 请求既慢又容易被限流。解决方案配置batchSizespring: ai: openai: embedding: batch-size: 100 # 一次最多发 100 个文本但注意batch-size不是越大越好。OpenAI 的/embeddings接口有 token 限制2048 tokens per request。如果你的 chunk 平均长度是 500 字符100 个就是 50000 字符远超限制。我们实测的最佳值是32既能利用批量优势又不会触发429 Too Many Requests。5.3 坑三Tool 的参数名必须和 model 的 function calling schema 100% 匹配——大小写和下划线是魔鬼Spring AI 会把Tool方法的参数名原样映射到 JSON Schema 的propertieskey。如果你的方法是Tool public String queryOrder(Description(订单号) String orderNo) { ... }生成的 schema 是properties: { orderNo: { type: string, description: 订单号 } }但 OpenAI 的 function calling 返回的参数是order_no下划线风格。模型调用时就会传{order_no: ORD-123}而你的方法期待orderNo结果orderNo是 null整个调用失败。解决方案用JsonProperty强制指定Tool public String queryOrder( JsonProperty(order_no) Description(订单号) String orderNo) { ... }或者更彻底地在application.yml里全局配置 Jacksonspring: jackson: property-naming-strategy: SNAKE_CASE这样所有orderNo都会序列化成order_no和 OpenAI 的约定一致。5.4 坑四本地开发用 Ollama千万别信ollama pull的默认 tag——qwen2:7b 和 qwen2:latest 是两回事Ollama 的qwen2:latest指向的是qwen2:7b但qwen2:7b的 context length 只有 32k而qwen2:14b是 128k。如果你的知识库 chunk 很长或者对话历史很长7b版本会直接 truncation导致 RAG 失效。解决方案明确指定 tagollama pull qwen2:14b # 启动时指定 OLLAMA_HOST0.0.0.0:11434 ollama run qwen2:14b并在 Spring AI 配置里写死spring: ai: ollama: chat: options: model: qwen2:14b5.5 坑五Spring AI 的AiClient不是线程安全的——高并发下的静默失败AiClient的chat()方法内部会缓存ChatRequest如果多个线程共用一个AiClientBean就会互相覆盖messages导致 A 用户的请求里混入了 B 用户的历史。解决方案永远用Scope(prototype)Bean Scope(prototype) public AiClient aiClient() { return new AiClient(chatModel, embeddingModel); }或者更推荐的方式用AiClient.Builder每次创建新实例Service public class AiService { private final ChatModel chatModel; private final EmbeddingModel embeddingModel; public AiService(ChatModel chatModel, EmbeddingModel embeddingModel) { this.chatModel chatModel; this.embeddingModel embeddingModel; } public ChatResponse chat(ListMessage messages) { // 每次都新建 AiClient确保线程安全 AiClient client AiClient.builder() .chatModel(chatModel) .embeddingModel(embeddingModel) .build(); return client.chat(messages); } }这个坑最隐蔽因为不是报错而是返回错误的结果。我们线上压测时QPS 到 50 就开始出现“用户 A 问订单返回用户 B 的订单”查了三天才发现是AiClient共享的问题。6. 项目收尾一个可运行的 Spring AI RAG Demo 结构最后给你一个最小可行的项目结构你可以直接 clone 下来跑通spring-ai-rag-demo/ ├── pom.xml ├── src/main/java/com/example/demo/ │ ├── DemoApplication.java │ ├── config/ │ │ ├── AiConfig.java # AiClient, ChatModel, EmbeddingModel 配置 │ │ └── VectorStoreConfig.java # PgVectorStore 配置 │ ├── model/ │ │ └── Document.java # Spring AI 的 Document │ ├── service/ │ │ ├── RagService.java # RAG 主流程切块 - 嵌入 - 存储 - 检索 │ │ ├── ToolService.java # 所有 Tool 的实现 │ │ └── ConversationService.java # 多轮对话状态管理 │ ├── controller/ │ │ └── AiController.java # /api/chat 接口 │ └── dto/ │ └── ChatRequest.java # { sessionId: ..., message: ... } └── src/main/resources/ ├── application.yml # 所有配置openai key, pgvector url, ollama host └── static/ └── index.html # 简单的聊天界面pom.xml的关键依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI 核心 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-openai-spring-boot-starter/artifactId version1.0.0-M5/version exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /exclusion /exclusions /dependency !-- 向量库 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId /dependency dependency groupIdio.github.givimad/groupId artifactIdpgvector-spring-jdbc/artifactId version0.9.0/version /dependency !-- 切块工具 -- dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-core/artifactId version0.30.0/version /dependency /dependenciesapplication.yml的最小配置spring: datasource: url: jdbc:postgresql://localhost:5432/rag_demo username: postgres password: password ai: openai: api-key: sk-xxx # 你的 OpenAI key chat: options: model: gpt-4o-mini embedding: batch-size: 32 ollama: base-url: http://localhost:11434 chat: options: model: qwen2:14b server: port: 8080跑起来之后访问http://localhost:8080输入“今天有什么优惠”它会用MarkdownHeaderTextSplitter切块你的优惠文档用qwen2:14b生成向量存入 pgvector用混合检索关键词 向量找到“今日限时 8 折”如果你接着问“这个优惠能用在海鲜套餐上吗”它会调用QueryDishTool查海鲜套餐详情最终给出完整回答。整个过程没有一行代码脱离 Spring 的范式。这就是“吃透”的意思它不是让你学一门新语言而是让你用最熟悉的 Spring去驾驭最前沿的 AI 能力。我在实际项目里从零开始搭建这套系统前后花了 3 天。第一天搭环境、跑通 Hello World第二天集成 RAG 和 pgvector第三天加上工具调用和多轮对话。团队里最资深的 Java 工程师只花了半天就看懂了全部代码。这背后没有魔法只有对 Spring 生态的深刻理解和对 AI 工程化的务实态度。如果你也正在评估 Spring AI别被“RAG”“Agent”这些词吓住就从pom.xml里加一行依赖开始然后照着这篇的路径一行一行敲下去。你会发现所谓“AI 原生应用”不过是把RestController换成了AiController把JdbcTemplate换成了VectorStore仅此而已。
返回列表