Chuyển đến nội dung chính

第 8 課:函數呼叫和工具使用 - 工具註冊表、安全執行和輸出驗證

設計工具註冊表、OpenAI/Anthropic 函數呼叫、安全執行沙箱、輸出驗證、工具鏈、錯誤處理、每個工具的速率限制。

🏗️ 建築 — 第 8 課 第 8 課:函數呼叫與工具使用-工具 註冊表、安全執行和輸出 驗證

企業人工智慧聊天機器人平台架構-從原型到生產

第 3 部分:代理架構

亞洲開發網

1. 函數呼叫-將LLM變成動作引擎

函數呼叫啟用LLM 呼叫外部工具/API 而不僅僅是生成文字。這是每個代理商聊天機器人的基礎——從尋找訂單、預約到執行複雜的工作流程。


┌────────────── FUNCTION CALLING FLOW ──────────────────┐
│                                                        │
│  User: "Kiểm tra đơn hàng #12345"                     │
│                  │                                     │
│                  ▼                                     │
│  ┌───────────────────┐                                 │
│  │   LLM decides:    │                                 │
│  │   call tool       │                                 │
│  │   "get_order"     │                                 │
│  │   args: {id:12345}│                                 │
│  └─────────┬─────────┘                                 │
│            │                                           │
│            ▼                                           │
│  ┌───────────────────┐    ┌────────────────────┐       │
│  │  Tool Executor    │───▶│ Order Service API  │       │
│  │  (Sandbox)        │◀───│                    │       │
│  └─────────┬─────────┘    └────────────────────┘       │
│            │                                           │
│            ▼                                           │
│  ┌───────────────────┐                                 │
│  │   LLM formats     │                                 │
│  │   response with   │                                 │
│  │   tool result     │                                 │
│  └───────────────────┘                                 │
│                                                        │
│  Bot: "Đơn hàng #12345 đang được vận chuyển..."       │
└────────────────────────────────────────────────────────┘

2. 工具註冊表設計


interface ToolDefinition {
  name: string;
  description: string;
  category: 'query' | 'action' | 'computation';
  parameters: JSONSchema;           // OpenAI function schema
  requiredPermissions: string[];    // RBAC permissions needed
  rateLimit: { maxCalls: number; windowMs: number };
  timeout: number;                  // Max execution time in ms
  retryPolicy: { maxRetries: number; backoffMs: number };
  dangerLevel: 'safe' | 'moderate' | 'dangerous';
  requiresConfirmation: boolean;    // Ask user before executing?
}

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

  register(tool: ToolDefinition, handler: ToolHandler): void {
    // Validate schema
    this.validateSchema(tool.parameters);

    this.tools.set(tool.name, {
      definition: tool,
      handler,
      metrics: { totalCalls: 0, totalErrors: 0, avgLatencyMs: 0 },
    });
  }

  getToolsForLLM(
    tenantId: string,
    userPermissions: string[],
  ): OpenAIToolDefinition[] {
    return Array.from(this.tools.values())
      .filter(t => this.hasPermission(t.definition, userPermissions))
      .map(t => ({
        type: 'function' as const,
        function: {
          name: t.definition.name,
          description: t.definition.description,
          parameters: t.definition.parameters,
        },
      }));
  }

  private hasPermission(tool: ToolDefinition, permissions: string[]): boolean {
    return tool.requiredPermissions.every(p => permissions.includes(p));
  }
}

// Example tool registrations
registry.register(
  {
    name: 'get_order_status',
    description: 'Get the current status of a customer order by order ID',
    category: 'query',
    parameters: {
      type: 'object',
      properties: {
        order_id: { type: 'string', description: 'The order ID (e.g., ORD-12345)' },
      },
      required: ['order_id'],
    },
    requiredPermissions: ['orders:read'],
    rateLimit: { maxCalls: 10, windowMs: 60_000 },
    timeout: 5_000,
    retryPolicy: { maxRetries: 2, backoffMs: 1000 },
    dangerLevel: 'safe',
    requiresConfirmation: false,
  },
  async (args: { order_id: string }) => {
    const order = await orderService.getOrder(args.order_id);
    return {
      orderId: order.id,
      status: order.status,
      estimatedDelivery: order.estimatedDelivery,
      items: order.items.map(i => ({ name: i.name, quantity: i.quantity })),
    };
  },
);

