乐于分享
好东西不私藏

多智能体架构源码实例系列:02-Skill 模式(Skills Pattern)

多智能体架构源码实例系列:02-Skill 模式(Skills Pattern)

02-Skill 模式(Skills Pattern)

目录

  • 1. 什么是 Skill 模式
  • 2. 为什么使用 Skill 模式
  • 3. 架构总览
  • 4. 核心概念
  • 5. 关键实现代码
  • 6. 完整工作流
  • 7. 与其他模式对比
  • 8. 选型决策表
  • 9. 完整示例代码

1. 什么是 Skill 模式

Skill 模式(Skills Pattern) 是多智能体架构之一,基于 渐进式披露(progressive disclosure) 原则:Agent的系统提示中始终只包含一份"Skill菜单"(每个Skill 1-2 句简介),只有当Agent通过工具调用明确请求某个Skill时,该Skill的完整内容才会被加载到对话历史中。

这一设计的关键观察是:大模型的认知开销,主要源于一次性喂入的指令量;而非可调用的工具数量。Skill 模式把"指令"与"工具"解耦 —— 描述(轻量)始终在线,内容(重量)按需注入。

典型应用场景

  • 企业 SQL 助手:Agent只看到 schema 的精简描述(数据源、用途),收到查询请求后才加载完整的表结构与业务规则(本文档的示例即取自此场景)
  • 跨部门合规 RAG:HR、法务、财务三套领域指令各自维护,Agent按需加载
  • 大规模 API 工具集:成百上千个 API 接口的鉴权、限流、错误处理规则,可拆分为若干 Skill 由不同团队自治维护
  • 多业务线知识库:订单、库存、CRM、ERP 等模块各自打包为一个 Skill

与路由器模式的视角差异

四种多智能体架构各有侧重:路由器模式关注"任务分派 —— 哪个子Agent接收请求";Skill 模式则关注"知识分派 —— 哪份领域指令需要加载"。前者强调"将工作分给对的Agent",后者强调"把对的提示递给同一个Agent"。


2. 为什么使用 Skill 模式

相比单一巨型系统提示或纯 RAG(向量检索),Skill 模式提供了四方面优势:

优势说明
节省上下文仅加载 2-3 个相关Skill,其余Skill描述仅占少量 token
团队自治各业务线独立维护自己的 Skill(类似微服务)
可扩展性可承载数十至数百个Skill而不撑爆上下文
对话历史简洁单Agent + 单会话线程,易于追踪、调试、checkpoint

典型权衡

收益的另一面是几项需要警惕的权衡:

  • 延迟成本:首次加载某 Skill 需要一次额外的模型往返(LLM 先决定加载哪个 Skill,再读 Skill 内容生成回答)。
  • 工作流控制:基础实现完全靠 prompt 引导,无法硬约束"必须先加载 Skill A 才能调用工具 B"——若业务有此要求,需要扩展 AgentState(参见 4.4 节)。
  • 依赖语义清晰:Skill 的 description 字段是Agent决策的唯一线索,含糊不清会导致 LLM 选错 Skill。

渐进式披露的三段式架构

Skill 模式的核心是一种三段式信息架构:

  1. 元数据(始终在系统提示):Skill的 name + description(1-2 句)
  2. 核心内容(按需加载):Skill完整指令、规则、约束 —— 通常 500-2000 token
  3. 详细资源(可选):代码示例、文件路径、API 端点 —— 可被 Skill 内提到的工具进一步按需调用

这种分层的精髓在于:LLM 用最低成本的"菜单"做路由决策,只在必要时才"下单"读取详细说明。同理,这一思路可与少样本提示(few-shot prompting)结合 —— 把若干问答对作为 Skill 内容的一部分,让Agent在加载 Skill 后看到具体样例,适用于复杂查询场景。


3. 架构总览

