# Skills 渐进式展开实现方案（LangChain 1.0+）

## 一、需求概述

用 **Skills** 替代零散的工具调用，实现**渐进式展开**（Progressive Disclosure）：  
Agent 在 system prompt 中只看到技能摘要，按需加载详细技能内容，减少 token 消耗、提升扩展性。

| 技能 | 英文标识 | 职责 |
|------|----------|------|
| 查找相关商品 | lookup_related | 基于文本/图片查找相似或相关商品（图片需先分析风格） |
| 搜索商品 | search_products | 按自然语言描述搜索商品 |
| 检验商品 | check_product | 检验商品是否符合用户要求 |
| 结果包装 | result_packaging | 格式化、排序、筛选并呈现结果 |
| 售后相关 | after_sales | 退换货、物流、保修等售后问题 |

---

## 二、LangChain 1.0 中的 Skills 实现方式

### 2.1 两种实现路线

| 方式 | 适用场景 | 依赖 |
|------|----------|------|
| **方式 A：create_agent + 自定义 Skill 中间件** | 购物导购等业务 Agent | `langchain>=1.0`、`langgraph>=1.0` |
| **方式 B：Deep Agents + SKILL.md** | 依赖文件系统、多技能目录 | `deepagents` |

购物导购场景推荐**方式 A**，更易与现有 Search API 等服务集成。

### 2.2 核心思路：Progressive Disclosure

```
用户请求 → Agent 看轻量描述 → 判断需要的技能 → load_skill → 拿到完整说明 → 执行工具 → 回复
```

- **启动时**：只注入技能名称 + 简短描述（1–2 句）
- **按需加载**：Agent 调用 `load_skill(skill_name)` 获取完整指令
- **执行**：按技能说明调用对应工具

---

## 三、实现架构

### 3.1 技能定义结构

```python
from typing import TypedDict

class Skill(TypedDict):
    """可渐进式展开的技能"""
    name: str          # 唯一标识
    description: str   # 1-2 句，展示在 system prompt
    content: str      # 完整指令，仅在 load_skill 时返回
```

### 3.2 五个技能定义示例

```python
SKILLS: list[Skill] = [
    {
        "name": "lookup_related",
        "description": "查找与某商品相关的其他商品，支持文本相似、同品类推荐。",
        "content": """# 查找相关商品

## 适用场景
- 用户上传图片要求「找类似的」
- 用户说「和这个差不多」「搭配的裤子」
- 用户已有一件商品，想找相关款

## 操作步骤
1. **有图片**：先调用 `analyze_image_style` 理解风格，再调用 `search_products` 用描述搜索
2. **无图片**：用 `search_products` 描述品类+风格+颜色
3. 可结合上下文中的商品 ID、品类做同品类推荐

## 可用工具
- `search_products(query, limit)`：文本搜索
- `analyze_image_style(image_path)`：分析图片风格""",
    },
    {
        "name": "search_products",
        "description": "按自然语言描述搜索商品，如「红色连衣裙」「运动鞋」等。",
        "content": """# 搜索商品

## 适用场景
- 用户用文字描述想要什么
- 如「冬天穿的外套」「正装衬衫」「跑步鞋」

## 操作步骤
1. 将用户描述整理成结构化 query（品类+颜色+风格+场景）
2. 调用 `search_products(query, limit)`，limit 默认 5–10
3. 如有图片，可先 `analyze_image_style` 提炼关键词再搜索

## 可用工具
- `search_products(query, limit)`：自然语言搜索""",
    },
    {
        "name": "check_product",
        "description": "检验商品是否符合用户要求，如尺寸、材质、场合、价格区间等。",
        "content": """# 检验商品是否符合要求

## 适用场景
- 用户问「这款适合我吗」「有没有 XX 材质的」
- 用户提出约束：尺寸、价格、场合、材质

## 操作步骤
1. 从对话中提取约束条件（尺寸、材质、场合、价格等）
2. 对已召回商品做筛选或二次搜索
3. 调用 `search_products` 时在 query 中带上约束
4. 回复时明确说明哪些符合、哪些不符合

## 注意
- 无专门工具时，用 search_products 的 query 表达约束
- 可结合商品元数据（baseColour, season, usage 等）做简单筛选""",
    },
    {
        "name": "result_packaging",
        "description": "对搜索结果进行格式化、排序、筛选并呈现给用户。",
        "content": """# 结果包装

## 适用场景
- 工具返回多条商品后需要整理呈现
- 用户要求「按价格排序」「只要前 3 个」

## 操作步骤
1. 按相关性/相似度排序
2. 限制展示数量（通常 3–5 个）
3. **必须使用以下格式**呈现每个商品：

```
1. [Product Name]
   ID: [Product ID Number]
   Category: [Category]
   Color: [Color]
   Gender: [Gender]
   Season: [Season]
   Usage: [Usage]
   Relevance: [XX%]
