【LangChain4j系列03】AI Services 高层抽象设计解析

AI Services 是 LangChain4j 最核心的创新:声明一个 Java 接口,框架自动生成代理实现。本章深入解析其原理、完整方法签名、Result 包装类、以及高级配置。


3.1 设计哲学:接口即契约

AI Services 的设计灵感来自 Spring Data JPA 和 Retrofit:

kotlin 复制代码
Spring Data JPA:  interface UserRepo extends JpaRepository<User, Long>
                   → Spring 自动实现 findByName(String name)

Retrofit:         interface GitHubService
                   → Retrofit 自动实现 @GET("users/{id}")

LangChain4j:      interface Assistant
                   → AiServices.create() 自动编排 LLM 调用全链路

代理替你做了哪些事?


3.2 最小可用示例到完整配置

三步启动

java 复制代码
// Step 1: 定义接口
interface Assistant {
    String chat(String userMessage);
}

// Step 2: 创建 ChatModel
ChatModel model = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();

// Step 3: 创建代理
Assistant assistant = AiServices.create(Assistant.class, model);

// 使用
String answer = assistant.chat("用一句话解释 JVM 的 G1 垃圾回收器");

AiServices Builder 的全部配置项

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
    // === 核心模型 ===
    .chatModel(model)                            // ChatModel 实例
    .streamingChatModel(streamingModel)          // StreamingChatModel(流式场景)

    // === 提示词 ===
    .systemMessageProvider(memoryId -> "...")     // 动态 SystemMessage
    .systemMessageTransformer((msg, ctx) -> msg)  // SystemMessage 后处理

    // === 记忆 ===
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .chatMemoryProvider(memoryId -> createMemory())

    // === 工具 ===
    .tools(new Calculator())                     // 单个工具对象
    .tools(List.of(calculator, translator))      // 多个工具
    .toolProvider(toolProvider)                  // 动态工具提供者
    .toolSearchStrategy(new SimpleToolSearchStrategy()) // 工具搜索策略

    // === RAG ===
    .contentRetriever(contentRetriever)          // Naive RAG
    .retrievalAugmentor(retrievalAugmentor)       // Advanced RAG
    .storeRetrievedContentInChatMemory(false)    // 是否将检索内容存入记忆

    // === 安全 ===
    .moderationModel(moderationModel)            // 自动内容审核
    .inputGuardrails(guardrail1, guardrail2)     // 输入护栏
    .outputGuardrails(guardrail1, guardrail2)    // 输出护栏

    // === 其他 ===
    .chatRequestTransformer(req -> req)          // 每次请求前改写 ChatRequest

    .build();

3.3 @SystemMessage 全解

静态系统消息

java 复制代码
interface FriendlyAssistant {
    // 方式1:直接写死在注解中
    @SystemMessage("你是一个幽默风趣的助手,回答要使用口语化表达")
    String chat(String userMessage);

    // 方式2:从 classpath 资源文件加载(支持模板变量)
    @SystemMessage(fromResource = "prompts/friendly-system.txt")
    String chat2(String userMessage);
}

动态系统消息(基于 Memory ID)

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .systemMessageProvider(chatMemoryId -> {
        // chatMemoryId 即 @MemoryId 的值
        if ("vip_user".equals(chatMemoryId)) {
            return "你是一个尊贵的白金会员专属客服,使用敬语";
        }
        return "你是一个普通的客服助手";
    })
    .build();

SystemMessageTransformer(后处理)

java 复制代码
// 在每个 SystemMessage 后追加当前日期
.systemMessageTransformer((systemMessage, context) -> {
    if (systemMessage == null) return null;
    return systemMessage + "\n今天的日期是:" + LocalDate.now();
})

3.4 @UserMessage:模板与变量绑定

基础模板

java 复制代码
interface Translator {
    @UserMessage("将以下文本翻译成{{language}}:\n\n{{it}}")
    String translate(@V("language") String language, String text);
}

模板中 {{it}} 引用第一个/唯一的无注解参数,{{variableName}} 引用 @V("variableName") 标注的参数。

@V 注解的必要性

java 复制代码
// 如果编译时加了 -parameters 标志,可以不写 @V
@UserMessage("分析{{country}}的经济状况")
String analyze(String country);  // 参数名可从字节码获取

