大模型开发手记(十三):langchain skills(下):构建skills架构agent实战

目录

前言

在上一篇博客中,我们深入理解了Agent Skill的核心概念------它是一种通过渐进式披露来扩展Agent专业能力的模块化方式。理论说完了,今天我们就来真刀真枪地实现一个。

我们的目标:构建一个旅行助手Agent,它拥有两个独立的Skill:

  • 酒店预订Skill:包含酒店预订的业务规则、政策、操作流程。
  • 景点推荐Skill:包含热门景点的介绍、推荐逻辑、注意事项。

Agent启动时只知道这两个Skill的名字和一句话描述,只有当你问及具体问题时,它才会动态加载对应Skill的详细内容。

一、整体架构预览

python 复制代码
travel-agent/
├── skills/                         # 存放所有Skill
│   ├── hotel_booking/              # 酒店预订Skill目录
│   │   ├── skill.json              # 元数据(name, description)
│   │   └── content.md              # 详细内容(业务规则、示例等)
│   └── attraction/                 # 景点推荐Skill目录
│       ├── skill.json
│       └── content.md
├── agent.py                        # 主程序:构建Agent、加载Skill
└── skill_loader.py                 # 工具函数:从文件系统加载Skill

核心流程:

  1. Agent启动时,扫描skills/目录,读取所有子目录中的skill.json文件,提取元数据(名称+描述)。
  2. 将这些元数据注入到系统提示词中,让Agent"知道"自己有哪些能力。
  3. 提供load_skill工具,Agent可以在需要时调用它,传入技能名称,从而加载对应目录下content.md的完整内容。
  4. 通过中间件自动完成系统提示词的注入。

二、实战

2.1 第一步:定义Skill文件

每个Skill都是一个独立的目录,元数据和内容分离。未来新增Skill只需新建一个子目录并放入对应的skill.json和content.md即可。

酒店预订Skill

  1. 先定义元数据,我们先创建skills/hotel_booking/skill.json,这是酒店预订Skill的元数据:

    python 复制代码
    {
        "name": "hotel_booking",
        "description": "酒店预订助手,支持查询房型、预订流程、取消政策等。"
    }
  2. 再创建同目录下的skills/hotel_booking/content.md,这是详细的技能内容:

    python 复制代码
    # 酒店预订助手
    
    ## 可用的酒店
    - 海景大床房:每晚 680 元,含双早
    - 山景双床房:每晚 520 元,含单早
    - 行政套房:每晚 1280 元,含双早+行政酒廊权益
    
    ## 预订流程
    1. 确认入住日期和离店日期
    2. 选择合适的房型
    3. 提供入住人姓名和联系电话
    4. 确认订单后,用户需支付 30% 定金
    5. 入住当天 18:00 前可免费取消,之后取消收取首晚房费
    
    ## 注意事项
    - 每间房最多入住 2 名成人 + 1 名儿童
    - 加床服务:每晚 200 元
    - 宠物不可入内
    
    ## 特殊政策
    - 连住 3 晚以上可享受 9 折优惠
    - 金卡会员可享延迟退房至 14:00
    
    ## 示例对话
    用户:我想订一间海景大床房,住两晚
    助手:好的,海景大床房每晚 680 元,两晚共 1360 元。请问您计划什么时候入住?

景点推荐Skill

  1. 创建元数据:skills/attraction/skill.json

    python 复制代码
    {
        "name": "attraction",
        "description": "城市景点推荐助手,提供热门景点、游玩路线、门票信息等。"
    }
  2. 创建content.md文件存储详细的技能内容:skills/attraction/content.md

    python 复制代码
    # 景点推荐助手
    
    ## 热门景点
    
    ### 滨海公园
    - 门票:免费
    - 开放时间:全天
    - 推荐理由:城市地标,适合散步、骑行,傍晚看日落
    
    ### 古城文化街
    - 门票:30 元
    - 开放时间:09:00 - 21:00
    - 推荐理由:明清建筑风格,汇聚特色小吃和手工艺品
    
    ### 海洋世界
    - 门票:成人 180 元,儿童 120 元
    - 开放时间:09:00 - 17:30
    - 推荐理由:适合亲子游,有海豚表演和海底隧道
    
    ## 推荐逻辑
    - 亲子游:优先推荐海洋世界
    - 文艺青年:优先推荐古城文化街
    - 休闲放松:优先推荐滨海公园
    
    ## 注意事项
    - 节假日期间景点人流量大,建议提前购票
    - 古城文化街周末有民俗表演,下午 2 点和 4 点各一场
    
    ## 示例对话
    用户:带孩子去哪里玩比较好?
    助手:如果带孩子的话,强烈推荐海洋世界,里面有海豚表演和海底隧道,孩子会很喜欢。成人票 180 元,儿童票 120 元,开放时间是 09:00 到 17:30。

2.2 第二步:编写Skill加载工具

