第2069篇:LangChain4j进阶——@AiService的原理与高级用法
2026/4/30大约 8 分钟
第2069篇:LangChain4j进阶——@AiService的原理与高级用法
适读人群:正在使用或准备使用LangChain4j的Java工程师 | 阅读时长:约19分钟 | 核心价值:深入理解@AiService的代理机制,掌握复杂场景下的高级配置和扩展方法
@AiService是LangChain4j最优雅的设计之一。你定义一个接口,加几个注解,就能得到一个完整的AI Service——内部的Prompt构建、LLM调用、结果解析全都帮你做了。
但大多数人用了一段时间后会发现,默认配置在复杂场景下有各种限制。这篇文章深入@AiService的机制,讲那些文档里说不清楚的部分。
@AiService的代理机制
先明白它是怎么工作的,才知道怎么扩展:
/**
* 理解@AiService的各种配置选项
*/
// 最基础的用法
@AiService
public interface SimpleAssistant {
String chat(String message);
}
// 带System Prompt的版本
@AiService
@SystemMessage("你是一个专业的技术助手,擅长Java和AI工程。")
public interface TechAssistant {
// @P注解控制Prompt中的变量填充
@UserMessage("请用{{level}}的级别解释:{{question}}")
String explainConcept(
@P("level") String expertLevel, // beginner/intermediate/expert
@P("question") String question
);
}
// 带记忆的版本
@AiService
@SystemMessage("你是用户的私人助理。")
public interface PersonalAssistant {
// @MemoryId:每个用户有独立的对话记忆
String chat(@MemoryId String userId, @UserMessage String message);
}自定义@AiService的高级配置
/**
* 通过AiServices.builder()进行精细化配置
* 这是比注解配置更灵活的方式
*/
@Configuration
@RequiredArgsConstructor
public class AiServiceConfig {
private final ChatLanguageModel llm;
private final EmbeddingModel embeddingModel;
private final EmbeddingStore<TextSegment> vectorStore;
/**
* 手动构建AI Service,精细控制每个组件
*/
@Bean
public CustomerServiceAssistant customerServiceAssistant() {
// 1. 构建记忆工厂(每个用户独立实例)
ChatMemoryProvider memoryProvider = memoryId ->
MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(new RedisChatMemoryStore())
.build();
// 2. 构建检索增强器
EmbeddingStoreContentRetriever retriever =
EmbeddingStoreContentRetriever.builder()
.embeddingStore(vectorStore)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.7)
.build();
// 3. 构建工具集(可以是Spring Bean)
CustomerServiceTools tools = applicationContext.getBean(CustomerServiceTools.class);
return AiServices.builder(CustomerServiceAssistant.class)
.chatLanguageModel(llm)
.chatMemoryProvider(memoryProvider)
.contentRetriever(retriever)
.tools(tools)
// 4. 设置响应增强器(可以在这里做后处理)
.retrievalAugmentor(buildCustomRetriever())
.build();
}
/**
* 自定义检索增强器:多路检索 + 重排序
*/
private RetrievalAugmentor buildCustomRetriever() {
// 内容检索器
EmbeddingStoreContentRetriever contentRetriever =
EmbeddingStoreContentRetriever.from(vectorStore, embeddingModel);
// 查询转换器:先扩展查询再检索
QueryTransformer queryExpander = query -> {
// 生成多个相关查询
List<String> expandedQueries = expandQuery(query.text());
return expandedQueries.stream()
.map(q -> Query.from(q, query.metadata()))
.toList();
};
return DefaultRetrievalAugmentor.builder()
.queryTransformer(queryExpander)
.contentRetriever(contentRetriever)
.build();
}
private List<String> expandQuery(String originalQuery) {
// 简单的查询扩展
return List.of(
originalQuery,
originalQuery.replace("怎么", "如何"),
originalQuery + " 解决方案"
);
}
}处理复杂返回类型
/**
* @AiService支持返回结构化对象
* LangChain4j会自动解析JSON格式的LLM输出
*/
@AiService
@SystemMessage("你是一个情感分析专家。请以JSON格式返回分析结果。")
public interface SentimentAnalyzer {
/**
* 返回自定义对象——LangChain4j自动生成JSON解析指令
*/
SentimentResult analyze(@UserMessage String text);
/**
* 返回List——批量分析
*/
@UserMessage("""
分析以下{{count}}条文本的情感,返回JSON数组:
{{texts}}
""")
List<SentimentResult> analyzeBatch(
@P("count") int count,
@P("texts") String texts
);
/**
* 流式返回——适合长文本生成
*/
TokenStream streamAnalysis(@UserMessage String text);
@Data
class SentimentResult {
private String sentiment; // POSITIVE/NEGATIVE/NEUTRAL
private double confidence; // 0-1
private String mainEmotion; // 主要情绪
private String keyPhrase; // 关键短语
}
}/**
* 结构化输出的自动解析工作原理:
*
* 当返回类型是POJO时,LangChain4j会:
* 1. 生成JSON Schema描述
* 2. 在Prompt末尾追加"请以如下JSON格式输出:{schema}"
* 3. 解析LLM返回的JSON映射到对象
*
* 但有个坑:对于复杂嵌套对象,LLM可能生成不合规的JSON
* 需要加额外的重试逻辑
*/
@Service
@RequiredArgsConstructor
public class RobustAiServiceWrapper {
private final SentimentAnalyzer analyzer;
/**
* 带重试的结构化分析
* 解析失败时自动重试,最多3次
*/
public SentimentAnalyzer.SentimentResult analyzeWithRetry(String text) {
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
SentimentAnalyzer.SentimentResult result = analyzer.analyze(text);
// 验证结果合理性
if (isValidResult(result)) {
return result;
}
log.warn("第{}次分析结果不合理,重试", attempt);
} catch (Exception e) {
log.warn("第{}次分析失败: {}", attempt, e.getMessage());
if (attempt == maxRetries) throw e;
}
}
// 所有重试都失败,返回默认值
return createDefaultResult();
}
private boolean isValidResult(SentimentAnalyzer.SentimentResult result) {
return result != null &&
result.getSentiment() != null &&
result.getConfidence() >= 0 && result.getConfidence() <= 1;
}
private SentimentAnalyzer.SentimentResult createDefaultResult() {
SentimentAnalyzer.SentimentResult defaultResult = new SentimentAnalyzer.SentimentResult();
defaultResult.setSentiment("NEUTRAL");
defaultResult.setConfidence(0.5);
return defaultResult;
}
}动态System Prompt
有时候需要根据运行时状态动态改变System Prompt:
/**
* 动态System Prompt的实现方式
*
* 问题:@SystemMessage是静态注解,无法在运行时修改
* 解决方案:通过@UserMessage传递动态上下文
*/
@AiService
public interface DynamicContextAssistant {
/**
* 方案1:把动态上下文塞进UserMessage
* 适合:上下文不太长的场景
*/
@UserMessage("""
[系统上下文]
当前用户:{{userName}}({{userLevel}}级别)
当前时间:{{currentTime}}
可用工具:{{availableTools}}
[用户问题]
{{question}}
""")
String chatWithContext(
@P("userName") String userName,
@P("userLevel") String userLevel,
@P("currentTime") String currentTime,
@P("availableTools") String availableTools,
@P("question") String question
);
/**
* 方案2:通过Prompt模板服务动态构建
* 适合:System Prompt需要从外部配置加载的场景
*/
String chat(@MemoryId String userId, @UserMessage String message);
}
/**
* 包装器:在调用@AiService之前动态处理
*/
@Service
@RequiredArgsConstructor
public class DynamicAiServiceFacade {
private final DynamicContextAssistant assistant;
private final UserProfileService userService;
private final PromptVersionService promptService;
private final ChatLanguageModel llm;
public String chat(String userId, String question) {
UserProfile profile = userService.getProfile(userId);
return assistant.chatWithContext(
profile.getName(),
profile.getLevel().toString(),
LocalDateTime.now().toString(),
String.join(", ", profile.getEnabledTools()),
question
);
}
/**
* 完全动态的实现方案:绕过@AiService,直接用ChatLanguageModel
* 适合:System Prompt完全由外部控制的场景
*/
public String chatWithDynamicPrompt(String userId, String question) {
// 从版本服务获取当前激活的Prompt
String systemPrompt = promptService.getActivePrompt(
"chat.system-prompt",
"你是一个有帮助的助手。"
);
// 动态替换变量
UserProfile profile = userService.getProfile(userId);
systemPrompt = systemPrompt
.replace("{{userName}}", profile.getName())
.replace("{{userLevel}}", profile.getLevel().toString());
return llm.generate(
SystemMessage.from(systemPrompt),
UserMessage.from(question)
).content().text();
}
}工具调用的高级用法
/**
* 工具调用的几个高级场景
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class AdvancedTools {
private final OrderService orderService;
private final DatabaseService dbService;
/**
* 高级用法1:工具可以触发其他工具(工具组合)
* 实现:查询订单详情并检查是否可退货
*/
@Tool("查询订单并判断是否可以退货")
public OrderRefundCheckResult checkOrderRefundEligibility(
@P("orderId") String orderId) {
// 工具内部可以做多步骤操作
Order order = orderService.findById(orderId);
if (order == null) {
return OrderRefundCheckResult.notFound(orderId);
}
boolean isEligible = isRefundEligible(order);
String reason = isEligible ? null : getIneligibilityReason(order);
return OrderRefundCheckResult.builder()
.orderId(orderId)
.orderStatus(order.getStatus().toString())
.isEligible(isEligible)
.reason(reason)
.deadline(isEligible ? order.getOrderDate().plusDays(7) : null)
.build();
}
/**
* 高级用法2:工具可以返回格式化的上下文
* AI会利用这些信息做更精准的回答
*/
@Tool("查询用户的历史订单,了解购买偏好")
public String getUserOrderHistory(
@P("userId") String userId,
@P("recentMonths") int recentMonths) {
List<Order> orders = orderService.findByUserRecent(userId, recentMonths);
if (orders.isEmpty()) {
return "用户" + userId + "在最近" + recentMonths + "个月内没有订单记录。";
}
// 返回结构化的文字描述,方便AI理解
StringBuilder sb = new StringBuilder();
sb.append("用户").append(userId).append("最近").append(recentMonths)
.append("个月的订单摘要:\n");
sb.append("- 订单总数:").append(orders.size()).append("个\n");
Map<String, Long> categoryCounts = orders.stream()
.collect(Collectors.groupingBy(Order::getCategory, Collectors.counting()));
sb.append("- 购买品类分布:").append(categoryCounts).append("\n");
double avgOrderValue = orders.stream()
.mapToDouble(Order::getTotalAmount)
.average().orElse(0);
sb.append("- 平均订单金额:").append(String.format("%.2f", avgOrderValue)).append("元\n");
// 最近一笔订单
orders.stream().max(Comparator.comparing(Order::getOrderDate)).ifPresent(latest -> {
sb.append("- 最近订单:").append(latest.getOrderDate())
.append(" 购买了 ").append(latest.getProductName());
});
return sb.toString();
}
/**
* 高级用法3:工具可以执行写操作
* 注意:应该在AI确认之后才执行
*/
@Tool("提交退货申请(在用户明确确认后调用)")
public RefundApplicationResult submitRefundApplication(
@P("orderId") String orderId,
@P("reason") String reason,
@P("userId") String userId) {
log.info("AI触发退货申请: orderId={}, userId={}, reason={}", orderId, userId, reason);
try {
String refundId = orderService.submitRefund(orderId, reason, userId);
return RefundApplicationResult.success(refundId);
} catch (Exception e) {
log.error("退货申请失败: {}", e.getMessage());
return RefundApplicationResult.failed(e.getMessage());
}
}
private boolean isRefundEligible(Order order) {
return order.getStatus() == OrderStatus.DELIVERED &&
order.getOrderDate().isAfter(LocalDate.now().minusDays(7));
}
private String getIneligibilityReason(Order order) {
if (order.getStatus() != OrderStatus.DELIVERED) {
return "订单状态为" + order.getStatus() + ",只有已签收的订单才能申请退货";
}
if (!order.getOrderDate().isAfter(LocalDate.now().minusDays(7))) {
return "超过7天退货期限,购买日期为" + order.getOrderDate();
}
return "不符合退货条件";
}
}多模态支持
/**
* LangChain4j的多模态支持
* 处理包含图片的输入
*/
@AiService
public interface MultimodalAssistant {
/**
* 分析图片内容
* 需要配置支持视觉的模型(如gpt-4o)
*/
@UserMessage("请分析这张图片:{{question}}")
String analyzeImage(
@P("question") String question,
@UserMessage ImageContent imageContent // 图片内容
);
}
/**
* 图片分析服务
*/
@Service
@RequiredArgsConstructor
public class ImageAnalysisService {
private final MultimodalAssistant assistant;
/**
* 分析上传的产品图片
*/
public String analyzeProductImage(byte[] imageBytes, String question) {
// 转换为Base64
String base64 = Base64.getEncoder().encodeToString(imageBytes);
ImageContent imageContent = ImageContent.from(
"data:image/jpeg;base64," + base64
);
return assistant.analyzeImage(question, imageContent);
}
/**
* 从URL分析图片
*/
public String analyzeImageFromUrl(String imageUrl, String question) {
ImageContent imageContent = ImageContent.from(imageUrl);
return assistant.analyzeImage(question, imageContent);
}
}常见问题排查
实际使用中遇到最多的坑:
/**
* 常见问题和解决方案
*/
public class CommonIssuesFixes {
/*
* 问题1:@MemoryId的内存泄漏
* 现象:每个新用户都创建一个ChatMemory实例,但永远不销毁
* 原因:ChatMemoryProvider是在内存里维护Map<memoryId, ChatMemory>
*
* 解决:使用有驱逐策略的缓存
*/
@Bean
public ChatMemoryProvider memoryProvider() {
// 使用Caffeine缓存,1小时不活跃自动驱逐
Cache<Object, ChatMemory> memoryCache = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.maximumSize(10_000) // 最多10000个用户的记忆
.build();
return memoryId -> memoryCache.get(memoryId, id ->
MessageWindowChatMemory.withMaxMessages(20)
);
}
/*
* 问题2:工具调用死循环
* 现象:AI调用工具A,工具A触发条件让AI再次调用工具A
*
* 解决:工具内部做调用深度检查,或用超时机制
*/
@Tool("查询商品信息")
public String getProductInfo(@P("productId") String productId) {
// 防止递归:通过ThreadLocal记录调用深度
int depth = CALL_DEPTH.get();
if (depth > 3) {
return "已达最大查询深度,请用更具体的产品ID查询";
}
CALL_DEPTH.set(depth + 1);
try {
return doGetProductInfo(productId);
} finally {
CALL_DEPTH.set(depth);
}
}
private static final ThreadLocal<Integer> CALL_DEPTH = ThreadLocal.withInitial(() -> 0);
/*
* 问题3:结构化输出解析失败
* 现象:LLM有时候输出了额外的解释文字,JSON解析失败
*
* 解决:在Prompt中强调输出格式,或手动提取JSON
*/
}@AiService设计很巧妙,核心是把LLM调用的复杂性(Prompt构建、记忆管理、工具调用)全部封装在代理层里。理解了代理机制,就能知道在哪个层面做扩展最合适。
