1000行代码实现极简版openclaw(附源码)(4)
摘要 本文介绍了如何构建统一的LLM接口来连接不同大模型API(如OpenAI、DeepSeek等)。主要内容包括: 问题背景:不同LLM提供商API格式存在差异(如OpenAI与Anthropic格式不同),导致调用方式不统一。 解决方案:采用适配器模式设计统一接口,通过LLMProvider抽象层隔离底层实现差异。 代码实现: 提供OpenAI兼容格式的Provider实现(支持OpenAI/
·
03 - LLM 接口:连接大脑 github 源码(欢迎star)
目标
理解如何连接 OpenAI、DeepSeek 等大模型 API,实现统一的 LLM 接口。
1. 为什么需要统一接口?
问题:不同 LLM API 格式不同
OpenAI 格式:
POST https://api.openai.com/v1/chat/completions
{
"model": "gpt-4",
"messages": [{"role": "user", "content": "你好"}]
}
DeepSeek 格式(兼容 OpenAI):
POST https://api.deepseek.com/v1/chat/completions
{
"model": "deepseek-chat",
"messages": [{"role": "user", "content": "你好"}]
}
Anthropic 格式(不同):
POST https://api.anthropic.com/v1/messages
{
"model": "claude-3",
"messages": [{"role": "user", "content": "你好"}]
}
解决方案:适配器模式
┌─────────────────────────────────────────────────────────┐
│ Agent │
│ (使用统一接口) │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ LLMProvider (接口) │
│ complete(request) -> Promise<response> │
└─────────────────────────┬───────────────────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│OpenAIProvider│ │DeepSeek │ │Anthropic │
│ │ │Provider │ │Provider │
└────────────┘ └────────────┘ └────────────┘
│ │ │
▼ ▼ ▼
api.openai api.deepseek api.anthropic
好处:Agent 不需要关心底层是哪个模型,统一调用。
2. 完整代码实现
创建文件 src/core/llm.ts:
/**
* LLM Provider 实现 - 多模型统一接口
*/
import { LLMProvider, LLMRequest, LLMResponse } from './types.js';
// ============================================================================
// 第一部分:OpenAI 格式 Provider
// ============================================================================
/**
* OpenAI 格式 Provider
*
* 支持:
* - OpenAI (GPT-3.5, GPT-4)
* - DeepSeek (兼容 OpenAI 格式)
* - 其他兼容 OpenAI 格式的国产模型
*/
export class OpenAIProvider implements LLMProvider {
readonly name = 'openai';
/**
* @param apiKey API 密钥
* @param baseUrl API 基础地址,默认 OpenAI 官方
*/
constructor(
private apiKey: string,
private baseUrl = 'https://api.openai.com/v1'
) {}
/**
* 调用 LLM 完成请求
*
* @param request LLMRequest 对象
* @returns LLMResponse 对象
*/
async complete(request: LLMRequest): Promise<LLMResponse> {
// 调用 API
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: request.model,
messages: request.messages,
tools: request.tools,
max_tokens: request.maxTokens,
temperature: request.temperature,
}),
});
// 处理错误
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
}
// 解析响应
const data = await response.json();
// OpenAI 响应格式:
// {
// "choices": [{
// "message": {
// "content": "...",
// "tool_calls": [...]
// }
// }],
// "usage": {...}
// }
const choice = data.choices?.[0];
return {
// 文本内容
content: choice?.message?.content,
// 工具调用
toolCalls: choice?.message?.tool_calls?.map((tc: any) => ({
id: tc.id,
type: 'function',
function: {
name: tc.function.name,
arguments: tc.function.arguments,
},
})),
// Token 使用量
usage: {
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0,
},
};
}
}
// ============================================================================
// 第二部分:Anthropic 格式 Provider
// ============================================================================
/**
* Anthropic (Claude) Provider
*
* Anthropic API 格式与 OpenAI 略有不同,需要转换
*/
export class AnthropicProvider implements LLMProvider {
readonly name = 'anthropic';
constructor(
private apiKey: string,
private baseUrl = 'https://api.anthropic.com/v1'
) {}
async complete(request: LLMRequest): Promise<LLMResponse> {
// Anthropic 使用 "messages" 端点,不是 "chat/completions"
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'x-api-key': this.apiKey, // 注意:不是 Authorization
'Content-Type': 'application/json',
'anthropic-version': '2023-06-01', // 需要版本头
},
body: JSON.stringify({
model: request.model,
messages: request.messages.map(m => ({
// Anthropic 不支持 system 角色,转为 user
role: m.role === 'system' ? 'user' : m.role,
content: m.content,
})),
tools: request.tools?.map(t => ({
name: t.function.name,
description: t.function.description,
input_schema: t.function.parameters, // 字段名不同
})),
max_tokens: request.maxTokens || 4000, // Anthropic 要求必须
temperature: request.temperature,
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
}
const data = await response.json();
// Anthropic 响应格式不同
// content 是数组,包含 text 和 tool_use
const textContent = data.content
?.filter((c: any) => c.type === 'text')
.map((c: any) => c.text)
.join('');
const toolCalls = data.content
?.filter((c: any) => c.type === 'tool_use')
.map((c: any) => ({
id: c.id,
type: 'function',
function: {
name: c.name,
arguments: JSON.stringify(c.input), // input 是对象,需要 stringify
},
}));
return {
content: textContent,
toolCalls,
usage: {
inputTokens: data.usage?.input_tokens || 0,
outputTokens: data.usage?.output_tokens || 0,
},
};
}
}
// ============================================================================
// 第三部分:Provider 工厂
// ============================================================================
/**
* 创建 Provider 的工厂函数
*
* 根据模型名称自动选择合适的 Provider
*
* 模型名称格式:provider/model
* 例如:
* - openai/gpt-4
* - anthropic/claude-3-sonnet
* - deepseek-chat(默认使用 OpenAI 格式)
*/
export function createProvider(
model: string,
apiKey: string,
baseUrl?: string
): LLMProvider {
// 解析 provider/model 格式
const [provider] = model.split('/');
switch (provider) {
case 'openai':
return new OpenAIProvider(apiKey, baseUrl);
case 'anthropic':
case 'claude':
return new AnthropicProvider(apiKey, baseUrl);
default:
// 默认使用 OpenAI 格式(兼容多数国产模型)
return new OpenAIProvider(apiKey, baseUrl);
}
}
3. 代码详解
3.1 fetch API 使用
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
关键点:
Authorization: Bearer {token}- 标准的 API 认证方式Content-Type: application/json- 告诉服务器发送的是 JSONJSON.stringify()- 将 JS 对象转为 JSON 字符串
3.2 错误处理
if (!response.ok) {
const errorText = await response.text();
throw new Error(`LLM API 错误: ${response.status} - ${errorText}`);
}
HTTP 状态码:
- 200 OK - 成功
- 401 Unauthorized - API Key 错误
- 429 Too Many Requests - 请求太频繁
- 500 Internal Server Error - 服务器错误
3.3 响应解析
OpenAI 响应结构:
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "你好!",
"tool_calls": [{
"id": "call_123",
"type": "function",
"function": {
"name": "write_file",
"arguments": "{\"path\":\"test.txt\"}"
}
}]
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}
提取数据:
const choice = data.choices?.[0]; // 取第一个选择
const content = choice?.message?.content; // 文本内容
const toolCalls = choice?.message?.tool_calls; // 工具调用
3.4 格式转换
Anthropic 的特殊处理:
// OpenAI: tool_calls[].function.arguments 是字符串
// Anthropic: content[].input 是对象
// 需要转换
function: {
name: c.name,
arguments: JSON.stringify(c.input), // 对象 -> 字符串
}
4. 使用示例
4.1 基本调用
const provider = createProvider(
'deepseek-chat',
'sk-your-api-key',
'https://api.deepseek.com/v1'
);
const response = await provider.complete({
model: 'deepseek-chat',
messages: [
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: '你好' }
],
});
console.log(response.content); // "你好!有什么我可以帮助你的吗?"
4.2 带工具调用
const response = await provider.complete({
model: 'deepseek-chat',
messages: [{ role: 'user', content: '创建一个文件' }],
tools: [{
type: 'function',
function: {
name: 'write_file',
description: '写入文件',
parameters: {
type: 'object',
properties: {
path: { type: 'string' },
content: { type: 'string' }
},
required: ['path', 'content']
}
}
}],
});
if (response.toolCalls) {
// LLM 要求调用工具
for (const call of response.toolCalls) {
console.log(`调用: ${call.function.name}`);
console.log(`参数: ${call.function.arguments}`);
}
}
4.3 不同模型对比
// OpenAI
const openai = createProvider(
'openai/gpt-4',
'sk-openai-key'
);
// DeepSeek
const deepseek = createProvider(
'deepseek-chat',
'sk-deepseek-key',
'https://api.deepseek.com/v1'
);
// Anthropic
const anthropic = createProvider(
'anthropic/claude-3-sonnet',
'sk-ant-key'
);
// 统一接口调用
for (const provider of [openai, deepseek, anthropic]) {
const response = await provider.complete({
model: '...',
messages: [{ role: 'user', content: '你好' }],
});
console.log(response.content);
}
5. 高级话题
5.1 流式响应
流式响应可以实时显示 LLM 的输出(打字机效果):
async function* streamComplete(request: LLMRequest) {
const response = await fetch(url, {
...options,
headers: {
...headers,
'Accept': 'text/event-stream', // 关键头
},
});
const reader = response.body?.getReader();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
// 解析 SSE 数据
const text = new TextDecoder().decode(value);
yield text;
}
}
// 使用
for await (const chunk of streamComplete(request)) {
process.stdout.write(chunk); // 实时输出
}
5.2 重试机制
网络可能失败,需要重试:
async function completeWithRetry(
provider: LLMProvider,
request: LLMRequest,
maxRetries = 3
): Promise<LLMResponse> {
for (let i = 0; i < maxRetries; i++) {
try {
return await provider.complete(request);
} catch (error) {
if (i === maxRetries - 1) throw error;
// 指数退避
const delay = Math.pow(2, i) * 1000;
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('Unreachable');
}
5.3 Token 计算
// 简单估算(不准确,仅参考)
function estimateTokens(text: string): number {
// 英文:1 token ≈ 4 字符
// 中文:1 token ≈ 1 字符
const englishChars = text.replace(/[^\x00-\x7F]/g, '').length;
const chineseChars = text.length - englishChars;
return Math.ceil(englishChars / 4) + chineseChars;
}
6. 练习
练习 1:添加重试
为 complete 方法添加重试机制。
练习 2:支持更多模型
实现一个 Google Gemini 的 Provider。
练习 3:流式输出
实现流式响应,实时显示 LLM 输出。
更多推荐


所有评论(0)