news 2026/9/27 2:02:12

给智能体加“专业技能包”:用LangChain实现按需加载的Agent Skills

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
给智能体加“专业技能包”:用LangChain实现按需加载的Agent Skills
告别巨型系统提示,让Agent只在需要时加载对应指令,成本更低、扩展性更强

小伙伴们,你有没有好奇过:ChatGPT、Gemini 这些聊天界面为什么能直接生成PPT、Excel文件?明明底层跑的只是大语言模型,哪来的生成文档的能力?

这个问题的答案不是模型本身变“聪明”了,而是一套更轻量的设计:技能(Skills)——也就是智能体只在需要时才加载的专用指令集。今天我们就用LangChain框架,从零搭建一个带技能的智能体,看看这个机制怎么落地。

先搞懂三个核心概念

LangChain是当前主流的LLM应用开发框架,支持构建智能体、链、检索管道等多种形态的系统,同时封装了模型调用、工具管理、记忆管理等通用能力,其中create_agent辅助函数可以把模型、工具集、系统提示快速拼装成可运行的智能体,大幅降低开发门槛。

在这个基础上,中间件(Middleware)是技能机制的基础:它挂在智能体和模型之间,每次交互都可以重写请求、检查响应、甚至注入额外工具,完全不需要修改智能体的核心逻辑,设计思路和Web开发里的HTTP中间件非常相似。

而技能(Skills)就是建立在中间件之上的自包含指令包:智能体平时只会看到一份简短的技能列表,只有当用户的需求匹配到某个技能时,才会通过load_skill工具拉取该技能的完整指令。这比把所有可能的指令都塞进一个巨型系统提示要高效得多——毕竟大模型每次交互都要完整读取系统提示,指令越多、成本越高。

实战:搭建带双技能的文档生成智能体

我们这次要做的智能体带两个专属技能:一个是生成PPT演示文稿的pptx_builder,一个是生成Excel报表的excel_reporter,两个技能都以独立的SKILL.md文件形式存在,最终会调用真实工具把文件保存到本地。

前置准备

首先你需要准备这些内容:

  1. 1. 一个OpenAI API密钥,也可以替换成其他兼容的模型;

  2. 2. 运行环境用Google Colab或者本地Jupyter Notebook都可以;

  3. 3. 新建一个skills文件夹,在里面分别创建两个技能对应的markdown文件定义技能逻辑。

图片说明:File directory structure for project skills

两个技能的核心定义分别如下,这里用占位符代替具体代码:excel_reporter/SKILL.md的核心逻辑:

--- name: excel_reporter description: Build an Excel (.xlsx) report from one or more named tables --- You are now a **spreadsheet analyst**. Turn the user's request into a clean Excel report. Guidelines: - Organize data into one or more sheets; each sheet is a named table. - First row of each sheet is the header row. - Keep numbers as numbers (not strings) so Excel can sum/format them. - Once you've drafted the data, call the `create_excel` tool with: - `title`: workbook file name (no extension) - `sheets`: a list of {"sheet_name": str, "headers": list[str], "rows": list[list]} - Tell the user the file path once it's created.

pptx_builder/SKILL.md的核心逻辑:

--- name: pptx_builder description: Build a PowerPoint (.pptx) deck from a title and a list of slides --- You are now a **presentation specialist**. Turn the user's request into a short, well-structured slide deck. Guidelines: - 4-8 slides unless the user asks for more. - Each slide needs a short title and 2-4 concise bullet points (no walls of text). - The first slide is a title slide (title + optional subtitle, no bullets). - Pick a `theme_color` and `font_name` that fit the topic (e.g. green for eco/sustainability, navy/gray for finance, warm orange for food/hospitality). Don't default to the same colors every time — vary them based on what the deck is about, or honor an explicit request ("make it blue", "use Georgia"). - Once you've drafted the outline, call the `create_pptx` tool with: - `title`: deck title - `slides`: a list of {"heading": str, "bullets": list[str]} - `theme_color`: 6-digit hex (no `#`) used for the title slide background and accent bars - `font_name`: a font available in PowerPoint's defaults, e.g. "Calibri", "Georgia", "Verdana" - Tell the user the file path once it's created.

接下来安装项目需要的依赖,其中python-pptx负责生成PPT文件,openpyxl负责生成Excel文件:

!pip install -q langchain langchain-core langchain-openai langgraph python-pptx openpyxl

注意:python-pptx和openpyxl将分别用于创建PPT和Excel文件。

为了避免密钥泄露,我们可以在运行时动态输入OpenAI密钥,不要把它硬编码到 notebook 里:

