AiServices 源码级拆解:一个接口如何变成 RAG Agent

AiServices 源码级拆解:一个接口如何变成 RAG Agent

一句话结论 :AiServices 不是魔法,而是"动态代理 + 上下文装配 + 消息构建 + 输出解析"的运行时。理解装配顺序,才能定位 90% 的"工具不触发、结构化输出失败、记忆不生效"。

适用版本 :LangChain4j 1.23.0 / JDK 21

运行方式:内存模式 + Mock,无需 Docker / PgVector / Redis


0. 先说结论

先看三个真实问题:

  • 加了 @Tool,模型就是不调用;
  • 返回 POJO 偶尔解析失败;
  • 加了 chatMemoryProvider,多轮对话还是"失忆"。

它们看起来是三个问题,实际上都指向同一件事:你没有按 AiServices 的装配顺序去理解它。

本文按一次方法调用的生命周期拆开:

  1. 接口如何变成代理;
  2. 代理如何把方法参数变成 ChatRequest;
  3. 记忆、RAG、工具在什么时机被织入;
  4. 返回值如何被解析;
  5. 出问题时,应该先看哪一层。

1. 四步:从接口到代理

AiServices 的核心流程只有四步:

text 复制代码
定义接口
  → builder 收集配置
  → 生成动态代理
  → 方法调用时装配 ChatRequest、调用模型、解析返回值

1.1 定义接口

java 复制代码
interface SupportAssistant {
    @SystemMessage("你是企业客服助手,回答必须基于资料。")
    String chat(@MemoryId String conversationId,
                @UserMessage String question);
}

接口没有实现类。@SystemMessage、@UserMessage、@MemoryId 是给代理看的"元数据"。

1.2 builder 收集配置

java 复制代码
SupportAssistant assistant = AiServices.builder(SupportAssistant.class)
        .chatModel(chatModel)
        .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))
        .contentRetriever(retriever)
        .tools(new OrderTools())
        .build();

builder 把这些组件收进一个上下文对象(可以理解为 AiServiceContext):模型、流式模型、记忆提供者、检索器、工具、护栏、监听器。

1.3 生成动态代理

build() 返回的是接口的代理对象,不是你的实现类。每次调用接口方法,都会被 InvocationHandler 拦截。

java 复制代码
// 你写的
assistant.chat("user-42", "订单到哪了?");

// 代理实际做的
// 1. 解析方法签名与注解
// 2. 组装 SystemMessage / UserMessage
// 3. 读取记忆、检索内容
// 4. 调用 ChatModel
// 5. 解析返回值

1.4 调用模型与解析