// 如果没有 -parameters(默认),必须用 @V 显式指定
@UserMessage("分析{{country}}的经济状况")
String analyze(@V("country") String country);

Spring Boot 和 Quarkus 默认启用 -parameters,通常不需要 @V

无参方法

java 复制代码
// 完全不需要用户输入的场景
@SystemMessage("你是一个知识渊博的助手")
@UserMessage("德国首都是哪里?直接回答城市名")
String germanCapital();

3.5 完整方法签名参考

UserMessage 的形式

java 复制代码
// 最简单的
String chat(String userMessage);

// 显式标注
String chat(@UserMessage String userMessage);

// 带上下文参数
String chat(@UserMessage String userMessage, @V("country") String country);

// 多模态
String chat(@UserMessage String userMessage, @UserMessage ImageContent image);
String chat(@UserMessage String userMessage, @UserMessage List<Content> contents);
String chat(List<AudioContent> contents);

// 完全由注解定义
@UserMessage("What is the capital of Germany?")
String chat();

// 模板
@UserMessage("What is the capital of {{it}}?")
String chat(String country);

@UserMessage("What is the {{what}} of {{country}}?")
String chat(@V("what") String what, @V("country") String country);

与 @SystemMessage 的组合

java 复制代码
// 固定 SystemMessage + 可变 UserMessage
@SystemMessage("你是一个翻译专家")
String chat(@UserMessage String userMessage);

// SystemMessage 也可以使用模板变量
@SystemMessage("Given a name of a country, {{answerInstructions}}")
String chat(@V("answerInstructions") String instructions, @UserMessage String userMessage);

// 完全固定的问答
@SystemMessage("你是一个地理专家")
@UserMessage("中国最大的淡水湖是哪个?")
String chat();

3.6 返回类型的完整能力

String 返回

java 复制代码
String chat(String message);  // 原始 LLM 输出,不做解析

结构化输出(后续专门章节分析讲解)

java 复制代码
// boolean
@UserMessage("Does '{{it}}' have a positive sentiment?")
boolean isPositive(String text);

// Enum
enum Priority { CRITICAL, HIGH, LOW }
@UserMessage("Analyze the priority: {{it}}")
Priority analyzePriority(String issue);

// POJO
@UserMessage("Extract person info from: {{it}}")
Person extractPerson(String text);

Result 元数据包装

java 复制代码
interface Assistant {
    @UserMessage("Generate an outline for: {{it}}")
    Result<List<String>> generateOutline(String topic);
}

Result<List<String>> result = assistant.generateOutline("Java Virtual Threads");

// 数据
List<String> outline = result.content();

// 元数据
TokenUsage usage = result.tokenUsage();                // Token 用量
List<Content> sources = result.sources();               // RAG 来源文档
List<ToolExecution> toolExecutions = result.toolExecutions(); // 工具调用全记录
FinishReason finishReason = result.finishReason();      // 最终停止原因

Result 的价值 :当 AI Service 背后涉及多次 LLM 调用(工具调用循环),tokenUsage() 返回的是所有调用的 token 总和,这是裸返回类型无法获取的关键信息。

TokenStream(流式)

java 复制代码
interface Assistant {
    TokenStream chat(String message);
}

后续专门章节分析讲解。


3.7 ChatMemory 集成

单用户场景(共享记忆)

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .build();

assistant.chat("我叫张三");   // LLM 记住"张三"
assistant.chat("我叫什么?");  // LLM 回答"张三"

多用户场景(隔离记忆)

java 复制代码
interface Assistant {
    String chat(@MemoryId int memoryId, @UserMessage String message);
}

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemoryProvider(memoryId ->
        MessageWindowChatMemory.withMaxMessages(10))
    .build();

assistant.chat(1, "我叫张三");  // 用户1的记忆
assistant.chat(2, "我叫李四");  // 用户2的记忆,完全隔离

运行时记忆访问

java 复制代码
interface Assistant extends ChatMemoryAccess {
    String chat(@MemoryId int memoryId, @UserMessage String message);
}

// 查询某个用户的对话历史
List<ChatMessage> history = assistant.getChatMemory(1).messages();

// 清除某个用户的记忆
assistant.evictChatMemory(2);

并发警告 :同一 @MemoryId 不要并发调用,ChatMemory 没有内置并发保护(ArrayList 实现)。


