OpenAI と Anthropic API を Web アプリに統合する
言語モデル API 統合は最も需要の高いスキルの 1 つです 現代のWeb開発では。チャットボットの構築であっても、アシスタントの構築であっても、 コンテンツ生成またはテキスト分析システム、コミュニケーション能力 API を使用して効果的に OpenAI e 人間的 それは基本的なことです。
このシリーズの 4 番目の記事では、 Web 開発者のための AI、詳しく調べていきます 両方の API を完全な Web アプリケーションに統合する方法。設定から始めます 最初に、メッセージ構造を分析し、応答ストリーミングを実装します。 そして、API キーを保護するプロキシ パターンを使用して安全なアーキテクチャを構築します。
何を学ぶか
- OpenAI および Anthropic API を使用して構成および認証する
- 2 つのメッセージ システムの構造的な違いを理解する
- リアルタイム エクスペリエンスのために応答ストリーミングを実装する
- エラー、再試行、レート制限を強力に処理します
- トークンをカウントし、使用コストを最適化する
- API キーを保護するために安全なバックエンド プロキシを構築する
- ストリーミングをサポートする完全なチャット インターフェイスを作成する
シリーズ概要
| # | アイテム | 集中 |
|---|---|---|
| 1 | RAG の概要 | 基本的な概念 |
| 2 | TypeScript と LangChain を使用した RAG | 実用化 |
| 3 | ベクトルデータベースの比較 | Chromadb、松ぼっくり、Weaviate |
| 4 | OpenAI と Anthropic API (ここにいます) | API統合 |
| 5 | オラマとLLMローカル | クラウドを使わないAI |
| 6 | 微調整と RAG の比較 | いつ何を使うか |
| 7 | AIエージェント | エージェントベースのアーキテクチャ |
| 8 | CI/CD における AI | インテリジェントな自動化 |
1. APIの設定と認証
最初のリクエストを送信する前に、ログイン認証情報を取得する必要があります 開発環境を正しく構成します。どちらのプロバイダーにも、 安全に管理する必要がある API キー。
APIキーの取得
のために OpenAIに登録してください platform.openai.com そしてキーを生成します
「API キー」セクションで。のために 人間的、次の場所でコンソールにアクセスします。
console.anthropic.com 設定パネルで API キーを作成します。
APIキーのセキュリティ
API キーをソース コードまたはコミットされたファイルに直接含めないでください。 Git 上で。常に環境変数またはシークレット管理サービスを使用してください。 キーが公開されると、予期せぬコストが発生し、セキュリティが侵害される可能性があります。
# File .env (mai committare su Git!)
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx
# Configurazione opzionale
OPENAI_ORG_ID=org-xxxxxxxxxxxxxxxx
OPENAI_MODEL=gpt-4o
ANTHROPIC_MODEL=claude-sonnet-4-20250514
SDKのインストール
どちらのプロバイダーも、簡単に実行できる TypeScript/JavaScript 用の公式 SDK を提供しています。 大幅に統合。 SDK はシリアル化を自動的に処理します。 基本的な逆シリアル化と再試行。
# SDK ufficiali
npm install openai @anthropic-ai/sdk
# Gestione variabili d'ambiente
npm install dotenv
# Per il server proxy Express
npm install express cors
npm install -D @types/express @types/cors
クライアントの初期化
// src/lib/ai-clients.ts
import OpenAI from 'openai';
import Anthropic from '@anthropic-ai/sdk';
import dotenv from 'dotenv';
dotenv.config();
// Client OpenAI
export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID, // opzionale
});
// Client Anthropic
export const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
// Verifica configurazione
export function validateConfig(): boolean {
if (!process.env.OPENAI_API_KEY) {
console.error('OPENAI_API_KEY non configurata');
return false;
}
if (!process.env.ANTHROPIC_API_KEY) {
console.error('ANTHROPIC_API_KEY non configurata');
return false;
}
return true;
}
2. メッセージ構造の比較
どちらの API も会話内のメッセージの概念で動作しますが、 構造が大きく異なります。これらの違いを理解する 統一された抽象概念を構築するために不可欠です。
構造の比較
| 特性 | OpenAI | 人間的 |
|---|---|---|
| メッセージの役割 | システム、ユーザー、アシスタント、ツール | ユーザー、アシスタント(別システム) |
| システムプロンプト | メッセージの中で | 専用パラメータ system |
| モデル | model: "gpt-4o" | model: "claude-sonnet-4-20250514" |
| 最大出力 | max_tokens (オプション) | max_tokens (必須) |
| 温度 | 0.0 ~ 2.0 (デフォルトは 1.0) | 0.0 ~ 1.0 (デフォルトは 1.0) |
| ストリーミング | サーバー送信イベント | サーバー送信イベント |
| エンドポイント | /v1/チャット/コンプリート | /v1/メッセージ |
OpenAIでリクエスト
// src/lib/openai-chat.ts
import { openai } from './ai-clients';
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
export async function chatWithOpenAI(
messages: ChatMessage[],
model: string = 'gpt-4o'
): Promise<string> {
const response = await openai.chat.completions.create({
model,
messages,
temperature: 0.7,
max_tokens: 2048,
});
return response.choices[0].message.content ?? '';
}
// Esempio di utilizzo
const risposta = await chatWithOpenAI([
{ role: 'system', content: 'Sei un assistente tecnico esperto.' },
{ role: 'user', content: 'Spiega il pattern Observer in TypeScript.' },
]);
console.log(risposta);
Anthropic へのリクエスト
// src/lib/anthropic-chat.ts
import { anthropic } from './ai-clients';
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
export async function chatWithAnthropic(
messages: ChatMessage[],
systemPrompt: string = '',
model: string = 'claude-sonnet-4-20250514'
): Promise<string> {
const response = await anthropic.messages.create({
model,
max_tokens: 2048,
system: systemPrompt,
messages,
});
// Il contenuto è un array di blocchi
const textBlock = response.content.find(
(block) => block.type === 'text'
);
return textBlock?.text ?? '';
}
// Esempio di utilizzo
const risposta = await chatWithAnthropic(
[{ role: 'user', content: 'Spiega il pattern Observer in TypeScript.' }],
'Sei un assistente tecnico esperto.'
);
console.log(risposta);
3. 応答のストリーミング
ストリーミングは、シームレスなユーザー エクスペリエンスを提供するための鍵です。待つ代わりに メッセージ全体が生成され、トークンがモデルとして送信されること それらを生成し、体感的な待ち時間を大幅に短縮します。
OpenAIによるストリーミング
// src/lib/openai-stream.ts
import { openai } from './ai-clients';
export async function* streamOpenAI(
messages: Array<{ role: string; content: string }>,
model: string = 'gpt-4o'
): AsyncGenerator<string> {
const stream = await openai.chat.completions.create({
model,
messages: messages as any,
stream: true,
temperature: 0.7,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
// Utilizzo
async function main() {
const messages = [
{ role: 'user', content: 'Scrivi una funzione di ordinamento.' }
];
for await (const token of streamOpenAI(messages)) {
process.stdout.write(token);
}
}
Anthropic でストリーミング
// src/lib/anthropic-stream.ts
import { anthropic } from './ai-clients';
export async function* streamAnthropic(
messages: Array<{ role: string; content: string }>,
systemPrompt: string = '',
model: string = 'claude-sonnet-4-20250514'
): AsyncGenerator<string> {
const stream = anthropic.messages.stream({
model,
max_tokens: 2048,
system: systemPrompt,
messages: messages as any,
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
yield event.delta.text;
}
}
}
// Utilizzo con metriche
async function main() {
const start = Date.now();
let totalTokens = 0;
for await (const token of streamAnthropic(
[{ role: 'user', content: 'Scrivi una funzione di ordinamento.' }],
'Sei un programmatore esperto.'
)) {
process.stdout.write(token);
totalTokens++;
}
console.log(`\nTempo: ${Date.now() - start}ms, Token: ${totalTokens}`);
}
4. エラー管理と再試行
外部 API は、レート制限、タイムアウト、 サーバーエラーやネットワークの問題。堅牢なリトライ システムが不可欠 アプリケーションの信頼性を確保するため。
一般的なエラーコード
| コード | 意味 | 戦略 |
|---|---|---|
| 400 | 不正な形式のリクエスト | 再試行せずにパラメータを修正してください |
| 401 | 認証に失敗しました | APIキーを確認してください |
| 429 | レート制限を超えました | 待ってからバックオフで再試行してください |
| 500 | サーバーエラー | 指数バックオフを使用して再試行してください |
| 503 | サービスは利用できません | 数秒後にもう一度試してください |
| 529 | オーバーロード (人外) | 長いバックオフで再試行してください |
// src/lib/retry.ts
interface RetryConfig {
maxRetries: number;
baseDelay: number; // millisecondi
maxDelay: number; // millisecondi
backoffFactor: number;
}
const DEFAULT_CONFIG: RetryConfig = {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2,
};
export async function withRetry<T>(
fn: () => Promise<T>,
config: Partial<RetryConfig> = {}
): Promise<T> {
const cfg = { ...DEFAULT_CONFIG, ...config };
let lastError: Error | null = null;
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Non ritentare per errori client (4xx eccetto 429)
if (error.status && error.status >= 400 &&
error.status < 500 && error.status !== 429) {
throw error;
}
if (attempt === cfg.maxRetries) break;
// Calcola il delay con jitter
const delay = Math.min(
cfg.baseDelay * Math.pow(cfg.backoffFactor, attempt) +
Math.random() * 1000,
cfg.maxDelay
);
console.warn(
`Tentativo ${attempt + 1} fallito. ` +
`Retry tra ${Math.round(delay)}ms...`
);
// Usa Retry-After se disponibile
const retryAfter = error.headers?.get?.('retry-after');
const waitTime = retryAfter
? parseInt(retryAfter) * 1000
: delay;
await new Promise(r => setTimeout(r, waitTime));
}
}
throw lastError;
}
5. レート制限と周波数制御
アプリケーションが多数の同時ユーザーを処理する場合、次の実装が重要です。 課せられた制限を超えないようにするためのクライアント側のレート制限システム プロバイダーによるリソースの公平な使用を確保します。
// src/lib/rate-limiter.ts
export class TokenBucketRateLimiter {
private tokens: number;
private lastRefill: number;
constructor(
private maxTokens: number,
private refillRate: number, // token al secondo
) {
this.tokens = maxTokens;
this.lastRefill = Date.now();
}
private refill(): void {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
this.tokens = Math.min(
this.maxTokens,
this.tokens + elapsed * this.refillRate
);
this.lastRefill = now;
}
async acquire(): Promise<void> {
this.refill();
if (this.tokens < 1) {
const waitTime = (1 - this.tokens) / this.refillRate * 1000;
await new Promise(r => setTimeout(r, waitTime));
this.refill();
}
this.tokens -= 1;
}
}
// Limiti per provider
const openaiLimiter = new TokenBucketRateLimiter(60, 1); // 60 RPM
const anthropicLimiter = new TokenBucketRateLimiter(50, 0.83); // 50 RPM
6. トークンカウントとコストの最適化
API コストは、処理されるトークンの数に直接比例します。 トークン消費の監視と最適化はコストを維持するために重要です 特にトラフィック量の多い用途では、制御下にあります。
100万トークンの参考コスト(2026年2月)
| モデル | 入力 (1M トークン) | 出力(1Mトークン) |
|---|---|---|
| GPT-4o | 2.50ドル | $10.00 |
| GPT-4oミニ | $0.15 | $0.60 |
| クロード・ソネット 4 | $3.00 | $15.00 |
| クロード俳句 3.5 | $0.80 | $4.00 |
注記: 価格は参考値であり、変更される場合があります。 最新のコストについては、必ず公式ドキュメントを確認してください。
// src/lib/token-counter.ts
import { encoding_for_model } from 'tiktoken';
export class TokenCounter {
private encoder;
constructor(model: string = 'gpt-4o') {
this.encoder = encoding_for_model(model as any);
}
count(text: string): number {
return this.encoder.encode(text).length;
}
countMessages(
messages: Array<{ role: string; content: string }>
): number {
let total = 0;
for (const msg of messages) {
total += 4; // overhead per messaggio
total += this.count(msg.role);
total += this.count(msg.content);
}
total += 2; // overhead della richiesta
return total;
}
estimateCost(
inputTokens: number,
outputTokens: number,
inputCostPer1M: number,
outputCostPer1M: number
): number {
return (
(inputTokens / 1_000_000) * inputCostPer1M +
(outputTokens / 1_000_000) * outputCostPer1M
);
}
dispose(): void {
this.encoder.free();
}
}
7. API キー セキュリティのプロキシ パターン
API キーは必須ではありません 一度もない フロントエンドに公開されます。パターン 間の仲介者として機能するプロキシ サーバーを作成することをお勧めします。 ユーザーのブラウザーと AI プロバイダー API。フロントエンドはあなたのものと通信します サーバーがキーを安全に管理します。
// src/server/proxy.ts
import express from 'express';
import cors from 'cors';
import { openai, anthropic } from '../lib/ai-clients';
import { withRetry } from '../lib/retry';
const app = express();
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json());
// Middleware di rate limiting per IP
const ipRequests = new Map<string, number[]>();
function rateLimitMiddleware(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const ip = req.ip ?? 'unknown';
const now = Date.now();
const windowMs = 60_000; // 1 minuto
const maxRequests = 20;
const requests = (ipRequests.get(ip) ?? [])
.filter(t => now - t < windowMs);
if (requests.length >= maxRequests) {
return res.status(429).json({
error: 'Troppe richieste. Riprova tra un minuto.'
});
}
requests.push(now);
ipRequests.set(ip, requests);
next();
}
app.use('/api', rateLimitMiddleware);
// Endpoint chat unificato
app.post('/api/chat', async (req, res) => {
const { provider, messages, system, model } = req.body;
try {
let content: string;
if (provider === 'openai') {
const result = await withRetry(() =>
openai.chat.completions.create({
model: model ?? 'gpt-4o',
messages,
temperature: 0.7,
})
);
content = result.choices[0].message.content ?? '';
} else {
const result = await withRetry(() =>
anthropic.messages.create({
model: model ?? 'claude-sonnet-4-20250514',
max_tokens: 2048,
system: system ?? '',
messages,
})
);
const textBlock = result.content.find(b => b.type === 'text');
content = textBlock?.text ?? '';
}
res.json({ content });
} catch (error: any) {
res.status(error.status ?? 500).json({
error: error.message ?? 'Errore interno del server'
});
}
});
app.listen(3001, () =>
console.log('Proxy AI in ascolto su porta 3001')
);
8. フロントエンドへの応答のストリーミング
ユーザーのブラウザー、プロキシ サーバーまでストリーミング エクスペリエンスを提供するには サポートしなければなりません サーバー送信イベント (SSE)。このプロトコルにより、 サーバーに送信して、単一のサーバーを通じて段階的にデータをクライアントに送信します。 HTTP接続。
// src/server/stream-endpoint.ts
app.post('/api/chat/stream', async (req, res) => {
const { provider, messages, system, model } = req.body;
// Configura headers SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
if (provider === 'openai') {
const stream = await openai.chat.completions.create({
model: model ?? 'gpt-4o',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content;
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
} else {
const stream = anthropic.messages.stream({
model: model ?? 'claude-sonnet-4-20250514',
max_tokens: 2048,
system: system ?? '',
messages,
});
for await (const event of stream) {
if (
event.type === 'content_block_delta' &&
event.delta.type === 'text_delta'
) {
res.write(
`data: ${JSON.stringify({ content: event.delta.text })}\n\n`
);
}
}
}
res.write('data: [DONE]\n\n');
res.end();
} catch (error: any) {
res.write(
`data: ${JSON.stringify({ error: error.message })}\n\n`
);
res.end();
}
});
ストリーミング用フロントエンドクライアント
// src/app/services/ai-chat.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AiChatService {
private apiUrl = '/api/chat';
streamChat(
provider: 'openai' | 'anthropic',
messages: Array<{ role: string; content: string }>,
system?: string
): Observable<string> {
return new Observable(subscriber => {
const controller = new AbortController();
fetch(`${this.apiUrl}/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider, messages, system }),
signal: controller.signal,
})
.then(response => {
const reader = response.body!.getReader();
const decoder = new TextDecoder();
function read() {
reader.read().then(({ done, value }) => {
if (done) {
subscriber.complete();
return;
}
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
subscriber.complete();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.content) {
subscriber.next(parsed.content);
}
} catch {
// Ignora righe non parsabili
}
}
}
read();
});
}
read();
})
.catch(err => subscriber.error(err));
return () => controller.abort();
});
}
}
9. 完全なチャット インターフェイスを構築する
これまでに構築したすべてのコンポーネントを組み合わせることで、インターフェースを作成できます 両方のプロバイダーをサポートし、メッセージ履歴を管理する完全なチャット リアルタイムでストリーミング応答を表示します。
// src/app/components/chat/chat.component.ts
import { Component, signal } from '@angular/core';
import { AiChatService } from '../../services/ai-chat.service';
interface Message {
role: 'user' | 'assistant';
content: string;
provider?: string;
timestamp: Date;
}
@Component({
selector: 'app-chat',
standalone: true,
template: `...`,
})
export class ChatComponent {
messages = signal<Message[]>([]);
inputText = signal('');
isLoading = signal(false);
provider = signal<'openai' | 'anthropic'>('anthropic');
currentStream = signal('');
constructor(private chatService: AiChatService) {}
sendMessage(): void {
const text = this.inputText().trim();
if (!text || this.isLoading()) return;
// Aggiungi messaggio utente
this.messages.update(msgs => [
...msgs,
{ role: 'user', content: text, timestamp: new Date() }
]);
this.inputText.set('');
this.isLoading.set(true);
this.currentStream.set('');
// Prepara messaggi per l'API
const apiMessages = this.messages().map(m => ({
role: m.role,
content: m.content,
}));
// Avvia streaming
this.chatService.streamChat(
this.provider(),
apiMessages,
'Sei un assistente esperto e amichevole.'
).subscribe({
next: (token) => {
this.currentStream.update(s => s + token);
},
complete: () => {
this.messages.update(msgs => [
...msgs,
{
role: 'assistant',
content: this.currentStream(),
provider: this.provider(),
timestamp: new Date(),
}
]);
this.currentStream.set('');
this.isLoading.set(false);
},
error: (err) => {
console.error('Errore streaming:', err);
this.isLoading.set(false);
},
});
}
}
10. ベストプラクティスと最適化
基本的な統合を実装した後は、いくつかの高度な実践があります。 品質、信頼性、効率を大幅に向上させます アプリケーションの。
統合プロバイダーの抽象化
共通インターフェースを作成すると、あるプロバイダーから別のプロバイダーに切り替えることができます。 アプリケーションコードを変更する必要はありません。これにより実装も容易になります フォールバック戦略の。
// src/lib/unified-ai.ts
interface AIProvider {
chat(messages: ChatMessage[], system?: string): Promise<string>;
stream(messages: ChatMessage[], system?: string): AsyncGenerator<string>;
}
export class UnifiedAI {
private providers: Map<string, AIProvider> = new Map();
private primaryProvider: string;
constructor(primary: string) {
this.primaryProvider = primary;
}
registerProvider(name: string, provider: AIProvider): void {
this.providers.set(name, provider);
}
async chatWithFallback(
messages: ChatMessage[],
system?: string
): Promise<{ content: string; provider: string }> {
const providerOrder = [
this.primaryProvider,
...Array.from(this.providers.keys())
.filter(p => p !== this.primaryProvider)
];
for (const name of providerOrder) {
const provider = this.providers.get(name);
if (!provider) continue;
try {
const content = await withRetry(
() => provider.chat(messages, system),
{ maxRetries: 2 }
);
return { content, provider: name };
} catch (error) {
console.warn(`Provider ${name} fallito, provo il successivo...`);
}
}
throw new Error('Tutti i provider AI sono non disponibili');
}
}
避けるべきよくある間違い
- フロントエンドで API キーを公開します。 常にプロキシサーバーを使用する
- レート制限を無視する: クライアント側のレートリミッタを実装します
- タイムアウトを処理しない: 各リクエストに適切なタイムアウトを設定する
- 履歴全体を送信します。 古いメッセージを切り詰めてトークンを節約する
- コストを追跡しない: 支出のしきい値に関するアラートを実装する
- モデルをハードコードします。 簡単に更新できるようにテンプレートを構成可能にする
統合チェックリスト
- API キーは環境変数に保存され、コードには保存されません
- CORS とレート制限を使用して構成されたプロキシ サーバー
- 指数バックオフを実装して再試行する
- ストリーミングは両方のプロバイダーで機能します
- アクティブなトークンのカウントとコストの監視
- ユーザーへの情報メッセージによるエラー管理
- 両方のプロバイダーの統合テスト
- デバッグのための集中ログ
結論
OpenAI と Anthropic API の統合には細部への注意が必要です ただし、正しく構成されれば、構築するための強固な基盤が提供されます。 強力で信頼性の高い AI アプリケーション。プロキシ パターンはキーを保護します。 再試行システムは回復力を保証し、ストリーミングはエクスペリエンスを提供します 良質なユーザー。
次の記事では、その方法について説明します Ollama を使用したローカル LLM、 クラウドへの依存を排除し、興味深い可能性を切り開きます。 プライバシー、コスト、オフライン開発。