Skill 模式可以用一句话概括其核心思想:Agent系统提示只装"菜单",按需通过工具调用"上菜"。其架构由三个层次组成:

  1. 元数据层(Metadata):始终在系统提示中 —— 每个Skill的 name + description(通常 1-2 句)
  2. 核心内容层(Core Content):仅在被加载时进入对话 —— 完整的指令、规则、约束、示例
  3. 详细资源层(Detailed Resources):可选 —— 代码示例、文件路径、嵌套工具调用

典型工作流为:

用户提问 → Agent扫描系统提示中的"Skill菜单" → LLM 内部推理决定是否需要某个 Skill 的详细内容 → :调用 load_skill 工具,Skill 内容作为 ToolMessage 注入对话历史 → Agent基于完整上下文作答;:直接基于通用知识作答

图示说明

  • Start → SystemPrompt:用户的原始问题进入Agent
  • SystemPrompt → Decide:Agent扫描系统提示中的Skill菜单,做出路由判断(LLM 决策)
  • Decide → LoadSkill/SkipSkill:分支 —— 加载需要的Skill,或直接作答
  • LoadSkill → Schema:load_skill 工具将完整 skill content 注入为 ToolMessage
  • Schema → Response:基于完整 Schema 与业务规则,Agent生成最终 SQL

4. 核心概念

本章按"由静到动"的顺序介绍 Skill 模式涉及的四个核心组件:Skill 数据结构 → 加载工具 → 中间件 → (可选)状态追踪。

4.1 Skill 数据结构

Skill 是一个简单的 TypedDict,包含三个字段:

字段作用何时被使用
name唯一标识符工具调用入参
description1-2 句简介始终注入系统提示
content详细指令与资源按需通过工具返回
python
from typing import TypedDictclassSkill(TypedDict):"""可按需渐进式披露给Agent的Skill"""    name: str# Skill唯一标识    description: str# 1-2 句简介,展示在系统提示中    content: str# 完整Skill内容(只在加载时注入上下文)

设计要点:描述永远在、内容按需来description 的措辞决定了 LLM 能否识别何时该加载这个 Skill —— 这是 Skill 模式中"人工可调的杠杆"。LangChain 官方建议每条描述聚焦于何时使用(use case),而非怎么使用(mechanics)。

4.2 load_skill 工具

工具的签名直接定义了其行为契约:

  • 入参:skill_name: str
  • 返回:str(完整Skill内容)
  • 关键设计:返回字符串作为 ToolMessage 进入对话历史,不会动态修改系统提示
