构建你的第一个多模态AI智能体:结合 Langchain、ModelScope 实现文生图与识图

构建你的第一个多模态AI智能体:结合 Langchain、ModelScope 实现文生图与识图

在人工智能飞速发展的今天,多模态能力——即理解和生成多种类型信息(如文本、图片、音频)的能力——正成为 AI 应用的新前沿。Langchain 框架以其强大的编排能力,使得构建复杂的 AI 应用变得更加容易。本文将带你一步步构建一个基于 Langchain 和 ModelScope 平台的 AI 智能体,它不仅能根据文本描述生成图片(文生图),还能理解并描述图片内容(识1图)。

想象一下,你可以用自然语言命令你的 AI 助手:“画一只戴着宇航员头盔的猫”,然后它为你生成图片;你也可以上传一张图片,问它:“这张图片里有什么?”它会告诉你答案。这就是我们今天要构建的智能体的核心功能!

目录

  1. 项目概览与目标
  2. 环境准备与配置
    • 安装必要库
    • 设置环境变量
  3. 核心组件拆解
    • 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 和执行器
  4. 运行与测试
    • 示例 1: 文生图请求
    • 示例 2: 识图请求 (网络图片 URL)
    • 示例 3: 识图请求 (本地图片)
  5. 总结与展望

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_requestparse_image_api_response 以使用一个更通用的 DashScope 文生图 API 示例 (wanx-v1 模型)。你需要根据你实际使用的文生图模型的 API 文档来调整 urlpayload (包括 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输出格式有时不完全符合预期的情况

关键步骤:

  1. tools 列表: 将我们定义好的两个 @tool 函数放入列表。
  2. 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) 通常更高效、更可靠。
  3. create_react_agent: 根据 LLM、工具集和 Prompt 创建 Agent 逻辑。
  4. 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 的世界充满了无限可能,期待你创造出更惊艳的应用!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值