如果模型返回的是普通文本,直接解析成 String 或 POJO;如果返回的是工具调用请求,代理会执行工具,把结果回填,再次调用模型,直到得到最终回答或达到工具调用上限。
#mermaid-svg-ZrkgqYBsI2avXiFT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZrkgqYBsI2avXiFT .error-icon{fill:#552222;}#mermaid-svg-ZrkgqYBsI2avXiFT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZrkgqYBsI2avXiFT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZrkgqYBsI2avXiFT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZrkgqYBsI2avXiFT .marker.cross{stroke:#333333;}#mermaid-svg-ZrkgqYBsI2avXiFT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZrkgqYBsI2avXiFT p{margin:0;}#mermaid-svg-ZrkgqYBsI2avXiFT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster-label text{fill:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster-label span{color:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster-label span p{background-color:transparent;}#mermaid-svg-ZrkgqYBsI2avXiFT .label text,#mermaid-svg-ZrkgqYBsI2avXiFT span{fill:#333;color:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT .node rect,#mermaid-svg-ZrkgqYBsI2avXiFT .node circle,#mermaid-svg-ZrkgqYBsI2avXiFT .node ellipse,#mermaid-svg-ZrkgqYBsI2avXiFT .node polygon,#mermaid-svg-ZrkgqYBsI2avXiFT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZrkgqYBsI2avXiFT .rough-node .label text,#mermaid-svg-ZrkgqYBsI2avXiFT .node .label text,#mermaid-svg-ZrkgqYBsI2avXiFT .image-shape .label,#mermaid-svg-ZrkgqYBsI2avXiFT .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZrkgqYBsI2avXiFT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZrkgqYBsI2avXiFT .rough-node .label,#mermaid-svg-ZrkgqYBsI2avXiFT .node .label,#mermaid-svg-ZrkgqYBsI2avXiFT .image-shape .label,#mermaid-svg-ZrkgqYBsI2avXiFT .icon-shape .label{text-align:center;}#mermaid-svg-ZrkgqYBsI2avXiFT .node.clickable{cursor:pointer;}#mermaid-svg-ZrkgqYBsI2avXiFT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZrkgqYBsI2avXiFT .arrowheadPath{fill:#333333;}#mermaid-svg-ZrkgqYBsI2avXiFT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZrkgqYBsI2avXiFT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZrkgqYBsI2avXiFT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZrkgqYBsI2avXiFT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZrkgqYBsI2avXiFT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZrkgqYBsI2avXiFT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster text{fill:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT .cluster span{color:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZrkgqYBsI2avXiFT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZrkgqYBsI2avXiFT rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZrkgqYBsI2avXiFT .icon-shape,#mermaid-svg-ZrkgqYBsI2avXiFT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZrkgqYBsI2avXiFT .icon-shape p,#mermaid-svg-ZrkgqYBsI2avXiFT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZrkgqYBsI2avXiFT .icon-shape rect,#mermaid-svg-ZrkgqYBsI2avXiFT .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZrkgqYBsI2avXiFT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZrkgqYBsI2avXiFT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZrkgqYBsI2avXiFT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-ZrkgqYBsI2avXiFT .client>*{fill:#0E2A33!important;stroke:#22D3EE!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .client span{fill:#0E2A33!important;stroke:#22D3EE!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .client tspan{fill:#E8EEF9!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .service>*{fill:#111A2E!important;stroke:#4F8CFF!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .service span{fill:#111A2E!important;stroke:#4F8CFF!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .service tspan{fill:#E8EEF9!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .model>*{fill:#2A1F0B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .model span{fill:#2A1F0B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .model tspan{fill:#E8EEF9!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .tool>*{fill:#2A1F3D!important;stroke:#A78BFA!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .tool span{fill:#2A1F3D!important;stroke:#A78BFA!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .tool tspan{fill:#E8EEF9!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .decision>*{fill:#1E293B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .decision span{fill:#1E293B!important;stroke:#F5A524!important;color:#E8EEF9!important;stroke-width:2px!important;}#mermaid-svg-ZrkgqYBsI2avXiFT .decision tspan{fill:#E8EEF9!important;} 是
否
assistant.chat(id, question)
动态代理

解析方法签名与注解
装配上下文

System / User / Memory / RAG
ChatModel.chat(ChatRequest)
返回工具调用?
执行工具

回填 ToolExecutionResult
解析返回值

String / POJO / TokenStream

图 1:AiServices 的一次调用生命周期。工具调用是一个循环,不是一次性分支。


1.5 动态代理的边界:哪些能做,哪些不能

AiServices 基于 JDK 动态代理,因此它继承了这个机制的能力与限制。

能力 支持情况 建议
接口 ✅ 用接口定义 AI Service
抽象类 ❌ 改成接口
默认方法 ⚠️ 不要放业务逻辑,容易被误当成 AI 方法
方法重载 ⚠️ 一个业务动作一个方法,避免歧义
泛型返回 ⚠️ 用具体类型,别依赖类型擦除后的推断
toString / equals ⚠️ 不要依赖代理的默认实现
静态方法 ❌ 代理无法拦截

三个直接结论:

  1. 接口就是契约:方法名、参数、注解、返回类型共同决定 Prompt 与解析方式;
  2. 不要用继承复用 AI Service :父接口的 @SystemMessage 不会被子接口自动继承;
  3. 默认方法要谨慎:它可能被代理当成普通 AI 方法处理,行为不符合预期。