python
from langchain.tools import tool@tooldefload_skill(skill_name: str) -> str:"""按需加载Skill的完整内容"""for skill in SKILLS:if skill["name"] == skill_name:returnf"Loaded skill: {skill_name}\n\n{skill['content']}"    available = ", ".join(s["name"for s in SKILLS)returnf"Skill '{skill_name}' not found. Available skills: {available}"

与 RAG 的差异:LangChain 官方文档明确指出,这种模式可视作 RAG 的一种特例 —— 每个 Skill 是一个检索单元,但检索介质是直接列表查找 / 文件操作 / 静态索引,而不是 embedding + 相似度搜索。这意味着 Skill 模式的检索没有近似误差,适合"业务边界清晰、每个领域有专属指令"的场景;RAG 则更适合"千级文档、无法枚举"的场景。

4.3 SkillMiddleware(系统提示注入)

AgentMiddleware 的 wrap_model_call 钩子在每次模型调用前被触发。中间件将Skill菜单拼接到系统消息末尾,只占用少量 token:

python
from langchain.agents.middleware import (    AgentMiddleware, ModelRequest, ModelResponse)from langchain.messages import SystemMessageclassSkillMiddleware(AgentMiddleware):"""将Skill描述注入系统提示的中间件"""    tools = [load_skill]   # 类变量注册工具defwrap_model_call(self, request, handler):        skills_addendum = (f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n""Use the load_skill tool when you need detailed information ""about handling a specific type of request."        )        new_content = list(request.system_message.content_blocks) + [            {"type""text""text": skills_addendum}        ]        new_system = SystemMessage(content=new_content)return handler(request.override(system_message=new_system))

两个非显而易见的设计点:

  1. tools = [load_skill] 是类变量(而非实例变量) —— LangChain 在中间件初始化阶段通过这个类属性发现工具并注册到Agent的可用工具集合中。
  2. 修改后的 new_system 通过 request.override(system_message=new_system) 传递,不会持久化到原始请求 —— 每次调用都重新构造,保证中间件逻辑无副作用。

生产化提示:教程示例在 __init__ 中固化Skill列表。生产环境应在 before_agent 钩子中动态加载,以便支持Skill热更新(从数据库或配置中心读取最新的 Skill 定义)。

4.4 (可选)追踪已加载Skill

基础 Skill 模式完全靠 LLM 自觉 —— Agent可以调用任何工具。但如果业务有硬性要求:“必须先加载 safety_policy 才能调用执行类工具”,则需要扩展 AgentState 与工具的 Command 返回:

python
from typing import NotRequiredfrom langgraph.types import Commandfrom langchain.agents.middleware import AgentStateclassCustomState(AgentState):    skills_loaded: NotRequired[list[str]]   # 已加载的Skill名@tooldefload_skill(skill_name: str, runtime) -> Command:# 查找Skill逻辑同上return Command(update={"messages": [ToolMessage(content=...)],"skills_loaded": [skill_name],   # 累积到状态字段    })

何时用:当出现"安全策略/合规规则/操作权限"等不允许 LLM 自由决策的场景时,在 wrap_model_call 中读取 skills_loaded 字段做硬拦截 —— 例如发现Agent尝试调用 execute_query 但未加载 data_safety,直接抛出 PermissionError。

这是 Skill 模式的进阶选项,简单场景下不必启用。


5. 关键实现代码

本节展示关键片段, 完整可运行版本请见第 9 章

5.1 安装与模型

bash
pip install -U "langchain[openai]"# 或 uv add / conda 等价命令export OPENAI_API_KEY="sk-..."

模型选择

python
from langchain.chat_models import init_chat_modelmodel = init_chat_model("openai:gpt-4o")# 也支持: anthropic / azure_openai / google_genai / bedrock / openrouter / huggingface ...

5.2 定义Skill列表

使用两个真实业务域 Skill:sales_analytics 与 inventory_management

python
SKILLS: list[Skill] = [    {"name""sales_analytics","description""销售分析的数据库 schema 与业务规则,涵盖 customers、orders、revenue。","content""""# Sales Analytics Schema## Tables### customers(关键字段)- customer_id (PRIMARY KEY)- customer_tier (bronze/silver/gold/platinum)### orders(关键字段)- order_id, customer_id (FK), order_date- status (pending/completed/cancelled/refunded)- total_amount, sales_region### order_items- item_id, order_id, product_id, quantity, unit_price## Business Logic- **Active customers**:status = 'active' AND signup_date <= CURRENT_DATE - INTERVAL '90 days'- **Revenue 仅算 completed 订单**- **High-value orders**:total_amount > 1000## Example QuerySELECT c.customer_id, c.name, SUM(o.total_amount) ...;""",    },    {"name""inventory_management","description""库存管理的数据库 schema 与业务规则,涵盖 products、warehouses、stock levels。","content""""# Inventory Management Schema## Tables### products(关键字段)- product_id, product_name, sku, category- reorder_point, discontinued### warehouses- warehouse_id, warehouse_name, location, capacity### inventory- inventory_id, product_id, warehouse_id, quantity_on_hand## Business Logic- **库存预警**:SUM(quantity_on_hand) <= reorder_point- **停售商品默认排除**(discontinued = false)## Example QuerySELECT p.product_id, p.product_name, SUM(i.quantity_on_hand) AS total_stock ...HAVING total_stock <= p.reorder_point;""",    },]

5.3 load_skill 工具 + SkillMiddleware 整合

第 4 章已分别展示 load_skill 与 SkillMiddleware 的实现,本节展示它们的组装形态(与列表耦合):

python
@tooldefload_skill(skill_name: str) -> str:"""按需加载Skill的完整内容..."""for skill in SKILLS:if skill["name"] == skill_name:returnf"Loaded skill: {skill_name}\n\n{skill['content']}"    available = ", ".join(s["name"for s in SKILLS)returnf"Skill '{skill_name}' not found. Available skills: {available}"classSkillMiddleware(AgentMiddleware):    tools = [load_skill]def__init__(self):self.skills_prompt = "\n".join(f"- **{s['name']}**: {s['description']}"for s in SKILLS        )defwrap_model_call(self, request, handler):        addendum = (f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n""Use the load_skill tool when you need detailed information ""about handling a specific type of request."        )        new_content = list(request.system_message.content_blocks) + [            {"type""text""text": addendum}        ]        new_system = SystemMessage(content=new_content)return handler(request.override(system_message=new_system))

5.4 创建 Agent

python
from langchain.agents import create_agentfrom langgraph.checkpoint.memory import InMemorySaveragent = create_agent(    model,    system_prompt=("你是一名 SQL 查询助手,帮助用户针对业务数据库编写合规 SQL。"    ),    middleware=[SkillMiddleware()],    checkpointer=InMemorySaver(),)

关键三件套解读:

  1. middleware=[SkillMiddleware()]:动态注入Skill菜单 —— 每次模型调用前,中间件都会把最新 Skill 列表拼接到系统消息末尾
  2. checkpointer=InMemorySaver():维持多轮对话状态(checkpoint 是 Skill 模式的核心要素,因为加载的 Skill 内容必须保留在历史中,后续轮次才能"记住"它已加载过的事实,从而避免重复加载)
  3. system_prompt:精简领域身份 —— 不写业务细节(业务细节都在 Skill 里),系统提示只描述Agent的"通用身份",便于 Skill 的横切复用

6. 完整工作流

6.1 启动会话与提问

python
from langchain_core.utils.uuid import uuid7thread_id = str(uuid7())   # 每次会话唯一config = {"configurable": {"thread_id": thread_id}}result = agent.invoke(    {"messages": [{"role""user","content""编写 SQL:查找上个月下过超过 1000 元订单的客户"    }]},    config,)for msg in result["messages"]:ifhasattr(msg, 'pretty_print'):        msg.pretty_print()else:print(f"{msg.type}{msg.content}")

6.2 预期输出(对话链)

================================ Human Message =================================编写 SQL:查找上个月下过超过 1000 元订单的客户================================== Ai Message ==================================Tool Calls:  load_skill (call_abc123)    Call ID: call_abc123     Args:      skill_name: sales_analytics================================= Tool Message =================================Name: load_skillLoaded skill: sales_analytics# Sales Analytics Schema[... 完整 Schema 与业务规则被注入,详细见第 5.2 节 ...]## Business Logic- **Revenue 仅算 completed 订单**- **High-value orders**:total_amount > 1000================================== Ai Message ==================================以下是符合业务规则的 SQL:```sql-- 查找上个月所有单笔订单 > 1000 元的客户(去重)SELECT DISTINCT    c.customer_id,    c.name,    c.email,    c.customer_tierFROM customers cJOIN orders o ON c.customer_id = o.customer_idWHERE o.total_amount > 1000  AND o.status = 'completed'           -- 仅计完成订单(对应 Revenue 规则)  AND o.order_date >= CURRENT_DATE - INTERVAL '1 month'ORDER BY c.customer_id;

要点解读:

  • JOIN 关联 customers 与 orders
  • total_amount > 1000 过滤大额订单(对应业务规则 High-value orders)
  • status = 'completed' 只算实际收入(对应 Revenue 规则)
  • INTERVAL '1 month' 限定时间窗
  • DISTINCT 避免同一客户多次下单时重复

6.3 调用链解读

整个流程可拆为三步链:

  1. 系统提示预加载:Agent一启动就看到 sales_analytics 与 inventory_management 两条简介(SkillMiddleware.wrap_model_call 注入),系统提示 token 占用约 50。
  2. LLM 决策:看到用户提问后,LLM 内部推理"这是销售数据相关",决定调用 load_skill(skill_name="sales_analytics")
  3. 内容注入 + 生成:load_skill 返回完整 schema + 业务规则作为 ToolMessage,LLM 基于此上下文输出合规 SQL。

关键点:Skill 的加载是一次性的 —— 后续在同一 thread_id 的对话中,已加载的 Skill 不会再次注入,因为相关 ToolMessage 已经存在对话历史里(LLM 能看到)。这就是为何需要 checkpointer=InMemorySaver() —— 没有它,每一轮对话都是独立会话,Skill 会被反复重新加载,造成 token 浪费与延迟开销。

如果你希望Agent"在 N 步之后遗忘 Skill 内容以释放上下文"(例如长对话场景),可通过 LangGraph 的 Edit 节点或 MessagesState 的 trim 机制主动删除对应 ToolMessage。


7. 与其他模式对比

LangChain 提供四种多智能体模式,适用于不同场景。本节将 Skill 模式与其它三种模式横向对比,并与 RAG 模式比较边界。

7.1 四模式总览

模式核心思想并行能力上下文连续性
路由器(Router)分类后扇出到专家,合成结果✅ 强弱(每轮独立)
子Agent(Subagents)主Agent动态调度子Agent✅ 中
交接(Handoffs)状态机式流转
Skill(Skills)按需渐进加载Skill⚠️ 弱

Skill 模式与三种多智能体模式的关系:

  • 同一层:与路由器/子Agent/交接同属"多智能体架构"层 —— 都涉及"多个 X 协作完成一个任务"
  • 不同维度:路由器/子Agent/交接关注"任务如何分派",Skill 关注"知识如何按需加载"
  • 可组合:Skill 模式可与其他三种叠加使用(例如:路由器将任务分发到多个垂直领域的Agent,每个Agent内部又用 Skill 模式加载自己的领域知识)

7.2 Skill vs 路由器(关键对比)

维度路由器Skill
决策对象哪个子Agent接收任务加载哪份知识
决策时机路由步骤集中决策一次每轮都可能再次加载
典型用途多源并行查询大型知识库按需注入
架构特征多Agent + StateGraph单Agent + Middleware
多领域并行✅ 原生支持⚠️ 同轮可多次加载(LLM 决策)
典型延迟(单任务)3 次模型调用3-4 次模型调用(取决于加载次数)

关键洞见:路由器关注"任务分派",Skill 关注"知识分派"。在大规模知识库(数百 schema、数千文档)场景下,Skill 模式的可扩展性远超路由器 —— 因为 Skill 把"决策粒度"从"子Agent"细化到"单个知识块"。

7.3 Skill vs 子Agent(关键对比)

维度子AgentSkill
决策者主Agent LLM主Agent LLM
决策风格“现在该调用谁”“现在需要哪些知识”
典型场景多步任务编排复杂问题按需检索
关联实现create_agent(subagents=[...])create_agent(middleware=[SkillMiddleware()])
隔离性强(每个子Agent独立上下文)弱(单Agent共享同一上下文)

子Agent适合"任务可以分解为多个独立子流程"的场景;Skill 适合"同一Agent需要不同领域的深度知识"的场景。例如:一个法律咨询Agent,可能同时需要合同法、公司法、知识产权三类知识 —— 这时 Skill 是更优雅的选择(否则要为每个子领域创建一个子Agent,反而臃肿)。

7.4 Skill vs RAG

LangChain 官方将 Skill 模式视作 RAG 的一种特例。下表对比两者的适用边界:

维度RAG(传统向量检索)Skill(直接查找)
检索单位chunk(文档片段)skill(完整业务包)
检索方式embedding + 相似度文件查找 / API / 列表索引
近似误差有(向量召回)无(精确匹配)
何时用千级文档,无明确业务分组数十业务领域,每个有专属指令
典型延迟检索 + LLM 阅读LLM 决策 + 单次工具调用
可解释性弱(召回不一定可追溯)强(load_skill 调用明确)

关键定位:Skill 模式不是要替代 RAG,而是补充 —— 当业务边界清晰指令比数据更重要时,Skill 更合适;当文档量巨大问题模糊时,RAG 更合适。生产中两者常常组合:先用 Skill 注入业务专属指令,再用 RAG 检索该领域内的具体资料。


8. 选型决策表

使用以下决策表选择合适的多智能体模式:

场景特征推荐模式理由
用户查询可能同时涉及多个独立的知识领域,需要并行查询路由器原生并行,合成结果
不同查询涉及的领域组合完全不同(选择性路由)路由器路由步骤智能挑选源
需要在对话中累积上下文、跨多轮决策子Agent主Agent持有连续上下文
同一查询需在不同"状态"间流转(如客服流程:接待 → 诊断 → 修复)交接(Handoffs)状态机式转换更清晰
智能体需要根据查询渐进加载大型Skill库Skill(Skills)按需注入上下文,节省 token
业务领域多但每个领域有清晰边界(数十至数百)Skill(Skills)微服务式分工,团队自治
知识库规模大但业务指令比数据更重要Skill(Skills)Skill 中的指令可直接指导行为,不必 embedding
仅一个知识领域,无需多智能体单一Agent + 工具YAGNI

快速判断口诀(本系列四模式):

  • 一句话能描述的"分类后并行" → 路由器
  • 需要 LLM 在对话中"边走边决定" → 子Agent
  • 流程图能画成状态机 → 交接
  • 上下文窗口有限但Skill/知识很多 → Skill(Skills)

Skill 模式专项检查清单(满足 ≥ 3 条即可优先考虑 Skill):

  • [ ] 单系统需要支持 10+ 业务领域
  • [ ] 每个领域有专属的指令、规则、约束
  • [ ] 各领域由不同团队维护
  • [ ] 整体 schema/文档会让单次 LLM 调用超 token 上限
  • [ ] 常见查询只涉及 1-3 个领域

若满足多数,在四种模式中应优先考虑 Skill 模式。


9. 完整示例代码(skills_sql_agent.py)

重要说明:关于具备查询执行、纠错和验证功能的完整 SQL Agent示例,请参见我们另外的文章 。本文聚焦于可应用于任何领域的渐进披露模式

复制下方代码到 skills_sql_agent.py,设置好 OPENAI_API_KEY 后:

bash
export OPENAI_API_KEY="sk-..."python skills_sql_agent.py

预期输出:

  • 第一轮:Agent 调用 load_skill("sales_analytics"),Schema 与业务规则注入
  • 第二轮:Agent 输出合规 SQL,带 DISTINCT/JOIN/INTERVAL 等关键子句
  • 第三轮(同 thread_id):用户可继续问"再加个按客户分层统计",Agent 不会重复 load_skill(因已在历史中)
python
"""Skill 模式 SQL 助手示例(production-ready 注解版)本示例演示多智能体系统中的"Skill(Skills)"模式 —— 渐进式披露:  1. Agent 系统提示中仅含 Skill 简介(~50 token);  2. 收到用户提问后,Agent 决定加载哪些 Skill(通过工具调用);  3. Skill 完整内容作为 ToolMessage 注入对话历史;  4. Agent 基于完整 Schema + 业务规则生成合规 SQL。应用场景对比:  - 路由器: 适合"查询多个数据源并并行汇总"  - Skill:   适合"知识库极大,需按业务域按需加载"  - 子Agent:  适合"任务可分解为多个独立子流程"  - 交接:    适合"流程图能画成状态机""""import osfrom typing import TypedDictfrom langchain.agents import create_agentfrom langchain.agents.middleware import AgentMiddlewarefrom langchain.chat_models import init_chat_modelfrom langchain.tools import toolfrom langgraph.checkpoint.memory import InMemorySaver# ======================== Skill 数据结构 ========================classSkill(TypedDict):"""单一Skill的完整定义:名称 + 简介 + 详细内容"""    name: str    description: str    content: str# ======================== 业务Skill定义 ========================SKILLS: list[Skill] = [    {"name""sales_analytics","description""销售分析:customers、orders、revenue 相关查询","content""""# Sales Analytics Schema### customers- customer_id (PK), name, email, customer_tier (bronze/silver/gold/platinum)- signup_date, region### orders- order_id (PK), customer_id (FK), order_date- status (pending/completed/cancelled/refunded), total_amount### order_items- item_id (PK), order_id (FK), product_id, quantity, unit_price## 业务规则- 活跃客户:status = 'active' AND signup_date <= CURRENT_DATE - INTERVAL '90 days'- 营收:仅 sum(total_amount) WHERE status = 'completed'- 高价值订单:total_amount > 1000""",    },    {"name""inventory_management","description""库存管理:products、warehouses、stock levels","content""""# Inventory Management Schema### products- product_id (PK), product_name, sku, category, reorder_point, discontinued### warehouses- warehouse_id (PK), warehouse_name, location, capacity### inventory- inventory_id (PK), product_id (FK), warehouse_id (FK), quantity_on_hand## 业务规则- 补货预警:SUM(quantity_on_hand) <= reorder_point- 默认排除 discontinued 商品""",    },]# ======================== load_skill 工具 ========================@tooldefload_skill(skill_name: str) -> str:"""按需加载Skill的完整内容(返回字符串注入 ToolMessage)"""for skill in SKILLS:if skill["name"] == skill_name:returnf"Loaded skill: {skill_name}\n\n{skill['content']}"    available = ", ".join(s["name"for s in SKILLS)returnf"Skill '{skill_name}' not found. Available skills: {available}"# ======================== SkillMiddleware ========================classSkillMiddleware(AgentMiddleware):"""将Skill菜单拼接到系统消息末尾"""    tools = [load_skill]   # 类变量注册工具(被 LangChain 框架发现)def__init__(self):self.skills_prompt = "\n".join(f"- **{s['name']}**: {s['description']}"for s in SKILLS        )defwrap_model_call(self, request, handler):        addendum = (f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n""Use load_skill when you need detailed information ""about a specific business area."        )        new_content = list(request.system_message.content_blocks) + [            {"type""text""text": addendum}        ]from langchain.messages import SystemMessage        new_system = SystemMessage(content=new_content)return handler(request.override(system_message=new_system))# ======================== Agent 创建 ========================os.environ.setdefault("OPENAI_API_KEY""sk-...")   # 真实使用时替换为你的 Keymodel = init_chat_model("openai:gpt-4o")agent = create_agent(    model,    system_prompt="你是一名 SQL 助手,帮助用户编写业务合规的 SQL。",    middleware=[SkillMiddleware()],    checkpointer=InMemorySaver(),)# ======================== 调用测试 ========================if __name__ == "__main__":from langchain_core.utils.uuid import uuid7    config = {"configurable": {"thread_id"str(uuid7())}}    result = agent.invoke(        {"messages": [{"role""user","content""编写 SQL:查找上个月所有订单 > 1000 的客户",        }]},        config,    )for msg in result["messages"]:ifhasattr(msg, "pretty_print"):            msg.pretty_print()else:print(f"{msg.type}{msg.content}")