Nếu bạn đang dùng Claude qua web UI và muốn tích hợp nó vào ứng dụng .NET — tự động generate content, xây chatbot, phân tích document, hay tạo AI assistant cho hệ thống quản lý — bài viết này dành cho bạn.
Anthropic đã release official C# SDK (package Anthropic trên NuGet, version 10+). Trước đó community phải dùng unofficial SDK hoặc gọi REST API thủ công. Giờ có SDK chính thức, mọi thứ đơn giản hơn nhiều — nhưng vẫn có những gotcha mà docs không nói rõ.
Bài viết này đi từ setup cơ bản đến streaming, tool use, tích hợp với Dependency Injection trong ASP.NET Core, và những pattern mình dùng trong dự án thực tế.
Cài đặt và gọi API đầu tiên
Cài package
dotnet add package Anthropic
Package Anthropic (version 10+) là official SDK từ Anthropic. Yêu cầu .NET Standard 2.0 trở lên — nghĩa là chạy được trên .NET 6, 7, 8 và mới hơn.
Lưu ý quan trọng: trên NuGet có package Anthropic.SDK (unofficial) — đây là SDK cộng đồng, vẫn hoạt động tốt nhưng khác với official SDK. Bài này mình dùng official Anthropic package.
Lấy API Key
Vào console.anthropic.com → API Keys → Create Key. Copy key và lưu an toàn — key chỉ hiện một lần.
Hello Claude — Request đầu tiên
using Anthropic;
using Anthropic.Models.Messages;
// Cách 1: Đọc API key từ environment variable ANTHROPIC_API_KEY
AnthropicClient client = new();
// Cách 2: Truyền API key trực tiếp (không khuyến khích cho production)
// AnthropicClient client = new(new() { ApiKey = "sk-ant-..." });
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 1024,
Messages =
[
new()
{
Role = Role.User,
Content = "Giải thích SOLID principles cho junior developer, " +
"mỗi principle một câu ngắn gọn."
}
]
};
var response = await client.Messages.Create(parameters);
// Lấy text từ response
var text = response.Content
.OfType<TextBlock>()
.FirstOrDefault()?.Text;
Console.WriteLine(text);
Chạy thử. Nếu thấy Claude trả lời — bạn đã kết nối thành công. Nếu lỗi 401 Unauthorized — kiểm tra API key. Nếu timeout — có thể network block API endpoint.
Models — Chọn đúng model cho đúng việc
Anthropic hiện có ba dòng model chính:
| Model | Model String | Input/Output (per 1M tokens) | Khi nào dùng |
|---|---|---|---|
| Haiku 4.5 | claude-haiku-4-5-20251001 | $0.80 / $4 | Task đơn giản, tốc độ cao, chi phí thấp |
| Sonnet 4.5 | claude-sonnet-4-5-20250929 | $3 / $15 | Cân bằng giữa chất lượng và chi phí |
| Opus 4.6 | claude-opus-4-6 | $5 / $25 | Task phức tạp, cần chất lượng cao nhất |
Trong dự án mình, pattern thường dùng: Haiku cho classification/extraction (nhanh, rẻ), Sonnet cho generate content và code (cân bằng nhất), Opus cho task cần reasoning phức tạp (phân tích kiến trúc, review code dài).
SDK có enum Model với các constant:
// Dùng enum
Model = Model.ClaudeSonnet4_5_20250929,
// Hoặc string trực tiếp
Model = "claude-sonnet-4-5-20250929",
Mình hay dùng string vì dễ config qua appsettings.json — đổi model chỉ cần đổi config, không cần rebuild.
System Prompt — Định hướng Claude
System prompt định nghĩa "nhân cách" và hành vi của Claude cho toàn bộ conversation:
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 2048,
System = "Bạn là assistant hỗ trợ kỹ thuật cho hệ thống quản lý " +
"kho lốp xe. Trả lời bằng tiếng Việt. Khi được hỏi về " +
"sản phẩm, luôn bao gồm thông tin: tên lốp, kích thước, " +
"mùa (summer/winter/all-season), và giá nếu có.",
Messages =
[
new()
{
Role = Role.User,
Content = "Khách hỏi về lốp Michelin cho xe sedan, " +
"ngân sách khoảng 2-3 triệu/chiếc."
}
]
};
System prompt không bắt buộc nhưng rất nên dùng — nó giúp Claude trả lời consistent và đúng context.
Multi-turn Conversation — Hội thoại nhiều lượt
API của Anthropic là stateless — nó không nhớ message trước. Bạn phải gửi toàn bộ lịch sử hội thoại mỗi lần gọi:
var messages = new List<MessageParam>
{
new() { Role = Role.User, Content = "Tôi cần tìm lốp xe cho Toyota Camry 2023." },
new() { Role = Role.Assistant, Content = "Toyota Camry 2023 thường dùng kích thước 235/45R18..." },
new() { Role = Role.User, Content = "Có loại all-season nào trong khoảng 3 triệu không?" }
};
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 1024,
Messages = messages
};
var response = await client.Messages.Create(parameters);
// Thêm response vào history cho lượt tiếp theo
messages.Add(new MessageParam
{
Role = Role.Assistant,
Content = response.Content.OfType<TextBlock>().First().Text
});
Mỗi message trong history tốn input token — conversation càng dài, chi phí càng cao. Với chatbot, mình thường giữ tối đa 20-30 message gần nhất, cắt bớt message cũ khi quá dài.
Streaming — Nhận response realtime
Với response dài (generate bài viết, phân tích document), user phải chờ cho đến khi Claude xong toàn bộ mới thấy kết quả. Streaming giải quyết vấn đề này — response được gửi từng chunk, user thấy text xuất hiện dần.
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 4096,
Messages =
[
new()
{
Role = Role.User,
Content = "Viết hướng dẫn setup Docker cho dự án " +
"ASP.NET Core + PostgreSQL."
}
]
};
await foreach (var chunk in client.Messages.CreateStreaming(parameters))
{
// Mỗi chunk là một phần nhỏ của response
Console.Write(chunk);
}
Streaming quan trọng ở hai điểm. Một là UX tốt hơn nhiều — user thấy text ngay lập tức thay vì loading 10-15 giây. Hai là SDK sẽ throw error nếu non-streaming request dự kiến chạy quá 10 phút — nên với task dài, streaming gần như bắt buộc.
Streaming trong ASP.NET Core API
Nếu bạn xây API cho frontend consume:
[HttpPost("chat/stream")]
public async Task StreamChat(
[FromBody] ChatRequest request,
CancellationToken cancellationToken)
{
Response.ContentType = "text/event-stream";
Response.Headers.CacheControl = "no-cache";
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 4096,
System = _systemPrompt,
Messages = request.Messages.Select(m => new MessageParam
{
Role = m.Role == "user" ? Role.User : Role.Assistant,
Content = m.Content
}).ToList()
};
await foreach (var chunk in client.Messages.CreateStreaming(parameters))
{
if (cancellationToken.IsCancellationRequested) break;
await Response.WriteAsync($"data: {chunk}\n\n", cancellationToken);
await Response.Body.FlushAsync(cancellationToken);
}
}
Frontend dùng EventSource hoặc fetch với ReadableStream để nhận SSE events.
Tool Use — Cho Claude gọi function của bạn
Đây là feature mạnh nhất và cũng phức tạp nhất. Tool use cho phép Claude gọi function trong code C# của bạn — ví dụ query database, gọi external API, hoặc thực hiện calculation.
Định nghĩa tool
using Anthropic.Models.Messages;
var searchProductsTool = new Tool
{
Name = "search_products",
Description = "Tìm kiếm sản phẩm lốp xe theo tiêu chí. " +
"Dùng khi user hỏi về sản phẩm cụ thể.",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["brand"] = JsonDocument.Parse("""
{
"type": "string",
"description": "Hãng lốp: Michelin, Bridgestone, Continental..."
}
""").RootElement,
["size"] = JsonDocument.Parse("""
{
"type": "string",
"description": "Kích thước lốp, ví dụ: 205/55R16"
}
""").RootElement,
["max_price"] = JsonDocument.Parse("""
{
"type": "number",
"description": "Giá tối đa (VNĐ)"
}
""").RootElement
},
Required = ["brand"]
}
};
Gửi request với tool
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 1024,
Tools = [searchProductsTool],
Messages =
[
new()
{
Role = Role.User,
Content = "Tìm lốp Michelin 205/55R16 dưới 3 triệu"
}
]
};
var response = await client.Messages.Create(parameters);
Xử lý tool call
Khi Claude quyết định dùng tool, response chứa ToolUseBlock thay vì TextBlock:
foreach (var block in response.Content)
{
if (block is ToolUseBlock toolUse)
{
Console.WriteLine($"Claude muốn gọi: {toolUse.Name}");
Console.WriteLine($"Với params: {toolUse.Input}");
// Thực thi tool và trả kết quả về cho Claude
var toolResult = await ExecuteTool(toolUse.Name, toolUse.Input);
// Gửi kết quả tool về cho Claude để generate response cuối
var followUp = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 1024,
Tools = [searchProductsTool],
Messages =
[
// Message gốc
new() { Role = Role.User, Content = "Tìm lốp Michelin..." },
// Response của Claude (chứa tool_use)
new() { Role = Role.Assistant, Content = response.Content },
// Kết quả tool
new()
{
Role = Role.User,
Content = new List<ContentBlock>
{
new ToolResultBlock
{
ToolUseId = toolUse.Id,
Content = toolResult
}
}
}
]
};
var finalResponse = await client.Messages.Create(followUp);
// Claude sẽ dùng data từ tool để trả lời user
}
}
Implement tool handler
private async Task<string> ExecuteTool(string name, JsonElement input)
{
return name switch
{
"search_products" => await SearchProducts(input),
"get_inventory" => await GetInventory(input),
_ => JsonSerializer.Serialize(new { error = $"Unknown tool: {name}" })
};
}
private async Task<string> SearchProducts(JsonElement input)
{
var brand = input.GetProperty("brand").GetString();
var size = input.TryGetProperty("size", out var s) ? s.GetString() : null;
var maxPrice = input.TryGetProperty("max_price", out var p)
? p.GetDecimal() : (decimal?)null;
// Query database thật
var query = _context.Products
.Where(p => p.Brand == brand);
if (!string.IsNullOrEmpty(size))
query = query.Where(p => p.Size == size);
if (maxPrice.HasValue)
query = query.Where(p => p.Price <= maxPrice.Value);
var products = await query
.Take(5)
.Select(p => new
{
p.Name, p.Brand, p.Size, p.Season,
Price = p.Price.ToString("N0") + " VNĐ"
})
.ToListAsync();
return JsonSerializer.Serialize(products);
}
Flow hoàn chỉnh: User hỏi → Claude phân tích cần tool nào → gọi tool với params phù hợp → bạn thực thi tool (query DB) → trả kết quả cho Claude → Claude format response đẹp cho user. Toàn bộ 2 API calls, nhưng user chỉ thấy một câu trả lời hoàn chỉnh.
Dependency Injection trong ASP.NET Core
Trong dự án thật, bạn không new AnthropicClient() mọi nơi. Dùng DI để quản lý client lifecycle:
Đăng ký service
// Program.cs hoặc Startup.cs
builder.Services.AddSingleton<AnthropicClient>(_ =>
{
return new AnthropicClient(new()
{
ApiKey = builder.Configuration["Anthropic:ApiKey"]
});
});
// Hoặc wrap trong service của riêng bạn
builder.Services.AddScoped<IAiAssistantService, AiAssistantService>();
Tạo service wrapper
public interface IAiAssistantService
{
Task<string> AskAsync(string question, CancellationToken ct = default);
IAsyncEnumerable<string> AskStreamingAsync(string question,
CancellationToken ct = default);
Task<string> AnalyzeDocumentAsync(string content,
CancellationToken ct = default);
}
public class AiAssistantService : IAiAssistantService
{
private readonly AnthropicClient _client;
private readonly string _model;
private readonly string _systemPrompt;
public AiAssistantService(
AnthropicClient client,
IConfiguration config)
{
_client = client;
_model = config["Anthropic:Model"] ?? "claude-sonnet-4-5-20250929";
_systemPrompt = config["Anthropic:SystemPrompt"]
?? "Bạn là assistant hỗ trợ kỹ thuật.";
}
public async Task<string> AskAsync(
string question, CancellationToken ct = default)
{
var parameters = new MessageCreateParams
{
Model = _model,
MaxTokens = 2048,
System = _systemPrompt,
Messages =
[
new() { Role = Role.User, Content = question }
]
};
var response = await _client.Messages.Create(parameters);
return response.Content
.OfType<TextBlock>()
.FirstOrDefault()?.Text ?? string.Empty;
}
public async IAsyncEnumerable<string> AskStreamingAsync(
string question,
[EnumeratorCancellation] CancellationToken ct = default)
{
var parameters = new MessageCreateParams
{
Model = _model,
MaxTokens = 4096,
System = _systemPrompt,
Messages =
[
new() { Role = Role.User, Content = question }
]
};
await foreach (var chunk in client.Messages
.CreateStreaming(parameters).WithCancellation(ct))
{
yield return chunk.ToString();
}
}
public async Task<string> AnalyzeDocumentAsync(
string content, CancellationToken ct = default)
{
var parameters = new MessageCreateParams
{
Model = _model,
MaxTokens = 4096,
System = "Phân tích document được cung cấp. " +
"Trích xuất thông tin quan trọng, " +
"tóm tắt nội dung chính.",
Messages =
[
new()
{
Role = Role.User,
Content = $"Phân tích document sau:\n\n{content}"
}
]
};
var response = await _client.Messages.Create(parameters);
return response.Content
.OfType<TextBlock>()
.FirstOrDefault()?.Text ?? string.Empty;
}
}
Config trong appsettings.json
{
"Anthropic": {
"ApiKey": "sk-ant-...",
"Model": "claude-sonnet-4-5-20250929",
"SystemPrompt": "Bạn là assistant hỗ trợ hệ thống quản lý kho lốp xe."
}
}
Trong production, API key nên nằm trong User Secrets, Azure Key Vault, hoặc environment variable — không bao giờ commit vào git.
Error Handling — Xử lý lỗi đúng cách
API có thể fail vì nhiều lý do: rate limit, network, invalid request. SDK cung cấp exception hierarchy rõ ràng:
try
{
var response = await _client.Messages.Create(parameters);
}
catch (Anthropic4xxException ex) when (ex is AnthropicRateLimitException)
{
// 429 — Rate limit. Đợi rồi retry.
var retryAfter = ex.Headers?.RetryAfter ?? TimeSpan.FromSeconds(30);
_logger.LogWarning("Rate limited. Retry after {Seconds}s",
retryAfter.TotalSeconds);
await Task.Delay(retryAfter);
// Retry...
}
catch (Anthropic4xxException ex)
{
// 400, 401, 403, etc. — Client error
_logger.LogError(ex, "API client error: {Message}", ex.Message);
throw;
}
catch (AnthropicApiException ex)
{
// 500+ — Server error. Có thể retry.
_logger.LogError(ex, "API server error: {Status}", ex.StatusCode);
}
catch (AnthropicIOException ex)
{
// Network error
_logger.LogError(ex, "Network error calling Anthropic API");
}
Retry pattern với Polly
Với production, mình dùng Polly để retry tự động:
builder.Services.AddSingleton<AnthropicClient>(sp =>
{
return new AnthropicClient(new()
{
ApiKey = builder.Configuration["Anthropic:ApiKey"],
MaxRetries = 3 // SDK có built-in retry
});
});
SDK đã có built-in retry cho transient errors. Nếu cần custom hơn (circuit breaker, fallback model), thì wrap trong Polly.
Gửi hình ảnh — Vision API
Claude hỗ trợ phân tích hình ảnh — hữu ích cho OCR, phân loại ảnh sản phẩm, hay đọc invoice scan:
// Đọc ảnh từ file
var imageBytes = await File.ReadAllBytesAsync("invoice-scan.jpg");
var base64Image = Convert.ToBase64String(imageBytes);
var parameters = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 2048,
Messages =
[
new()
{
Role = Role.User,
Content = new List<ContentBlock>
{
new ImageBlock
{
Source = new ImageBlockSource
{
Type = "base64",
MediaType = "image/jpeg",
Data = base64Image
}
},
new TextBlock
{
Text = "Đọc invoice này và trích xuất: " +
"số invoice, ngày, tên khách hàng, " +
"danh sách sản phẩm, tổng tiền. " +
"Trả về dạng JSON."
}
}
}
]
};
Mình dùng pattern này để build tính năng upload invoice ảnh → auto extract data → pre-fill form cho user.
Tối ưu chi phí
API tính tiền theo token — cả input lẫn output. Một vài tips giảm chi phí:
Chọn model phù hợp
Không phải task nào cũng cần Sonnet. Phân loại email? Haiku đủ rồi, rẻ hơn gần 4 lần. Chỉ dùng Sonnet/Opus cho task thực sự cần reasoning phức tạp.
// Phân loại — dùng Haiku (rẻ, nhanh)
var classifyParams = new MessageCreateParams
{
Model = "claude-haiku-4-5-20251001",
MaxTokens = 50, // Classification chỉ cần vài token output
Messages = [new() { Role = Role.User, Content = "..." }]
};
// Generate nội dung — dùng Sonnet
var generateParams = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929",
MaxTokens = 4096,
Messages = [new() { Role = Role.User, Content = "..." }]
};
Set MaxTokens hợp lý
MaxTokens giới hạn output — không phải bạn luôn phải dùng hết. Classification? 50 tokens. Tóm tắt? 500. Generate article? 4096. Đặt đúng giúp response nhanh hơn và rẻ hơn.
System prompt ngắn gọn
System prompt cũng tốn input token. Viết đủ ý, đừng lặp lại. System prompt 200 token × 1000 requests/ngày = 200K token/ngày chỉ cho system prompt.
Prompt caching
Với request lặp lại cùng system prompt hoặc context dài, prompt caching giảm đáng kể chi phí — cache hit chỉ tốn 10% so với input thường. SDK hỗ trợ tự động cache CLAUDE.md và system prompt.
Pattern thực tế: AI-powered search
Kết hợp tất cả lại — đây là pattern mình dùng cho tính năng "hỏi đáp thông minh" về sản phẩm:
public class SmartProductSearchService
{
private readonly AnthropicClient _client;
private readonly AppDbContext _context;
private readonly ILogger<SmartProductSearchService> _logger;
public async Task<SmartSearchResult> SearchAsync(string userQuery)
{
// Bước 1: Dùng Claude để extract search criteria từ câu hỏi
var extractParams = new MessageCreateParams
{
Model = "claude-haiku-4-5-20251001", // Haiku cho extraction
MaxTokens = 200,
System = "Extract search criteria from user query. " +
"Return JSON with: brand, size, season, " +
"min_price, max_price. Null for unknown fields.",
Messages = [new() { Role = Role.User, Content = userQuery }]
};
var extraction = await _client.Messages.Create(extractParams);
var criteria = ParseCriteria(extraction);
// Bước 2: Query database với criteria
var products = await QueryProducts(criteria);
// Bước 3: Dùng Claude để format response thân thiện
var formatParams = new MessageCreateParams
{
Model = "claude-sonnet-4-5-20250929", // Sonnet cho generate
MaxTokens = 1024,
System = "Bạn là tư vấn viên lốp xe. Dựa trên data sản phẩm, " +
"tư vấn cho khách hàng bằng tiếng Việt. " +
"Ngắn gọn, thực tế, có so sánh nếu cần.",
Messages =
[
new()
{
Role = Role.User,
Content = $"Khách hỏi: {userQuery}\n\n" +
$"Sản phẩm tìm được:\n{JsonSerializer.Serialize(products)}"
}
]
};
var answer = await _client.Messages.Create(formatParams);
return new SmartSearchResult
{
Answer = answer.Content.OfType<TextBlock>().First().Text,
Products = products
};
}
}
Pattern này: Haiku extract intent (rẻ) → query DB (free) → Sonnet generate answer (chất lượng). Tối ưu cả chi phí lẫn chất lượng.
Kết luận
Anthropic API từ C# giờ đã dễ hơn nhiều với official SDK. Những điều quan trọng nhất cần nhớ: dùng streaming cho response dài (cả UX lẫn kỹ thuật), chọn model theo task (Haiku cho simple, Sonnet cho balanced, Opus cho complex), wrap client trong DI service thay vì new trực tiếp, và handle error đầy đủ vì network call nào cũng có thể fail.
Tool use là feature mạnh nhất — nó biến Claude từ "chatbot trả lời câu hỏi" thành "AI assistant tương tác với hệ thống thật". Kết hợp tool use với database query, bạn có thể xây những feature tưởng như phức tạp (smart search, auto-fill form, document analysis) chỉ với vài trăm dòng code.
Tài liệu chính thức của Anthropic API: docs.anthropic.com. SDK GitHub: github.com/anthropics/anthropic-sdk-csharp.
Leave a comment
Your email address will not be published. Required fields are marked *