import os from getpass import getpass if not os.environ.get("OPENAI_API_KEY"): os.environ["OPENAI_API_KEY"] = getpass("Enter your OpenAI API key: ")
实现技能加载机制

首先我们需要把skills文件夹下的所有SKILL.md文件加载到内存,一开始只给智能体展示每个技能的名称和简介,不需要暴露完整内容:

from pathlib import Path from typing import TypedDict SKILLS_DIR = Path("skills") OUTPUT_DIR = Path("outputs") OUTPUT_DIR.mkdir(exist_ok=True) class Skill(TypedDict): name: str description: str content: str def _load_skills() -> list[Skill]: skills = [] for skill_file in sorted(SKILLS_DIR.glob("*/SKILL.md")): text = skill_file.read_text() _, front_matter, content = text.split("---", 2) name = front_matter.split("name:")[1].split("\n")[0].strip() description = front_matter.split("description:")[1].split("\n")[0].strip() skills.append(Skill(name=name, description=description, content=content.strip())) return skills SKILLS = _load_skills() [(s["name"], s["description"]) for s in SKILLS]

加载完成后,智能体就能看到一份简短的可用技能列表,长这样:

图片说明:List of available software tools and descriptions

接下来给智能体提供一个load_skill工具,它可以根据技能名称拉取对应技能的完整指令:

from langchain.tools import tool @tool def load_skill(skill_name: str) -> str: """Load the full instructions for a specialized skill by name.""" for skill in SKILLS: if skill["name"] == skill_name: return skill["content"] return f"Unknown skill '{skill_name}'. Options: {[s['name'] for s in SKILLS]}"

真正的技能机制通过中间件实现:这个中间件负责向智能体声明所有可用技能,同时把load_skill工具注入到智能体的工具池里:

from typing import Callable from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse from langchain.messages import SystemMessage class SkillMiddleware(AgentMiddleware): """Injects skill descriptions into the system prompt and exposes load_skill.""" tools = [load_skill] def __init__(self): self.skills_prompt = "\n".join( f"- **{skill['name']}**: {skill['description']}" for skill in SKILLS ) def wrap_model_call( self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse], ) -> ModelResponse: skills_addendum = ( f"\n\n## Available Skills\n\n{self.skills_prompt}\n\n" "Call load_skill with the matching name before generating content " "for that kind of request." ) new_content = list(request.system_message.content_blocks) + [ {"type": "text", "text": skills_addendum} ] modified_request = request.override( system_message=SystemMessage(content=new_content) ) return handler(modified_request)
实现技能对应的真实工具

pptx_builder技能最终会调用一个真实的PPT生成工具,这个工具支持自定义主题颜色和字体,避免所有生成的演示文稿风格都千篇一律:

from pptx import Presentation from pptx.dml.color import RGBColor from pptx.util import Emu def _rgb(hex_color: str) -> RGBColor: return RGBColor.from_string(hex_color.lstrip("#")) def _tint(color: RGBColor, amount: float) -> RGBColor: """Lighten an RGBColor toward white by `amount` (0-1).""" blend = lambda c: int(c + (255 - c) * amount) return RGBColor(blend(color[0]), blend(color[1]), blend(color[2])) @tool def create_pptx( title: str, slides: list[dict], theme_color: str = "1F4E79", font_name: str = "Calibri", ) -> str: """Create a styled .pptx deck and save it to outputs.""" accent = _rgb(theme_color) tint = _tint(accent, 0.85) prs = Presentation() title_layout = prs.slide_layouts[0] bullet_layout = prs.slide_layouts[1] def style_text(text_frame, color=None, bold=None): for paragraph in text_frame.paragraphs: for run in paragraph.runs: run.font.name = font_name if color is not None: run.font.color.rgb = color if bold is not None: run.font.bold = bold for i, slide_data in enumerate(slides): heading = slide_data.get("heading", "") bullets = slide_data.get("bullets", []) if i == 0: slide = prs.slides.add_slide(title_layout) slide.background.fill.solid() slide.background.fill.fore_color.rgb = accent slide.shapes.title.text = heading style_text( slide.shapes.title.text_frame, color=RGBColor(0xFF, 0xFF, 0xFF), bold=True, ) if bullets: slide.placeholders[1].text = bullets[0] style_text( slide.placeholders[1].text_frame, color=tint, ) else: slide = prs.slides.add_slide(bullet_layout) slide.background.fill.solid() slide.background.fill.fore_color.rgb = RGBColor( 0xFF, 0xFF, 0xFF ) # Accent bar under the title bar = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, # 1 Emu(0), Emu(0), prs.slide_width, Emu(60000), ) bar.fill.solid() bar.fill.fore_color.rgb = accent bar.line.fill.background() bar.shadow.inherit = False slide.shapes.title.text = heading style_text( slide.shapes.title.text_frame, color=accent, bold=True, ) body = slide.placeholders[1].text_frame body.clear() for j, bullet in enumerate(bullets): p = body.paragraphs[0] if j == 0 else body.add_paragraph() p.text = bullet style_text( body, color=RGBColor(0x33, 0x33, 0x33), ) file_path = OUTPUT_DIR / f"{title.replace(' ', '_')}.pptx" prs.save(file_path) return ( f"Saved deck with {len(slides)} slides " f"({font_name}, #{theme_color}) to {file_path}" )

