ในฐานะนักพัฒนาที่เคยสร้างระบบ AI สำหรับอีคอมเมิร์ซมากกว่า 30 โปรเจกต์ ผมเจอปัญหา Function Calling ซ้ำแล้วซ้ำเล่า — บางที AI ตอบกลับมาผิด format,บางที tool ล้มเหลวแต่ไม่รู้จะ retry อย่างไร และบางที validation ที่ควรจะ catch ได้กลับผ่านไปทำให้ระบบพัง

บทความนี้จะแชร์ patterns ที่พิสูจน์แล้วว่าใช้ได้จริงใน production พร้อมโค้ดตัวอย่างที่ copy-paste ได้เลย

ทำไมการจัดการข้อผิดพลาดของ Function Calling ถึงสำคัญ

Claude Function Calling แตกต่างจาก simple chat completion ตรงที่มันมี "ลูป" ของการเรียกใช้ tool — AI ส่ง request, เราต้อง execute tool แล้วส่งผลลัพธ์กลับไป ถ้าขาดการ validate หรือ retry logic ที่ดี ระบบจะพังเมื่อเจอ edge case

จากประสบการณ์ของผม ระบบ RAG ขององค์กรที่ไม่มี error handling ที่ดีจะมีอาการ hallucination บ่อยกว่า 40% เพราะถ้า retrieval ล้มเหลวแต่ระบบยังถามต่อ AI ก็จะพยายาม "เดา" คำตอบแทน

กรณีศึกษา: ระบบ AI ลูกค้าสัมพันธ์อีคอมเมิร์ซ

ผมเคยพัฒนาระบบที่ต้อง handle การ track order, คำนวณส่วนลด และตอบคำถามสินค้า — ทั้งหมดผ่าน Function Calling ปัญหาที่เจอคือ:

โครงสร้างพื้นฐาน: HolySheep AI Integration

ก่อนจะเข้าเรื่อง error handling มาดูโครงสร้าง client พื้นฐานที่ใช้ สมัครที่นี่ กันก่อน — HolySheep AI ให้บริการ Claude ผ่าน OpenAI-compatible API ที่มี latency ต่ำกว่า 50ms และราคาถูกกว่า 85% เมื่อเทียบกับ direct API

const OpenAI = require('openai');

const client = new OpenAI({
  baseURL: 'https://api.holysheep.ai/v1',
  apiKey: 'YOUR_HOLYSHEEP_API_KEY',
  timeout: 30000,
  maxRetries: 3,
});

// ตัวอย่างการเรียก Claude พร้อม function calling
async function callClaudeWithFunctions(userMessage, tools) {
  try {
    const response = await client.chat.completions.create({
      model: 'claude-sonnet-4-5',
      messages: [{ role: 'user', content: userMessage }],
      tools: tools,
      tool_choice: 'auto',
      max_tokens: 1024,
    });
    
    const message = response.choices[0].message;
    
    // ตรวจสอบว่ามี function call หรือไม่
    if (message.tool_calls && message.tool_calls.length > 0) {
      return {
        type: 'function_call',
        calls: message.tool_calls,
        finishReason: message.finish_reason
      };
    }
    
    return {
      type: 'text',
      content: message.content,
      finishReason: message.finish_reason
    };
    
  } catch (error) {
    throw new FunctionCallingError(error);
  }
}

Pattern 1: Robust Tool Execution Loop

นี่คือ pattern ที่ผมใช้ในทุกโปรเจกต์ — มี retry, validation และ graceful degradation

class FunctionCallingExecutor {
  constructor(client, maxIterations = 10) {
    this.client = client;
    this.maxIterations = maxIterations;
    this.executionHistory = [];
  }

