Skip to content

Commit d1eba46

Browse files
anthhubclaude
andcommitted
feat: Chapter 4 — Agentic Loop (query cycle) — KEY MILESTONE
Demo code: - Add query.ts with full Agentic Loop: user input → API call → tool extraction → execution (read-only concurrent, write serial) → results appended → repeat until end_turn - Add utils/messages.ts with messagesToAPIParams(), createToolResultBlock(), extractToolUseBlocks() - Update main.ts with Agentic Loop demo (works with ANTHROPIC_API_KEY) This is the first time users can see AI autonomously calling tools and reasoning in a loop — the core of what makes Claude Code work. Documentation: - Add "Hands-on: Query Loop (Agentic Loop)" to Ch4 (zh-CN and en) - Explain loop pseudocode, tool orchestration strategy, message utils Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 942b132 commit d1eba46

5 files changed

Lines changed: 630 additions & 22 deletions

File tree

demo/main.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import type {
3737

3838
import { buildTool } from "./Tool.js";
3939
import { allTools, findToolByName, getToolsForAPI } from "./tools.js";
40+
import { query } from "./query.js";
4041

4142
// ─── 验证类型系统 ────────────────────────────────────────────────────────────
4243

@@ -178,38 +179,45 @@ console.log(` Bash: "${bashResult.content.trim()}"`);
178179
console.log();
179180
console.log("类型系统验证通过!");
180181

181-
// ─── 第 3 章:API 服务层演示 ──────────────────────────────────────────────
182+
// ─── 第 3 章:系统提示词预览 ──────────────────────────────────────────────
182183

183184
// 构建系统提示词
184185
const systemPrompt = buildSystemPrompt(allTools, process.cwd());
185186
console.log("系统提示词预览(前 200 字符):");
186187
console.log(` "${systemPrompt.substring(0, 200)}..."`);
187188
console.log();
188189

189-
// API 客户端演示(需要 ANTHROPIC_API_KEY 环境变量)
190+
// ─── 第 4 章:Agentic Loop 演示 ───────────────────────────────────────────
191+
190192
if (process.env.ANTHROPIC_API_KEY) {
191-
console.log("API 流式调用演示:");
192-
const client = createClient();
193-
const apiToolDefs = getToolsForAPI();
194-
195-
process.stdout.write(" AI: ");
196-
for await (const event of streamMessage(client, {
197-
model: DEFAULT_MODEL,
198-
maxTokens: 256,
199-
system: systemPrompt,
200-
messages: [{ role: "user", content: "Say hello in one sentence." }],
201-
tools: apiToolDefs,
202-
})) {
203-
if (event.type === "text") {
204-
process.stdout.write(event.text ?? "");
205-
} else if (event.type === "message_end") {
206-
console.log();
207-
console.log(` [tokens: ${event.usage?.inputTokens} in, ${event.usage?.outputTokens} out]`);
193+
console.log("Agentic Loop 演示:");
194+
console.log("─".repeat(40));
195+
196+
const result = await query(
197+
"请读取当前目录下的 package.json 文件,告诉我项目名称和版本号。",
198+
[],
199+
{
200+
model: DEFAULT_MODEL,
201+
maxTokens: 4096,
202+
onText: (text) => process.stdout.write(text),
203+
onToolUse: (name, input) => {
204+
console.log(`\n [工具调用] ${name}(${JSON.stringify(input)})`);
205+
},
206+
onToolResult: (name, result, isError) => {
207+
const icon = isError ? "❌" : "✅";
208+
console.log(` [工具结果] ${icon} ${name}: ${result.substring(0, 80)}...`);
209+
},
208210
}
209-
}
211+
);
212+
213+
console.log();
214+
console.log("─".repeat(40));
215+
console.log(`循环轮次: ${result.turns}`);
216+
console.log(`消息总数: ${result.messages.length}`);
217+
console.log(`Token 使用: ${result.inputTokens} 输入 / ${result.outputTokens} 输出`);
210218
} else {
211-
console.log("API 演示跳过(设置 ANTHROPIC_API_KEY 环境变量后可体验流式调用)");
219+
console.log("Agentic Loop 演示跳过(设置 ANTHROPIC_API_KEY 后可体验完整的 AI 工具调用循环)");
212220
}
213221

214222
console.log();
215-
console.log("下一步: 第 4 章 - 查询循环(Agentic Loop)");
223+
console.log("下一步: 第 5 章 - 完善工具实现(FileWrite、FileEdit、Glob)");

demo/query.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/**
2+
* query.ts - 查询循环(Agentic Loop)
3+
*
4+
* 对应真实 Claude Code: src/query.ts + src/QueryEngine.ts
5+
*
6+
* 这是 mini-claude 最核心的模块。它实现了 AI Agent 的核心循环:
7+
* 用户输入 → 调用 API → 收到工具调用 → 执行工具 → 结果发回 API → 重复
8+
*
9+
* 循环持续直到 AI 返回 end_turn(认为任务完成),
10+
* 或达到最大轮次限制。
11+
*/
12+
13+
import type { Message, ContentBlock, ToolUseBlock } from "./types/index.js";
14+
import { createClient, streamMessage } from "./services/api/claude.js";
15+
import { buildSystemPrompt } from "./context.js";
16+
import { allTools, findToolByName, getToolsForAPI } from "./tools.js";
17+
import {
18+
messagesToAPIParams,
19+
createUserMessage,
20+
createAssistantMessage,
21+
createToolResultBlock,
22+
extractToolUseBlocks,
23+
} from "./utils/messages.js";
24+
25+
/** 查询循环配置 */
26+
export interface QueryOptions {
27+
model: string;
28+
maxTokens: number;
29+
maxTurns?: number; // 最大循环轮次,防止无限循环
30+
apiKey?: string;
31+
cwd?: string;
32+
/** 文本输出回调——用于实时渲染 AI 的文字输出 */
33+
onText?: (text: string) => void;
34+
/** 工具调用回调——用于显示工具执行状态 */
35+
onToolUse?: (name: string, input: Record<string, unknown>) => void;
36+
/** 工具结果回调 */
37+
onToolResult?: (name: string, result: string, isError: boolean) => void;
38+
}
39+
40+
/** 查询结果 */
41+
export interface QueryResult {
42+
messages: Message[]; // 完整对话历史
43+
turns: number; // 实际循环轮次
44+
inputTokens: number; // 总输入 token
45+
outputTokens: number; // 总输出 token
46+
}
47+
48+
/**
49+
* 执行查询循环(Agentic Loop)
50+
*
51+
* 这是 mini-claude 的核心函数。完整流程:
52+
*
53+
* 1. 构建系统提示词
54+
* 2. 将消息历史转换为 API 格式
55+
* 3. 调用 API(流式)
56+
* 4. 收集 AI 回复(文本 + 工具调用)
57+
* 5. 如果有工具调用:
58+
* a. 执行所有工具(只读工具并发,写工具串行)
59+
* b. 将工具结果作为 user 消息追加
60+
* c. 回到步骤 2(继续循环)
61+
* 6. 如果没有工具调用(end_turn):返回结果
62+
*/
63+
export async function query(
64+
userInput: string,
65+
messages: Message[],
66+
options: QueryOptions
67+
): Promise<QueryResult> {
68+
const {
69+
model,
70+
maxTokens,
71+
maxTurns = 10,
72+
apiKey,
73+
cwd = process.cwd(),
74+
onText,
75+
onToolUse,
76+
onToolResult,
77+
} = options;
78+
79+
const client = createClient(apiKey);
80+
const systemPrompt = buildSystemPrompt(allTools, cwd);
81+
const apiTools = getToolsForAPI();
82+
83+
// 添加用户消息到历史
84+
const userMsg = createUserMessage(userInput);
85+
messages.push(userMsg);
86+
87+
let totalInputTokens = 0;
88+
let totalOutputTokens = 0;
89+
let turn = 0;
90+
91+
// ─── Agentic Loop ───────────────────────────────────────────────────
92+
while (turn < maxTurns) {
93+
turn++;
94+
95+
// 1. 将消息历史转换为 API 格式
96+
const apiMessages = messagesToAPIParams(messages);
97+
98+
// 2. 调用 API(流式)
99+
const contentBlocks: ContentBlock[] = [];
100+
let currentText = "";
101+
const toolUseBuffers = new Map<string, { id: string; name: string; input: string }>();
102+
let stopReason: string | undefined;
103+
104+
for await (const event of streamMessage(client, {
105+
model,
106+
maxTokens,
107+
system: systemPrompt,
108+
messages: apiMessages,
109+
tools: apiTools,
110+
})) {
111+
switch (event.type) {
112+
case "text":
113+
currentText += event.text ?? "";
114+
onText?.(event.text ?? "");
115+
break;
116+
117+
case "tool_use_start":
118+
toolUseBuffers.set(event.toolUseId!, {
119+
id: event.toolUseId!,
120+
name: event.toolName!,
121+
input: "",
122+
});
123+
break;
124+
125+
case "tool_use_delta":
126+
// 累积工具输入 JSON
127+
for (const buf of toolUseBuffers.values()) {
128+
buf.input += event.inputDelta ?? "";
129+
}
130+
break;
131+
132+
case "tool_use_end": {
133+
const buf = toolUseBuffers.get(event.toolUseId!);
134+
if (buf) {
135+
// 解析完整的工具输入 JSON
136+
let input: Record<string, unknown> = {};
137+
try {
138+
input = JSON.parse(buf.input || "{}");
139+
} catch {
140+
// JSON 解析失败时使用空对象
141+
}
142+
contentBlocks.push({
143+
type: "tool_use",
144+
id: buf.id,
145+
name: buf.name,
146+
input,
147+
});
148+
toolUseBuffers.delete(event.toolUseId!);
149+
onToolUse?.(buf.name, input);
150+
}
151+
break;
152+
}
153+
154+
case "message_end":
155+
stopReason = event.stopReason;
156+
totalInputTokens += event.usage?.inputTokens ?? 0;
157+
totalOutputTokens += event.usage?.outputTokens ?? 0;
158+
break;
159+
}
160+
}
161+
162+
// 3. 将文本块添加到内容
163+
if (currentText) {
164+
contentBlocks.unshift({ type: "text", text: currentText });
165+
}
166+
167+
// 4. 创建助手消息并追加到历史
168+
const assistantMsg = createAssistantMessage(
169+
contentBlocks,
170+
model,
171+
(stopReason as "end_turn" | "tool_use" | "max_tokens") ?? null
172+
);
173+
messages.push(assistantMsg);
174+
175+
// 5. 提取工具调用
176+
const toolUses = extractToolUseBlocks(assistantMsg);
177+
178+
// 6. 如果没有工具调用,循环结束
179+
if (toolUses.length === 0) {
180+
break;
181+
}
182+
183+
// 7. 执行工具并收集结果
184+
const toolResultBlocks: ContentBlock[] = [];
185+
186+
// 分离只读和读写工具
187+
const readOnlyTools: ToolUseBlock[] = [];
188+
const writeTools: ToolUseBlock[] = [];
189+
190+
for (const tu of toolUses) {
191+
const tool = findToolByName(tu.name);
192+
if (tool?.isReadOnly) {
193+
readOnlyTools.push(tu);
194+
} else {
195+
writeTools.push(tu);
196+
}
197+
}
198+
199+
// 并发执行只读工具
200+
if (readOnlyTools.length > 0) {
201+
const results = await Promise.all(
202+
readOnlyTools.map(async (tu) => {
203+
const tool = findToolByName(tu.name);
204+
if (!tool) {
205+
return createToolResultBlock(tu.id, `Error: Unknown tool '${tu.name}'`, true);
206+
}
207+
const result = await tool.call(tu.input);
208+
onToolResult?.(tu.name, result.content.substring(0, 100), !!result.isError);
209+
return createToolResultBlock(tu.id, result.content, result.isError);
210+
})
211+
);
212+
toolResultBlocks.push(...results);
213+
}
214+
215+
// 串行执行读写工具
216+
for (const tu of writeTools) {
217+
const tool = findToolByName(tu.name);
218+
if (!tool) {
219+
toolResultBlocks.push(
220+
createToolResultBlock(tu.id, `Error: Unknown tool '${tu.name}'`, true)
221+
);
222+
continue;
223+
}
224+
const result = await tool.call(tu.input);
225+
onToolResult?.(tu.name, result.content.substring(0, 100), !!result.isError);
226+
toolResultBlocks.push(
227+
createToolResultBlock(tu.id, result.content, result.isError)
228+
);
229+
}
230+
231+
// 8. 将工具结果作为 user 消息追加到历史
232+
const toolResultMsg = createUserMessage(toolResultBlocks);
233+
messages.push(toolResultMsg);
234+
235+
// 继续循环(回到步骤 1)
236+
}
237+
238+
return {
239+
messages,
240+
turns: turn,
241+
inputTokens: totalInputTokens,
242+
outputTokens: totalOutputTokens,
243+
};
244+
}

0 commit comments

Comments
 (0)