构建你的第一个多模态AI智能体:结合 Langchain、ModelScope 实现文生图与识图
在人工智能飞速发展的今天,多模态能力——即理解和生成多种类型信息(如文本、图片、音频)的能力——正成为 AI 应用的新前沿。Langchain 框架以其强大的编排能力,使得构建复杂的 AI 应用变得更加容易。本文将带你一步步构建一个基于 Langchain 和 ModelScope 平台的 AI 智能体,它不仅能根据文本描述生成图片(文生图),还能理解并描述图片内容(识1图)。
想象一下,你可以用自然语言命令你的 AI 助手:“画一只戴着宇航员头盔的猫”,然后它为你生成图片;你也可以上传一张图片,问它:“这张图片里有什么?”它会告诉你答案。这就是我们今天要构建的智能体的核心功能!
目录
- 项目概览与目标
- 环境准备与配置
- 安装必要库
- 设置环境变量
- 核心组件拆解
- Part 1: 初始化多模态大模型 (Qwen2.5-VL)
- Part 2: 图像处理工具集 (本地图片转 Base64)
- Part 3: 远程图片下载与保存模块
- Part 4: 工具 ① - 文生图 Tool (Text-to-Image)
- Part 5: 工具 ② - 识图 Tool (Image-to-Text/VQA)
- Part 6: Agent 的“大脑” - 选择决策型 LLM
- Part 7: 组装 Agent - 定义工具、Prompt 和执行器
- 运行与测试
- 示例 1: 文生图请求
- 示例 2: 识图请求 (网络图片 URL)
- 示例 3: 识图请求 (本地图片)
- 总结与展望
1. 项目概览与目标
本项目旨在创建一个 Langchain Agent,它具备以下两种核心的多模态交互能力:
- 文生图 (Text-to-Image): 用户输入一段文本描述,Agent 调用 ModelScope 上的文生图模型基于Flux(如
AIkaiyuanfenxiangKK/chengxuyuan
)生成相应的图片,并返回图片 URL。 - 识图 (Image-to-Text / Visual Question Answering): 用户提出关于某个图片的问题,并提供图片的 URL 或本地路径,Agent 调用 ModelScope 上的多模态视觉语言模型(如
Qwen/Qwen2.5-VL-72B-Instruct
)来回答问题或描述图片内容。
我们将使用 Langchain 的 Agent 机制,通过 ReAct (Reasoning and Acting) 范式,让一个语言模型(作为 Agent 的大脑)来决定何时以及如何调用上述工具。
2. 环境准备与配置
在开始之前,请确保你已经安装了 Python 和相关的库,并获取了 ModelScope 的 API 密钥。
安装必要库
pip install python-dotenv requests Pillow langchain langchain_openai
python-dotenv
: 用于从.env
文件加载环境变量。requests
: 用于发送 HTTP 请求到 ModelScope API。Pillow
: 用于图像处理,如缩放、格式转换。langchain
: 核心框架。langchain_openai
: Langchain 与 OpenAI 兼容 API(包括 ModelScope 提供的兼容 API)的集成。
### 设置环境变量
在你的项目根目录下创建一个 `.env` 文件,并填入你的 ModelScope API 密钥和 Base URL:
```env
MODELSCOPE_API_KEY="YOUR_MODELSCOPE_API_KEY"
MODELSCOPE_BASE_URL="YOUR_MODELSCOPE_API_BASE_URL" # 例如:[https://dashscope.aliyuncs.com/compatible-mode/v1](https://dashscope.aliyuncs.com/compatible-mode/v1)
注意: MODELSCOPE_BASE_URL
与 MODELSCOPE_API_KEY 是 ModelScope 提供的与 OpenAI API 兼容的端点。请查阅 ModelScope 文档以获取正确的 Base URL。对于Qwen系列模型,这通常是 DashScope 的兼容端点。(此处选取ModelScope因为每天有2000次免费调用,且包括文生图模型,Very Nice)
然后在你的 Python 脚本顶部加载这些变量:
# --- 确保在最顶部加载环境变量 ---
from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env file
# --- 导入必要的库 ---
import requests
import json
from PIL import Image, UnidentifiedImageError
from io import BytesIO
import os
import re
import base64 # 用于Base64转换
from langchain.agents import AgentExecutor, create_react_agent # 或 create_tool_calling_agent
from langchain import hub
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage # 用于构建多模态消息
# --- 获取 API 密钥和 Base URL 从环境变量 ---
modelscope_api_key = os.getenv("MODELSCOPE_API_KEY")
modelscope_base_url = os.getenv("MODELSCOPE_BASE_URL")
if not modelscope_api_key:
raise ValueError("MODELSCOPE_API_KEY environment variable not set.")
if not modelscope_base_url:
raise ValueError("MODELSCOPE_BASE_URL environment variable not set.")
3. 核心组件拆解
现在,我们来逐步解析构成这个多模态 Agent 的各个关键部分。
Part 1: 初始化多模态大模型 (Qwen2.5-VL)
这个模型将专门用于我们的“识图”工具。我们选择 Qwen/Qwen2.5-VL-72B-Instruct
作为示例,它是一个强大的视觉语言模型。
# --- 1. 定义并实例化多模态模型 (Qwen2.5-VL) ---
# 这个模型实例将在 Tool 内部使用
multimodal_model_name = "Qwen/Qwen2.5-VL-72B-Instruct" # 确保这是 ModelScope 上正确的模型ID
# 实例化 ChatOpenAI for Multimodal
# 注意:ModelScope 的多模态 API 可能需要特定的 model_kwargs,请查阅文档
chatLLM_multimodal = ChatOpenAI(
openai_api_key=modelscope_api_key,
openai_api_base=modelscope_base_url,
model_name=multimodal_model_name,
temperature=0.8 # 对多模态任务通常希望结果更客观准确
)
这里,我们使用 ChatOpenAI
类来与 ModelScope 提供的 OpenAI 兼容 API 交互。temperature
设置为较低的值,倾向于生成更确定和事实性的描述。
Part 2: 图像处理工具集 (本地图片转 Base64)
为了让多模态模型能够处理本地图片,我们需要将其转换为 Base64编码的 Data URL。这个过程包括读取、缩放和编码。
# --- Base64 转换函数 (如果需要处理本地图片) ---
def read_image_file(image_path: str) -> bytes:
"""读取图片文件并返回原始字节数据"""
try:
with open(image_path, "rb") as f:
return f.read()
except FileNotFoundError:
print(f"ERROR: Image file not found at {image_path}")
return None
except Exception as e:
print(f"ERROR: Failed to read image file: {e}")
return None
def process_image(image_bytes: bytes, max_size: tuple = (768, 768)) -> Image.Image:
"""处理图片数据,包括打开、缩放和格式转换"""
try:
print(f"DEBUG: Original image file size: {len(image_bytes)} bytes")
img = Image.open(BytesIO(image_bytes))
print(f"DEBUG: Original image size (W, H): {img.size}")
img.thumbnail(max_size, Image.Resampling.LANCZOS) # 缩放图片
print(f"DEBUG: Resized image size (W, H): {img.size} (max_size={max_size})")
# 处理一些常见的格式转换,例如确保JPEG不是RGBA
if img.format == 'JPEG' and img.mode == 'RGBA':
img = img.convert('RGB')
return img
except UnidentifiedImageError:
print("ERROR: Could not identify image format")
return None
except Exception as e:
print(f"ERROR: Failed to process image: {e}")
return None
def encode_image_to_base64(img: Image.Image) -> str:
"""将Pillow Image对象编码为Base64 Data URL"""
try:
buffered = BytesIO()
# 根据原始格式保存,优先使用 JPEG, PNG, GIF,否则默认为 PNG
save_format = img.format if img.format in ['JPEG', 'PNG', 'GIF'] else 'PNG'
mime_type = f'image/{save_format.lower()}'
if save_format == 'JPEG':
img.save(buffered, format="JPEG", quality=85) # JPEG可以指定质量
else:
img.save(buffered, format=save_format)
base64_encoded = base64.b64encode(buffered.getvalue()).decode('utf-8')
print(f"DEBUG: Final Base64 string length: {len(base64_encoded)}")
return f"data:{mime_type};base64,{base64_encoded}"
except Exception as e:
print(f"ERROR: Failed to encode image: {e}")
return None
def image_to_base64_data_url(image_path: str, max_size: tuple = (768, 768)) -> str:
"""将本地图片文件转换为Base64 Data URL"""
image_bytes = read_image_file(image_path)
if not image_bytes: return None
img = process_image(image_bytes, max_size)
if not img: return None
return encode_image_to_base64(img)
关键点:
read_image_file
: 读取本地图片为字节流。process_image
: 使用 Pillow 库打开图片,并将其缩放到max_size
(默认为 768x768)。这有助于减少数据量,并符合某些模型的输入限制。encode_image_to_base64
: 将处理后的 Pillow Image 对象保存到内存中的 BytesIO 对象,然后进行 Base64 编码,并构造成标准的 Data URL (e.g.,data:image/jpeg;base64,...
)。image_to_base64_data_url
: 整合了以上三个步骤,提供一个简单接口将本地图片路径直接转换为 Base64 Data URL。
Part 3: 远程图片下载与保存模块
当“文生图”工具生成图片后,它会返回一个图片 URL。这些辅助函数用于从该 URL 下载图片并保存到本地。
# --- 9. 定义图片下载辅助函数 ---
def extract_image_url(agent_output_string: str) -> str:
"""从 agent 输出字符串中提取图片 URL,支持普通URL或Markdown格式。"""
match = re.search(r'\[.*?\]\((.*?)\)', agent_output_string) # 尝试Markdown链接
if match:
return match.group(1)
# 如果没有Markdown链接,假设整个字符串就是URL(去除首尾空格)
return agent_output_string.strip()
def download_image(image_url: str) -> bytes:
"""从给定的URL下载图片。"""
if not image_url or not (image_url.startswith('http://') or image_url.startswith('https://')):
raise ValueError(f"Invalid image URL: {image_url}")
print(f"Attempting to download image from URL: {image_url}")
response = requests.get(image_url, stream=True, timeout=30)
response.raise_for_status() # 如果请求失败 (状态码 4xx 或 5xx), 则抛出异常
return response.content
def save_image(image_content: bytes, save_directory: str = "images/", filename: str = None) -> str:
"""将图片内容保存到本地文件。"""
current_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本文件所在目录
full_save_dir = os.path.join(current_dir, save_directory)
os.makedirs(full_save_dir, exist_ok=True) # 确保保存目录存在
img_name = filename or "downloaded_image.png" # 使用提供文件名或默认名
full_save_path = os.path.join(full_save_dir, img_name)
image = Image.open(BytesIO(image_content))
# 根据图片内容推断格式或默认为PNG
save_format = image.format if image.format in ['JPEG', 'PNG', 'GIF'] else 'PNG'
image.save(full_save_path, format=save_format)
return full_save_path
def download_image_from_agent_output(agent_output_string: str, save_directory: str = "images/") -> bool:
"""从Agent输出中提取URL,下载并保存图片。"""
try:
image_url = extract_image_url(agent_output_string)
if not image_url:
print(f"Error: Could not find a valid image URL in the agent output: {agent_output_string}")
return False
image_content = download_image(image_url)
# 尝试从URL中提取文件名
img_name = os.path.basename(image_url.split('?')[0]) # 去除查询参数再取文件名
if not img_name or '.' not in img_name : # 简单的文件名有效性检查
img_name = "generated_image.png" # 回退到默认文件名
print(f"Warning: Could not reliably extract filename from URL '{image_url}'. Using fallback name '{img_name}'.")
full_save_path = save_image(image_content, save_directory, img_name)
print(f"Successfully downloaded and saved image to {full_save_path}")
return True
except requests.exceptions.RequestException as e:
print(f"Error downloading image: {e}")
return False
except UnidentifiedImageError:
print("Error: Could not identify image format from the downloaded content.")
return False
except Exception as e:
print(f"An unexpected error occurred while processing or saving the image: {e}")
return False
注意: 在 save_image
函数中, os.path.dirname(__file__)
用于获取当前执行脚本的目录,确保 images/
文件夹相对于脚本位置创建。extract_image_url
增加了对 Markdown 格式链接的提取,以增强兼容性。download_image_from_agent_output
中的文件名提取逻辑也做了一些改进。
Part 4: 工具 ① - 文生图 Tool (Text-to-Image)
这个工具接收文本描述,调用 ModelScope 的文生图 API,并返回生成的图片 URL。
# --- 2. 定义文生图 Tool ---
def make_image_api_request(prompt: str) -> dict:
"""向ModelScope API发送图片生成请求"""
# 注意: ModelScope 的文生图 API 端点可能与 LLM 端点不同
# 这里使用的是通义万相的API端点示例,具体请查阅 ModelScope 文档
url = '[https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis](https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis)' # 示例URL,请替换为你的模型服务API
# 使用您的 ModelScope API Key
headers = {
'Authorization': f'Bearer {modelscope_api_key}',
'Content-Type': 'application/json'
# 'X-DashScope-Async': 'enable' # 如果需要异步,ModelScope可能有特定header
}
payload = {
'model': 'wanx-v1', # ModelScope上的文生图模型ID,例如通义万相的 'wanx-v1'
'input': {
'prompt': prompt
},
'parameters': { # 根据模型API文档调整参数
'size': '1024*1024', # 例如,指定图片尺寸
'n': 1 # 生成图片数量
}
}
response = requests.post(
url,
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
headers=headers,
timeout=60 # 增加超时时间,文生图可能较慢
)
response.raise_for_status()
return response.json()
def parse_image_api_response(response_data: dict) -> str:
"""解析ModelScope API的响应数据"""
# ModelScope 的响应格式可能各异,以下是一个通用示例,需要根据实际API调整
if response_data.get('status_code') == 200 or response_data.get('code') == 0 or 'output' in response_data: # 检查多种成功状态
# 尝试多种可能的路径获取图片 URL
images = response_data.get('output', {}).get('results') # 路径1
if not images and 'data' in response_data and 'task_status' in response_data['data']: # 路径2 (异步任务)
if response_data['data']['task_status'] == 'SUCCEEDED':
images = response_data['data'].get('results')
elif response_data['data']['task_status'] in ['PENDING', 'RUNNING']:
return f"Image generation task is still {response_data['data']['task_status']}. Task ID: {response_data['data'].get('task_id')}"
else:
return f"Image generation task failed or status unknown: {response_data['data'].get('message', 'No error message')}"
if images and isinstance(images, list) and len(images) > 0:
# 图像数据可能直接是 URL,也可能在某个字段下
first_image = images[0]
if isinstance(first_image, dict) and 'url' in first_image:
image_url = first_image.get('url')
elif isinstance(first_image, str): #直接是URL列表
image_url = first_image
else: # 其他嵌套结构
image_url = None # 需要根据实际API调整
if image_url:
return image_url
return "Error: Image URL not found or in unexpected format in the API response."
# 处理API返回的错误信息
error_message = response_data.get('message') or \
response_data.get('Output', {}).get('Message') or \
response_data.get('error', {}).get('message') or \
"Unknown error"
return f"Error from ModelScope API: {error_message}"
@tool
def generate_image_from_text(prompt: str) -> str:
"""
Generates an image based on a text description (prompt) using the ModelScope text-to-image API.
Input should be a string representing the image description.
Returns the URL of the generated image if successful, or an error message.
"""
try:
print(f"Text-to-Image Tool: Received prompt - '{prompt}'")
# 确保使用正确的文生图模型API和payload结构
# 注意:您代码中 `make_image_api_request` 使用的 `AIkaiyuanfenxiangKK/chengxuyuan`
# 其API端点和请求体可能与DashScope通用文生图不同。
# 以下保持您原有的请求结构,但请务必确认其正确性。
# 如果 'AIkaiyuanfenxiangKK/chengxuyuan' 是通过DashScope的某个通用模型服务部署的,
# 那么请求结构可能需要参照DashScope文档。
# 如果它有独立的API,则保持原有结构。
# 为清晰起见,我们假设您的 `make_image_api_request` 已适配目标模型。
# 您的原始请求结构:
# url = '[https://api-inference.modelscope.cn/v1/images/generations](https://api-inference.modelscope.cn/v1/images/generations)'
# payload = {
# 'model': 'AIkaiyuanfenxiangKK/chengxuyuan', # 这个模型ID需要确认
# 'prompt': prompt
# }
# headers = {
# 'Authorization': f'Bearer {modelscope_api_key}',
# 'Content-Type': 'application/json'
# }
# response = requests.post(url, data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), headers=headers, timeout=60)
# response.raise_for_status()
# response_data = response.json()
# 使用更新后的、更通用的 make_image_api_request 和 parse_image_api_response
response_data = make_image_api_request(prompt) # 调用上面适配过的函数
image_url = parse_image_api_response(response_data)
if image_url and image_url.startswith('http'):
print(f"Generated image URL: {image_url}")
download_image_from_agent_output(image_url) # 自动下载图片
return image_url
else:
# image_url 可能是错误信息或者异步任务提示
print(f"Text-to-Image Tool: API did not return a valid URL. Response/Error: {image_url}")
return image_url # 返回错误信息或提示
except requests.exceptions.RequestException as e:
return f"Error calling ModelScope API: {e}"
except json.JSONDecodeError:
return "Error: Failed to parse JSON response from ModelScope API."
except Exception as e:
return f"An unexpected error occurred in generate_image_from_text: {e}"
重要更新与说明:
- API 端点与 Payload 结构: ModelScope 上的不同模型(尤其是文生图模型)可能有不同的 API 端点和请求/响应结构。我修改了
make_image_api_request
和parse_image_api_response
以使用一个更通用的 DashScope 文生图 API 示例 (wanx-v1
模型)。你需要根据你实际使用的文生图模型的 API 文档来调整url
、payload
(包括model
ID 和parameters
) 以及响应解析逻辑。 你原始代码中使用的https://api-inference.modelscope.cn/v1/images/generations
和模型AIkaiyuanfenxiangKK/chengxuyuan
的具体API细节需要核实。 - 错误处理和响应解析:
parse_image_api_response
增强了对不同成功和错误响应格式的适应性,并能处理异步任务的中间状态。 - 自动下载: 工具成功生成图片 URL 后,会调用
download_image_from_agent_output
自动下载图片到本地images/
目录。 @tool
装饰器: 将generate_image_from_text
函数注册为一个 Langchain Tool,使其可以被 Agent 调用。
Part 5: 工具 ② - 识图 Tool (Image-to-Text/VQA)
这个工具的核心是利用 Part 1 初始化的 chatLLM_multimodal
(Qwen2.5-VL) 来理解图片内容并回答相关问题。
# --- 3. 定义识图 Tool (使用 Qwen2.5-VL) ---
def process_image_identifier(image_identifier: str, max_size: tuple = (768, 768)) -> dict or str:
"""
处理图片标识符 (URL、Base64 Data URL 或本地路径)。
返回 Langchain 多模态消息中图片部分所需的字典,或错误字符串。
"""
# 检查是否是完整的 Base64 Data URL
if image_identifier.lower().startswith('data:image'):
print("Recognized image identifier as a Base64 Data URL.")
return {"type": "image_url", "image_url": {"url": image_identifier}}
# 检查是否是普通 URL
elif image_identifier.lower().startswith('http://') or image_identifier.lower().startswith('https://'):
print(f"Recognized image identifier as a web URL: {image_identifier}")
return {"type": "image_url", "image_url": {"url": image_identifier}}
# 否则,假定为本地文件路径,尝试转换为 Base64 Data URL
else:
print(f"Assuming image identifier is a local file path, attempting Base64 conversion: {image_identifier}")
base64_data_url = image_to_base64_data_url(image_identifier, max_size) # 使用定义好的转换函数
if not base64_data_url:
return f"Error: Could not convert local image file '{image_identifier}' to Base64. Check file path or format."
print("Successfully converted local path to Base64 Data URL.")
return {"type": "image_url", "image_url": {"url": base64_data_url}}
@tool
def describe_image_with_vl(input_string: str) -> str:
"""
使用视觉语言模型(Qwen2.5-VL)回答关于图片的问题。
输入字符串格式应为 'QUESTION: <你的问题> IMAGE: <图片URL、本地路径或Base64 Data URL>'。
例如: 'QUESTION: 这张图片里有什么动物? IMAGE: /path/to/your/image.jpg'
或: 'QUESTION: 描述这张图。 IMAGE: [http://example.com/image.png](http://example.com/image.png)'
或: 'QUESTION: 这是什么? IMAGE: data:image/jpeg;base64,...'
严格按照 'QUESTION: ... IMAGE: ...' 格式,问题和图片标识符之间用 ' IMAGE: ' 分隔。
"""
try:
# 使用更鲁棒的分割方法
if ' IMAGE: ' not in input_string:
return "Error: Input string is not formatted correctly. Expected 'QUESTION: <Your question> IMAGE: <Image Identifier>' with ' IMAGE: ' as a separator."
parts = input_string.split(' IMAGE: ', 1)
question_part = parts[0]
image_identifier = parts[1].strip()
if not question_part.upper().startswith('QUESTION:'):
return "Error: Question part missing or not starting with 'QUESTION:'."
question = question_part[len('QUESTION:'):].strip()
if not question:
return "Error: No question provided in the input string."
if not image_identifier:
return "Error: No image identifier (URL, path, or Base64 data) provided."
print(f"Image Description Tool - Question: '{question}', Image Identifier: '{image_identifier[:70]}...'")
# 处理图片标识符 (URL、本地路径或直接的Base64 Data URL)
image_content_part = process_image_identifier(image_identifier)
if isinstance(image_content_part, str): # 如果返回的是错误信息字符串
return image_content_part
# 构建多模态消息
# Qwen-VL API 要求图片内容在文本内容之后
multimodal_message_content = [
{"type": "text", "text": question},
image_content_part # 这是包含 {"type": "image_url", "image_url": {"url": "..."}} 的字典
]
multimodal_message = HumanMessage(content=multimodal_message_content)
# 调用多模态模型
print("Calling Qwen2.5-VL model with message content:", multimodal_message_content)
response = chatLLM_multimodal.invoke([multimodal_message])
print("Qwen2.5-VL response received.")
return response.content
except Exception as e:
# 提供更详细的错误追踪信息
import traceback
print(f"ERROR in describe_image_with_vl: {e}\n{traceback.format_exc()}")
return f"An error occurred in the describe_image_with_vl tool: {e}"
关键改进与说明:
process_image_identifier
函数: 这个新函数负责智能地处理image_identifier
。它可以识别完整的 Base64 Data URL、普通的网络 URL,如果两者都不是,则假定为本地文件路径并尝试使用前面定义的image_to_base64_data_url
进行转换。这使得工具的输入更加灵活。- 输入格式:
@tool
的描述中明确了input_string
的期望格式:QUESTION: <问题> IMAGE: <图片URL、本地路径或Base64 Data URL>
。 正则表达式被替换为更简单的字符串分割,以提高鲁棒性。 HumanMessage
构建:content
参数现在是一个列表,包含一个文本部分和由process_image_identifier
生成的图片部分。这是 Langchain 构建多模态消息的标准方式。 顺序很重要:对于 Qwen-VL API,通常图片信息应放在文本提示之后。- 错误处理: 增加了更详细的错误打印,包括 traceback,便于调试。
Part 6: Agent 的“大脑” - 选择决策型 LLM
这个 LLM 负责理解用户的总体指令,并决定调用哪个工具(文生图或识图)、以及如何组织输入给这些工具。我们选择一个强大的通用对话模型。
# --- 5. 定义 Agent 使用的 LLM (智能体的思考核心) ---
# 这个 LLM 负责理解用户指令并选择工具
agent_llm_name = "Qwen/Qwen-235B-A22B" # 您选择的 Agent LLM, 例如 Qwen2-72B-Instruct,或者任何强大的对话模型
# agent_llm_name = "Qwen/Qwen2-7B-Instruct" # 也可以是小一点的模型,但能力会稍弱
agent_llm = ChatOpenAI(
openai_api_key=modelscope_api_key,
openai_api_base=modelscope_base_url,
model_name=agent_llm_name,
temperature=0.7 # Agent LLM 可以稍微有些创造性,但仍需精确
# model_kwargs={"extra_body": {"enable_thinking": True}} # 根据ModelScope API文档决定是否需要
)
选择 Agent LLM 的考量:
- 理解能力: 需要能准确理解用户意图,并从用户输入中提取必要信息(如文生图的 prompt,识图的问题和图片位置)。
- 工具调用能力: 如果使用
create_tool_calling_agent
(推荐),模型需要支持函数调用/工具调用。如果使用create_react_agent
,模型需要能生成符合 ReAct 格式的思考和行动指令。 Qwen/Qwen2-72B-Instruct
或类似的 Qwen 模型通常表现良好。即使是Qwen/Qwen2-7B-Instruct
也可能胜任,但复杂指令的理解能力会稍逊一筹。temperature
设为 0.7 允许一定的灵活性,同时保持决策的合理性。
Part 7: 组装 Agent - 定义工具、Prompt 和执行器
现在,我们将所有部件组装起来。
# --- 4. 定义 Agent 需要使用的工具列表 ---
tools = [
generate_image_from_text, # 文生图 Tool
describe_image_with_vl # 识图 Tool
]
# --- 6. 获取 Agent Prompt ---
# 使用 Langchain Hub 提供的标准 ReAct prompt
# 这个 prompt 指导 LLM 如何思考、选择工具、观察结果并给出最终答案
prompt = hub.pull("hwchase17/react")
# 对于支持工具调用的模型,可以考虑: hub.pull("hwchase17/openai-functions-agent") 或其他tool-calling prompt
# --- 7. 创建 Agent ---
# create_react_agent 适用于不一定原生支持函数调用的模型,通过特定的prompt格式让模型输出工具调用指令
agent = create_react_agent(agent_llm, tools, prompt)
# 如果你的 agent_llm (如较新的Qwen模型) 明确支持并优化了工具调用 (Function Calling):
# from langchain.agents import create_tool_calling_agent
# prompt = hub.pull("hwchase17/openai-tools-agent") # 这是一个 tool calling prompt 示例
# agent = create_tool_calling_agent(agent_llm, tools, prompt)
# --- 8. 创建 Agent Executor ---
# AgentExecutor 负责实际运行 Agent 的思考-行动循环
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
# verbose=True: 打印 Agent 的完整思考过程,非常有助于调试
# handle_parsing_errors=True: 帮助处理Agent输出格式有时不完全符合预期的情况
关键步骤:
tools
列表: 将我们定义好的两个@tool
函数放入列表。prompt
: 从 Langchain Hub 拉取一个预设的 Agent Prompt。hwchase17/react
是一个经典的 ReAct prompt,它指导 LLM 进行 "Thought, Action, Action Input, Observation" 的循环。- 如果你的
agent_llm
强大且支持原生的工具调用 (OpenAI Function Calling 风格),使用create_tool_calling_agent
和相应的 prompt (如hwchase17/openai-tools-agent
) 通常更高效、更可靠。
- 如果你的
create_react_agent
: 根据 LLM、工具集和 Prompt 创建 Agent 逻辑。AgentExecutor
: 这是真正运行 Agent 的地方。verbose=True
对调试至关重要,它会显示 Agent 的每一步思考和行动。handle_parsing_errors=True
可以让 Agent 对 LLM 输出的一些小格式问题有更强的容错性。
4. 运行与测试
一切准备就绪!现在我们可以通过向 agent_executor.invoke
传递用户输入来测试我们的多模态 Agent 了。
# --- 10. 运行 Agent ---
print("Running agent...")
# 示例 1: 文生图请求
user_query_generate = "帮我画一张科幻风格的城市夜景,要有飞行汽车和霓虹灯。"
print(f"\n\n--- USER QUERY (TEXT-TO-IMAGE) ---\n{user_query_generate}")
response_generate = agent_executor.invoke({"input": user_query_generate})
print("\n--- AGENT RESPONSE (TEXT-TO-IMAGE) ---")
print(response_generate['output'])
# 生成的图片会自动下载到 images/ 目录 (如果成功)
print("\n" + "="*80 + "\n")
# 示例 2: 识图请求 (提供图片 URL)
image_url_for_description = "[https://modelscope.oss-cn-beijing.aliyuncs.com/demo/images/audrey_hepburn.jpg](https://modelscope.oss-cn-beijing.aliyuncs.com/demo/images/audrey_hepburn.jpg)" # 奥黛丽赫本的经典照片
# user_query_describe_url = f"QUESTION: 请描述这张图片中的主要人物和她的情绪。 IMAGE: {image_url_for_description}"
user_query_describe_url = f"这张图片里是谁?帮我描述一下她的穿着和表情。图片来源是:{image_url_for_description}"
# 对于ReAct Agent,你需要让Agent自己决定如何调用识图工具。
# Agent LLM 需要从这个自然语言输入中,识别出问题是 "这张图片里是谁?帮我描述一下她的穿着和表情。"
# 以及图片标识符是 "[https://modelscope.oss-cn-beijing.aliyuncs.com/demo/images/audrey_hepburn.jpg](https://modelscope.oss-cn-beijing.aliyuncs.com/demo/images/audrey_hepburn.jpg)"
# 然后格式化为 'QUESTION: <parsed_question> IMAGE: <parsed_image_url>' 传递给 describe_image_with_vl 工具
print(f"\n\n--- USER QUERY (IMAGE-TO-TEXT - URL) ---\n{user_query_describe_url}")
response_describe_url = agent_executor.invoke({"input": user_query_describe_url})
print("\n--- AGENT RESPONSE (IMAGE-TO-TEXT - URL) ---")
print(response_describe_url['output'])
print("\n" + "="*80 + "\n")
# 示例 3: 识图请求 (提供本地图片路径)
# 确保你有一个本地图片,例如项目下的 'images/my_local_image.jpg'
# 为了测试,你可以先运行一次文生图,然后用生成的图片进行识图
# 假设 images/ 目录下已有一张名为 "generated_image.png" 的图片
local_image_path_for_description = "images/generated_image.png" # 替换为你的本地图片路径
if not os.path.exists(local_image_path_for_description):
print(f"Warning: Local image for description not found at '{local_image_path_for_description}'. Skipping example 3.")
else:
# user_query_describe_local = f"QUESTION: 这张本地图片里画了什么? IMAGE: {local_image_path_for_description}"
user_query_describe_local = f"我电脑上有一张图片,路径是 {local_image_path_for_description},告诉我这张图片的主要内容是什么?"
print(f"\n\n--- USER QUERY (IMAGE-TO-TEXT - LOCAL FILE) ---\n{user_query_describe_local}")
response_describe_local = agent_executor.invoke({"input": user_query_describe_local})
print("\n--- AGENT RESPONSE (IMAGE-TO-TEXT - LOCAL FILE) ---")
print(response_describe_local['output'])
关于用户输入 (User Query) 的说明:
- 文生图: 用户可以直接用自然语言描述想生成的图片内容。Agent LLM 会识别这个意图,并提取描述作为
prompt
传递给generate_image_from_text
工具。 - 识图:
- 用户需要同时提供问题和图片信息 (URL 或本地路径)。
- 关键在于 Agent LLM (
agent_llm
) 如何理解这个输入并正确地格式化它以调用describe_image_with_vl
工具。describe_image_with_vl
工具期望的输入格式是QUESTION: ... IMAGE: ...
。 - 当用户输入
这张图片里是谁?图片来源是:{image_url_for_description}
时,ReAct Agent 的agent_llm
会在其 "Thought" 过程中分析这个输入,识别出用户的意图是想描述图片,并提取出问题部分 ("这张图片里是谁?") 和图片来源部分 (image_url_for_description
)。然后,它会构造出类似Action Input: QUESTION: 这张图片里是谁? IMAGE: https://...jpg
的指令来调用describe_image_with_vl
工具。 - 这种方式更自然,但对
agent_llm
的理解能力要求更高。如果agent_llm
难以正确解析,你可能需要在用户输入时就采用更接近工具期望的格式,或者在 Agent 的 Prompt 中给出更明确的指示。
调试提示:
仔细观察 verbose=True 打印出的 Agent 思考过程。你会看到 Agent 如何解析用户输入,选择哪个工具,以及传递给工具的实际参数是什么。这是理解和优化 Agent 行为的最佳方式。
5. 总结与展望
恭喜你!你已经成功构建并理解了一个可以进行文生图和识图的多模态 AI 智能体。通过 Langchain 的编排和 ModelScope 提供的强大模型,我们实现了复杂 AI 应用的快速搭建。
本项目关键点回顾:
- 使用独立的 LLM (
agent_llm
) 进行决策和工具选择。 - 为识图功能专门实例化了一个多模态 LLM (
chatLLM_multimodal
)。 - 实现了灵活的图片处理,支持网络 URL、本地路径和 Base64 Data URL。
- 定义了两个核心工具:
generate_image_from_text
和describe_image_with_vl
。 - 通过 Langchain Agent (ReAct 范式) 将所有组件有机地结合起来。
未来可探索的方向:
- 更智能的工具输入构造: 优化 Agent LLM 的 Prompt,使其能更鲁棒地从非常自由的用户输入中提取信息并格式化给工具。
- 支持多图输入: 修改
describe_image_with_vl
工具以接受和处理多张图片。 - 集成更多模态: 例如增加语音输入/输出能力。
- 错误处理与用户反馈: 建立更完善的错误处理机制,并允许用户对 Agent 的行为进行反馈和纠正。
- 流式输出: 对于生成时间较长的任务(如复杂的图像生成或视频理解),实现流式输出以改善用户体验。
- 尝试 Tool Calling Agent: 如果你的
agent_llm
和 ModelScope API 对工具调用有良好支持,切换到create_tool_calling_agent
可能会带来性能和可靠性的提升。
希望本篇能帮助你深入理解并动手实践 Langchain 多模态 Agent 的构建。AI 的世界充满了无限可能,期待你创造出更惊艳的应用!