  async execute(messages, tools) {
    let iteration = 0;
    
    while (iteration < this.maxIterations) {
      iteration++;
      
      try {
        // เรียก Claude
        const response = await this.client.chat.completions.create({
          model: 'claude-sonnet-4-5',
          messages: messages,
          tools: tools,
          tool_choice: 'auto',
        });
        
        const assistantMessage = response.choices[0].message;
        messages.push(assistantMessage);
        
        // ถ้าไม่มี tool call แสดงว่าจบแล้ว
        if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
          return {
            success: true,
            finalMessage: assistantMessage.content,
            iterations: iteration,
            history: this.executionHistory
          };
        }
        
        // Execute แต่ละ tool call
        for (const toolCall of assistantMessage.tool_calls) {
          const result = await this.executeToolCall(toolCall);
          messages.push({
            role: 'tool',
            tool_call_id: toolCall.id,
            content: JSON.stringify(result)
          });
          this.executionHistory.push({
            tool: toolCall.function.name,
            args: JSON.parse(toolCall.function.arguments),
            result: result,
            timestamp: Date.now()
          });
        }
        
      } catch (error) {
        // จัดการข้อผิดพลาดแต่ละประเภท
        const errorResult = this.handleError(error, messages, iteration);
        messages.push({
          role: 'system',
          content: Error occurred: ${errorResult.message}. Please ${errorResult.suggestion}
        });
        
        if (!errorResult.retryable) {
          return {
            success: false,
            error: errorResult.message,
            iterations: iteration,
            history: this.executionHistory
          };
        }
      }
    }
    
    return {
      success: false,
      error: 'Max iterations exceeded',
      iterations: iteration,
      history: this.executionHistory
    };
  }

  async executeToolCall(toolCall) {
    const { name, arguments: argsString } = toolCall.function;
    let args;
    
    // Parse และ validate arguments
    try {
      args = JSON.parse(argsString);
    } catch (parseError) {
      throw new ToolExecutionError(
        Invalid JSON in function arguments for ${name},
        'INVALID_ARGUMENTS',
        false
      );
    }
    
    // Validate ด้วย Zod schema
    const validator = this.validators[name];
    if (validator && !validator.safeParse(args).success) {
      throw new ToolExecutionError(
        Validation failed for ${name}: ${JSON.stringify(validator.safeParse(args).error)},
        'VALIDATION_FAILED',
        false
      );
    }
    
    // Execute tool
    const tool = this.tools[name];
    if (!tool) {
      throw new ToolExecutionError(Tool ${name} not found, 'TOOL_NOT_FOUND', false);
    }
    
    try {
      return await tool.execute(args);
    } catch (error) {
      throw new ToolExecutionError(
        Tool ${name} execution failed: ${error.message},
        'TOOL_EXECUTION_FAILED',
        true // retryable
      );
    }
  }

  handleError(error, messages, iteration) {
    // Rate limit — retry with backoff
    if (error.status === 429) {
      return {
        message: error.message,
        suggestion: 'please wait and retry',
        retryable: true,
        backoffMs: Math.min(1000 * Math.pow(2, iteration), 30000)
      };
    }
    
    // Server error — retry
    if (error.status >= 500) {
      return {
        message: error.message,
        suggestion: 'please retry',
        retryable: true,
        backoffMs: 500 * iteration
      };
    }
    
    // Client error — ไม่ retry
    return {
      message: error.message,
      suggestion: 'rephrase your request',
      retryable: false,
      backoffMs: 0
    };
  }
}

Pattern 2: Tool Definition พร้อม Validation

สิ่งที่หลายคนมองข้ามคือการ validate arguments ก่อน execute — ปล่อยให้ AI ส่งอะไรมาก็ execute ตามนั้นเป็นเรื่องอันตราย

const { z } = require('zod');

// กำหนด schema สำหรับแต่ละ function
const functionSchemas = {
  get_order_status: {
    schema: z.object({
      order_id: z.string().regex(/^ORD-\d{8}$/, 'รูปแบบ order ID ไม่ถูกต้อง'),
      include_history: z.boolean().optional().default(false)
    }),
    description: 'ตรวจสอบสถานะคำสั่งซื้อ',
    parameters: {
      type: 'object',
      properties: {
        order_id: {
          type: 'string',
          description: 'หมายเลขคำสั่งซื้อ (รูปแบบ: ORD-XXXXXXXX)'
        },
        include_history: {
          type: 'boolean',
          description: 'รวมประวัติการเปลี่ยนแปลงสถานะ'
        }
      },
      required: ['order_id']
    }
  },
  
  calculate_discount: {
    schema: z.object({
      original_price: z.number().positive('ราคาต้องเป็นบวก'),
      coupon_code: z.string().length(10, 'รหัสคูปองต้อง 10 ตัวอักษร').optional(),
      customer_tier: z.enum(['bronze', 'silver', 'gold', 'platinum'])
    }),
    description: 'คำนวณส่วนลดสำหรับลูกค้า',
    parameters: {
      type: 'object',
      properties: {
        original_price: {
          type: 'number',
          description: 'ราคาเดิม (บาท)'
        },
        coupon_code: {
          type: 'string',
          description: 'รหัสคูปอง (ถ้ามี)'
        },
        customer_tier: {
          type: 'string',
          enum: ['bronze', 'silver', 'gold', 'platinum'],
          description: 'ระดับสมาชิก'
        }
      },
      required: ['original_price', 'customer_tier']
    }
  }
};