接口化还有一个好处:测试时可以直接实现接口做 Fake,不需要启动模型。


1.6 为什么是接口,而不是继承

AI Service 选择"接口 + 代理",而不是"继承基类",有三个工程原因:

  1. 组合优于继承:你可以把同一个接口装配成不同模型、不同记忆、不同检索器的实例;
  2. 测试友好:测试时直接实现接口做 Fake,不需要启动模型;
  3. 边界清晰:接口只暴露业务动作,不暴露框架内部状态。

代价是:接口不能有状态、不能有构造器注入、不能靠父类复用。解决办法是把状态放在 ChatMemory/Store,把复用放在组合根,而不是塞进接口。


2. 装配顺序:先什么,后什么

顺序 阶段 输入 输出 失败表现
1 System message 注解 / Provider SystemMessage 人设不生效
2 User message 方法参数 / 模板 UserMessage 变量为空、模板报错
3 Memory @MemoryId + Store 历史消息 失忆、串号
4 RAG Query + Retriever 检索内容 答非所问
5 Tools @Tool 方法 工具规格 工具不触发
6 Model ChatRequest ChatResponse 超时、限流
7 Output parser 模型文本 String / POJO 解析失败

2.1 System message 先于一切

System message 决定角色、边界与输出格式。它来自三种来源,优先级从高到低:

  1. 方法上的 @SystemMessage;
  2. 接口上的 @SystemMessage;
  3. systemMessageProvider 动态生成。

如果用了 systemMessageProvider,还可以按 memoryId 给不同租户不同的系统提示。

2.2 User message 与模板变量

@UserMessage 支持 {``{变量}},配合 @V 绑定参数:

java 复制代码
@UserMessage("请用 {{style}} 的风格回答:{{question}}")
String chat(@V("style") String style,
            @V("question") String question);

模板错误通常在启动后第一次调用才暴露,建议为每个 Prompt 写一个契约测试(A15)。

2.3 记忆:按 @MemoryId 隔离

chatMemoryProvider 收到的是方法上 @MemoryId 的值;没有该注解时,默认值是字符串 "default"。

java 复制代码
.chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))

两个关键约束:

  • 同一个 @MemoryId 不应并发调用,否则可能损坏记忆;框架不负责加锁。
  • 记忆不会自动清理,长会话要设 TTL 或显式 evictChatMemory。

2.4 RAG:检索内容在模型调用前注入

contentRetriever 会在每次调用时检索,并把内容注入到请求中。注入位置由 ContentInjector 决定;默认实现通常把内容放到用户消息附近。

如果你不想每个请求都检索,可以把 ContentRetriever 包装成 @Tool,让模型自己决定是否检索。这两种方式的成本与召回行为不同:

方式 时机 优点 缺点
contentRetriever 每次调用 稳定、实现简单 简单问题也会检索
RAG as Tool 模型决定 省检索成本 依赖模型判断,可能漏检

2.5 工具:在模型返回后执行

工具不是"提前执行",而是模型返回 ToolExecutionRequest 后才执行。执行结果以 ToolExecutionResultMessage 回填,再调用模型。

这解释了一个常见现象:工具没被调用,不是工具代码有问题,而是模型没有选择它。 优先检查:

  1. 工具是否注册;
  2. 工具描述是否清晰;
  3. 参数 schema 是否可解析;
  4. 模型是否支持 function calling;
  5. 系统提示是否明确要求使用工具。

2.6 输出解析:最后一步,也最容易失败

String 返回最简单;POJO 返回需要模型输出可解析的 JSON。常见失败原因:

  • Prompt 没有明确要求"只输出 JSON";
  • 模型在 JSON 前后加了说明文字;
  • 字段类型不匹配;
  • 嵌套结构过于复杂。

修复顺序:先收紧 Prompt,再简化 DTO,最后才考虑自定义输出解析器。


2.7 结构化输出实战:从 Prompt 到 POJO

结构化输出的目标不是"让模型输出 JSON",而是"让下游拿到可校验的对象"。

