乐于分享
好东西不私藏

第15篇-实战:构建一个多功能AI助手Agent

第15篇-实战:构建一个多功能AI助手Agent

📝 作者说:前两篇,我们学会了给 AI 装工具(第13篇)和让 AI 自己决策(第14篇)。但都是小打小闹——查个天气、查个订单,一两行代码的事。这篇来个大的:构建一个真正的多功能私人助手,能管日程、能发邮件、能搜知识库,还能在工具出错时优雅降级。代码可以直接跑。

一、需求:一个真正有用的私人助手

1.1 为什么之前的实战都不够”真”?

回忆一下第13篇的实战:
用户:北京天气怎么样?AI:晴天,25°C。用户:帮我安排明天下午3点开会AI:已安排。
太简单了。 真实场景里,用户的需求是这样的:

“帮我查一下明天下午有没有会议,如果有冲突就改到后天,然后给参会人发邮件通知变更,顺便帮我查一下上次季度汇报的PPT在哪个文件里。”

这一句话涉及:
  • 📅 日历查询 + 修改
  • 📧 邮件发送
  • 🔍 知识库检索(找文件)

🤔 思考 1:你觉得一个 Agent 能同时管日历、邮件、搜索吗?会不会”手忙脚乱”?

答案是:能,但需要正确的设计。 这篇就来解决怎么让一个 Agent 同时管理多个领域的工具,还不乱套。

1.2 本篇目标

我们要构建的助手叫 “SuperAssistant“,具备以下能力:

能力

工具

说明

📅 日程管理

CalendarTool

查询、添加、删除日程

📧 邮件发送

EmailTool

发送邮件通知

🔍 知识检索

KnowledgeTool

从本地文档中搜索信息

🛡️ 异常回退

框架层

工具出错时优雅降级,不死掉

最终效果:

用户:帮我查明天下午有没有会议,如果有的话给张三发邮件确认AI:  → 自动查日历 → 发现有"项目评审会"    → 自动调用邮件工具 → 给张三发确认邮件    → 回复:明天下午3点有项目评审会,已给张三发送确认邮件。用户:上次的销售数据报告在哪?AI:  → 自动检索知识库 → 找到相关文档    → 回复:在文档库的 /reports/2026-Q1-销售报告.pdf,第三页有完整数据。

二、多个 Tool 的协调与注册

2.1 按领域分组:别把所有工具塞一个类

🤔 思考 2:如果把日历、邮件、搜索的工具全部写在一个类里,会怎样?

后果:一个 500 行的上帝类,维护困难,职责不清。正确做法是按领域分组
tools/├── CalendarTool.java    ← 日程管理相关├── EmailTool.java       ← 邮件相关└── KnowledgeTool.java   ← 知识检索相关

2.2 日历工具组