```

4. ID 字段不可省略，用于前端展示图片""",
    },
    {
        "name": "after_sales",
        "description": "处理退换货、物流、保修、尺码建议等售后问题。",
        "content": """# 售后相关

## 适用场景
- 退换货政策、运费、签收时间
- 尺码建议、洗涤说明
- 保修、客服联系方式

## 操作步骤
1. 此类问题无需调用商品搜索工具
2. 按平台统一售后政策回答
3. 涉及具体商品时，可结合商品 ID 查询详情后再回答
4. 复杂问题引导用户联系客服""",
    },
]
```

---

## 四、核心代码实现

### 4.1 load_skill 工具

```python
from langchain.tools import tool

@tool
def load_skill(skill_name: str) -> str:
    """加载技能的完整内容到 Agent 上下文中。

    当需要处理特定类型请求时，调用此工具获取该技能的详细说明和操作步骤。

    Args:
        skill_name: 技能名称，可选值：lookup_related, search_products, check_product, result_packaging, after_sales
    """
    for skill in SKILLS:
        if skill["name"] == skill_name:
            return f"Loaded skill: {skill_name}\n\n{skill['content']}"

    available = ", ".join(s["name"] for s in SKILLS)
    return f"Skill '{skill_name}' not found. Available: {available}"
```

### 4.2 SkillMiddleware（注入技能描述）

```python
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain.messages import SystemMessage
from typing import Callable

class ShoppingSkillMiddleware(AgentMiddleware):
    """将技能描述注入 system prompt，使 Agent 能发现并按需加载技能"""

    tools = [load_skill]

    def __init__(self):
        skills_list = []
        for skill in SKILLS:
            skills_list.append(f"- **{skill['name']}**: {skill['description']}")
        self.skills_prompt = "\n".join(skills_list)

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        skills_addendum = (
            f"\n\n## 可用技能（按需加载）\n\n{self.skills_prompt}\n\n"
            "在需要详细说明时，使用 load_skill 工具加载对应技能。"
        )
        new_content = list(request.system_message.content_blocks) + [
            {"type": "text", "text": skills_addendum}
        ]
        new_system_message = SystemMessage(content=new_content)
        modified_request = request.override(system_message=new_system_message)
        return handler(modified_request)
```

### 4.3 创建带 Skills 的 Agent

```python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import MemorySaver

# 基础工具（搜索、风格分析等）
from app.tools.search_tools import search_products, analyze_image_style

agent = create_agent(
    model="gpt-4o-mini",
    tools=[
        load_skill,           # 技能加载
        search_products,
        analyze_image_style,
    ],
    system_prompt="""你是智能时尚购物助手。根据用户需求，先判断使用哪个技能，必要时用 load_skill 加载技能详情。

处理商品结果时，必须遵守 result_packaging 技能中的格式要求。""",
    middleware=[ShoppingSkillMiddleware()],
    checkpointer=MemorySaver(),
)
```

---

## 五、与工具的关系

| 能力 | 技能 | 工具 |
|------|------|------|
| 查找相关 | lookup_related | search_products, analyze_image_style |
| 搜索商品 | search_products | search_products |
| 检验商品 | check_product | search_products（用 query 表达约束） |
| 结果包装 | result_packaging | 无（纯 prompt 约束） |
| 售后 | after_sales | 无（或对接客服 API） |

- **技能**：提供「何时用、怎么用」的说明，支持渐进式加载。
- **工具**：实际执行搜索、分析等操作。

---

## 六、可选：技能约束（进阶）

若希望「先加载技能再使用工具」，可结合 `ToolRuntime` 和 state 做约束：

```python
from langchain.tools import tool, ToolRuntime
from langgraph.types import Command
from langchain.messages import ToolMessage
from typing_extensions import NotRequired

class CustomState(AgentState):
    skills_loaded: NotRequired[list[str]]

@tool
def load_skill(skill_name: str, runtime: ToolRuntime) -> Command:
    """..."""
    for skill in SKILLS:
        if skill["name"] == skill_name:
            content = f"Loaded skill: {skill_name}\n\n{skill['content']}"
            return Command(update={
                "messages": [ToolMessage(content=content, tool_call_id=runtime.tool_call_id)],
                "skills_loaded": [skill_name],
            })
    # ...

# 在 check_product 等工具中检查 skills_loaded
```

---

## 七、依赖与版本

```text
# requirements.txt
langchain>=1.0.0
langchain-openai>=0.2.0
langchain-core>=0.3.0
langgraph>=1.0.0
```

- Python 3.10+
- 若使用 Deep Agents 的 SKILL.md，需额外安装 `deepagents`

---

## 八、总结

| 项目 | 说明 |
|------|------|
| **效果** | 系统 prompt 只放简短技能描述，按需加载完整内容，减少 token、便于扩展 |
| **流程** | 轻量描述 → load_skill → 完整说明 → 调用工具 → 回复 |
| **实现** | `SkillMiddleware` + `load_skill` + `create_agent` |
| **技能** | lookup_related, search_products, check_product, result_packaging, after_sales |

完整示例可参考官方教程：[Build a SQL assistant with on-demand skills](https://docs.langchain.com/oss/python/langchain/multi-agent/skills-sql-assistant)。
