从零实现 AI Agent 框架:Tool Calling、Agent Loop 与 Memory 的工程化实战

用 300 行 TypeScript 从零构建一个生产级 AI Agent 框架,深入解析 Tool Calling 协议、ReAct Agent Loop、对话记忆系统,附完整可运行代码与主流框架对比。

开发者效率 2026-06-10 18 分钟

Anthropic 在 2026 年发布的数据显示,基于 Agent 架构的 AI 应用比纯 Chat 架构的任务完成率高出 3.2 倍,但市面上的 Agent 框架(LangChain、LlamaIndex、CrewAI)动辄数万行代码,抽象层叠抽象层,出了问题根本没法调试。本文的核心观点是:一个生产可用的 AI Agent 框架,核心逻辑不超过 300 行 TypeScript。理解了底层原理,你才能在框架之上做出正确的架构决策。

📌 记住: Agent 的本质不是「更聪明的 Chat」,而是一个 感知-推理-行动-观察 的循环。理解这个循环,就理解了所有 Agent 框架的共同内核。

🧠 一、Agent 核心架构:从 Chat 到 Agent 的本质区别

1.1 Chat 模式 vs Agent 模式

Chat 模式是单轮交互:用户发消息,LLM 返回回答,结束。整个过程是被动的——LLM 只负责「回答」,不负责「行动」。Agent 模式完全不同——LLM 会自主决定下一步做什么:调用工具、查询数据库、执行代码,然后根据结果继续推理,直到任务完成。

这个区别的实际影响有多大?举个例子:用户问「北京和上海哪个更热?」,Chat 模式下 LLM 只能凭训练数据猜测(可能过时),而 Agent 模式会先查天气 API、再做比较计算、最后给出有数据支撑的回答。准确性差距是数量级的。

Chat 模式:   用户 → LLM → 回答
Agent 模式:  用户 → LLM → 思考 → 调用工具 → 观察 → 继续思考 → ... → 最终回答

核心区别在于 Tool Calling(工具调用)Agent Loop(代理循环) 两个机制。Tool Calling 让 LLM 从「只能说」变成「能做事」;Agent Loop 让 LLM 从「一次性回答」变成「多步推理」。两者结合,才是真正的 Agent。

一个完整的 Agent 由四个组件构成:

组件 职责 类比
🧠 LLM 推理与决策 大脑
🔧 Tools 执行操作 双手
💾 Memory 存储上下文 记忆
🔄 Loop 驱动循环 意志力

💡 提示: Loop 引擎最容易被忽视但最关键。没有 Loop,Agent 就退化为一次性的 Function Calling。Loop 的核心设计决策包括:最大循环次数(防失控)、错误处理策略(防崩溃)、和终止条件判断(防死循环)。

🔧 二、Tool Calling 机制实现

2.1 工具注册中心

Tool Calling 的本质是结构化输出协议——LLM 返回 JSON 表示「我想调用某个工具,参数是 xxx」。这个协议最初由 OpenAI 在 2023 年提出(当时叫 Function Calling),现在已成为行业标准,Anthropic、Google、Mistral 都支持。

工具定义的关键在于 description 字段——它直接决定了 LLM 能否正确选择和使用工具。好的 description 应该:说清楚工具做什么、接受什么输入、返回什么输出、什么时候该用什么时候不该用。模糊的 description 会导致 LLM 频繁选错工具,这是新手最常犯的错误。

先实现工具注册中心:

// 工具定义与注册中心
interface ToolDefinition {
  name: string;
  description: string;  // 告诉 LLM 这个工具做什么
  parameters: {
    type: 'object';
    properties: Record<string, { type: string; description: string; enum?: string[] }>;
    required: string[];
  };
  execute: (args: Record<string, unknown>) => Promise<string>;
}

class ToolRegistry {
  private tools = new Map<string, ToolDefinition>();

  register(tool: ToolDefinition): void {
    if (this.tools.has(tool.name)) throw new Error(`Duplicate tool: ${tool.name}`);
    this.tools.set(tool.name, tool);
  }

  // 给 LLM 看的定义(不含 execute 函数)
  getDefinitions() {
    return Array.from(this.tools.values()).map(({ execute, ...def }) => def);
  }