java 复制代码
public record Answer(
        String conclusion,
        List<String> evidence,
        double confidence) {}

interface Analyst {
    @SystemMessage("""
            你是严谨的技术分析师。
            只输出 JSON,不要 Markdown 代码块,不要额外解释。
            JSON 结构:
            {"conclusion":"...","evidence":["..."],"confidence":0.0}
            confidence 取 0 到 1 之间的小数。
            """)
    Answer analyze(@UserMessage String question);
}

四个提高成功率的做法:

  1. 字段越少越稳:先 3--5 个扁平字段,不要一上来就嵌套;
  2. 给完整示例:示例比"请输出 JSON"有效;
  3. 禁止额外文本:明确"不要 Markdown 代码块、不要解释";
  4. 失败可重试:第一次解析失败,用更严格的 Prompt 重试一次,再走兜底。

仍然要记住:POJO 返回不是契约,只是解析结果。 模型可能返回多余字段、类型错误或缺失字段,生产代码要校验,不能直接信任。


2.8 记忆与 RAG 的先后顺序

装配顺序会影响最终 ChatRequest 的消息结构:

text 复制代码
SystemMessage
→ 历史消息(ChatMemory)
→ 检索内容(RAG)
→ 当前 UserMessage

三个实践建议:

  • RAG 内容不要写回记忆:检索结果是"本次上下文",不是"对话历史",写回会导致下轮重复注入;
  • 历史消息要裁剪:RAG 已经占掉大量 Token,记忆窗口要相应缩小;
  • 用 Listener 看最终请求:不要凭想象推断顺序,直接打印脱敏后的消息角色与长度。

顺序本身没有绝对对错,但必须与你的 Prompt 设计一致。如果 SystemMessage 说"只依据提供的资料回答",而检索内容排在历史消息之前,模型可能优先参考历史。


3. 返回类型:不同返回类型走不同路径

返回类型 执行方式 适用场景 注意
String 阻塞 简单问答 最稳定
POJO / Result<T> 阻塞 + 解析 结构化输出 需要 JSON 约束
TokenStream 回调流式 SSE、打字机效果 需要 StreamingChatModel
CompletableFuture<T> 非阻塞 高并发、异步编排 需要模型支持异步
Flow.Publisher<T> 响应式流 WebFlux、背压 需要响应式适配

1.20 之后,LangChain4j 用"返回类型驱动执行模型"。选错返回类型,会在吞吐、延迟与可取消性上付出代价(A05 详述)。


3.1 返回类型选择的三个反例

反例 1:要 POJO,但 Prompt 没约束。

结果:模型输出"好的,以下是 JSON:...",解析失败。修复:收紧 Prompt,只输出 JSON。

反例 2:返回 String,却在下游用正则抠字段。

结果:模型换个措辞,正则就崩。修复:需要结构化就用 POJO,并在服务端校验。

反例 3:返回 TokenStream,但业务要完整 JSON。

结果:流式片段无法直接反序列化。修复:流式只用于展示,完整对象在 onCompleteResponse 后解析。

结论:返回类型由消费端决定,不由模型决定。


4. 一次带工具调用的调试示例

java 复制代码
// LangChain4j 1.23.0 | JDK 21
interface SupportAssistant {
    @SystemMessage("""
            你是企业客服助手。
            查询订单必须调用 queryOrderStatus,不要凭记忆回答。
            """)
    String chat(@MemoryId String conversationId,
                @UserMessage String question);
}

public static class OrderTools {
    @Tool("查询订单状态。只读操作,不会修改订单。")
    public String queryOrderStatus(@P("订单号") String orderId) {
        return switch (orderId) {
            case "SO-2026-0001" -> "已发货,预计 2026-11-07 送达";
            default -> "未找到订单:" + orderId;
        };
    }
}

SupportAssistant assistant = AiServices.builder(SupportAssistant.class)
        .chatModel(chatModel)
        .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(20))
        .tools(new OrderTools())
        .build();

String answer = assistant.chat("user-42", "订单 SO-2026-0001 到哪了?");

