多模态Agent-处理图片音频与文本的Agent
*多模态 Agent 架构——文本/图片/音频输入经多模态 LLM 统一处理,调用不同类型工具*
多模态 Agent:处理图片、音频与文本的 Agent
1.1 多模态的技术基础:如何让模型理解非文字信息
多模态 Agent 架构——文本/图片/音频输入经多模态 LLM 统一处理,调用不同类型工具
在深入实现之前,需要理解多模态能力的技术原理——不是为了自己实现它,而是为了理解它的能力边界和限制来自哪里。
LLM 只能处理 token,但图像不是 token
LLM 的基本工作单元是 token(文本片段)。图像、音频、视频本质上是不同维度的数值矩阵,和 token 序列在形式上完全不同。要让 LLM "看懂"图像,必须先把图像转换成 LLM 能理解的格式。
视觉编码器(Vision Encoder)的作用
多模态模型(如 GPT-4o、Claude 的视觉版本)内置了视觉编码器——一个专门处理图像的神经网络组件(通常基于 CLIP 或 ViT 架构)。它的工作是把图像切分成固定大小的"图块"(patch),对每个图块提取特征向量,然后通过一个映射层把这些特征向量转换成 LLM 能直接处理的 token 序列。
这就是为什么图像会消耗大量 token——一张 1024x1024 的图片被切成几十上百个 patch,每个 patch 对应几个 token,合计可能消耗上千个 token。这也解释了为什么图像的 detail 参数(高分辨率 vs 低分辨率)会影响 token 消耗和识别精度。
音频处理的不同路径
大多数主流 LLM 的对话接口不直接接受音频(部分例外如 GPT-4o Realtime API)。通常的处理路径是先用语音识别(ASR,Automatic Speech Recognition)模型把音频转成文字,再让 LLM 处理文字。OpenAI 的 Whisper 是目前最常用的 ASR 模型,它本质上是一个 Transformer,专门训练用来做语音转文字。
跨模态对齐的挑战
"让模型同时理解图像和文字"比听起来难得多。图像和文字在向量空间里的分布完全不同,训练时需要大量图文对(图像 + 描述文字的配对数据)来让模型学会"对齐"——把同一个概念在不同模态里的表示拉近。这个对齐质量,决定了模型能否准确回答"图里说的 XX 和文本中提到的 XX 是不是同一件事"。
理解了这些原理,后面代码里的各种参数(detail 等级、token 消耗、识别精度限制)就有了合理的解释依据。
早期的 AI Agent 只能处理文字。用户上传一张报表截图,Agent 看不懂;录制一段语音描述,Agent 无从处理。多模态 LLM 的普及改变了这个局面——现在的 Agent 可以直接"看图说话"、"听音转文",并在此基础上推理和决策。
本文介绍多模态 Agent 的核心能力和实现方式,涵盖图像输入、音频处理、视频理解,并给出一个能分析图表的数据分析 Agent 完整示例。
1.2 主流多模态 LLM 能力对比
| 模型 | 图像理解 | 音频输入 | 视频 | 图像生成 | 备注 |
|---|---|---|---|---|---|
| GPT-5.4 | 支持 | 支持 | 支持 | 不支持 | 综合能力最强,Computer Use GA |
| Claude Opus 4.6 | 支持 | 不支持 | 不直接支持 | 不支持 | 代码和指令遵从最强 |
| Gemini 3 Pro | 支持 | 支持 | 支持 | 支持 | 多模态最强,1M上下文 |
| Gemini 2.0 Flash | 支持 | 支持 | 支持 | 支持 | 速度快,性价比高 |
| DeepSeek V3 | 支持 | 不支持 | 不支持 | 不支持 | 开源,成本极低 |
图像理解是目前最成熟的多模态能力,主流模型都支持。音频原生输入(直接传入音频文件而非转录文字)仍是少数模型的能力,通用做法是先用 Whisper(OpenAI 开源的语音转文字模型)转文字再处理。
1.3 图像输入的两种方式
向 LLM 传入图像有两种方式:URL 直链和 base64 编码。
URL 方式:图片已在公网可访问,直接传 URL,LLM 在处理时会自动抓取图片。适合图片来自网络、CDN(Content Delivery Network,内容分发网络,将静态资源缓存到全球各地的服务器以加速访问)或公开存储桶的场景。
from openai import OpenAI
client = OpenAI()
def analyze_image_url(image_url: str, question: str) -> str:
"""通过 URL 传入图片,让 LLM 分析。"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": image_url,
# detail 控制图像处理精度:
# "low" - 固定 512x512,低 Token 消耗(约 85 tokens)
# "high" - 按原图分辨率处理,高精度(每 512x512 区块约 170 tokens)
# "auto" - 由模型自动选择(默认)
"detail": "high",
},
},
{"type": "text", "text": question},
],
}
],
max_tokens=1024,
)
return response.choices[0].message.content
# 示例:分析公网图表
result = analyze_image_url(
image_url="https://example.com/sales_chart.png",
question="这张图表显示了什么趋势?找出增长最快的季度。",
)
print(result)
base64 方式:图片在本地或私有存储中,无法通过 URL 访问。先读取文件转换为 base64 字符串,再传入 API。适合处理用户上传的文件、内网系统的截图等场景。
import base64
from pathlib import Path
def image_to_base64(image_path: str) -> tuple[str, str]:
"""
将本地图片转换为 base64 字符串。
返回 (base64_string, media_type) 元组。
媒体类型从文件扩展名推断,LLM 需要此信息才能正确解码。
"""
ext_to_type = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
}
path = Path(image_path)
media_type = ext_to_type.get(path.suffix.lower(), "image/jpeg")
image_data = base64.standard_b64encode(path.read_bytes()).decode("utf-8")
return image_data, media_type
def analyze_local_image(image_path: str, question: str) -> str:
"""分析本地图片文件。"""
image_data, media_type = image_to_base64(image_path)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
# base64 格式:data:{media_type};base64,{data}
"url": f"data:{media_type};base64,{image_data}",
"detail": "high",
},
},
{"type": "text", "text": question},
],
}
],
)
return response.choices[0].message.content
1.4 音频处理:Whisper + LLM
主流 LLM 的对话接口不直接接受音频文件(GPT-4o 的实时 API 例外)。通用做法是两步走:用 Whisper 将音频转录为文字,再将文字交给 LLM 处理。
from openai import OpenAI
from pathlib import Path
client = OpenAI()
def transcribe_audio(audio_path: str, language: str = "zh") -> str:
"""
使用 Whisper 将音频转录为文字。
支持格式:mp3, mp4, mpeg, mpga, m4a, wav, webm
单文件最大 25MB。
language 参数可以提升准确率,zh 表示中文。
"""
with open(audio_path, "rb") as audio_file:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language=language,
# response_format="verbose_json" 可以获取时间戳信息
response_format="text",
)
return transcript
def process_audio_query(audio_path: str, system_prompt: str = None) -> dict:
"""
完整的音频问答流程:转录 → LLM 处理 → 返回答案。
适用于语音助手、会议录音分析等场景。
"""
# 第一步:转录
transcription = transcribe_audio(audio_path)
print(f"转录结果:{transcription}")
# 第二步:LLM 处理
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": transcription})
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
)
return {
"transcription": transcription,
"response": response.choices[0].message.content,
}
# 示例:处理语音问题
result = process_audio_query(
audio_path="question.mp3",
system_prompt="你是一个客服助手,根据用户的语音问题提供帮助。",
)
1.5 视频理解:抽帧 + 多图分析
主流 LLM 不直接接受视频文件(Gemini 除外)。处理视频的通用方案是:按固定间隔抽取关键帧,将多张图片一起传入 LLM,让模型综合理解视频内容。
import cv2
import base64
from pathlib import Path
def extract_frames(video_path: str, interval_seconds: float = 5.0, max_frames: int = 20) -> list[str]:
"""
从视频中按时间间隔抽取帧,返回 base64 编码的图片列表。
interval_seconds:抽帧间隔(秒)
max_frames:最多抽取的帧数,防止 Token 超限
注意:帧数越多,Token 消耗越大。每帧在 high 模式下约 1000-2000 tokens。
"""
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(fps * interval_seconds)
frames = []
frame_count = 0
while len(frames) < max_frames:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_count)
ret, frame = cap.read()
if not ret:
break
# 将 OpenCV BGR 图像编码为 JPEG base64
_, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
frame_b64 = base64.standard_b64encode(buffer).decode("utf-8")
frames.append(frame_b64)
frame_count += frame_interval
cap.release()
return frames
def analyze_video(video_path: str, question: str, interval_seconds: float = 5.0) -> str:
"""
分析视频内容,将多帧图像一起传入 LLM。
"""
frames = extract_frames(video_path, interval_seconds=interval_seconds)
print(f"共抽取 {len(frames)} 帧")
# 构造多图消息
content = []
for i, frame_b64 in enumerate(frames):
content.append({
"type": "text",
"text": f"第 {i + 1} 帧(时间约 {i * interval_seconds:.0f}s):",
})
content.append({
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{frame_b64}",
"detail": "low", # 视频帧多,用 low 降低 Token 消耗
},
})
content.append({"type": "text", "text": question})
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": content}],
max_tokens=2048,
)
return response.choices[0].message.content
1.6 多模态 Agent 处理流程
1.7 完整示例:图表分析 Agent
以下是一个可以分析图表的数据分析 Agent,支持用户上传图表截图,Agent 自动识别图表类型、提取数据、给出分析结论。
import json
import base64
from pathlib import Path
from openai import OpenAI
client = OpenAI()
def extract_chart_data(image_source: str) -> dict:
"""
从图表图片中提取结构化数据。
image_source 可以是本地文件路径或 URL。
"""
# 判断输入类型:URL 还是本地文件
if image_source.startswith("http://") or image_source.startswith("https://"):
image_content = {
"type": "image_url",
"image_url": {"url": image_source, "detail": "high"},
}
else:
path = Path(image_source)
if not path.exists():
return {"success": False, "error": f"文件不存在:{image_source}"}
image_data = base64.standard_b64encode(path.read_bytes()).decode("utf-8")
ext = path.suffix.lower()
media_type = {"png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg"}.get(ext, "image/png")
image_content = {
"type": "image_url",
"image_url": {"url": f"data:{media_type};base64,{image_data}", "detail": "high"},
}
# 让 LLM 提取结构化数据
extraction_prompt = """请仔细分析这张图表,提取以下信息:
1. 图表类型(折线图、柱状图、饼图、散点图等)
2. 标题(如果有)
3. X 轴标签和单位
4. Y 轴标签和单位
5. 所有数据系列的名称和数值
请以 JSON 格式返回,格式如下:
{
"chart_type": "图表类型",
"title": "图表标题",
"x_axis": {"label": "标签", "unit": "单位"},
"y_axis": {"label": "标签", "unit": "单位"},
"data_series": [
{"name": "系列名", "data": [{"x": "...", "y": ...}]}
]
}
如果某个字段无法识别,设为 null。"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
image_content,
{"type": "text", "text": extraction_prompt},
],
}
],
response_format={"type": "json_object"}, # 强制 JSON 输出,避免格式混乱
max_tokens=2048,
temperature=0,
)
try:
data = json.loads(response.choices[0].message.content)
return {"success": True, "data": data}
except json.JSONDecodeError:
return {"success": False, "error": "JSON 解析失败", "raw": response.choices[0].message.content}
def analyze_chart_trends(chart_data: dict) -> str:
"""
基于提取的图表数据进行趋势分析。
将结构化数据转换为分析文字。
"""
if not chart_data.get("success"):
return f"数据提取失败:{chart_data.get('error')}"
data = chart_data["data"]
analysis_prompt = f"""基于以下图表数据,提供专业的数据分析报告:
{json.dumps(data, ensure_ascii=False, indent=2)}
请分析:
1. 主要趋势(上升/下降/波动)
2. 最高点和最低点
3. 显著的变化节点
4. 数据背后可能的原因和业务含义
5. 对未来走势的预判
请用简洁专业的语言表达,约 200-300 字。"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": analysis_prompt}],
temperature=0.3,
)
return response.choices[0].message.content
class ChartAnalysisAgent:
"""
图表分析 Agent。
支持用户持续上传图表并提问,维护多轮对话历史。
"""
def __init__(self):
self.conversation_history = []
self.analyzed_charts = [] # 已分析的图表数据缓存
def analyze(self, image_source: str, question: str = None) -> str:
"""
分析图表并回答用户问题。
如果没有具体问题,自动给出全面分析。
"""
print(f"正在分析图表:{image_source}")
# 步骤一:提取图表数据
chart_data = extract_chart_data(image_source)
if not chart_data["success"]:
return f"图表识别失败:{chart_data['error']}"
self.analyzed_charts.append(chart_data["data"])
# 步骤二:根据问题类型选择分析策略
if question is None:
# 无具体问题:给出全面分析
result = analyze_chart_trends(chart_data)
else:
# 有具体问题:带上图表数据和问题一起分析
self.conversation_history.append({
"role": "user",
"content": [
self._build_image_content(image_source),
{
"type": "text",
"text": f"图表数据:\n{json.dumps(chart_data['data'], ensure_ascii=False)}\n\n问题:{question}",
},
],
})
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "你是一个专业的数据分析师,擅长从图表中提取洞察。用简洁专业的语言回答问题。",
}
] + self.conversation_history,
max_tokens=1024,
)
result = response.choices[0].message.content
self.conversation_history.append({
"role": "assistant",
"content": result,
})
return result
def _build_image_content(self, image_source: str) -> dict:
"""构造图片输入内容。"""
if image_source.startswith("http"):
return {"type": "image_url", "image_url": {"url": image_source, "detail": "high"}}
image_data = base64.standard_b64encode(Path(image_source).read_bytes()).decode("utf-8")
return {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_data}", "detail": "high"}}
def compare_charts(self, question: str) -> str:
"""
基于已分析的多个图表进行比较分析。
需要先分析至少两张图表。
"""
if len(self.analyzed_charts) < 2:
return "需要至少两张图表才能进行比较分析"
prompt = f"""请比较以下 {len(self.analyzed_charts)} 张图表:
{json.dumps(self.analyzed_charts, ensure_ascii=False, indent=2)}
用户问题:{question}"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
# 使用示例
if __name__ == "__main__":
agent = ChartAnalysisAgent()
# 分析单张图表
analysis = agent.analyze(
image_source="sales_chart.png",
question="2024 年哪个季度的增长最显著?",
)
print("分析结果:", analysis)
# 继续追问(多轮对话)
followup = agent.analyze(
image_source="sales_chart.png",
question="与去年同期相比,整体表现如何?",
)
print("追问回答:", followup)
1.8 限制与注意事项
Token 消耗:图像会消耗大量 Token。一张 1024x1024 的图片在 high 模式下约消耗 1000+ tokens,是纯文字请求的数倍。批量处理图片时需要估算成本。
分辨率限制:各模型对最大图片尺寸有限制,通常为 2048x2048 或 4096x4096。超过限制的图片会被自动压缩,可能影响细节识别精度(如图表中的小字、精细数字)。
识别精度:LLM 的图像理解不是 OCR,复杂图表(多数据系列、密集文字、特殊图表类型)的识别精度会下降。精度要求高的场景建议结合专用 OCR 工具(如 Azure Document Intelligence)做二次验证。
隐私安全:图片传输到第三方 LLM API 时需考虑数据合规问题。包含敏感信息(人脸、证件、财务报告)的图片,使用前需评估数据安全风险。本地部署的多模态模型(如 Qwen-VL、LLaVA)是数据不出域的替代方案。
小结: 多模态 Agent 的核心在于将不同类型的输入(图片、音频、视频)统一转换为 LLM 可理解的格式,再利用 LLM 的理解能力完成分析和推理。图像通过 URL 或 base64 直接传入,音频经 Whisper 转录后处理,视频通过抽帧分解为多图。随着多模态模型能力的持续提升,Agent 处理非结构化媒体数据的准确率还在快速提高,这是一个值得持续关注的方向。