欢迎光临
我们一直在努力

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

目录

  • 前言
  • 一、整体架构预览
  • 二、实战
    • 2.1 第一步:定义Skill文件
      • 酒店预订Skill
      • 景点推荐Skill
    • 2.2 第二步:编写Skill加载工具
    • 2.3 第三步:构建Skill中间件
    • 2.4 第四步:创建agent.py:
    • 第四步:运行与验证
  • 三、扩展思路

前言

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

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

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

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

一、整体架构预览

travelagent/
├── 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

核心流程:

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

    2.1 第一步:定义Skill文件

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

    酒店预订Skill

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

    {
    "name": "hotel_booking",
    "description": "酒店预订助手,支持查询房型、预订流程、取消政策等。"
    }

  • 再创建同目录下的skills/hotel_booking/content.md,这是详细的技能内容:

    # 酒店预订助手

    ## 可用的酒店
    海景大床房:每晚 680 元,含双早
    山景双床房:每晚 520 元,含单早
    行政套房:每晚 1280 元,含双早+行政酒廊权益

    ## 预订流程
    1. 确认入住日期和离店日期
    2. 选择合适的房型
    3. 提供入住人姓名和联系电话
    4. 确认订单后,用户需支付 30% 定金
    5. 入住当天 18:00 前可免费取消,之后取消收取首晚房费

    ## 注意事项
    每间房最多入住 2 名成人 + 1 名儿童
    加床服务:每晚 200
    宠物不可入内

    ## 特殊政策
    连住 3 晚以上可享受 9 折优惠
    金卡会员可享延迟退房至 14:00

    ## 示例对话
    用户:我想订一间海景大床房,住两晚
    助手:好的,海景大床房每晚 680 元,两晚共 1360 元。请问您计划什么时候入住?

  • 景点推荐Skill

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

    {
    "name": "attraction",
    "description": "城市景点推荐助手,提供热门景点、游玩路线、门票信息等。"
    }

  • 创建content.md文件存储详细的技能内容:skills/attraction/content.md

    # 景点推荐助手

    ## 热门景点

    ### 滨海公园
    门票:免费
    开放时间:全天
    推荐理由:城市地标,适合散步、骑行,傍晚看日落

    ### 古城文化街
    门票:30
    开放时间:09:00 21:00
    推荐理由:明清建筑风格,汇聚特色小吃和手工艺品

    ### 海洋世界
    门票:成人 180 元,儿童 120
    开放时间:09:00 17:30
    推荐理由:适合亲子游,有海豚表演和海底隧道

    ## 推荐逻辑
    亲子游:优先推荐海洋世界
    文艺青年:优先推荐古城文化街
    休闲放松:优先推荐滨海公园

    ## 注意事项
    节假日期间景点人流量大,建议提前购票
    古城文化街周末有民俗表演,下午 2 点和 4 点各一场

    ## 示例对话
    用户:带孩子去哪里玩比较好?
    助手:如果带孩子的话,强烈推荐海洋世界,里面有海豚表演和海底隧道,孩子会很喜欢。成人票 180 元,儿童票 120 元,开放时间是 09:0017:30

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

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

    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清晰地知道自己有哪些技能。

    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核心内容加载到上下文中,发送给大模型

    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的输出:

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

  • 测试2的输出:

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

  • 关键观察点:

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

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

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

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

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

  • 赞(0)
    未经允许不得转载:171主机测评 » 大模型开发手记(十三):langchain skills(下):构建skills架构agent实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址