构建生产级 AI Agent:架构设计、工具调用与工程避坑指南

深入解析 AI Agent 核心架构,从零实现支持 Function Calling、记忆管理、重试机制的生产级 Agent,含完整 TypeScript 代码和性能对比。

前端开发 2026-06-07 15 分钟

2026 年,AI Agent 已经从概念验证走向生产部署。据统计,超过 60% 的企业级 AI 应用采用 Agent 架构而非简单的 Chatbot,但其中近 40% 的项目在上线后遭遇了工具调用失败率高、上下文溢出、幻觉循环等问题。这篇文章不是教你调 API,而是从工程视角拆解一个能在生产环境稳定运行的 AI Agent 到底该怎么构建——包括架构设计、工具调用协议、记忆管理和错误恢复机制。

🏗️ 一、Agent 核心架构:不只是 while 循环调 LLM

大多数开发者对 AI Agent 的理解停留在"LLM + 工具调用的 while 循环",这在 demo 阶段够用,但生产环境会遇到一系列工程问题。一个成熟的 Agent 架构需要包含以下核心模块:

1.1 三层架构设计

生产级 Agent 通常采用三层架构:

层级 职责 关键技术点
编排层(Orchestrator) 控制 Agent 循环、决定何时终止、管理并发子 Agent 状态机、最大轮次限制、超时控制
推理层(Reasoning) LLM 调用、Prompt 构建、响应解析 流式输出、结构化输出、Token 计数
执行层(Execution) 工具调用、结果序列化、错误处理 并发执行、超时、沙箱隔离

📌 **记住:**编排层是 Agent 的大脑,推理层是思考能力,执行层是手脚。三层解耦才能独立迭代和监控。

1.2 从零实现最小 Agent 内核

以下是一个最小但完整的 Agent 内核实现,支持 Function Calling 和最大轮次限制:

// agent-kernel.ts — 最小 Agent 内核,支持工具调用和轮次控制
interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  tool_call_id?: string;
  name?: string;
}

interface Tool {
  name: string;
  description: string;
  parameters: Record<string, unknown>;
  execute: (args: Record<string, unknown>) => Promise<string>;
}

interface AgentConfig {
  maxIterations: number;      // 最大推理轮次,防止无限循环
  maxTokens: number;          // 单次响应最大 Token
  timeoutMs: number;          // 单轮超时时间
  temperature: number;
}

class Agent {
  private tools: Map<string, Tool> = new Map();
  private config: AgentConfig;
  private llm: LLMClient;

  constructor(llm: LLMClient, config: Partial<AgentConfig> = {}) {
    this.llm = llm;
    this.config = {
      maxIterations: 10,
      maxTokens: 4096,
      timeoutMs: 30_000,
      temperature: 0.1,
      ...config,
    };
  }

  registerTool(tool: Tool): void {
    this.tools.set(tool.name, tool);
  }

  async run(userMessage: string, systemPrompt: string): Promise<string> {
    const messages: Message[] = [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userMessage },
    ];

    for (let iteration = 0; iteration < this.config.maxIterations; iteration++) {
      // 调用 LLM,传入工具定义
      const response = await this.callLLM(messages);

      // 如果 LLM 返回纯文本(无工具调用),任务完成
      if (!response.tool_calls || response.tool_calls.length === 0) {
        return response.content;
      }

      // 执行所有工具调用(支持并行)
      const toolResults = await this.executeTools(response.tool_calls);

      // 将工具结果追加到消息历史
      messages.push({ role: 'assistant', content: response.content });
      for (const result of toolResults) {
        messages.push({
          role: 'tool',
          tool_call_id: result.toolCallId,
          content: result.output,
        });
      }
    }

    // ⚠️ 达到最大轮次,强制终止
    return '[Agent] 已达到最大推理轮次限制,请简化任务后重试。';
  }

  private async callLLM(messages: Message[]): Promise<LLMResponse> {
    const toolDefs = Array.from(this.tools.values()).map(t => ({
      type: 'function',
      function: { name: t.name, description: t.description, parameters: t.parameters },
    }));

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);

    try {
      return await this.llm.chat({
        messages,
        tools: toolDefs,
        temperature: this.config.temperature,
        max_tokens: this.config.maxTokens,
        signal: controller.signal,
      });
    } finally {
      clearTimeout(timeout);
    }
  }

  private async executeTools(toolCalls: ToolCall[]): Promise<ToolResult[]> {
    // 并发执行所有工具调用,单个失败不影响其他
    const results = await Promise.allSettled(
      toolCalls.map(async (tc) => {
        const tool = this.tools.get(tc.function.name);
        if (!tool) {
          return { toolCallId: tc.id, output: `错误:未知工具 ${tc.function.name}` };
        }
        const args = JSON.parse(tc.function.arguments);
        const output = await tool.execute(args);
        return { toolCallId: tc.id, output };
      })
    );

    return results.map((r, i) => {
      if (r.status === 'fulfilled') return r.value;
      return { toolCallId: toolCalls[i].id, output: `工具执行失败: ${r.reason}` };
    });
  }
}

