从通义灵码、CodeBuddy等编程助手,到Cursor、Trae等AI编程IDE,再到Claude Code、Gemini Cli、Qwen Code等命令行工具,AI编程Agent这个赛道一直很热闹,那么这些AI Agent是如何实现的呢?这篇文章我来介绍一下我用Java实现出来的一个简单版的AI编程Agent,你可以理解为是一个简单版本的Qwen Code。
为什么用Java?其实我也用Python写了一版,也没别的原因,就想试试Java到底行不行...
ReAct
首先介绍一下什么是ReAct,ReAct拆开就是Reason+Action,也就是思考+行动。为什么需要ReAct? 第一个原因是LLM只能输出文字,没有行动能力,比如不能创建文件、写文件、读文件,因此我们需要给LLM提供工具,让它有行动能力。 第二个原因是LLM不管是解决简单问题还是复杂问题,都是一次性给出答案,但实际情况是,对于复杂问题往往需要分阶段思考、分阶段行动会更好。
ReAct核心就是一个循环:
- 思考:根据问题,给出一个解决问题的计划,然后执行计划中的第一步
- 行动:针对当前步骤,分析出要执行哪些工具,工具入参是什么
- 观察:观察工具执行结果,将结果信息返回给大模型,模型继续思考和行动。
这个"思考-行动-观察"的循环不断重复,直到任务完成,有点抽象,我们来看具体代码。
第一步:配置
首先看看配置,配置DASHSCOPE平台的API_KEY、BASE_URL和我们要使用的LLM。
arduino
/**
* 需要完整代码可以联系我,微信号:it_zhouyu
*/
public class ModelConfig {
public static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
public static final String BASE_URL = "<https://dashscope.aliyuncs.com/compatible-mode/v1>";
public static final String LLM_NAME = "qwen-plus";
}
第二步:定义工具
然后定义Agent需要的工具,在AgentTools中可以定义多个工具,@Tool和@ToolParam都是我自己定义的,并没有用Spring AI。
typescript
/**
* 需要完整代码可以联系我,微信号:it_zhouyu
*/
public class AgentTools {
@Tool(description = "将指定内容写入本地文件。")
public String writeFile(@ToolParam(description = "包含 'file_path' 和 'content' 的 JSON 字符串。") String jsonInput) {
try {
JsonNode rootNode = objectMapper.readTree(jsonInput);
String filePath = rootNode.get("file_path").asText();
String content = rootNode.get("content").asText();
try (FileWriter writer = new FileWriter(filePath)) {
writer.write(content);
return String.format("成功将内容写入文件 '%s'。", filePath);
} catch (IOException e) {...}
} catch (Exception e) {...}
}
}
这里的关键是注解@Tool
和@ToolParam
:
- @Tool(description = "..."): 用来描述工具的作用,当有多个工具时,LLM能根据每个工具的描述来选择该用哪个工具来完成当前任务。
- @ToolParam(description = "..."): 用来描述参数的用途,当调用工具时,LLM需要根据参数描述来构造输入数据。
第三步:构建核心ReAct提示词
ReAct中最核心的就是提示词,因为我们需要LLM能够按照我们需要的格式返回,然后我们再通过Java代码来按格式进行解析,比如解析出该调用哪个工具,也就是该调用AgentTools中的哪个方法,传什么参数,以下是我的提示词:
arduino
/**
* 需要完整代码可以联系我,微信号:it_zhouyu
*/
private static final String REACT_PROMPT_TEMPLATE = """
你是一个强大的 AI 助手,通过思考和使用工具来解决用户的问题。
你的任务是尽你所能回答以下问题。你可以使用以下工具:
{tools}
请严格遵循以下规则和格式:
1. 你的行动必须基于一个清晰的"Thought"过程。
2. 你必须按顺序使用 "Thought:", "Action:", "Action Input:"。
3. 在每次回复中,你只能生成 **一个** Thought/Action/Action Input 组合。
4. **绝对不要** 自己编造 "Observation:"。系统会在你执行动作后,将真实的结果作为 Observation 提供给你。
5. 当你拥有足够的信息来直接回答用户的问题时,请使用 "Final Answer:" 来输出最终答案。
6. 在每次回复中,"Thought:", "Action:", "Action Input:"和"Final Answer:"不能同时出现。
下面是你的思考和行动格式:
Thought: 我需要做什么来解决问题?下一步是什么?
Action: 我应该使用哪个工具?必须是 [{tool_names}] 中的一个。
Action Input: 我应该给这个工具提供什么输入?这必须是一个 JSON 对象。
--- 开始 ---
Question: {input}
{agent_scratchpad}
""";
其中:
- {tools} : 占位符,用来填充所有的工具信息,也就是@Tool、@ToolParam中的内容
- {tool_names} : 占位符,用来填充所有工具的名字
- {input} : 占位符,用户的问题
- {agent_scratchpad} : 占位符,用来记录之前所有步骤的"思考-行动-观察"历史,帮助LLM做出符合上下文的决策。
这里重点讲一下第4点和第6点:
- 第4点,是因为我在测试时,发现模型有时候会自己模拟工具的执行,编造一个假的工具执行结果,导致没有真正的执行工具。
- 第6点,是因为我在测试时,发现模型有时候会既给出行动指令又给出最终答案,这其实是矛盾的,所以得要求它不要既给出行动指令又给出最终答案。
第四步:实现ReAct核心循环
这块代码比较多,贴在文章里也不方便阅读,这里就只给出实现思路,大家想要完整代码可以加我微信领取,微信在文末。
- 外层是一个循环,设置一个maxIterations,因为不能让ReAct无限循环
- 填充提示词,解析工具信息,以及用户的问题需求,填充到REACT_PROMPT_TEMPLATE提示词模版中,得到完整的提示词
- 调用LLM,得到LLM原始输出
- 先判断原始输出中是否有Final Answer,如果有,则直接返回最终答案,结束循环
- 再判断原始输出中,是否有Thought、Action和Action Input,如果有,则提取出要执行的方法名和入参对象
- 执行工具,并得到工具结果Observation
- 然后将当前步骤的Thought、Action、Action Input、Observation等信息,保存到agentScratchpad中,用于下一次循环的输入
- 进行下一次循环,直到达到maxIterations次数或者LLM返回了Final Answer
第五步:组装Agent
最后将Agent组装起来就可以用了:
java
/**
* 需要完整代码可以联系我,微信号:it_zhouyu
*/
public class Main {
public static void main(String[] args) {
// 1. 初始化 API 客户端
OpenAIClient apiClient = ...;
// 2. 创建 AgentTools 实例
AgentTools agentTools = new AgentTools();
// 3. 创建 ReActAgent 实例
ReActAgent agent = new ReActAgent(apiClient, agentTools);
// 4. 定义问题并运行 Agent
String question = "请帮我用HTML、CSS、JS创建一个简单的贪吃蛇游戏,分成三个文件,分别是snake.html、snake.css、snake.js";
String finalResult = agent.run(question, 10);
// 5. 打印最终结果
System.out.println("AGENT 执行结束,最终结果为:");
System.out.println(finalResult);
}
}
总结
以上就是我手写的简单版的Qwen Code Agent的核心思路,虽然还不能投入生产使用,但对于大家理解AI Agent核心原理、什么是ReAct、如何开发一个Agent应该都有帮助,希望能得到大家的点赞、分享、关注,最后给出我的联系方式,需要完整代码的可以加我微信号:it_zhouyu。