Java开发者转型AI工程化Week 2:从核心能力到生产就绪
作者: 一位正在转型的Java开发者
时间: 2026年4月
系列: Java开发者AI工程化转型记录(Week 2/2)
标签: Spring AI, 流式响应, Embedding, RAG, Prompt工程, 生产级架构
前言:Week 1的延续与深化
在Week 1中,我完成了AI工程化的认知建立、Spring AI基础集成、Prompt工程入门以及多模型适配的初步探索。Week 2的核心目标是将这些基础能力深化为生产级实践——流式响应与函数调用、Prompt模板工程化、Embedding与RAG系统、结构化输出、五层架构设计,以及最终的测试策略与性能调优。
如果说Week 1是"让AI跑起来",Week 2就是"让AI稳定、高效、可维护地跑下去"。
Day 1:Chat Models深度实战——流式响应与函数调用
流式响应:从"黑盒等待"到"白盒渐进"
Week 1的API调用是同步阻塞的——发送请求,等待数秒,一次性拿到完整响应。这种体验在用户交互场景中显然不够友好。
Spring AI的ChatClient.stream()返回Flux<ChatResponse>,基于Project Reactor实现非阻塞流式处理:
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(@RequestParam String question) {
return chatClient.prompt()
.user(question)
.stream()
.map(response -> ServerSentEvent.builder(
response.getResult().getOutput().getContent()
).build());
}
核心洞察:流式响应的本质是背压控制的异步数据流。通过SSE(Server-Sent Events)协议,AI生成的每个Token都能实时推送到前端,将"黑盒等待"转化为"白盒渐进体验"。这与传统Web开发中的响应式编程(Reactive Programming)完全一致——Java开发者积累的Reactor经验在这里直接复用。
函数调用:给AI装上"手脚"
更让我兴奋的是FunctionCallback机制。AI不再只是"聊天",而是能主动调用外部工具完成复杂任务:
@Bean
public FunctionCallback weatherFunction() {
return FunctionCallback.builder()
.function("getWeather", new WeatherService())
.description("查询指定城市的实时天气")
.inputType(WeatherRequest.class)
.build();
}
// AI会根据对话上下文智能判断是否需要调用此函数
chatClient.prompt()
.user("北京明天适合出门吗?")
.functions("getWeather")
.call();
这种"模型决策→函数执行→结果整合→最终生成"的闭环,让AI从对话系统升级为行动系统。
工程挑战:函数调用的粒度设计(单一职责原则)、多函数并行编排(旅行规划需要同时查航班、酒店、景点)、部分失败处理(酒店API超时怎么办?)——这些都需要严谨的工程化设计。
上下文管理:Token约束下的优化艺术
大模型的上下文窗口是有限的(如GPT-4的8K/32K/128K),但多轮对话会快速消耗Token。我学到了三层优化策略:
- 动态窗口:保留最近N轮+标记为重要的历史消息
- 摘要压缩:用AI自身压缩旧对话(
SummaryCompressor用AI解决AI问题) - 消息优化:移除冗余格式,压缩重复内容
这让我意识到:传统应用的状态管理(Session/Redis)是确定性的,而AI的上下文管理是资源受限下的概率性优化问题——这正是资深工程师的核心竞争力所在。
Day 2:Prompt模板引擎实战——从字符串到工程资产
超越简单变量替换
Week 1的PromptTemplate只是简单的{variable}替换。Week 2的模板引擎实现了真正的动态生成逻辑
@Component
public class DynamicTemplateFactory {
public Prompt createCodeReviewPrompt(String code, Severity severity) {
StringBuilder template = new StringBuilder();
template.append("你是一位{role},请对以下代码进行评审:\n\n{code}");
// 条件判断:根据严重程度动态调整提示词
if (severity == Severity.STRICT) {
template.append("\n\n请以最高标准审查,关注:安全性、性能、设计模式违规");
} else {
template.append("\n\n请进行常规审查,关注:可读性、潜在Bug、规范符合度");
}
return new PromptTemplate(template.toString())
.create(Map.of("role", "资深架构师", "code", code));
}
}
这种工厂模式+条件逻辑的设计,让同一业务场景(代码评审)能根据参数动态生成"严格模式"或"常规模式"的提示词,实现了Prompt的多态性。
变量治理:生产级模板的安全基石
生产环境不能容忍模板渲染失败。我构建了四层防御体系:
| 防御层 | 机制 | 示例 |
|---|---|---|
| 嵌套变量 | 支持复杂对象结构 | user.address.city |
| 默认值 | 防止空值渲染失败 | {productName:未知产品} |
| 类型转换 | Java对象标准化映射 | VariableValueConverter |
| 防注入 | 正则过滤危险字符 | 过滤{}<>[]防止Prompt Injection |
这让我联想到Thymeleaf/Freemarker的防御性设计——浏览器是确定性执行环境,AI是概率性推理引擎,后者需要更强的安全防护。
可观测性:模板系统的"体检报告"
通过TemplateDebugger记录渲染轨迹,TemplateMetrics收集耗时和成功率,TemplateCacheManager(Caffeine缓存)优化高频创建开销,BatchTemplateRenderer(线程池并行)提升批量吞吐量——这四个维度让模板系统从"功能可用"进化为"生产就绪"。
Day 3:Embedding模型与RAG系统——让AI成为"专才"
Embedding:语义的数学化压缩
Embedding将离散文本映射到固定维度的稠密向量(如1536维),使得语义相似性转化为向量空间中的距离计算(余弦相似度/点积/欧氏距离)。
@Autowired
private EmbeddingClient embeddingClient;
public List<Double> embed(String text) {
return embeddingClient.embed(text);
}
// 语义搜索:将查询向量化后,在向量空间中找最近邻
public List<Document> semanticSearch(String query, int topK) {
float[] queryVector = embeddingClient.embed(query);
return vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(topK)
);
}
这种"理解意思"的计算基础,是RAG、推荐系统、语义搜索的底层基础设施。
RAG:检索增强生成的工程化范式
RAG(Retrieval-Augmented Generation)通过"文档切分→向量化→存储→检索→上下文构建→Prompt增强→生成"的完整链路,让通用大模型获得领域知识能力。
关键工程细节:
- 切分策略:固定长度切分(如1000字符)+ 100-200字符重叠保留上下文连贯性
- top-k选择:平衡召回率与噪声(通常5-10个片段)
- 阈值过滤:置信度控制,过滤低质量匹配
// RAG核心流程
public String ragQuery(String question) {
// 1. 检索相关文档片段
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(question).withTopK(5)
);
// 2. 构建增强Prompt
String context = docs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n---\n"));
Prompt prompt = new PromptTemplate("""
基于以下参考资料回答问题:
{context}
问题:{question}
如果资料不足以回答,请明确说明。
""").create(Map.of("context", context, "question", question));
// 3. 生成回答
return chatClient.prompt(prompt).call().content();
}
感悟:没有RAG的大模型像博览群书的学者,但无法回答"我们公司内部的技术规范";有了RAG,AI变成了配备专业图书馆的顾问。这种"冗余换质量"的工程权衡(文档切分的重叠策略),正是资深开发者的经验所在。
Day 4:输出解析器——给AI装上"类型系统"
StructuredOutputConverter:从不可控到类型安全
AI的输出本质上是自然语言,但业务系统需要结构化数据。StructuredOutputConverter通过 "先约束后转换" 的双阶段模式解决这个问题:
// 定义目标结构
public record Product(String name, BigDecimal price, int stock) {}
// 创建转换器
BeanOutputParser<Product> parser = new BeanOutputParser<>(Product.class);
// 构建Prompt时附加格式指令
Prompt prompt = new PromptTemplate("""
请分析以下商品描述,提取信息:
{description}
{format}
""").create(Map.of(
"description", "iPhone 15 Pro,售价8999元,库存150台",
"format", parser.getFormat() // 自动附加JSON Schema指令
));
// AI会返回结构化JSON,自动映射为Java Bean
Product product = parser.parse(
chatClient.prompt(prompt).call().content()
);
核心洞察:StructuredOutputConverter是AI工程化的 "类型系统" 。过去用OpenAPI/Swagger约束REST接口,现在用JSON Schema约束AI接口——AI的输出终于被纳入了Java的类型安全体系。
分层防御:不追求完美,追求可用
生产环境不能因AI偶尔的格式偏离而崩溃。我设计了四层错误处理:
- 预防层:强化Prompt指令、降低temperature提高确定性
- 清理层:移除markdown代码块标记、正则清洗
- 重试层:清理后再次尝试解析
- 降级层:返回带错误信息的默认对象,而非抛异常
@Component
public class ResilientConverter<T> implements StructuredOutputConverter<T> {
private final StructuredOutputConverter<T> delegate;
private final T defaultValue;
@Override
public T convert(String text) {
try {
return delegate.convert(text);
} catch (Exception e) {
// 尝试清理后重试
String cleaned = cleanText(text);
try {
return delegate.convert(cleaned);
} catch (Exception e2) {
// 返回默认值,确保系统可用性
log.warn("解析失败,返回默认值: {}", e2.getMessage());
return defaultValue;
}
}
}
}
设计哲学:不追求100%的完美解析,而是追求100%的系统可用性——这种优雅降级的思维正是生产级系统的标志。
Day 5:AI工程化项目结构——从Demo到生产
五层架构:超越传统MVC
AI应用不能简单套用传统MVC,我设计了五层架构:
关键设计决策:
- AI服务层独立:将Prompt模板、RAG流程、工具调用封装为领域服务,业务层像调用本地方法一样使用AI能力
- 配置外部化:通过Profile机制(dev/test/prod)和Jasypt加密实现环境隔离与敏感信息保护
- 接口驱动测试:
ChatService接口隔离具体模型,测试环境注入MockChatService
"模型即服务"的思维转变
最启发我的是AI服务层的独立抽象——这与微服务架构中的"数据库即服务"异曲同工。业务层无需关心底层是OpenAI还是通义千问,只面向稳定的契约编程。
Day 6:测试策略与性能调优——生产就绪的最后一公里
分层防御的测试策略
AI服务的概率性本质要求转变测试思维:从"验证精确结果"转向"验证结果结构"和"验证系统行为" 。
| 测试层 | 策略 | 工具 |
|---|---|---|
| 单元测试 | Mockito模拟ChatClient,验证业务逻辑 | Mockito, JUnit 5 |
| 集成测试 | @MockBean覆盖Bean,验证组件协作 | SpringBootTest |
| 基础设施测试 | Testcontainers启动真实PostgreSQL/Redis | Testcontainers |
@ExtendWith(MockitoExtension.class)
class ChatServiceTest {
@Mock
private ChatClient chatClient;
@InjectMocks
private ChatService chatService;
@Test
void shouldReturnStructuredResponse() {
// 给定:模拟AI返回JSON
when(chatClient.prompt(any()).call().content())
.thenReturn("{"name":"iPhone","price":8999}");
// 当:调用业务方法
Product product = chatService.analyzeProduct("iPhone描述");
// 则:验证结构正确,而非精确值
assertThat(product.name()).isNotEmpty();
assertThat(product.price()).isGreaterThan(BigDecimal.ZERO);
}
}
性能优化:削峰填谷与降级容错
线程池配置:
@Bean
public ThreadPoolTaskExecutor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4); // 核心线程
executor.setMaxPoolSize(10); // 最大线程(防雪崩)
executor.setQueueCapacity(100); // 队列缓冲(削峰填谷)
executor.setThreadNamePrefix("ai-");
return executor;
}
Resilience4j三级防护:
- 熔断:失败率阈值50%,连续失败5次打开断路器
- 限流:60请求/分钟,防止API配额耗尽
- 重试:3次间隔1秒,仅对网络错误重试
缓存策略:对确定性Prompt(如代码模板生成)使用SHA-256哈希作为Redis key,实现响应复用,降低API调用成本40%以上。
可观测性:业务+技术双维度
@Component
public class AIPerformanceMonitor {
private final MeterRegistry meterRegistry;
public void recordCall(String model, long durationMs, boolean success, int tokens) {
// 技术维度
Timer.builder("ai.call.duration")
.tag("model", model)
.register(meterRegistry)
.record(durationMs, TimeUnit.MILLISECONDS);
Counter.builder("ai.tokens.consumed")
.tag("model", model)
.register(meterRegistry)
.increment(tokens);
// 业务维度
if (!success) {
Counter.builder("ai.call.errors")
.tag("model", model)
.register(meterRegistry)
.increment();
}
}
}
监控指标:
- 技术维度:P95延迟<300ms,错误率<0.5%,Token消耗量
- 业务维度:用户满意度,服务可用性>99.9%
Week 2复盘:三个核心突破
1. 从"调用API"到"工程化系统"
Week 1关注的是"如何调用AI",Week 2关注的是"如何构建可维护、可测试、可观测的AI系统"。五层架构、防御性编程、分层测试——这些传统软件工程的黄金准则在AI领域完全适用。
2. 从"字符串拼接"到"类型安全契约"
通过StructuredOutputConverter和JSON Schema约束,AI的输出被纳入Java类型系统;通过PromptTemplate的工厂模式和条件逻辑,提示词从硬编码字符串升级为可治理的配置资产。
3. 从"确定性执行"到"概率性推理的工程化"
AI的不确定性不是缺陷,而是需要被工程化管理的特性。通过降级策略、缓存优化、流式响应、弹性设计,我们让概率性系统具备了生产级的可靠性。
待解决的深层问题
Week 2留下了几个值得持续探索的问题:
1. 多Agent/多工具编排模式 复杂场景(如旅行规划)需要并行调用多个函数,如何设计编排策略?是否需要引入Saga模式处理部分失败?
2. RAG高级检索策略 固定长度切分无法处理段落级概念和文档级主题的混合查询,如何设计"文档-段落-句子"多粒度检索架构?
3. AI调用与业务事务的一致性 AI调用是外部HTTP请求(不可逆且付费),数据库操作是本地事务,如何设计跨层最终一致性机制?
项目规划:将知识转化为实践
基于Week 2的学习,我规划了一个智能技术笔记助手项目作为实践载体:
核心功能:
- 内容摘要:使用
ChatClient生成笔记摘要 - 知识点提取:使用
EmbeddingClient向量化,聚类相关知识点 - 结构优化:使用
PromptTemplate规范笔记格式 - 脑图生成:使用
OutputParser生成结构化知识图谱
技术栈:Spring Boot 3.x + Spring AI + PostgreSQL(PGVector)+ Redis + Docker
架构:严格遵循五层架构,AI服务层独立封装,配置外部化,完整的单元测试和集成测试覆盖。
给同行者的建议
如果你正在跟随这个系列学习,Week 2的关键收获是:
AI工程化的本质是传统软件工程能力在新领域的迁移。你不需要成为算法专家,但需要将十年积累的架构设计、防御性编程、测试驱动、性能优化经验,应用到AI调用链路中。
最关键的思维转变:接受不确定性,但用工程化手段约束它;拥抱概率性,但用类型系统和降级策略管理它。