// สร้าง tools สำหรับ API call
const tools = Object.entries(functionSchemas).map(([name, config]) => ({
  type: 'function',
  function: {
    name: name,
    description: config.description,
    parameters: config.parameters
  }
}));

// Tool implementations
const toolImplementations = {
  get_order_status: {
    async execute(args) {
      const { order_id, include_history } = args;
      
      // Simulate database query
      const order = await db.orders.findOne({ 
        where: { order_id } 
      });
      
      if (!order) {
        throw new ToolExecutionError(
          ไม่พบคำสั่งซื้อ ${order_id},
          'ORDER_NOT_FOUND',
          false
        );
      }
      
      const response = {
        order_id: order.order_id,
        status: order.status,
        total: order.total,
        items: order.items.length
      };
      
      if (include_history) {
        response.history = await db.orderHistory.findAll({
          where: { order_id },
          order: [['created_at', 'DESC']]
        });
      }
      
      return response;
    }
  },
  
  calculate_discount: {
    async execute(args) {
      const { original_price, coupon_code, customer_tier } = args;
      
      // Base discount จาก tier
      const tierDiscounts = {
        bronze: 0,
        silver: 0.05,
        gold: 0.10,
        platinum: 0.15
      };
      
      let discount = tierDiscounts[customer_tier];
      let appliedCoupons = [];
      
      // ตรวจสอบคูปอง
      if (coupon_code) {
        const coupon = await db.coupons.findOne({
          where: { code: coupon_code, active: true }
        });
        
        if (coupon) {
          discount += coupon.discount_percent / 100;
          appliedCoupons.push(coupon_code);
        }
      }
      
      const finalPrice = Math.round(original_price * (1 - discount) * 100) / 100;
      
      return {
        original_price,
        discount_percent: Math.round(discount * 100),
        discount_amount: Math.round((original_price - finalPrice) * 100) / 100,
        final_price: finalPrice,
        customer_tier,
        applied_coupons: appliedCoupons
      };
    }
  }
};

Pattern 3: Streaming Response พร้อม Error Recovery

สำหรับ use case ที่ต้องการ streaming เช่น chatbot ที่ต้องตอบเร็ว ผมแนะนำให้ handle partial response และ recovery จาก streaming interruption

class StreamingFunctionCaller {
  constructor(client) {
    this.client = client;
  }

  async *streamWithFunctionCalls(messages, tools) {
    const toolCalls = [];
    let textBuffer = '';
    
    try {
      const stream = await this.client.chat.completions.create({
        model: 'claude-sonnet-4-5',
        messages: messages,
        tools: tools,
        stream: true,
        stream_options: { include_usage: true }
      });
      
      for await (const chunk of stream) {
        const delta = chunk.choices[0]?.delta;
        
        if (!delta) continue;
        
        // Handle text content
        if (delta.content) {
          textBuffer += delta.content;
          yield {
            type: 'text',
            content: delta.content,
            partial: true
          };
        }
        
        // Handle tool calls
        if (delta.tool_calls) {
          for (const toolCallDelta of delta.tool_calls) {
            const index = toolCallDelta.index;
            
            // Initialize ถ้ายังไม่มี
            while (toolCalls.length <= index) {
              toolCalls.push({
                id: '',
                type: 'function',
                function: { name: '', arguments: '' }
              });
            }
            
            // Update fields
            if (toolCallDelta.id) {
              toolCalls[index].id = toolCallDelta.id;
            }
            if (toolCallDelta.function?.name) {
              toolCalls[index].function.name += toolCallDelta.function.name;
            }
            if (toolCallDelta.function?.arguments) {
              toolCalls[index].function.arguments += toolCallDelta.function.arguments;
            }
            
            yield {
              type: 'tool_call_start',
              index: index,
              name: toolCalls[index].function.name
            };
          }
        }
      }
      
      // ส่ง final tool calls
      if (toolCalls.length > 0) {
        yield {
          type: 'tool_calls_complete',
          calls: toolCalls,
          fullText: textBuffer
        };
      }
      
    } catch (error) {
      // Handle streaming error
      if (error.code === 'RESPONSE_FORMAT_ERROR') {
        yield {
          type: 'error',
          message: 'AI returned malformed response',
          recoverable: true,
          partialText: textBuffer
        };
      } else if (error.code === 'CONNECTION_ERROR') {
        yield {
          type: 'error',
          message: 'Connection lost, attempting to resume...',
          recoverable: true,
          partialText: textBuffer,
          retryAt: Date.now() + 2000
        };
      } else {
        yield {
          type: 'error',
          message: error.message,
          recoverable: false,
          partialText: textBuffer
        };
      }
    }
  }

