轻量化 Agent 产品设计:开源工具链驱动的极简架构实践

cover

一、Agent 产品的重量陷阱:从概念验证到产品化的鸿沟

Agent 是当下 AI 应用最热门的形态。一个 Demo 级 Agent 只需要几百行代码:拼接提示词、调用 LLM、解析输出、执行工具。但把 Demo 推向产品,问题接踵而至。工具调用的可靠性不足 70%,多轮对话的上下文窗口溢出,Agent 行为不可预测导致测试无法覆盖,单次请求成本随工具链增长而飙升。

更根本的问题是架构选择。LangChain 生态庞大,但依赖链深达数十层,一个版本升级就可能破坏兼容性。CrewAI 多 Agent 协作优雅,但调度开销在并发场景下急剧上升。很多团队在技术选型阶段被框架的"开箱即用"承诺吸引,上线后才发现框架的抽象层成为性能瓶颈和调试黑盒。

轻量化 Agent 的核心主张:用最少的抽象层完成从意图理解到工具执行的闭环,把控制权还给开发者。本文将围绕开源工具链,给出一套可落地的轻量 Agent 架构方案。

二、Agent 执行循环的底层机制:从意图到行动的数据流

Agent 的本质是一个循环:感知环境 → 推理决策 → 执行动作 → 观察结果 → 继续推理。这个循环被称为 ReAct(Reasoning + Acting)范式。理解这个循环的内部机制,是做轻量化设计的前提。

flowchart TB
    A[用户输入] --> B[意图解析与槽位提取]
    B --> C{是否需要工具?}
    C -->|否| D[直接 LLM 生成]
    C -->|是| E[工具选择与参数映射]
    E --> F[工具执行沙箱]
    F --> G{执行成功?}
    G -->|是| H[结果注入上下文]
    G -->|否| I[错误回退与重试]
    I --> J{重试次数 < 上限?}
    J -->|是| E
    J -->|否| K[降级为 LLM 直接回答]
    H --> L[LLM 基于工具结果生成回答]
    D --> M[输出格式校验]
    L --> M
    M --> N{输出合规?}
    N -->|是| O[返回用户]
    N -->|否| P[修正提示词重新生成]
    P --> D

    style C fill:#ffd93d,color:#333
    style G fill:#ffd93d,color:#333
    style N fill:#ffd93d,color:#333
    style F fill:#6bcb77,color:#fff

上图揭示了 Agent 执行循环中三个关键决策点(黄色节点):是否需要工具、工具执行是否成功、输出是否合规。每个决策点都需要明确的退出条件,否则循环可能无限进行。轻量化设计的核心就是精确控制每个决策点的逻辑,避免过度抽象。

关键机制解析:

  • 意图解析:不是让 LLM 自由输出,而是用结构化 Schema 约束输出格式,确保意图和槽位可被程序解析
  • 工具选择:基于意图匹配工具注册表,而非让 LLM 从数百个工具中自行选择——后者是幻觉的主要来源
  • 执行沙箱:工具执行必须在受控环境中进行,限制超时、资源消耗和副作用范围

三、生产级轻量 Agent 实现:极简工具链代码

3.1 工具注册与 Schema 定义

// tools.ts — 工具定义与注册,Schema 驱动的参数校验
interface ToolSchema {
  name: string;
  description: string;
  parameters: Record<string, {
    type: 'string' | 'number' | 'boolean';
    description: string;
    required: boolean;
    enum?: string[];
  }>;
}

interface ToolResult {
  success: boolean;
  data: unknown;
  error?: string;
}

type ToolExecutor = (params: Record<string, unknown>) => Promise<ToolResult>;

interface RegisteredTool {
  schema: ToolSchema;
  execute: ToolExecutor;
  // 超时控制,默认 10 秒
  timeout_ms: number;
  // 是否允许重试
  retryable: boolean;
}

// 工具注册表:全局单例,管理所有可用工具
class ToolRegistry {
  private tools = new Map<string, RegisteredTool>();

  register(
    schema: ToolSchema,
    executor: ToolExecutor,
    options: { timeout_ms?: number; retryable?: boolean } = {}
  ): void {
    if (this.tools.has(schema.name)) {
      throw new Error(`工具已注册: ${schema.name}`);
    }
    this.tools.set(schema.name, {
      schema,
      execute: executor,
      timeout_ms: options.timeout_ms ?? 10000,
      retryable: options.retryable ?? false,
    });
  }

  get(name: string): RegisteredTool | undefined {
    return this.tools.get(name);
  }