💡 **提示:**注意 maxIterations 的设置——生产环境建议 5-15 次。太小会限制复杂任务,太大会浪费 Token 甚至导致 Agent 在错误路径上越走越远。

1.3 Agent 循环的三个致命陷阱

陷阱 1:无最大轮次限制 — Agent 在工具调用失败时可能反复重试,产生无限循环,Token 费用爆炸。

陷阱 2:工具结果直接拼入 Prompt — 如果工具返回超大结果(如数据库查询返回 10 万行),直接塞入上下文会导致 Token 溢出。

陷阱 3:不做 Token 预算管理 — 多轮对话后上下文超出模型窗口,LLM 会截断早期消息,丢失关键工具定义。

🔧 二、工具调用系统:协议设计与错误处理

工具调用是 Agent 的核心能力,但也是最容易出问题的环节。一个设计良好的工具调用系统需要解决三个问题:参数校验、执行隔离、结果压缩。

2.1 工具定义与参数校验

LLM 返回的工具参数是 JSON 字符串,直接 JSON.parse 后使用是危险的。必须做严格的 Schema 校验:

// tool-schema.ts — 使用 Zod 做工具参数校验
import { z } from 'zod';

// 定义工具的参数 Schema
const searchWebSchema = z.object({
  query: z.string().min(1).max(500),
  maxResults: z.number().int().min(1).max(20).default(5),
  language: z.enum(['zh', 'en']).default('zh'),
});

// 创建工具时绑定 Schema
function createTool<T extends z.ZodType>(
  name: string,
  description: string,
  schema: T,
  handler: (args: z.infer<T>) => Promise<string>
): Tool {
  return {
    name,
    description,
    parameters: zodToJsonSchema(schema), // 转为 JSON Schema 供 LLM 理解
    execute: async (rawArgs: Record<string, unknown>) => {
      // ⚡ 关键:先校验再执行
      const result = schema.safeParse(rawArgs);
      if (!result.success) {
        // 返回校验错误信息,让 LLM 自行修正参数
        return `参数错误: ${result.error.issues.map(i => `${i.path.join('.')}: ${i.message}`).join('; ')}`;
      }
      return handler(result.data);
    },
  };
}

// 使用示例
const searchTool = createTool(
  'search_web',
  '搜索互联网获取最新信息',
  searchWebSchema,
  async ({ query, maxResults, language }) => {
    const results = await fetch(`https://api.search.example/q=${encodeURIComponent(query)}&limit=${maxResults}&lang=${language}`);
    const data = await results.json();
    return JSON.stringify(data.items.slice(0, maxResults));
  }
);

⚠️ **警告:**永远不要信任 LLM 返回的参数格式。我见过 Agent 调用文件删除工具时传入 / 作为路径——参数校验是你的安全底线。

2.2 工具执行的并发控制与超时

生产环境中,工具可能是外部 API 调用、数据库查询或文件操作,必须做并发控制和超时管理:

策略 适用场景 实现方式
串行执行 有依赖关系的工具链 for...of + await
完全并行 无依赖的独立工具 Promise.all
受限并行 控制外部 API 并发数 p-limit 或自定义 Semaphore
超时熔断 防止单个工具卡死 AbortController + Promise.race
// tool-executor.ts — 带超时和并发控制的工具执行器
class ToolExecutor {
  private semaphore: number;
  private queue: Array<() => void> = [];
  private running = 0;

  constructor(private maxConcurrency: number = 3) {
    this.semaphore = maxConcurrency;
  }