public class CalendarTool {    // 模拟日程存储(实际项目用数据库)    private final Map<StringList<CalendarEvent>> events = new ConcurrentHashMap<>();    @Tool("查询指定日期的所有日程安排,返回时间、标题和参与者")    public String queryEvents(            @P("日期,格式 yyyy-MM-dd,例如:2026-04-25"String date    ) {        List<CalendarEvent> list = events.get(date);        if (list == null || list.isEmpty()) {            return date + " 没有日程安排";        }        StringBuilder sb = new StringBuilder();        sb.append(date).append(" 共 ").append(list.size()).append(" 个日程:\n");        for (CalendarEvent e : list) {            sb.append(String.format("  %s %s(参与者:%s)\n",                    e.time, e.titleString.join("、", e.participants)));        }        return sb.toString();    }    @Tool("添加一条新的日程安排")    public String addEvent(            @P("日程标题,如'项目评审会'"String title,            @P("日期,格式 yyyy-MM-dd"String date,            @P("时间,格式 HH:mm"String time,            @P("参与者列表,用逗号分隔,如'张三,李四'"String participants    ) {        CalendarEvent event = new CalendarEvent(title, time,                Arrays.asList(participants.split("[,,]")));        events.computeIfAbsent(date, k -> new ArrayList<>()).add(event);        return "日程已添加:" + date + " " + time + " " + title;    }    @Tool("删除指定日期的某个日程")    public String removeEvent(            @P("日期,格式 yyyy-MM-dd"String date,            @P("要删除的日程标题"String title    ) {        List<CalendarEvent> list = events.get(date);        if (list == nullreturn date + " 没有日程";        boolean removed = list.removeIf(e -> e.title.equals(title));        return removed ? "已删除:" + title : "未找到日程:" + title;    }    // 初始化一些测试数据    public CalendarTool() {        events.put("2026-04-25"List.of(            new CalendarEvent("项目评审会""15:00"List.of("张三""李四")),            new CalendarEvent("1:1 周会""10:00"List.of("王五"))        ));    }    record CalendarEvent(String title, String time, List<String> participants) {}}

2.3 邮件工具组

public class EmailTool {    @Tool("发送一封邮件给指定收件人")    public String sendEmail(            @P("收件人姓名"String recipient,            @P("邮件主题"String subject,            @P("邮件正文内容"String body    ) {        // 实际项目中:调用邮件服务 API(JavaMail / SendGrid / 企业微信)        // 这里用控制台输出模拟        System.out.println("📧 发送邮件 ====");        System.out.println("  收件人:" + recipient);        System.out.println("  主题:" + subject);        System.out.println("  正文:" + body);        System.out.println("================");        return "邮件已发送给 " + recipient + ",主题:" + subject;    }    @Tool("查询某个联系人的邮箱地址")    public String lookupEmail(            @P("联系人姓名"String name    ) {        // 模拟通讯录        Map<StringString> contacts = Map.of(            "张三""zhangsan@company.com",            "李四""lisi@company.com",            "王五""wangwu@company.com"        );        String email = contacts.get(name);        return email != null ? email : "未找到 " + name + " 的邮箱";    }}

2.4 知识检索工具组(RAG 集成)

public class KnowledgeTool {    private final EmbeddingStore<TextSegment> embeddingStore;    private final EmbeddingModel embeddingModel;    public KnowledgeTool(EmbeddingStore<TextSegment> store, EmbeddingModel model) {        this.embeddingStore = store;        this.embeddingModel = model;    }    @Tool("从知识库中搜索与查询相关的文档片段,返回最匹配的内容")    public String searchKnowledge(            @P("搜索关键词或问题,如'销售报告'、'Q1数据'") String query    ) {        try {            // 生成查询向量            Embedding queryEmbedding = embeddingModel.embed(query).content();            // 搜索最相关的 3 个文档片段            List<EmbeddingMatch<TextSegment>> matches = embeddingStore.search(                EmbeddingSearchRequest.builder()                    .queryEmbedding(queryEmbedding)                    .maxResults(3)                    .minScore(0.5)                    .build()            ).matches();            if (matches.isEmpty()) {                return "知识库中未找到与'" + query + "'相关的内容";            }            StringBuilder sb = new StringBuilder("找到以下相关内容:\n");            for (int i = 0; i < matches.size(); i++) {                EmbeddingMatch<TextSegment> match = matches.get(i);                sb.append(String.format("\n[片段%d](相关度:%.0f%%)\n%s\n",                        i + 1, match.score() * 100, match.embedded().text()));            }            return sb.toString();        } catch (Exception e) {            return "知识检索出错:" + e.getMessage();        }    }}

2.5 注册到 AiService

🤔 思考 3:三个工具组,各自是一个对象。注册时怎么让 AI Service 知道所有工具?

// 构建 AI Service:把三个工具组的实例全部注册SuperAssistant assistant = AiServices.builder(SuperAssistant.class)        .chatModel(chatModel)        .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(30))        .tools(            new CalendarTool(),    // 日历工具组            new EmailTool(),       // 邮件工具组            knowledgeTool          // 知识检索工具组        )        .build();
关键理解:tools() 方法可以接收多个对象,框架会自动扫描每个对象上所有带 @Tool 注解的方法,汇总成完整的工具列表给 LLM。

LLM 看到的工具列表:

工具1:queryEvents(日期) → 查询日程工具2:addEvent(标题, 日期, 时间, 参与者) → 添加日程工具3:removeEvent(日期, 标题) → 删除日程工具4:sendEmail(收件人, 主题, 正文) → 发邮件工具5:lookupEmail(姓名) → 查邮箱工具6:searchKnowledge(查询) → 搜索知识库
6 个工具,AI 根据用户问题自动选择调用哪个。

三、Agent 的 Planning 能力:让 AI 自己规划多步骤任务

3.1 单步 vs 多步:AI 能自己”想”吗?

回忆第14篇讲的 ReAct 循环。当用户说:

“帮我查明天有没有会议,如果有的话给张三发邮件确认”

AI 内部的思考过程(ReAct):
1轮:用户提问  → LLM 思考:需要先查日历  → 调用 queryEvents("2026-04-25")  → 结果:"15:00 项目评审会(张三, 李四)"2轮:LLM 分析结果  → LLM 思考:有会议,需要给张三发邮件  → 先查张三的邮箱:lookupEmail("张三")  → 结果:"zhangsan@company.com"3轮:LLM 分析结果  → LLM 思考:有邮箱了,可以发邮件  → 调用 sendEmail("张三""明天会议确认""请确认...")  → 结果:"邮件已发送"4轮:LLM 判断任务完成  → 输出最终回答
这个 4 步的规划,AI 自己完成的,你不需要写任何 if-else。

🤔 思考 4:AI 怎么知道”有会议”就要”发邮件”?是谁教它的?

答案是:SystemMessage。你只需要在提示词里说清楚规则,AI 就会按规则执行。

3.2 设计高质量的 SystemMessage

SystemMessage 是 Agent 的”操作手册”,写得越清楚,AI 规划越准确:
@SystemMessage("""    你是一个高效的私人助手,名叫"小助"。你同时管理日历、邮件和知识库。    ## 工具使用规则:    1. 日程查询:用户问到"有没有会议"、"明天有什么安排"时,先查日历    2. 邮件发送:       - 发邮件前,先用 lookupEmail 查收件人的邮箱地址       - 不要编造邮箱地址,只使用 lookupEmail 返回的地址       - 邮件内容要简洁专业    3. 知识检索:用户问到公司文档、报告、数据时,搜索知识库    4. 组合操作:用户说"如果...就..."时,先查条件,再根据结果决定下一步    ## 终止条件:    - 当你已经完成了用户要求的所有操作,输出总结性回复    - 不要重复调用已经成功执行的工具    ## 出错处理:    - 如果工具返回错误,向用户说明情况并建议替代方案    - 不要因为一个工具失败就放弃整个任务    """)interface SuperAssistant {    String chat(@MemoryId String userId, @UserMessage String message);}

💡 经验之谈:SystemMessage 里最重要的是终止条件出错处理。没有终止条件,AI 可能无限循环;没有出错处理,一个工具挂了整个 Agent 就瘫痪。

四、对话上下文的 Tool 调用追踪

4.1 为什么需要追踪?

多工具协作时,AI 一次请求可能调用 3-4 个工具。如果最终回答不对,你无从知道是哪一步出了问题:
❌ 用户问题 → AI 回答错误  是工具选错了?参数传错了?还是 AI 理解错了用户意图?  → 没有追踪 = 盲人摸象

4.2 用 ChatModelListener 记录调用链

ChatModelListener listener = new ChatModelListener() {    private final AtomicInteger round = new AtomicInteger(0);    @Override    public void onRequest(ChatModelRequestContext context) {        int r = round.incrementAndGet();        int msgCount = context.chatRequest().messages().size();        System.out.printf("\n🔷 [第%d轮] 发送给 LLM,消息数:%d%n", r, msgCount);    }    @Override    public void onResponse(ChatModelResponseContext context) {        AiMessage ai = context.chatResponse().aiMessage();        if (ai.hasToolExecutionRequests()) {            for (ToolExecutionRequest req : ai.toolExecutionRequests()) {                System.out.printf("🔧 调用工具:%s(%s)%n", req.name(), req.arguments());            }        } else {            System.out.println("✅ 最终回答:" + ai.text());        }    }    @Override    public void onError(ChatModelErrorContext context) {        System.err.println("❌ 错误:" + context.error().getMessage());    }};

4.3 追踪输出示例

用户:明天有没有会议?有的话给张三发邮件确认🔷 [第1轮] 发送给 LLM,消息数:2🔧 调用工具:queryEvents({"date":"2026-04-25"})🔷 [第2轮] 发送给 LLM,消息数:4🔧 调用工具:lookupEmail({"name":"张三"})🔷 [第3轮] 发送给 LLM,消息数:6🔧 调用工具:sendEmail({"recipient":"张三","subject":"明天会议确认","body":"..."})🔷 [第4轮] 发送给 LLM,消息数:8✅ 最终回答:明天下午3点有项目评审会,已给张三(zhangsan@company.com)发送确认邮件。

🤔 思考 5:通过追踪日志,你能看出 AI 的”思考链路”。如果第2轮调的不是 lookupEmail 而是直接调 sendEmail,说明 SystemMessage 里”先查邮箱再发邮件”的规则没生效,需要优化提示词。

五、异常处理与回退机制设计

5.1 工具会出错,Agent 不能死

🤔 思考 6:如果邮件服务宕机了,Agent 应该怎么办?

方案

体验

❌ 直接崩溃

用户看到500错误

❌ 返回 null

AI 不知道出错了,继续编造结果

✅ 返回错误描述 + 建议

AI 告知用户邮件发不了,建议替代方案

5.2 工具内部异常处理

每个工具方法都应该有 try-catch,返回描述性错误:
@Tool("发送一封邮件给指定收件人")public String sendEmail(        @P("收件人姓名"String recipient,        @P("邮件主题"String subject,        @P("邮件正文内容"String body) {    try {        // 调用邮件服务        emailService.send(recipient, subject, body);        return "邮件已成功发送给 " + recipient;    } catch (TimeoutException e) {        // 超时 → 降级        return "⚠️ 邮件服务响应超时,邮件未能发送。" +               "建议:您可以稍后重试,或通过企业微信直接联系 " + recipient;    } catch (Exception e) {        // 其他异常        return "⚠️ 邮件发送失败:" + e.getMessage() +               "。建议:请检查收件人姓名是否正确,或联系管理员。";    }}

5.3 AI 收到错误信息后的行为

关键来了——AI 读到错误信息后,会怎么做
第1轮:用户说"给张三发邮件"第2轮:AI 调用 lookupEmail("张三") → 成功第3轮:AI 调用 sendEmail("张三", ...) → 返回 "⚠️ 邮件服务超时"第4轮:AI 看到错误信息,自动判断:  → 输出:"抱歉,邮件服务目前响应超时,未能发送给张三。           您可以通过企业微信直接联系张三,或者稍后让我重试。"
AI 自己理解了错误,还给了解决建议! 你不需要写任何异常页面的代码。

💡 核心原则:工具方法永远不要抛异常,永远返回 String 描述。让 AI 自己决定怎么告诉用户。

5.4 多级降级策略

工具正常        → 直接返回结果     → AI 使用结果回答工具超时        → 返回超时+建议    → AI 告知用户稍后重试工具彻底不可用   → 返回不可用信息   → AI 建议替代方案所有工具都不可用 → AI 只剩对话能力  → 降级为纯聊天模式

六、RAG + Tool 混合 Agent

6.1 为什么需要混合?

前面的知识检索工具(KnowledgeTool)是一种手动集成 RAG 的方式——把检索逻辑写成一个 @Tool,让 AI 决定什么时候调。

LangChain4j 还提供了一种更优雅的方式:RetrievalAugmentor,自动把检索结果注入到每次对话的消息中。

6.2 两种模式对比

模式 A:RAG 作为 Tool(本章做法)  用户提问 → AI 判断是否需要检索 → 调用 searchKnowledge() → 获取结果 → 回答  ✅ AI 自主决定是否检索  ✅ 只在需要时消耗 Token  ❌ 多一轮工具调用,响应稍慢模式 B:RetrievalAugmentor 自动注入  用户提问 → 框架自动检索 → 把相关内容塞到消息里 → AI 直接回答  ✅ 响应更快(少一轮调用)  ✅ AI 总是能看到相关上下文  ❌ 每次都检索,即使不需要  ❌ 消耗更多 Token

🤔 思考 7:哪种模式更适合”私人助手”场景?

答案:混合使用。通用知识用 RetrievalAugmentor 自动注入,特定查询用 Tool 按需调用。

6.3 混合集成代码

// 1. 创建 EmbeddingStore 和 EmbeddingModelEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();EmbeddingModel embeddingModel = new BgeSmallEnV15QuantizedEmbeddingModel();// 2. 索引一些测试文档TextSegment doc1 = TextSegment.from(    "2026年Q1销售报告:总销售额580万元,同比增长15%。" +    "其中线上渠道占320万,线下渠道占260万。报告文件位于 /reports/2026-Q1-销售报告.pdf");TextSegment doc2 = TextSegment.from(    "项目评审流程:每次评审需提前2个工作日提交材料," +    "评审委员会由3名技术专家和2名业务专家组成。");EmbeddingStoreIngestor.builder()    .embeddingModel(embeddingModel)    .embeddingStore(embeddingStore)    .build()    .ingest(List.of(doc1, doc2));// 3. 创建 RetrievalAugmentor(自动注入模式)ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()    .embeddingStore(embeddingStore)    .embeddingModel(embeddingModel)    .maxResults(3)    .minScore(0.5)    .build();RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()    .contentRetriever(contentRetriever)    .build();// 4. 同时保留 KnowledgeTool(按需调用模式)KnowledgeTool knowledgeTool = new KnowledgeTool(embeddingStore, embeddingModel);// 5. 构建 Agent:Tool + RetrievalAugmentor 双管齐下SuperAssistant assistant = AiServices.builder(SuperAssistant.class)        .chatModel(chatModel)        .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(30))        .tools(new CalendarTool(), new EmailTool(), knowledgeTool)        .contentRetriever(contentRetriever)  // ← 自动注入 RAG        .build();
运行时行为:
用户:上次的销售数据怎么样?  → RetrievalAugmentor 自动检索到 Q1 销售报告片段  → 注入到消息上下文中  → AI 直接基于注入的内容回答(不需要额外调工具)用户:帮我找一下去年Q4的客户满意度调查报告  → 自动注入的内容可能不包含这个  → AI 判断需要更精确搜索 → 调用 searchKnowledge("Q4客户满意度")  → 返回更精准的结果

七、【完整实战】SuperAssistant 完整实现

7.1 完整主程序

import dev.langchain4j.data.embedding.Embedding;import dev.langchain4j.data.segment.TextSegment;import dev.langchain4j.memory.chat.MessageWindowChatMemory;import dev.langchain4j.model.chat.ChatModel;import dev.langchain4j.model.chat.listener.ChatModelErrorContext;import dev.langchain4j.model.chat.listener.ChatModelListener;import dev.langchain4j.model.chat.listener.ChatModelRequestContext;import dev.langchain4j.model.chat.listener.ChatModelResponseContext;import dev.langchain4j.model.embedding.EmbeddingModel;import dev.langchain4j.model.openai.OpenAiChatModel;import dev.langchain4j.model.openai.OpenAiEmbeddingModel;import dev.langchain4j.service.AiServices;import dev.langchain4j.service.MemoryId;import dev.langchain4j.service.SystemMessage;import dev.langchain4j.service.UserMessage;import dev.langchain4j.store.embedding.EmbeddingMatch;import dev.langchain4j.store.embedding.EmbeddingSearchRequest;import dev.langchain4j.store.embedding.EmbeddingStore;import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;public class SuperAssistantDemo {    // ==================== Agent 接口定义 ====================    @SystemMessage("""        你是一个高效的私人助手,名叫"小助"。你同时管理日历、邮件和知识库。        ## 工具使用规则:        1. 日程查询:用户问到"有没有会议"、"明天有什么安排"时,先查日历        2. 邮件发送:           - 发邮件前,先用 lookupEmail 查收件人的邮箱地址           - 不要编造邮箱地址,只使用 lookupEmail 返回的地址           - 邮件内容要简洁专业        3. 知识检索:用户问到公司文档、报告、数据时,搜索知识库        4. 组合操作:用户说"如果...就..."时,先查条件,再根据结果决定下一步        ## 终止条件:        - 当你已经完成了用户要求的所有操作,输出总结性回复        - 不要重复调用已经成功执行的工具        ## 出错处理:        - 如果工具返回错误,向用户说明情况并建议替代方案        - 不要因为一个工具失败就放弃整个任务        """)    interface SuperAssistant {        String chat(@MemoryId String userId, @UserMessage String message);    }    // ==================== 启动入口 ====================    public static void main(String[] args) {        // 1. 创建 ChatModel        ChatModel chatModel = OpenAiChatModel.builder()                .apiKey(System.getenv("API_KEY"))                .baseUrl("https://api.minimax.chat/v1")                .modelName("MiniMax-M2.5")                .listeners(List.of(new TraceListener()))                .build();        // 2. 创建 Embedding 模型和向量库(通过 API 调用,无需本地 ONNX)        EmbeddingModel embeddingModel = OpenAiEmbeddingModel.builder()                .apiKey(System.getenv("API_KEY"))                .baseUrl("https://open.bigmodel.cn/api/paas/v4")                .modelName("embedding-3")                .build();        EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();        // 3. 索引测试文档        indexSampleDocuments(embeddingModel, embeddingStore);        // 4. 构建 Agent        SuperAssistant assistant = AiServices.builder(SuperAssistant.class)                .chatModel(chatModel)                .chatMemoryProvider(id -> MessageWindowChatMemory.withMaxMessages(30))                .tools(                        new CalendarTool(),                        new EmailTool(),                        new KnowledgeTool(embeddingStore, embeddingModel)                )                .build();        // 5. 测试对话        System.out.println("===== 测试1:查日历 + 发邮件(组合任务)=====");        String r1 = assistant.chat("user_001",                "帮我查一下2026-04-25有没有会议,如果有的话给张三发邮件确认");        System.out.println("AI:" + r1);        System.out.println("\n===== 测试2:知识检索 =====");        String r2 = assistant.chat("user_001",                "上次Q1的销售数据怎么样?报告文件在哪?");        System.out.println("AI:" + r2);        System.out.println("\n===== 测试3:添加日程 + 通知 =====");        String r3 = assistant.chat("user_001",                "帮我下周一上午10点安排一个产品评审会,参与者是张三和李四," +                        "然后发邮件通知他们");        System.out.println("AI:" + r3);        System.out.println("\n===== 测试4:综合查询 =====");        String r4 = assistant.chat("user_001",                "帮我看下2026-04-25的安排,如果有空的话帮我安排下午2点技术分享");        System.out.println("AI:" + r4);    }    private static void indexSampleDocuments(EmbeddingModel model,                                             EmbeddingStore<TextSegment> store) {        List<TextSegment> docs = List.of(                TextSegment.from("2026年Q1销售报告:总销售额580万元,同比增长15%。" +                        "线上渠道320万,线下渠道260万。文件位于 /reports/2026-Q1-销售报告.pdf"),                TextSegment.from("项目评审流程:每次评审需提前2个工作日提交材料," +                        "评审委员会由3名技术专家和2名业务专家组成。")        );        for (TextSegment doc : docs) {            Embedding embedding = model.embed(doc).content();            store.add(embedding, doc);        }    }    // ==================== 调用追踪 Listener ====================    static class TraceListener implements ChatModelListener {        private final AtomicInteger round = new AtomicInteger(0);        @Override        public void onRequest(ChatModelRequestContext context) {            System.out.printf("  🔷 [第%d轮] → LLM%n", round.incrementAndGet());        }        @Override        public void onResponse(ChatModelResponseContext context) {            var ai = context.chatResponse().aiMessage();            if (ai.hasToolExecutionRequests()) {                for (var req : ai.toolExecutionRequests()) {                    System.out.printf("  🔧 工具调用:%s(%s)%n",                            req.name(), req.arguments());                }            }        }        @Override        public void onError(ChatModelErrorContext context) {            System.err.println("  ❌ " + context.error().getMessage());        }    }}

7.2 运行效果

===== 测试1:查日历 + 发邮件(组合任务)=====  🔷 [第1轮] → LLM  🔧 工具调用:queryEvents({"date":"2026-04-25"})  🔷 [第2轮] → LLM  🔧 工具调用:lookupEmail({"name":"张三"})  🔷 [第3轮] → LLM  🔧 工具调用:sendEmail({"recipient":"张三","subject":"会议确认","body":"..."})  🔷 [第4轮] → LLMAI:2026-04-25 有一个项目评审会(15:00,参与者:张三、李四),已给张三发送确认邮件。===== 测试2:知识检索 =====  🔷 [第1轮] → LLM  🔧 工具调用:searchKnowledge({"query":"Q1销售数据报告"})  🔷 [第2轮] → LLMAI:根据Q1销售报告,总销售额580万元,同比增长15%。其中线上渠道320万,线下渠道260万。   报告文件在 /reports/2026-Q1-销售报告.pdf。===== 测试3:添加日程 + 通知 =====  🔷 [第1轮] → LLM  🔧 工具调用:addEvent({"title":"产品评审会","date":"2026-04-27","time":"10:00","participants":"张三,李四"})  🔷 [第2轮] → LLM  🔧 工具调用:lookupEmail({"name":"张三"})  🔷 [第3轮] → LLM  🔧 工具调用:lookupEmail({"name":"李四"})  🔷 [第4轮] → LLM  🔧 工具调用:sendEmail({"recipient":"张三","subject":"产品评审会通知","body":"..."})  🔷 [第5轮] → LLM  🔧 工具调用:sendEmail({"recipient":"李四","subject":"产品评审会通知","body":"..."})  🔷 [第6轮] → LLMAI:已安排下周一上午10点的产品评审会,并分别给张三和李四发送了通知邮件。===== 测试4:综合查询 =====  🔷 [第1轮] → LLM  🔧 工具调用:queryEvents({"date":"2026-04-25"})  🔷 [第2轮] → LLM  🔧 工具调用:addEvent({"title":"技术分享","date":"2026-04-25","time":"14:00","participants":""})  🔷 [第3轮] → LLMAI:2026-04-25 已有两个日程(10:00 1:1周会、15:00 项目评审会),下午2点空闲,已安排技术分享。

7.3 测试3 的 AI 思考链路分析

🤔 思考 8:测试3中,AI 为什么先查了张三和李四各自的邮箱,而不是一次发两封邮件?

因为 sendEmail 的参数是单个收件人,不是收件人列表。 AI 看到”给张三和李四发邮件”,自动拆解成了:
1.查张三邮箱 → 发邮件给张三
2.查李四邮箱 → 发邮件给李四
这就是 ReAct 的力量——AI 自己分解了多步任务。

如果想让 AI 更高效,可以增加一个 sendBulkEmail 工具支持批量发送,减少调用轮次。

八、工程实践:生产环境 Checklist

8.1 上线前的检查清单

检查项

说明

本章方案

工具描述准确

每个工具描述是否清晰、有示例?

✅ 每个参数都有格式说明

SystemMessage 有终止条件

AI 知道什么时候停吗?

✅ 明确写了终止规则

异常不外泄

工具方法都有 try-catch?

✅ 每个工具都处理异常

Memory 有上限

对话不会无限膨胀?

✅ maxMessages=30

调用可追踪

出问题能查到是哪一步?

✅ ChatModelListener

工具按领域分离

不是一个大杂烩类?

✅ Calendar/Email/Knowledge 三组

8.2 性能优化建议

当前(演示级):  用户提问 → LLM 调工具(1-2轮) → LLM 再调工具(1-2轮) → 回答  总延迟:3-8秒优化方向:  1. 工具合并:把 lookupEmail + sendEmail 合并为 sendEmailToContact     → 减少1轮调用,省2-3秒  2. 并行工具调用:LangChain4j 支持 LLM 一次请求返回多个 ToolExecutionRequest     → 查张三邮箱 + 查李四邮箱同时进行  3. 缓存工具结果:日历查询结果缓存5分钟,避免重复查询     → 相同日期不重复调API  4. Token 预算:设置 maxTokens 限制,防止单次响应过长

8.3 Token 消耗估算

单次对话(3轮工具调用):  消息上下文:~2000 tokens(SystemMessage + 历史 + 工具结果)  每轮 LLM 调用:~500 tokens(输入+输出)  总计:2000 + 3×500 = ~3500 tokens按 MiniMax-M2.5 定价(假设 ¥0.01/1K tokens):  单次对话成本:¥0.035  1000/天:¥35/

九、总结

核心要点

按领域分组工具        CalendarTool / EmailTool / KnowledgeTool多工具注册            AiServices.builder().tools(obj1, obj2, obj3)SystemMessage 规则    明确工具使用规则 + 终止条件 + 出错处理ReAct 多步规划        AI 自己决定先查日历→再发邮件→最后汇报异常降级              工具内 try-catch → 返回错误描述 → AI 给建议RAG + Tool 混合       RetrievalAugmentor 自动注入 + KnowledgeTool 按需搜索调用追踪              ChatModelListener 记录每轮工具调用

从第13篇到第15篇的完整演进

13篇:给 AI 装上"手"Tool  → AI 能调工具了,但只能单步执行14篇:给 AI 装上"脑"Agent  → AI 能自主规划多步任务,有了记忆15篇:让 AI 成为真正的"助手"(实战整合)  → 多工具协作 + RAG + 异常降级 + 调用追踪  → 这就是一个能上线的 Agent 了

9.1 下一步

本篇构建的是单 Agent——一个助手搞定所有事。但当业务复杂到一定程度,一个 Agent 不够用了:
·文档分析 Agent 专门处理文件
·日程 Agent 专门管日历
·一个”调度员” Agent 负责把任务分给专门的 Agent
这就是 Multi-Agent 编排,LangChain4j 的 langchain4j-agentic 模块专门解决这个问题。

📌 下一篇预告:第16篇——Multi-Agent 编排:让一群 AI 协同作战

剧透

·SequentialAgent / ParallelAgent / SupervisorAgent 三大编排模式

·把本篇的 SuperAssistant 拆成”日程 Agent”+”邮件 Agent”+”检索 Agent”

·Supervisor 模式:一个”经理 Agent” 调度多个”员工 Agent”

·AgenticScope:Agent 之间如何传递数据和共享上下文

📱 小貔貅Agent日记

一个专注Java AI应用开发的技术号

关注我,带你用Java玩转AI!