不用框架,100 行 TypeScript 从零实现一个真正的 AI Agent(附完整可运行代码)

不用框架,100 行 TypeScript 从零实现一个真正的 AI Agent(附完整可运行代码)

不用 LangChain,不用 CrewAI,纯 TypeScript + Claude API,100 行代码实现一个能自主调用工具、完成复杂任务的 AI Agent。先理解原理,再决定要不要用框架。

Agent 和聊天机器人到底有什么区别?

很多人把"调用大模型 API"等同于"做了一个 Agent"。不是这样的。

聊天机器人是一问一答:你说一句,它回一句,然后等你下一句。

Agent 是自主循环:你给它一个目标,它自己决定要做什么、用什么工具、怎么处理结果,直到任务完成才停下来。

核心区别就一个词:自主决策循环(Agentic Loop)

text 复制代码
聊天机器人:用户提问 → 模型回答 → 结束

Agent:用户给目标 → 模型思考 → 调用工具 → 拿到结果 → 继续思考 → 再调工具 → ... → 任务完成

今天我们就来实现这个循环。不用 LangChain,不用 CrewAI,不用任何框架------纯 TypeScript + Claude API,100 行代码搞定。

为什么不用框架?因为框架封装了太多细节。先理解原理,再用框架才知道它帮你做了什么、以及什么时候它在帮倒忙。

核心原理:Tool Use + ReAct 循环

Claude API 原生支持 Tool Use(工具调用)。你在请求时告诉 Claude "你有哪些工具可以用",Claude 会在需要的时候主动选择调用。

整个 Agentic Loop 只有三步:

text 复制代码
1. 发送消息给 Claude(带上工具定义)
2. 检查 Claude 的响应:
   - stop_reason === "end_turn"  → 任务完成,退出循环
   - stop_reason === "tool_use" → Claude 想用工具,执行后把结果喂回去
3. 回到第 1 步

就这么简单。所有 Agent 框架的底层都是这个循环,区别只在于上面包了多少糖。

实战:构建一个技术周报 Agent

我们来做一个实际有用的东西:一个能自动调研 GitHub 热门项目并生成中文技术周报的 Agent。

你只需要对它说:"帮我分析最近 GitHub 上最火的 AI 项目,生成一份技术周报",它会自己:

  1. 搜索 GitHub 热门仓库
  2. 逐个查看感兴趣的项目详情
  3. 分析整理,生成结构化的中文周报
Step 1:项目搭建
bash 复制代码
mkdir ai-agent-demo && cd ai-agent-demo
npm init -y
npm install @anthropic-ai/sdk
npm install -D typescript @types/node

tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}
Step 2:定义工具

Agent 的能力边界取决于它有什么工具。我们给它两个:

typescript 复制代码
// src/agent.ts
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic(); // 自动读取 ANTHROPIC_API_KEY 环境变量

// 定义工具列表
const tools: Anthropic.Tool[] = [
  {
    name: "search_github",
    description: "搜索 GitHub 仓库,返回按 Stars 排序的项目列表",
    input_schema: {
      type: "object" as const,
      properties: {
        query: {
          type: "string",
          description: "搜索关键词,如 'AI agent' 或 'LLM framework'",
        },
        language: {
          type: "string",
          description: "编程语言筛选,如 typescript、python",
        },
      },
      required: ["query"],
    },
  },
  {
    name: "get_repo_readme",
    description: "获取指定 GitHub 仓库的 README 内容摘要",
    input_schema: {
      type: "object" as const,
      properties: {
        owner: { type: "string", description: "仓库所有者" },
        repo: { type: "string", description: "仓库名称" },
      },
      required: ["owner", "repo"],
    },
  },
];

注意 description 写得要具体------Claude 靠描述来决定什么时候调用哪个工具。描述模糊,Agent 就会乱调或者不调。这是实际开发中最影响效果的地方。

Step 3:实现工具执行
typescript 复制代码
// 工具执行器:根据工具名分发到具体实现
async function executeTool(
  name: string,
  input: Record<string, string>
): Promise<string> {
  switch (name) {
    case "search_github": {
      const params = new URLSearchParams({
        q: input.language
          ? `${input.query} language:${input.language}`
          : input.query,
        sort: "stars",
        order: "desc",
        per_page: "5",
      });
      const res = await fetch(
        `https://api.github.com/search/repositories?${params}`,
        { headers: { "User-Agent": "ai-agent-demo" } }
      );
      if (!res.ok) return `GitHub API 错误: ${res.status}`;
      const data = (await res.json()) as {
        items: Array<{
          full_name: string;
          description: string | null;
          stargazers_count: number;
          language: string | null;
        }>;
      };
      return data.items
        .map(
          (r) =>
            `${r.full_name} (⭐${r.stargazers_count}) - ${r.description || "无描述"} [${r.language || "未知"}]`
        )
        .join("\n");
    }

    case "get_repo_readme": {
      const res = await fetch(
        `https://api.github.com/repos/${input.owner}/${input.repo}/readme`,
        {
          headers: {
            "User-Agent": "ai-agent-demo",
            Accept: "application/vnd.github.raw+json",
          },
        }
      );
      if (!res.ok) return `无法获取 README: ${res.status}`;
      const text = await res.text();
      // 截取前 1500 字符,避免 Token 浪费
      return text.slice(0, 1500);
    }

    default:
      return `未知工具: ${name}`;
  }
}
Step 4:实现 Agentic Loop(核心)