  async executeWithTimeout<T>(
    fn: () => Promise<T>,
    timeoutMs: number = 10_000
  ): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error(`工具执行超时 (${timeoutMs}ms)`));
      }, timeoutMs);

      fn().then(
        (result) => { clearTimeout(timer); resolve(result); },
        (err) => { clearTimeout(timer); reject(err); }
      );
    });
  }

  async executeLimited<T>(fn: () => Promise<T>, timeoutMs?: number): Promise<T> {
    // 等待获取执行许可
    await this.acquire();
    try {
      return timeoutMs
        ? await this.executeWithTimeout(fn, timeoutMs)
        : await fn();
    } finally {
      this.release();
    }
  }

  private acquire(): Promise<void> {
    if (this.semaphore > 0) {
      this.semaphore--;
      return Promise.resolve();
    }
    return new Promise<void>((resolve) => {
      this.queue.push(resolve);
    });
  }

  private release(): void {
    const next = this.queue.shift();
    if (next) {
      next();
    } else {
      this.semaphore++;
    }
  }
}

2.3 工具结果压缩与 Token 预算

当工具返回大量数据时,直接塞入上下文是灾难性的。必须做结果压缩:

  • 截断策略:保留前 N 条结果 + 总数提示(如"共 1000 条,显示前 10 条")
  • 摘要策略:对长文本用 LLM 做摘要后再放入上下文
  • 结构化提取:只提取 LLM 需要的字段,去掉冗余数据
  • 避免:把完整 API 响应(含 HTTP headers、status 等)直接传给 LLM

🧠 三、记忆系统与上下文管理

Agent 的记忆系统决定了它能否处理复杂的多步任务。简单地把所有历史消息拼接在一起,很快就会超出上下文窗口。

3.1 三层记忆架构

记忆层级 类比 生命周期 存储方式
工作记忆 人的短期记忆 单次任务 消息数组,滑动窗口
情景记忆 人的经历记忆 跨会话 向量数据库,按相关性检索
程序记忆 人的肌肉记忆 永久 工具定义 + 系统 Prompt

⚡ **关键结论:**工作记忆管当下对话,情景记忆管历史经验,程序记忆管"我会什么"。三层配合才能让 Agent 处理复杂任务。

3.2 上下文窗口管理实现

// context-manager.ts — 智能上下文窗口管理
interface ContextBudget {
  totalTokens: number;      // 模型上下文窗口大小
  reserveForResponse: number; // 预留给响应的 Token
  systemPromptTokens: number; // 系统 Prompt 占用
  toolsTokens: number;        // 工具定义占用
}

class ContextManager {
  private budget: ContextBudget;
  private tokenCounter: TokenCounter;

  constructor(model: string) {
    this.tokenCounter = new TokenCounter(model);
    // 根据模型设置不同的上下文预算
    this.budget = this.getBudget(model);
  }

  private getBudget(model: string): ContextBudget {
    const budgets: Record<string, ContextBudget> = {
      'gpt-4o': { totalTokens: 128_000, reserveForResponse: 4096, systemPromptTokens: 0, toolsTokens: 0 },
      'claude-sonnet-4-20250514': { totalTokens: 200_000, reserveForResponse: 8192, systemPromptTokens: 0, toolsTokens: 0 },
    };
    return budgets[model] || budgets['gpt-4o'];
  }

  // 获取可用于消息的 Token 预算
  get availableTokens(): number {
    return this.budget.totalTokens
      - this.budget.reserveForResponse
      - this.budget.systemPromptTokens
      - this.budget.toolsTokens;
  }

  // 裁剪消息历史,保留关键上下文
  trimMessages(messages: Message[]): Message[] {
    let totalTokens = 0;
    const systemMessages: Message[] = [];
    const conversationMessages: Message[] = [];

    for (const msg of messages) {
      if (msg.role === 'system') {
        systemMessages.push(msg);
      } else {
        conversationMessages.push(msg);
      }
    }

    // 从最新消息向前遍历,直到 Token 预算用完
    const trimmed: Message[] = [];
    for (let i = conversationMessages.length - 1; i >= 0; i--) {
      const msgTokens = this.tokenCounter.count(conversationMessages[i].content);
      if (totalTokens + msgTokens > this.availableTokens) {
        // 在截断处插入提示
        trimmed.unshift({
          role: 'system',
          content: `[注意:以上为较早的历史记录,已被截断以节省上下文空间]`,
        });
        break;
      }
      totalTokens += msgTokens;
      trimmed.unshift(conversationMessages[i]);
    }

    return [...systemMessages, ...trimmed];
  }
}

3.3 避免上下文污染的最佳实践

在实际开发中,上下文污染是 Agent 失败的常见原因:

  • 工具结果隔离:每个工具调用结果用明确的分隔符包裹,防止 LLM 混淆工具输出和用户输入
  • 敏感信息脱敏:工具可能返回包含密码、Token 的日志,需要在放入上下文前过滤
  • 错误信息降级:工具执行失败时,只返回错误类型和建议,不返回完整堆栈
  • 避免:把整个 JSON 文件(数千行)作为工具结果返回给 LLM