  // Helper: execute tools and stream results
  async *executeToolsAndStream(toolCalls, toolImplementations) {
    for (const call of toolCalls) {
      yield {
        type: 'tool_start',
        name: call.function.name,
        arguments: call.function.arguments
      };
      
      try {
        const tool = toolImplementations[call.function.name];
        if (!tool) {
          throw new Error(Tool ${call.function.name} not found);
        }
        
        const args = JSON.parse(call.function.arguments);
        const result = await tool.execute(args);
        
        yield {
          type: 'tool_complete',
          name: call.function.name,
          result: result
        };
        
      } catch (error) {
        yield {
          type: 'tool_error',
          name: call.function.name,
          error: error.message
        };
        
        // แจ้งให้ AI จัดการกับ error
        yield {
          type: 'error_context',
          message: The tool ${call.function.name} failed: ${error.message}. Please provide an appropriate response to the user.
        };
      }
    }
  }
}

กรณีศึกษา: ระบบ RAG องค์กร

สำหรับองค์กรที่ใช้ RAG (Retrieval Augmented Generation) ผมเจอปัญหาหลักคือ retrieval บางครั้งดึง context ที่ไม่เกี่ยวข้อง หรือ document ถูกลบไปแล้วแต่ index ยังชี้ไปที่นั้น

วิธีแก้คือต้องมี validation layer ที่ตรวจสอบ retrieved context ก่อนส่งให้ AI

class RAGFunctionCalling {
  constructor(embeddings, vectorDB, llm) {
    this.embeddings = embeddings;
    this.vectorDB = vectorDB;
    this.llm = llm;
  }

