【JchatMind智能体 | 第六天】Agent Loop 的第一次落地
本文介绍了JChatMindV2智能代理系统的升级过程,重点阐述了如何从仅能聊天的V1版本演进为具备工具调用能力的V2版本。系统通过引入AgentLoop机制,将代理功能划分为Think决策、LLMs推理和Execute执行三大模块,形成完整的思考-执行循环。V2新增了工具管理、执行控制和状态维护等关键组件,采用手动接管工具调用流程的设计思路,确保系统拥有完整的执行轨迹记录和流程控制能力。文章详细
前言
本项目非原创,我也是作为一名初学者跟着一起学习。项目来源于:代码随想录-知识星球。
在知识星球里看到卡哥分享这个项目 ,感觉还不错,于是想要学习一下这个项目怎么写。项目日记也会同步更新。(本人不分享本项目源码,支持项目付费)
本文由我学习该项目并结合AI整理总结而来,分享出来学习过程中的心得体会,由浅入深,用于日后的回顾,同时也希望能给你带来帮助。
目录
【JchatMind智能体 | 第五天】实现带记忆的聊天功能
https://blog.csdn.net/h52412224/article/details/159158927
【JchatMind智能体 | 第四天】Spring AI 集成与多模型支持
https://blog.csdn.net/h52412224/article/details/159078281【JchatMind智能体 | 第三天】数据模型设计
https://blog.csdn.net/h52412224/article/details/159043756
在上一章里,我们用 JChatMindV1 实现了一个能聊天的最小 Agent。它已经具备了几个关键特征:
-
通过 ChatClient 接入模型能力
-
用 ChatMemory 显式维护对话上下文
-
对话是有 session、有状态的
但 V1 有个很明显的局限:它只能说话,不能做事。
一旦用户的问题需要和外部世界交互,比如查天气、查日期或者调用数据库,V1 就无能为力了。
这一章的目标,就是在不推翻 V1 原有结构的基础上,引入 Agent Loop,让 Agent 真正开始具备做事的能力:
-
判断什么时候需要调用外部工具
-
主动发起工具调用
-
根据工具返回的结果继续推进任务
从聊天到做事,差的是什么
这一次模型的回复,是不是最终答案?
如果不是,系统就必须介入,推动对话进入下一个阶段。
JChatMindV2 的全部设计,都是围绕这个问题展开的。
V2 相比 V1,多了哪些关键组件
我们先看看 V2 在类结构上的变化:
public class JChatMindV2 extends JChatMindV1 {
protected List<ToolCallback> availableTools;
protected ToolCallingManager toolCallingManager;
protected ChatOptions chatOptions;
protected ChatResponse lastChatResponse;
}
相比 V1,新增的这几个字段是关键:
-
availableTools:当前 Agent 可以使用的所有外部工具列表
-
ToolCallingManager:统一管理和执行所有工具调用的协调器
-
ChatOptions:显式控制模型的行为,比如是否自动执行工具
-
lastChatResponse:保存最近一次模型的完整输出,作为后续执行阶段的输入
Agent Loop 的执行流程控制
在 V2 的构造函数里,我们做了一个重要的设置:
this.chatOptions = DefaultToolCallingChatOptions.builder()
.internalToolExecutionEnabled(false)
.build();
Spring AI 原生支持模型自动执行工具并回填结果的一条龙流程,但我们在这里选择了手动接管。
原因很简单,Agent Loop 的核心不是工具调用,而是对执行过程的控制。
如果让 Spring AI 自动执行工具,Agent 就会失去很多关键能力:
-
记录完整执行轨迹的能力
-
插入中断、回滚和重试的机会
-
判断是否需要继续执行的决策权
所以在 V2 中,我们宁愿多写一些代码,也要把执行的控制权牢牢握在自己手里。
Agent Loop 的三大核心模块
整个 Agent Loop 可以清晰地分为三个功能模块:
-
Think 决策模块:分析当前对话上下文,决定下一步该做什么,是直接回答还是调用工具
-
LLMs 大语言模型:根据决策模块的要求,生成结构化的推理结果,可能是普通回复,也可能是工具调用指令
-
Execute 执行器:解析并执行模型生成的工具调用请求,把真实的执行结果回写到对话历史中
这三个模块会形成一个循环:
Think → LLMs → Execute → Think,直到任务完成或者达到最大循环次数。
Think 阶段:让模型决定下一步干什么
Agent Loop 的第一步,就是 Think 阶段。
protected boolean think() {
String thinkPrompt = """
现在你是一个智能的决策模块。
请根据当前对话上下文,决定下一步的动作。
如果需要调用工具来完成任务,请调用相应的工具。
""";
// ... 模型调用代码
}
这里有个非常关键的区别:Think 阶段的目标不是直接给用户答案,而是做决策。
它要判断,当前这个问题,是靠模型自己就能回答,还是必须调用外部工具。
在调用模型时,我们做了三件事:
-
把完整的对话上下文(来自 ChatMemory)都发给模型
-
向模型暴露当前 Agent 所有可用的工具
-
获取完整的 ChatResponse 对象,而不是只拿文本,因为里面包含了工具调用的指令
ToolCall 出现时,为什么不立刻写入 memory
在 Think 阶段结束后,我们做了一个预防 bug 的处理:
if (toolCalls.isEmpty()) {
chatMemory.add(sessionId, output);
}
也就是说:
-
如果模型没有生成任何工具调用,说明这就是最终回复,直接写入 memory
-
如果有工具调用,就先不写入,等工具执行完后再统一处理
这一点非常重要。如果提前把包含工具调用的消息写入 memory,万一后面工具执行失败,就会导致上下文信息不一致。
这个坑只有在真正写代码的时候才会遇到。
Execute 阶段:系统真正动手的地方
Execute 阶段,是 Agent 从会想走向会做的关键一步。
ToolExecutionResult toolExecutionResult =
toolCallingManager.executeToolCalls(prompt, this.lastChatResponse);
ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolExecutionResult
.conversationHistory()
.get(toolExecutionResult.conversationHistory().size() - 1);
这几行代码背后,其实是一整套完整的工具调用流程:
-
从 lastChatResponse 中解析出工具调用的指令
-
根据工具名称和参数,调用对应的外部工具
-
把工具的返回结果包装成标准化的 ToolResponseMessage
-
返回更新后的对话历史
Step:Think + Execute 的最小循环单元
我们把一次 Think 和一次可选的 Execute 封装成了一个最小的循环单元,叫做 step:
protected void step() {
if (think()) {
execute();
} else {
agentState = AgentState.FINISHED;
}
}
一次 step,就是 Agent 完整的一次思考和行动。
chat() 方法,不再是一次调用
在 V2 中,chat() 方法的语义已经完全变了:
for (int i = 0; i < MAX_STEPS && agentState != AgentState.FINISHED; i++) {
step();
}
它不再是简单的问一句答一句,而是变成了:
给 Agent 一个目标,让它自己通过循环跑完整个流程。
MAX_STEPS 的存在,也体现了一个重要的工程原则:
-
Agent 不能无限循环下去
-
模型的推理深度是有上限的
-
系统必须有兜底机制
到这里,我们已经真正拥有了一个 Agent Loop
到 JChatMindV2 为止,我们完成了 Agent 系统中最关键的一步:
-
模型不再直接回答问题
-
模型开始参与下一步该做什么的决策
-
系统和模型有了明确的职责分工
有了这个基础,后续无论是引入更复杂的规划、加入失败重试机制、接入 RAG 知识库,还是做执行轨迹的可视化,都只是在这个 Loop 上不断叠加功能而已。
测试工具调用能力
最后,我们写个测试函数,验证一下 Agent Loop 和工具调用是否正常工作:

上述内容也同步在我的飞书,欢迎访问
https://my.feishu.cn/wiki/QLauws6lWif1pnkhB8IcAvkhncc?from=from_copylink
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,你们的支持就是我坚持下去的动力!
更多推荐


所有评论(0)