  async execute(name: string, args: Record<string, unknown>): Promise<string> {
    const tool = this.tools.get(name);
    if (!tool) return JSON.stringify({ error: `Unknown tool: ${name}` });
    try {
      return await tool.execute(args);
    } catch (err) {
      return JSON.stringify({ error: `Tool "${name}" failed: ${err instanceof Error ? err.message : String(err)}` });
    }
  }
}

2.2 注册实际工具

// 天气查询 + 数学计算两个示例工具
const registry = new ToolRegistry();

registry.register({
  name: 'get_weather',
  description: '获取指定城市的当前天气信息,返回温度、湿度和天气状况',
  parameters: {
    type: 'object',
    properties: {
      city: { type: 'string', description: '城市名称,如"北京"' }
    },
    required: ['city']
  },
  execute: async (args) => {
    const data: Record<string, object> = {
      '北京': { city: '北京', temp: 32, humidity: 45, condition: '晴' },
      '上海': { city: '上海', temp: 28, humidity: 72, condition: '多云' },
    };
    return JSON.stringify(data[args.city as string] ?? { error: '城市未找到' });
  }
});

registry.register({
  name: 'calculate',
  description: '执行数学计算,支持加减乘除',
  parameters: {
    type: 'object',
    properties: {
      expression: { type: 'string', description: '数学表达式,如 "2 + 3 * 4"' }
    },
    required: ['expression']
  },
  execute: async (args) => {
    // ⚠️ 生产环境请使用 mathjs 等安全解析器
    try {
      const result = Function(`"use strict"; return (${args.expression})`)();
      return JSON.stringify({ expression: args.expression, result });
    } catch {
      return JSON.stringify({ error: `无法计算: ${args.expression}` });
    }
  }
});

⚠️ 警告: 上面的 calculate 使用了 Function() 构造器,生产环境是严重安全隐患。应使用 mathjs 或 expr-eval 等安全表达式解析库。

2.3 LLM 调用封装

// 将工具转换为 OpenAI function calling 格式并调用 LLM
interface LLMMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  tool_call_id?: string;
  tool_calls?: Array<{
    id: string;
    type: 'function';
    function: { name: string; arguments: string };
  }>;
}

async function callLLM(messages: LLMMessage[], tools: object[]): Promise<LLMMessage> {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages,
      tools,
      tool_choice: 'auto',  // 让模型自己决定是否调用工具
      temperature: 0.1,     // Agent 场景建议低温度
    }),
  });
  const data = await response.json();
  return data.choices[0].message;
}

🔄 三、Agent Loop:ReAct 模式的核心实现

3.1 ReAct 循环

ReAct(Reasoning + Acting)是 2022 年由 Yao et al. 提出的 Agent 推理模式,目前被几乎所有主流框架采用。每轮循环包含三个步骤:Thought(LLM 分析当前状态,决定下一步)→ Action(调用工具获取外部信息)→ Observation(将工具结果反馈给 LLM)。

ReAct 的核心优势在于可解释性——每一步推理都有明确的 Thought 记录,开发者可以清楚地看到 LLM 为什么做出某个决策。这比「端到端黑盒」的方案更容易调试和审计。但 ReAct 也有缺点:每轮都要调用 LLM,延迟和成本随循环次数线性增长。对于简单任务(1-2 步),直接的 Function Calling 更高效;只有需要多步推理、条件分支、或错误恢复的场景,才值得使用 Agent Loop。

3.2 完整 Agent Loop(核心代码,不到 80 行)

// Agent Loop:ReAct 模式的核心实现
interface AgentConfig {
  maxIterations?: number;
  systemPrompt?: string;
  verbose?: boolean;
}

class MiniAgent {
  private registry: ToolRegistry;
  private config: Required<AgentConfig>;

  constructor(registry: ToolRegistry, config: AgentConfig = {}) {
    this.registry = registry;
    this.config = {
      maxIterations: config.maxIterations ?? 10,
      systemPrompt: config.systemPrompt ?? '你是一个有用的AI助手,可以使用工具来完成任务。',
      verbose: config.verbose ?? false,
    };
  }

