11.2 视觉模型集成

LLMClient 视觉模型支持

在 Step 9 中,我们扩展了 LLMClient 以支持视觉模型。核心思路是:当检测到消息中包含图像时,自动切换到配置的视觉模型。

配置扩展

首先,在 LLMConfig 类型中添加视觉模型配置:

1
2
3
4
5
6
7
8
9
10
11
// src/llm/client.ts

export type LLMConfig = {
apiKey: string;
baseURL?: string;
model?: string; // 默认文本模型
visionModel?: string; // 新增:视觉模型
temperature?: number;
maxTokens?: number;
streaming?: boolean;
};

配置示例:

1
2
3
4
5
6
7
8
{
"llm": {
"apiKey": "sk-...",
"baseUrl": "https://api.openai.com/v1",
"model": "gpt-4", // 文本对话模型
"visionModel": "gpt-4-vision-preview" // 视觉模型
}
}

模型选择逻辑

LLMClient 添加了 getModel() 方法,根据消息内容自动选择合适的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/llm/client.ts

/**
* 检测消息是否包含图像
*/
private hasImage(messages: LLMMessage[]): boolean {
return messages.some((msg) => {
const content = msg.content;
if (typeof content === "string") {
return false;
}
if (Array.isArray(content)) {
return content.some((item) => item.type === "image");
}
if (content && typeof content === "object") {
return content.type === "image";
}
return false;
});
}

/**
* 获取要使用的模型
*/
private getModel(messages: LLMMessage[]): string {
const hasImages = this.hasImage(messages);
if (hasImages && this.config.visionModel) {
return this.config.visionModel; // 使用视觉模型
}
return this.config.model || "deepseek-chat"; // 使用默认模型
}

工作流程:

1
2
3
4
5
6
7
用户消息

hasImage() 检测

包含图像?→ 是 → 使用 visionModel
↓ 否
使用默认 model

消息格式转换

视觉模型通常要求使用特定的消息格式。OpenAI 的视觉 API 使用 image_url 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// src/llm/client.ts

/**
* 转换消息为 OpenAI 格式
*/
private convertMessage(msg: LLMMessage): any {
const baseMsg: any = {
role: msg.role,
};

if (msg.role === "tool") {
baseMsg.tool_call_id = msg.tool_call_id;
baseMsg.name = msg.name;
baseMsg.content = msg.content;
} else {
// 处理多模态内容
const content = msg.content;
if (typeof content === "string") {
// 纯文本
baseMsg.content = content;
} else if (Array.isArray(content)) {
// 多模态数组
baseMsg.content = content.map((item: any) => {
if (item.type === "text") {
return { type: "text", text: item.text };
} else if (item.type === "image") {
return {
type: "image_url",
image_url: {
url: `data:${item.mediaType};base64,${item.data}`,
},
};
}
});
} else if (content.type === "image") {
// 单个图像
baseMsg.content = [{
type: "image_url",
image_url: {
url: `data:${content.mediaType};base64,${content.data}`,
},
}];
}
}

return baseMsg;
}

格式转换示例

输入消息(内部格式):

1
2
3
4
5
6
7
8
9
10
11
{
role: "user",
content: [
{ type: "text", text: "这是什么?" },
{
type: "image",
mediaType: "image/png",
data: "iVBORw0KGgo..."
}
]
}

输出消息(OpenAI 格式):

1
2
3
4
5
6
7
8
9
10
11
12
{
role: "user",
content: [
{ type: "text", text: "这是什么?" },
{
type: "image_url",
image_url: {
url: "data:image/png;base64,iVBORw0KGgo..."
}
}
]
}

完整调用流程

chat() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async chat(
systemPrompt: string,
messages: LLMMessage[]
): Promise<LLMResponse> {
// 1. 构建消息数组
const openAIMessages = [
{ role: "system", content: systemPrompt },
...messages.map((msg) => this.convertMessage(msg))
];

// 2. 选择模型
const model = this.getModel(messages);

// 3. 调用 API
const completion = await this.client.chat.completions.create({
model: model,
messages: openAIMessages,
temperature: this.config.temperature ?? 0.7,
max_tokens: this.config.maxTokens,
tools: this.getTools(),
});

// 4. 返回结果
return {
content: completion.choices[0].message.content,
toolCalls: extractToolCalls(completion)
};
}

chatStream() 方法

流式输出同样支持视觉模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async chatStream(
systemPrompt: string,
messages: LLMMessage[],
onChunk: (chunk: StreamChunk) => void
): Promise<LLMResponse> {
const openAIMessages = [
{ role: "system", content: systemPrompt },
...messages.map((msg) => this.convertMessage(msg))
];

const model = this.getModel(messages);

const stream = await this.client.chat.completions.create({
model: model,
messages: openAIMessages,
temperature: this.config.temperature ?? 0.7,
max_tokens: this.config.maxTokens,
tools: this.getTools(),
stream: true,
});

// 处理流式响应...
}

配置最佳实践

模型选择建议

OpenAI 系列:

1
2
3
4
{
"model": "gpt-4",
"visionModel": "gpt-4o"
}
  • 使用 GPT-4o 处理视觉(速度快、效果好)
  • 使用 GPT-4 处理纯文本(成本更低)

Anthropic 系列:

1
2
3
4
{
"model": "claude-3-opus-20240229",
"visionModel": "claude-3-opus-20240229"
}
  • Claude 3 Opus 同时支持文本和视觉
  • 使用同一模型简化配置

混合方案:

1
2
3
4
{
"model": "deepseek-chat",
"visionModel": "gpt-4o"
}
  • 文本使用 DeepSeek(成本低)
  • 视觉使用 GPT-4o(效果好)

降级处理

如果未配置视觉模型,可以优雅降级:

1
2
3
4
5
6
7
8
9
10
11
12
13
private getModel(messages: LLMMessage[]): string {
const hasImages = this.hasImage(messages);

if (hasImages) {
if (this.config.visionModel) {
return this.config.visionModel;
}
// 如果没有配置视觉模型,使用默认模型
console.warn("消息包含图像但未配置视觉模型,可能无法正确处理");
}

return this.config.model || "deepseek-chat";
}

API 差异处理

不同的视觉模型可能有不同的 API 格式:

OpenAI 格式

1
2
3
4
{
type: "image_url",
image_url: { url: "data:image/png;base64,..." }
}

Anthropic 格式

1
2
3
4
5
6
7
8
{
type: "image",
source: {
type: "base64",
media_type: "image/png",
data: "..."
}
}

统一处理方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private convertMessage(msg: LLMMessage): any {
// 根据模型提供商选择格式
const isAnthropic = this.config.baseURL?.includes("anthropic");

if (isAnthropic && item.type === "image") {
return {
type: "image",
source: {
type: "base64",
media_type: item.mediaType,
data: item.data
}
};
}

// 默认 OpenAI 格式
return {
type: "image_url",
image_url: {
url: `data:${item.mediaType};base64,${item.data}`
}
};
}

小结

本节介绍了如何在 LLMClient 中集成视觉模型:

  • 添加 visionModel 配置
  • 实现自动模型选择
  • 处理多模态消息格式转换
  • 支持流式和同步两种调用方式

导航

上一篇: 11.1 多模态 AI 概述

下一篇: 11.3 图像处理与传输