  // 生成工具描述,供 LLM 理解可用工具
  getToolDescriptions(): string {
    const descriptions: string[] = [];
    for (const [, tool] of this.tools) {
      const params = Object.entries(tool.schema.parameters)
        .map(([key, p]) => `  - ${key} (${p.type}${p.required ? ', 必填' : ''}): ${p.description}`)
        .join('\n');
      descriptions.push(`${tool.schema.name}: ${tool.schema.description}\n${params}`);
    }
    return descriptions.join('\n\n');
  }

  // 校验参数是否符合 Schema
  validateParams(toolName: string, params: Record<string, unknown>): {
    valid: boolean;
    errors: string[];
  } {
    const tool = this.tools.get(toolName);
    if (!tool) {
      return { valid: false, errors: [`工具不存在: ${toolName}`] };
    }

    const errors: string[] = [];
    for (const [key, schema] of Object.entries(tool.schema.parameters)) {
      if (schema.required && !(key in params)) {
        errors.push(`缺少必填参数: ${key}`);
        continue;
      }
      if (key in params) {
        const value = params[key];
        if (schema.enum && !schema.enum.includes(String(value))) {
          errors.push(`参数 ${key} 的值不在允许范围内: ${schema.enum.join(', ')}`);
        }
      }
    }
    return { valid: errors.length === 0, errors };
  }
}

3.2 Agent 核心执行循环

// agent.ts — 轻量 Agent 执行引擎,ReAct 循环的极简实现
interface AgentConfig {
  model: string;
  systemPrompt: string;
  maxIterations: number;      // 最大推理轮次,防止无限循环
  maxToolCallsPerTurn: number; // 单轮最大工具调用次数
  toolCallTimeout_ms: number;  // 工具调用超时
}

interface AgentState {
  messages: Array<{ role: 'system' | 'user' | 'assistant' | 'tool'; content: string }>;
  iteration: number;
  toolCallCount: number;
}

class LightweightAgent {
  private registry: ToolRegistry;
  private config: AgentConfig;

  constructor(registry: ToolRegistry, config: AgentConfig) {
    this.registry = registry;
    this.config = config;
  }

  async run(userInput: string): Promise<string> {
    const state: AgentState = {
      messages: [
        { role: 'system', content: this.buildSystemPrompt() },
        { role: 'user', content: userInput },
      ],
      iteration: 0,
      toolCallCount: 0,
    };

    while (state.iteration < this.config.maxIterations) {
      state.iteration++;

      // 调用 LLM 获取下一步决策
      const response = await this.callLLM(state.messages);

      // 解析 LLM 输出:是否包含工具调用
      const toolCalls = this.parseToolCalls(response);

      if (toolCalls.length === 0) {
        // 无工具调用,视为最终回答
        return response;
      }

      if (state.toolCallCount + toolCalls.length > this.config.maxToolCallsPerTurn) {
        // 工具调用次数超限,强制终止并返回已有信息
        state.messages.push({
          role: 'assistant',
          content: '工具调用次数已达上限,基于已有信息生成回答。',
        });
        return await this.callLLM(state.messages);
      }

      // 执行工具调用
      for (const call of toolCalls) {
        state.toolCallCount++;
        const result = await this.executeToolWithTimeout(call);

        // 将工具结果注入上下文
        state.messages.push({
          role: 'tool',
          content: JSON.stringify({
            tool: call.name,
            params: call.params,
            result: result.success ? result.data : `错误: ${result.error}`,
          }),
        });
      }

      // 工具结果注入后,继续让 LLM 推理
    }

    // 超过最大轮次,返回兜底回答
    return '抱歉,处理您的请求需要过多步骤,请尝试简化问题。';
  }

  private buildSystemPrompt(): string {
    const toolDesc = this.registry.getToolDescriptions();
    return `${this.config.systemPrompt}

你可以使用以下工具:
${toolDesc}

当你需要使用工具时,请用以下 JSON 格式输出:
<tool_call/>
{"name": "工具名", "params": {"参数名": "参数值"}}
</tool_call/>

如果你已经有足够信息回答用户,直接输出回答,不要使用工具。`;
  }

  private parseToolCalls(response: string): Array<{ name: string; params: Record<string, unknown> }> {
    const calls: Array<{ name: string; params: Record<string, unknown> }> = [];
    const regex = /<tool_call\/>([\s\S]*?)<\/tool_call>/g;
    let match;

    while ((match = regex.exec(response)) !== null) {
      try {
        const parsed = JSON.parse(match[1].trim());
        // 校验工具和参数
        const validation = this.registry.validateParams(parsed.name, parsed.params);
        if (validation.valid) {
          calls.push({ name: parsed.name, params: parsed.params });
        }
      } catch {
        // JSON 解析失败,跳过这个工具调用
        continue;
      }
    }

    return calls;
  }