如果工具没有被调用,按这个顺序排查:

text 复制代码
1. 去掉 SystemMessage,看是否触发
2. 把工具描述改得更具体,看是否触发
3. 把参数改成 String,看 schema 是否问题
4. 换一个支持 function calling 的模型
5. 打开 ChatModelListener,确认请求里是否包含 tools

4.1 工具调用循环的终止条件

工具调用是一个循环:模型请求工具 → 执行 → 回填 → 再请求模型。没有终止条件,就会变成"调用---失败---再调用"的烧钱循环。

终止条件 建议阈值 行为
最大轮次 3--5 轮 返回"需要人工确认"
相同调用重复 同一工具 + 同一参数出现 2 次 停止并提示模型换方法
连续失败 2--3 次 兜底回答 + 告警
时间预算 单请求 30--60s 中断并返回部分结果
Token 预算 按业务设定 中断并降级
java 复制代码
// 伪代码:在工具执行包装层做终止判断
if (round++ >= maxRounds) {
    return "工具调用轮次超限,请人工确认。";
}
if (seenCalls.contains(toolName + argsHash)) {
    return "检测到重复工具调用,已停止。";
}

终止后不要直接抛异常,要给用户一个可读的降级回答;同时把终止原因写入日志与指标,方便定位是描述问题、参数问题还是模型能力问题。


4.2 工具调用日志应该记什么

字段 用途 注意
工具名 定位哪个工具 无
参数哈希 检测重复调用 不记录明文敏感参数
轮次 判断是否循环 无
耗时 定位慢工具 无
结果状态 成功/失败/待审批 不记录完整返回体
错误码 分类统计 无
模型 invocationId 串联一次对话 无

没有这些字段,线上只能看到"模型答错了",无法判断是工具没调用、调用失败,还是结果没被采纳。


5. 常见失败模式

现象 最可能的层 排查动作
工具不触发 L5 / L6 看 ChatRequest 是否带 tools;看描述与参数
POJO 解析失败 L2 / L7 收紧 Prompt;检查 DTO;看原始输出
记忆不生效 L3 确认 @MemoryId 与 provider;确认 Store 读写
RAG 没用到 L4 看检索结果是否为空;看注入位置
流式不流 L1 / L6 确认用了 StreamingChatModel;确认返回类型
同一会话错乱 L3 检查是否并发调用同一个 @MemoryId

5.1 一个排查案例:工具为什么没触发

现象 :OrderTools 已注册,问"订单 SO-2026-0001 到哪了",模型却回答"请提供订单号"。

按五步排查,不要一上来就换模型:

步骤 观察点 常见问题 修复
1. 是否注册 ChatRequest.tools() 忘记 .tools(...) 注册工具
2. 描述是否清晰 工具描述文本 只写"查询订单" 写清场景、参数、只读性
3. schema 是否可解析 参数类型与注解 参数是复杂对象/泛型 改成 String/基本类型
4. 模型是否支持 模型能力说明 选了不支持 function calling 的模型 换模型或改用文本解析
5. 系统提示是否冲突 SystemMessage 写了"不要调用外部工具" 明确要求使用工具

这个顺序的价值是:先排除工程问题,再怀疑模型。 大多数"工具不触发"死在 2 和 3。


6. 源码阅读地图

建议按这个顺序读 1.23.0 源码:

  1. AiServices:builder 与入口;
  2. AiServiceContext:装配了哪些组件;
  3. 动态代理与 InvocationHandler:方法调用如何被拦截;
  4. ServiceOutputParser:返回值如何解析;
  5. 工具执行相关类:ToolService / ToolExecutor;
  6. DefaultRetrievalAugmentor:RAG 如何织入。

内部类名会随版本变化,读源码时以目标 tag 为准;本文关注的是调用顺序与职责边界,不是某个类的实现细节。


6.1 读源码的三条线索

  1. 入口线 :从 AiServices.builder(...).build() 进入,跟到代理创建;
  2. 装配线:看上下文对象收了哪些组件,以及默认值是什么;
  3. 调用线 :在动态代理的 invoke 打断点,观察方法签名如何变成消息与工具请求。

