用 Python 代码实现 Mage 多模态对话的调用骨架

文章导读
要用Python调通Mage多模态对话,核心是先定好HTTP请求骨架,而不是依赖特定SDK。以下代码假设Mage服务暴露/chat端点,接收含文本和图像的JSON消息,返回带reply字段的响应。你可以根据实际接口调整URL、字段名和鉴权方式。
📋 目录
  1. A 搭建一个最简的 HTTP 客户端
  2. B 定义多模态输入数据模型
  3. C 封装对话管理与返回解析
  4. D 增加超时重试与日志记录
  5. E 编写测试用例验证调用链路
A A

要用Python调通Mage多模态对话,核心是先定好HTTP请求骨架,而不是依赖特定SDK。以下代码假设Mage服务暴露/chat端点,接收含文本和图像的JSON消息,返回带reply字段的响应。你可以根据实际接口调整URL、字段名和鉴权方式。

对于Mage这类多模态对话服务,调用骨架可以统一为:组装输入→发POST请求→解析响应。适用场景是开发者已有服务地址和接口文档;操作动作为按示例封装请求和解析;验证方式为用mock测试模拟响应;风险边界是不同部署版本字段可能不同,需要结合环境确认。

搭建一个最简的 HTTP 客户端

请求层只做一件事:把消息列表序列化成JSON,POST到Mage的接口,并返回反序列化后的字典。以下函数用requests实现了这个动作,并显式设置超时,避免连接挂死。

import requests

def call_mage_api(messages, api_url='http://127.0.0.1:8000/chat', timeout=30):
    payload = {'messages': messages}
    resp = requests.post(api_url, json=payload, timeout=timeout)
    resp.raise_for_status()  # 非2xx状态码会抛异常
    return resp.json()

这个函数是后续所有调用链路的底层。如果你的Mage服务需要鉴权,可以在headers里加Authorization字段,但这里先保持最简。验证方式:在本地启动服务后,直接调用这个函数,检查返回的dict是否符合预期。

定义多模态输入数据模型

为了不让文本、图片混杂在裸字典里,建议用dataclass固定消息结构。下面分别定义文本块和图像块,再组合成一条用户消息。

from dataclasses import dataclass, field
from typing import List, Dict

@dataclass
class ImagePart:
    type: str = 'image'
    source: str = ''  # 图片URL或base64字符串

@dataclass
class TextPart:
    type: str = 'text'
    text: str = ''

@dataclass
class Message:
    role: str = 'user'
    content: List[Dict] = field(default_factory=list)

    def add_text(self, text: str):
        self.content.append(TextPart(text=text).__dict__)

    def add_image(self, source: str):
        self.content.append(ImagePart(source=source).__dict__)

    def to_dict(self):
        return {'role': self.role, 'content': self.content}

这里用__dict__把dataclass实例转成dict。字段名需要与Mage接口约定一致;如果不一致,就改这里的属性名。如果后续新增音频等模态,只需增加对应的Part类并添加到content列表。

封装对话管理与返回解析

容器类MageClient负责维护对话历史,并把用户输入转换为消息模型,最后解析返回。假设接口返回格式为{'reply': '模型生成文本'},你按实际情况调整取值。

用 Python 代码实现 Mage 多模态对话的调用骨架
class MageClient:
    def __init__(self, api_url='http://127.0.0.1:8000/chat', timeout=30):
        self.api_url = api_url
        self.timeout = timeout
        self.history = []  # 保存对话上下文

    def chat(self, text=None, image_source=None):
        msg = Message(role='user')
        if text:
            msg.add_text(text)
        if image_source:
            msg.add_image(image_source)
        if not msg.content:
            raise ValueError('至少需要提供文本或图像之一')

        messages = [m.to_dict() for m in self.history] + [msg.to_dict()]
        data = call_mage_api(messages, self.api_url, self.timeout)
        reply = data.get('reply', '')
        if not reply:
            raise RuntimeError('响应中未找到reply字段')

        self.history.append(msg)
        self.history.append(Message(role='assistant', content=[TextPart(text=reply).__dict__]))
        return reply

这里把history存成Message对象,发送前统一转dict。返回时只提取reply,其他字段如思考过程、工具调用等可自行扩展。对话管理的关键是每次请求携带完整上下文,否则模型会丢失前文。

增加超时重试与日志记录

网络调用需要具备重试和可观测性。下面定义了一个重试装饰器,对requests.RequestException进行指数退避重试,并用logging记录每次失败的上下文。

import logging
import time
from functools import wraps

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('mage_client')

def retry_on_exception(retries=3, delay=1.0, exceptions=(requests.RequestException,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(retries):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    logger.warning('Mage调用失败(第%d次):%s', attempt + 1, e)
                    if attempt == retries - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry_on_exception(retries=3)
def call_mage_api_with_retry(messages, api_url, timeout):
    return call_mage_api(messages, api_url, timeout)

在第三小节的chat方法中,把调用call_mage_api的那一行替换为call_mage_api_with_retry即可。重试次数和延迟可以根据网络状况调整;注意幂等性,如果请求可能产生副作用,重试需谨慎。

编写测试用例验证调用链路

unittest配合unittest.mock模拟requests.post,这样不依赖真实服务也能验证请求构造和返回解析。下面给出两个测试:一个只传文本,一个验证图像字段是否被正确装入payload。

import unittest
from unittest.mock import patch, MagicMock

class TestMageClient(unittest.TestCase):
    @patch('requests.post')
    def test_chat_returns_reply(self, mock_post):
        mock_post.return_value = MagicMock(
            json=lambda: {'reply': '你好'},
            raise_for_status=lambda: None,
        )
        client = MageClient()
        reply = client.chat(text='你好')
        self.assertEqual(reply, '你好')

    @patch('requests.post')
    def test_chat_payload_includes_image(self, mock_post):
        def fake_json():
            return {'reply': '看到图片了'}
        mock_resp = MagicMock(json=fake_json, raise_for_status=lambda: None)
        mock_post.return_value = mock_resp

        client = MageClient()
        client.chat(text='描述图片', image_source='https://example.com/a.png')

        args, kwargs = mock_post.call_args
        payload = kwargs['json']
        content = payload['messages'][0]['content']
        self.assertTrue(any(item['type'] == 'image' for item in content))

测试中的call_args可以从mock中取出最近一次调用的参数,用于断言请求体内的结构。运行python -m unittest即可执行。注意测试文件需要能导入MageClient等定义。