ในฐานะนักพัฒนาที่เคยสร้างระบบ 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 ปัญหาที่เจอคือ:
- AI ส่ง parameter ที่ไม่ตรงกับ schema
- Database query timeout แต่ไม่มี retry
- User พิมพ์คำสั่งที่ไม่ครบถ้วน ทำให้ tool call ล้มเหลว
โครงสร้างพื้นฐาน: 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 },
'