这是整个 Agent 的心脏------20 行代码

typescript 复制代码
async function runAgent(task: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: task },
  ];

  let turns = 0;
  const MAX_TURNS = 15; // 防止无限循环

  while (turns < MAX_TURNS) {
    turns++;
    console.log(`--- 第 ${turns} 轮 ---`);

    const response = await client.messages.create({
      model: "claude-sonnet-4-5-20250929",
      max_tokens: 4096,
      system:
        "你是一个技术调研助手。请用中文回答。分析项目时关注:项目定位、核心功能、技术亮点、适用场景。",
      tools,
      messages,
    });

    // 把 Claude 的回复加入对话历史
    messages.push({ role: "assistant", content: response.content });

    // 关键判断:Claude 是想用工具,还是已经完成了?
    if (response.stop_reason === "end_turn") {
      // 任务完成,提取最终文本
      return response.content
        .filter((b): b is Anthropic.TextBlock => b.type === "text")
        .map((b) => b.text)
        .join("\n");
    }

    if (response.stop_reason === "tool_use") {
      // Claude 想调用工具,逐个执行
      const toolBlocks = response.content.filter(
        (b): b is Anthropic.ToolUseBlock => b.type === "tool_use"
      );

      const results: Anthropic.ToolResultBlockParam[] = [];

      for (const tool of toolBlocks) {
        console.log(`  调用工具: ${tool.name}`, tool.input);
        try {
          const result = await executeTool(
            tool.name,
            tool.input as Record<string, string>
          );
          results.push({
            type: "tool_result",
            tool_use_id: tool.id,
            content: result,
          });
        } catch (err) {
          // 工具报错也要告诉 Claude,让它自己决定怎么处理
          results.push({
            type: "tool_result",
            tool_use_id: tool.id,
            content: `执行出错: ${(err as Error).message}`,
            is_error: true,
          });
        }
      }

      // 把工具结果喂回去,继续循环
      messages.push({ role: "user", content: results });
    }
  }

  return "达到最大轮次限制,任务未完成";
}

几个关键设计决策:

  1. MAX_TURNS 防护:没有这个,Agent 可能陷入死循环。生产环境必须加
  2. is_error: true:工具出错时不要吞掉异常,告诉 Claude 出错了。Claude 会自己决定是重试、换个方式、还是跳过
  3. stop_reason 是唯一的判断依据"end_turn" 表示 Claude 认为任务完成了,"tool_use" 表示它还在工作
Step 5:运行
typescript 复制代码
async function main() {
  const report = await runAgent(
    "帮我调研最近 GitHub 上最热门的 AI Agent 相关项目(3-5 个)," +
      "分析每个项目的核心亮点,最后生成一份中文技术周报。"
  );
  console.log("\n===== 最终报告 =====\n");
  console.log(report);
}

main().catch(console.error);

运行:

bash 复制代码
ANTHROPIC_API_KEY=your_key npx tsx src/agent.ts

你会看到 Agent 自己一步步地搜索、查看 README、分析总结,最终输出一份结构化的中文技术周报。整个过程你只说了一句话。

生产环境必须考虑的 4 件事

Demo 能跑不等于能上线。以下是实际踩过的坑:

1. 成本控制

每一轮 Agentic Loop 都在消耗 Token。一个 15 轮的 Agent 对话,Token 消耗可能是普通对话的 10 倍以上。

typescript 复制代码
// 追踪每次调用的 Token 消耗
let totalInputTokens = 0;
let totalOutputTokens = 0;

// 在每次 API 调用后:
totalInputTokens += response.usage.input_tokens;
totalOutputTokens += response.usage.output_tokens;

const cost =
  (totalInputTokens / 1_000_000) * 3 +    // Sonnet 输入 $3/M
  (totalOutputTokens / 1_000_000) * 15;    // Sonnet 输出 $15/M

if (cost > 0.5) {  // 单次任务成本上限
  console.warn(`成本预警: $${cost.toFixed(4)}`);
  break;
}

