AiServices 实现原理与 API 使用
在 Java 应用中接入大语言模型,最初往往是手写 ChatRequest、拼接消息、解析 JSON,再处理记忆和工具调用。AiServices 提供了更高层的接口:开发者声明一个 Java 接口,运行时由框架生成实现,把方法调用转换为模型请求,再把模型响应转换为方法返回值。
本文使用 LangChain4j 的公开 API 说明这一过程。适配器类名、模型名称和注解细节可能随版本变化,实际项目应以当前官方文档为准。
AiServices 的定位
AiServices 是模型调用的编排和类型映射层,不是模型本身,也不是一个自动完成所有任务的自主 Agent。它主要负责:
- 读取接口和方法上的提示词、参数、返回类型等元数据;
- 把方法参数组装为用户消息或 Prompt;
- 调用
ChatLanguageModel; - 协调记忆、工具调用和流式事件;
- 将文本或结构化输出转换为
String、POJO 或结果包装类型。
最小示例:
interface Assistant {
String chat(String userMessage);
}
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4o-mini")
.build();
Assistant assistant = AiServices.create(Assistant.class, model);
String answer = assistant.chat("用一句话解释 RAG");
System.out.println(answer);接口没有实现类,但 AiServices.create 返回了一个可调用对象。这是 Java 动态代理的典型用法:调用接口方法时,代理拦截调用并交给框架处理。
一次调用如何完成
可以把一次调用抽象成下面的链路:
Java interface call
-> invocation handler
-> message/prompt assembly (+ memory)
-> ChatLanguageModel
-> tool loop or stream callbacks
-> output parser
-> String / POJO / Result<T>读取接口元数据
创建服务时,框架会检查接口方法及其注解,确定系统消息、用户消息模板、参数绑定方式、返回类型和可用工具。方法名本身通常不是主要提示词来源,真正影响请求的是注解和参数。
绑定参数并组装消息
调用 assistant.chat("...") 时,参数会被绑定到用户消息。若配置了 ChatMemory,历史消息会按会话策略加入当前请求。最终请求仍会落到底层的消息模型,例如系统消息、用户消息和历史 AI 消息。
调用模型并处理事件
框架将消息交给 ChatLanguageModel。同步调用等待完整响应;流式调用逐段接收响应;启用工具后,模型可能先返回工具调用请求,框架执行工具并将结果作为新消息再次提交模型。
解析返回值
返回 String 时直接提取文本。返回 Java 类时,框架通常要求模型生成结构化内容,再执行反序列化。解析失败、模型拒答、字段缺失和类型不匹配都应被视为正常的失败路径,而不是只靠提示词保证。
提示词与参数绑定
系统消息用于稳定的角色和规则,用户消息用于本次输入。一个翻译服务可以这样声明:
interface Translator {
@SystemMessage("你是准确、简洁的翻译助手。")
@UserMessage("把下面的文本翻译成{language}:\n{text}")
String translate(
@V("language") String language,
@V("text") String text
);
}@V 的模板语法和注解包名需要与项目采用的 LangChain4j 版本一致。提示词应保持短而明确:身份规则、输出约束和必要的示例可以放在系统消息中,业务数据则作为方法参数传入。
结构化输出
让接口直接返回 Java record,可以减少业务层的字符串解析:
record Sentiment(String label, double score) {}
interface Classifier {
@SystemMessage("判断文本情感,只返回结构化结果。")
Sentiment classify(String text);
}结构化输出仍然依赖模型能力和适配器支持。生产代码至少要校验:
label是否属于允许的枚举值;score是否在约定范围内;- 字段是否缺失或出现额外内容;
- 模型拒答或返回非 JSON 时如何降级。
如果需要访问原始文本、Token 用量或其他元数据,可以使用当前版本支持的结果包装类型(例如 Result<T>),不要假设所有模型都提供相同的元数据字段。
记忆与多用户隔离
记忆不是模型永久记住了信息,而是框架在下一次请求时重新加入历史消息。例如使用窗口记忆:
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();Web 服务不能让一个默认单例记忆承载所有用户的对话。应按 userId 或 sessionId 选择独立记忆,并明确窗口大小、过期时间和敏感信息清理策略。记忆越长,Token 成本和上下文噪声通常越高。
工具调用
工具把受控的 Java 方法暴露给模型。下面的例子只返回模拟天气,不连接真实系统:
class WeatherTools {
@Tool("根据城市名称查询当前天气")
String currentWeather(@P("城市名称,例如上海") String city) {
return city + " 当前晴,25 摄氏度";
}
}
interface WeatherAssistant {
@SystemMessage("你是天气助手,优先使用工具获取天气,再回答用户。")
String ask(String message);
}
WeatherAssistant weather = AiServices.builder(WeatherAssistant.class)
.chatLanguageModel(model)
.tools(new WeatherTools())
.build();模型只负责选择工具和生成参数,应用仍必须负责权限、参数校验、超时、幂等、审计和敏感操作确认。对写数据库、发起支付或删除数据的工具,不应只依赖模型自行判断。
流式响应
需要实时显示输出时,应使用当前版本提供的流式返回类型或回调接口。典型处理包含三种终态:部分内容、完成和错误。
interface StreamingAssistant {
TokenStream chat(String message);
}
TokenStream stream = streamingAssistant.chat("分三点解释向量检索");
stream.onPartialResponse(System.out::print)
.onCompleteResponse(response -> System.out.println("\n完成"))
.onError(Throwable::printStackTrace)
.start();上例用于说明生命周期,具体类型和链式方法以当前版本官方示例为准。接入 SSE 或 WebSocket 时,还要处理客户端断线、取消请求、背压、超时以及异常后的连接关闭。
何时使用底层 API
AiServices 适合“接口方法映射为 Prompt,并自动完成常见结果转换”的场景。以下情况更适合直接使用 ChatLanguageModel、ChatRequest 和 ChatResponse:
- 需要动态编排多角色消息和采样参数;
- 需要精细控制重试、超时、取消和流式生命周期;
- 需要读取完整的原始响应和供应商特有字段;
- 需要自定义状态机或复杂的工具循环。
两层 API 可以组合:边界清晰的业务助手使用 AiServices,底层网关或特殊流程保留低层调用能力。
生产检查清单
- API Key 从环境变量或密钥管理系统读取,不写进源码。
- 结构化返回值经过业务校验,不把反序列化成功当成业务正确。
- 记忆按用户或会话隔离,并设置长度、过期和敏感信息策略。
- 工具执行具备权限、超时、审计和失败降级。
- 同步和流式调用都设置超时、取消和错误处理。
- 记录模型、Token、延迟和错误指标,便于评估成本与质量。
总结
AiServices 的核心价值是把一次模型调用包装成熟悉的 Java 接口。它在运行时读取接口元数据,组装消息,调用模型,协调工具或记忆,最后把输出解析为声明的返回类型。理解这条链路后,开发者既能快速构建类型安全的 AI 服务,也能在需求超出抽象边界时准确退回底层 API。