📊 四、生产部署的可靠性工程

Agent 在生产环境中面临的核心挑战是可靠性。LLM 的输出天然具有不确定性,工具调用也可能失败,必须建立完整的错误恢复机制。

4.1 错误分级与重试策略

错误类型 示例 重试策略 最大重试
LLM 限流 429 Too Many Requests 指数退避 3 次
工具执行失败 API 超时、网络错误 告诉 LLM 失败原因,让它决定是否重试 由 LLM 决策
参数校验失败 LLM 传了错误参数 返回校验错误,LLM 自动修正 3 次
上下文溢出 Token 超限 截断历史 + 重试 1 次
幻觉循环 LLM 反复调用不存在的工具 记录工具名黑名单,强制终止 0 次

⚠️ **警告:**幻觉循环是最危险的故障模式——LLM 反复调用一个不存在的工具,每次都失败,每次都重试。必须设置"未知工具调用计数器",超过阈值立即终止。

4.2 可观测性:Agent 的日志与监控

生产 Agent 必须记录每一次推理循环的完整状态:

// agent-logger.ts — Agent 可观测性日志
interface AgentStepLog {
  iteration: number;
  timestamp: number;
  llmTokensUsed: { input: number; output: number };
  toolCalls: Array<{
    name: string;
    args: string;
    resultPreview: string;  // 截断到 200 字符
    durationMs: number;
    success: boolean;
    error?: string;
  }>;
  decision: 'continue' | 'finish' | 'error' | 'max_iterations';
  totalDurationMs: number;
}

class AgentLogger {
  private steps: AgentStepLog[] = [];
  private startTime = Date.now();

  logStep(step: AgentStepLog): void {
    this.steps.push(step);
    // 实时输出到监控系统
    console.log(JSON.stringify({
      event: 'agent_step',
      iteration: step.iteration,
      tools: step.toolCalls.map(t => `${t.name}(${t.success ? 'ok' : 'fail'})`),
      tokens: step.llmTokensUsed,
      duration: step.totalDurationMs,
      decision: step.decision,
    }));
  }

  getSummary(): AgentSummary {
    const totalTokens = this.steps.reduce(
      (acc, s) => acc + s.llmTokensUsed.input + s.llmTokensUsed.output, 0
    );
    const totalTools = this.steps.reduce((acc, s) => acc + s.toolCalls.length, 0);
    const failedTools = this.steps.reduce(
      (acc, s) => acc + s.toolCalls.filter(t => !t.success).length, 0
    );

    return {
      totalIterations: this.steps.length,
      totalTokens,
      totalToolCalls: totalTools,
      failedToolCalls: failedTools,
      toolSuccessRate: totalTools > 0 ? (totalTools - failedTools) / totalTools : 1,
      totalDurationMs: Date.now() - this.startTime,
    };
  }
}

✅ 总结与实践建议

构建生产级 AI Agent 不是一个 LLM API 调用问题,而是一个系统工程问题。以下是核心要点:

  1. 架构解耦:编排层、推理层、执行层分离,方便独立测试和监控
  2. 工具参数校验:永远用 Zod/Joi 等 Schema 库校验 LLM 返回的参数
  3. Token 预算管理:计算系统 Prompt + 工具定义 + 历史消息的 Token 总量,预留响应空间
  4. 错误恢复:区分可重试错误和致命错误,让 LLM 自行决定是否重试工具调用
  5. 可观测性:记录每轮推理的 Token 消耗、工具调用结果、决策原因
  6. 避免无限循环:必须设置 maxIterations,这是 Agent 的"死人开关"
  7. 避免盲目信任:LLM 返回的参数、工具名、执行顺序都需要校验

⚡ **关键结论:**一个好的 Agent 不是"能跑通 demo"的 Agent,而是"在工具失败、网络超时、上下文溢出时依然能优雅降级"的 Agent。工程可靠性比推理能力更重要。

相关工具推荐:

  • 🔧 Vercel AI SDK — 提供统一的 LLM 调用接口和流式输出支持
  • 🔧 Zod — TypeScript-first 的参数校验库,与 JSON Schema 无缝转换
  • 🔧 tiktoken / js-tiktoken — 精确的 Token 计数,用于上下文预算管理
  • 🔧 LangSmith / Langfuse — Agent 可观测性平台,追踪每次工具调用和推理决策

📚 相关文章