OpenAI 및 Anthropic API를 웹 앱에 통합
언어 모델 API 통합은 가장 수요가 많은 기술 중 하나입니다. 현대 웹 개발에서. 챗봇을 구축하든, 보조자든 콘텐츠 생성이나 텍스트 분석 시스템, 의사소통 능력 API를 사용하여 효과적으로 오픈AI e 인류학 그것은 근본적이다.
이 시리즈의 네 번째 기사에서는 웹 개발자를 위한 AI, 우리는 자세히 알아볼 것입니다 두 API를 완전한 웹 애플리케이션에 통합하는 방법. 구성부터 시작하겠습니다. 초기에는 메시지 구조를 분석하고 응답 스트리밍을 구현합니다. 그리고 API 키를 보호하기 위해 프록시 패턴을 갖춘 보안 아키텍처를 구축할 것입니다.
무엇을 배울 것인가
- OpenAI 및 Anthropic API를 사용하여 구성 및 인증
- 두 메시지 시스템 간의 구조적 차이점 이해
- 실시간 경험을 위한 응답 스트리밍 구현
- 오류, 재시도 및 속도 제한을 강력하게 처리합니다.
- 토큰 계산 및 사용 비용 최적화
- API 키를 보호하기 위한 보안 백엔드 프록시 구축
- 스트리밍 지원으로 완전한 채팅 인터페이스 만들기
시리즈 개요
| # | Articolo | 집중하다 |
|---|---|---|
| 1 | RAG 소개 | 기본 개념 |
| 2 | TypeScript 및 LangChain을 사용한 RAG | 실제 구현 |
| 3 | 벡터 데이터베이스 비교 | Chromadb, 솔방울, Weaviate |
| 4 | OpenAI 및 Anthropic API(현재 위치) | API 통합 |
| 5 | Ollama와 함께하는 LLM 로컬 | 클라우드 없는 AI |
| 6 | 미세 조정과 RAG | 언제 무엇을 사용할 것인가 |
| 7 | AI 에이전트 | 에이전트 기반 아키텍처 |
| 8 | CI/CD의 AI | 지능형 자동화 |
1. API 설정 및 인증
첫 번째 요청을 제출하기 전에 로그인 자격 증명을 얻어야 합니다. 개발 환경을 올바르게 구성합니다. 두 제공업체 모두 안전하게 관리해야 하는 API Key입니다.
API 키 얻기
을 위한 오픈AI, 등록하세요 platform.openai.com 그리고 키를 생성하세요
API 키 섹션에서. 을 위한 인류학, 다음에서 콘솔에 액세스하세요.
console.anthropic.com 설정 패널에서 API 키를 생성하세요.
API 키 보안
소스 코드나 커밋된 파일에 API 키를 직접 포함하지 마세요. 힘내에서. 항상 환경 변수나 비밀 관리 서비스를 사용하세요. 노출된 키는 예상치 못한 비용을 발생시키고 보안을 손상시킬 수 있습니다.
# 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 모두 대화의 메시지 개념으로 작동하지만 구조가 크게 다릅니다. 차이점을 이해하세요 통합된 추상화를 구축하는 데 필수적입니다.
구조 비교
| 특성 | 오픈AI | 인류학 |
|---|---|---|
| 메시지 역할 | 시스템, 사용자, 보조자, 도구 | 사용자, 보조자(별도 시스템) |
| 시스템 프롬프트 | 메시지에서 | 전용 매개변수 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 비용은 처리된 토큰 수에 정비례합니다. 토큰 소비를 모니터링하고 최적화하는 것은 비용을 유지하는 데 중요합니다. 특히 트래픽이 많은 애플리케이션에서는 제어가 가능합니다.
1M 토큰의 표시 비용(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, 클라우드에 대한 의존성을 제거하고 흥미로운 가능성을 열어줍니다. 개인 정보 보호, 비용 및 오프라인 개발.