创建skill_loader.py,负责扫描目录、加载元数据和按需加载内容:

python 复制代码
import json
from pathlib import Path
from typing import Dict, List, Optional

from langchain.tools import tool

# Skill根目录
SKILLS_ROOT = Path(__file__).parent / "skills"


class SkillLoader:
    """负责加载Skill元数据和内容"""
    
    def __init__(self, skills_root: Path):
        self.skills_root = skills_root
        self._metadata_cache: Optional[List[Dict[str, str]]] = None
    
    def load_all_metadata(self) -> List[Dict[str, str]]:
        """扫描所有Skill目录,返回元数据列表"""
        if self._metadata_cache is not None:
            return self._metadata_cache
        
        metadata_list = []
        for skill_dir in self.skills_root.iterdir():
            if not skill_dir.is_dir():
                continue
            json_path = skill_dir / "skill.json"
            if not json_path.exists():
                continue
            with open(json_path, "r", encoding="utf-8") as f:
                data = json.load(f)
                metadata_list.append({
                    "name": data["name"],
                    "description": data["description"]
                })
        self._metadata_cache = metadata_list
        return metadata_list
    
    def load_full_content(self, skill_name: str) -> Optional[str]:
        """根据技能名称加载完整的content.md内容"""
        for skill_dir in self.skills_root.iterdir():
            if not skill_dir.is_dir():
                continue
            json_path = skill_dir / "skill.json"
            if not json_path.exists():
                continue
            with open(json_path, "r", encoding="utf-8") as f:
                data = json.load(f)
                if data["name"] == skill_name:
                    content_path = skill_dir / "content.md"
                    if content_path.exists():
                        with open(content_path, "r", encoding="utf-8") as cf:
                            return f"# 已加载技能:{skill_name}\n\n{cf.read()}"
                    else:
                        return f"技能 {skill_name} 缺少 content.md 文件"
        return None


# 全局加载器实例
loader = SkillLoader(SKILLS_ROOT)


@tool
def load_skill(skill_name: str) -> str:
    """按需加载技能的详细内容。

    当你需要某个技能的具体信息(如业务规则、操作流程、详细知识)时,
    调用此工具加载对应技能。
    
    Args:
        skill_name: 技能名称,可选值由系统提示词中的可用技能列表提供。
    """
    content = loader.load_full_content(skill_name)
    if content is None:
        available = ", ".join(s["name"] for s in loader.load_all_metadata())
        return f"未找到名为 {skill_name} 的技能。可用技能:{available}"
    return content

这个类做了几件事:

  • load_all_metadata:扫描skills/下的所有子目录,读取skill.json,提取name和description。
  • load_full_content:根据技能名称找到对应的目录,读取其中的content.md文件,返回完整的Markdown内容。
  • 通过@tool装饰器将load_skill暴露给Agent使用。

2.3 第三步:构建Skill中间件

中间件负责在模型调用前将技能元数据注入系统提示词。通过模型上下文的方式(wrap_model_call中间件),在模型调用前将所有skills的元数据(名称+描述),注入到系统提示词中,让agent清晰地知道自己有哪些技能。

python 复制代码
import uuid
from typing import Callable

from langchain.agents import create_agent
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
from langchain.messages import SystemMessage
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver

from skill_loader import loader, load_skill


class SkillMiddleware(AgentMiddleware):
    """将技能列表注入系统提示词的中间件"""

    # 注册工具,让Agent可以调用load_skill
    tools = [load_skill]

    def __init__(self):
        # 启动时加载所有技能的元数据
        self.skills_metadata = loader.load_all_metadata()
        self.skills_prompt = self._build_skills_prompt()

    def _build_skills_prompt(self) -> str:
        """生成技能列表的描述文本"""
        if not self.skills_metadata:
            return "当前没有可用技能。"
        
        lines = ["## 可用技能列表\n"]
        for skill in self.skills_metadata:
            lines.append(f"- **{skill['name']}**:{skill['description']}")
        lines.append("\n当你需要某个技能的详细内容时,请调用 `load_skill` 工具加载。")
        return "\n".join(lines)

    def wrap_model_call(
        self,
        request: ModelRequest,
        handler: Callable[[ModelRequest], ModelResponse],
    ) -> ModelResponse:
        """在调用模型前,将技能描述追加到系统消息中"""
        # 构建追加的内容
        addendum = f"\n\n{self.skills_prompt}"
        
        # 获取原有系统消息的内容块,并追加新内容
        original_blocks = request.system_message.content_blocks
        new_blocks = original_blocks + [{"type": "text", "text": addendum}]
        new_system_message = SystemMessage(content=new_blocks)
        
        # 用新的系统消息覆盖原请求
        modified_request = request.override(system_message=new_system_message)
        return handler(modified_request)

2.4 第四步:创建agent.py

创建agent,配置中间件,在invoke或者stream 启动时会将 所有skills的元数据(名称+描述)注入到系统提示词中,然后agent运行中会调用工具将skills核心内容加载到上下文中,发送给大模型

