AI Agent 项目学习笔记(二):Spring AI 与 ChatClient 主链路解析 1. 本期目标上一篇文章整体介绍了ai_agent项目。我们已经知道这个项目不是一个简单聊天机器人而是一个基于Spring Boot 3和Spring AI的智能体能力实践项目主要覆盖多轮对话、RAG 检索增强、工具调用、结构化输出和会话记忆等能力。项目 README 中也明确说明它的目标是通过ChatClient Advisor ChatMemory组织对话主流程再结合 RAG 和 Tool Calling 扩展智能体能力。(GitHub)这一期进入项目的核心类LoveApp本期主要解决几个问题1. LoveApp 在项目中起什么作用 2. ChatModel 和 ChatClient 分别是什么 3. ChatClient.builder() 如何构建智能体主链路 4. defaultSystem() 如何固定智能体角色 5. defaultAdvisors() 如何接入记忆和日志增强 6. doChat() 的完整调用流程是什么 7. chatResponse() 和 entity() 分别适合什么场景 8. 这个主链路设计有什么优点和不足2. 为什么先分析 ChatClient学习这个项目不能一上来就直接看 RAG 或工具调用。因为 RAG、记忆、工具调用这些能力本质上都是挂在模型对话主链路上的增强能力。也就是说项目的核心不是RAG 单独运行 工具单独运行 记忆单独运行而是用户输入 ↓ ChatClient 组织对话请求 ↓ Advisor 加入记忆、日志、RAG 等增强 ↓ 模型生成结果 ↓ 必要时调用工具 ↓ 返回最终回答所以ChatClient是整个项目的主干。如果没有理解ChatClient后面看Advisor、ChatMemory、QuestionAnswerAdvisor、ToolCallback[]时就会比较散。3. ChatModel 和 ChatClient 的关系在LoveApp构造函数中项目注入的是public LoveApp(ChatModel dashscopeChatModel)然后通过ChatClient.builder(dashscopeChatModel)构建ChatClient。源码中可以看到LoveApp接收ChatModel随后使用ChatClient.builder(dashscopeChatModel)设置默认系统提示词和默认 Advisor最后调用.build()得到chatClient。(GitHub)可以这样理解ChatModel 底层模型接口负责真正和大模型交互。 ChatClient Spring AI 提供的高级客户端负责把 prompt、system message、advisor、tool、response 等能力组织起来。 LoveApp 面向业务场景的智能体封装类。也就是说ChatModel更底层ChatClient更适合业务开发。项目没有直接在每个方法里调用ChatModel而是先构造一个带有默认配置的ChatClient。这样后续普通对话、结构化输出、RAG 对话、工具调用都可以复用同一条主链路。4. ChatClient 是什么Spring AI 官方文档中ChatClient被定义为一种用于和 AI 模型通信的 Fluent API它支持同步和流式调用并可以逐步构造传给模型的 Prompt。Prompt 中通常包含系统消息、用户消息以及其他上下文信息。(spring-doc.cn)可以把ChatClient理解成 Spring AI 里的“模型调用编排器”。它不是只负责发送一句话而是负责组织system prompt user message chat memory advisor tool callback response format普通模型调用可能是String answer model.call(你好);而ChatClient更像chatClient .prompt() .system(你是谁) .user(用户问题) .advisors(...) .toolCallbacks(...) .call() .chatResponse();这就是 Fluent API 的好处模型调用链路可以一步一步拼出来代码的可读性更强。5. LoveApp 的定位LoveApp位于src/main/java/com/ai/aiagent/app/LoveApp.java它被标注为Component Slf4j public class LoveApp也就是说它是一个 Spring Bean可以被其他组件注入和调用。源码中LoveApp内部维护了一个private final ChatClient chatClient;并通过构造函数完成初始化。(GitHub)从项目结构看LoveApp是智能体应用层的入口。它提供了四类能力doChat普通多轮对话 doChatWithReport结构化报告输出 doChatWithRagRAG 检索增强对话 doChatWithTools工具调用对话README 中也将LoveApp作为智能体主链路核心入口并明确列出普通对话、结构化报告、RAG 对话和工具调用这四种方法。(GitHub)所以可以这样理解LoveApp 不是普通 Service 而是一个封装了智能体核心能力的应用类6. SYSTEM_PROMPT先固定智能体身份在LoveApp中最先值得关注的是private static final String SYSTEM_PROMPT ...这个系统提示词要求模型扮演“深耕恋爱心理领域的专家”开场向用户表明身份并围绕单身、恋爱、已婚三种状态引导用户描述问题。源码中也明确写了单身、恋爱、已婚三类场景下应该关注的提问方向。(GitHub)这说明项目不是让模型自由聊天而是先用系统提示词限定角色。可以理解为没有 system prompt 模型只是一个通用助手。 有 system prompt 模型被限定为恋爱心理咨询方向的智能体。这一步很关键。因为智能体首先要有“角色边界”。如果没有系统提示词用户问什么模型就答什么系统就很难形成垂直场景能力。7. defaultSystem()设置默认系统提示词在构造ChatClient时项目使用.defaultSystem(SYSTEM_PROMPT)这表示后续通过这个chatClient发起的默认对话都会带上这个系统提示词。也就是说doChat()里虽然没有重新写 system prompt但它仍然会继承构造时配置的默认系统提示词。可以理解为构造 ChatClient 时 设置默认角色 每次 doChat 调用时 自动带着这个角色去回答这种写法比每个方法都重复写 system prompt 更清晰。如果后续项目要从“恋爱咨询 Agent”扩展成多个 Agent可以为不同业务类配置不同 system promptLoveApp恋爱咨询智能体 PaperApp论文阅读智能体 CodeApp代码分析智能体 SecurityApp安全分析智能体每个 App 都可以有自己的defaultSystem()。8. ChatMemory先构造对话记忆在LoveApp构造函数中项目创建了一个ChatMemoryChatMemory chatMemory MessageWindowChatMemory.builder() .chatMemoryRepository(new InMemoryChatMemoryRepository()) .build();同时源码中还能看到一行被注释掉的文件式记忆实现// ChatMemory chatMemory new FileBasedChatMemory(fileDir);这说明项目当前默认使用内存版MessageWindowChatMemory但也预留了文件持久化记忆的切换方式。(GitHub)可以理解为MessageWindowChatMemory 负责保存最近一段对话窗口。 InMemoryChatMemoryRepository 把对话历史保存在内存中。 FileBasedChatMemory 把对话历史保存到本地文件中。本期不展开记忆实现细节只要先理解一点ChatClient 本身不等于记忆 记忆是通过 Advisor 接入 ChatClient 的9. defaultAdvisors()给所有对话加默认增强LoveApp构造ChatClient时还使用了.defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build(), new MyLoggerAdvisor() )也就是说项目默认给所有对话加了两个增强MessageChatMemoryAdvisor 负责把会话记忆接入模型调用。 MyLoggerAdvisor 负责打印 AI 请求和响应日志。源码中LoveApp的defaultAdvisors()正是这样配置的。(GitHub)这说明Advisor可以理解成模型调用链路中的增强器。它有点像 Web 后端中的拦截器请求进入模型前 Advisor 可以修改或增强请求。 模型返回后 Advisor 可以记录、观察或处理响应。当然Advisor 不只是日志。后面 RAG 中的QuestionAnswerAdvisor也是 Advisor。所以整个项目的设计思路是ChatClient 负责主流程 Advisor 负责增强流程10. MyLoggerAdvisor日志增强MyLoggerAdvisor是项目自定义的日志 Advisor。从源码看它实现了CallAdvisor和StreamAdvisor在请求前打印AI Request在响应后打印AI Response。同步调用时它通过adviseCall()包裹模型调用流式调用时它通过ChatClientMessageAggregator聚合流式响应后再观察输出。(GitHub)可以简单理解为用户请求进入模型前 打印请求内容 模型生成结果后 打印响应内容这对学习项目很有帮助。因为初学 Agent 项目时最容易困惑的是最终传给模型的 prompt 到底长什么样 模型返回了什么 Advisor 有没有生效 RAG 有没有把资料拼进去日志 Advisor 就是为了帮助观察这些过程。11. doChat()普通对话主链路现在来看最基础的方法public String doChat(String message, String chatId)源码中doChat()的链路是ChatResponse response chatClient .prompt() .user(message) .advisors(spec - spec.param(ChatMemory.CONVERSATION_ID, chatId)) .call() .chatResponse(); String content response.getResult().getOutput().getText(); return content;源码显示doChat()接收用户消息和chatId通过prompt()开始构造请求使用.user(message)设置用户输入使用 Advisor 参数传入ChatMemory.CONVERSATION_ID然后调用.call().chatResponse()获取模型响应并从响应对象中取出文本。(GitHub)这个方法就是整个项目最基础的对话链路。12. doChat() 流程拆解可以把doChat()拆成六步第一步chatClient.prompt() 开始构造一次模型请求。 第二步user(message) 把用户输入作为 user message。 第三步advisors(...) 给本次请求设置 conversationId。 第四步call() 同步调用模型。 第五步chatResponse() 获取完整 ChatResponse 对象。 第六步getText() 提取模型最终文本回答。画成流程就是message chatId ↓ chatClient.prompt() ↓ .user(message) ↓ .advisors(conversationId chatId) ↓ .call() ↓ .chatResponse() ↓ response.getResult().getOutput().getText()这就是LoveApp的普通对话主流程。13. conversationId 在 doChat() 中的作用doChat()中最关键的一行是.advisors(spec - spec.param(ChatMemory.CONVERSATION_ID, chatId))这行代码不是普通参数而是告诉MessageChatMemoryAdvisor这次对话属于哪个会话也就是说chatId 会话编号同一个chatId下的消息会被认为属于同一段连续对话。例如chatId user_001_session_001 第一轮我和女朋友吵架了 第二轮那我应该怎么开口 第三轮她一直不回消息怎么办这些消息可以被同一个会话记忆串起来。如果换成另一个chatId上下文就会隔离。所以chatId的作用可以概括为让多轮对话有边界14. call() 和 chatResponse() 的含义在doChat()中项目使用的是.call() .chatResponse()其中call() 表示同步调用模型等待模型完整返回。 chatResponse() 表示返回完整的 ChatResponse 对象。Spring AI 文档中也给出了类似用法通过chatClient.prompt().user(...).call().chatResponse()可以拿到包含模型响应和元数据的ChatResponse。(spring-doc.cn)为什么项目不用更简单的.call() .content()因为chatResponse()拿到的是完整响应对象后续可以扩展更多信息比如模型输出文本 token 使用情况 generation metadata finish reason虽然当前项目最后只取了文本response.getResult().getOutput().getText()但使用ChatResponse给后续扩展留下了空间。15. doChatWithReport()结构化输出链路除了普通文本回答项目还提供了结构化输出方法public LoveReport doChatWithReport(String message, String chatId)在这个方法中项目定义了一个 Java recordrecord LoveReport(String title, ListString suggestions) {}然后通过.entity(LoveReport.class)把模型结果映射成LoveReport对象。源码中doChatWithReport()还会在 system prompt 后追加“每次对话后都要生成恋爱结果标题为{用户名}的恋爱报告内容为建议列表”这类结构化要求。(GitHub)Spring AI 文档中也说明entity()方法可以把模型返回结果映射成 Java 对象例如将模型输出映射为一个 record。(spring-doc.cn)所以doChatWithReport()和doChat()的区别是doChat 返回 String自然语言回答。 doChatWithReport 返回 LoveReport结构化业务对象。16. 为什么结构化输出很重要在普通聊天场景中返回字符串就够了。但在业务系统中经常需要模型返回可解析结果。例如标题 建议列表 风险等级 计划步骤 推荐地点 待办事项如果只返回字符串后端很难稳定解析。而结构化输出可以让后端继续处理模型输出 ↓ 映射成 Java 对象 ↓ 保存数据库 ↓ 前端卡片展示 ↓ 生成 PDF 报告在这个项目中LoveReport只是一个简单示例。但它背后的思想很重要AI 的输出不只是给人看 也可以给程序继续使用17. doChatWithRag()在主链路上接入 RAG虽然本期重点是ChatClient主链路但可以简单看一下 RAG 方法。doChatWithRag()的流程是先对用户问题进行查询重写 ↓ 把重写后的问题作为 user message ↓ 传入 conversationId ↓ 添加 MyLoggerAdvisor ↓ 添加 QuestionAnswerAdvisor ↓ 调用模型源码中可以看到方法先调用queryRewriter.doQueryRewrite(message)然后使用new QuestionAnswerAdvisor(loveAppVectorStore)接入本地向量知识库。(GitHub)这里要注意RAG 并不是另起一套模型调用流程。它仍然是chatClient.prompt()只是额外加了.advisors(new QuestionAnswerAdvisor(loveAppVectorStore))所以 RAG 的本质是在 ChatClient 主链路上增加检索增强 Advisor18. QueryRewriterRAG 前的查询改写QueryRewriter负责在检索前改写用户问题。从源码看它通过ChatClient.builder(dashscopeChatModel)创建查询重写所需的客户端构造器再使用RewriteQueryTransformer.builder().chatClientBuilder(builder).build()构造查询转换器。执行时它把用户 prompt 封装成Query调用queryTransformer.transform(query)得到改写后的查询文本。(GitHub)可以理解为用户原始问题 我和她最近关系有点冷淡怎么办 改写后可能变成 恋爱关系冷淡、沟通减少、亲密关系修复建议改写后的问题更适合去知识库里检索相关内容。这个模块后面可以单独写一篇这里只需要知道它也是服务于ChatClient RAG Advisor主链路的。19. doChatWithTools()在主链路上接入工具工具调用方法是public String doChatWithTools(String message, String chatId)它和普通对话的主要区别是多了一行.toolCallbacks(allTools)源码中allTools是通过Resource注入的ToolCallback[]doChatWithTools()会把这些工具回调注入到本次模型调用中。(GitHub)可以理解为普通 doChat 模型只能回答。 doChatWithTools 模型可以根据需要调用工具。完整流程是用户提出复杂需求 ↓ ChatClient 构造请求 ↓ 注入 ToolCallback[] ↓ 模型判断是否需要工具 ↓ 后端执行工具 ↓ 工具结果返回模型 ↓ 模型整理最终回答这也是 Agent 和普通聊天机器人最大的区别之一。20. 四条链路的共同点现在可以把LoveApp中的四条链路放在一起看doChat ChatClient System Prompt Memory Logger doChatWithReport ChatClient System Prompt Memory Structured Output doChatWithRag ChatClient Memory Logger Query Rewrite QuestionAnswerAdvisor doChatWithTools ChatClient Memory Logger ToolCallback[]它们看起来功能不同但底层主线是一样的chatClient .prompt() .user(...) .advisors(...) .call()区别只在于每条链路额外加了什么能力结构化输出 加 entity() RAG 加 QuestionAnswerAdvisor 工具调用 加 toolCallbacks() 多轮记忆 加 conversationId 参数所以学习这个项目时要抓住一个核心不同智能体能力都是围绕 ChatClient 主链路扩展出来的21. 这个主链路设计有什么优点21.1 代码结构清晰LoveApp把智能体能力集中封装起来。外部调用时不需要关心底层怎么拼 prompt、怎么传 advisor、怎么取 response只需要调用doChat doChatWithReport doChatWithRag doChatWithTools这让项目结构更清晰。21.2 默认配置复用性强系统提示词、记忆 Advisor、日志 Advisor 都在构造ChatClient时配置。这样普通对话、结构化输出、RAG 对话和工具调用都可以复用基础能力。21.3 扩展能力比较自然需要 RAG 时加一个QuestionAnswerAdvisor。需要工具调用时加一个ToolCallback[]。需要结构化输出时加.entity()。这种写法很符合 Spring AI 的链式风格也便于逐步学习和扩展。21.4 适合从简单到复杂演进项目不是一开始就写一个特别复杂的 Agent而是分成几条方法先做普通对话 再做结构化输出 再接入 RAG 最后接入工具调用这很适合学习者理解 Agent 的演进过程。22. 当前实现中可以改进的地方22.1 LoveApp 承担的职责有点多目前LoveApp同时包含普通对话 结构化报告 RAG 对话 工具调用对于学习项目来说这样写很直观。但如果后续业务复杂可以拆成LoveChatService LoveReportService LoveRagService LoveToolAgentService这样每个类职责更单一。22.2 system prompt 可以外部配置化当前SYSTEM_PROMPT写死在代码中。后续可以把它放到配置文件或数据库中例如agent: love: system-prompt: 扮演深耕恋爱心理领域的专家...这样修改智能体角色时就不用重新改代码。22.3 chatId 需要更规范的生成规则当前doChat()直接接收chatId。后续如果接入正式接口需要明确chatId 从哪里来 一个用户能不能访问另一个用户的 chatId chatId 是否需要和用户 ID 绑定 会话历史是否需要持久化否则会话隔离会存在风险。22.4 日志需要注意隐私MyLoggerAdvisor会打印用户输入和 AI 输出。学习阶段这样很方便观察。但正式系统中恋爱咨询内容可能包含隐私信息所以需要考虑敏感信息脱敏 日志级别控制 生产环境关闭详细日志 用户隐私合规22.5 工具调用链路需要安全边界doChatWithTools()把所有工具都注入给模型。后续可以按场景区分工具权限普通咨询 只允许文本回答 约会规划 允许网页搜索和网页抓取 报告生成 允许 PDF 生成 高级任务 才允许文件操作和终端执行这样比“一次性注入所有工具”更安全。23. 本期重点理解这一期最重要的是理解LoveApp的主链路。可以总结为五点第一ChatModel 是底层模型接口ChatClient 是更高层的对话编排客户端。 第二LoveApp 通过 ChatClient.builder(dashscopeChatModel) 构建智能体主流程。 第三defaultSystem(SYSTEM_PROMPT) 用于固定智能体角色。 第四defaultAdvisors() 默认接入对话记忆和日志增强。 第五doChat、doChatWithReport、doChatWithRag、doChatWithTools 都是在 ChatClient 主链路上的不同扩展。一句话概括LoveApp 的核心作用是把 Spring AI 的 ChatClient 封装成一个面向恋爱咨询场景的智能体应用入口。24. 我的理解我认为LoveApp是这个项目最值得先读懂的类。它展示了一个 Agent 项目的基本组织方式先用 system prompt 定义角色 再用 ChatClient 统一模型调用 再用 Advisor 接入记忆和日志 再按需要扩展 RAG、结构化输出和工具调用这个思路比直接写“调用模型 API”更工程化。普通模型调用只解决怎么问模型而LoveApp解决的是怎么把模型封装成一个可持续扩展的智能体这也是学习 Spring AI Agent 项目的关键。25. 本期小结本期主要分析了ai_agent项目中的 Spring AI 与ChatClient主链路。项目通过LoveApp封装智能体核心能力。LoveApp构造函数接收ChatModel并通过ChatClient.builder(dashscopeChatModel)创建对话客户端通过defaultSystem(SYSTEM_PROMPT)固定恋爱心理专家角色通过defaultAdvisors()默认接入MessageChatMemoryAdvisor和MyLoggerAdvisor在doChat()中通过prompt()、user()、advisors()、call()、chatResponse()完成一次支持多轮记忆的普通对话。同时项目还基于同一条主链路扩展了结构化输出、RAG 检索增强和工具调用能力。这一期可以用一句话总结ChatClient 主链路的作用是把模型调用、系统提示词、会话记忆、日志增强、结构化输出、RAG 和工具调用统一组织到一条可扩展的智能体调用流程中。下一期可以继续分析AI Agent 项目学习笔记三Advisor 机制与对话增强设计下一期重点分析MessageChatMemoryAdvisor、MyLoggerAdvisor、QuestionAnswerAdvisor和ReReadingAdvisor理解 Advisor 为什么可以看作 Spring AI 智能体中的“对话增强中间件”。