  async run(userInput: string): Promise<string> {
    const messages: LLMMessage[] = [
      { role: 'system', content: this.config.systemPrompt },
      { role: 'user', content: userInput },
    ];
    const tools = this.registry.getDefinitions().map(t => ({
      type: 'function' as const, function: t
    }));

    for (let i = 0; i < this.config.maxIterations; i++) {
      const assistant = await callLLM(messages, tools);
      messages.push(assistant);

      // 没有工具调用 → 任务完成
      if (!assistant.tool_calls?.length) return assistant.content ?? '';

      // 执行所有工具调用
      for (const tc of assistant.tool_calls) {
        const args = JSON.parse(tc.function.arguments);
        if (this.config.verbose) console.log(`🔧 ${tc.function.name}(${JSON.stringify(args)})`);

        const result = await this.registry.execute(tc.function.name, args);
        if (this.config.verbose) console.log(`📋 ${result.substring(0, 200)}`);

        messages.push({ role: 'tool', content: result, tool_call_id: tc.id });
      }
    }
    return '⚠️ 达到最大循环次数,任务未完成';
  }
}

3.3 运行示例:多步推理

// 创建 Agent 并执行需要 3 步的多步任务
const agent = new MiniAgent(registry, {
  maxIterations: 8,
  verbose: true,
  systemPrompt: '你是天气分析助手。先查天气,需要计算时用计算工具,最后给出分析结论。',
});

const result = await agent.run('北京和上海的温差是多少?哪个城市更适合出门?');
console.log(result);

预期执行流程:

🔧 get_weather({"city":"北京"})  →  {"temp":32,"humidity":45,"condition":"晴"}
🔧 get_weather({"city":"上海"})  →  {"temp":28,"humidity":72,"condition":"多云"}
🔧 calculate({"expression":"32-28"})  →  {"result":4}
✅ 最终回答:温差 4°C,建议北京(晴,湿度低)

💾 四、Memory 系统:让 Agent 拥有记忆

4.1 对话历史管理

基础 Agent 有一个严重缺陷:每轮对话都是「一次性」的。当对话历史超过上下文窗口(Context Window),早期的信息就会丢失。更糟的是,工具调用的 JSON 结果通常很长(几百到几千 token),几轮对话就能撑爆上下文。

Memory 系统的核心策略是分层管理:短期记忆(当前对话的完整历史)、工作记忆(当前任务的关键中间结果)、和压缩记忆(旧对话的自动摘要)。下面实现的是短期记忆 + 自动压缩的方案,这是最实用的起步方案:

// 带自动摘要的对话记忆
class ConversationMemory {
  private messages: LLMMessage[] = [];
  private summary = '';

  constructor(private maxMessages = 20) {}

  add(message: LLMMessage): void {
    this.messages.push(message);
    if (this.messages.length > this.maxMessages) this.compress();
  }

  private compress(): void {
    const keep = Math.floor(this.maxMessages * 0.6);
    const old = this.messages.slice(0, -keep);
    this.messages = this.messages.slice(-keep);

    const oldText = old
      .filter(m => m.role === 'user' || m.role === 'assistant')
      .map(m => `${m.role}: ${(m.content ?? '').substring(0, 200)}`)
      .join('\n');
    this.summary += `\n[历史摘要]\n${oldText}\n`;
  }

  getMessages(): LLMMessage[] {
    const result: LLMMessage[] = [];
    if (this.summary) result.push({ role: 'system', content: `之前的对话摘要:${this.summary}` });
    result.push(...this.messages);
    return result;
  }
}

⚠️ 警告: 工具调用历史可能包含敏感信息。生产环境中务必对 argsresult 做脱敏处理后再存储。

⚡ 五、流式输出与错误恢复

5.1 流式 Agent

用户不想等 Agent 完成所有循环才看到结果。用 AsyncGenerator 实现实时输出:

// 流式 Agent:实时输出每个步骤
class StreamingAgent extends MiniAgent {
  async *runStream(userInput: string): AsyncGenerator<{
    type: 'thinking' | 'tool_call' | 'tool_result' | 'answer';
    content: string;
  }> {
    const messages: LLMMessage[] = [
      { role: 'system', content: this.config.systemPrompt },
      { role: 'user', content: userInput },
    ];
    const tools = this.registry.getDefinitions().map(t => ({
      type: 'function' as const, function: t
    }));

    for (let i = 0; i < this.config.maxIterations; i++) {
      yield { type: 'thinking', content: `🔄 第 ${i + 1} 轮推理...` };
      const assistant = await callLLM(messages, tools);
      messages.push(assistant);

      if (!assistant.tool_calls?.length) {
        yield { type: 'answer', content: assistant.content ?? '' };
        return;
      }

      for (const tc of assistant.tool_calls) {
        const args = JSON.parse(tc.function.arguments);
        yield { type: 'tool_call', content: `🔧 ${tc.function.name}(${JSON.stringify(args)})` };

        const start = Date.now();
        const result = await this.registry.execute(tc.function.name, args);
        yield { type: 'tool_result', content: `📋 (${Date.now() - start}ms) ${result.substring(0, 300)}` };

        messages.push({ role: 'tool', content: result, tool_call_id: tc.id });
      }
    }
    yield { type: 'answer', content: '⚠️ 达到最大循环次数' };
  }
}

