从零构建AI时尚顾问:Stylist MCP Server开发全记录

从零构建AI时尚顾问:Stylist MCP Server开发全记录

引言:当AI遇见时尚

"推荐3套适合约会的优雅穿搭"——这样一句简单的自然语言请求,背后需要怎样的技术栈来实现?

最近我完成了一个有趣的项目:Stylist MCP Server,一个AI驱动的时尚推荐服务。它能理解用户的穿搭需求(支持中英文),从53,789件服装数据库中智能检索,并给出专业的搭配建议。更重要的是,它遵循Anthropic的Model Context Protocol (MCP)标准,可以无缝集成到Claude Desktop、Cursor等AI工具中。

今天,我想分享整个开发过程中的设计思路、技术选型和踩坑经验。

项目架构总览

在深入细节之前,先看看整体架构:

┌─────────────────┐                              ┌─────────────────┐
│  Claude Desktop │                              │                 │
│  / Cursor       │──┬── stdio ──────────────────│                 │
└─────────────────┘  │                           │                 │
                     ├── SSE (legacy) ───────────│  MCP Server     │──▶ ChromaDB
┌─────────────────┐  │                           │  (Starlette)    │    (53,789 items)
│  VS Code        │──┴── Streamable HTTP ⭐ ─────│                 │
│  + Skill        │      (推荐)                  │                 │
└─────────────────┘                              └────────┬────────┘
                                                 ┌────────▼─────────┐
                                                 │  LLM Client      │
                                                 │  ├─ Anthropic    │
                                                 │  ├─ Azure OpenAI │
                                                 │  └─ OpenAI       │
                                                 └──────────────────┘

核心组件包括:

  • MCP Server: 基于Starlette的异步HTTP服务,支持三种传输模式(stdio/SSE/Streamable HTTP)
  • ChromaDB: 向量数据库,存储服装的语义嵌入和元数据
  • LLM Client: 统一的大模型接口层,支持多provider切换
  • Skill Layer: 配套的展示层,将MCP返回结果转化为精美的可视化报告
  • Nginx + Let's Encrypt: 生产环境的HTTPS反向代理

第一步:构建服装向量索引

数据源:DressCode数据集

我使用的是DressCode数据集,包含约54,000张高质量服装图片,分为三个类别:

  • upper_body: 上装(T恤、衬衫、外套等)
  • lower_body: 下装(裤子、裙子、短裤等)
  • dresses: 连衣裙

每件服装都有详细的属性标注,包括颜色、风格、适用场合、季节等。

ChromaDB索引构建

ChromaDB的优势在于开箱即用的向量搜索能力,同时支持元数据过滤。我的索引策略是:

# 将服装属性转换为自然语言描述
def build_description(attrs: dict) -> str:
    parts = []
    if attrs.get("colors"):
        parts.append(f"Colors: {', '.join(attrs['colors'])}")
    if attrs.get("styles"):
        parts.append(f"Styles: {', '.join(attrs['styles'])}")
    if attrs.get("occasions"):
        parts.append(f"Occasions: {', '.join(attrs['occasions'])}")
    # ... 更多属性
    return " | ".join(parts)

# 添加到ChromaDB
collection.add(
    ids=[garment_id],
    documents=[description],  # 用于语义搜索
    metadatas=[{
        "category": "upper_body",
        "garment_type": "t-shirt",
        "colors": json.dumps(["white", "blue"]),
        # ... 结构化元数据用于过滤
    }]
)

关键设计决策:

  1. 描述文本用于语义搜索:将属性拼接成自然语言,让嵌入模型理解语义
  2. 元数据用于精确过滤:category、garment_type等字段支持精确匹配
  3. 混合检索策略:先用元数据缩小范围,再用向量相似度排序

构建完成后,53,789件服装被索引到ChromaDB,支持毫秒级检索。

第二步:LLM意图解析

用户的自然语言请求需要被解析为结构化的搜索参数。这是整个系统的"大脑"。

Prompt工程:跨模型兼容性

最初我的prompt是这样的:

Extract the following parameters:

1. LANGUAGE & MODE DETECTION:
   - language: "zh" | "en"
   - recommendation_mode: "single_item" | "full_outfit"
   ...

2. GARMENT FILTERS:
   - garment_type: ...
   ...

这个prompt在Claude Haiku上工作完美,但切换到GPT-4o-mini时出问题了——GPT返回了嵌套的JSON结构,完全按照prompt中的分类标题组织:

{
  "LANGUAGE & MODE DETECTION": {
    "language": "en",
    "recommendation_mode": "single_item"
  },
  "GARMENT FILTERS": { ... }
}

而我的代码期望的是扁平结构:intent.get("recommendation_mode")直接返回None!

教训:不同LLM对prompt的理解方式不同。GPT更"忠实"地遵循格式,Claude更"聪明"地推断意图。

解决方案:重写prompt,明确要求扁平JSON:

Extract these parameters and return a FLAT JSON object (no nested objects):

- language: "zh" | "en"
- recommendation_mode: "single_item" | "full_outfit"
- garment_type: one of ["dress", "t-shirt", ...] or null
...

IMPORTANT: Return ONLY a flat JSON object with all fields at the top level.
Do NOT use nested structures or category headers.

修改后,两个模型都能正确返回扁平JSON,测试全部通过(14/14)。

第三步:多模型LLM支持

为了同时支持开发环境(本地Agent Maestro代理Claude)和生产环境(Azure OpenAI GPT-4o-mini),我设计了一个统一的LLM抽象层:

# src/llm_client.py
from abc import ABC, abstractmethod

class LLMClient(ABC):
    @abstractmethod
    def chat(self, messages: list, max_tokens: int, timeout: int) -> str:
        pass

class AnthropicClient(LLMClient):
    """支持Agent Maestro代理或直接Anthropic API"""
    def chat(self, messages, max_tokens=1024, timeout=60):
        response = httpx.post(
            self.endpoint,
            json={"model": self.model, "messages": messages, "max_tokens": max_tokens},
            timeout=timeout
        )
        return response.json()["content"][0]["text"]

class AzureOpenAIClient(LLMClient):
    """Azure OpenAI服务"""
    def chat(self, messages, max_tokens=1024, timeout=60):
        # 转换消息格式(Anthropic -> OpenAI)
        openai_messages = self._convert_messages(messages)
        response = self.client.chat.completions.create(
            model=self.deployment,
            messages=openai_messages,
            max_tokens=max_tokens
        )
        return response.choices[0].message.content

def get_llm_client() -> LLMClient:
    """工厂函数,根据环境变量选择provider"""
    provider = os.getenv("LLM_PROVIDER", "anthropic")
    if provider == "azure_openai":
        return AzureOpenAIClient()
    elif provider == "openai":
        return OpenAIClient()
    else:
        return AnthropicClient()

配置切换只需修改环境变量:

# 开发模式(Agent Maestro代理Claude)
LLM_PROVIDER=anthropic
LLM_API_ENDPOINT=http://localhost:23333/api/anthropic/v1/messages

# 生产模式(Azure OpenAI)
LLM_PROVIDER=azure_openai
AZURE_OPENAI_ENDPOINT=https://xxx.openai.azure.com
AZURE_OPENAI_DEPLOYMENT=gpt-4o-mini

性能对比:

模型完整穿搭推荐耗时测试通过率
Claude Haiku~22秒14/14
GPT-4o-mini~9秒14/14

GPT-4o-mini在响应速度上明显更快,非常适合生产环境。

第四步:多传输模式与远程访问

MCP协议支持三种传输模式:

  • stdio: 本地进程通信,适合Claude Desktop直接集成
  • SSE (Server-Sent Events): HTTP长连接,传统远程访问方式
  • Streamable HTTP ⭐: 最新推荐的远程访问方式,配置更简洁

对于跨机器访问(比如从我的MacBook连接到Azure VM上的服务),远程传输模式是必须的。

Streamable HTTP:更简洁的远程访问(推荐)

Streamable HTTP 是 MCP 协议的最新传输方式,相比 SSE 有明显优势:

特性SSEStreamable HTTP
配置复杂度需要 mcp-remote 中转直接 URL 连接
客户端支持需要 npx 命令原生支持
配置行数6-8 行3-4 行

Streamable HTTP 客户端配置(以 VS Code/Cursor 为例):

{
  "mcpServers": {
    "stylist-recommender": {
      "url": "https://stylist.polly.wang/mcp",
      "headers": {
        "X-API-Key": "YOUR_API_KEY"
      }
    }
  }
}

就这么简单!不需要 npx,不需要 mcp-remote,直接填 URL 和认证信息即可。

对比传统 SSE 配置

{
  "mcpServers": {
    "stylist-remote": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://stylist.polly.wang/sse?apiKey=YOUR_API_KEY",
        "--transport", "sse-only"
      ]
    }
  }
}

显然 Streamable HTTP 更加优雅。

SSE 模式(兼容旧客户端)

对于跨机器访问(比如从我的MacBook连接到Azure VM上的服务),SSE是必须的。

Starlette异步服务器

from starlette.applications import Starlette
from starlette.routing import Route, Mount
from mcp.server.sse import SseServerTransport

app = Starlette(
    routes=[
        Route("/health", health_check),
        Route("/sse", sse_handler),
        Route("/messages/", message_handler, methods=["POST"]),
        Mount("/images", StaticFiles(directory=DRESSCODE_ROOT)),
    ],
    middleware=[Middleware(APIKeyMiddleware)]
)

API Key认证

为了安全性,我实现了API Key认证中间件:

class APIKeyMiddleware:
    PUBLIC_PATHS = {"/health", "/favicon.ico"}
    PUBLIC_PREFIXES = ("/images/",)  # 图片不需要认证
    
    async def __call__(self, scope, receive, send):
        if self._is_public_path(scope["path"]):
            return await self.app(scope, receive, send)
        
        api_key = self._extract_api_key(scope)
        if api_key != self.expected_key:
            response = Response("Unauthorized", status_code=401)
            return await response(scope, receive, send)
        
        await self.app(scope, receive, send)

支持三种认证方式:

  • Query参数:?apiKey=xxx
  • Header:X-API-Key: xxx
  • Bearer Token:Authorization: Bearer xxx

Nginx + Let's Encrypt

生产环境使用Nginx作为反向代理,Let's Encrypt提供免费SSL证书:

server {
    listen 443 ssl;
    server_name stylist.polly.wang;
    
    ssl_certificate /etc/letsencrypt/live/stylist.polly.wang/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/stylist.polly.wang/privkey.pem;
    
    location / {
        proxy_pass http://127.0.0.1:8888;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffering off;  # SSE必须禁用缓冲
        proxy_read_timeout 86400;  # 长连接超时
    }
}

关键配置:

  • proxy_buffering off: SSE必须禁用响应缓冲
  • proxy_read_timeout 86400: 保持长连接24小时

第五步:服装图片直接访问

MCP协议的一个限制是不支持直接传输二进制数据。最初我设计了一个get_garment_image工具返回base64编码的图片,但这既低效又不优雅。

更好的方案:直接在推荐结果中返回图片URL。

def _format_garment(self, garment, include_image_url=True):
    result = {
        "garment_id": garment["garment_id"],
        "category": garment["category"],
        "description": garment["description"],
        # ...
    }
    if include_image_url:
        # 生成HTTPS图片URL
        result["image_url"] = f"https://{MCP_EXTERNAL_HOST}/images/{category}/images/{garment_id}_1.jpg"
    return result

这样客户端可以直接在浏览器中打开图片链接,无需额外的API调用。

测试套件

为了确保系统稳定性,我编写了一个综合测试脚本(14个测试用例):

python scripts/test_mcp.py

测试覆盖:

  • 📦 数据库测试: 基本搜索、过滤器、多类别
  • 👕 单品模式: T恤、连衣裙、中文查询
  • 👔 全套穿搭: 基础、正式场合、中文、男装(无连衣裙)
  • 🧠 LLM推理: 评分、理由、搭配建议
  • 🌐 远程服务: Health端点、Tools端点、图片访问