registry.register(
  {
    name: 'cancel_order',
    description: 'Cancel a customer order. Only works for orders not yet shipped.',
    category: 'action',
    parameters: {
      type: 'object',
      properties: {
        order_id: { type: 'string', description: 'The order ID to cancel' },
        reason: { type: 'string', description: 'Cancellation reason' },
      },
      required: ['order_id', 'reason'],
    },
    requiredPermissions: ['orders:write'],
    rateLimit: { maxCalls: 5, windowMs: 60_000 },
    timeout: 10_000,
    retryPolicy: { maxRetries: 1, backoffMs: 2000 },
    dangerLevel: 'moderate',
    requiresConfirmation: true, // Ask user before cancelling
  },
  async (args: { order_id: string; reason: string }) => {
    return orderService.cancelOrder(args.order_id, args.reason);
  },
);

3. 安全執行沙箱


class ToolExecutor {
  constructor(
    private registry: ToolRegistry,
    private rateLimiter: ToolRateLimiter,
    private auditLog: AuditLogger,
  ) {}

  async execute(
    toolCall: LLMToolCall,
    context: ExecutionContext,
  ): Promise<ToolResult> {
    const tool = this.registry.get(toolCall.name);
    if (!tool) {
      return { success: false, error: `Unknown tool: ${toolCall.name}` };
    }

    // 1. Permission check
    if (!this.hasPermission(tool.definition, context.userPermissions)) {
      return { success: false, error: 'Insufficient permissions' };
    }

    // 2. Rate limit check
    const allowed = await this.rateLimiter.check(
      `${context.tenantId}:${context.userId}:${toolCall.name}`,
      tool.definition.rateLimit,
    );
    if (!allowed) {
      return { success: false, error: 'Rate limit exceeded. Please try again later.' };
    }

    // 3. Input validation
    const validation = this.validateArgs(toolCall.arguments, tool.definition.parameters);
    if (!validation.valid) {
      return { success: false, error: `Invalid arguments: ${validation.errors.join(', ')}` };
    }

    // 4. Sanitize inputs (prevent injection)
    const sanitizedArgs = this.sanitizeArgs(toolCall.arguments);

    // 5. Confirmation check for dangerous actions
    if (tool.definition.requiresConfirmation) {
      return {
        success: true,
        requiresConfirmation: true,
        confirmationMessage: `Bạn có muốn thực hiện "${tool.definition.description}" không?`,
        pendingAction: { toolName: toolCall.name, args: sanitizedArgs },
      };
    }

    // 6. Execute with timeout
    const startTime = Date.now();
    try {
      const result = await this.executeWithTimeout(
        tool.handler,
        sanitizedArgs,
        tool.definition.timeout,
      );

      // 7. Output validation (prevent data leakage)
      const sanitizedResult = this.sanitizeOutput(result, tool.definition);

      // 8. Audit log
      await this.auditLog.log({
        tenantId: context.tenantId,
        userId: context.userId,
        tool: toolCall.name,
        args: sanitizedArgs,
        result: 'success',
        latencyMs: Date.now() - startTime,
      });

      return { success: true, data: sanitizedResult };
    } catch (error) {
      await this.auditLog.log({
        tenantId: context.tenantId,
        userId: context.userId,
        tool: toolCall.name,
        args: sanitizedArgs,
        result: 'error',
        error: error instanceof Error ? error.message : 'Unknown error',
        latencyMs: Date.now() - startTime,
      });

      return { success: false, error: 'Tool execution failed. Please try again.' };
    }
  }