不要从工具类、解析类开始读,容易迷路。先建立"入口 → 装配 → 调用"的主干,再按问题下钻。


7. FAQ

AiServices 生成的代理是线程安全的吗?

代理本身基本无状态,但记忆不是:同一个 @MemoryId 并发调用可能损坏 ChatMemory。生产上要按会话串行化,或使用带锁的 ChatMemory 包装。

接口方法可以重载吗?

可以定义多个方法,但建议一个业务动作一个方法;重载会增加 Prompt 与参数解析的歧义。

为什么结构化输出偶尔失败?

模型输出不是严格 JSON 是常态。用清晰的 schema、低 temperature、只输出 JSON 的指令,并准备重试/兜底解析。

工具调用有次数上限吗?

生产上要显式设置工具调用上限(以及单次请求的超时与预算),避免模型陷入"调用---失败---再调用"的循环。具体配置项以 1.23.0 实际 API 为准。

如何看到实际发送的 ChatRequest?

配置 ChatModelListener,在 onRequest 中记录消息、工具、参数;注意脱敏,不要把完整用户数据写日志。


为什么工具日志不能记录完整参数?

订单号、手机号、金额都可能是敏感信息。日志里记录参数哈希或掩码,排错时用 traceId 去受控审计库查完整值,既保留可追溯性,又避免泄露。

7.1 什么时候不该用 AiServices

  • 只调用一次模型、无记忆/RAG/工具:直接用 ChatModel 更透明;
  • 需要完全控制消息结构与工具循环:用低层 API;
  • 返回结构极其复杂、需要严格 schema 校验:低层 API + 显式 JSON Schema 更稳;
  • 团队还没理解装配顺序:先用低层 API 写一遍,再上 AiServices。

AiServices 是"装配器",不是"万能层"。它的价值在组合,不在单次调用。


8. 速查表

阶段 关键点 排查入口
System 优先级:方法 > 接口 > Provider 看最终 SystemMessage
User @V 变量绑定 看模板渲染结果
Memory @MemoryId 隔离 看 Store key 与并发
RAG 检索时机与注入位置 看 Top-K 与内容
Tool 模型选择后才执行 看 ChatRequest.tools
Model 超时、限流、能力 看 Listener 与指标
Parser JSON 约束 看原始输出

9. 系列导航

  • 上一篇:A02《LangChain4j 生产级工程骨架:12 个模块 + Docker 一键启动》
  • 下一篇:A04《ChatMemory 上下文工程:6 种记忆策略与 Redis 持久化》
  • 全系列:《LangChain4j 生产级实战》20 篇路线图(专栏内查看)

相关推荐
Flynn Liang37 分钟前
LangChain4j 1.23 架构全景:Java 大模型应用的 6 层抽象
java·langchain·大模型·rag·ai agent·langchain4j
聪明的笨猪猪1 小时前
实际工程问题:AI生成Redis分布式锁的原子性缺陷导致库存超卖
java·经验分享·面试·ai编程
软件技术NINI2 小时前
基于SpringBoot和Vue的新能源汽车销售推荐系统
java·vue.js·spring boot·后端·汽车·毕业设计·毕设
GISer_Jing2 小时前
Agent相关岗位时收集的各类技术面试题
后端·ai·langchain
Flynn Liang2 小时前
LangChain4j 生产级工程骨架:12 个模块 + Docker 一键启动
java·spring boot·langchain·rag·langchain4j
叼烟扛炮3 小时前
[LangChain] 学习笔记(08):工具调用底层闭环与第三方搜索(Tavily)实战
人工智能·笔记·学习·langchain
噢,我明白了4 小时前
JAVA中的一些基础应用知识点
java·开发语言
GISer_Jing4 小时前
AI Agent秋招面试万字通关指南|架构、ReAct、上下文工程、RAG、评测、生产运维全解
后端·ai·langchain
SL_staff4 小时前
私有化低代码不是妥协,而是企业级数字化的自主可控主航道
java