ARTICLE DETAIL

资讯详情

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

AI应用工程化实战:Spring AI + Ollama + RAG全流程指南

AI应用工程化实战:Spring AI + Ollama + RAG全流程指南 1. 从“AI 很酷”到“AI 能用”理解 AI Effect1.1 什么是 AI Effect“AI Effect”在技术圈通常有两种理解一种是心理学意义上的“AI 效应”指的是当某项技术足够成熟、被广泛集成到日常产品后人们就不再称它为“人工智能”比如早期的搜索引擎排序、垃圾邮件过滤、推荐系统今天很少有人会刻意强调“这是 AI”。另一种也是本文更关注的工程视角——AI 技术引入软件系统后带来的开发模式、架构设计、团队协作和部署运维层面的连锁效应。过去两年AI 开发者的关注点逐渐从“做一个聊天机器人 Demo”转向“如何把大模型能力稳定地嵌入业务系统”。这个转变本身就对应着 AI Effect 的显现当技术从概念验证走向生产环境所有工程问题都会被放大——模型响应不稳定、上下文管理复杂、成本难以预估、安全和合规边界模糊、评测和回归困难。你会发现AI 应用开发的第一性原理已经从“模型有多聪明”变成了“系统有多可靠”。1.2 AI 应用开发为什么难传统软件开发的输入和输出都是明确的、结构化的逻辑通过代码显式表达错误可以复现、调试、修复。AI 应用则不同输出不确定同样的 Prompt 可能得到不同回答甚至同一输入在不同时间返回不同结果。知识有边界大模型的训练数据有截止时间无法自动感知企业内部业务数据。上下文敏感同一 Prompt 在不同对话历史、不同角色设定下的表现差异巨大。评测困难功能正确性可以通过断言验证但“回答是否合理”很难自动化断言。成本动态化Token 消耗、模型调用频率、缓存命中率都直接影响生产费用。换句话说AI Effect 带来的核心挑战是用工程化的方法来管理不确定性。这与传统软件工程“消灭不确定性”的思维存在本质冲突。1.3 AI 应用开发的技术栈全景为了应对这些挑战业界逐渐形成了一套相对稳定的技术栈。过去你可能只需要一个 OpenAI API Key现在一个完整的 AI 应用通常包含以下层次层次技术组件作用模型层GPT、Claude、Qwen、Llama 等提供核心推理和生成能力部署层Ollama、vLLM、GPU 服务器本地/私有化模型推理服务编排层Spring AI、LangChain、LlamaIndex管理 Prompt、上下文、工具调用增强层RAG、向量数据库、Embedding 模型注入私有知识、实时数据应用层Web API、Agent、工作流面向用户的具体业务封装运维层链路追踪、评测集、成本监控保障生产可用性和可观测性本文不打算泛泛介绍每一个组件而是围绕一条主线当 AI Effect 进入真实工程环境如何从零搭建一个稳定、可维护、可评估的 AI 应用。我们将以 Java/Spring Boot 技术栈 Python 本地模型部署为例给出完整的实现路径。2. 环境准备本地模型部署与工程项目构建2.1 硬件与操作系统在开始之前先明确一个基本事实大模型推理对算力有硬性要求尤其是本地部署场景。本文的示例将优先使用 Ollama 作为本地推理引擎因为它安装简单、跨平台支持好、对开发者友好。如果你计划在本地跑 7B 参数级别的模型量化版本建议至少满足内存16GB 以上推荐 32GB显卡NVIDIA GPU 显存 6GB 以上可选但强烈推荐操作系统Windows 10/11、macOS 12、主流 Linux 发行版均可如果你没有 GPU也不用担心。Ollama 支持纯 CPU 推理只是速度会明显变慢示例中的小模型仍可以完成验证。AMD 平台的读者注意Ollama 目前对 NVIDIA GPU 支持最完善AMD 显卡需要额外配置 ROCm 环境早期版本会碰到一些兼容问题建议先以 CPU 模式跑通流程再逐步优化。2.2 安装 OllamaOllama 是一个开源的本地大模型运行工具它把模型下载、推理服务封装成了简单的命令行接口。访问官方网站下载对应系统的安装包安装完成后打开终端验证ollama --version如果你的环境还没有安装也可以使用一键安装脚本curl -fsSL https://ollama.com/install.sh | sh安装完成后拉取一个适合入门的小模型。qwen2.5 是阿里推出的中英文模型系列在中文场景下表现稳定3B 版本对资源要求较低ollama pull qwen2.5:3b这个命令会从模型仓库下载模型文件首次执行需要一些时间。下载完成后启动本地服务Ollama 默认会在 11434 端口提供服务ollama serve此时你已经拥有了一个本地的大模型推理服务。可以通过 curl 快速验证curl http://localhost:11434/api/generate -d { model: qwen2.5:3b, prompt: 用一句话介绍你自己, stream: false }返回的 JSON 中会包含模型生成的文本内容。2.3 创建 Java 工程并添加 Spring AI 依赖接下来我们用 Spring AI 把本地模型接入到一个标准 Java 后端应用中。Spring AI 是 Spring 官方推出的大模型集成框架它的设计目标是让 AI 能力像 Spring Data、Spring Integration 一样成为企业级开发的一等公民。使用 Spring Initializr 创建一个 Spring Boot 项目或者直接在你的构建文件中添加依赖。这里以 Maven 为例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId version1.0.0-M4/version /dependency /dependencies注意Spring AI 目前版本迭代非常快不同版本的 API 可能存在差异。上面的版本号只是示例实际开发时建议到 Spring AI 官方文档确认最新的稳定版本。在application.yml中配置 Ollama 连接信息spring: ai: ollama: base-url: http://localhost:11434 chat: model: qwen2.5:3b这样一个最小可运行的 AI 后端工程就搭好了。接下来我们深入讲解配置和编码细节。3. 核心模块拆解从 Prompt 到 RAG 到 Agent3.1 Prompt 管理AI 应用的“接口设计”在传统开发中确定性的代码逻辑通过函数参数交互在 AI 应用中Prompt 就是开发者与模型之间的“接口协议”。一个设计良好的 Prompt 模板比频繁更换模型更有效地提升结果质量。Prompt 设计的核心原则明确角色告诉模型“你是什么身份”例如“你是一位资深 Java 技术专家”。交代任务背景提供足够的上下文不要假设模型“知道”你的业务。定义输出格式要求模型按指定结构输出便于程序解析。给出示例对于复杂任务few-shot 示例比纯文字描述更有效。设置边界明确“不要做什么”避免越界响应。在 Spring AI 中我们可以通过PromptTemplate把 Prompt 模板化// 文件路径src/main/java/com/example/aieffect/service/ChatService.java Service public class ChatService { private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) { this.chatClient builder.build(); } public String explainConcept(String topic) { String template 你是一位耐心的技术导师擅长把复杂概念讲得通俗易懂。 请用 200 字左右解释下面的概念要求包含一个生活化类比 概念{topic} ; Prompt prompt new Prompt( new PromptTemplate(template) .createMessage(Map.of(topic, topic)) ); return chatClient.call(prompt).getResult().getOutput().getContent(); } }这里我使用的是 Spring AI 中相对稳定的ChatClient同步 API。它封装了模型调用的细节让开发者可以像调用普通 Service 一样使用大模型能力。一个容易被忽视的细节Prompt 模板中的占位符命名要语义化并且保持和业务概念一致。当项目规模变大后Prompt 会和代码一起审计、测试、迭代它已经是代码资产的一部分。3.2 上下文管理从单轮对话到多轮对话如果你只需要一个“输入一句话、返回一句话”的工具那直接调用模型就好。但真实业务场景往往需要多轮对话或者需要模型记住之前讨论的内容。大模型本身是无状态的每次调用都是独立事件。多轮对话的实现原理很简单把历史消息拼接进上下文。Spring AI 中的Message接口区分了SystemMessage、UserMessage、AssistantMessage三种角色。public String chatWithHistory(ListMapString, String history, String userInput) { ListMessage messages new ArrayList(); messages.add(new SystemMessage(你是一个友好的中文助手回答简洁准确。)); for (MapString, String turn : history) { messages.add(new UserMessage(turn.get(user))); messages.add(new AssistantMessage(turn.get(assistant))); } messages.add(new UserMessage(userInput)); Prompt prompt new Prompt(messages); return chatClient.call(prompt).getResult().getOutput().getContent(); }需要特别注意的是Token 长度窗口。每个模型都有最大上下文长度例如常见模型支持 8K、32K、128K。当你把越来越多历史消息塞进上下文最终会超过模型窗口限制。工程上通常有三种处理策略截断只保留最近 N 轮对话丢弃更早的内容。摘要把较早的对话用模型生成摘要压缩后放入上下文。向量检索结合 RAG只从历史记录中检索与当前问题相关的片段。关于第三点我们在下一节详细展开。3.3 知识增强RAG 的落地方式RAGRetrieval-Augmented Generation检索增强生成是目前解决“大模型不了解私有数据”问题的主流方案。它的核心思想是先检索再生成。用户提问后系统先从知识库中找到最相关的文档片段把片段和问题一起交给大模型让模型基于这些资料生成答案。一个典型的 RAG 流程包括文档切分把长文档按标题、段落、固定长度切分成多个 chunk。向量化用 Embedding 模型把每个 chunk 转成向量。存储向量和原始文本存入向量数据库。检索用户提问时把问题向量化在向量库中做相似度搜索。生成把检索到的文本片段拼入 Prompt调用大模型生成回答。由于 Spring AI 对向量数据库的集成相对分散且各向量库 API 差异较大这里我给出一个用 Python 实现的简化版 RAG 示例这样你可以先跑通流程再迁移到生产级工具链。# 文件路径rag_basic_demo.py from sentence_transformers import SentenceTransformer import numpy as np import requests # 1. 加载 Embedding 模型 embed_model SentenceTransformer(BAAI/bge-small-zh-v1.5) # 2. 准备文档并切分 documents [ Spring AI 是 Spring 官方推出的大模型应用开发框架。, RAG 通过检索相关文档来增强大模型的回答能力。, 向量数据库用于存储文本的向量表示支持相似度搜索。, Ollama 是一个支持本地部署大模型的推理引擎。, ] chunks [documents[i:i1] for i in range(len(documents))] chunk_texts [doc[0] for doc in chunks] # 3. 向量化并构建简单的内存索引 chunk_vectors embed_model.encode(chunk_texts) def search(query, top_k2): query_vec embed_model.encode([query])[0] scores np.dot(chunk_vectors, query_vec) top_indices np.argsort(scores)[-top_k:][::-1] return [chunk_texts[i] for i in top_indices] # 4. 生成先检索再调用本地模型 def rag_answer(question): related search(question) context \n.join(related) prompt f请根据以下资料回答问题。 资料 {context} 问题{question} 回答 resp requests.post(http://localhost:11434/api/generate, json{ model: qwen2.5:3b, prompt: prompt, stream: False }) return resp.json()[response] # 测试 print(rag_answer(什么是 RAG))这个示例的检索部分使用的是简单的点积相似度生产环境会替换为 FAISS、Milvus、pgvector 等专用向量数据库。但你应当关注的不是工具本身而是 RAG 的关键设计问题切块大小怎么定检索阈值怎么设上下文怎么排优先级这些直接决定了回答质量。3.4 Agent 雏形让模型自动调用工具当模型不再只是“回答问题”而是能够自主决策“下一步做什么”这就进入了 Agent智能体的范畴。Agent 的核心机制是ReAct 模式模型先思考Reason然后决定调用哪个工具Act拿到工具返回值后再继续思考直到完成最终任务。一个最简单的 Agent 设计给模型配备一个“获取天气”的工具函数。模型判断用户问题需要天气数据时会输出一个特殊格式的命令程序解析命令后执行本地函数再把结果返回给模型。// 文件路径src/main/java/com/example/aieffect/agent/SimpleAgent.java Service public class SimpleAgent { private final ChatClient chatClient; public SimpleAgent(ChatClient.Builder builder) { this.chatClient builder.build(); } public String run(String userQuery) { String systemPrompt 你是智能助理。当用户询问天气时你必须先调用工具获取天气然后基于工具结果回答。 工具调用的格式如下 [TOOL_CALL: get_weather] 当用户问题与天气无关时直接正常回答。 ; Prompt prompt new Prompt(new SystemMessage(systemPrompt), new UserMessage(userQuery)); String response chatClient.call(prompt).getResult().getOutput().getContent(); if (response.contains([TOOL_CALL: get_weather])) { // 调用模拟天气 API String weatherInfo getWeatherFromMockApi(); Prompt finalPrompt new Prompt( new SystemMessage(systemPrompt), new UserMessage(userQuery), new SystemMessage(工具返回结果 weatherInfo) ); return chatClient.call(finalPrompt).getResult().getOutput().getContent(); } return response; } private String getWeatherFromMockApi() { // 实际项目中可替换为真实天气 API 调用 return 上海今天多云23摄氏度东南风3级。; } }上面这种方式是把“工具调用”写死在程序逻辑里更灵活的 Agent 框架会在模型中定义工具 Schema由模型自主选择合适的工具。Spring AI 在 1.0 版本中也加入了 Function Calling 支持你可以通过注册Bean的FunctionCallback来暴露工具。注意Agent 是 AI 应用中最有趣也最难控制的部分。模型自主决策意味着错误路径也会被“自主执行”因此必须在设计层限制工具集合、增加人工确认、设置最大迭代次数。4. 完整实战构建一个可运行的“文档问答助手”前面几节我们分别讲了 Prompt、上下文、RAG 和 Agent 的核心概念。现在把它们串联起来实现一个完整的文档问答助手。这个实战案例会用到 Spring Boot Spring AI Ollama完成从依赖配置到接口联调的全过程。4.1 需求分析我们要构建的是“技术文档问答助手”用户向接口提交一个技术问题系统先从本地知识库中检索相关资料再调用本地大模型生成答案。需求拆解为提供POST /api/ask接口接收{ question: ... }。服务启动时加载本地文档目录下的docs文件夹。文档切块后向量化存入内存向量索引简化实现。查询时先检索 Top K 文档片段再作为上下文传入 Prompt。返回格式为{ answer: ..., sources: [片段1, 片段2] }。4.2 项目结构ai-effect-demo/ ├── pom.xml ├── src/main/java/com/example/aieffect/ │ ├── AiEffectApplication.java │ ├── config/ │ │ ├── OllamaConfig.java │ │ └── VectorIndex.java │ ├── controller/ │ │ └── AskController.java │ ├── service/ │ │ ├── RAGService.java │ │ └── DocumentLoader.java │ └── dto/ │ ├── AskRequest.java │ └── AskResponse.java ├── src/main/resources/ │ ├── application.yml │ └── docs/ │ ├── spring-ai-intro.md │ └── rag-principles.md4.3 编写核心代码首先是启动类// 文件路径src/main/java/com/example/aieffect/AiEffectApplication.java SpringBootApplication public class AiEffectApplication { public static void main(String[] args) { SpringApplication.run(AiEffectApplication.class, args); } }接着是文档加载器负责从 resources/docs 目录读取 markdown 文档// 文件路径src/main/java/com/example/aieffect/service/DocumentLoader.java Service public class DocumentLoader { private static final String DOCS_DIR docs/; public ListString loadChunks() throws IOException { ListString chunks new ArrayList(); ResourcePatternResolver resolver new PathMatchingResourcePatternResolver(); Resource[] resources resolver.getResources(classpath: DOCS_DIR *.md); for (Resource resource : resources) { String content new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); chunks.addAll(splitIntoChunks(content, 300)); } return chunks; } private ListString splitIntoChunks(String text, int chunkSize) { ListString chunks new ArrayList(); String[] paragraphs text.split(\\n\\n); StringBuilder currentChunk new StringBuilder(); for (String para : paragraphs) { if (currentChunk.length() para.length() chunkSize currentChunk.length() 0) { chunks.add(currentChunk.toString()); currentChunk new StringBuilder(); } currentChunk.append(para).append(\n\n); } if (currentChunk.length() 0) { chunks.add(currentChunk.toString()); } return chunks; } }然后是简化的向量索引。这里我们不引入外部依赖使用词频向量做语义近似足以演示 RAG 流程// 文件路径src/main/java/com/example/aieffect/config/VectorIndex.java Component public class VectorIndex { private ListString documents new ArrayList(); private ListMapString, Integer termFreqVectors new ArrayList(); public void buildIndex(ListString chunks) { documents chunks; termFreqVectors chunks.stream() .map(this::tokenize) .collect(Collectors.toList()); } public ListString search(String query, int topK) { MapString, Integer queryVector tokenize(query); PriorityQueueScoredDoc pq new PriorityQueue( Comparator.comparingDouble(ScoredDoc::score).reversed() ); for (int i 0; i documents.size(); i) { double score cosineSimilarity(queryVector, termFreqVectors.get(i)); pq.offer(new ScoredDoc(documents.get(i), score)); } return IntStream.range(0, topK) .mapToObj(i - pq.poll()) .filter(d - d.score() 0.01) .map(ScoredDoc::doc) .collect(Collectors.toList()); } private MapString, Integer tokenize(String text) { // 简单中文分词按 2-gram 切分用于演示 MapString, Integer freq new HashMap(); String cleaned text.replaceAll([\\p{Punct}\\s], ); for (int i 0; i cleaned.length() - 1; i) { String gram cleaned.substring(i, i 2); freq.merge(gram, 1, Integer::sum); } return freq; } private double cosineSimilarity(MapString, Integer a, MapString, Integer b) { double dotProduct 0; double normA 0; double normB 0; for (var entry : a.entrySet()) { int freqB b.getOrDefault(entry.getKey(), 0); dotProduct entry.getValue() * freqB; normA entry.getValue() * entry.getValue(); } for (int freq : b.values()) { normB freq * freq; } if (normA 0 || normB 0) return 0; return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } private record ScoredDoc(String doc, double score) {} }RAG 服务负责串联检索和生成// 文件路径src/main/java/com/example/aieffect/service/RAGService.java Service public class RAGService { private final VectorIndex vectorIndex; private final ChatClient chatClient; public RAGService(VectorIndex vectorIndex, ChatClient.Builder builder) { this.vectorIndex vectorIndex; this.chatClient builder.build(); } PostConstruct public void init() throws IOException { DocumentLoader loader new DocumentLoader(); ListString chunks loader.loadChunks(); vectorIndex.buildIndex(chunks); } public AskResponse ask(String question) { ListString sources vectorIndex.search(question, 2); if (sources.isEmpty()) { String answer chatClient.call( 请回答 question ).getResult().getOutput().getContent(); return new AskResponse(answer, List.of(未找到本地知识库资料以上为模型直接回答)); } String context String.join(\n\n---\n\n, sources); String template 你是一名专业的技术支持工程师。请严格基于以下资料回答用户问题。 如果资料中找不到答案请明确说明“资料中未提及”不要编造信息。 资料 {context} 用户问题{question} 请用简洁的中文回答 ; Prompt prompt new Prompt( new PromptTemplate(template) .createMessage(Map.of(context, context, question, question)) ); String answer chatClient.call(prompt).getResult().getOutput().getContent(); return new AskResponse(answer, sources); } }最后是 Controller 和 DTO// 文件路径src/main/java/com/example/aieffect/controller/AskController.java RestController RequestMapping(/api) public class AskController { private final RAGService ragService; public AskController(RAGService ragService) { this.ragService ragService; } PostMapping(/ask) public AskResponse ask(RequestBody AskRequest request) { return ragService.ask(request.question()); } }// 文件路径src/main/java/com/example/aieffect/dto/AskRequest.java public record AskRequest(String question) {}// 文件路径src/main/java/com/example/aieffect/dto/AskResponse.java public record AskResponse(String answer, ListString sources) {}4.4 运行与验证启动应用前确认 Ollama 服务正在运行并且已经拉取了 qwen2.5:3b 模型。然后运行 Spring Boot 启动类控制台应看到 Spring 自动装配日志。使用 curl 测试接口curl -X POST http://localhost:8080/api/ask \ -H Content-Type: application/json \ -d {question: 什么是 RAG}预期返回内容类似{ answer: RAGRetrieval-Augmented Generation是一种将信息检索与文本生成结合的AI技术方案。它的核心流程是先对用户问题做检索从知识库中找到最相关的文档片段再将片段与问题一起发送给大模型让模型基于检索到的资料生成回答。, sources: [ RAG 的核心思想是先检索再生成。用户提问后系统先从知识库中找到最相关的文档片段把片段和问题一起交给大模型让模型基于这些资料生成答案。, RAGRetrieval-Augmented Generation检索增强生成是目前解决\大模型不了解私有数据\问题的主流方案。 ] }注意观察返回结果的sources字段本地知识库中应该预先放入关于 RAG 的介绍文档我们从知识库检索到了对应片段并且模型给出的回答明显是基于这些资料生成的。如果文档中完全没有相关内容模型应当如实回答“资料中未提及”这正是 RAG 相比裸调模型的核心优势——减少幻觉增强可信度。4.5 结果分析与扩展点这个 Demo 展示了 RAG 应用的核心架构文档加载、文本切分、向量化检索、Prompt 增强、模型生成。你可以在此基础上扩展把内存向量索引替换为 Elasticsearch 或 Milvus支持大规模文档。使用真正的 Embedding 模型替换 2-gram 词频向量检索效果会提升一个量级。增加文件上传功能让知识库动态更新。在AskResponse中增加耗时、Token 消耗、模型版本等元信息方便监控。5. 常见问题与排查思路5.1 Ollama 启动失败或连接拒绝问题现象常见原因解决思路curl: Failed to connect to localhost port 11434Ollama 服务未启动执行ollama serve启动服务端口被占用本机其他进程占用了 11434换个端口并同步修改 Spring 配置初次请求耗时过长模型未加载到内存首次推理需要冷启动稍等即可生产环境可用ollama run预热5.2 中文回答质量差或乱码问题现象常见原因解决思路回答出现英文或夹杂拼音选择了英文能力更强的模型而不是中文模型使用 qwen2.5、glm4 等中文优化模型控制台输出乱码终端编码不是 UTF-8Windows 下执行chcp 65001切换编码回答内容与文档无关Prompt 中缺少强制约束模型自由发挥在 Prompt 中强调“仅依据资料回答不要使用外部知识”5.3 Spring Boot 启动时报依赖找不到Spring AI 的部分里程碑版本发布在自定义仓库中如果你的 Maven 无法从中央仓库解析需要在pom.xml中添加 Spring 的里程碑仓库repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshots enabledfalse/enabled /snapshots /repository /repositories5.4 RAG 检索结果不准确问题现象常见原因解决思路返回的回答与资料无关检索阶段没有命中相关文档调整切块大小增加 TopK 值更换更好的 Embedding 模型文档过长被截断切块粒度太大按段落切块控制单块长度在 300~800 字多次提问结果波动Prompt 中列出的文档顺序不同按相关性分数排序后再塞入 Prompt保证顺序稳定5.5 排查 Checklist当 AI 应用出现异常时建议按以下顺序排查确认 Ollama 进程和服务端口正常。用 curl 直接调用 Ollama API排除 Spring 层问题。查看 Spring Boot 日志中是否出现模型调用超时或 5xx 错误。检查 Prompt 模板中的占位符是否被正确替换。确认知识库文档已正确加载向量索引构建无异常。最后才考虑调整模型参数温度、TopP 等。6. AI 应用工程化的最佳实践6.1 安全边界AI 能力必须被“约束”在把 AI 能力接入生产系统时最需要关注的是安全边界。大模型是一个概率系统你无法保证它在所有输入下都表现正确因此不能在关键业务流程中无防护地依赖模型输出。以下是几条硬性建议最小权限原则AI 服务只使用完成业务所需的最小权限。如果 AI 需要查询数据库应为它创建只读账号而不是复用管理员连接。输出校验模型输出在入库、展示、执行前必须经过校验。例如要求模型输出 JSON必须用 JSON 解析器验证格式不能直接信任字符串。敏感信息隔离不要把生产级 API Key 写入代码或配置中心。Ollama 本地服务如果暴露到公网必须加认证层。内容审核对于面向用户的内容生成场景需要接入内容安全审核服务不能只依赖模型自身的对齐能力。6.2 成本控制Token 即金钱大模型的每次调用都在产生成本。工程上常见的成本控制思路包括缓存对相同或相似的请求做结果缓存尤其是无明显时效性的问答。模型分级简单任务用便宜的小模型复杂任务用贵的大模型。上下文瘦身定期清理对话历史防止长对话导致 Token 爆炸。批量处理对于非实时场景可以在夜间低峰批量调用。Spring AI 中可以通过CacheManager或自定义 AOP 切面实现结果缓存// 缓存思路示例用 Spring Cache 注解缓存模型返回结果 Cacheable(value aiAnswers, key #question) public String getAnswer(String question) { return chatClient.call(question).getResult().getOutput().getContent(); }注意这要求 Redis 等缓存组件可用同时要理解“缓存 AI 结果”意味着可能错过模型升级后的质量改进所以缓存策略需要结合业务时效性设计。6.3 可观测性AI 应用的三条日志传统后端日志记录“请求参数、耗时、错误”。AI 应用还需要额外记录Prompt 日志记录发送给模型的完整 Prompt脱敏后存储。这是排查回答质量问题的基础。模型原始响应保留模型未经过后处理的原始输出用于对比和审计。Token 统计每次调用的输入 Tokens、输出 Tokens、模型名、温度等参数。一个完整的调用日志对象大约是这样{ timestamp: 2025-07-11T10:00:00Z, requestId: a1b2c3, model: qwen2.5:3b, prompt: 用户问题..., response: 模型回答..., inputTokens: 1024, outputTokens: 256, latencyMs: 1830, temperature: 0.7 }这些日志既服务于性能分析和成本核算也是后续构建评测集的原始数据来源。6.4 评测体系别让 AI 应用“裸奔”AI 应用最难的工程问题是如何保证质量不退化传统功能有单元测试和断言AI 应用则需要建立针对生成结果的评测集。一个最小可行的评测方案准备 50~100 条典型问题和预期要点。每次模型或 Prompt 变更时用同一批问题跑一遍。人工或调用“评测模型”对回答按要点评分。记录每次变更前后的评分差异作为是否上线的依据。这个过程很耗时但它是 AI 应用从“demo”走向“生产”的关键一步。许多团队上线 AI 功能后出现质量波动根本原因就是没有建立基线评测集。6.5 开发流程建议从技术验证到生产落地最后给出一条清晰的开发路径参考可行性验证用现成模型 业务数据样本手工验证效果是否达标。架构设计明确模型层、编排层、应用层的分工确定 RAG 和 Agent 的必要性。最小实现先做通主链路不要贪多。评测与优化建立评测集测量效果迭代 Prompt 和检索策略。生产加固补充安全、日志、监控、缓存、限流。灰度上线先让内部用户试用确认稳定后再逐步开放流量。7. 总结AI Effect 带来的不仅是新的应用场景更是对软件工程方法论的扩展。本文从一个宏观视角切入落到具体的 AI 应用开发实操希望你掌握的不只是某段代码而是整套决策框架如何选模型、如何设计 Prompt、如何构建 RAG 流程、如何规避工程风险。具体来说今天我们完成了这些事从概念上拆解了 AI 应用开发与传统软件开发的差异。完成了 Ollama 本地模型的部署和验证。搭建了 Spring AI Spring Boot 的基础工程。实现了从单轮对话到多轮对话、再到 RAG 的完整链路。通过一个文档问答助手把“文档加载 — 文本切块 — 向量检索 — 增强生成”全流程跑通。整理了 AI 应用最常见的错误现象和排查思路。从安全、成本、可观测性、评测四个维度建立了 AI 应用工程化的基本认知。AI 技术仍然在快速迭代今天用的一些框架和 API 可能半年后就变了。但工程化的核心问题——如何管理不确定性、如何控制成本、如何保障质量、如何守住安全边界——是稳定不变的。理解了这些你在日后的 AI 应用开发中就不会被眼花缭乱的工具牵着走。如果你正在评估大模型如何融入自己的业务建议先不要急着选框架而是对照本文的步骤先跑通一条最简链路本地模型起来了吗业务数据能切块检索吗最后输出的答案敢给用户看吗这三个问题都解决了再谈架构演进和性能优化你会发现一切都顺理成章。
返回列表