Files
claude-code/docs/zh/s01-the-agent-loop.md
T
2026-03-25 00:15:00 +08:00

319 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# s01: The Agent Loop (智能体循环)
`[ s01 ] s02 > s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12`
> *"One loop & Bash is all you need"* -- 一个工具 + 一个循环 = 一个智能体。
>
> **Harness 层**: 循环 -- 模型与真实世界的第一道连接。
## 问题
语言模型能推理代码, 但碰不到真实世界 -- 不能读文件、跑测试、看报错。没有循环, 每次工具调用你都得手动把结果粘回去。你自己就是那个循环。
## 解决方案
```
+--------+ +-------+ +---------+
| User | ---> | LLM | ---> | Tool |
| prompt | | | | execute |
+--------+ +---+---+ +----+----+
^ |
| tool_result |
+----------------+
(ChatClient.call() 自动循环直到无工具调用)
```
一个 `call()` 调用控制整个流程。Spring AI 自动循环, 直到模型不再调用工具。
## 工作原理
### 1. 构建 ChatClient:注入模型 + 注册工具
通过 Spring Boot 自动配置注入 `ChatModel`,用 `ChatClient.builder()` 构建客户端,设置系统提示和工具。
```java
// TIP: Python 版在模块级创建 client = Anthropic() 和 MODEL。
// Spring AI 通过自动配置注入 ChatModel,再用 builder 构建 ChatClient。
public S01AgentLoop(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a coding agent at " + System.getProperty("user.dir")
+ ". Use bash to solve tasks. Act, don't explain.")
.defaultTools(new BashTool()) // @Tool 注解的工具对象
.build();
}
```
### 2. `@Tool` 注解:声明式工具注册
Spring AI 通过 `@Tool` 注解自动发现和注册工具。框架在启动时扫描 `defaultTools()` 传入的对象,提取所有 `@Tool` 方法的签名和描述,生成 LLM 需要的 tool schema(名称、参数、描述),然后在每次 `call()` 请求中自动携带。
```java
// BashTool —— 对应 Python 版的 run_bash() 函数
public class BashTool {
@Tool(description = "Run a shell command and return stdout + stderr")
public String bash(@ToolParam(description = "The shell command to execute")
String command) {
// 危险命令检查 + ProcessBuilder 执行 + 超时控制 + 输出截断
// ...
}
}
```
> 对比 Python 版的手动注册方式:
> - Python: `TOOLS = [{"name": "bash", "input_schema": {...}}]` + `TOOL_HANDLERS = {"bash": run_bash}`
> - Java: 只需 `@Tool` + `@ToolParam` 注解,框架自动完成 schema 生成和方法分派
### 3. Spring AI 内部自动循环:`call()` 的底层实现
**这是理解 Java 版与 Python 版最关键的区别。** Python 版本需要手写 while 循环来驱动工具调用:
```python
# Python 版 —— 手动循环
def agent_loop(messages):
while True:
response = client.messages.create(model=MODEL, messages=messages, tools=TOOLS)
# 收集 assistant 消息
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
return response # 模型不再调用工具,退出循环
# 执行工具并回传结果
for block in response.content:
if block.type == "tool_use":
result = TOOL_HANDLERS[block.name](block.input)
messages.append({"role": "user", "content": [{"type": "tool_result", ...}]})
```
Spring AI 的 `ChatClient.call()` **内部封装了完全等价的逻辑**
```
call() 内部流程:
┌─────────────────────────────────────────────────────┐
│ 1. 组装请求: system prompt + user message + tools │
│ 2. 发送给 LLM │
│ 3. 解析响应 │
│ ├── 有 tool_use? ──→ 是: │
│ │ a. 提取工具名和参数 │
│ │ b. 通过反射调用对应的 @Tool 方法 │
│ │ c. 将 tool_result 追加到消息列表 │
│ │ d. 回到步骤 2(自动循环) │
│ └── 否 ──→ 返回最终文本 │
└─────────────────────────────────────────────────────┘
```
关键点:
- **工具检测**: Spring AI 检查响应中是否有 `tool_use` 类型的 content block(对应 Python 的 `stop_reason == "tool_use"`
- **反射分派**: 框架通过 Java 反射机制,根据 LLM 返回的工具名称找到对应的 `@Tool` 方法并调用(对应 Python 的 `TOOL_HANDLERS[block.name]`
- **结果回传**: 工具执行结果自动包装为 `tool_result` 消息追加到对话(对应 Python 手动构造 `tool_result` content block
- **循环终止**: 当模型返回纯文本(无工具调用)时,`call()` 返回最终结果
因此,Python 版约 15 行的 while 循环,在 Java 版中浓缩为一行 `.call()`
### 4. `AgentRunner.interactive()`REPL 交互循环
`AgentRunner` 是所有课程共用的交互式 REPLRead-Eval-Print Loop)工具类,对应 Python 版 `if __name__ == "__main__"` 中的 `input()` 循环。
```java
public class AgentRunner {
/**
* 启动交互式 REPL 循环。
* @param prefix 提示符前缀(如 "s01"
* @param handler 处理用户输入并返回 Agent 响应的函数
*/
public static void interactive(String prefix, Function<String, String> handler) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入 'q' 或 'exit' 退出");
while (true) {
System.out.print("\033[36m" + prefix + " >> \033[0m"); // 彩色提示符
String input;
try {
if (!scanner.hasNextLine()) break;
input = scanner.nextLine().trim();
} catch (Exception e) {
break;
}
if (input.isEmpty() || "exit".equalsIgnoreCase(input) || "q".equalsIgnoreCase(input)) {
break;
}
try {
String response = handler.apply(input); // 调用 Agent 处理
if (response != null && !response.isBlank()) {
System.out.println(response);
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
System.out.println();
}
System.out.println("Bye!");
}
}
```
工作流程:`Scanner` 读取输入 → `handler.apply()` 发给 Agent → 打印响应 → 循环。`handler` 是一个函数式接口,每个课程传入自己的 Agent 调用逻辑。
### 5. 组装为完整的 Agent 类
```java
@SpringBootApplication(scanBasePackages = "io.mybatis.learn.core")
public class S01AgentLoop implements CommandLineRunner {
private final ChatClient chatClient;
public S01AgentLoop(ChatModel chatModel) {
this.chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a coding agent at ...")
.defaultTools(new BashTool())
.build();
}
@Override
public void run(String... args) {
AgentRunner.interactive("s01", userMessage ->
chatClient.prompt()
.user(userMessage)
.call() // ← 这一个调用 = Python 的整个 while 循环
.content()
);
}
}
```
> **TIPS — Python → Java 关键适配点:**
> - Python 的 `while True` + `stop_reason` 手动循环 → Spring AI `ChatClient.call()` 内置自动循环
> - Python 的 `TOOLS` 数组 + `TOOL_HANDLERS` 字典 → `@Tool` 注解 + `defaultTools()` 自动注册与反射分派
> - Python 的 `client = Anthropic()` → Spring Boot 自动配置注入 `ChatModel`
> - Python 的 `input()` 交互 → `AgentRunner.interactive()` 封装 Scanner REPL + 函数式接口
不到 40 行核心代码, 这就是整个智能体。后面 11 个章节都在这个循环上叠加机制 -- 循环本身始终不变。
## 变更内容
| 组件 | 之前 | 之后 |
|---------------|------------|--------------------------------------------------|
| Agent loop | (无) | `ChatClient.call()` 内置工具循环 |
| Tools | (无) | `BashTool` (单一 `@Tool` 工具) |
| Messages | (无) | Spring AI 内部管理消息列表 |
| Control flow | (无) | 框架自动判断: 无工具调用时返回最终文本 |
```java
// 核心代码 —— 构建 + 调用
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a coding agent ...")
.defaultTools(new BashTool())
.build();
AgentRunner.interactive("s01", userMessage ->
chatClient.prompt().user(userMessage).call().content()
);
```
## 试一试
```sh
cd learn-claude-code
mvn exec:java -Dexec.mainClass=io.mybatis.learn.s01.S01AgentLoop
```
> 运行前需设置环境变量: `AI_API_KEY`, `AI_BASE_URL`, `AI_MODEL`
>
> **当前默认使用 OpenAI 协议**(兼容所有 OpenAI API 格式的服务,包括 OpenAI 官方、Azure OpenAI、各类第三方大模型服务的 OpenAI 兼容接口等)。
> 如需使用 Anthropic 协议(Claude 系列模型原生接口),请展开下方「切换 AI 协议」。
<details>
<summary><strong>切换 AI 协议(OpenAI ↔ Anthropic</strong></summary>
本项目通过 Spring AI 的 **Starter 依赖 + 配置文件** 来切换底层协议,Java 业务代码(`ChatModel``ChatClient`**无需任何修改**。
#### 方式一:OpenAI 协议(默认)
`pom.xml` 依赖:
```xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
```
`application.yml` 配置:
```yaml
spring:
ai:
openai:
api-key: ${AI_API_KEY:sk-xxx}
base-url: ${AI_BASE_URL:https://api.openai.com}
chat:
options:
model: ${AI_MODEL:gpt-4o}
```
环境变量示例(以 OpenAI 官方为例):
```sh
export AI_API_KEY=sk-proj-xxxxxxxx
export AI_BASE_URL=https://api.openai.com # 可替换为任何 OpenAI 兼容接口
export AI_MODEL=gpt-4o
```
> **TIP**: 许多第三方大模型服务(如 DeepSeek、Mistral、通义千问等)提供了 OpenAI 兼容接口,只需修改 `AI_BASE_URL` 和 `AI_MODEL` 即可接入,无需切换协议。
#### 方式二:Anthropic 协议(Claude 原生接口)
**第 1 步**:修改 `pom.xml`,将 OpenAI starter 替换为 Anthropic starter
```xml
<!-- 注释或删除 OpenAI starter -->
<!-- <dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency> -->
<!-- 添加 Anthropic starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
```
**第 2 步**:修改 `application.yml`,将 `spring.ai.openai` 替换为 `spring.ai.anthropic`
```yaml
spring:
ai:
anthropic:
api-key: ${AI_API_KEY}
base-url: ${AI_BASE_URL:https://api.anthropic.com}
chat:
options:
model: ${AI_MODEL:claude-sonnet-4-20250514}
```
**第 3 步**:设置环境变量:
```sh
export AI_API_KEY=sk-ant-xxxxxxxx
export AI_BASE_URL=https://api.anthropic.com
export AI_MODEL=claude-sonnet-4-20250514
```
#### 切换原理
Spring AI 的设计使得 `ChatModel` 是一个统一的抽象接口。不同的 Starter 提供不同的实现:
| Starter 依赖 | 自动注入的 ChatModel 实现 | 配置前缀 |
|---|---|---|
| `spring-ai-starter-model-openai` | `OpenAiChatModel` | `spring.ai.openai.*` |
| `spring-ai-starter-model-anthropic` | `AnthropicChatModel` | `spring.ai.anthropic.*` |
业务代码始终面向 `ChatModel` 接口编程,切换协议只需替换依赖和配置,无需改动任何 Java 代码。
</details>
试试这些 prompt(英文 prompt 对 LLM 效果更好, 也可以用中文):
1. `Create a file called Hello.java that prints "Hello, World!"`
2. `List all Java files in this directory`
3. `What is the current git branch?`
4. `Create a directory called test_output and write 3 files in it`