  async queryWithContext(userQuery, topK = 5) {
    // 1. Embed query
    const queryEmbedding = await this.embeddings.embed(userQuery);
    
    // 2. Retrieve candidates
    const candidates = await this.vectorDB.search(
      queryEmbedding,
      topK * 2 // ดึงมากกว่าที่ต้องการเผื่อ filter
    );
    
    // 3. Validate retrieved documents
    const validatedDocs = [];
    const errors = [];
    
    for (const doc of candidates) {
      // ตรวจสอบว่า document ยังมีอยู่จริง
      if (doc.deleted_at) {
        errors.push({
          docId: doc.id,
          error: 'Document was deleted',
          action: 'removed_from_index'
        });
        continue;
      }
      
      // ตรวจสอบ relevance score
      if (doc.score < 0.7) {
        errors.push({
          docId: doc.id,
          error: 'Low relevance score',
          score: doc.score,
          action: 'filtered_out'
        });
        continue;
      }
      
      // ตรวจสอบ freshness
      const daysSinceUpdate = (Date.now() - doc.updated_at) / (1000 * 60 * 60 * 24);
      if (daysSinceUpdate > 90) {
        errors.push({
          docId: doc.id,
          error: 'Document may be outdated',
          daysSinceUpdate,
          action: 'marked_stale'
        });
      }
      
      validatedDocs.push(doc);
      
      if (validatedDocs.length >= topK) break;
    }
    
    // 4. Log retrieval metrics
    console.log({
      query: userQuery,
      candidatesFound: candidates.length,
      validatedCount: validatedDocs.length,
      errors: errors,
      retrievalRate: validatedDocs.length / candidates.length
    });
    
    // 5. Build context
    const context = validatedDocs
      .map(doc => [Source: ${doc.title}]\n${doc.content})
      .join('\n\n---\n\n');
    
    // 6. Fallback ถ้าไม่พบ docs ที่ดีพอ
    if (validatedDocs.length === 0) {
      return {
        answer: null,
        error: 'No relevant documents found',
        suggestions: [
          'Try rephrasing your query',
          'Contact support for manual assistance'
        ],
        retrievalMetrics: { errors }
      };
    }
    
    // 7. Generate answer
    const messages = [
      {
        role: 'system',
        content: `You are a helpful assistant. Use the following context to answer the user's question. If the context doesn't contain enough information, say so clearly.

IMPORTANT: 
- Only use information from the provided context
- If you're uncertain about something, acknowledge it
- Reference the source document when making specific claims`
      },
      {
        role: 'user',
        content: Context:\n${context}\n\nQuestion: ${userQuery}
      }
    ];
    
    const response = await this.llm.chat.completions.create({
      model: 'claude-sonnet-4-5',
      messages: messages,
      tools: [
        {
          type: 'function',
          function: {
            name: 'cite_source',
            description: 'อ้างอิงแหล่งข้อมูลในคำตอบ',
            parameters: {
              type: 'object',
              properties: {
                source_id: { type: 'string' },
                quote: { type: 'string' },
                relevance: { type: 'string' }
              },
              required: ['source_id', 'quote']
            }
          }
        },
        {
          type: 'function',
          function: {
            name: 'flag_uncertainty',
            description: 'แจ้งว่าข้อมูลบางส่วนไม่แน่นอน',
            parameters: {
              type: 'object',
              properties: {
                uncertain_parts: { type: 'array' },
                confidence: { type: 'number', minimum: 0, maximum: 1 }
              },
              required: ['uncertain_parts']
            }
          }
        }
      ],
      tool_choice: 'auto'
    });
    
    return {
      answer: response.choices[0].message.content,
      toolCalls: response.choices[0].message.tool_calls,
      sources: validatedDocs.map(d => ({
        id: d.id,
        title: d.title,
        score: d.score
      })),
      retrievalMetrics: {
        totalCandidates: candidates.length,
        validated: validatedDocs.length,
        errors,
        avgScore: validatedDocs.reduce((a, b) => a + b.score, 0) / validatedDocs.length
      }
    };
  }
}

ข้อผิดพลาดที่พบบ่อยและวิธีแก้ไข

1. Error: "Invalid tool_calls format" — มักเกิดจากการ parse JSON ผิด

สาเหตุ: Claude บางครั้งส่ง JSON ที่ไม่ valid มา โดยเฉพาะเมื่อ arguments มี nested quotes

// ❌ วิธีที่ผิด — ใช้ JSON.parse โดยตรง
const args = JSON.parse(toolCall.function.arguments);

// ✅ วิธีที่ถูก — มี fallback
function safeParseArguments(argsString) {
  try {
    return { success: true, data: JSON.parse(argsString) };
  } catch (firstError) {
    // ลอง sanitize ก่อน
    try {
      // แก้ปัญหา escaped quotes
      const sanitized = argsString
        .replace(/\\'/g, "'")
        .replace(/\\"/g, '"')
        .replace(/\\\\/g, '\\');
      return { success: true, data: JSON.parse(sanitized) };
    } catch (secondError) {
      // สุดท้ายลองใช้ Function constructor
      try {
        return { success: true, data: (new Function(return ${argsString}))() };
      } catch (thirdError) {
        return { 
          success: false, 
          error: Parse failed: ${firstError.message},
          raw: argsString
        };
      }
    }
  }
}

// ใช้งาน
const result = safeParseArguments(toolCall.function.arguments);
if (!result.success) {
  throw new ToolExecutionError(
    Cannot parse arguments for ${toolCall.function.name}: ${result.error},
    'PARSE_ERROR',
    false
  );
}

2. Error: "Model does not support tools" — มักเกิดจาก model name ผิด

สาเหตุ: ใช้ model ที่ไม่รองรับ function calling หรือพิมพ์ชื่อผิด

// ❌ วิธีที่ผิด — hardcode model name
const response = await client.chat.completions.create({
  model: 'gpt-4', // GPT-4 เวอร์ชันเก่าไม่รองรับ tools
  // ...
});

// ✅ วิธีที่ถูก — validate model ก่อนใช้งาน
const SUPPORTED_MODELS = {
  'claude-sonnet-4-5': { tools: true, maxTokens: 8192 },
  'claude-opus-4': { tools: true, maxTokens: 8192 },
  'gpt-4o': { tools: true, maxTokens: 16384 },
  '