  private async executeWithTimeout<T>(
    handler: ToolHandler,
    args: unknown,
    timeoutMs: number,
  ): Promise<T> {
    return Promise.race([
      handler(args),
      new Promise<never>((_, reject) =>
        setTimeout(() => reject(new Error('Tool execution timed out')), timeoutMs),
      ),
    ]);
  }

  private sanitizeArgs(args: Record<string, unknown>): Record<string, unknown> {
    const sanitized: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(args)) {
      if (typeof value === 'string') {
        // Prevent SQL injection, command injection
        sanitized[key] = value
          .replace(/[;\-\-]/g, '')
          .replace(/['"`]/g, '')
          .trim();
      } else {
        sanitized[key] = value;
      }
    }
    return sanitized;
  }
}

4. 工具鏈-多步驟工具調用


class ToolChainExecutor {
  private maxChainDepth = 5; // Prevent infinite loops

  async executeChain(
    messages: LLMMessage[],
    tools: OpenAIToolDefinition[],
    context: ExecutionContext,
  ): Promise<ChainResult> {
    const toolResults: ToolCallRecord[] = [];
    let depth = 0;

    while (depth < this.maxChainDepth) {
      // Call LLM
      const response = await this.llm.chat({
        messages,
        tools,
        tool_choice: 'auto',
      });

      // If no tool calls, we're done
      if (!response.toolCalls?.length) {
        return { finalResponse: response.content, toolResults };
      }

      // Execute all tool calls (can be parallel)
      const results = await Promise.all(
        response.toolCalls.map(async (tc) => {
          const result = await this.toolExecutor.execute(tc, context);
          return { toolCall: tc, result };
        }),
      );

      // Handle confirmation requests
      const needsConfirmation = results.find(r => r.result.requiresConfirmation);
      if (needsConfirmation) {
        return {
          finalResponse: null,
          toolResults,
          pendingConfirmation: needsConfirmation.result,
        };
      }

      // Append tool results to messages
      messages.push({
        role: 'assistant',
        content: null,
        tool_calls: response.toolCalls.map(tc => ({
          id: tc.id,
          type: 'function',
          function: { name: tc.name, arguments: JSON.stringify(tc.arguments) },
        })),
      });

      for (const { toolCall, result } of results) {
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: JSON.stringify(result.data ?? { error: result.error }),
        });
        toolResults.push({ tool: toolCall.name, args: toolCall.arguments, result });
      }

      depth++;
    }

    return {
      finalResponse: 'Đã đạt giới hạn số bước xử lý. Vui lòng thử lại.',
      toolResults,
    };
  }
}

5. 結構化輸出驗證


import { z } from 'zod';

// Define expected tool output schemas
const OrderStatusSchema = z.object({
  orderId: z.string(),
  status: z.enum(['pending', 'processing', 'shipped', 'delivered', 'cancelled']),
  estimatedDelivery: z.string().datetime().nullable(),
  items: z.array(z.object({
    name: z.string(),
    quantity: z.number().positive(),
  })),
});

class ToolOutputValidator {
  private schemas = new Map<string, z.ZodSchema>();

  register(toolName: string, schema: z.ZodSchema): void {
    this.schemas.set(toolName, schema);
  }

  validate(toolName: string, output: unknown): ValidationResult {
    const schema = this.schemas.get(toolName);
    if (!schema) return { valid: true, data: output };

    const result = schema.safeParse(output);
    if (result.success) {
      return { valid: true, data: result.data };
    }

    return {
      valid: false,
      errors: result.error.errors.map(e => `${e.path.join('.')}: ${e.message}`),
    };
  }
}

第 8 課總結

  • 工具註冊表:管理工具定義、權限、速率限制、危險級別
  • 安全執行:權限檢查→速率限制→輸入驗證→清理→逾時→審核日誌
  • 工具鏈:LLM自動連結多個工具調用,最大深度= 5以避免無限循環
  • 確認:危險操作(取消、刪除)需要使用者確認後才能執行
  • 輸出驗證:在傳回 LLM 之前使用 Zod 模式驗證工具輸出

下一篇: 多代理編排-代理路由、主管模式、切換協定、代理之間的共享記憶體。