excel_reporter技能对应的工具支持设置表头,以及每个工作表的的多行数据,满足常规报表的需求:

from openpyxl import Workbook @tool def create_excel(title: str, sheets: list[dict]) -> str: """Create an .xlsx workbook and save it to outputs.""" wb = Workbook() wb.remove(wb.active) for sheet_data in sheets: ws = wb.create_sheet(sheet_data["sheet_name"][:31]) # Excel sheet-name limit ws.append(sheet_data["headers"]) for row in sheet_data["rows"]: ws.append(row) file_path = OUTPUT_DIR / f"{title.replace(' ', '_')}.xlsx" wb.save(file_path) return f"Saved workbook with {len(sheets)} sheet(s) to {file_path}"
组装智能体

现在我们把所有组件拼起来:两个文档生成工具、一行极简的系统提示,剩下的技能加载、指令分发工作都交给SkillMiddleware处理:

from langchain.agents import create_agent agent = create_agent( model="openai:gpt-4o-mini", tools=[create_pptx, create_excel], system_prompt="You are a document-generation assistant.", middleware=[SkillMiddleware()], )

测试智能体的技能调用

我们先给智能体提一个生成PPT的需求:为可复用的咖啡杯创业项目做一份演示文稿。智能体应该会自动匹配到pptx_builder技能,拉取完整指令后起草大纲、选择主题,最终生成符合要求的PPT:

result = agent.invoke( { "messages": [ { "role": "user", "content": "Make a slide pitch deck for a startup that sells eco-friendly reusable coffee cups. Use a green theme and a clean font.", } ] } ) print(result["messages"][-1].content) Done, I created the pitch deck here: `outputs/Eco-Friendly_Reusable_Coffee_Cups_Pitch_Deck.pptx` It uses a green theme and a clean font.

生成的演示文稿效果如下:

图片说明:Presentation slides for a reusable coffee cup startup

接下来我们测试Excel生成能力,让智能体做一份面包店的季度财务业绩表:

result = agent.invoke( { "messages": [ { "role": "user", "content": "Build a spreadsheet tracking Q1-Q4 revenue and expenses for a small bakery", } ] } ) print(result["messages"][-1].content) Done, your spreadsheet is ready: `outputs/bakery_q1_q4_revenue_expenses.xlsx`

生成的报表效果如下:

图片说明:Quarterly financial performance table for a bakery

关于Skills的常见疑问

很多同学会问,这个技能机制有没有什么额外成本?这里整理三个最常见的问题:

  1. 1.只有LangChain能用Skills吗?当然不是,其他框架也提供了类似的实现模式,甚至不依赖任何框架,从零用“工具+提示”的组合也能实现技能机制,本质是一样的。

  2. 2.Skills会增加API调用成本吗?会的,因为load_skill是普通工具调用,智能体每次加载技能都会多一次和模型的往返调用,不过相比把全量指令塞进系统提示的成本,这个开销通常更低。

  3. 3.一次请求可以同时用多个技能吗?可以,如果用户的需求横跨多个专业领域,智能体可以在同一次运行中多次调用load_skill,依次加载需要的技能。

最后要提醒的是:技能不会让你的智能体更“聪明”,但会让它更有条理。通过按需加载详细指令,你完全可以用一个智能体实现几十种专业行为,既不会让系统提示膨胀,也不用为每个任务单独启动子智能体。不妨从第一个简单技能开始尝试,慢慢扩展你的智能体能力边界。


感谢大家的点赞和关注,我们下期见!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 2:00:18

STM32串口空闲中断卡死根因:USART_ITConfig初始化顺序陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:59:58

MTK Android 10侧键改造成相机键:全链路实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:59:57

工业以太网温湿度变送器PCB设计与EMC整改全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:59:16

STM32 HAL库UART中断接收回调不执行?六大原因与排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:57:07

Prompt本质:从命令行到AI的指令演化与任务建模

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 1:56:52

Cadence Allegro 16.6 DRC避坑指南:精准定位高频报错根源

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华