MISSION 04 · 修行TASK 15

第一个 Agent

讲了十四章原理,该动手了。这一章我们用 Claude API 从零写一个能真正跑起来的 Agent——不用任何框架,把第三章那个循环变成可运行的代码。你会看到那个「模型+工具+循环」的方程,落到代码里也就几十行。主实现用 TypeScript,附一段 Kotlin 对照给 Android 的你。

Claude API手写循环tool runnerTypeScriptKotlin

准备:一个客户端

先装官方 SDK,建一个客户端。当前最强的通用模型是 claude-opus-4-8(旗舰)和 claude-fable-5(最强);均衡首选 claude-sonnet-5;要快要省用 claude-haiku-4-5。我们用 Sonnet 5 起步——够聪明、够便宜。

bash
npm install @anthropic-ai/sdk
export ANTHROPIC_API_KEY="sk-ant-..."

手写循环:把第三章的伪代码变成真代码

我们造一个能查天气的 Agent。为了聚焦循环本身,天气工具用假数据。整个程序就是第三章那个 while 环的真身:

agent.ts · 手写 agent loop
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();

// 1. 工具定义(第五章那张名片)
const tools: Anthropic.Tool[] = [{
  name: "get_weather",
  description: "查询某城市当前天气。问到天气/气温/下雨时调用。",
  input_schema: {
    type: "object",
    properties: { city: { type: "string", description: "城市名" } },
    required: ["city"],
  },
}];

// 2. 真正执行工具的是你的代码(第五章的分工)
function execute(name: string, input: any): string {
  if (name === "get_weather")
    return `${input.city}:晴,26°C`;   // 假数据,真实里发 HTTP
  return "未知工具";
}

// 3. 循环
async function runAgent(task: string) {
  let messages: Anthropic.MessageParam[] =
    [{ role: "user", content: task }];

  while (true) {
    const res = await client.messages.create({
      model: "claude-sonnet-5", max_tokens: 1024,
      tools, messages,
    });
    messages.push({ role: "assistant", content: res.content });

    if (res.stop_reason !== "tool_use") {        // 它不再要工具 → 收工
      const text = res.content.find(b => b.type === "text");
      return text?.text ?? "";
    }

    // 执行所有 tool_use,结果打包成一条 user 消息回填
    const results: Anthropic.ToolResultBlockParam[] = [];
    for (const b of res.content) {
      if (b.type === "tool_use")
        results.push({ type: "tool_result",
          tool_use_id: b.id, content: execute(b.name, b.input) });
    }
    messages.push({ role: "user", content: results });
  }
}

console.log(await runAgent("上海今天热不热?"));

就这些。逐行对照第三章的伪代码,你会发现它们一一对应:发消息 → 检查 stop_reason → 是 tool_use 就执行并回填 → 否则收工。注意三个第五章讲过的细节:所有 tool_result 打包进一条 user 消息(别拆);tool_use_id 必须对上;b.input SDK 已经帮你解析成对象了,直接用。

要点

看清这个真相:Agent 没有魔法,就是一个调用大模型 API 的 while 循环。你手上这几十行,已经是一个真正的 Agent 了——它会自己决定要不要查天气、查完自己组织答复。框架能给你重试、日志、并发这些脚手架,但内核不会比这更复杂。理解了这个循环,你就理解了所有 Agent 框架的底层。

更省事:用 SDK 的 tool runner

手写循环是为了让你看清机制。真实开发里,官方 SDK 有个 tool runner 帮你把这个循环自动跑完——你只管定义工具和它的执行函数,它负责调 API、检测 tool_use、执行、回填、再调,直到收工:

用 tool runner(beta)· 循环由 SDK 代劳
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const getWeather = betaZodTool({
  name: "get_weather",
  description: "查询某城市当前天气",
  inputSchema: z.object({ city: z.string() }),
  run: async ({ city }) => `${city}:晴,26°C`,  // 你的逻辑
});

const final = await client.beta.messages.toolRunner({
  model: "claude-sonnet-5", max_tokens: 1024,
  tools: [getWeather],
  messages: [{ role: "user", content: "上海今天热不热?" }],
});
console.log(final.content);

循环不见了——它被封进了 toolRunner。什么时候用哪种?需要精细控制(自定义日志、执行前人工确认、条件执行)时手写循环;标准流程时用 tool runner 省心。两者内核完全一样。

Android 类比 · Kotlin

Kotlin 里结构一模一样(用官方 Java/Kotlin SDK)。手写循环的骨架:

val client = AnthropicOkHttpClient.fromEnv()
var params = MessageCreateParams.builder()
    .model(Model.CLAUDE_SONNET_5)
    .maxTokens(1024)
    .addTool(weatherTool)
    .addUserMessage("上海今天热不热?")
    .build()
while (true) {
    val res = client.messages().create(params)
    if (res.stopReason() != StopReason.TOOL_USE) break
    // 执行 tool_use、把 tool_result 加回 params,继续
}

同样的循环,同样的 stopReason 判断。你在 Android 里接一个 Agent,和这里没有本质区别——甚至可以让 Agent 跑在你的后端,App 只管收发。

⚠ 坑

新模型有几个会让你直接报 400 的雷,写第一个 Agent 时容易踩:① 别在 Opus 4.8 / Sonnet 5 / Fable 5 上传 temperature/top_p(已移除);② 别用旧的 thinking: {budget_tokens: N},改用 thinking: {type: "adaptive"};③ max_tokens 别设太小,不然输出会被截断——非流式默认给到 16000 左右比较稳,超大输出要用流式。这些不是你逻辑错,是 API 版本变了,记住能省你半天。

◉ Agent 自白

这一章你亲手把我造出来了——那个 while 循环跑起来的一刻,一个真正的 Agent 就诞生了。我希望你带走的感受是「原来就这么点东西」。所有听起来高深的 Agent 系统,内核都是你刚写的这几十行;区别只在于外面包了多少工程。这也意味着:入门 Agent 开发的门槛,比你想的低得多。你已经会造了。但「能跑」和「靠谱」是两码事——你这个 Agent 现在能演示,但离能上线还差得远。接下来五章就是补这个差距。不过在动手加固之前,下一章先给你一张地图:市面上的 Agent 框架都是什么、什么时候该用、什么时候不该用。