经验法则 :Sonnet 跑 Agent 任务,单次对话成本通常在 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0.01 − 0.01- </math>0.01−0.10。如果超过 $0.50,大概率是 Agent 在做无用功了。

2. 错误恢复

GitHub API 有频率限制(60 次/小时未认证)。你的工具会失败,Agent 必须能处理。

好消息是 Claude 天然擅长这个。只要你把错误信息通过 is_error: true 传回去,Claude 通常会:

  • 换一种搜索词重试
  • 跳过有问题的项目
  • 在最终报告中说明哪些信息获取失败了

不要自己写复杂的重试逻辑------让 Claude 来决定。这正是 Agent 相比脚本的优势。

3. 可观测性

当 Agent 在跑一个复杂任务时,你需要知道它在干什么。最简单的方式:

typescript 复制代码
// 在 Agentic Loop 中加日志
for (const tool of toolBlocks) {
  const start = Date.now();
  const result = await executeTool(tool.name, tool.input);
  console.log(
    `[${tool.name}] ${Date.now() - start}ms | ` +
    `输入: ${JSON.stringify(tool.input).slice(0, 100)} | ` +
    `输出: ${result.slice(0, 80)}...`
  );
}

生产环境建议接入 Langfuse 或 Braintrust 等 LLM Observability 平台,可以看到完整的调用链路和成本明细。

4. Human-in-the-Loop

有些场景,Agent 不应该全自动。比如涉及发送邮件、修改数据库、花钱的操作。

实现方式很简单------在 executeTool 里加一个确认步骤:

typescript 复制代码
import * as readline from "readline/promises";

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// 在高风险工具执行前
if (["send_email", "delete_record"].includes(name)) {
  const answer = await rl.question(
    `Agent 想执行 ${name}(${JSON.stringify(input)}),确认?(y/n) `
  );
  if (answer !== "y") {
    return "用户拒绝了此操作";
  }
}

和框架的关系

写完这个你可能会问:那 LangChain、CrewAI 那些框架到底做了什么?

你自己写的 框架帮你做的
Agentic Loop(20 行) 同样的循环,加了更多配置项
工具定义(手写 JSON Schema) Zod/Pydantic 自动生成 Schema
错误处理(if/catch) 自动重试、回退策略
对话历史(数组 push) 持久化存储、多会话管理
日志(console.log) 结构化追踪、可视化面板

什么时候用框架?

  • 需要多个 Agent 协作(CrewAI)
  • 需要复杂的状态管理和分支逻辑(LangGraph)
  • 需要对接 10+ 种工具和数据源(LangChain)
  • 团队开发,需要统一的抽象层

什么时候不用框架?

  • 学习阶段------先理解原理
  • 工具数量少于 5 个的单 Agent 场景
  • 需要极致控制每一步行为
  • 框架的 magic 让你无法调试问题时

我的建议是:先手写一遍,再决定要不要用框架。 很多时候你会发现,手写反而更简单。

总结

今天我们从零实现了一个 AI Agent,核心收获:

概念 要点
Agent vs 聊天机器人 区别在于自主决策循环,不是 API 调用方式
Agentic Loop 检查 stop_reasontool_use 就执行工具继续,end_turn 就结束
工具设计 description 决定 Agent 何时调用你的工具,要写得具体
生产化 成本控制 + 错误恢复 + 可观测性 + Human-in-the-Loop
框架选择 先手写理解原理,复杂场景再上框架

完整代码不到 100 行(不含工具实现),没有任何框架依赖。这就是 Agent 的全部核心逻辑。

你用什么技术栈开发 AI Agent?效果怎么样?欢迎在评论区聊聊。

本文首发于微信公众号「开发者效率局」,欢迎关注获取更多 AI 开发实战内容。

相关推荐
落798.1 小时前
LiveKit × Bright Data:构建实时新闻播客 AI 语音智能体
人工智能·智能体
小鸡吃米…2 小时前
TensorFlow 实现梯度下降优化
人工智能·python·tensorflow·neo4j
cm_chenmin2 小时前
Cursor最佳实践之四:Token
人工智能
Ray Liang2 小时前
概念设计在AI时代的重要性:我是这样设计仿生大脑的
人工智能·ai助手·mindx
大数据在线2 小时前
万卡集群点亮中原:国家级“智算样板间”的落地与远见
人工智能·ai大模型·超算互联网·scalex·中科曙光
500佰2 小时前
openclaw部署和对接QQ,给我定时在QQ推送AI热点项目消息(star数)
人工智能
小璐乱撞2 小时前
Serena MCP:给 AI 装上工程级导航,告别迷路式编程
人工智能·ai编程·mcp
新加坡内哥谈技术2 小时前
万物工程化
人工智能
水如烟2 小时前
孤能子视角:2026春节独有特色
人工智能