5.2 错误恢复策略

Agent Loop 中最常见的失败是 LLM「幻觉工具调用」。错误处理策略:

错误类型 处理策略 推荐做法
工具不存在 返回错误列表给 LLM ✅ 返回清晰的错误信息
参数格式错误 反馈给 LLM 自动修正 ✅ 包含期望的参数格式
工具执行超时 设置超时中断 ✅ 使用 AbortController
API 限流 指数退避重试 ✅ 最多重试 3 次
LLM 幻觉工具名 列出可用工具 ✅ 在 error 中列出可选项

⚠️ 警告: 永远不要让 Agent Loop 无限循环。设置 maxIterations(建议 5-15),并监控 token 消耗。一个失控的 Agent 可能在几分钟内烧掉上百美元。

📊 六、框架对比与选型建议

特性 Mini Agent(本文) LangChain LlamaIndex CrewAI
代码量 ~300 行 ~50,000 行 ~30,000 行 ~15,000 行
学习曲线 ⭐ 极低 ⭐⭐⭐⭐ 高 ⭐⭐⭐ 中 ⭐⭐⭐ 中
调试难度 ✅ 直接看代码 ❌ 层层抽象 ⚠️ 部分可见 ⚠️ 部分可见
Memory ✅ 基础 ✅ 完善 ✅ 完善 ✅ 完善
Multi-Agent ❌ 需扩展 ✅ 内置 ⚠️ 有限 ✅ 核心特性
依赖体积 ~0(纯 TS) ~15MB ~10MB ~8MB

关键结论: 如果需求是「LLM + 几个工具 + 简单记忆」,300 行代码完全够用。只有需要复杂多 Agent 协作、RAG 集成时,才值得引入大型框架。

生产环境必须做的五件事

  • 设置 Token 上限:每次 LLM 调用限制 max_tokens,防止输出过长
  • 超时控制:每个工具调用设置 30 秒超时,使用 AbortController
  • 日志记录:记录每轮 Thought/Action/Observation,用于调试审计
  • 成本监控:跟踪 token 消耗,设置每日预算上限
  • 输入验证:对 LLM 输出的工具参数做严格校验,不要信任模型输出

必须避免的五个坑

  • 不要用 eval 执行 LLM 生成的代码——最严重的安全漏洞
  • 不要把系统密钥放在工具描述中——LLM 可能泄露
  • 不要忽略工具调用错误——错误信息是 LLM 自我修正的关键信号
  • 不要在 Agent Loop 中做数据库写操作——LLM 决策可能有误
  • 不要让 Agent 访问生产环境——先在沙箱验证

关键结论: Agent 的安全性与能力成正比。一个能调用 10 个工具的 Agent,风险面是单工具 Agent 的 10 倍。遵循最小权限原则——只给 Agent 完成任务所必需的最少工具和权限。

📝 总结

一个 AI Agent 框架的核心只需要三个组件:Tool Registry(工具注册)、Agent Loop(ReAct 循环)、Memory(记忆管理)。理解这三层,你就拥有了评估任何 Agent 框架的能力。

选型建议:

  • 🎯 快速原型 / 学习原理:本文的 Mini Agent,300 行搞定
  • 🎯 单 Agent + 多工具生产应用:LangChain 或自研,重点做好错误处理和监控
  • 🎯 多 Agent 协作:CrewAI 或 AutoGen,做好成本控制
  • 🎯 RAG 增强 Agent:LlamaIndex,检索和索引能力最强

推荐工具:OpenAI Function Calling 文档Anthropic Tool UseVercel AI SDKInstructor

💡 提示: Agent 开发的核心不是代码量,而是对 LLM 行为的理解。多试几个 prompt,观察 LLM 在不同场景下的 Tool Calling 决策,这比任何框架文档都有价值。

📚 相关文章