3.8 Service 编排:小而精的组合

官方强烈建议将复杂任务分解为多个职责单一的 AI Service,而不是一个超级 Service。

示例:客服系统

java 复制代码
// ─── 独立的小服务,各司其职 ───

interface GreetingExpert {
    @UserMessage("Is the following text a greeting? Text: {{it}}")
    boolean isGreeting(String text);
}

interface SentimentAnalyzer {
    @UserMessage("Does '{{it}}' have a positive sentiment?")
    boolean isPositive(String text);
}

interface PriorityAnalyzer {
    @UserMessage("Analyze priority of: {{it}}")
    Priority analyzePriority(String issue);
}

interface ChatBot {
    @SystemMessage("You are a polite customer support agent of Miles of Smiles.")
    String reply(String userMessage);
}

// ─── 编排类(纯 Java 代码,可测试) ───

class CustomerSupportOrchestrator {
    private final GreetingExpert greetingExpert;
    private final SentimentAnalyzer sentimentAnalyzer;
    private final PriorityAnalyzer priorityAnalyzer;
    private final ChatBot chatBot;

    public String handle(String userMessage) {
        // LLM 驱动的 if/else
        if (greetingExpert.isGreeting(userMessage)) {
            return "Greetings from Miles of Smiles! How can I help?";
        }

        // LLM 驱动的 switch
        Priority priority = priorityAnalyzer.analyzePriority(userMessage);
        return switch (priority) {
            case CRITICAL -> escalate(userMessage);
            case HIGH, LOW -> chatBot.reply(userMessage);
        };
    }

    private String escalate(String msg) {
        // 危急问题走人工通道
        return "Your issue has been escalated. Agent will contact you within 1 hour.";
    }
}

这种设计的好处

优势 说明
成本控制 isGreeting() 可用便宜模型(如本地 Llama),复杂对话用 GPT-4
独立测试 每个 Service 可独立做集成测试和评估
独立演进 换模型、调 prompt、加护栏,不影响其他 Service
确定性 + LLM 混合 业务逻辑用 Java 代码,语言理解用 LLM,取长补短

3.9 设计原理:反射代理的工作方式

当前版本,AiServices.create() 内部使用 Java 动态代理(java.lang.reflect.Proxy):

java 复制代码
// 概念上等价于:
Object proxy = Proxy.newProxyInstance(
    classLoader,
    new Class[]{Assistant.class},
    (proxy, method, args) -> {
        // 1. 读取 @SystemMessage、@UserMessage 注解
        // 2. 用实际参数替换模板变量
        // 3. 从 ChatMemoryProvider 获取历史消息
        // 4. 组装成 ChatRequest
        // 5. 调用 ChatModel.chat()
        // 6. 根据返回类型解析(String/POJO/Enum/Result/TokenStream)
        // 7. 触发 guardrails
        // 8. 返回
    }
);

官方表示正在考虑其他替代实现方式(如编译期代码生成),但目前反射代理是唯一实现。

相关推荐
星火10241 小时前
【LangChain4j系列04】Tools 工具调用机制详解
人工智能·后端
用户69371750013841 小时前
深夜炸场!DeepSeek 没发新模型,却重构了整个 Agent 生态
前端·后端·github
fthux1 小时前
装闭 RenoPit 源码解析(08):多模态AI调用、重试与文本降级
人工智能·ai·开源·github·open source·renopit
Raas1001 小时前
MAIGateway,魔芋企业级AI网关的安全基建化设计
大数据·人工智能·网关·网络安全·api网关·mai gateway·魔芋
冬奇Lab1 小时前
开源项目第186期:Open Ontologies — Rust 实现的 AI 原生本体工程 MCP 服务器
人工智能·开源·资讯
tachibana21 小时前
文件上传分布式限流如何做?
人工智能·ai·大模型·llm·prompt
武子康1 小时前
实现 GPT-Live-like:两条路线、一个控制面和六阶段验收
人工智能·chatgpt·agent
郑州光合科技余经理1 小时前
餐饮预定系统架构拆解:订单链路、权限组织与私有化源码交付
java·开发语言·前端·数据库·人工智能·系统架构·php
Wang's Blog1 小时前
AI Agent白手起家71: LangGraph 云平台与本地开发实战
人工智能