目录
- 前言
- 一、整体架构预览
- 二、实战
-
- [2.1 第一步:定义Skill文件](#2.1 第一步:定义Skill文件)
- [2.2 第二步:编写Skill加载工具](#2.2 第二步:编写Skill加载工具)
- [2.3 第三步:构建Skill中间件](#2.3 第三步:构建Skill中间件)
- [2.4 第四步:创建agent.py:](#2.4 第四步:创建agent.py:)
- 第四步:运行与验证
- 三、扩展思路
前言
在上一篇博客中,我们深入理解了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
核心流程:
- 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的元数据:python{ "name": "hotel_booking", "description": "酒店预订助手,支持查询房型、预订流程、取消政策等。" } -
再创建同目录下的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
-
创建元数据:
skills/attraction/skill.jsonpython{ "name": "attraction", "description": "城市景点推荐助手,提供热门景点、游玩路线、门票信息等。" } -
创建content.md文件存储详细的技能内容:
skills/attraction/content.mdpython# 景点推荐助手 ## 热门景点 ### 滨海公园 - 门票:免费 - 开放时间:全天 - 推荐理由:城市地标,适合散步、骑行,傍晚看日落 ### 古城文化街 - 门票: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的输出:
pythonHuman: 我想订一间海景大床房,住两晚,多少钱? AI: [调用工具 load_skill(skill_name="hotel_booking")] Tool: [返回酒店Skill的完整Markdown内容] AI: 根据酒店政策,海景大床房每晚680元,两晚总价为1360元。如需预订,请提供入住日期和联系人信息。 -
测试2的输出:
pythonHuman: 带孩子去哪里玩比较好? AI: [调用工具 load_skill(skill_name="attraction")] Tool: [返回景点Skill的完整Markdown内容] AI: 如果您带孩子出行,我强烈推荐海洋世界,这里有海豚表演和海底隧道,非常适合亲子游玩。成人票180元,儿童票120元,开放时间为09:00-17: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机制可以实现这一点