python 复制代码
def main():
    # 初始化模型(请替换为你的API Key或使用其他模型)
    model = ChatOpenAI(model="gpt-4o-mini")
    
    # 创建Agent,使用我们的中间件
    agent = create_agent(
        model,
        system_prompt="你是一个友好的旅行助手,帮助用户解决酒店预订和景点推荐等问题。",
        middleware=[SkillMiddleware()],
        checkpointer=InMemorySaver(),
    )
    
    # 对话线程ID(用于维护状态)
    thread_id = str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}
    
    # 测试1:询问酒店预订
    print("=== 测试:酒店预订 ===")
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "我想订一间海景大床房,住两晚,多少钱?"}]},
        config
    )
    for msg in result["messages"]:
        if hasattr(msg, "pretty_print"):
            msg.pretty_print()
        else:
            print(f"{msg.type}: {msg.content}")
    
    print("\n" + "="*50 + "\n")
    
    # 测试2:询问景点推荐
    print("=== 测试:景点推荐 ===")
    result = agent.invoke(
        {"messages": [{"role": "user", "content": "带孩子去哪里玩比较好?"}]},
        config
    )
    for msg in result["messages"]:
        if hasattr(msg, "pretty_print"):
            msg.pretty_print()
        else:
            print(f"{msg.type}: {msg.content}")


if __name__ == "__main__":
    main()

第四步:运行与验证

  1. 测试1的输出:

    python 复制代码
    Human: 我想订一间海景大床房,住两晚,多少钱?
    AI: [调用工具 load_skill(skill_name="hotel_booking")]
    Tool: [返回酒店Skill的完整Markdown内容]
    AI: 根据酒店政策,海景大床房每晚680元,两晚总价为1360元。如需预订,请提供入住日期和联系人信息。
  2. 测试2的输出:

    python 复制代码
    Human: 带孩子去哪里玩比较好?
    AI: [调用工具 load_skill(skill_name="attraction")]
    Tool: [返回景点Skill的完整Markdown内容]
    AI: 如果您带孩子出行,我强烈推荐海洋世界,这里有海豚表演和海底隧道,非常适合亲子游玩。成人票180元,儿童票120元,开放时间为09:00-17:30。
  3. 关键观察点:

    • 初始对话:Agent的系统提示词中只包含两个Skill的简短描述,没有加载任何详细内容。
    • 按需加载:当用户提出具体问题后,Agent先判断需要哪个Skill,然后调用load_skill获取完整内容。
    • 上下文精简:整个对话历史中,只加载了被使用的Skill的内容,另一个Skill从未被加载。

三、扩展思路

这个基础实现已经足够演示核心机制,但在实际项目中你可能会需要以下增强:

  1. 支持动态刷新Skill

    如果Skill目录下的文件在运行时被修改,你可以让中间件在wrap_model_call中重新读取元数据,或者提供一个reload_skills工具供管理员调用。

  2. 更复杂的加载逻辑

    当前load_skill一次性返回整个content.md。如果Skill内容非常庞大(如完整的产品手册),你可以改为分块返回,或者让Agent在Skill内进行"二次搜索"。Markdown格式天然支持分节,你可以先返回目录,再让Agent根据需要加载具体章节。

  3. 为Skill关联工具

    有些Skill可能需要配套的工具,比如酒店预订Skill应该有一个make_booking工具。你可以在load_skill被调用时,动态向Agent注册这些工具。LangChain的ToolRuntime和Command机制可以实现这一点

相关推荐
大模型真好玩5 小时前
LangChain DeepAgents 速通指南(五)—— 快速了解DeepAgents框架及其核心特性
人工智能·langchain·agent
星浩AI6 小时前
MCP 系列(协议篇):深入理解 MCP 协议机制
后端·langchain·agent
龘龍龙7 小时前
大模型学习(二)-RAG、LangChain
学习·langchain
小超同学你好8 小时前
Langgragh 19. Skills 4. SkillToolset 式设计 —— 工具化按需加载的 Skills(含代码示例)
人工智能·语言模型·langchain
java1234_小锋9 小时前
基于LangChain的RAG与Agent智能体开发 - RunnableLambda实现复杂多模型链路调用
langchain·rag
深藏功yu名10 小时前
Day22:RAG 王炸进阶!多格式文档 (PDF_Word)+ 多文档知识库搭建
人工智能·python·pycharm·langchain·pdf·word·rag
Dontla10 小时前
黑马大模型RAG与Agent智能体实战教程LangChain提示词——52、Agent智能体——Agent项目中间件和Agent创建
langchain
国医中兴1 天前
Flutter 三方库 langchain_google 的鸿蒙化适配指南 - 链接 Gemini 智慧中枢、LangChain AI 实战、鸿蒙级智能应用专家
flutter·langchain·harmonyos