  private async executeToolWithTimeout(
    call: { name: string; params: Record<string, unknown> }
  ): Promise<ToolResult> {
    const tool = this.registry.get(call.name);
    if (!tool) {
      return { success: false, data: null, error: `工具未注册: ${call.name}` };
    }

    try {
      const result = await Promise.race([
        tool.execute(call.params),
        this.createTimeout(tool.timeout_ms, call.name),
      ]);
      return result;
    } catch (err) {
      return {
        success: false,
        data: null,
        error: err instanceof Error ? err.message : '工具执行失败',
      };
    }
  }

  private async callLLM(
    messages: Array<{ role: string; content: string }>
  ): Promise<string> {
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.LLM_API_KEY}`,
      },
      body: JSON.stringify({
        model: this.config.model,
        messages,
        temperature: 0.1,
      }),
    });

    if (!response.ok) {
      throw new Error(`LLM 调用失败: ${response.status}`);
    }

    const data = await response.json();
    return data.choices[0].message.content;
  }

  private createTimeout(ms: number, toolName: string): Promise<never> {
    return new Promise((_, reject) =>
      setTimeout(() => reject(new Error(`工具 ${toolName} 执行超时: ${ms}ms`)), ms)
    );
  }
}

3.3 工具注册示例

// register-tools.ts — 注册具体业务工具
const registry = new ToolRegistry();

// 数据库查询工具
registry.register(
  {
    name: 'query_user_info',
    description: '根据用户 ID 查询用户基本信息',
    parameters: {
      user_id: {
        type: 'string',
        description: '用户唯一标识',
        required: true,
      },
      fields: {
        type: 'string',
        description: '需要返回的字段',
        required: false,
        enum: ['basic', 'profile', 'all'],
      },
    },
  },
  async (params) => {
    try {
      const userId = params.user_id as string;
      const fields = (params.fields as string) || 'basic';
      // 实际项目中替换为数据库查询
      const data = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
      if (!data) {
        return { success: false, data: null, error: '用户不存在' };
      }
      return { success: true, data: filterFields(data, fields) };
    } catch (err) {
      return {
        success: false,
        data: null,
        error: err instanceof Error ? err.message : '查询失败',
      };
    }
  },
  { timeout_ms: 5000, retryable: true }
);

// 创建 Agent 实例
const agent = new LightweightAgent(registry, {
  model: 'gpt-4o-mini',
  systemPrompt: '你是一个用户助手,可以帮助查询用户信息。请根据用户的问题选择合适的工具。',
  maxIterations: 5,
  maxToolCallsPerTurn: 3,
  toolCallTimeout_ms: 10000,
});

四、轻量化架构的代价:灵活性、安全性与可扩展性的取舍

工具调用的可靠性问题。 LLM 生成的工具调用参数可能不符合 Schema,即使有校验层,LLM 仍然可能传错参数类型。结构化输出(JSON Mode)能缓解这个问题,但并非所有模型都支持。实际项目中,对关键工具需要增加参数修正逻辑,这增加了代码量,也偏离了"轻量"的初衷。

安全边界的模糊性。 Agent 可以执行工具,意味着它可以产生副作用(写数据库、发邮件、调用外部 API)。沙箱隔离是必须的,但轻量实现往往只做了超时控制,缺乏资源限制和权限分级。生产环境需要按工具粒度配置权限白名单,这又引入了权限管理的复杂度。

多 Agent 协作的缺失。 单 Agent 架构简单,但复杂任务需要多 Agent 协作。轻量框架通常不内置多 Agent 调度,需要自行实现任务分发和结果聚合。这个实现成本不低,而且容易引入状态同步问题。

上下文窗口的硬约束。 每次工具调用结果都注入上下文,长对话中上下文窗口很快耗尽。截断策略(只保留最近 N 轮)简单粗暴,可能丢失关键信息。摘要策略(用 LLM 压缩历史)更优雅,但增加了调用成本和延迟。

适用边界:工具数量 < 20、推理轮次 < 10 的场景,轻量 Agent 架构足够;工具数量 > 50 或需要多 Agent 协作的场景,需要引入更完善的框架(如 LangGraph)来管理复杂度。

五、总结

轻量化 Agent 产品设计的核心是用最少的抽象层完成从意图理解到工具执行的闭环。工具注册表解决工具发现与校验问题,ReAct 循环解决推理与行动的编排问题,超时与重试机制解决可靠性问题。每一层抽象都有明确的职责,不引入多余的间接层。

落地路线:先定义工具 Schema 和注册表,建立工具调用的基础设施;再实现单 Agent 的 ReAct 循环,验证核心链路可用;最后根据线上数据逐步添加权限控制、上下文压缩、多 Agent 协作等能力。轻量不是简陋,而是每一行代码都有存在的理由。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