输出示例:

======================================================================
  Test Summary
======================================================================
  ✅ GarmentDatabase Basic (53789 garments)
  ✅ GarmentDatabase Filters (All filters work)
  ✅ Single Item: T-shirt (5 results)
  ✅ Single Item: Dress (5 results, category=dresses)
  ✅ Full Outfit: Basic (3 outfits)
  ✅ Full Outfit with Reasoning (3 outfits, 8.7s)
  ...
  ==============================
  14/14 tests passed
  🎉 All tests passed!

第六步:Skill 开发——让展示更优雅

MCP Server 返回的是结构化 JSON 数据,虽然信息完整,但直接展示给用户并不友好。于是我开发了配套的 stylist-presenter Skill,将推荐结果转化为精美的可视化报告。

MCP + Skill 协作模式

用户请求 → Skill 触发 → 调用 MCP Tool → 解析结果 → 生成 HTML 报告 → 浏览器预览

Skill 的核心价值:

  1. 自动触发:识别「穿搭推荐」「约会穿什么」等关键词
  2. 格式转换:将 JSON 转为美观的 HTML 页面
  3. 文件输出:生成独立的报告文件,可保存分享

HTML 报告设计

我设计了一套黑金配色的视觉风格:

/* 核心配色 */
--header-bg: #1a1a1a;      /* 黑色背景 */
--accent-color: #c9a86c;   /* 金色强调 */
--card-bg: #ffffff;        /* 白色卡片 */
--advice-bg: #fff3cd;      /* 黄色建议区 */

报告包含:

  • 推荐卡片:编号、标题、推荐度评分
  • 标签系统:风格、场合、颜色等 hover 交互
  • 图片网格:悬停放大效果
  • 造型师建议:黄色高亮区块
  • 后续推荐:引导用户继续探索

Skill 文件结构

stylist-presenter/
├── SKILL.md                    # 主指令文件
└── references/
    └── html-template.html      # 完整 HTML 模板

通过这种 MCP + Skill 的组合,用户只需说「推荐约会穿搭」,就能得到一份精美的可视化报告,而不是冷冰冰的 JSON。

Stylist Presenter 生成的 HTML 报告

成果展示

最终的系统支持这样的交互:

用户查询"推荐3套适合约会的优雅穿搭"

系统响应

{
  "mode": "full_outfit",
  "num_outfits": 3,
  "outfits": [
    {
      "type": "dress",
      "dress": {
        "garment_id": "033207",
        "description": "Elegant midi dress with subtle texture",
        "image_url": "https://stylist.polly.wang/images/dresses/images/033207_1.jpg"
      },
      "score": 9.2,
      "reason": "经典的中长连衣裙,完美适合约会场合..."
    }
  ],
  "stylist_advice": "这些穿搭都非常适合约会场合。连衣裙展现优雅气质,而上衣+裙子的组合则更加灵活多变..."
}

总结与反思

技术收获

  1. MCP协议的实践:从stdio到SSE再到Streamable HTTP,深入理解了MCP的设计哲学和演进方向
  2. 跨模型兼容性:不同LLM对prompt的理解差异,需要针对性优化
  3. 向量检索+LLM推理:混合架构在时尚推荐场景的有效应用
  4. 生产级部署:Nginx反向代理、SSL证书、API认证的完整实践
  5. MCP + Skill 协作:数据层与展示层分离,MCP 负责能力,Skill 负责体验

未来改进方向

  • 多模态输入:支持用户上传图片,实现"找相似"功能
  • 用户画像:记住用户偏好,提供个性化推荐
  • 实时库存:对接电商平台,显示可购买状态
  • 更多 Skill:开发 PPT 生成、社交分享等配套 Skill

项目地址

GitHub: Polly2014/Stylist-MCP-Server

如果你对MCP Server开发或AI时尚应用感兴趣,欢迎Star和交流!


写作这篇博文的过程,也是对整个项目的一次复盘。从最初的想法到生产可用,涉及的技术点远比预想的要多。但正是这些细节的打磨,才让最终的产品既稳定又优雅。

留言与讨论